Kubernetes cgroups详解

cgroups简介

cgroups(是control groups的简写)是Linux内核的一个功能,用来限制、控制与分离一个进程组的资源(如CPU、内存、磁盘输入输出等)。

这个项目最早是由Google的工程师(主要是Paul Menage和Rohit Seth)在2006年发起,最早的名称为进程容器(process containers)。
2007年,因为在Linux内核中,容器(container)这个名词有许多不同的意义,为避免混乱,被重命名为cgroup,并且被合并到2.6.24版的内核中去。自那以后,又添加了很多功能。

2008年,通过将cgroups的资源管理能力和Linux Namespace(命名空间)的视图隔离能力组合在一起,一项完整的容器技术LXC(Linux Container)出现在了Linux内核中,这就是如今被广泛应用的容器技术的实现基础。

2013年,Docker项目正式发布,让Linux容器技术逐步席卷天下。Docker本身其实也是属于LXC的一种封装,提供简单易用的容器使用接口。它最大的特性就是引入了容器镜像。Docker 通过容器镜像,将应用程序与运行该程序需要的环境,打包放在一个文件里面。运行这个文件,就会生成一个虚拟容器。

为了适应混合云场景下大规模集群的容器部署、管理等问题,Google在2014年6月推出了容器集群管理系统Kubernetes(简称 K8S)。K8S来源于google内部调度系统 Borg,拥有在混合云场景的生产环境下对容器进行管理、编排的功能。Kubernetes在容器的基础上引入了Pod功能,这个功能可以让不同容器之间互相通信,实现容器的分组调配。

得益于Google在大规模集群基础设施建设的强大积累,脱胎于Borg的K8S很快成为了行业的标准应用,堪称容器编排的必备工具。

设计目标

cgroups的一个设计目标是为不同的应用情况提供统一的接口,从控制单一进程到操作系统层虚拟化(像OpenVZ,Linux-VServer,LXC)。cgroups提供:

  • 资源限制:组可以被设置不超过设定的内存限制;这也包括虚拟内存。

  • 优先级:一些组可能会得到大量的CPU或磁盘IO吞吐量。

  • 结算:用来度量系统实际用了多少资源。

  • 控制:冻结组或检查点和重启动。

版本

cgroup有v1和v2两个版本,v1版本是最早的实现,当时resource controllers的开发各自为政,导致controller间存在不一致,并且controller的嵌套挂载使cgroup的管理非常复杂。Linux kernel 3.10 开始提供v2版本cgroup(Linux Control Group v2)。开始是试验特性,隐藏在挂载参数-o __DEVEL__sane_behavior中,直到Linuxe Kernel 4.5.0的时候,cgroup v2才成为正式特性。cgroup v2希望完全取代cgroup v1,但是为了兼容,cgroup v1没有被移除。cgroup v2实现的controller是cgroup v1的子集,可以同时使用cgroup v1和cgroup v2,但一个controller不能既在cgroup v1中使用,又在cgroup v2中使用。

识别 Linux 节点上的 cgroup 版本

cgroup 版本取决于正在使用的 Linux 发行版和操作系统上配置的默认 cgroup 版本。 要检查你的发行版使用的是哪个 cgroup 版本,请在该节点上运行 stat -fc %T /sys/fs/cgroup/ 命令:

stat -fc %T /sys/fs/cgroup/

对于 cgroup v2,输出为 cgroup2fs。

对于 cgroup v1,输出为 tmpfs。

扩展阅读:关于 cgroup v2

术语

术语 描述
task(任务) 系统中的线程
process(进程) 系统中的进程
cgroup(控制组) cgroups 中的资源控制都以cgroup为单位实现。cgroup表示按某种资源控制标准划分而成的任务组,包含一个或多个子系统。一个任务可以加入某个cgroup,也可以从某个cgroup迁移到另外一个cgroup
subsystem(子系统) cgroups中的subsystem就是一个资源调度控制器(Resource Controller)。比如CPU子系统可以控制CPU时间分配,内存子系统可以限制cgroup内存使用量。
hierarchy(层级树) hierarchy由一系列cgroup以一个树状结构排列而成,每个hierarchy通过绑定对应的subsystem进行资源调度。hierarchy中的cgroup节点可以包含零或多个子节点,子节点继承父节点的属性。整个系统可以有多个hierarchy。

子系统

每个 cgroup 子系统代表一种资源,如针对某个 cgroup 的处理器时间或者 pid 的数量,也叫进程数。Linux 内核提供对以下 13 种 cgroup 子系统的支持:

  • cpuset - 为 cgroup 内的任务分配独立的处理器和内存节点

  • cpu - 使用调度程序对 cgroup 内的任务提供 CPU 资源的访问

  • cpuacct - 生成 cgroup 中所有任务的 CPU 使用情况报告

  • io - 限制对块设备的读写操作

  • memory - 限制 cgroup 中的一组任务的内存使用

  • devices - 限制 cgroup 中的一组任务访问设备

  • freezer - 允许 cgroup 中的一组任务挂起/恢复

  • net_cls - 允许对 cgroup 中的任务产生的网络数据包进行标记

  • net_prio - 针对 cgroup 中的每个网络接口提供一种动态修改网络流量优先级的方法

  • perf_event - 支持访问 cgroup 中的性能事件,允许perf观测cgroup中的task

  • hugetlb - 为 cgroup 开启对大页内存的支持

  • pid - 限制 cgroup 中的进程数量

  • rdma - 限制RDMA资源

这里面每一个子系统都需要与内核的其他模块配合来完成资源的控制,比如对 cpu 资源的限制是通过进程调度模块根据 cpu 子系统的配置来完成的;对内存资源的限制则是内存模块根据 memory 子系统的配置来完成的,而对网络数据包的控制则需要 Traffic Control 子系统来配合完成。本文不会讨论内核是如何使用每一个子系统来实现资源的限制,而是重点放在内核是如何把 cgroups 对资源进行限制的配置有效的组织起来的,和内核如何把cgroups 配置和进程进行关联的,以及内核是如何通过 cgroups 文件系统把cgroups的功能暴露给用户态的。

查看 cgroup 挂载点:

# df -h | grep cgroup
tmpfs              244G     0  244G   0% /sys/fs/cgroup

我们可以在 cgroup 根目录下看到各子系统文件:

# ll /sys/fs/cgroup/
dr-xr-xr-x 4 root root  0 Oct 25 14:34 blkio
lrwxrwxrwx 1 root root 11 Oct 25 14:34 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 Oct 25 14:34 cpuacct -> cpu,cpuacct
dr-xr-xr-x 5 root root  0 Oct 25 14:34 cpu,cpuacct
dr-xr-xr-x 3 root root  0 Oct 25 14:34 cpuset
dr-xr-xr-x 4 root root  0 Oct 25 14:34 devices
dr-xr-xr-x 3 root root  0 Oct 25 14:34 freezer
dr-xr-xr-x 3 root root  0 Oct 25 14:34 hugetlb
dr-xr-xr-x 5 root root  0 Oct 25 14:34 memory
lrwxrwxrwx 1 root root 16 Oct 25 14:34 net_cls -> net_cls,net_prio
dr-xr-xr-x 3 root root  0 Oct 25 14:34 net_cls,net_prio
lrwxrwxrwx 1 root root 16 Oct 25 14:34 net_prio -> net_cls,net_prio
dr-xr-xr-x 3 root root  0 Oct 25 14:34 perf_event
dr-xr-xr-x 4 root root  0 Oct 25 14:34 pids
dr-xr-xr-x 2 root root  0 Oct 25 14:34 rdma
dr-xr-xr-x 5 root root  0 Oct 25 14:34 systemd

cpu 子系统

cpu子系统用于控制cgroup中所有进程可以使用的cpu时间片。

cpu子系统主要涉及5个参数:cpu.cfs_period_us,cpu.cfs_quota_us,cpu.shares,cpu.rt_period_us,cpu.rt_runtime_us。cfs表示Completely Fair Scheduler完全公平调度器,是Linux内核的一部分,负责进程调度。

参数 说明
cpu.cfs_period_us 用来设置一个CFS调度时间周期长度,默认值是100000us(100ms),一般cpu.cfs_period_us作为系统默认值我们不会去修改它。系统总CPU带宽:cpu核心数 * cfs_period_us
cpu.cfs_quota_us 用来设置在一个CFS调度时间周期(cfs_period_us)内,允许此控制组执行的时间。默认值为-1表示不限制时间。cfs_quota_us的最小值为1ms(1000)。通过cfs_period_us和cfs_quota_us可以以绝对比例限制cgroup的cpu使用,即cfs_quota_us/cfs_period_us 等于进程可以利用的cpu cores,不能超过这个数值。使用cfs_quota_us/cfs_period_us,例如20000us/100000us=0.2,表示允许这个控制组使用的CPU最大是0.2个CPU,即限制使用20% CPU。 如果cfs_quota_us/cfs_period_us=2,就表示允许控制组使用的CPU资源配置是2个。
cpu.shares 用来设置cpu cgroup子系统对于 cgroup 之间的cpu分配比例。默认值是1024。cpu.shares以相对比例限制cgroup的cpu。例如:在两个 cgroup 中都将 cpu.shares 设定为 1024 的任务将有相同的 CPU 时间,但在 cgroup 中将 cpu.shares 设定为 2048 的任务可使用的 CPU 时间是在 cgroup 中将 cpu.shares 设定为 1024 的任务可使用的 CPU 时间的两倍。
cpu.rt_runtime_us 以微秒(µs,这里以“us”代表)为单位指定在某个时间段中 cgroup 中的任务对 CPU 资源的最长连续访问时间。建立这个限制是为了防止一个 cgroup 中的任务独占 CPU 时间。如果 cgroup 中的任务应该可以每 5 秒中可有 4 秒时间访问 CPU 资源,请将 cpu.rt_runtime_us 设定为 4000000,并将 cpu.rt_period_us 设定为 5000000。
cpu.rt_period_us 以微秒(µs,这里以“us”代表)为单位指定在某个时间段中 cgroup 对 CPU 资源访问重新分配的频率。如果某个 cgroup 中的任务应该每 5 秒钟有 4 秒时间可访问 CPU 资源,则请将 cpu.rt_runtime_us 设定为 4000000,并将 cpu.rt_period_us 设定为 5000000。

注意cpu.cfs_quota_us/cpu.cfs_period_us决定cpu控制组中所有进程所能使用CPU资源的最大值,而cpu.shares决定了cpu控制组间可用CPU的相对比例,这个比例只有当主机上的CPU完全被打满时才会起作用。

cpuacct 子系统

cpuacct子系统(CPU accounting)会自动生成报告来显示cgroup中任务所使用的CPU资源。报告有两大类:cpuacct.statcpuacct.usage

参数 说明
cpuacct.stat cpuacct.stat记录cgroup的所有任务(包括其子孙层级中的所有任务)使用的用户和系统CPU时间。
cpuacct.usage cpuacct.usage记录这个cgroup中所有任务(包括其子孙层级中的所有任务)消耗的总CPU时间(纳秒)。
cpuacct.usage_percpu cpuacct.usage_percpu记录这个cgroup中所有任务(包括其子孙层级中的所有任务)在每个CPU中消耗的CPU时间(以纳秒为单位)。

cpuset 子系统

cpuset主要是为了 NUMA 使用的,numa技术将CPU划分成不同的node,每个node由多个CPU组成,并且有独立的本地内存、I/O等资源(硬件上保证)。可以使用numactl查看当前系统的node信息。

参数 说明
cpuset.cpus cpuset.cpus指定允许这个 cgroup 中任务访问的 CPU。这是一个用逗号分开的列表,格式为 ASCII,使用小横线("-")代表范围。如(0-2,16),代表 CPU 0、1、2 和 16。
cpuset.mems cpuset.mems指定允许这个 cgroup 中任务可访问的内存节点。这是一个用逗号分开的列表,格式为 ASCII,使用小横线("-")代表范围。如(0-2,16)代表内存节点 0、1、2 和 16。

