Kubernetes cgroups详解
发布网友
发布时间:2022-10-07 03:27
我来回答
共1个回答
热心网友
时间:2023-10-25 09:57
cgroups的一个设计目标是为不同的应用情况提供统一的接口,从控制单一进程到操作系统层虚拟化(像OpenVZ,Linux-VServer,LXC)。cgroups提供:
每个 cgroup 子系统代表一种资源,如针对某个 cgroup 的处理器时间或者 pid 的数量,也叫进程数。Linux 内核提供对以下 12 种 cgroup 子系统的支持:
这里面每一个子系统都需要与内核的其他模块配合来完成资源的控制,比如对 cpu 资源的*是通过进程调度模块根据 cpu 子系统的配置来完成的;对内存资源的*则是内存模块根据 memory 子系统的配置来完成的,而对网络数据包的控制则需要 Traffic Control 子系统来配合完成。本文不会讨论内核是如何使用每一个子系统来实现资源的*,而是重点放在内核是如何把 cgroups 对资源进行*的配置有效的组织起来的,和内核如何把cgroups 配置和进程进行关联的,以及内核是如何通过 cgroups 文件系统把cgroups的功能暴露给用户态的。
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 Scheler完全公平调度器,是Linux内核的一部分,负责进程调度。
cpu.cfs_quota_us/cpu.cfs_period_us决定cpu控制组中所有进程所能使用CPU资源的最大值,而cpu.shares决定了cpu控制组间可用CPU的相对比例,这个比例只有当主机上的CPU完全被打满时才会起作用。
cpuacct子系统(CPU accounting)会自动生成报告来显示cgroup中任务所使用的CPU资源。报告有两大类: cpuacct.stat和cpuacct.usage。
cpuset主要是为了numa使用的,numa技术将CPU划分成不同的node,每个node由多个CPU组成,并且有独立的本地内存、I/O等资源(硬件上保证)。可以使用numactl查看当前系统的node信息。
memory 子系统 自动生成 cgroup 任务使用内存资源的报告,并限定这些任务所用内存的大小。
容器可以通过设置 memory.swappiness 参数来决定是否使用 swap 空间。
Linux通过文件的方式,将cgroups的功能和配置暴露给用户,这得益于Linux的虚拟文件系统(VFS)。VFS将具体文件系统的细节隐藏起来,给用户态提供一个统一的文件系统API接口,cgroups和VFS之间的链接部分,称之为cgroups文件系统。
比如挂在 cpu、cpuset、memory 三个子系统到 /cgroups/cpu_mem 目录下:
其中-t选项指定文件系统类型为cgroup类型,-o指定本次创建的cgroup实例与cpu和momory子系统(或资源)关联,cpu_momory指定了当前cgroup实例在整个cgroup树中所处的层级名称,最后的路径为文件系统挂载点。
runtime 有两种 cgroup 驱动:一种是 systemd ,另外一种是 cgroupfs :
kubernetes 中默认 kubelet 的 cgroup 驱动就是 cgroupfs,若要使用 systemd,则必须将 kubelet 以及 runtime 都需要配置为 systemd 驱动。
由于 kubeadm 把 kubelet 视为一个系统服务来管理,所以对基于 kubeadm 的安装, 我们推荐使用 systemd 驱动,不推荐 cgroupfs 驱动。
配置 cgroup 驱动
kubelet作为kubernetes中的node agent,所有cgroup的操作都由其内部的containerManager模块实现,containerManager会通过cgroup将资源使用层层*: container-> pod-> qos -> node 。每一层都抽象出一种资源管理模型,通过这种方式提供了一种稳定的运行环境。如下图所示:
kubernetes对于容器级别的隔离其实是交由底层的runtime来负责的,例如docker, 当我们指定运行容器所需要资源的request和limit时,docker会为容器设置进程所运行cgroup的cpu.share, cpu.quota, cpu.period, mem.limit等指标来。
首先是 CPU 资源,我们先看一下 CPU request。CPU request 是通过 cgroup 中 CPU 子系统中的 cpu.shares 配置来实现的。当你指定了某个容器的 CPU request 值为 x millicores 时,kubernetes 会为这个 container 所在的 cgroup 的 cpu.shares 的值指定为 x * 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 配置两条信息:
在 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 request 信息并不会在 container level cgroup 中有体现。kubernetes 最终只会根据 memory limit 的值来配置 cgroup 的。
在这里 kubernetes 使用的 memory cgroup 子系统中的 memory.limit_in_bytes 配置来实现的。配置方式如下:
memory 子系统中的 memory.limit_in_bytes 配置,可以*一个 cgroup 中的所有进程可以申请使用的内存的最大量,如果超过这个值,那么根据 kubernetes 的默认配置,这个容器会被 OOM killed,容器实例就会发生重启。
对于内存来说,如果没有 limit 的指定的话,memory.limit_in_bytes 将会被指定为一个非常大的值,一般是 2^64 ,可见含义就是不对内存做出*。
一个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值。
其实上面三种设置方式对应的就是三种QoS pod。这样设置pod level cgourp可以确保在合理指定容器资源时能够防止资源的超量使用,如果未指定则可以使用到足够多的可用资源。 每次启动pod时kubelet就会同步对应的pod level cgroup。
kubernetes中会将所有的pod按照资源request, limit设置分为不同的QoS classes, 从而拥有不同的优先级。QoS(Quality of Service) 即服务质量,QoS 是一种控制机制,它提供了针对不同用户或者不同数据流采用相应不同的优先级,或者是根据应用程序的要求,保证数据流的性能达到一定的水准。kubernetes 中有三种 QoS,分别为:
三者的优先级如下所示,依次递增:
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的资源使用*如下:
对于不可压缩资源内存,要满足“高优先级pod使用资源时及时释放低优先级的pod占用的资源”就比较困难了,kubelet只能通过资源预留的机制,为高优先级的pod预留一定的资源,该特性默认关闭,用户可以通过开启 QOSReserved 特征门控(默认关闭),并设置 --qos-reserved 参数来预留的资源比例,例如 --qos-reserved=memory=50% 表示预留50%高优先级request的资源值,当前只支持memory , 此时qos cgroups的*如下:
同时根据 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层面的资源,kubernetes会将一个node上面的资源按照使用对象分为三部分:
通常情况下,我们为提高集群资源利用率,会进行适当超配资源,如果控制不当,业务进程可能会占用完整个node的资源,从而使的第二,三部分核心的程序所使用的资源受到压制,从而影响到系统稳定性,为避免这样的情况发生,我们需要合理*pods的资源使用,从而 为系统组件等核心程序预留足够的资源 ,保证即使在极端条件下有充足的资源来使用。
kubelet会将所有的pod都创建一个 kubepods 的cgroup下,通过该cgroup来*node上运行的pod最大可以使用的资源。该cgroup的资源*取值为:
其中 kube-reserved 是为kubernetes组件提供的资源预留, system-reserved 是为系统组件预留的资源,分别通过 --kube-reserved , --system-reserved 来指定,例如--kube-reserved=cpu=100m,memory=100Mi。
除了指定预留给系统运行的资源外,如果要*系统运行的资源,可以通过 --enforce-node-allocatable 来设置,该flag指定需要执行*的资源类型,默认值为pods,即通过上述kubepods来*pods的使用资源,此外还支持*的资源类型有:
如果需要指定多种类型,通过逗号分割枚举即可,注意如果开启了 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使用的资源量为:
这也是调度器进行调度时所使用的资源值。
除了上述提到的cgroup设置外,kubelet中还有一些对于单个组件的cgroup设置, 例如:
以上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 如下:
上述所有的操作在 kubelet 中是通过 containerManager 来实现的, containerManager 启动的时候首先会 setupNode 初始化各种cgroup,具体包括:
上述所有更新cgroup的操作都会利用一个 cgroupManager 来实现。
三种 Qos 在调度和底层表现上都不一样:
Linux资源管理之cgroups简介
kubernetes 中 Qos 的设计与实现
Cgroup中的CPU资源控制
重学容器29: 容器资源*之*容器的CPU
深入解析 kubernetes 资源管理