源码逐层分析Kubernetes 中的 Cgroup

我们知道 Linux cgroup为 Docker 和 Kubernetes 等容器技术奠定了资源限制的基础。

Kubernetes是一种管理和编排大量容器的工具。并且在配置或部署 Pod 时,用户可以通过资源的请求和限制将资源分配给单个容器。当 pod 出现故障并且状态为“OOMKilled”时。背后隐藏着怎样的逻辑?哪个 Kubernetes 组件实际上在这里生效?

答案是Kubelet,它运行在Kubernetes集群的工作节点上,管理pod的生命周期,连接到CRI、CNI、CSI等运行时接口,通过cgroup按照配置向pod提供资源,并在使用超过时删除或OOMKilled pod。

Cgroup 简介

Cgroups 是 Control Groups 的缩写,由 Linux内核提供。用于限制、记录和隔离进程组使用的物理资源(CPU、内存、i/o)。
Cgroups可以根据不同的资源类型,分别形成由多个子Cgroups组成的树状结构,从上到下统一控制整个资源使用情况。

在这里插入图片描述
其中,CPU子系统使用cgroups虚拟文件系统的伪文件中独立存在的各个参数来限制进程访问CPU。
在这里插入图片描述

K8s中的Cgroups

为了避免容器之间的资源竞争或Kubernetes对主机的影响,kubelet组件将依赖cgroups来限制容器的资源使用。因为cgroups可以限制多个进程资源,而kubelet只能使用有限的资源,如CPU、内存、pid和hugetlb。

kubelet启动时,它会根据需要创建一个4层的cgroup树。

  • Node(root) cgroup
  • QoS cgroup
  • Pod cgroup
  • Container cgroup
    在这里插入图片描述
    kubelet中的所有cgroup操作都是由其内部的containerManager模块实现的,该模块通过cgroup(从下到上)对资源使用进行逐层限制

container-> pod-> qos -> node(root)

并将每一层抽象为一个资源管理模型,提供一个稳定的运行环境。

让我们自下而上地揭开k8s中cgroup实现的神秘面纱。如果您感兴趣,可以稍后深入研究源代码。

容器级别的Cgroup

容器级别cgroup通常是限制Pod资源最简单的方法,它通过配置在容器Pod启动时传递给容器运行时的资源请求和限制来工作。实际调度的场景如下:

  • 如果容器超出其内存限制,则可能终止该容器。如果它是可重新启动的,kubelet将重新启动它,就像处理任何其他类型的运行时故障一样。

  • 如果容器超出了它的内存请求,那么每当节点耗尽内存时,它的Pod很可能会被逐出。

  • 容器可能被允许,也可能不被允许在较长时间内超过其CPU限制。但是,它不会因为CPU占用过多而被杀死。

实现并不复杂。在generateContainerConfig中生成一个ContainerConfig,并在通过CRI生成容器时传递它。

generateContainerConfig源码实现:https://github.com/kubernetes/kubernetes/blob/f0b7ad3ee06c5168fef5fa4f01fe445ece595f89/pkg/kubelet/kuberuntime/kuberuntime_container.go#L297

运行时有两个驱动程序,一个是systemd,另一个是cgroupfs。

cgroupfs更简单。例如,为了限制内存和共享CPU,它会将PID写入相应的cgroup任务文件,然后将相应的资源写入相应的内存cgroup文件和CPU cgroup文件。

该系统本身提供了一种cgroup管理方法。所有的cgroup编写操作都必须通过systemd接口完成,手动修改cgroup文件是不可行的。

cgroupfs是Kubernetes默认的驱动程序。因此,如果选择systemd,则需要将kubelet和运行时配置为systemd驱动。

Pod级别的Cgroup

pod可以配置多个容器,最常见的是一个业务容器加上多个sidecar容器,如fluentd。然而,它的资源设置并不是简单地添加所有容器的资源限制和请求,因为有些资源是在容器之间共享的,每个pod都有一定的开销资源,比如由sandbox容器或docker containerd-shim(在1.22中删除)使用的资源。另外,pod在指定内存类型的卷时也会占用内存资源。那么如何方便对每个pod进行资源核算,并将使用的资源合理纳入管理?