memory子系统

memory 子系统自动生成 cgroup 任务使用内存资源的报告,并限定这些任务所用内存的大小。

参数 说明
memory.limit_in_bytes 用来设置用户内存(包括文件缓存)的最大用量。如果没有指定单位,则该数值将被解读为字节。但是可以使用后缀代表更大的单位 —— k 或者 K 代表千字节,m 或者 M 代表兆字节 ,g 或者 G 代表千兆字节。
您不能使用 memory.limit_in_bytes 限制 root cgroup;您只能对层级中较低的群组应用这些值。
在 memory.limit_in_bytes 中写入 -1 可以移除全部已有限制。
memory.memsw.limit_in_bytes 用来设置内存与 swap 用量之和的最大值。如果没有指定单位,则该值将被解读为字节。但是可以使用后缀代表更大的单位 —— k 或者 K 代表千字节,m 或者 M 代表兆字节,g 或者 G 代表千兆字节。
您不能使用 memory.memsw.limit_in_bytes 来限制 root cgroup;您只能对层级中较低的群组应用这些值。
在 memory.memsw.limit_in_bytes 中写入 -1 可以删除已有限制。
memory.oom_control 用来设置当控制组中所有进程达到可以使用内存的最大值时,也就是发生OOM(Out of Memory)时是否触发linux的OOM killer杀死控制组内的进程。包含一个标志(0或1)来开启或者关闭cgroup的OOM killer,默认的配置是开启OOM killer的。
如果OOM killer关闭,那么进程尝试申请的内存超过允许,那么它就会被暂停(就是hang死),直到额外的内存被释放
memory.oom_control 文件也在 under_oom 条目下报告当前 cgroup 的 OOM 状态。如果该 cgroup 缺少内存,则会暂停它里面的任务。under_oom 条目报告值为 1。
memory.usage_in_bytes 这个参数是只读的,它里面的数值是当前控制组里所有进程实际使用的内存总和,主要是 RSS 内存和 Page Cache 内存的和
准确的内存使用量计算公式(memory.kmem.usage_in_bytes 表示该 memcg 内核内存使用量): memory.usage_in_bytes = memory.stat[rss] + memory.stat[cache] + memory.kmem.usage_in_bytes。
memory.stat 保存内存相关的统计数据,可以显示在当前控制组里各种内存类型的实际的开销。
想要判断容器真实的内存使用量,我们不能用 Memory Cgroup 里的 memory.usage_in_bytes,而需要用 memory.stat 里的 rss 值。
memory.swappiness 可以控制这个 Memroy Cgroup 控制组下面匿名内存和 page cache 的回收,取值的范围和工作方式和全局的 swappiness 差不多。这里有一个优先顺序,在 Memory Cgorup 的控制组里,如果你设置了 memory.swappiness 参数,它就会覆盖全局的 swappiness,让全局的 swappiness 在这个控制组里不起作用。不同于 /proc 文件系统下全局的 swappiness,当 memory.swappiness = 0 的时候,对匿名页的回收是始终禁止的,也就是始终都不会使用 Swap 空间。因此,我们可以通过 memory.swappiness 参数让需要使用 Swap 空间的容器和不需要 Swap 的容器,同时运行在同一个宿主机上。
memory.memsw.usage_in_bytes 报告该 cgroup 中进程当前所用的内存量和 swap 空间总和(以字节为单位)。
memory.max_usage_in_bytes 报告 cgroup 中进程所用的最大内存量(以字节为单位)。
memory.memsw.max_usage_in_bytes 报告该 cgroup 中进程的最大内存用量和最大 swap 空间用量(以字节为单位)。
memory.failcnt 报告内存达到 memory.limit_in_bytes 设定的限制值的次数。
memory.memsw.failcnt 报告内存和 swap 空间总和达到 memory.memsw.limit_in_bytes 设定的限制值的次数。
memory.force_empty 当设定为 0 时,该 cgroup 中任务所用的所有页面内存都将被清空。这个接口只可在 cgroup 没有任务时使用。如果无法清空内存,请在可能的情况下将其移动到父 cgroup 中。移除 cgroup 前请使用 memory.force_empty 参数以免将废弃的页面缓存移动到它的父 cgroup 中。
memory.use_hierarchy 包含标签(0 或者 1),它可以设定是否将内存用量计入 cgroup 层级的吞吐量中。如果启用(1),内存子系统会从超过其内存限制的子进程中再生内存。默认情况下(0),子系统不从任务的子进程中再生内存。

注意:容器可以通过设置 memory.swappiness 参数来决定是否使用 swap 空间。

核心文件

从使用的角度看,cgroup就是一个目录树,目录中可以创建子目录,这些目录称为“cgroup 目录”,在一些场景中为了体现层级关系,还会称为“cgroup 子目录”。
每个目录中有一些用来设置对应controller的文件,这些文件称呼为“cgroup控制器的文件接口”。这些文件包括:

  • notify_on_release

包含 Boolean 值,1 或者 0,分别可以启动和禁用释放代理的指令。如果 notify_on_release 启用,当 cgroup 不再包含任何任务时(即,cgroup 的 tasks 文件包含 PID,而 PID 被移除,致使文件变空),kernel 会执行 release_agent 文件的内容。通向此空 cgroup 的路径会作为释放代理的参数被提供。

notify_on_release 参数的默认值在 root cgroup 中是 0。所有非 root cgroup 从其父 cgroup 处继承 notify_on_release 的值。

  • release_agent

当 “notify on release” 被触发,它包含要执行的指令。一旦 cgroup 的所有进程被清空,并且 notify_on_release 标记被启用,kernel 会运行 release_agent 文件中的指令,并且提供通向被清空 cgroup 的相关路径(与 root cgroup 相关)作为参数。

注意:这个文件只会存在于root cgroup下面,其他cgroup里面不会有这个文件。

  • cgroup.procs

包含在 cgroup 中运行的线程群组列表(由它们的 TGID 表示)。TGID 列表不一定是有序的,也不一定是特有的(也就是说,可能包含重复条目)。将 TGID 写入 cgroup 的 cgroup.procs 文件,可将此线程组群移至该 cgroup。

  • tasks

包含一系列在 cgroup 中运行的进程(由它们的 PID 表示)。PID 列表不一定是有序的,也不一定是特有的(也就是说,可能包含重复条目)。将 PID 写入一个 cgroup 的 tasks 文件,可将此进程移至该 cgroup。

  • cgroup.clone_children

这个文件仅用于cpuset子系统,当该文件的内容为1时,新创建的cgroup将会继承父cgroup的配置,即从父cgroup里面拷贝配置文件来初始化新cgroup

按理说,该配置只对cpuset(subsystem)有用,应该放到cpuset的subsystem中,但是由于历史原因,其放到了全局cgroup文件中。

  • cgroup.event_control

该文件仅用于memory子系统,与 cgroup 的通知 API 一起,允许 cgroup 的变更状态通知被发送。

  • cgroup.sane_behavior

这个文件只会存在于root cgroup下面,其他cgroup里面不会有这个文件。该文件也是控制了cgroup->flags的一个叫做CGRP_ROOT_SANE_BEHAVIOR的位。

由于cgroup一直再发展,很多子系统有很多不同的特性,内核用CGRP_ROOT_SANE_BEHAVIOR来控制打开某些特性和关闭某些特性

注意: 该文件是只读的,也就是说不能随便修改该值,只有在mount各个子系统时,指定mount选项__DEVEL__sane_behavior是,该位的值才会置位1。

文件系统

Linux通过文件的方式,将cgroups的功能和配置暴露给用户,这得益于Linux的虚拟文件系统(VFS)。VFS将具体文件系统的细节隐藏起来,给用户态提供一个统一的文件系统API接口,cgroups和VFS之间的链接部分,称之为cgroups文件系统。

比如挂在 cpu、cpuset、memory 三个子系统到 /cgroups/cpu_mem 目录下:

mount -t cgroup -o cpu,cpuset,memory cpu_mem /cgroups/cpu_mem

其中 -t 选项指定文件系统类型为cgroup类型,-o 指定本次创建的cgroup实例与cpu和momory子系统(或资源)关联,cpu_momory指定了当前cgroup实例在整个cgroup树中所处的层级名称,最后的路径为文件系统挂载点。

Hierarchy 层级树

cgroup 使用层次结构 (Tree) 对资源做划分。参考下图:

每个层级都会有一个根节点, 子节点是根节点的比重划分。

子系统和层级的关系:

  1. 一个子系统最多附加到一个层级(Hierarchy) 上。

  2. 一个 层级(Hierarchy) 可以附加多个子系统

cgroups驱动

runtime 有两种 cgroup 驱动:一种是 systemd,另外一种是 cgroupfs

对于cgroup的操作驱动,大多数linux发行版上,默认的驱动都为systemd
简单了解到的两个驱动的区别:

  1. cgroupfs是文件驱动修改,内核功能没有提供任何的系统调用接口,而是对 linux vfs 的一个实现,因此可以用类似文件系统的方式进行操作。

  2. systemd封装了 cgroups 的软件也能让你通过它们定义的接口控制 cgroups 的内容,因此是通过接口调用驱动修改。

kubernetes 中默认 kubelet 的 cgroup 驱动就是 cgroupfs,若要使用 systemd,则必须将 kubelet 以及 runtime 都需要配置为 systemd 驱动。

注意:Kubernetes 中使用 systemd 驱动时,创建的 cgroup 会有 .slice 后缀。

注意:Docker默认的cgroup的驱动为cgroupfs,可通过启动参数 native.cgroupdriver=systemd 进行修改。
Kubernetes的默认驱动需要和docker的驱动指定一致,因此在docker使用默认配置的情况下,K8s使用systemd驱动安装K8s会出现:

kubelet cgroup driver: “systemd” is different from docker cgroup driver: “cgroupfs”

配置cgroups驱动

我们推荐使用 systemd 驱动,不推荐 cgroupfs 驱动。

因为Ubuntu系统、Debian系统、Centos7系统,都是使用systemd初始化系统的。systemd这边已经有一套cgroup管理器了,如果容器运行时和kubelet使用cgroupfs,此时就会存在cgroups和systemd两种cgroup管理器。也就意味着操作系统里面存在两种资源分配的视图,当操作系统上存在CPU,内存等等资源不足的时候,操作系统上的进程会变得不稳定。

扩展阅读:配置 cgroup 驱动

cgroups在K8s中的应用

kubelet作为kubernetes中的node agent,所有cgroup的操作都由其内部的ContainerManager模块实现,ContainerManager会通过cgroup将资源使用层层限制: container-> pod-> qos -> node。每一层都抽象出一种资源管理模型,通过这种方式提供了一种稳定的运行环境。如下图所示:

Conainer level cgroups

kubernetes对于容器级别的隔离其实是交由底层的runtime来负责的,例如docker,当我们指定运行容器所需要资源的request和limit时,kubelet 在将相关参数传给 docker,docker会为容器设置进程所运行cgroup的cpu.sharescpu.cfs_period_uscpu.cfs_quota_usmemory.limit_in_bytes等指标。

CPU

首先是 CPU 资源,我们先看一下 CPU request。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 资源的最低需求。

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

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

在 cgroup 的 CPU 子系统中,可以通过这两个配置,严格控制这个 cgroup 中的进程对 CPU 的使用量,保证使用的 CPU 资源不会超过 cfs_quota_us/cfs_period_us,也正好就是我们一开始申请的 limit 值。

对于cpu来说,如果指定了 request 而没有指定 limit 的话,那么 cfs_quota_us 将会被设置为 -1,即没有限制。而如果 limitrequest都没有指定的话,cpu.shares 将会被指定为 2,这个是 cpu.shares 允许指定的最小数值了。可见针对这种 pod,kubernetes 只会给他分配最少的 CPU 资源。

