1. 背景

NodeManager中,将机器的CPU资源抽象为vcore,将内存资源抽象为memory。例如机器为102核、256GB内存,希望赋予NodeManager196GB内存和72核CPU,用于给container分配资源。其配置如下:

   <property>
        <name>yarn.nodemanager.resource.memory-mb</name>
        <value>200704</value>
    </property>
    <property>
        <name>yarn.nodemanager.resource.cpu-vcores</name>
        <value>72</value>
    </property>

每次申请contaienr时,都会指定请求的vcore数量和memory数量。当单台nodemanager分配的总内存内存超过yarn.nodemanager.resource.memory-mb 或者分配的vcore超过yarn.nodemanager.resource.cpu-vcores ,ResourceManager就不会往这台机器上调度作业了。

上述逻辑中,对于内存,NodeManager能够快速发现container启动的进程是否超过请求的内存量,如果超过container申请的内存,这一般代表内存的占用会越来越多,因此会立即kill container,关掉任务。

但是对于CPU资源,NodeManager默认情况下无法限制住。这是因为虽然指定了container的核数,当进程启动了多个重CPU的线程,可能抢占了大部分的CPU。NodeManager服务也不能因为作业占用了较多的CPU就把作业kill掉,一旦作业快速执行完,那么kill掉重新跑的成本非常高。

因此,对于CPU的资源,希望以控制为主。当作业使用的CPU资源超过container申请后,能够立刻控制CPU不给其继续分配资源,保证其他容器也能正常执行任务。

目前,在生产环境中,多种情况都需要CPU资源隔离技术:

  1. Presto On Yarn时,如果该NodeManager机器运行了其他重CPU任务,会导致CPU过高产生presto的慢查询现象。
  2. Flink ON Yarn时,对于基于kafka的清洗任务,也是重CPU的情况,导致CPU占用率过高,这会导致该nodemanager上的其他flink作业无法获取CPU资源导致消费延迟,任务堆积。flink非常敏感。
  3. Spark ML作业也会导致单个container的CPU使用率过高。
  4. CPU使用率过高可能导致机器死机。

目前,对于Yarn来说,它支持通过cgroup对启动的container CPU进行限制。

2. Cgroup简要介绍

Cgroup全称control groups,是linux内核用来限制、控制CPU、内存、磁盘等进程资源的一个功能。它不提供任何的借口给用户,而是通过vfs虚拟文件系统的方式把功能暴露给用户态。

通过mount命令使用cpu限制模块挂载cgroup到linux机器中:

mount -t cgroup -o cpu cgroup  /sys/fs/cgroup/cpu
#其他资源限制也可以进行挂载
mount -t cgroup -o memory memory  /sys/fs/cgroup/memory
mount -t cgroup -o cpuacct cpuacct  /sys/fs/cgroup/cpuacct
mount -t cgroup -o cpuset cpuset  /sys/fs/cgroup/cpuset

挂载好后,出现了两个新挂载点:

Untitled.png

进入/sys/fs/cgroup/cpu的作业目录中,发现有以下文件。其中,以下几个文件非常重要:

  1. cgroup.procs:保存进程PID,表示要限制该进程的CPU资源。
  2. cpu.shares:设置CPU的相对值,假如进程A的shares时1024,进程B的shares是512,那么A得到66%的CPU资源,B得到33的CPU资源。
  3. cpu.cfs_period_us:调度的时间周期长度,一半是100000us(100ms),这个值一半不修改。
  4. cpu.cfs_quota_us:一个时间周期内,运行该进程执行的时间。

如下所示:

Untitled 1.png

分配CPU相对时间:如果只设置cpu.shares,不设置cpu.cfs_quota_us,那么当机器空闲时,进程可以占满CPU,如下设置cpu.shares为256:

Untitled 2.png

新增一个继承,其cpu.shares也为256:

Untitled 3.png

分配CPU绝对时间:只要设置了cpu.cfs_quota_us大于0,那么进程最大就只能使用cpu.cfs_quota_us / cpu.cfs_period_us个vcore。如下,设置cpu.cfs_quota_us=600000:

Untitled 4.png

cpu.cfs_quota_us / cpu.cfs_period_us = 600000 / 1000000 = 0.6cpu。可以看到,container最多使用60%的CPU,符合预期:

Untitled 5.png

4. NodeManager配置Cgroup

https://blog.51cto.com/u_15327484/7815432文章中曾经介绍过,NodeManager启动容器是在ContainerExecutor的实现类中执行的,默认时DefaultContainerExecutor启动shell脚本。但是,如果要支持Cgroup,就必须使用LinuxContainerExecutor。其配置如下:

