一文解决内核是如何给容器中的进程分配CPU资源的?

现在很多公司的服务都是跑在容器下,我来问几个容器 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 等几

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值