kubernetes kubelet组件中cgroup的层层\"戒备\"

kong000dao0 2019-10-27

cgroup是linux内核中用于实现资源使用限制和统计的模块,docker的风靡一时少不了cgroup等特性的支持。kubernetes作为容器编排引擎,除了借助docker进行容器进程的资源管理外,还提供了一些更加高级的资源管理功能,以提高资源利用率和更加稳定的程序运行环境,其中必然少不了cgroup这类资源管控技术的应用,那么kubernetes是如何使用cgroup哪? 如果仔细观察kubelet中关于cgroup的配置,就会发现这些配置参数多达十几个,错综复杂,怎样才能合理的配置这些参数哪?

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等指标来,具体cgroup中各个参数含义处不再分析,感兴趣的同学可以查阅相关资料。

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 = 2

其实上面三种设置方式对应的就是三种QoS pod。 这样设置pod level cgourp可以确保在合理指定容器资源时能够防止资源的超量使用, 如果未指定则可以使用到足够多的可用资源。 每次启动pod时kubelet就会同步对应的pod level cgroup。

QoS level cgroup

kubernetes中会将所有的pod按照资源request, limit设置分为不同的QoS classes, 从而拥有不同的优先级。如果指定了--cgroups-per-qos也会为每个QoS也会对应一个cgroup,该功能默认开启,这样就可以利用cgroup来做一些QoS级别的资源统计,必要时也可以通过该cgroup限制某个QoS级别的pod能使用的资源总和。此时每个QoS cgroup相当于一个资源pool, 内部的pod可以共用pool中的资源,但是对于整个pool会进行一些资源的限制,避免在资源紧张时低优先级的pod抢占高优先级的pod的资源。对于guaranteed级别的pod,因为pod本身已经指定了request和limit,拥有了足够的限制,无需再增加cgroup来约束。但是对于BurstableBestEffort类型的pod,因为有的pod和容器没有指定资源限制,在极端条件下会无限制的占用资源,所以我们需要分别设置BurstableBestEffortcgroup, 然后将对应的pod都创建在该cgroup下。kubelet希望尽可能提高资源利用率,让BurstableBestEffort类型的pod在需要的时候能够使用足够多的空闲资源,所以默认并不会为该QoS设置资源的limit。但是也需要保证当高优先级的pod需要使用资源时,低优先级的pod能够及时将资源释放出来:对于可压缩的资源例如CPU, kubelet会通过CPU CFS shares来控制,当CPU资源紧张时通过CFS share来将资源按照比例分配给各个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预留一定的资源,该特性默认关闭,用户可以通过--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上面的资源按照使用对象分为三部分: 

  1. 业务进程使用的资源, 即pods使用的资源; 
  2. kubernetes组件使用的资源,例如kubelet, docker; 
  3. 系统组件使用的资源,例如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-reservedkube-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路径下,默认是/,如果开启--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 如下:

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-reservedsystem-reserved三个cgroup的资源使用, 启动qosContainerManager来定期同步各个QoS class的资源使用,会在后台不断同步kubelet,docker,system cgroup。在每个pod启动/退出时候会调用podContainerManager来创建/删除pod级别的cgroup并调用 UpdateQoSCgroups 来更新QoS级别的cgroup。 上述所有更新cgroup的操作都会利用一个cgroupManager来实现。

Summary

可以看出kubelet是将cgroup使用到了极致的地步,通过层层限制提高node的隔离性、稳定性、可观测性。

相关推荐