<property>
    <name>yarn.nodemanager.linux-container-executor.resources-handler.class</name>
    <value>org.apache.hadoop.yarn.server.nodemanager.util.CgroupsLCEResourcesHandler</value>
  </property>
  <property>
    #yarn的container在基础目录下的子目录
    <name>yarn.nodemanager.linux-container-executor.cgroups.hierarchy</name>
    <value>/hadoop-yarn</value>
  </property>
  <property>
    #是否需要自动挂载cgroup
    <name>yarn.nodemanager.linux-container-executor.cgroups.mount</name>
    <value>true</value>
  </property>
  <property>
    #基础挂载目录
    <name>yarn.nodemanager.linux-container-executor.cgroups.mount-path</name>
    <value>/sys/fs/cgroup</value>
  </property>
  <property>
    #ContainerExcutor执行容器时使用的组
    <name>yarn.nodemanager.linux-container-executor.group</name>
    <value>hadoop</value>
  </property>
  <property>
    #限制只使用nodemanager CPU的百分比
    <name>yarn.nodemanager.resource.percentage-physical-cpu-limit</name>
    <value>90</value>
  </property>
  <property>
    #是否开启严格模式
    <name>yarn.nodemanager.linux-container-executor.cgroups.strict-resource-usage</name>
    <value>false</value>
  </property>
  <property>
    #是否将逻辑核数看作总核数,false时指定物理核数为总核数
    <name>yarn.nodemanager.resource.count-logical-processors-as-cores</name>
    <value>true</value>
  </property>

4.1 Yarn Cgroup代码计算逻辑

其生效逻辑如下所示。使用 yarn.nodemanager.resource.percentage-physical-cpu-limit 来设置所有 containers 的总的 CPU 使用率占用总的 CPU 资源的百分比。比如设置为 60,则所有的 containers 的 CPU 使用总和在任何情况下都不会超过机器总体 CPU 资源的 60 %:

public class NodeManagerHardwareUtils {
  public static int getNodeCpuPercentage(Configuration conf) {
    int nodeCpuPercentage =
        Math.min(conf.getInt(
          YarnConfiguration.NM_RESOURCE_PERCENTAGE_PHYSICAL_CPU_LIMIT,
          YarnConfiguration.DEFAULT_NM_RESOURCE_PERCENTAGE_PHYSICAL_CPU_LIMIT),
          100);
    nodeCpuPercentage = Math.max(0, nodeCpuPercentage);

    if (nodeCpuPercentage == 0) {
      String message =
          "Illegal value for "
              + YarnConfiguration.NM_RESOURCE_PERCENTAGE_PHYSICAL_CPU_LIMIT
              + ". Value cannot be less than or equal to 0.";
      throw new IllegalArgumentException(message);
    }
    return nodeCpuPercentage;
  }
}

第二步,设置cgroup文件:

  1. 在/sys/fs/cgroup/cpu路径下创建hadoop-yarn/container_xxx目录。
  2. 设置cpu.shares:每个container进程的大小是CPU_DEFAULT_WEIGHT * containerVCores,即1024 * containerVCores。将结果写入到cpu.shares文件中。
  3. 如果设置了严格模式,那么设置quotaUS = MAX_QUOTA_US,periodUS = MAX_QUOTA_US / yarnProcessors。quotaUS就是cpu.cfs_quota_us,periodUS就是cpu.cfs_period_us。那么最终的核数限制就是quotaUS/periodUS = yarnProcessors。而yarnProcessors=(containerVCores * yarnProcessors) / (float) nodeVCores = 总核数 * 限制cpu使用率 * (container vcores) / (nodemanager总vcores)。
  4. 如果没有设置严格模式,直接结束。