Kubernetes引入了pod cgroup,它授予每个pod自己的cgroup。它创建了一个pod<pod。每个pod的UID> cgroup为- cgroups-per-qos(自1.6以来默认为true)。

那么Kubelet如何计算Pod所需的资源呢?

  • 计算所有pod容器的CPU请求/限制和内存限制,并将CPU请求/限制转换为cpuhares和cpuQuota。
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])
  • 只有pod cgroup的cpu。如果其中一个容器只指定请求而不指定限制,则将设置共享值,而不设置其限制值。
pod<UID>/cpu.shares = sum(pod.spec.containers.resources.requests[cpu])
  • 只设置cpu。当无容器指定请求值或限制值时共享,即pod在资源空闲时使用所有节点资源,在资源受限时不获取任何资源,符合低优先级原则
pod<UID>/cpu.shares = 2

如果有兴趣,可以在ResourceConfigForPod源码中阅读详细的计算。

ResourceConfigForPod源码:https://github.com/kubernetes/kubernetes/blob/cde45fb161c5a4bfa7cfe45dfd814f6cc95433f7/pkg/kubelet/cm/helpers_linux.go#L116

QoS级别的Cgroup

创建QoS级别cgroup时,同时使用与Pod级别cgroup相同的- cgroups-per-qos参数。三种资源计费方法分别对应不同的QoS级别。

  • Guaranteed(保证型)::由请求设置的值等于由限制设置的值。
  • Burstable(突增型):请求设置的值小于限制设置的值,但是!= 0。
  • BestEffort(尽力而为型):请求和限制设置的值都是0。

优先级是Guaranteed > Burstable > BestEffort

此时,可以将每个QoS cgroup视为一个资源池,内部pod可以在其中共享资源,并根据其优先级合理地获取资源。
在这里插入图片描述
保证型Pod Qos将直接在RootCgroup/kubepods中创建,因为内部Pod已经设置了请求和限制本身,不需要cgroup。

但是,Burstable Pod QoS是在RootCgroup/kubepods/Burstable中创建的,beststeffort Pod QoS是在RootCgroup/kubepods/beststeffort中创建的。由于不是所有的pod和容器都指定了资源限制,我们需要Burstable和bestfortcgroup来避免在极端情况下无限占用资源,并在这个cgroup中创建相应的pod。

Kubelet寻求提高资源效率,默认情况下没有对Qos设置资源限制,以便Burstable和BestEffort pod可以在需要时使用足够的空闲资源。但是,当有保证的pod需要资源时,低优先级pod也需要及时释放资源。

那么如何?

对于CPU等可压缩资源,可以通过CPU CFS共享进行控制,按比例将资源分配给每个QoS pod,确保在CPU资源有限的情况下,每个pod都能获得自己申请的资源。

下面列出了CPU和内存限制的计算。
Burstable cgroup

