CPU Cgroup

Cgroups 是对指定进程做计算机资源限制的,CPU Cgroup 是 Cgroups 其中的一个 Cgroups 子系统,它是用来限制进程的 CPU 使用的。
对于进程的 CPU 使用,我们知道它只包含两部分:一个是用户态(top命令中的 us 和 ni);还有一部分是内核态(top 命令中的 sy)。
至于其他状态(top 命令中的 wa、hi、si),这些 I/O 或者中断相关的 CPU 使用,CPU Cgroup 不会去做限制。
每个 Cgroups 子系统都是通过一个虚拟文件系统挂载点的方式,挂到一个缺省的目录下,CPU Cgroup 一般在 Linux 发行版里会放在 /sys/fs/cgroup/cpu 这个目录下。

# mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)

每个 cgroup 子系统代表一种资源:

  • system - systemd 维护的自己使用的非 subsystem 的 cgroups 层级结构;
  • cpu - 使用调度程序对 cgroup 内的任务提供 CPU 资源的访问;
  • cpuacct - 生成 cgroup 中所有任务的处理器使用情况报告;
  • memory - 限制 memory 的利用率;
  • freezer- 允许 cgroup 中的一组任务挂起 / 恢复;
  • cpuset - 为 cgroup 内的任务分配独立的处理器和内存节点;
  • net_cls - 允许对 cgroup 中的任务产生的网络数据包进行标记;
  • net_prio - 针对 cgroup 中的每个网络接口提供一种动态修改网络流量优先级的方法;
  • pid - 限制 cgroup 中的进程数量;
  • rdma - 子系统 Remote Direct Memory Access Controller,限制进程对RDMA/IB资源的使用。这些进程分组使用RDMA控制器;
  • hugetlb - 为 cgroup 开启对 大页内存的支持 ;
  • devices - 限制 cgroup 中的一组任务访问设备;
  • perf_event - 支持访问 cgroup 中的 性能事件;
  • blkio - blkio子系统用于限制块设备I/O速率。;

比如说,我们在子系统的最顶层开始建立两个控制组(也就是建立两个目录)group1 和 group2

# cd /sys/fs/cgroup/cpu
# mkdir group1 group2
# cd group2/
# ls
cgroup.clone_children  cpuacct.stat   cpuacct.usage_all     cpuacct.usage_percpu_sys   cpuacct.usage_sys   cpu.cfs_period_us  cpu.rt_period_us   cpu.shares  notify_on_release
cgroup.procs           cpuacct.usage  cpuacct.usage_percpu  cpuacct.usage_percpu_user  cpuacct.usage_user  cpu.cfs_quota_us   cpu.rt_runtime_us  cpu.stat    tasks

考虑到在云平台里呢,大部分程序都不是实时调度的进程,而是普通调度(SCHED_NORMAL)类型进程,那什么是普通调度类型呢?
因为普通调度的算法在 Linux 中目前是 CFS (Completely Fair Scheduler,即完全公平调度器)。为了方便理解,我们就直接来看 CPU Cgroup 和 CFS 相关的参数,一共有三个。

# cat /sys/fs/cgroup/cpu/group2/cpu.cfs_period_us 
100000
# cat /sys/fs/cgroup/cpu/group2/cpu.cfs_quota_us 
-1
# cat /sys/fs/cgroup/cpu/group2/cpu.shares 
1024
  • 第一个参数是 cpu.cfs_period_us,它是 CFS 算法的一个调度周期,一般它的值是 100000,以 microseconds 为单位,也就 100ms。
  • 第二个参数是 cpu.cfs_quota_us,它表示 CFS 算法中,在一个调度周期里这个控制组被允许的运行时间,比如这个值为 50000 时,就是 50ms。如果用这个值去除以调度周期(也就是 cpu.cfs_period_us),50ms/100ms = 0.5,这样这个控制组被允许使用的 CPU 最大配额就是 0.5 个 CPU。从这里能够看出,cpu.cfs_quota_us 是一个绝对值。如果这个值是 200000,也就是 200ms,那么它除以 period,也就是 200ms/100ms=2。
  • 第三个参数, cpu.shares。这个值是 CPU Cgroup 对于控制组之间的 CPU 分配比例,它的缺省值是 1024。

在Kubernetes中

Kubernetes 会为每个容器都在 CPU Cgroup 的子系统中建立一个控制组,然后把容器中进程写入到这个控制组里。

cpu.cfs_quota_us、cpu.cfs_period_us

Kubernetes 是通过 CPU cgroup 控制模块中的 cpu.cfs_period_us,cpu.cfs_quota_us 两个配置来实现的。kubernetes 会为这个 container cgroup 配置两条信息:

cpu.cfs_period_us = 100000 (i.e. 100ms)
cpu.cfs_quota_us = quota = (cpu in millicores * 100000) / 1000

