现在很多公司的服务都是跑在容器下,我来问几个容器 CPU 相关的问题,看大家对天天在用的技术是否熟悉。
- 容器中的核是真的逻辑核吗?
- Linux 是如何对容器下的进程进行 CPU 限制的,底层是如何工作的?
- 容器中的 throttle 是什么意思?
- 为什么关注容器 CPU 性能的时候,除了关注使用率,还要关注 throttle 的次数和时间?
和真正使用物理机不同,Linux 容器中所谓的核并不是真正的 CPU 核。所以在理解容器 CPU 性能的时候,必然要有一些特殊的地方需要考虑。
各家公司的容器云上,底层不管使用的是 docker 引擎,还是 containerd 引擎,都是依赖 Linux 的 cgroup 的 cpu 子系统来工作的,所以今天我们就来深入地学习一下 cgroup cpu 子系统 。理解了这个,你将会对容器进程的 CPU 性能有更深入的把握。
一、cgroup 的 cpu 子系统
在 Linux 下, cgroup 提供了对 CPU、内存等资源实现精细化控制的能力。它的全称是 control groups。允许对某一个进程,或者一组进程所用到的资源进行控制。现在流行的 Docker 就是在这个底层机制上成长起来的。
在你的机器执行执行下面的命令可以查看当前 cgroup 都支持对哪些资源进行控制。
$ lssubsys -a
cpuset
cpu,cpuacct
...
其中 cpu 和 cpuset 都是对 CPU 资源进行控制的子系统。cpu 是通过执行时间来控制进程对 cpu 的使用,cpuset 是通过分配逻辑核的方式来分配 cpu。其它可控制的资源还包括 memory(内存)、net_cls(网络带宽)等等。
cgroup 提供了一个原生接口并通过 cgroupfs 提供控制。类似于 procfs 和 sysfs,是一种虚拟文件系统。默认情况下 cgroupfs 挂载在 /sys/fs/cgroup 目录下,我们可以通过修改 /sys/fs/cgroup 下的文件和文件内容来控制进程对资源的使用。
比如,想实现让某个进程只使用两个核,我们可以通过 cgroupfs 接口这样来实现,如下:
# cd /sys/fs/cgroup/cpu,cpuacct
# mkdir test
# cd test
# echo 100000 > cpu.cfs_period_us // 100ms
# echo 100000 > cpu.cfs_quota_us //200ms
# echo {$pid} > cgroup.procs
其中 cfs_period_us 用来配置时间周期长度,cfs_quota_us 用来配置当前 cgroup 在设置的周期长度内所能使用的 CPU 时间。这两个文件配合起来就可以设置 CPU 的使用上限。
上面的配置就是设置改 cgroup 下的进程每 100 ms 内只能使用 200 ms 的 CPU 周期,也就是说限制使用最多两个“核”。
要注意的是这种方式只限制的是 CPU 使用时间,具体调度的时候是可能会调度到任意 CPU 上执行的。如果想限制进程使用的 CPU 核,可以使用 cpuset 子系统,详情参见一次限制进程的 CPU 用量的实操过程
docker 默认情况下使用的就是 cgroupfs 接口,可以通过如下的命令来确认。
# docker info | grep cgroup
Cgroup Driver: cgroupfs
二、内核中进程和 cgroup 的关系
在上一节中,我们在 /sys/fs/cgroup/cpu,cpuacct 创建了一个目录 test,这其实是创建了一个 cgroup 对象。当我们把某个进程的 pid 添加到 cgroup 后,又是建立了进程结构体和 cgroup 之间的关系。
所以要想理解清 cgroup 的工作过程,就得先来了解一下 cgroup 和 task_struct 结构体之间的关系。
2.1 cgroup 内核对象
一个 cgroup 对象中可以指定对 cpu、cpuset、memory 等一种或多种资源的限制。我们先来找到 cgroup 的定义。
//file:include/linux/cgroup-defs.h
struct cgroup {
...
struct cgroup_subsys_state __rcu *subsys[CGROUP_SUBSYS_COUNT];
...
}
每个 cgroup 都有一个 cgroup_subsys_state 类型的数组 subsys,其中的每一个元素代表的是一种资源控制,如 cpu、cpuset、memory 等等。
这里要注意的是,其实 cgroup_subsys_state 并不是真实的资源控制统计信息结构,对于 CPU 子系统真正的资源控制结构是 task_group。它是 cgroup_subsys_state 结构的扩展,类似父类和子类的概念。
当 task_group 需要被当成 cgroup_subsys_state 类型使用的时候,只需要强制类型转换就可以。
对于内存子系统控制统计信息结构是 mem_cgroup,其它子系统也类似。
之所以要这么设计,目的是各个 cgroup 子系统都统一对外暴露 cgroup_subsys_state,其余部分不对外暴露,在自己的子系统内部维护和使用。
2.2 进程和 cgroup 子系统
一个 Linux 进程既可以对它的 cpu 使用进行限制,也可以对它的内存进行限制。所以,一个进程 task_struct 是可以和多种子系统有关联关系的。
和 cgroup 和多个子系统关联定义类似,task_struct 中也定义了一个 cgroup_subsys_state 类型的数组 subsys,来表达这种一对多的关系。
我们来简单看下源码的定义。
//file:include/linux/sched.h
struct task_struct {
...
struct css_set __rcu *cgroups;
...
}
//file:include/linux/cgroup-defs.h
struct css_set {
...
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
}
其中subsys是一个指针数组,存储一组指向 cgroup_subsys_state 的指针。一个 cgroup_subsys_state 就是进程与一个特定的子系统相关的信息。
通过这个指针,进程就可以获得相关联的 cgroups 控制信息了。能查到限制该进程对资源使用的 task_group、cpuset、mem_group 等子系统对象。
2.3 内核对象关系图汇总
我们把上面的内核对象关系图汇总起来看一下。
可以看到无论是进程、还是 cgroup 对象,最后都能找到和其关联的具体的 cpu、内存等资源控制自系统的对象。
2.4 cpu 子系统
因为今天我们重点是介绍进程的 cpu 限制,所以我们把 cpu 子系统相关的对象 task_group 专门拿出来理解理解。
//file:kernel/sched/sched.h
struct task_group {
struct cgroup_subsys_state css;
...
// task_group 树结构
struct task_group *parent;
struct list_head siblings;
struct list_head children;
//task_group 持有的 N 个调度实体(N = CPU 核数)
struct sched_entity **se;
//task_group 自己的 N 个公平调度队列(N = CPU 核数)
struct cfs_rq **cfs_rq;
//公平调度带宽限制
struct cfs_bandwidth cfs_bandwidth;
...
}
第一个 cgroup_subsys_state css 成员我们在前面说过了,这相当于它的“父类”。再来看 parent、siblings、children 等几