Skip to content

使用Alibaba Draognwell多租户特性管控运行时资源

Jonathan Lu edited this page Apr 28, 2020 · 4 revisions

本文将介绍如何修改应用、中间件等Java代码和配置来使用Alibaba Dragonwell多租户特性。 针对版本: Alibaba Dragonwell 8.3.3 及更高

1 多租户特性简介

在有些场景中,Java应用并非作为单一业务用途的程序存在的,而是作为一个平台存在,在他上面运行着一些更加轻量的应用或函数。 在这类Java应用上面,一般通过动态类加载机制把和应用无关、但遵循一定编程接口规范、和Java字节码技术兼容的软件模块(可以是从Java、Scala、Kotlin等语言编译而来)加载到应用进程,并运行其中的业务逻辑,一个典型的例子是Tomcat的动态加载机制。

这类架构有一个缺点:就是平台型Java应用缺乏对其上轻量应用所使用资源的管控能力。容易出现某个应用占用过多资源——比如CPU时间——而导致其他同进程应用无法响应。 JDK多租户技术的目的就是为平台型Java应用提供一个细粒度资源管控的能力。

1.1 适用场景

多租户技术的目的是为基于JVM技术栈的PaaS、SaaS、FaaS应用平台提供的底层资源管控能力; 并不是为了普通单目的业务应用设计的,也不会让此类Java应用受益。

1.2 实现原理

Alibaba Dragonwell多租户技术通过在JDK中创建虚拟的进程内容器“租户”,来让JVM可以识别出运行时代码所持有的资源组。

2 基本用法

使用多租户特性只需要下面三步:

  • 初始化多租户能力 多租户CPU管控的能力在Linux上面是依赖于cgroup(Linux control groups)这个特性来实现的,所以需要root权限做一些初始化工作。

主要目的是为了特定用户创建一个顶级的有读写权限的cgroup目录,这样后续启动租户就不再需要root权限。

如果不使用基于线程(未来会开源基于协程的管控能力)的CPU管控能力,可以忽略这一步。

这个目录层级是面向单个用户的运行时环境,目前不支持为同一个容器中多个用户同时初始化。

  • 修改JVM参数添加多租户相关选项; Alibaba Dragonwell JDK的多租户特性以及每个子特性,都可以通过JVM选项的方式打开或关闭,这样应用可以方便的根据实际需求要求JVM只管控部分资源或提供部分隔离支持。
  • 修改使用应用、中间件等组件的Java代码,把相关代码逻辑改到租户中运行。 Alibaba Dragonwell多租户特性面向上面第一部分介绍的使用场景,所以依靠平台型应用通过调用多租户的Java API来实现把资源管控能力嵌入到平台型应用之中。

2.1 初始化多租户CPU管控

这一步需要依赖于libcgroup-tools这个包,请在您所使用的发行版上安装对应软件包。 这一步可以写到Dockerfile中用来简化流程,但是需要在执行的时候指定docker run --privileged参数。 如果不需要CPU资源支持(-XX:+TenantCpuThrottling)可以跳过这一步 本步骤需要安装libcgroup-tools这个rpm包 一般情况下使用下面命令可以初始化cgroup sudo <jdk_home>/bin/jgroup -g <group> -u <user>

  • <user>,<group>是执行多租户Java进程的Linux用户对应的Linux用户名和组。
  • 一般需要sudo权限了来操作cgroup 如果下面命令能正常执行就证明初始化成功了 <jdk_home>/bin/java -XX:+MultiTenant -XX:+TenantCpuThrottling -version 如果无法执行成功,可以用下面命令调试下看看哪一步出错
export JGROUP_DEBUG=true
<jdk_home>/bin/java -XX:+MultiTenant -XX:+TenantCpuThrottling -version

不同集群cgroup配置五花八门,配置错误的也不在少数,请先参考文档保证cgroup配置正确 https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/resource_management_guide/ch01

2.2 修改JVM参数添加多租户支持

Dragonwell 8.3.3中添加了一些新的JVM参数用来精细的控制多租户特性的行为,应用可以根据需要打开、关闭部分的资源控制特性。

