Kubernetes cgroups详解
cgroups简介
cgroups(是control groups的简写)是Linux内核的一个功能,用来限制、控制与分离一个进程组的资源(如CPU、内存、磁盘输入输出等)。
这个项目最早是由Google的工程师(主要是Paul Menage和Rohit Seth)在2006年发起,最早的名称为进程容器(process containers)。在2007年时,因为在Linux内核中,容器(container)这个名词有许多不同的意义,为避免混乱,被重命名为cgroup,并且被合并到2.6.24版的内核中去。自那以后,又添加了很多功能。
设计目标
cgroups的一个设计目标是为不同的应用情况提供统一的接口,从控制单一进程到操作系统层虚拟化(像OpenVZ,Linux-VServer,LXC)。cgroups提供:
-
资源限制:组可以被设置不超过设定的内存限制;这也包括虚拟内存。
-
优先级:一些组可能会得到大量的CPU或磁盘IO吞吐量。
-
结算:用来度量系统实际用了多少资源。
-
控制:冻结组或检查点和重启动。
术语
| 术语 | 描述 |
|---|---|
| task(任务) | 系统中的进程 |
| cgroup(控制组) | cgroups 中的资源控制都以cgroup为单位实现。cgroup表示按某种资源控制标准划分而成的任务组,包含一个或多个子系统。一个任务可以加入某个cgroup,也可以从某个cgroup迁移到另外一个cgroup |
| subsystem(子系统) | cgroups中的subsystem就是一个资源调度控制器(Resource Controller)。比如CPU子系统可以控制CPU时间分配,内存子系统可以限制cgroup内存使用量。 |
| hierarchy(层级树) | hierarchy由一系列cgroup以一个树状结构排列而成,每个hierarchy通过绑定对应的subsystem进行资源调度。hierarchy中的cgroup节点可以包含零或多个子节点,子节点继承父节点的属性。整个系统可以有多个hierarchy。 |
cgroups子系统
每个 cgroup 子系统代表一种资源,如针对某个 cgroup 的处理器时间或者 pid 的数量,也叫进程数。Linux 内核提供对以下 12 种 cgroup 子系统的支持:
-
cpuset- 为 cgroup 内的任务分配独立的处理器和内存节点; -
cpu- 使用调度程序对 cgroup 内的任务提供 CPU 资源的访问; -
cpuacct- 生成 cgroup 中所有任务的处理器使用情况报告; -
io- 限制对块设备的读写操作; -
memory- 限制 cgroup 中的一组任务的内存使用; -
devices- 限制 cgroup 中的一组任务访问设备; -
freezer- 允许 cgroup 中的一组任务挂起/恢复; -
net_cls- 允许对 cgroup 中的任务产生的网络数据包进行标记; -
net_prio- 针对 cgroup 中的每个网络接口提供一种动态修改网络流量优先级的方法; -
perf_event- 支持访问 cgroup 中的性能事件; -
hugetlb- 为 cgroup 开启对大页内存的支持; -
pid- 限制 cgroup 中的进程数量。
这里面每一个子系统都需要与内核的其他模块配合来完成资源的控制,比如对 cpu 资源的限制是通过进程调度模块根据 cpu 子系统的配置来完成的;对内存资源的限制则是内存模块根据 memory 子系统的配置来完成的,而对网络数据包的控制则需要 Traffic Control 子系统来配合完成。本文不会讨论内核是如何使用每一个子系统来实现资源的限制,而是重点放在内核是如何把 cgroups 对资源进行限制的配置有效的组织起来的,和内核如何把cgroups 配置和进程进行关联的,以及内核是如何通过 cgroups 文件系统把cgroups的功能暴露给用户态的。
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),最大值为1s。通过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子系统对于控制组之间的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.stat和cpuacct.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,使用小横线("-")代表范围。 |
| cpuset.mems | cpuset.mems指定允许这个 cgroup 中任务可访问的内存节点。这是一个用逗号分开的列表,格式为 ASCII,使用小横线("-")代表范围。 |
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 空间。
cgroups文件系统
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树中所处的层级名称,最后的路径为文件系统挂载点。
cgroups驱动
runtime 有两种 cgroup 驱动:一种是 systemd,另外一种是 cgroupfs:
-
cgroupfs 比较好理解,比如说要限制内存是多少、要用 CPU share 为多少,其实直接把 pid 写入到对应cgroup task 文件中,然后把对应需要限制的资源也写入相应的 memory cgroup 文件和 CPU 的 cgroup 文件就可以了;
-
另外一个是 systemd 的 cgroup 驱动,这个驱动是因为 systemd 本身可以提供一个 cgroup 管理方式。所以如果用 systemd 做 cgroup 驱动的话,所有的写 cgroup 操作都必须通过 systemd 的接口来完成,不能手动更改 cgroup 的文件;
kubernetes 中默认 kubelet 的 cgroup 驱动就是 cgroupfs,若要使用 systemd,则必须将 kubelet 以及 runtime 都需要配置为 systemd 驱动。
配置cgroups驱动
由于 kubeadm 把 kubelet 视为一个系统服务来管理,所以对基于 kubeadm 的安装, 我们推荐使用 systemd 驱动,不推荐 cgroupfs 驱动。
cgroups在K8s中的应用
kubelet作为kubernetes中的node agent,所有cgroup的操作都由其内部的containerManager模块实现,containerManager会通过cgroup将资源使用层层限制: container-> pod-> qos -> node。每一层都抽象出一种资源管理模型,通过这种方式提供了一种稳定的运行环境。如下图所示:
Conainer level cgroups
kubernetes对于容器级别的隔离其实是交由底层的runtime来负责的,例如docker, 当我们指定运行容器所需要资源的request和limit时,docker会为容器设置进程所运行cgroup的cpu.share, cpu.quota, cpu.period, mem.limit等指标来。
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_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
在 cgroup 的 CPU 子系统中,可以通过这两个配置,严格控制这个 cgroup 中的进程对 CPU 的使用量,保证使用的 CPU 资源不会超过 cfs_quota_us/cfs_period_us,也正好就是我们一开始申请的 limit 值。
对于cpu来说,如果没有指定 limit 的话,那么 cfs_quota_us 将会被设置为 -1,即没有限制。而如果 limit 和 request 都没有指定的话,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 子系统中的 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值,则只设置pod cgroup的cpu.share, 该pod在资源空闲的时候可以使用完node所有的资源,但是当资源紧张的时候无法获取到任何资源来执行,这也符合低优先级任务的定位:
pod<UID>/cpu.shares = sum(pod.spec.containers.resources.requests[cpu])
其实上面三种设置方式对应的就是三种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,分别为:
-
1、
Guaranteed:pod 的 requests 与 limits 设定的值相等; -
2、
Burstable:pod requests 小于 limits 的值且不为 0; -
3、
BestEffort:pod 的 requests 与 limits 均为 0;
三者的优先级如下所示,依次递增:
BestEffort -> Burstable -> Guaranteed
如果指定了 --cgroups-per-qos 也会为每个QoS也会对应一个cgroup,该功能默认开启,这样就可以利用cgroup来做一些QoS级别的资源统计,必要时也可以通过该cgroup限制某个QoS级别的pod能使用的资源总和。此时每个QoS cgroup相当于一个资源pool, 内部的pod可以共用pool中的资源,但是对于整个pool会进行一些资源的限制,避免在资源紧张时低优先级的pod抢占高优先级的pod的资源。
对于guaranteed级别的pod,因为pod本身已经指定了request和limit,拥有了足够的限制,无需再增加cgroup来约束。但是对于Burstable和BestEffort类型的pod,因为有的pod和容器没有指定资源限制,在极端条件下会无限制的占用资源,所以我们需要分别设置Burstable和BestEffort 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上面的资源按照使用对象分为三部分:
-
业务进程使用的资源, 即pods使用的资源;
-
kubernetes组件使用的资源,例如kubelet, docker;
-
系统组件使用的资源,例如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 hierarchy
最后整个整个cgroup hierarchy 如下:
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)
上述所有的操作在 kubelet 中是通过 containerManager来实现的, 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来实现。
不同 Qos 的本质区别
三种 Qos 在调度和底层表现上都不一样:
-
在调度时调度器只会根据 request 值进行调度;
-
当系统 OOM上时对于处理不同 OOMScore 的进程表现不同,OOMScore 是针对 memory 的,当宿主上 memory 不足时系统会优先 kill 掉 OOMScore 值低的进程,可以使用
$ cat /proc/$PID/oom_score查看进程的 OOMScore。OOMScore 的取值范围为 [-1000, 1000]。-
Guaranteed pod 的默认值为
-998 -
Burstable pod 的值为
2~999 -
BestEffort pod 的值为
1000,也就是说当系统 OOM 时,首先会 kill 掉 BestEffort pod 的进程,若系统依然处于 OOM 状态,然后才会 kill 掉 Burstable pod,最后是 Guaranteed pod;
-
-
cgroups 的配置不同,kubelet 为会三种 Qos 分别创建对应的 QoS level cgroups。
- Guaranteed Pod Qos 的 cgroup level 会直接创建在 RootCgroup/kubepods 下
- Burstable Pod Qos 的创建在 RootCgroup/kubepods/burstable 下
- BestEffort Pod Qos 的创建在 RootCgroup/kubepods/BestEffort 下
参考文档
Linux资源管理之cgroups简介
kubernetes 中 Qos 的设计与实现
Cgroup中的CPU资源控制
重学容器29: 容器资源限制之限制容器的CPU
深入解析 kubernetes 资源管理