暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

源码逐层分析Kubernetes 中的 Cgroup

云原生CTO 2021-10-16
2210

CTO

 
 

 
 


 

Go
Rust
Python
Istio
containerd
CoreDNS
Envoy
etcd
Fluentd
Harbor
Helm
Jaeger
Kubernetes
Open Policy Agent
Prometheus
Rook
TiKV
TUF
Vitess
Argo
Buildpacks
CloudEvents
CNI
Contour
Cortex
CRI-O
Falco
Flux
gRPC
KubeEdge
Linkerd
NATS
Notary
OpenTracing
Operator Framework
SPIFFE
SPIRE
  Thanos






源码逐层分析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 test — image=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 truedo 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
实现事件调度需要技巧,很值得学习。

感谢你的阅读

5.3 参考资料

参考地址 [1]

参考地址 [2]

参考地址 [3]

参考资料

[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,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论