JVM选项 说明 必填?
-XX:+MultiTenant 打开多租户功能,必须添加此参数才能使用任何多租户功能
-XX:+TenantHeapThrottling 启用租户内存资源隔离,必须和-XX:+UseG1GC配合使用,不能和CMS、PS等GC策略混合使用
-XX:+TenantCpuThrottling 启用租户CPU资源控制,基于Linux cgroup实现,RHEL兼容的发行版上需要安装libcgroup、libcgroup-tools RPM包才可以工作,其他发行版请安装对应的软件包。
-XX:+TenantDataIsolation 启动系统静态变量隔离功能,JDK里面效果上静态的变量(static变量,threadlocal变量)将会被按照租户隔离,不同租户看到的是不同静态变量的副本,应用代码的静态变量不受影响; JDK的properties也会被按租户隔离,租户里调用System.setProperty不会影响其他租户

2.3 将受控代码移入租户

平台型Java应用需要使用多租户API,通过编程的方式配置受控代码可以使用的资源上限,并把受控代码包装起来执行,这样受控代码才消耗的资源才可以被管控起来。 Dragonwell 8.3.3提供了一套新的API位于com.alibaba.tenantcom.alibaba.rcm包内,用来把代码放到租户空间执行,后面会详细介绍。 下面是一个简单的HelloWorld例子

Iterable<Constraint> constraints = Stream.of(ResourceType.CPU_PERCENT.newConstraint(40),
                                             ResourceType.HEAP_RETRAINED.newConstraint(64 * 1024 * 1024))
                                         .collect(Collectors.toList());
ResourceContainer tenant = TenantContainerFactory.instance().createContainer(constraints);
Runnable task = ()->{
    System.out.println("Hello from tenant container!");
}
tenant.run(task);

上面的task是一段Java逻辑,可以看作受控的轻量级应用代码,tenant.run(task)调用将会在当前线程执行这段代码,并且控制它消耗的CPU资源不超过单核心的40%,最大内存资源不超过64MB。

上面三步展示了让平台型应用使用多租户能力的基本步骤,下面部分将详细介绍多租户资源管控框架。

3 多租户资源管控框架

Alibaba Dragonwell提供了一套抽象的管控受控代码执行的框架,我们称之为RCM框架(Resource Control Management),他为平台型应用提供了统一的抽象接口,多租户功能(MultiTenant)是针对RCM框架的一个具体实现。

3.1 基本概念

  • TenantContainer 即多租户特性提供的“租户”概念,用于将“一段执行的代码”所消耗的资源进行控制,这个控制不光包括了CPU、内存等资源,也包括了静态变量隔离、线程管控等更高级的特性。

未来会根据反馈开源更多经过验证的高级特性

  • ResourceContainer ResourceContainerTenantContainer的子集,用于提供抽象的接口来 只 做好“资源管理”的工作,用于管理一段执行的代码所消耗的CPU、内存、IO等资源。 ResourceContainer在多租户场景下,和TenantContainer1:1的对应关系。

由于有ResourceContainer这层抽象的存在,使得其他非多租户场景的资源管理也成为可能,比如协程场景。

  • ThreadTenantContainer TenantContainer是一个虚拟的“容器”的概念,并不会在内部隐式创建线程池,所有被管控的代码仍旧在自己本来的线程上执行,TenantContainer.run()方法只相当于在受控代码开始和结束的时候给所在线程打上/取消了一个特殊的标记,JVM的OS可以利用这个标记进行资源计费、管控工作。

租户场景下只支持线程级别的管控。

3.2 创建TenantContainer对象

直接创建

代码可以能通过静态方法TenantContainer.create()来创建租户对象,示例代码如下

TenantContainer tenant = TenantContainer.create(new TenantConfiguration()
                              .limitMemory(64 * 1024 * 1024)
                              .limitCpuShares(1024));

TenantConfiguration是用来记录租户所适用资源限制的配置对象。

从对应ResourceContainer对象获取

由于TenantContainerResourceContainer1:1的关系,可以从一个已有的ResourceContainer对象获取租户对象

ResourceContainer rc = TenantContainerFactory.createContainer(constraints);
TenantContainer tc = TenantContainerFactory.tenantContainerOf(rc);

3.3 创建ResourceContainer对象

直接创建

注意:每次新建一个ResourceContainer对象,会有一个TenantContainer对象被隐式的创建出来。

