1. 前言
在当前云原生技术的发展浪潮中,Kubernetes已经无可争议地成为领导者,其卓越的容器编排能力极大地方便了开发人员和运维团队。
然而,支撑Kubernetes的技术远比看上去的更加复杂。Cgroups作为 Linux内核的一个重要功能,被Kubernetes广泛应用于各级资源的管理。通过Cgroups,Kubernetes能够对CPU、内存等资源进行精细的控制和管理,从而显著提升了系统的可用性、稳定性和性能。
本文将深入讨论Kubernetes如何利用Cgroups管理资源,并提供一些推荐的配置和最佳实践。
2. Cgroups简介
Cgroups,全称为Control Groups,即控制组,允许对指定进程进行各种计算资源的限制,包括但不限于CPU使用量、内存使用量、IO设备的流量等。
Cgroups的优势在哪里?在Cgroups出现之前,任何进程都能创建数百甚至数千个线程,这可能会轻易耗尽一台计算机的所有CPU和内存资源。
然而,引入Cgroups技术后,我们便能对单个进程或一组进程的资源消耗进行限制。
Cgroups通过不同的子系统来限制不同的资源,其中每个子系统负责限制一种特定资源。这些子系统以类似的方式工作:将相关进程分配到一个控制组中,并通过树状结构进行管理,每个控制组都有其自定义的资源控制参数。
子系统 | 功能 |
cpu | 管理 cgroups 中进程的 CPU 使用率 |
cpuacct | 统计 cgroups 中进程的 CPU 使用情况 |
cpuset | 为 cgroups 中的任务分配独立的 CPU 和内存节点 |
memory | 管理 cgroups 中进程的内存使用量 |
blkio | 管理 cgroups 中进程的块设备 IO |
devices | 控制 cgroups 中进程对某些设备的访问权限 |
... | ... |
表1 常用 cgroups 子系统
在操作系统中,我们可以使用以下命令查看Cgroups挂载目录
$ mount | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
在 Cgroups根目录下,我们可以看到各子系统的文件
$ ll /sys/fs/cgroup
total 0
drwxr-xr-x 6 root root 0 Apr 16 05:13 blkio
lrwxrwxrwx 1 root root 11 Apr 16 05:13 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 Apr 16 05:13 cpuacct -> cpu,cpuacct
drwxr-xr-x 6 root root 0 Apr 16 05:13 cpu,cpuacct
drwxr-xr-x 5 root root 0 Apr 16 05:13 cpuset
drwxr-xr-x 5 root root 0 Apr 16 05:13 devices
drwxr-xr-x 4 root root 0 Apr 16 05:13 freezer
drwxr-xr-x 5 root root 0 Apr 16 05:13 hugetlb
drwxr-xr-x 6 root root 0 Apr 16 05:13 memory
lrwxrwxrwx 1 root root 16 Apr 16 05:13 net_cls -> net_cls,net_prio
drwxr-xr-x 4 root root 0 Apr 16 05:13 net_cls,net_prio
lrwxrwxrwx 1 root root 16 Apr 16 05:13 net_prio -> net_cls,net_prio
drwxr-xr-x 4 root root 0 Apr 16 05:13 perf_event
drwxr-xr-x 6 root root 0 Apr 16 05:13 pids
drwxr-xr-x 6 root root 0 Apr 16 05:13 systemd
3. Cgroups驱动介绍及选型
3.1 Cgroups 驱动介绍
Cgroups驱动主要有两种类型:
- systemd
- cgroupfs
systemd:
- systemd是一个系统和服务管理器,广泛用作许多现代Linux发行版的 init系统。
- systemd直接通过其控制接口管理cgroups,并通过工具(如 systemctl、systemd-cgls、systemd-cgtop等)提供cgroups的管理和监控功能。
cgroupfs:
- cgroupfs通过文件系统接口管理cgroups。
- cgroupfs将cgroups以文件系统的形式挂载至/sys/fs/cgroup目录,允许用户通过文件和目录操作来管理cgroups,这类似于对普通文件系统的操作。
systemd驱动与cgroupfs驱动的主要区别在于它们的管理接口。systemd 提供了一整套专用工具和接口来管理cgroups,而cgroupfs则通过文件系统接口进行管理。在实际应用中,尽管大多数现代Linux发行版采用 systemd来管理cgroups,cgroupfs仍然作为Linux内核支持的一个选项存在。
3.2 Kubernetes 中 Cgroups 驱动选择
Kubelet默认使用cgroupfs作为Cgroups驱动。然而,如果您通过 kubeadm部署Kubernetes 集群(版本 1.22 及以上),系统会默认采用 systemd作为Cgroups驱动。
我们不能让cgroupfs和systemd都作为我们的翅膀,必须二选一,那么我们应该如何选择呢?
其实也很简单,操作系统使用的是哪个Cgroups驱动,我们就选哪个。
因为如果一个系统中同时存在两种Cgroups管理器,假设kubelet和容器运行时(例如containerd)使用cgroupfs,而操作系统则使用systemd,那么在资源压力增大时,节点可能会变得不稳定。(修罗场)
对于大多数现代Linux发行版,如RHEL、CentOS、Fedora、Ubuntu、Debian等,它们都采用systemd来管理cgroups。因此,在这些Linux发行版上,kubelet和容器运行时应选择systemd作为Cgroups驱动。
4. Kubernetes 是如何使用 Cgroups 来管理资源的
在Kubernetes中,Cgroups用于管理不同级别的资源,这些级别从大到小依次为:
- Node
- Pod
- Container
接下来,我们将按照Node、Pod 和Container这三个级别,逐级描述 Kubernetes中的Cgroups管理方式。同时,我们也会讨论Kubernetes如何利用QoS (Quality of Service) 策略来优化资源分配和管理。
4.1 Node 级别
在Kubernetes集群的一个节点中,资源可分为三部分:
- 系统组件使用的资源:操作系统本身的进程所使用的资源,如 sshd, systemd等
- Kubernetes组件使用的资源:如 kubelet, 容器运行时(如 Docker, containerd)等
- Pod使用的资源:集群内Pod所使用的资源
如果我们不对这三个部分的资源使用进行管理,节点资源紧张时,可能会导致集群雪崩。
想象以下场景:
- 集群中的A节点上的Pod glutton吃光了节点上所有的资源
- 由于资源竞争,系统组件和Kubernetes组件因为抢不到资源而崩溃
- 节点A状态变为Not Ready
- Kubernetes尝试将节点A上的Pod glutton迁移到其他处于Ready状态的节点,例如节点B
- 节点B上的资源随后也被Pod glutton完全占用
- ...
本来集群和人都能跑,现在只有人能跑了。
那么,为了集群的健康,更为了人的健康(饭碗),聪明的你,或许已经想到了一个智慧的方法:提前为系统组件和Kubernetes组件预留资源,防止资源被恶意或异常的Pod完全占用。
巧了,Kubernetes的设计者也是这么想的。
4.1.1 Node 级别资源管理
在Node层面,Kubernetes允许我们修改以下配置来预留并限制资源:
- systemReserved
- kubeReserved
- evictionHard
- enforceNodeAllocatable
systemReserved
为系统组件预留的资源,如sshd等。
如果配置了此项,则需要同时配置systemReservedCgroup来指定系统组件的Cgroups路径。
同时,需要在kubelet启动前预先检查/创建systemReservedCgroup的路径,否则kubelet会启动失败。
示例:
systemReserved:
cpu: 100m
memory: 2048Mi
ephemeral-storage: 1Gi
pid: "1000"
systemReservedCgroup: /system.slice
kubeReserved
为Kubernetes组件预留的资源,如 kubelet,容器运行时等。
如果配置了此项,则需要同时配kubeReservedCgroup来指定Kubernetes 组件的Cgroups路径。
同时,需要在kubelet启动前预先检查/创建kubeReservedCgroup的路径,否则kubelet会启动失败。
示例:
kubeReserved:
cpu: 100m
memory: 2048Mi
ephemeral-storage: 1Gi
pid: "1000"
kubeReservedCgroup: /kube.slice
evictionHard
节点压力驱逐的条件,kubelet会监视集群节点上的内存、磁盘空间和文件系统inodes等资源。当这些资源中的一项或多项达到指定条件时,kubelet 会主动终止 pod以回收节点资源。
示例:
evictionHard:
imagefs.available: 10%
memory.available: 500Mi
nodefs.available: 5%
nodefs.inodesFree: 5%
enforceNodeAllocatable
指定需要限制的资源类型,默认为pods,即只会限制节点上pod使用的资源。
另外,也可以限制系统组件的资源(system-reserved)和Kubernetes 组件的资源(kube-reserved)。
为了确保核心组件的稳定运行,通常建议不要限制系统组件和 Kubernetes组件的资源,以避免这些核心组件出现资源不足的情况。
示例:
enforceNodeAllocatable:
-pods
经过上述配置后,节点上Pod所能使用的资源总量(Allocatable)为:
节点资源总量(Capacity) - evictionHard - kubeReserved - systemReserved
如下图:
图4.1 节点上 Pod 所能使用的资源总量
4.1.2 Node 级别的 Cgroups 层级
Cgroups层级目录如下(以 CPU 子系统为例)
图4.2 Cgroups 层级目录
在上图中:
- /system.slice:管理系统组件的资源使用
- /kube.slice:管理Kubernetes组件(如kubelet、容器运行时等)的资源使用
- /kubepods.slice:管理该节点上Kubernetes集群中Pod的资源使用
4.2 QoS 级别
QoS,全称Quality of Service,即服务质量。
Kubernetes对所有Pod进行分类,并根据它们的资源请求(Requests)和限制(Limits)的不同,将它们分配到特定的QoS级别。
在节点资源压力情况下,Kubernetes会根据QoS级别来决定哪些Pod应该被驱逐。
4.2.1 QoS 等级
Qo 等级共有 3 个:
- Guaranteed
- Burstable
- BestEffort
Guaranteed
Pod中所有的容器(不包含临时容器)都设置了CPU和内存资源的请求(Requests)和限制(Limits),且请求和限制的值相等。
Burstable
不满足Guaranteed等级的条件,但至少有一个容器设置了CPU和内存资源的请求(Requests)或限制(Limits)。
BestEffort
既不满足Guaranteed等级的条件,也不满足Burstable等级的条件,即 Pod中所有的容器都没有设置CPU和内存资源的请求(Requests)和限制(Limits)。
三种QoS等级的Pod重要性等级如下:
Guaranteed > Burstable > BestEffort
即当节点资源耗尽时,会首先驱逐BestEffort等级的Pod,然后是 Burstable等级的Pod,最后是Guaranteed等级的Pod。当此驱逐是由于资源压力而导致时,只有超出资源请求的Pod才是驱逐的候选者。
4.2.2 QoS 级别的 Cgroups 层级
Cgroups层级目录如下(以 CPU 子系统为例)
图4.3 Cgroups 层级目录
如图所示,Guaranteed等级的Pod的Cgroups目录在 最外层,Burstable 和BestEffort等级的Cgroups目录则是在子目录。
4.3 Pod 级别
4.3.1 Pod 级别的资源管理
一个Pod可以包含一个或多个容器。然而,Pod的总资源消耗并不仅限于其内部所有容器资源消耗的总和。实际上,Pod的资源使用还包括额外的开销,具体包括:
- Pod基础资源开销:每个Pod都会产生一些基础的资源开销,这主要包括网络配置和Pod内部运行的一些基础服务的资源消耗。
- Kubernetes系统容器:Kubernetes在每个Pod中运行一个特殊的容器,通常称为pause容器。这个容器作为所有其他容器的父容器,负责管理网络、卷挂载等任务。虽然pause容器消耗的资源相对较少,但在计算Pod的总资源消耗时,这部分资源也需要被考虑。
为了有效管理Pod的资源消耗,Kubernetes在Pod层面建立了一个 Cgroups层级,通过这种方式可以更精确地控制和监控Pod及其容器的资源使用情况。
4.3.2 Pod 级别的 Cgroups 层级
Cgroups层级目录如下(以 CPU 子系统为例)
图4.4 Cgroups 层级目录
4.4 Container级别
4.4.1 Container级别的资源管理
容器级别的资源隔离是由底层容器运行时(如 containerd)实现的。在 Kubernetes环境中,为容器配置资源请求(requests)和限制(limits)后,这些配置信息通过 kubelet 传递给容器运行时。这一过程依赖于 Kubernetes的容器运行时接口(Container Runtime Interface, CRI),CRI为kubelet与容器运行时之间的交互提供了标准化的协议和工具集。
容器运行时接收到资源配置信息后,利用Cgroups对每个容器的资源使用进行限制和隔离。通过配置Cgroups,容器运行时可以对CPU、内存等资源进行精确的控制。具体来说,它会配置cpu.shares、cpu.cfs_period_us、cpu.cfs_quota_us、memory.limit_in_bytes等参数,确保容器的资源消耗不会超出预设的限制。
总体而言,kubelet负责传递资源配置信息,而具体的资源隔离和限制则是由容器运行时通过配置cgroups实现的。这种设计使得Kubernetes能够高效且安全地管理容器资源。
4.4.2 CPU资源配置
- CPU 请求(request):通过Cgroups的cpu.shares 实现。当容器的 CPU请求设置为x millicores时,其cpu.shares 值计算为x * 1024 / 1000。例如,如果容器的CPU请求为1000 millicores(即相当于 1 个CPU核心),那么cpu.shares的值将为1024。这样做确保了即使在 CPU资源紧张的情况下,该容器也能获得至少相当于1个CPU核心的计算资源。
- CPU 限制(limit):通过Cgroups的cpu.cfs_period_us和 cpu.cfs_quota_us实现。在Kubernetes中,cpu.cfs_period_us被设置为100000微秒(即 100 毫秒),而cpu.cfs_quota_us根据CPU限制来计算。这两个参数共同作用,严格限制容器的CPU使用量,确保不会超过设定的限制。如果只指定了request而没有指定limit,那么cpu.cfs_quota_us将被设置为-1(表示没有限制)。如果既没有指定 request也没有指定limit,那么cpu.shares将被设置为2,意味着分配给该容器的CPU资源将是最低限度的。
4.4.3 内存资源配置
- 内存请求(request):Kubernetes 在进行调度决策时会考虑内存请求,这一参数虽然不会直接反映在容器级别的Cgroups配置中,但它确保了在节点上为容器分配了足够的内存资源。内存请求主要用于 Kubernetes 的调度过程,以保证节点具有为容器分配所需的内存资源。
- 内存限制(limit):内存限制是通过Cgroups的 memory.limit_in_bytes 参数来实现的,它定义了容器进程可以使用的最大内存量。若容器的内存使用超过了这一限制,将会触发OOM Killer,可能会导致容器被终止并重新启动。如果没有指定内存限制,则 memory.limit_in_bytes会被设置为一个极大的值,这通常意味着容器可以无限制地使用内存。
简而言之,Kubernetes通过Cgroups配置实现容器的资源隔离,确保每个容器根据其请求(request)和限制(limit)获得合理的资源分配。这一机制既提高了资源的利用效率,又防止了任何单一容器的过度资源消耗,从而保障了系统的整体稳定性。
4.4.4 Container 级别的 Cgroups 层级
Cgroups 层级目录如下(以 CPU 子系统为例)
图4.5 Cgroups 层级目录
最后,让我们一起看下CPU子系统的Cgroup目录结构。
图4.6 CPU子系统的Cgroups 层级目录
参考资料
[1] Kubernetes 官方文档(https://kubernetes.io/docs/home/)
作者:流月
来源-微信公众号:信也科技拍黑米
出处:https://mp.weixin.qq.com/s/BiMFUkZ47wo5znBhgAnuPA