容器 CPU 的上限由 cpu.cfs_quota_us 除以 cpu.cfs_period_us 得出的值来决定的。而且,在操作系统里,cpu.cfs_period_us 的值一般是个固定值。
在 cgroup 的 CPU 子系统中,可以通过这两个配置,严格控制这个 cgroup 中的进程对 CPU 的使用量,保证使用的 CPU 资源不会超过 cfs_quota_us/cfs_period_us,也正好就是申请的 limit 值。
对于 cpu 来说,如果没有指定 limit 的话,那么 cfs_quota_us 将会被设置为 -1,即没有限制。

cpu.shares

在 CPU Cgroup 中 cpu.shares == 1024 表示 1 个 CPU 的比例,那么 Request CPU 的值就是 n,给 cpu.shares 的赋值对应就是 n*1024。
CPU request 是通过 cgroup 中 CPU 子系统中的 cpu.shares 配置来实现的。当你指定了某个容器的 CPU request 值为 x millicores 时,kubernetes 会为这个 container 所在的 cgroup 的 cpu.shares 的值指定为 x * 1024 / 1000。即:

cpu.shares = (cpu in millicores * 1024) / 1000

举个例子,当你的 container 的 CPU request 的值为 1 时,它相当于 1000 millicores,所以此时这个 container 所在的 cgroup 组的 cpu.shares 的值为 1024。
这样做希望达到的终效果就是:即便在极端情况下,即所有在这个物理机上面的 pod 都是 CPU 繁忙型的作业的时候(分配多少 CPU 就会使用多少 CPU),仍旧能够保证这个 container 的能够被分配到 1 个核的 CPU 计算量。其实就是保证这个 container 的对 CPU 资源的低需求。即"Request CPU"就是无论其他容器申请多少 CPU 资源,即使运行时整个节点的 CPU 都被占满的情况下,我的这个容器还是可以保证获得需要的 CPU 数目。

在docker中

在docker中可以使用以下参数:

参数 作用
--cpuset-cpus 限制一个容器可以使用的特定CPU或内核。如果你有一个以上的CPU,一个以逗号分隔的列表或以连字符分隔的容器可以使用的CPU范围。第一个CPU的编号是0。一个有效的值可能是0-3(使用第一、第二、第三和第四个CPU)或1,3(使用第二和第四个CPU)。
--cpu-shares 将这个标志设置为一个大于或小于默认值1024的值,以增加或减少容器的重量,并使它能够获得更大或更小比例的主机CPU周期。这只有在CPU周期受到限制时才会强制执行。当有大量的CPU周期可用时,所有的容器都会根据自己的需要使用尽可能多的CPU。这样一来,这是一个软限制。
--cpu-period 指定CPU CFS调度器的周期,与--cpu-quota一起使用。默认为100000微秒(100毫秒)。大多数用户不改变这个默认值。对于大多数使用情况,--cpu是一个更方便的选择。
--cpu-quota 对容器施加一个CPU CFS配额。容器在节流前每--cpu-period被限制的微秒数。因此,作为有效的上限。对于大多数使用情况,--cpu是一个更方便的选择。
--cpus 指定一个容器可以使用多少可用的CPU资源。例如,如果主机有两个CPU,而你设置了–cpus=“1.5”,那么容器最多保证使用一个半的CPU。这相当于设置--cpu-period="100000"--cpu-quota="150000"。docker 1.13支持支持,替换cpu-period和cpu-quota

示例:
如果你有1个CPU,以下每个命令都能保证容器每秒最多使用50%的CPU。

docker run -it --cpus=".5" ubuntu /bin/bash

这相当于手动指定–cpu-period和–cpu-quota;

docker run -it --cpu-period=100000 --cpu-quota=50000 ubuntu /bin/bash

总结

  1. 每个进程的 CPU Usage 只包含用户态(us 或 ni)和内核态(sy)两部分,其他的系统 CPU 开销并不包含在进程的 CPU 使用中,而 CPU Cgroup 只是对进程的 CPU 使用做了限制。
  2. CPU Cgroup 中的主要参数,包括这三个:cpu.cfs_quota_us,cpu.cfs_period_us 还有 cpu.shares。
    1. cpu.cfs_quota_us 和 cpu.cfs_period_us 这两个值决定了每个控制组中所有进程的可使用 CPU 资源的最大值。cpu.cfs_quota_us(一个调度周期里这个控制组被允许的运行时间)除以 cpu.cfs_period_us(用于设置调度周期)得到的这个值决定了 CPU Cgroup 每个控制组中 CPU 使用的上限值。
    2. cpu.shares 参数,正是这个值决定了 CPU Cgroup 子系统下控制组可用 CPU 的相对比例,当系统上 CPU 完全被占满的时候,这个比例才会在各个控制组间起效。