List<Constraint> cs = new ArrayList<>();
cs.add(ResourceType.CPU_PERCENT.newConstraint(50));  // 50% single-core CPU resource
cs.add(ResourceType.HEAP_RETAINED.newConstraint(32 * 1024 * 1024));  // 32MB retained heap
ResourceContainer rc = TenantContainerFactory.createContainer(cs);

从对应TenantContainer对象获取

代码如下

TenantContainer tc = TenantContainer.create(new TenantConfiguration().limitMemory(64 * 1024 * 1024));
ResourceContainer rc = tc.getResourceContainer();

3.4 将代码移入租户空间执行

多租户JVM启动之后并不会自动创建任何租户,如果没有显示的调用TenantContainer.run,所有的Java代码运行于默认的非租户空间,我们称之为“ROOT租户”,这些代码是不受多租户资源管控的。 ROOT租户和一般租户的分界线就在TenantContainer.run(Runnable task)中task参数的run方法,如下面代码所示:

/*租户外*/
tenant.run (
    ()-> {
         /*租户内*/
    }
}
/*租户外*/

已经运行起来的ROOT租户的线程attach进一般租户

已经运行起来的线程,只能通过TenantContainer.run()把应用的代码逻辑放到租户空间执行,这个方法实现上是修改了当前线程对象的一些状态(在JVM中,java层不可见),使得在TenantContainer.run()这个方法执行期间所有资源消耗挂靠在该租户上。

Thread t = new Thread(
    ()->{
        tenant.run(
            ()-> {
                /*这个作用域的代码将被Root租户创建的线程t执行,并且运行在tenant租户里,受tenant租户的资源管控*/
            });
    });
t.start();

目前禁止嵌套执行TenantContainer.run()方法,不能从租户A的run方法里面调用租户B的run方法。

完全在租户内执行的线程

如下面代码所示的线程创建出来后就会一直租户空间执行,一般Java线程的入口Thread.run()会被多租户功能修改掉。

tenant.run(
    ()-> {
         Thread threadInTenant = new Thread(task);
         threadInTenant.start(); /* threadInTenant这个线程会一启动就在租户tenant里运行 */
    });

从租户内临时切换到ROOT租户执行一段代码

从租户内临时切换到ROOT租户执行一段代码

tenant.run(()-> {
    /*租户内*/
    TenantContainer.primitiveRunInRoot(()->{
        /*这里面的代码会临时切换到ROOT租户执行,所消耗资源不会算在当前租户*/
    });
    /*租户内*/
   long data = TenantContainer.primitiveRunInRoot(()->{
        /*这里面的代码会临时切换到ROOT租户执行,所消耗资源不会算在当前租户*/
        return 0;
   });
   /*租户内*/
});

4 性能和架构

4.1 性能

多租户特性为平台型应用提供了资源管控的能力,但并非没有代价。

  • 由于资源管控会指定资源上限,如果被管控模块使用的资源超过了给定上限,将会通过限制手段控制其可用资源,会导致诸如分配到的CPU时间减少、发生GC等现象,应用层可能也会表现出无法响应等现象。

这些负面现象是受控应用应有的惩罚,被认为是正常现象,如果您的应用上面嵌入的目标应用不能接受这些控制,请三思。

  • JVM执行资源管控也是需要占用较少资源

内存管理部分是通过修改GC实现的,基本可以认为不占用运行时资源; CPU部分是通过cgroup实现的,单线程单次切换租户会有十几微秒的开销,多线程竞争切换会更大些。

4.2 架构

经过实践检验,一般推荐如下架构会更好的利用这个特性

租户绑定到线程池

线程池的线程一创建就在租户中运行,通过提交任务把相关业务的任务使用的资源进行管控。

长请求线程动态绑定到租户

线程处理单次请求时间较长,十秒以上,动态绑定到租户上可以方便管控资源消耗。

当然,如果您有更多奇思妙想,也可以到Alibaba Dragonwell钉钉群和我们一起探讨,共同探索更多玩法

5 支持和答疑

如果您在使用过程中有发现Alibaba Dragonwell的问题,欢迎给我们提Issue,也欢迎使用钉钉群一起更轻松的探讨相关技术问题。

Clone this wiki locally