public void setupLimits(ContainerId containerId,
                           Resource containerResource) throws IOException {
    String containerName = containerId.toString();
    //默认为true
    if (isCpuWeightEnabled()) {
      //获取container申请的vcore
      int containerVCores = containerResource.getVirtualCores();
      //在/sys/fs/cgroup/cpu路径下创建hadoop-yarn/container_xxx子目录及其资源控制文件
      createCgroup(CONTROLLER_CPU, containerName);
      //cpuShares 默认 等于 1024 * 申请的vcores,这样,其cpu时间占比也按照container申请的核数呈等比例。
      int cpuShares = CPU_DEFAULT_WEIGHT * containerVCores;
      // cpuShares最小值为10
      cpuShares = Math.max(cpuShares, 10);
      //更新/sys/fs/cgroup/cpu/hadoop-yarn/container_xxx/cpu.shares文件
      updateCgroup(CONTROLLER_CPU, containerName, "shares", String.valueOf(cpuShares));
      if (strictResourceUsageMode) {
        //获取节点上总的vcores数量
        int nodeVCores = conf.getInt(YarnConfiguration.NM_VCORES, YarnConfiguration.DEFAULT_NM_VCORES);
        //不能将节点上的所有核分配给
        if (nodeVCores != containerVCores) {
          //  Yarn processor计算方式在下面
          //  根据配置,计算当前container能够获得的CPU核数,其中,yarnProcessors是当前节点CPU逻辑核数 * 总体使用率
          float containerCPU = (containerVCores * yarnProcessors) / (float) nodeVCores;
          //  根据计算得到的CPU核数,设置合理的CPU_PERIOD_US和CPU_QUOTA_US的值
          int[] limits = getOverallLimits(containerCPU);
          //更新/sys/fs/cgroup/cpu/hadoop-yarn/container_xxx/cpu.CPU_PERIOD_US文件
          updateCgroup(CONTROLLER_CPU, containerName, CPU_PERIOD_US, String.valueOf(limits[0]));
          //更新/sys/fs/cgroup/cpu/hadoop-yarn/container_xxx/cpu.CPU_QUOTA_US文件
          updateCgroup(CONTROLLER_CPU, containerName, CPU_QUOTA_US,  String.valueOf(limits[1]));
        }
      }
    }
  }

int[] getOverallLimits(float yarnProcessors) {

    int[] ret = new int[2];
    //省略
    int quotaUS = MAX_QUOTA_US;
    int periodUS = (int) (MAX_QUOTA_US / yarnProcessors);
    //省略
    ret[0] = periodUS;
    ret[1] = quotaUS;
    return ret;
  }

4.2 非严格模式和严格模式对比实践

非严格模式下,使用各container的cpu.shares比值规范每个容器使用CPU的比例。shares=1024 * container vcores。即每个container在非严格模式下,它们的CPU资源占比为container的vcores占比。

严格模式下,每个container使用的CPU资源=总核数 * 限制cpu使用率 * (container vcores) / (nodemanager总vcores)。可以发现,它们的CPU资源占比依然为container的vcores占比。因此,严格模式是在非严格模式的一种扩展。

nodemanager环境

  • debian 8, 内核 4.9。必须要debian8以上的操作系统。建议升级至debian10。
  • 0.9的CPU限制使用率,总共60vcores,每个container申请1个vcore。

非严格模式

默认cfs_quota_us为-1,即不使用固定的CPU时间分配:

Untitled 6.png

它cpu.shares等比例占有cpu,没有开启严格模式时,cpu的值都是1024*1

Untitled 7.png

可以看到,每个container都是用了较多的资源,它们的资源占比相同,符合预期:

Untitled 8.png

严格模式

最终的cfs_quota_us结果为600000:

Untitled 9.png

经过计算cpu.cfs_quota_us / cpu.cfs_period_us = 600000 / 1000000 = 0.6cpu。

同时,通过yarn的计算公式,container的核数 = 40(总核数) * 0.9 ( 物理cpu使用率) * 1 (个vcores ) / 60 ( 个总vcores )=0.6cpu。

两者结果相同,最终可以看到container最大使用为0.6,符合上述计算:

Untitled 10.png

5. Cgroup CPU资源隔离效果review

通过设置nodemanager最大25%、10%、5%的使用限额,发现最高就使用了25%、10%、5%,因此资源隔离生效。

Untitled 11.png

6. 问题及相关解决方法

6.1 启动nodemanager失败报错

`Caused by: java.io.IOException: Not able to enforce cpu weights; cannot write to cgroup at: /sys/fs/cgroup/cpu`

挂载异常,可以重新进行自动挂载:umount /sys/fs/cgroup/cpu取消挂载,然后让yarn组件自动挂载。

6.2 flink出现性能降级

部署cgroup时,线上的jdk是8u191之前的版本,它对cgroup的支持不好,无法正确识别限制的cpu核数。如下,设置container运行48核,但是flink task只识别出2个核。导致作业性能下降80%:

Untitled 12.png

如下,核数识别失败:

Untitled 13.png

在jdk191以上版本中,支持开启-XX:+UseContainerSupport,提示jvm这是容器环境:

Untitled 14.png

可以看到,升级了jdk版本的flink作业中,能够正确识别CPU限制:

Untitled 15.png