ROOT/burstable/cpu.shares = max(sum(Burstable pods cpu requests), 2)
ROOT/burstable/memory.limit_in_bytes = burstableLimit := allocatable — (qosMemoryRequests[v1.PodQOSGuaranteed] * percentReserve / 100

BestEffort cgroup

ROOT/besteffort/cpu.shares = 2
ROOT/besteffort/memory.limit_in_bytes = bestEffortLimit := burstableLimit — (qosMemoryRequests[v1.PodQOSBurstable] * percentReserve / 100)

启动这些cgroup后,kubelet将调用UpdateCgroups方法定期更新这三个组的cgroup资源限制,以尽可能快地响应更改。

启动started cgroup源码:https://github.com/kubernetes/kubernetes/blob/4d78db54a58e250b049c4fe17ac484e5c3ec662d/pkg/kubelet/cm/qos_container_manager_linux.go#L82

UpdateCgroups方法源码:https://github.com/kubernetes/kubernetes/blob/4d78db54a58e250b049c4fe17ac484e5c3ec662d/pkg/kubelet/cm/qos_container_manager_linux.go#L306

节点Node级别的Cgroup

对于节点级资源,Kubernetes根据使用对象将节点资源分为三类。

  • 业务流程使用的资源,即pod使用的资源。
  • Kubernetes组件使用的资源,如kubelet和docker。
  • 系统组件使用的资源,如logind、journald和其他进程。
    一般来说,第二组和第三组的资源利用是相对稳定的,而第一组的资源利用需要加以限制,以确保系统在极端条件下保持稳定的服务。
    Kubelet containerManager将在Starting func的根cgroup下创建一个名为kubepods的子cgroup。然后将(createNodeAllocatableCgroups)中的可分配资源写入kubepods中对应的cgroup文件,例如kubepods/cpu.share。此外,节点的所有pod cgroup都会存储在这个根cgroup下,从而限制节点的所有pod资源。

Starting func源码 : https://github.com/kubernetes/kubernetes/blob/2face135c730282320d7d7c9873e190e483bce6f/pkg/kubelet/cm/container_manager_linux.go#L608

createNodeAllocatableCgroups源码:https://github.com/kubernetes/kubernetes/blob/2face135c730282320d7d7c9873e190e483bce6f/pkg/kubelet/cm/node_container_manager_linux.go#L44

探索Cgroup文件

当我们迷失在如此多的概念中时,实践可以提高我们的理解。让我们通过将cgroup信息写入实时操作

我使用了kind工具,它可以通过本地docker构建一个Kubernetes集群。

首先,创建集群。

kind工具部署kubernetes集群:https://kind.sigs.k8s.io/docs/user/quick-start/
在这里插入图片描述

其次,使用创建的上下文。
kubectl cluster-info — context kind-cgroup-test
在这里插入图片描述
然后启动一个test pod

kubectl run testimage=busybox — limits "memory=100Mi"command — /bin/sh -c "while true; do sleep 2; done"

并打印当前pod资源。

kubectl get pods test -o=jsonpath={.spec.containers[0].resources}{}%

现在,登录到集群并找到当前docker进程。
在这里插入图片描述
然后执行docker exec -t -i 968f6927a1b6 /bin/bash
找出运行pod的进程。

ps aux | grep ‘/bin/sh’1606 ? Ss 0:00 /bin/sh -c while true; do sleep 2; done

然后,直接检查cgroup文件
cat /proc/1606/cgroup
在这里插入图片描述
这里我们可以看到真正的cgroup文件所在的路径,格式与上面提到的pod<pod. uid >相同。例如,与内存相关的文件

5:memory:/docker/968f6927a1b6777bbc42b9b5e13ff772dd15cb1d4ff048b582756e3d0f015703/kubepods/besteffort/pod*

现在注意memory.limit文件

ls -al /sys/fs/cgroup/memory/kubepods/burstable/pod05206322-d0f1–4ad9–90a6–7f59a9801fe3/

在这里插入图片描述
检查内存时显示值与设定值(100Mi)一致。limit_in_bytes文件。

cat /sys/fs/cgroup/memory/kubepods/burstable/pod05206322-d0f1–4ad9–90a6–7f59a9801fe3/memory.limit_in_bytes104857600

当我们在没有这些限制的情况下重新测试时,该值会变得更大,这是kubelet的默认值。

结语

cgroup技术为Containters奠定了基础,Kubernetes充分利用了该技术的优势。Kubernetes通过合理的资源分配和利用,保证了业务(Pod)和自身的可用性。此外,在源代码中通过Channel实现事件调度需要技巧,很值得学习。

[1]
参考地址: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#troubleshooting

[2]
参考地址: https://kubernetes.io/docs/setup/production-environment/container-runtimes/

[3]
参考地址: https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/

原文地址:【云原生CTO】公众号

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