Memory

针对内存资源,其实 memory request 信息并不会在 container level cgroup 中有体现。kubernetes 最终只会根据 memory limit 的值来配置 cgroup 的。

在这里 kubernetes 使用的 memory cgroup 子系统中的 memory.limit_in_bytes 配置来实现的。配置方式如下:

memory.limit_in_bytes = memory limit bytes

memory 子系统中的 limit_in_bytes 配置,可以限制一个 cgroup 中的所有进程可以申请使用的内存的最大量,如果超过这个值,那么根据 kubernetes 的默认配置,这个容器会被 OOM killed,容器实例就会发生重启。

对于内存来说,如果没有 limit 的指定的话,memory.limit_in_bytes 将会被指定为一个非常大的值,一般是 2^64 ,可见含义就是不对内存做出限制。

Pod level cgroups

一个pod中往往有一个或者有多个容器,但是如果我们将这些容器的资源使用进行简单的加和并不能准确的反应出整个pod的资源使用,因为每个pod都会有一些overhead的资源,例如sandbox容器使用的资源,docker的containerd-shim使用的资源,此外如果指定memory类型的volume时,这部分内存资源也是属于该pod占用的。因为这些资源并不属于某一个特定的容器,我们无法仅仅通过容器的资源使用量简单累加获取到整个pod的资源,为了方便统计一个pod所使用的资源(resource accounting),并且合理的将所有使用到的资源都纳入管辖范围内,kubernetes引入了pod level Cgroup,会为每个pod创建一个cgroup。该特性通过指定 --cgroups-per-qos=true 开启, 在1.6+版本中是默认开启。kubelet会为每个pod创建一个`pod<pod.UID>`的cgroup,该cgroup的资源限制取决于pod中容器的资源request,limit值。

  • 如果为所有容器都指定了request 和 limit 值,则pod cgroups资源值设置为所有容器的加和,即:
pod<UID>/cpu.shares = sum(pod.spec.containers.resources.requests[cpu])
pod<UID>/cpu.cfs_quota_us = sum(pod.spec.containers.resources.limits[cpu])
pod<UID>/memory.limit_in_bytes = sum(pod.spec.containers.resources.limits[memory])
  • 如果其中某个容器只指定了request没有指定limit则并不会设置pod cgroup的 limit 值, 只设置其 cpu.share值
pod<UID>/cpu.shares = sum(pod.spec.containers.resources.requests[cpu])
  • 如果所有容器没有指定request和limit值,则只设置其 cpu.share值为2, 该pod在资源空闲的时候可以使用完node所有的资源,但是当资源紧张的时候无法获取到任何资源来执行,这也符合低优先级任务的定位:
pod<UID>/cpu.shares = 2

其实上面三种设置方式对应的就是三种QoS pod。这样设置pod level cgourp可以确保在合理指定容器资源时能够防止资源的超量使用,如果未指定则可以使用到足够多的可用资源。每次启动pod时kubelet就会同步对应的pod level cgroup

QoS level cgroup

kubernetes中会将所有的pod按照资源request、limit设置分为不同的QoS classes, 从而拥有不同的优先级。QoS(Quality of Service) 即服务质量,QoS 是一种控制机制,它提供了针对不同用户或者不同数据流采用相应不同的优先级,或者是根据应用程序的要求,保证数据流的性能达到一定的水准。kubernetes 中有三种 QoS,分别为:

  • Guaranteed:pod 的 requests 与 limits 设定的值相等;

  • Burstable:pod requests 小于 limits 的值且不为 0;

  • BestEffort:pod 的 requests 与 limits 均为 0;

三者的优先级如下所示,依次递增:

BestEffort -> Burstable -> Guaranteed

如果指定了 --cgroups-per-qos 也会为每个QoS也会对应一个cgroup,该功能默认开启,这样就可以利用cgroup来做一些QoS级别的资源统计,必要时也可以通过该cgroup限制某个QoS级别的pod能使用的资源总和。此时每个QoS cgroup相当于一个资源池, 内部的pod可以共用池中的资源,但是对于整个资源池会进行一些资源的限制,避免在资源紧张时低优先级的pod抢占高优先级的pod的资源。

对于 Guaranteed 级别的pod,因为pod本身已经指定了request和limit,拥有了足够的限制,无需再增加cgroup来约束。但是对于BurstableBestEffort 类型的pod,因为有的pod和容器没有指定资源限制,在极端条件下会无限制的占用资源,所以我们需要分别设置 BurstableBestEffort cgroup, 然后将对应的pod都创建在该cgroup下。kubelet希望尽可能提高资源利用率,让Burstable和BestEffort类型的pod在需要的时候能够使用足够多的空闲资源,所以默认并不会为该QoS设置资源的limit。但是也需要保证当高优先级的pod需要使用资源时,低优先级的pod能够及时将资源释放出来:对于可压缩的资源例如CPU, kubelet会通过 cpu.shares 来控制,当CPU资源紧张时通过 cpu.shares 来将资源按照比例分配给各个QoS pod,保证每个pod都能够得到其所申请的资源。具体来说, 对于cpu的设置,besteffort和burstable的资源使用限制如下:

ROOT/besteffort/cpu.shares = 2
ROOT/burstable/cpu.shares = max(sum(Burstable pods cpu requests, 2)

对于不可压缩资源内存,要满足“高优先级pod使用资源时及时释放低优先级的pod占用的资源”就比较困难了,kubelet只能通过资源预留的机制,为高优先级的pod预留一定的资源,该特性默认关闭,用户可以通过开启 QOSReserved 特征门控(默认关闭),并设置 --qos-reserved 参数来预留的资源比例,例如 --qos-reserved=memory=50% 表示预留50%高优先级request的资源值,当前只支持memory, 此时qos cgroups的限制如下:

ROOT/burstable/memory.limit_in_bytes = 
    Node.Allocatable - {(summation of memory requests of `Guaranteed` pods)*(reservePercent / 100)}
ROOT/besteffort/memory.limit_in_bytes = 
   Node.Allocatable - {(summation of memory requests of all `Guaranteed` and `Burstable` pods)*(reservePercent / 100)}

同时根据 cpu.shares 的背后实现原理,位于不同层级下面的 cgroup,他们看待同样数量的 cpu.shares 配置可能最终获得不同的资源量。比如在 Guaranteed 级别的 pod cgroup 里面指定的 cpu.shares=1024,和 burstable 下面的某个 pod cgroup 指定 cpu.shares=1024 可能最终获取的 cpu 资源并不完全相同。所以每次创建、删除pod都需要根据上述公式动态计算cgroup值并进行调整。此时kubelet先会尽力去更新低优先级的pod,给高优先级的QoS预留足够的资源。因为memory是不可压缩资源,可能当pod启动时,低优先级的pod使用的资源已经超过限制了,如果此时直接设置期望的值会导致失败,此时kubelet会尽力去设置一个能够设置的最小值(即当前cgroup使用的资源值),避免资源使用进一步增加。通过设置qos资源预留能够保障高优先级的资源可用性,但是对低优先级的任务可能不太友好,官方默认是关闭该策略,可以根据不同的任务类型合理取舍。

Node level cgroups

对于node层面的资源,kubernetes会将一个node上面的资源按照使用对象分为三部分:

  1. 业务进程使用的资源, 即pods使用的资源;

  2. kubernetes组件使用的资源,例如kubelet, docker;

  3. 系统组件使用的资源,例如logind, journald等进程。

通常情况下,我们为提高集群资源利用率,会进行适当超配资源,如果控制不当,业务进程可能会占用完整个node的资源,从而使的第二,三部分核心的程序所使用的资源受到压制,从而影响到系统稳定性,为避免这样的情况发生,我们需要合理限制pods的资源使用,从而为系统组件等核心程序预留足够的资源,保证即使在极端条件下有充足的资源来使用。

kubelet会将所有的pod都创建一个 kubepods 的cgroup下,通过该cgroup来限制node上运行的pod最大可以使用的资源。该cgroup的资源限制取值为:

${Node Capacity} - ${kube-reserved} - ${system-reserved}

其中 kube-reserved 是为kubernetes组件提供的资源预留,system-reserved 是为系统组件预留的资源,分别通过 --kube-reserved--system-reserved 来指定,例如 --kube-reserved=cpu=100m,memory=100Mi

除了指定预留给系统运行的资源外,如果要限制系统运行的资源,可以通过 --enforce-node-allocatable 来设置,该flag指定需要执行限制的资源类型,默认值为 `pods ,即通过上述kubepods来限制pods的使用资源,此外还支持限制的资源类型有:

  • system-reserved:限制kubernetes组件的资源使用,如果开启该限制,则需要同时设置 --kube-reserved-cgroup 参数指定所作用的cgroup

  • kube-reserved:限制系统组件的资源使用,如果开启该限制则需要同时设置 --system-reserved-cgroup

  • none:不进行任何资源限制

如果需要指定多种类型,通过逗号分割枚举即可,注意如果开启了system-reserved和kube-reserved的限制,则意味着将限制这些核心组件的资源使用,以上述--kube-reserved=cpu=100m,memory=100Mi为例,所有的kubernetes组件最多可以使用cpu: 100m,memory: 100Mi。

注意:除非已经很了解自己的资源使用属性,否则并不建议对这两种资源进行限制,避免核心组件CPU饥饿或者内存OOM。

默认情况下该 --enforce-node-allocatable 的值为 pods,即只限制容器使用的资源,但不限制系统进程和kubernetes进程的资源使用量。

kubelet会在资源紧张的时候主动驱逐低优先级的pod,可以指定 {hard-eviction-threshold} 来设置阈值,这样一个node真正可以为pod使用的资源量为:

 ${Allocatable} = ${Node Capacity} - ${kube-Reserved} - ${system-Reserved} - ${hard-eviction-threshold}

这也是调度器进行调度时所使用的资源值。

核心组件的Cgroup

除了上述提到的cgroup设置外,kubelet中还有一些对于单个组件的cgroup设置,例如:

  • --runtime-cgroups:用来指定docker等runtime运行的Cgroup。目前docker-CRI的实现dockershim会管理该cgroup和oom score, 确保dockerd和docker-containerd进程是运行在该cgroup之内,这里会对内存进行限制,使其最大使用宿主机70%的内存,主要是为了防止docker之前内存泄露的bug。 kubelet在此处只是不断获取该cgroup信息供kuelet SummarProvider进行获取统计信息,从而通过summary api暴露出去。

  • --system-cgroups:将所有系统进程都移动到该cgroup下,会进行统计资源使用。如果不指定该参数则不运行在容器中,对应的summary stat数据也不会统计。此处系统进程不包括内核进程, 因为我们并不想限制内核进程的使用。

  • --kubelet-cgroups:如果指定改参数,则containerManager会确保kubelet在该cgroup内运行,同样也会做资源统计,也会调整OOM score值。 summaryProvider会定期同步信息,来获取stat信息。如果不指定该参数,则kubelet会自动地找到kubelet所在的cgroup, 并进行资源的统计。

  • --cgroup-root:kubelet中所有的cgroup层级都会在该root路径下,默认是/,root cgroup 可以通过 $ mount | grep cgroup 看到。如果开启--cgroups-per-qos=true,则在kubelet containerManager中会调整为 /kubepods

以上runtime-cgroups,system-cgroups,kubelet-cgoups的设置都是可选的,如果不进行指定也可以正常运行。但是如果显式指定后就需要与前面提到的 --kube-reserved-cgroup--system-reserved-cgroup 搭配使用,如果配置不当难以达到预期效果:

如果在 --enforce-node-allocatable 参数中指定了 kube-reserved 来限制kubernetes组件的资源限制后,kube-reserved-cgroup的应该是:runtime-cgroups, kubelet-cgoups的父cgroup。只有对应的进程都应该运行该cgroup之下,才能进行限制,kubelet会设置 kube-reserved-cgroup 的资源限制但并不会将这些进程加入到该cgroup中,我们要想让该配置生效,就必须让通过制定--runtime-cgroups,--kubelet-cgoups来将这些进程加入到该cgroup中。同理如果上述--enforce-node-allocatable参数中指定了system-reserved来限制系统进程的资源,则 --system-reserved-cgroup 的参数应该与 --system-cgroups 参数相同,这样系统进程才会运行到system-reserved-cgroup中起到资源限制的作用。

cgroup 层级树

最后整个整个cgroup 层级树如下:

root
 | 
 +- kube-reserved
 |   |
 |   +- kubelet (kubelet process)
 |   | 
 |   +- runtime (docker-engine, containerd...)
 |
 +- system-reserved (systemd process: logind...)
 |
 +- kubepods
 |    |
 |    +- Pod1
 |    |   |
 |    |   +- Container11 (limit: cpu: 10m, memory: 1Gi)
 |    |   |     |
 |    |   |     +- cpu.quota: 10m
 |    |   |     +- cpu.share: 10m
 |    |   |     +- mem.limit: 1Gi
 |    |   |
 |    |   +- Container12 (limit: cpu: 100m, memory: 2Gi)
 |    |   |     |
 |    |   |     +- cpu.quota: 10m
 |    |   |     +- cpu.share: 10m
 |    |   |     +- mem.limit: 2Gi
 |    |   |
 |    |   +- cpu.quota: 110m  
 |    |   +- cpu.share: 110m
 |    |   +- mem.limit: 3Gi
 |    |
 |    +- Pod2
 |    |   +- Container21 (limit: cpu: 20m, memory: 2Gi)
 |    |   |     |
 |    |   |     +- cpu.quota: 20m
 |    |   |     +- cpu.share: 20m
 |    |   |     +- mem.limit: 2Gi
 |    |   |
 |    |   +- cpu.quota: 20m  
 |    |   +- cpu.share: 20m
 |    |   +- mem.limit: 2Gi
 |    |
 |    +- burstable
 |    |   |
 |    |   +- Pod3
 |    |   |   |
 |    |   |   +- Container31 (limit: cpu: 50m, memory: 2Gi; request: cpu: 20m, memory: 1Gi )
 |    |   |   |     |
 |    |   |   |     +- cpu.quota: 50m
 |    |   |   |     +- cpu.share: 20m
 |    |   |   |     +- mem.limit: 2Gi
 |    |   |   |
 |    |   |   +- Container32 (limit: cpu: 100m, memory: 1Gi)
 |    |   |   |     |
 |    |   |   |     +- cpu.quota: 100m
 |    |   |   |     +- cpu.share: 100m
 |    |   |   |     +- mem.limit: 1Gi
 |    |   |   |
 |    |   |   +- cpu.quota: 150m  
 |    |   |   +- cpu.share: 120m
 |    |   |   +- mem.limit: 3Gi
 |    |   |
 |    |   +- Pod4
 |    |   |   +- Container41 (limit: cpu: 20m, memory: 2Gi; request: cpu: 10m, memory: 1Gi )
 |    |   |   |     |
 |    |   |   |     +- cpu.quota: 20m
 |    |   |   |     +- cpu.share: 10m
 |    |   |   |     +- mem.limit: 2Gi
 |    |   |   |
 |    |   |   +- cpu.quota: 20m  
 |    |   |   +- cpu.share: 10m
 |    |   |   +- mem.limit: 2Gi
 |    |   |
 |    |   +- cpu.share: 130m
 |    |   +- mem.limit: $(Allocatable - 5Gi)
 |    |
 |    +- besteffort
 |    |   |
 |    |   +- Pod5
 |    |   |   |
 |    |   |   +- Container6 
 |    |   |   +- Container7
 |    |   |
 |    |   +- cpu.share: 2
 |    |   +- mem.limit: $(Allocatable - 7Gi)

不同 Qos 的本质区别

三种 Qos 在调度和底层表现上都不一样:

  1. 在调度时调度器只会根据 request 值进行调度

  2. 当系统 OOM上时对于处理不同 OOMScore 的进程表现不同,OOMScore 是针对 memory 的,当宿主上 memory 不足时系统会优先 kill 掉 OOMScore 值低的进程,可以使用 $ cat /proc/$PID/oom_score 查看进程的 OOMScore。OOMScore 的取值范围为 [-1000, 1000]。

    1. Guaranteed 和 critical pod 的默认值为 -997

    2. Burstable pod 的值为 2~999

    3. BestEffort pod 的值为 1000,也就是说当系统 OOM 时,首先会 kill 掉 BestEffort pod 的进程,若系统依然处于 OOM 状态,然后才会 kill 掉 Burstable pod,最后是 Guaranteed pod;

  3. cgroups 的配置不同,kubelet 为会三种 Qos 分别创建对应的 QoS level cgroups。

    1. Guaranteed Pod Qos 的 cgroup level 会直接创建在 RootCgroup/kubepods 下

    2. Burstable Pod Qos 的创建在 RootCgroup/kubepods/burstable 下

    3. BestEffort Pod Qos 的创建在 RootCgroup/kubepods/BestEffort 下

代码实现

相关参数

参数 类型 含义 推荐配置 备注
cgroup-root string kubelet 中所有的 cgroup 层级都会在该 root 路径下,默认是 /,root cgroup 可以通过 $ mount | grep cgroup 看到。如果开启--cgroups-per-qos=true,则在 kubelet containerManager 中会调整为 /kubepods。 / 默认挂载到 /sys/fs/cgroup
runtime-cgroups string 用来指定 docker 等 runtime 运行的 Cgroup。目前 docker-CRI 的实现 dockershim 会管理该 cgroup 和 oom score, 确保 dockerd 和 docker-containerd 进程是运行在该 cgroup 之内,这里会对内存进行限制,使其最大使用宿主机 70% 的内存,主要是为了防止 docker 之前内存泄露的 bug。 不应该包含 cgroup 挂载路径
kubelet-cgroups string 如果指定改参数,则 containerManager 会确保 kubelet 在该 cgroup 内运行,同样也会做资源统计,也会调整 OOM score 值。summaryProvider 会定期同步信息,来获取 stat 信息。如果不指定该参数,则 kubelet 会自动地找到 kubelet 所在的 cgroup, 并进行资源的统计。
system-cgroups string 将所有系统进程都移动到该 cgroup 下,会进行统计资源使用。如果不指定该参数则不运行在 cgroup 中,对应的 summary stat 数据也不会统计。此处系统进程不包括内核进程, 因为我们并不想限制内核进程的使用。
cgroups-per-qos bool 启用基于QoS的Cgroup层次结构:QoS类的顶级Cgroup所有Burstable和BestEffort pod都在其特定的顶级QoS Cgroup下。默认开启。 true
cgroup-driver string 默认为cgroupfs,另一可选项为systemd。取决于容器运行时使用的cgroup driver,kubelet与其保持一致。比如你配置docker使用systemd cgroup driver,那么kubelet也需要配置--cgroup-driver=systemd。 systemd
kube-reserved map[string]string 用于配置为kube组件(kubelet,kube-proxy,dockerd等)预留的资源量,比如—kube-reserved=cpu=1000m,memory=8Gi,ephemeral-storage=16Gi
kube-reserved-cgroup string 如果你设置了--kube-reserved,那么请一定要设置对应的cgroup,并且该cgroup目录要事先创建好,否则kubelet将不会自动创建导致kubelet启动失败。比如设置为kube-reserved-cgroup=/kubelet.service 。
system-reserved map[string]string 用于配置为系统进程预留的资源量,比如—system-reserved=cpu=500m,memory=4Gi,ephemeral-storage=4Gi。
system-reserved-cgroup string 如果你设置了--system-reserved,那么请一定要设置对应的cgroup,并且该cgroup目录要事先创建好,否则kubelet将不会自动创建导致kubelet启动失败。比如设置为system-reserved-cgroup=/system.slice。
enforce-node-allocatable []string 默认为pods,要为kube组件和System进程预留资源,则需要设置为pods,kube-reserved,system-reserve 实施节点可分配约束

核心接口

CgroupManager

CgroupManager 用于 cgroup管理。支持cgroup创建、删除和更新。

type CgroupManager interface {
    // 创建 cgroup,并应用cgroup配置。它只是创建叶cgroups。它期望父cgroup已经存在。
    Create(*CgroupConfig) error
    // 销毁指定的 cgroup
    Destroy(*CgroupConfig) error
    // 更新 cgroup 配置
    Update(*CgroupConfig) error
    // 判断 cgroup 是否已存在
    Exists(name CgroupName) bool
    // 将 CgroupName 转换成 cgroupfs 路径,不同的驱动转换规则不同。
  // 我们希望systemd实现进行适当的名称转换。例如,如果我们传递{“foo”,“bar”},
  // 那么systemd应该将名称转换为类似foo.slice/foo-bar.slice的名称
    Name(name CgroupName) string
  // 将 cgroupfs  路径转换为 CgroupName。以 systemd 驱动为例,即将 foo.slice/foo-bar.slice 转成 {“foo”,“bar”}
  // CgroupName() 方法是 Name() 方法的逆行操作
    CgroupName(name string) CgroupName
    // 扫描所有子系统,并解析其 cgroup.procs 文件,以查找与指定cgroup关联的pid
    Pids(name CgroupName) []int
    // 将CPU CFS值减小到最小共享量,即设置 cpu.shares 为2。
    ReduceCPULimits(cgroupName CgroupName) error
    // 返回从cgroupfs读取的指定cgroup的统计信息,这里只读取内存使用量。
    GetResourceStats(name CgroupName) (*ResourceStats, error)
}

PodContainerManager

PodContainerManager 用于存储和管理pod级容器。Pod workers 和 PodContainerManager 交互,以创建和销毁 Pod 的容器。

type PodContainerManager interface {
    // 获取 Pod 的 cgroup name
    GetPodContainerName(*v1.Pod) (CgroupName, string)

    // EnsureExists takes a pod as argument and makes sure that
    // pod cgroup exists if qos cgroup hierarchy flag is enabled.
    // If the pod cgroup doesn't already exist this method creates it.
    EnsureExists(*v1.Pod) error

    // 如果pod cgroup存在,则返回true。
    Exists(*v1.Pod) bool

    // 以pod Cgroup名称作为参数并销毁pod的容器。
    Destroy(name CgroupName) error

    // ReduceCPULimits reduces the CPU CFS values to the minimum amount of shares.
    ReduceCPULimits(name CgroupName) error

    // GetAllPodsFromCgroups enumerates the set of pod uids to their associated cgroup based on state of cgroupfs system.
    GetAllPodsFromCgroups() (map[types.UID]CgroupName, error)

    // IsPodCgroup returns true if the literal cgroupfs name corresponds to a pod
    IsPodCgroup(cgroupfs string) (bool, types.UID)
}

QOSContainerManager

QOSContainerManager 用于管理不同类型QOS(Guaranteed、Burstable和BestEffort)的 cgroup。

type QOSContainerManager interface {
  // 在kubelet启动过程中调用,完成 Burstable和BestEffort 两类 QOS cgroup 的创建
  // 并定期调用 UpdateCgroups() 同步 QOS cgroup 使其和实际值保持一致
    Start(func() v1.ResourceList, ActivePodsFunc) error
  // 查询不同类型QOS(Guaranteed、Burstable和BestEffort)的 cgroup 信息 
    GetQOSContainersInfo() QOSContainersInfo
  // 同步 QOS cgroup 使其和实际值保持一致
    UpdateCgroups() error
}

kubelet启动时

ContainerManager 启动的时候首先会 setupNode 初始化各种cgroup,具体包括:

  • 通过enforceNodeAllocatableCgroups来设置 kubepods,kube-reserved,system-reserved三个cgroup的资源使用

  • 启动QOSContainerManager来定期同步各个QoS class的资源使用,会在后台不断同步kubelet,docker,system cgroup配置。

  • 在每个pod启动/退出时候会调用PodContainerManager来创建/删除pod级别的cgroup并调用 UpdateQoSCgroups 来更新QoS级别的cgroup。

上述所有更新cgroup的操作都会利用一个 CgroupManager 来实现。

调用关系

run()

run() 方法会根据命令行参数配置,获取 kubelet 依赖的各类 cgroup 路径。

  1. 规范 nodeAllocatableRoot、cgroupRoots、kubeletCgroup、runtimeCgroup、SystemCgroups路径,并放入 cgroupRoots 中

    1. 如果 --cgroup-per-qos为true,则 nodeAllocatableRoot 为 /kubepods

    2. 如果 cgroup 驱动为 systemd,则 nodeAllocatableRoot 为 /kubepods.slice(即 /kubepods.slice)

    3. 获取 kubeletCgroup,实现方式:

      1. 找到kubelet进程pid,解析 /proc/${pid}/cgroup 文件,从而找到 kubelet cgroup(即 /system.slice/kubelet.service)
    4. 获取 runtimeCgroup

      1. 根据 dockerd 进程名或 /var/run/docker.pid 文件找到 docker 进程id

      2. 解析 /proc/${pid}/cgroup 文件,从而找到 docker cgroup(即 /system.slice/docker.service)

      3. 如果是其他类型 runtime,则直接返回 --runtime-cgroup 参数

    5. 获取 SystemCgroups

  2. 初始化 CAdvisorInterface,cAdvisor 是实现也依赖 cgroup

  3. 构建 ContainerManager,将 cgroup 相关配置传入 ContainerManager

func run(ctx context.Context, s *options.KubeletServer, kubeDeps *kubelet.Dependencies, featureGate featuregate.FeatureGate) (err error) {
    // ... 省略部分代码
  // 1\. 规范 nodeAllocatableRoot、kubeletCgroup、runtimeCgroup、SystemCgroups路径,并放入 cgroupRoots 中
    var cgroupRoots []string
  // 如果 --cgroup-per-qos为true,则 nodeAllocatableRoot 为 /kubepods
  // 如果 cgroup 驱动为 systemd,则 nodeAllocatableRoot 为 /kubepods.slice
    nodeAllocatableRoot := cm.NodeAllocatableRoot(s.CgroupRoot, s.CgroupsPerQOS, s.CgroupDriver)
    cgroupRoots = append(cgroupRoots, nodeAllocatableRoot)
  // 获取 kubeletCgroup
    kubeletCgroup, err := cm.GetKubeletContainer(s.KubeletCgroups)
    if err != nil {
        klog.InfoS("Failed to get the kubelet's cgroup. Kubelet system container metrics may be missing.", "err", err)
    } else if kubeletCgroup != "" {
        cgroupRoots = append(cgroupRoots, kubeletCgroup)
    }
  // 获取 runtimeCgroup
    runtimeCgroup, err := cm.GetRuntimeContainer(s.ContainerRuntime, s.RuntimeCgroups)
    if err != nil {
        klog.InfoS("Failed to get the container runtime's cgroup. Runtime system container metrics may be missing.", "err", err)
    } else if runtimeCgroup != "" {
        // RuntimeCgroups is optional, so ignore if it isn't specified
        cgroupRoots = append(cgroupRoots, runtimeCgroup)
    }

    if s.SystemCgroups != "" {
        // SystemCgroups is optional, so ignore if it isn't specified
        cgroupRoots = append(cgroupRoots, s.SystemCgroups)
    }
  // 2\. 初始化 CAdvisorInterface
    if kubeDeps.CAdvisorInterface == nil {
        imageFsInfoProvider := cadvisor.NewImageFsInfoProvider(s.ContainerRuntime, s.RemoteRuntimeEndpoint)
        kubeDeps.CAdvisorInterface, err = cadvisor.New(imageFsInfoProvider, s.RootDirectory, cgroupRoots, cadvisor.UsingLegacyCadvisorStats(s.ContainerRuntime, s.RemoteRuntimeEndpoint))
        if err != nil {
            return err
        }
    }

    makeEventRecorder(kubeDeps, nodeName)

    if kubeDeps.ContainerManager == nil {
        if s.CgroupsPerQOS && s.CgroupRoot == "" {
            klog.InfoS("--cgroups-per-qos enabled, but --cgroup-root was not specified.  defaulting to /")
            s.CgroupRoot = "/"
        }

        // 省略 CPU 预留等参数相关逻辑
    // 3\. 构建 ContainerManager
        kubeDeps.ContainerManager, err = cm.NewContainerManager(
            kubeDeps.Mounter,
            kubeDeps.CAdvisorInterface,
            cm.NodeConfig{
                RuntimeCgroupsName:    s.RuntimeCgroups,
                SystemCgroupsName:     s.SystemCgroups,
                KubeletCgroupsName:    s.KubeletCgroups,
                ContainerRuntime:      s.ContainerRuntime,
                CgroupsPerQOS:         s.CgroupsPerQOS,
                CgroupRoot:            s.CgroupRoot,
                CgroupDriver:          s.CgroupDriver,
                KubeletRootDir:        s.RootDirectory,
                ProtectKernelDefaults: s.ProtectKernelDefaults,
        // 传递 cgroup 相关配置
                NodeAllocatableConfig: cm.NodeAllocatableConfig{
                    KubeReservedCgroupName:   s.KubeReservedCgroup,
                    SystemReservedCgroupName: s.SystemReservedCgroup,
                    EnforceNodeAllocatable:   sets.NewString(s.EnforceNodeAllocatable...),
                    KubeReserved:             kubeReserved,
                    SystemReserved:           systemReserved,
                    ReservedSystemCPUs:       reservedSystemCPUs,
                    HardEvictionThresholds:   hardEvictionThresholds,
                },
                QOSReserved:                             *experimentalQOSReserved,
                ExperimentalCPUManagerPolicy:            s.CPUManagerPolicy,
                ExperimentalCPUManagerReconcilePeriod:   s.CPUManagerReconcilePeriod.Duration,
                ExperimentalMemoryManagerPolicy:         s.MemoryManagerPolicy,
                ExperimentalMemoryManagerReservedMemory: s.ReservedMemory,
                ExperimentalPodPidsLimit:                s.PodPidsLimit,
                EnforceCPULimits:                        s.CPUCFSQuota,
                CPUCFSQuotaPeriod:                       s.CPUCFSQuotaPeriod.Duration,
                ExperimentalTopologyManagerPolicy:       s.TopologyManagerPolicy,
                ExperimentalTopologyManagerScope:        s.TopologyManagerScope,
            },
            s.FailSwapOn,
            devicePluginEnabled,
            kubeDeps.Recorder)

        if err != nil {
            return err
        }
    }
  // ······
}

NewContainerManager()

  1. 获取 cgroup 子系统的挂载点和挂载路径

  2. 如果 --cgroups-per-qos 为 true,创建 CgroupManager,并确保 cgroupRoot(即 /)存在。设置 cgroupRoot=cgroupRoot+”kubepods“

  3. 构建 QOSContainerManager

func NewContainerManager(mountUtil mount.Interface, cadvisorInterface cadvisor.Interface, nodeConfig NodeConfig, failSwapOn bool, devicePluginEnabled bool, recorder record.EventRecorder) (ContainerManager, error) {
    // 1\. 获取 cgroup 子系统的挂载点和挂载路径
  // 如果是 cgroup v1,从 /proc/self/mountinfo 和 /proc/self/cgroup 文件中读取子系统配置信息
  // 如果是 cgroup v2,从 /sys/fs/cgroup/cgroup.controllers 文件中读取子系统配置信息
  subsystems, err := GetCgroupSubsystems()
    if err != nil {
        return nil, fmt.Errorf("failed to get mounted cgroup subsystems: %v", err)
    }
  // 省略关闭swap、获取机器信息及 pidlimits 相关逻辑

    // 2\. 构建 CgroupManager,如果 CgroupsPerQOS==true,设置 cgroupRoot=cgroupRoot+”kubepods“
    cgroupRoot := ParseCgroupfsToCgroupName(nodeConfig.CgroupRoot)
    cgroupManager := NewCgroupManager(subsystems, nodeConfig.CgroupDriver)
    if nodeConfig.CgroupsPerQOS {
    // 判断 cgroupRoot 是否存在
        if !cgroupManager.Exists(cgroupRoot) {
            return nil, fmt.Errorf("invalid configuration: cgroup-root %q doesn't exist", cgroupRoot)
        }
        // 设置 cgroupRoot = cgroupRoot + ”kubepods“
        cgroupRoot = NewCgroupName(cgroupRoot, defaultNodeAllocatableCgroupName)
    }
    klog.InfoS("Creating Container Manager object based on Node Config", "nodeConfig", nodeConfig)
  // 3\. 构建 QOSContainerManager
    qosContainerManager, err := NewQOSContainerManager(subsystems, cgroupRoot, nodeConfig, cgroupManager)
    if err != nil {
        return nil, err
    }
  // 4\. 构建 containerManagerImpl
    cm := &containerManagerImpl{
        cadvisorInterface:   cadvisorInterface,
        mountUtil:           mountUtil,
        NodeConfig:          nodeConfig,
        subsystems:          subsystems,
        cgroupManager:       cgroupManager,
        capacity:            capacity,
        internalCapacity:    internalCapacity,
        cgroupRoot:          cgroupRoot,
        recorder:            recorder,
        qosContainerManager: qosContainerManager,
    }
  // 省略 TopologyManager、DeviceManager、CPUManager、MemoryManager 构建逻辑
    return cm, nil
}

containerManagerImpl.setupNode()

setupNode() 方法负责顶级 cgroup(即 /kubepods)、 runtime cgroup(/docker)、system cgroup(/system)、kubelet cgroup(/kubelet)。实现过程如下:

  1. 检查是否安装了所需的cgroups子系统。到目前为止,只需要“cpu”和“memory”。cpu配额是软限制。

  2. 仅当 --cgroups-per-qos 为 true 时,才创建顶级qos容器(即 /kubepods )

  3. 如果 runtime 是 docker,创建定时任务,定期获取并更新 runtime cgroup

  4. 确保 system cgroup 已存在,并将所有非内核线程和没有容器的进程1移动到该容器。

  5. 确保 kubelet cgroup 已存在,将 kubelet 进程pid写入 cgroup.procs 文件中,并设置 OOM score

func (cm *containerManagerImpl) setupNode(activePods ActivePodsFunc) error {
  // 1\. 检查是否安装了所需的cgroups子系统。到目前为止,只需要“cpu”和“memory”。cpu配额是软限制。
  // 解析 /proc/mounts 文件,并遍历所有 cgroup 挂载点(相当于执行 cat /proc/mounts | grep cgroup),
  // 找到 cpu 子系统的挂载点(即 /sys/fs/cgroup/cpu,cpuacct),判断 cpu.cfs_period_us 和 cpu.cfs_quota_us
  // 两个文件是否存在
    f, err := validateSystemRequirements(cm.mountUtil)
    if err != nil {
        return err
    }
    if !f.cpuHardcapping {
        cm.status.SoftRequirements = fmt.Errorf("CPU hardcapping unsupported")
    }
    b := KernelTunableModify
    if cm.GetNodeConfig().ProtectKernelDefaults {
        b = KernelTunableError
    }

    // 2\. 仅当CgroupsPerQOS标志指定为true时,才设置顶级qos容器(即 /kubepods )
    if cm.NodeConfig.CgroupsPerQOS {
        if err := cm.createNodeAllocatableCgroups(); err != nil {
            return err
        }
        err = cm.qosContainerManager.Start(cm.getNodeAllocatableAbsolute, activePods)
        if err != nil {
            return fmt.Errorf("failed to initialize top level QOS containers: %v", err)
        }
    }

    // Enforce Node Allocatable (if required)
    if err := cm.enforceNodeAllocatableCgroups(); err != nil {
        return err
    }
  // 3\. 如果 runtime 是 docker,创建定时任务,定期获取 runtime cgroup
    systemContainers := []*systemContainer{}
    if cm.ContainerRuntime == "docker" {
    // 通过集成 docker CRI,dockershim管理docker进程的cgroups和oom分数。
    // 定期检查docker的cgroup,以便kubelet可以为docker运行时提供统计信息。
    // 1\. 根据 Docker 进程名称 dockerd 或 pid文件(/var/run/docker.pid)得到 Docker 进程的pid
    // 2\. 解析 /proc/${pid}/cgroup 文件得到 runtime cgroup(即 /system.slice/docker.service)
        cm.periodicTasks = append(cm.periodicTasks, func() {
            klog.V(4).InfoS("Adding periodic tasks for docker CRI integration")
            cont, err := getContainerNameForProcess(dockerProcessName, dockerPidFile)
            if err != nil {
                klog.ErrorS(err, "Failed to get container name for process")
                return
            }
            klog.V(2).InfoS("Discovered runtime cgroup name", "cgroupName", cont)
            cm.Lock()
            defer cm.Unlock()
            cm.RuntimeCgroupsName = cont
        })
    }
  // 4\. 确保 system cgroup 已存在
    if cm.SystemCgroupsName != "" {
        if cm.SystemCgroupsName == "/" {
            return fmt.Errorf("system container cannot be root (\"/\")")
        }
        cont, err := newSystemCgroups(cm.SystemCgroupsName)
        if err != nil {
            return err
        }
        cont.ensureStateFunc = func(manager cgroups.Manager) error {
            return ensureSystemCgroups("/", manager)
        }
        systemContainers = append(systemContainers, cont)
    }
    // 5\. 确保 kubelet cgroup 已存在
    if cm.KubeletCgroupsName != "" {
        cont, err := newSystemCgroups(cm.KubeletCgroupsName)
        if err != nil {
            return err
        }

        cont.ensureStateFunc = func(_ cgroups.Manager) error {
            return ensureProcessInContainerWithOOMScore(os.Getpid(), qos.KubeletOOMScoreAdj, cont.manager)
        }
        systemContainers = append(systemContainers, cont)
    } else {
        cm.periodicTasks = append(cm.periodicTasks, func() {
            if err := ensureProcessInContainerWithOOMScore(os.Getpid(), qos.KubeletOOMScoreAdj, nil); err != nil {
                klog.ErrorS(err, "Failed to ensure process in container with oom score")
                return
            }
            cont, err := getContainer(os.Getpid())
            if err != nil {
                klog.ErrorS(err, "Failed to find cgroups of kubelet")
                return
            }
            cm.Lock()
            defer cm.Unlock()

            cm.KubeletCgroupsName = cont
        })
    }

    cm.systemContainers = systemContainers
    return nil
}

containerManagerImpl.createNodeAllocatableCgroups()

当 --cgroups-per-qos 为true时,创建顶级Cgroup(即 /kubepods),并配置该 cgroup 可分配资源量,即:

顶级cgroup 可分配资源量=节点资源总量-system reserved - kubelet reserved

func (cm *containerManagerImpl) createNodeAllocatableCgroups() error {
    nodeAllocatable := cm.internalCapacity
    // 配置顶级 cgroup 可分配资源量:顶级cgroup 可分配资源量=节点资源总量-system reserved - kubelet reserved
    nc := cm.NodeConfig.NodeAllocatableConfig
    if cm.CgroupsPerQOS && nc.EnforceNodeAllocatable.Has(kubetypes.NodeAllocatableEnforcementKey) {
        nodeAllocatable = cm.getNodeAllocatableInternalAbsolute()
    }
    klog.Infof("createNodeAllocatableCgroups cgroupRoot: %s", cm.cgroupRoot)
    cgroupConfig := &CgroupConfig{
        Name: cm.cgroupRoot,
        // The default limits for cpu shares can be very low which can lead to CPU starvation for pods.
        ResourceParameters: getCgroupConfig(nodeAllocatable),
    }
    if cm.cgroupManager.Exists(cgroupConfig.Name) {
        return nil
    }
  // 创建顶级 cgroup /kubepods
    if err := cm.cgroupManager.Create(cgroupConfig); err != nil {
        klog.ErrorS(err, "Failed to create cgroup", "cgroupName", cm.cgroupRoot)
        return err
    }
    return nil
}

containerManagerImpl.enforceNodeAllocatableCgroups()

enforceNodeAllocatableCgroups() 在 --cgroups-per-qos 为 false时也会被调用,该方法用于确保 /kubepods、/kubelet、/system 等 cgroups 的存在,并设定其资源限额。处理过程为:

  1. 获取节点可分配资源量,并更新顶级Cgroup(即 /kubepods)

  2. 创建并配置 system group

  3. 创建并配置 kubelet cgroup

func (cm *containerManagerImpl) enforceNodeAllocatableCgroups() error {
    nc := cm.NodeConfig.NodeAllocatableConfig

    // 1\. 获取节点可分配资源量,并更新顶级Cgroup(即 /kubepods)
    nodeAllocatable := cm.internalCapacity
    // Use Node Allocatable limits instead of capacity if the user requested enforcing node allocatable.
    if cm.CgroupsPerQOS && nc.EnforceNodeAllocatable.Has(kubetypes.NodeAllocatableEnforcementKey) {
        nodeAllocatable = cm.getNodeAllocatableInternalAbsolute()
    }

    klog.V(4).InfoS("Attempting to enforce Node Allocatable", "config", nc)

    cgroupConfig := &CgroupConfig{
        Name:               cm.cgroupRoot,
        ResourceParameters: getCgroupConfig(nodeAllocatable),
    }

    // Using ObjectReference for events as the node maybe not cached; refer to #42701 for detail.
    nodeRef := &v1.ObjectReference{
        Kind:      "Node",
        Name:      cm.nodeInfo.Name,
        UID:       types.UID(cm.nodeInfo.Name),
        Namespace: "",
    }

    // If Node Allocatable is enforced on a node that has not been drained or is updated on an existing node to a lower value,
    // existing memory usage across pods might be higher than current Node Allocatable Memory Limits.
    // Pod Evictions are expected to bring down memory usage to below Node Allocatable limits.
    // Until evictions happen retry cgroup updates.
    // Update limits on non root cgroup-root to be safe since the default limits for CPU can be too low.
    // Check if cgroupRoot is set to a non-empty value (empty would be the root container)
    if len(cm.cgroupRoot) > 0 {
        go func() {
            for {
                err := cm.cgroupManager.Update(cgroupConfig)
                if err == nil {
                    cm.recorder.Event(nodeRef, v1.EventTypeNormal, events.SuccessfulNodeAllocatableEnforcement, "Updated Node Allocatable limit across pods")
                    return
                }
                message := fmt.Sprintf("Failed to update Node Allocatable Limits %q: %v", cm.cgroupRoot, err)
                cm.recorder.Event(nodeRef, v1.EventTypeWarning, events.FailedNodeAllocatableEnforcement, message)
                time.Sleep(time.Minute)
            }
        }()
    }
    // 2\. 创建并配置 system group
    if nc.EnforceNodeAllocatable.Has(kubetypes.SystemReservedEnforcementKey) {
        klog.V(2).InfoS("Enforcing system reserved on cgroup", "cgroupName", nc.SystemReservedCgroupName, "limits", nc.SystemReserved)
        if err := enforceExistingCgroup(cm.cgroupManager, cm.cgroupManager.CgroupName(nc.SystemReservedCgroupName), nc.SystemReserved); err != nil {
            message := fmt.Sprintf("Failed to enforce System Reserved Cgroup Limits on %q: %v", nc.SystemReservedCgroupName, err)
            cm.recorder.Event(nodeRef, v1.EventTypeWarning, events.FailedNodeAllocatableEnforcement, message)
            return fmt.Errorf(message)
        }
        cm.recorder.Eventf(nodeRef, v1.EventTypeNormal, events.SuccessfulNodeAllocatableEnforcement, "Updated limits on system reserved cgroup %v", nc.SystemReservedCgroupName)
    }
    if nc.EnforceNodeAllocatable.Has(kubetypes.KubeReservedEnforcementKey) {
        klog.V(2).InfoS("Enforcing kube reserved on cgroup", "cgroupName", nc.KubeReservedCgroupName, "limits", nc.KubeReserved)
        if err := enforceExistingCgroup(cm.cgroupManager, cm.cgroupManager.CgroupName(nc.KubeReservedCgroupName), nc.KubeReserved); err != nil {
            message := fmt.Sprintf("Failed to enforce Kube Reserved Cgroup Limits on %q: %v", nc.KubeReservedCgroupName, err)
            cm.recorder.Event(nodeRef, v1.EventTypeWarning, events.FailedNodeAllocatableEnforcement, message)
            return fmt.Errorf(message)
        }
        cm.recorder.Eventf(nodeRef, v1.EventTypeNormal, events.SuccessfulNodeAllocatableEnforcement, "Updated limits on kube reserved cgroup %v", nc.KubeReservedCgroupName)
    }
    return nil
}

// 3\. 创建并配置 kubelet group
func enforceExistingCgroup(cgroupManager CgroupManager, cName CgroupName, rl v1.ResourceList) error {
    cgroupConfig := &CgroupConfig{
        Name:               cName,
        ResourceParameters: getCgroupConfig(rl),
    }
    if cgroupConfig.ResourceParameters == nil {
        return fmt.Errorf("%q cgroup is not config properly", cgroupConfig.Name)
    }
    klog.V(4).InfoS("Enforcing limits on cgroup", "cgroupName", cName, "cpuShares", cgroupConfig.ResourceParameters.CpuShares, "memory", cgroupConfig.ResourceParameters.Memory, "pidsLimit", cgroupConfig.ResourceParameters.PidsLimit)
    if !cgroupManager.Exists(cgroupConfig.Name) {
        return fmt.Errorf("%q cgroup does not exist", cgroupConfig.Name)
    }
    if err := cgroupManager.Update(cgroupConfig); err != nil {
        return err
    }
    return nil
}

QoS Cgroups的初始化和定期更新

qosContainerManagerImpl.Start() 方法会在 kubelet 启动时初始化各类 QoS Cgroups配置,并在之后定期(每分钟)根据所有活跃 Pod 的 request.cpu 和 request.memory 配置重新计算并更新各类 QoS Cgroups配置。处理过程如下:

  1. 判断 cgroupRoot(即 /kubepods.slice) 是否存在

  2. 只为Burstable和BestEffort类创建顶级Qos容器

  3. BestEffort QoS类具有静态配置的 cpu.shares 值(为2)

  4. 确保 hugetlb 是大小无上限

  5. 判断 qos cgroup 是否已存在(即/kubepods.slice/kubepods-besteffort.slice 和 /kubepods.slice/kubepods-burstable.slice)

    1. 如果不存在就创建,如果已存在就更新为最新配置

    2. 如果已存在,为了确保状态正确,我们在启动时更新配置

  6. 启动定时任务,定期更新qos cgroup,以确保所需状态与实际状态同步。

func (m *qosContainerManagerImpl) Start(getNodeAllocatable func() v1.ResourceList, activePods ActivePodsFunc) error {
    cm := m.cgroupManager
    rootContainer := m.cgroupRoot
  if !cm.Exists(rootContainer) {  // 判断 cgroupRoot(即 /kubepods.slice) 是否存在 
        return fmt.Errorf("root container %v doesn't exist", rootContainer)
    }

    // 只为Burstable和BestEffort类创建顶级Qos容器
    qosClasses := map[v1.PodQOSClass]CgroupName{
        v1.PodQOSBurstable:  NewCgroupName(rootContainer, strings.ToLower(string(v1.PodQOSBurstable))),
        v1.PodQOSBestEffort: NewCgroupName(rootContainer, strings.ToLower(string(v1.PodQOSBestEffort))),
    }

    // Create containers for both qos classes
    for qosClass, containerName := range qosClasses {
        resourceParameters := &ResourceConfig{}
        // BestEffort QoS类具有静态配置的 cpu.shares 值(为2)
        if qosClass == v1.PodQOSBestEffort {
            minShares := uint64(MinShares)
            resourceParameters.CpuShares = &minShares
        }

        // containerConfig object stores the cgroup specifications
        containerConfig := &CgroupConfig{
            Name:               containerName,
            ResourceParameters: resourceParameters,
        }

        // 确保 hugetlb 是大小无上限
        m.setHugePagesUnbounded(containerConfig)

        // 判断 qos cgroup 是否已存在(即/kubepods.slice/kubepods-besteffort.slice 和 /kubepods.slice/kubepods-burstable.slice)
    // 如果不存在就创建,如果已存在就更新为最新配置
        if !cm.Exists(containerName) {
            if err := cm.Create(containerConfig); err != nil {
                return fmt.Errorf("failed to create top level %v QOS cgroup : %v", qosClass, err)
            }
        } else {
            // 为了确保状态正确,我们在启动时更新配置
            if err := cm.Update(containerConfig); err != nil {
                return fmt.Errorf("failed to update top level %v QOS cgroup : %v", qosClass, err)
            }
        }
    }
    // Store the top level qos container names
    m.qosContainersInfo = QOSContainersInfo{
        Guaranteed: rootContainer,
        Burstable:  qosClasses[v1.PodQOSBurstable],
        BestEffort: qosClasses[v1.PodQOSBestEffort],
    }
    m.getNodeAllocatable = getNodeAllocatable
    m.activePods = activePods

    // 启动定时任务,定期更新qos cgroup,以确保所需状态与实际状态同步。
    go wait.Until(func() {
        err := m.UpdateCgroups()
        if err != nil {
            klog.InfoS("Failed to reserve QoS requests", "err", err)
        }
    }, periodicQOSCgroupUpdateInterval, wait.NeverStop)

    return nil
}

定期更新 QoS Cgroups

qosContainerManagerImpl.UpdateCgroups() 用于实现 QoS Cgroups 的定期更新。更新过程为:

  1. 计算Burstable 和 BestEffort两类 QoS cgroup的 cpu.shares 值

  2. 确保 hugetlb 无上限

  3. 如果开启了 QOSReserved,配置内存上限

  4. 更新 QoS cgroups 配置

func (m *qosContainerManagerImpl) UpdateCgroups() error {
    m.Lock()
    defer m.Unlock()

    qosConfigs := map[v1.PodQOSClass]*CgroupConfig{
        v1.PodQOSBurstable: {
            Name:               m.qosContainersInfo.Burstable,
            ResourceParameters: &ResourceConfig{},
        },
        v1.PodQOSBestEffort: {
            Name:               m.qosContainersInfo.BestEffort,
            ResourceParameters: &ResourceConfig{},
        },
    }

    // 更新Burstable 和 BestEffort两类 QoS cgroup的 cpu.shares 值
    if err := m.setCPUCgroupConfig(qosConfigs); err != nil {
        return err
    }
    // 确保 hugetlb 无上限
    if err := m.setHugePagesConfig(qosConfigs); err != nil {
        return err
    }
    // 如果开启了 QOSReserved,配置内存上限
    if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.QOSReserved) {
    // 将QOS类中所有pod的内存限制相加,计算QOS类内存限制,
    // 并在CgroupConfig中为每个QOS类设置这些限制。
        for resource, percentReserve := range m.qosReserved {
            switch resource {
            case v1.ResourceMemory:
                m.setMemoryReserve(qosConfigs, percentReserve)
            }
        }

        updateSuccess := true
        for _, config := range qosConfigs {
            err := m.cgroupManager.Update(config)
            if err != nil {
                updateSuccess = false
            }
        }
        if updateSuccess {
            klog.V(4).InfoS("Updated QoS cgroup configuration")
            return nil
        }

        // 为了增加调用的成功率,再次尝试设置内存预留
        for resource, percentReserve := range m.qosReserved {
            switch resource {
            case v1.ResourceMemory:
                m.retrySetMemoryReserve(qosConfigs, percentReserve)
            }
        }
    }
    // 更新 QoS cgroups 配置
    for _, config := range qosConfigs {
        err := m.cgroupManager.Update(config)
        if err != nil {
            klog.ErrorS(err, "Failed to update QoS cgroup configuration")
            return err
        }
    }

    klog.V(4).InfoS("Updated QoS cgroup configuration")
    return nil
}

计算 CPU Cgroup

qosContainerManagerImpl 会定期(每分钟)计算Burstable 和 BestEffort两类 QoS cgroup的 cpu.shares 值。计算过程如下:

  1. 获取所有活跃的Pod,并遍历这些Pod

  2. 选择QoS为Burstable的Pods,并累计pods的requests.cpu值

  3. 确保 best effort 的 cpu.shares 始终为2

  4. 基于当前观察状态设置burstable的 cpu.shares

func (m *qosContainerManagerImpl) setCPUCgroupConfig(configs map[v1.PodQOSClass]*CgroupConfig) error {
    pods := m.activePods()  // 获取所有活跃的Pod
    burstablePodCPURequest := int64(0)
    for i := range pods {   // 遍历 Pods
        pod := pods[i]
        qosClass := v1qos.GetPodQOS(pod)
        if qosClass != v1.PodQOSBurstable {
            // 仅关心QoS 为 burstable的pods
            continue
        }
    // 累计所有QoS 为 burstable的pods的requests.cpu
        req, _ := resource.PodRequestsAndLimits(pod)
        if request, found := req[v1.ResourceCPU]; found {
            burstablePodCPURequest += request.MilliValue()
        }
    }

    // 确保 best effort 的 cpu.shares 始终为2
    bestEffortCPUShares := uint64(MinShares)
    configs[v1.PodQOSBestEffort].ResourceParameters.CpuShares = &bestEffortCPUShares

    // 基于当前观察状态设置burstable的 cpu.shares
    burstableCPUShares := MilliCPUToShares(burstablePodCPURequest)
    configs[v1.PodQOSBurstable].ResourceParameters.CpuShares = &burstableCPUShares
    return nil
}

配置内存预留

qosContainerManagerImpl.setMemoryReserve()方法用于配置内存预留。它将QOS类中所有pod的内存限制相加,计算QOS类内存限制,并在CgroupConfig中为每个QOS类设置这些限制。配置过程如下:

  1. 分别累计所有 Burstable 和 Guaranteed两类 Qos pod的 request.memory 值

  2. 获取节点可分配内存总量

  3. 根据预留内存比例计算 QoS 内存限制值

func (m *qosContainerManagerImpl) setMemoryReserve(configs map[v1.PodQOSClass]*CgroupConfig, percentReserve int64) {
    qosMemoryRequests := map[v1.PodQOSClass]int64{
        v1.PodQOSGuaranteed: 0,
        v1.PodQOSBurstable:  0,
    }
  // 累计所有 Burstable 和 Guaranteed两类 Qos pod的 request.memory 值
    pods := m.activePods()
    for _, pod := range pods {
        podMemoryRequest := int64(0)
        qosClass := v1qos.GetPodQOS(pod)
        if qosClass == v1.PodQOSBestEffort {
            // limits are not set for Best Effort pods
            continue
        }
        req, _ := resource.PodRequestsAndLimits(pod)
        if request, found := req[v1.ResourceMemory]; found {
            podMemoryRequest += request.Value()
        }
        qosMemoryRequests[qosClass] += podMemoryRequest
    }
    // 获取节点可分配内存总量
    resources := m.getNodeAllocatable()
    allocatableResource, ok := resources[v1.ResourceMemory]
    if !ok {
        klog.V(2).InfoS("Allocatable memory value could not be determined, not setting QoS memory limits")
        return
    }
    allocatable := allocatableResource.Value()
    if allocatable == 0 {
        klog.V(2).InfoS("Allocatable memory reported as 0, might be in standalone mode, not setting QoS memory limits")
        return
    }

    for qos, limits := range qosMemoryRequests {
        klog.V(2).InfoS("QoS pod memory limit", "qos", qos, "limits", limits, "percentReserve", percentReserve)
    }

    // 计算 QoS 内存限制值
    burstableLimit := allocatable - (qosMemoryRequests[v1.PodQOSGuaranteed] * percentReserve / 100)
    bestEffortLimit := burstableLimit - (qosMemoryRequests[v1.PodQOSBurstable] * percentReserve / 100)
    configs[v1.PodQOSBurstable].ResourceParameters.Memory = &burstableLimit
    configs[v1.PodQOSBestEffort].ResourceParameters.Memory = &bestEffortLimit
}

创建和更新Pods

在创建和更新 pods 时,也需要同步创建和更新 pod 依赖的 QOS cgroup及 pod cgroup。

调用关系

syncPod

podWorkers 在接收到 pod 相关事件时,如果事件类型不是 TerminatedPodWork 和 TerminatingPodWork,就会调用 syncPod() 方法同步 pod 的状态。

如果启用了 --cgroups-per-qos,syncPod() 方法中关于 pod cgroup 的处理逻辑为:

  1. 会调用 PodContainerManager.Exists() 方法来判断 pod 的cgroup 是否存在

    1. 如果 pod cgroup 不存在,且pod需要被清理,则kill掉pod

    2. 如果 pod cgroup 不存在,且 pod 不需要被清理

      1. 调用 containerManager.UpdateQOSCgroups() 方法更新 QoS cgroups 配置

      2. 创建 pod cgroup

func (kl *Kubelet) syncPod(ctx context.Context, updateType kubetypes.SyncPodType, pod, mirrorPod *v1.Pod, podStatus *kubecontainer.PodStatus) error {
   // ······
    // 如果启用了 cgroups-per-qos,则为pod创建Cgroups并对其应用资源参数。
    pcm := kl.containerManager.NewPodContainerManager()
    // 如果pod已经终止,那么我们不需要创建或更新pod的cgroup
    if !kl.podWorkers.IsPodTerminationRequested(pod.UID) {
        // 如果 kubelet 启用了 cgroups-per-qos,当kubelet重启时
    // 检查这是否是该pod的第一次同步
        firstSync := true
        for _, containerStatus := range apiPodStatus.ContainerStatuses {
            if containerStatus.State.Running != nil {
                firstSync = false
                break
            }
        }
        // 如果pod的cgroups已经存在或者pod第一次运行,不要杀死pod中的容器
        podKilled := false
        if !pcm.Exists(pod) && !firstSync {
            p := kubecontainer.ConvertPodStatusToRunningPod(kl.getRuntime().Type(), podStatus)
            if err := kl.killPod(pod, p, nil); err == nil {
                podKilled = true
            } else {
                klog.ErrorS(err, "killPod failed", "pod", klog.KObj(pod), "podStatus", podStatus)
            }
        }
        // 创建和更新pod的Cgroups
    // 如果 pod 只运行一次,且已经被 kill,则不为其创建 cgroups
        if !(podKilled && pod.Spec.RestartPolicy == v1.RestartPolicyNever) {
            if !pcm.Exists(pod) {
        // 更新 QOS cgroups 配置
                if err := kl.containerManager.UpdateQOSCgroups(); err != nil {
                    klog.V(2).InfoS("Failed to update QoS cgroups while syncing pod", "pod", klog.KObj(pod), "err", err)
                }
        // 创建 pod cgroup
                if err := pcm.EnsureExists(pod); err != nil {
                    kl.recorder.Eventf(pod, v1.EventTypeWarning, events.FailedToCreatePodContainer, "unable to ensure pod container exists: %v", err)
                    return fmt.Errorf("failed to ensure that the pod: %v cgroups exist and are correctly applied: %v", pod.UID, err)
                }
            }
        }
    }
  // ······
}

SyncPod

SyncPod() 的处理流程:

  1. 计算sandbox容器和其它容器的变更情况。

  2. 必要时kill掉 sandbox 容器。

  3. kill 掉所有不应该运行的容器。

  4. 如有必要,创建 sandbox 容器。

  5. 创建临时容器。

  6. 创建 init 容器。

  7. 创建普通容器。

在创建各类容器时,都需要调用 GetPodCgroupParent() 方法获取到容器的 parent cgroup path(即 pod cgroup),然后将该参数传给 runtime,由 runtime 负责创建容器 cgroup。

func (m *kubeGenericRuntimeManager) SyncPod(pod *v1.Pod, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, backOff *flowcontrol.Backoff) (result kubecontainer.PodSyncResult) {
    // 1\. 计算sandbox容器和其它容器的变更情况
    podContainerChanges := m.computePodActions(pod, podStatus)

    // 2\. 必要时kill掉 sandbox 容器
    if podContainerChanges.KillPod {
        killResult := m.killPodWithSyncResult(pod, kubecontainer.ConvertPodStatusToRunningPod(m.runtimeName, podStatus), nil)

    } else {
        // 3\. kill 掉所有不应该运行的容器
        for containerID, containerInfo := range podContainerChanges.ContainersToKill {
            if err := m.killContainer(pod, containerID, containerInfo.name, containerInfo.message, containerInfo.reason, nil); err != nil {
                killContainerResult.Fail(kubecontainer.ErrKillContainer, err.Error())
                klog.ErrorS(err, "killContainer for pod failed", "containerName", containerInfo.name, "containerID", containerID, "pod", klog.KObj(pod))
                return
            }
        }
    }

    // 4\. 如有必要,创建 sandbox 容器
    podSandboxID := podContainerChanges.SandboxID
    if podContainerChanges.CreateSandbox {
        var msg string
        var err error

        klog.V(4).InfoS("Creating PodSandbox for pod", "pod", klog.KObj(pod))
        createSandboxResult := kubecontainer.NewSyncResult(kubecontainer.CreatePodSandbox, format.Pod(pod))
        result.AddSyncResult(createSandboxResult)
    // 创建 sandbox 容器
        podSandboxID, msg, err = m.createPodSandbox(pod, podContainerChanges.Attempt)
        // 检查 sandbox 容器运行状态
        podSandboxStatus, err := m.runtimeService.PodSandboxStatus(podSandboxID)

    }

    // 获取 podSandboxConfig 以运行其它容器
    configPodSandboxResult := kubecontainer.NewSyncResult(kubecontainer.ConfigPodSandbox, podSandboxID)
    result.AddSyncResult(configPodSandboxResult)
    podSandboxConfig, err := m.generatePodSandboxConfig(pod, podContainerChanges.Attempt)

  // start() 方法用于启动临时容器、init 容器和普通容器
    start := func(typeName string, spec *startSpec) error {
        // 创建并启动容器
        if msg, err := m.startContainer(podSandboxID, podSandboxConfig, spec, pod, podStatus, pullSecrets, podIP, podIPs); err != nil {

        }
        return nil
    }

    // 5\. 启动临时容器
    if utilfeature.DefaultFeatureGate.Enabled(features.EphemeralContainers) {
        for _, idx := range podContainerChanges.EphemeralContainersToStart {
            start("ephemeral container", ephemeralContainerStartSpec(&pod.Spec.EphemeralContainers[idx]))
        }
    }

    // 6\. 启动 init 容器
    if container := podContainerChanges.NextInitContainerToStart; container != nil {
    }

    // 7\. 启动普通容器
    for _, idx := range podContainerChanges.ContainersToStart {
        start("container", containerStartSpec(&pod.Spec.Containers[idx]))
    }

    return
}
GetPodCgroupParent()
// GetPodCgroupParent gets pod cgroup parent from container manager.
func (kl *Kubelet) GetPodCgroupParent(pod *v1.Pod) string {
    pcm := kl.containerManager.NewPodContainerManager()
    _, cgroupParent := pcm.GetPodContainerName(pod)
    return cgroupParent
}
GetPodContainerName()
// GetPodContainerName returns the CgroupName identifier, and its literal cgroupfs form on the host.
func (m *podContainerManagerImpl) GetPodContainerName(pod *v1.Pod) (CgroupName, string) {
    podQOS := v1qos.GetPodQOS(pod)
    // Get the parent QOS container name
    var parentContainer CgroupName
    switch podQOS {
    case v1.PodQOSGuaranteed:
        parentContainer = m.qosContainersInfo.Guaranteed
    case v1.PodQOSBurstable:
        parentContainer = m.qosContainersInfo.Burstable
    case v1.PodQOSBestEffort:
        parentContainer = m.qosContainersInfo.BestEffort
    }
    podContainer := GetPodCgroupNameSuffix(pod.UID)

    // Get the absolute path of the cgroup
    cgroupName := NewCgroupName(parentContainer, podContainer)
    // Get the literal cgroupfs name
    cgroupfsName := m.cgroupManager.Name(cgroupName)

    return cgroupName, cgroupfsName
}

销毁Pods

kubelet 中消耗 Pod 的方式有两种,一种是正常终止,另一种是定期清理孤儿Pod。无论是哪种方式,都会涉及到清理pod的cgroup。

调用关系

定期清理Pod

kubelet会定时(2s)触发housekeeping,会调用 HandlePodCleanups() 方法完成Pod删除后的一些清理回收工作。包括终止pod worker、删除不需要的pod以及删除孤立的卷/pod目录。在执行此方法时,不会向pod worker发送配置更改,这意味着不会出现新的pod。

当然HandlePodCleanups的作用不仅仅是清理not running的pod,再比如数据已经在apiserver中强制清理掉了,或者由于其他原因这个节点上还有一些没有完成清理的pod,都是在这个流程中进行处理。

HandlePodCleanups 会从 cgroup 和 podManager 中分别获取所有 pod 列表,然后将两者进行比对。存在与 cgroup 而不存在与 podManager 中的 pod 则认为是需要清理的 Orphaned pod(孤儿pod),然后调用 cleanupOrphanedPodCgroups() 方法将其从 cgroup 目录中清理掉。

清理孤儿pod

cleanupOrphanedPodCgroups() 方法用于删除已经不存在的 pod 的cgroup目录。处理过程为:

  1. 遍历所有 cgroup 中找到的pod以验证它们是否应该运行

  2. 如果pod在运行集合中,则它不是清理的候选对象

  3. 如果pod关联的卷尚未卸载/分离,请不要删除cgroup,否则可能导致 parent cgroup资源计算异常

  4. 销毁所有不应该运行的pod cgroup

func (kl *Kubelet) cleanupOrphanedPodCgroups(pcm cm.PodContainerManager, cgroupPods map[types.UID]cm.CgroupName, possiblyRunningPods map[types.UID]sets.Empty) {
    // 遍历所有找到的pod以验证它们是否应该运行
    for uid, val := range cgroupPods {
        // 如果pod在运行集合中,则它不是清理的候选对象
        if _, ok := possiblyRunningPods[uid]; ok {
            continue
        }

        // 如果卷尚未卸载/分离,请不要删除cgroup,否则可能导致 parent cgroup资源计算异常。
    // 如果卷仍然存在,请在等待时将cgroup中任何进程的 cpu.shares 调整到最小值。
    // 如果kubelet被配置为保留终止的卷,我们将删除cgroup而不是块设备。
        if podVolumesExist := kl.podVolumesExist(uid); podVolumesExist && !kl.keepTerminatedPodVolumes {
            klog.V(3).InfoS("Orphaned pod found, but volumes not yet removed.  Reducing cpu to minimum", "podUID", uid)
            if err := pcm.ReduceCPULimits(val); err != nil {
                klog.InfoS("Failed to reduce cpu time for pod pending volume cleanup", "podUID", uid, "err", err)
            }
            continue
        }
        klog.V(3).InfoS("Orphaned pod found, removing pod cgroups", "podUID", uid)
        // 销毁所有不应该运行的pod cgroup,首先杀死这些cgroup的所有附加进程。
        go pcm.Destroy(val)
    }
}

Destroy()

Destroy() 方法会先 kill 掉Pod cgroup 中的所有进程,然后再安全地移除 pod 的cgroup。

func (m *podContainerManagerImpl) Destroy(podCgroup CgroupName) error {
    // 扫描整个cgroup目录并终止连接到pod cgroup或pod cgroup下的容器cgroup的所有进程
    if err := m.tryKillingCgroupProcesses(podCgroup); err != nil {
        klog.InfoS("Failed to kill all the processes attached to cgroup", "cgroupName", podCgroup, "err", err)
        return fmt.Errorf("failed to kill all the processes attached to the %v cgroups : %v", podCgroup, err)
    }

    // 现在可以安全地移除 pod 的cgroup
    containerConfig := &CgroupConfig{
        Name:               podCgroup,
        ResourceParameters: &ResourceConfig{},
    }
    if err := m.cgroupManager.Destroy(containerConfig); err != nil {
        klog.InfoS("Failed to delete cgroup paths", "cgroupName", podCgroup, "err", err)
        return fmt.Errorf("failed to delete cgroup paths for %v : %v", podCgroup, err)
    }
    return nil
}

Pod终止时

当 podWorks 接收到 pod 终止的事件时,会调用 Kubelet.syncTerminatedPod() 方法清理已终止的pod(没有正在运行的容器),并删除该 pod 的cgroup 。

func (kl *Kubelet) syncTerminatedPod(ctx context.Context, pod *v1.Pod, podStatus *kubecontainer.PodStatus) error {
    klog.V(4).InfoS("syncTerminatedPod enter", "pod", klog.KObj(pod), "podUID", pod.UID)
    defer klog.V(4).InfoS("syncTerminatedPod exit", "pod", klog.KObj(pod), "podUID", pod.UID)

    // generate the final status of the pod
    // TODO: should we simply fold this into TerminatePod? that would give a single pod update
    apiPodStatus := kl.generateAPIPodStatus(pod, podStatus)
    kl.statusManager.SetPodStatus(pod, apiPodStatus)

    // volumes are unmounted after the pod worker reports ShouldPodRuntimeBeRemoved (which is satisfied
    // before syncTerminatedPod is invoked)
    if err := kl.volumeManager.WaitForUnmount(pod); err != nil {
        return err
    }
    klog.V(4).InfoS("Pod termination unmounted volumes", "pod", klog.KObj(pod), "podUID", pod.UID)

    // Note: we leave pod containers to be reclaimed in the background since dockershim requires the
    // container for retrieving logs and we want to make sure logs are available until the pod is
    // physically deleted.

    // remove any cgroups in the hierarchy for pods that are no longer running.
    if kl.cgroupsPerQOS {
        pcm := kl.containerManager.NewPodContainerManager()
        name, _ := pcm.GetPodContainerName(pod)
        if err := pcm.Destroy(name); err != nil {
            return err
        }
        klog.V(4).InfoS("Pod termination removed cgroups", "pod", klog.KObj(pod), "podUID", pod.UID)
    }

    // mark the final pod status
    kl.statusManager.TerminatePod(pod)
    klog.V(4).InfoS("Pod is terminated and will need no more status updates", "pod", klog.KObj(pod), "podUID", pod.UID)

    return nil
}

参考文档

Linux资源管理之cgroups简介

一篇搞懂容器技术的基石: cgroup

cgroup 中默认控制文件的内核实现分析

kubernetes 中 Qos 的设计与实现

Cgroup中的CPU资源控制

重学容器29: 容器资源限制之限制容器的CPU

深入解析 kubernetes 资源管理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容