kubernetes 中 Qos 的設(shè)計與實現(xiàn)

kubernetes 中的 Qos

QoS(Quality of Service) 即服務(wù)質(zhì)量力九,QoS 是一種控制機制,它提供了針對不同用戶或者不同數(shù)據(jù)流采用相應(yīng)不同的優(yōu)先級,或者是根據(jù)應(yīng)用程序的要求亭螟,保證數(shù)據(jù)流的性能達到一定的水準(zhǔn)残家。kubernetes 中有三種 Qos榆俺,分別為:

  • 1、Guaranteed:pod 的 requests 與 limits 設(shè)定的值相等坞淮;
  • 2茴晋、Burstable:pod requests 小于 limits 的值且不為 0;
  • 3回窘、BestEffort:pod 的 requests 與 limits 均為 0诺擅;

三者的優(yōu)先級如下所示,依次遞增:

BestEffort -> Burstable -> Guaranteed

不同 Qos 的本質(zhì)區(qū)別

三種 Qos 在調(diào)度和底層表現(xiàn)上都不一樣:

  • 1啡直、在調(diào)度時調(diào)度器只會根據(jù) request 值進行調(diào)度烁涌;
  • 2、二是當(dāng)系統(tǒng) OOM上時對于處理不同 OOMScore 的進程表現(xiàn)不同酒觅,OOMScore 是針對 memory 的撮执,當(dāng)宿主上 memory 不足時系統(tǒng)會優(yōu)先 kill 掉 OOMScore 值高的進程,可以使用 $ cat /proc/$PID/oom_score 查看進程的 OOMScore舷丹。OOMScore 的取值范圍為 [-1000, 1000]抒钱,Guaranteed pod 的默認值為 -998,Burstable pod 的值為 2~999颜凯,BestEffort pod 的值為 1000谋币,也就是說當(dāng)系統(tǒng) OOM 時,首先會 kill 掉 BestEffort pod 的進程装获,若系統(tǒng)依然處于 OOM 狀態(tài)瑞信,然后才會 kill 掉 Burstable pod,最后是 Guaranteed pod穴豫;
  • 3凡简、三是 cgroup 的配置不同,kubelet 為會三種 Qos 分別創(chuàng)建對應(yīng)的 QoS level cgroups精肃,Guaranteed Pod Qos 的 cgroup level 會直接創(chuàng)建在 RootCgroup/kubepods 下秤涩,Burstable Pod Qos 的創(chuàng)建在 RootCgroup/kubepods/burstable 下,BestEffort Pod Qos 的創(chuàng)建在 RootCgroup/kubepods/BestEffort 下司抱,上文已經(jīng)說了 root cgroup 可以通過 $ mount | grep cgroup看到筐眷,在 cgroup 的每個子系統(tǒng)下都會創(chuàng)建 Qos level cgroups, 此外在對應(yīng)的 QoS level cgroups 還會為 pod 創(chuàng)建 Pod level cgroups习柠;

啟用 Qos 和 Pod level cgroup

在 kubernetes 中為了限制容器資源的使用匀谣,避免容器之間爭搶資源或者容器影響所在的宿主機照棋,kubelet 組件需要使用 cgroup 限制容器資源的使用量,cgroup 目前支持對進程多種資源的限制武翎,而 kubelet 只支持限制 cpu烈炭、memory、pids宝恶、hugetlb 幾種資源符隙,與此資源有關(guān)的幾個參數(shù)如下所示:
--cgroups-per-qos:啟用后會為每個 pod 以及 pod 對應(yīng)的 Qos 創(chuàng)建 cgroups 層級樹,默認啟用垫毙;
--cgroup-root:指定 root cgroup霹疫,如果不指定默認為“”,若為默認值則直接使用 root cgroup dir综芥,在 node 上執(zhí)行 $ mount | grep cgroup 可以看到 cgroup 所有子系統(tǒng)的掛載點丽蝎,這些掛載點就是 root cgroup;
--cpu-manager-policy:默認為 "none"毫痕,即默認不開啟 ,支持使用 "static"征峦,開啟后可以支持對 Guaranteed Pod 進行綁核操作,綁核的主要目的是為了高效使用 cpu cache 以及內(nèi)存節(jié)點消请;
--kube-reserved:為 kubernetes 系統(tǒng)組件設(shè)置預(yù)留資源值栏笆,可以設(shè)置 cpu、memory臊泰、ephemeral-storage蛉加;
--kube-reserved-cgroup:指定 kube-reserved 的 cgroup dir name,默認為 “/kube-reserved”缸逃;
--system-reserved:為非 kubernetes 組件設(shè)置預(yù)留資源值针饥,可以設(shè)置 cpu、memory需频、ephemeral-storage丁眼;
--system-reserved-cgroup:設(shè)置 system-reserved 的 cgroup dir name,默認為 “/system-reserved”昭殉;
--qos-reserved:Alpha feature苞七,可以通過此參數(shù)為高優(yōu)先級 pod 設(shè)置預(yù)留資源比例,目前只支持預(yù)留 memory挪丢,使用前需要開啟 QOSReserved feature gate蹂风;

當(dāng)啟用了 --cgroups-per-qos 后,kubelet 會為不同 Qos 創(chuàng)建對應(yīng)的 level cgroups乾蓬,在 Qos level cgroups 下也會為 pod 創(chuàng)建對應(yīng)的 pod level cgroups惠啄,在 pod level cgroups 下最終會為 container 創(chuàng)建對應(yīng)的 level cgroups,從 Qos --> pod --> container,層層限制每個 level cgroups 的資源使用量撵渡。

配置 cgroup driver

runtime 有兩種 cgroup 驅(qū)動:一種是 systemd融柬,另外一種是 cgroupfs

  • cgroupfs 比較好理解,比如說要限制內(nèi)存是多少姥闭、要用 CPU share 為多少丹鸿,其實直接把 pid 寫入到對應(yīng)cgroup task 文件中,然后把對應(yīng)需要限制的資源也寫入相應(yīng)的 memory cgroup 文件和 CPU 的 cgroup 文件就可以了棚品;
  • 另外一個是 systemd 的 cgroup 驅(qū)動,這個驅(qū)動是因為 systemd 本身可以提供一個 cgroup 管理方式廊敌。所以如果用 systemd 做 cgroup 驅(qū)動的話铜跑,所有的寫 cgroup 操作都必須通過 systemd 的接口來完成,不能手動更改 cgroup 的文件骡澈;

kubernetes 中默認 kubelet 的 cgroup 驅(qū)動就是 cgroupfs锅纺,若要使用 systemd,則必須將 kubelet 以及 runtime 都需要配置為 systemd 驅(qū)動肋殴。

關(guān)于 cgroupfs 與 systemd driver 的區(qū)別可以參考 k8s 官方文檔:container-runtimes/#cgroup-drivers囤锉,或者 runc 中的實現(xiàn) github.com/opencontainers/runc/libcontainer/cgroups

kubernetes 中的 cgroup level

kubelet 啟動后會在 root cgroup 下面創(chuàng)建一個叫做 kubepods 子 cgroup护锤,kubelet 會把本機的 allocatable 資源寫入到 kubepods 下對應(yīng)的 cgroup 文件中官地,比如 kubepods/cpu.share,而這個 cgroup 下面也會存放節(jié)點上面所有 pod 的 cgroup烙懦,以此來達到限制節(jié)點上所有 pod 資源的目的驱入。在 kubepods cgroup 下面,kubernetes 會進一步再分別創(chuàng)建兩個 QoS level cgroup氯析,名字分別叫做 burstablebesteffort亏较,這兩個 QoS level 的 cgroup 是作為各自 QoS 級別的所有 Pod 的父 cgroup 來存在的,在為 pod 創(chuàng)建 cgroup 時掩缓,首先在對應(yīng)的 Qos cgroup 下創(chuàng)建 pod level cgroup雪情,然后在 pod level cgroup 繼續(xù)創(chuàng)建對應(yīng)的 container level cgroup,對于 Guaranteed Qos 對應(yīng)的 pod 會直接在 kubepods 同級的 cgroup 中創(chuàng)建 pod cgroup你辣。

目前 kubernetes 僅支持 cpu巡通、memory、pids 绢记、hugetlb 四個 cgroup 子系統(tǒng)扁达。

當(dāng) kubernetes 在收到一個 pod 的資源申請信息后通過 kubelet 為 pod 分配資源,kubelet 基于 pod 申請的資源以及 pod 對應(yīng)的 QoS 級別來通過 cgroup 機制最終為這個 pod 分配資源的蠢熄,針對每一種資源跪解,它會做以下幾件事情:

  • 首先判斷 pod 屬于哪種 Qos,在對應(yīng)的 Qos level cgroup 下對 pod 中的每一個容器在 cgroup 所有子系統(tǒng)下都創(chuàng)建一個 pod level cgroup 以及 container level cgroup,并且 pod level cgroup 是 container level cgroup 的父 cgroup叉讥,Qos level cgroup 在 kubelet 初始化時已經(jīng)創(chuàng)建完成了窘行;
  • 然后根據(jù) pod 的資源信息更新 QoS level cgroup 中的值;
  • 最后會更新 kubepods level cgroup 中的值图仓;

對于每一個 pod 設(shè)定的 requests 和 limits罐盔,kubernetes 都會轉(zhuǎn)換為 cgroup 中的計算方式,CPU 的轉(zhuǎn)換方式如下所示:

  • cpu.shares = (cpu in millicores * 1024) / 1000
  • cpu.cfs_period_us = 100000 (i.e. 100ms)
  • cpu.cfs_quota_us = quota = (cpu in millicores * 100000) / 1000
  • memory.limit_in_bytes

CPU 最終都會轉(zhuǎn)換為以微秒為單位救崔,memory 會轉(zhuǎn)換為以 bytes 為單位惶看。

以下是 kubernetes 中的 cgroup level 的一個示例,此處僅展示 cpu六孵、memory 對應(yīng)的子 cgroup:

.
|-- blkio
|-- cpu -> cpu,cpuacct
|-- cpu,cpuacct
|   |-- init.scope
|   |-- kubepods
|   |   |-- besteffort
|   |   |-- burstable
|   |   `-- podd15c4b83-c250-4f1e-94ff-8a4bf31c6f25
|   |-- system.slice
|   `-- user.slice
|-- cpuacct -> cpu,cpuacct
|-- cpuset
|   |-- kubepods
|   |   |-- besteffort
|   |   |-- burstable
|   |   `-- podd15c4b83-c250-4f1e-94ff-8a4bf31c6f25
|-- devices
|-- hugetlb
|-- memory
|   |-- init.scope
|   |-- kubepods
|   |   |-- besteffort
|   |   |-- burstable
|   |   `-- podd15c4b83-c250-4f1e-94ff-8a4bf31c6f25
|   |-- system.slice
|   |   |-- -.mount
|   `-- user.slice
|-- net_cls -> net_cls,net_prio
|-- net_cls,net_prio
|-- net_prio -> net_cls,net_prio
|-- perf_event
|-- pids
`-- systemd
image

例如纬黎,當(dāng)創(chuàng)建資源如下所示的 pod:

spec:
  containers:
  - name: nginx
    image: nginx:latest
    imagePullPolicy: IfNotPresent
    resources:
      requests:
        cpu: 250m
        memory: 1Gi
      limits:
        cpu: 500m
        memory: 2Gi

首先會根據(jù) pod 的 Qos 該 pod 為 burstable 在其所屬 Qos 下創(chuàng)建 ROOT/kubepods/burstable/pod<UID>/container<UID> 兩個 cgroup level,然后會更新 pod 的父 cgroup 也就是 burstable/ cgroup 中的值劫窒,最后會更新 kubepods cgroup 中的值本今,下面會針對每個 cgroup level 一一進行解釋。

Container level cgroups

在 Container level cgroups 中主巍,kubelet 會根據(jù)上述公式將 pod 中每個 container 的資源轉(zhuǎn)換為 cgroup 中的值并寫入到對應(yīng)的文件中冠息。

/sys/fs/cgroup/cpu/kubepods/burstable/pod<UID>/container<UID>/cpu.shares = 256
/sys/fs/cgroup/cpu/kubepods/burstable/pod<UID>/container<UID>/cpu.cfs_quota_us = 50000
/sys/fs/cgroup/memory/kubepods/burstable/pod<UID>/container<UID>/memory.limit_in_bytes = 104857600

Pod level cgroups

在創(chuàng)建完 container level 的 cgroup 之后,kubelet 會為同屬于某個 pod 的 containers 創(chuàng)建一個 pod level cgroup孕索。為何要引入 pod level cgroup逛艰,主要是基于以下幾點原因:

  • 方便對 pod 內(nèi)的容器資源進行統(tǒng)一的限制;
  • 方便對 pod 使用的資源進行統(tǒng)一統(tǒng)計檬果;

對于不同 Pod level cgroups 的設(shè)置方法如下所示:

Guaranteed Pod QoS
pod<UID>/cpu.shares = sum(pod.spec.containers.resources.requests[cpu])
pod<UID>/cpu.cfs_period_us = 100000
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])
Burstable Pod QoS
pod<UID>/cpu.shares = sum(pod.spec.containers.resources.requests[cpu])
pod<UID>/cpu.cfs_period_us = 100000
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])
BestEffort Pod QoS
pod<UID>/cpu.shares = 2
pod<UID>/cpu.cfs_quota_us = -1

cpu.shares 指定了 cpu 可以使用的下限瓮孙,cpu 的上限通過使用 cpu.cfs_period_us + cpu.cfs_quota_us 兩個參數(shù)做動態(tài)絕對配額,兩個參數(shù)的意義如下所示:

  • cpu.cfs_period_us:指 cpu 使用時間的周期統(tǒng)計选脊;
  • cpu.cfs_quota_us:指周期內(nèi)允許占用的 cpu 時間(指單核的時間, 多核則需要在設(shè)置時累加) 杭抠;

container runtime 中 cpu.cfs_period_us 的值默認為 100000。若 kubelet 啟用了 --cpu-manager-policy=static 時恳啥,對于 Guaranteed Qos偏灿,如果它的 request 是一個整數(shù)的話,cgroup 會同時設(shè)置 cpuset.cpuscpuset.mems 兩個參數(shù)以此來對它進行綁核钝的。

如果 pod 指定了 requests 和 limits翁垂,kubelet 會按以上的計算方式為 pod 設(shè)置資源限制,如果沒有指定 limit 的話硝桩,那么 cpu.cfs_quota_us 將會被設(shè)置為 -1沿猜,即沒有限制。而如果 limit 和 request 都沒有指定的話碗脊,cpu.shares 將會被指定為 2啼肩,這個是 cpu.shares 允許指定的最小數(shù)值了,可見針對這種 pod,kubernetes 只會給它分配最少的 cpu 資源祈坠。而對于內(nèi)存來說害碾,如果沒有 limit 的指定的話,memory.limit_in_bytes 將會被指定為一個非常大的值赦拘,一般是 2^64 慌随,可見含義就是不對內(nèi)存做出限制。

針對上面的例子躺同,其 pod level cgroups 中的配置如下所示:

pod<UID>/cpu.shares = 102
pod<UID>/cpu.cfs_quota_us = 20000

QoS level cgroups

上文已經(jīng)提到了 kubelet 會首先創(chuàng)建 kubepods cgroup阁猜,然后會在 kubepods cgroup 下面再分別創(chuàng)建 burstable 和 besteffort 兩個 QoS level cgroup,那么這兩個 QoS level cgroup 存在的目的是什么笋籽?為什么不為 guaranteed Qos 創(chuàng)建 cgroup level蹦漠?

首先看一下三種 QoS level cgroups 的設(shè)置方法,對于 guaranteed Qos 因其直接使用 root cgroup车海,此處只看另外兩種的計算方式:

Burstable cgroup

ROOT/burstable/cpu.shares = max(sum(Burstable pods cpu requests), 2)
ROOT/burstable/memory.limit_in_bytes =
    Node.Allocatable - {(summation of memory requests of `Guaranteed` pods)*(reservePercent / 100)}

BestEffort cgroup

ROOT/besteffort/cpu.shares = 2
ROOT/besteffort/memory.limit_in_bytes =
    Node.Allocatable - {(summation of memory requests of all `Guaranteed` and `Burstable` pods)*(reservePercent / 100)}

首先第一個問題,所有 guaranteed 級別的 pod 的 cgroup 直接位于 kubepods 這個 cgroup 之下隘击,和 burstable侍芝、besteffort QoS level cgroup 同級,主要原因在于 guaranteed 級別的 pod 有明確的資源申請量(request)和資源限制量(limit)埋同,所以并不需要一個統(tǒng)一的 QoS level 的 cgroup 進行管理或限制州叠。

針對 burstable 和 besteffort 這兩種類型的 pod,在默認情況下凶赁,kubernetes 則是希望能盡可能地提升資源利用率咧栗,所以并不會對這兩種 QoS 的 pod 的資源使用做限制。但在某些場景下我們還是希望能夠盡可能保證 guaranteed level pod 這種高 QoS 級別 pod 的資源虱肄,尤其是不可壓縮資源(如內(nèi)存)致板,不要被低 QoS 級別的 pod 搶占,導(dǎo)致高 QoS 級別的 pod 連它 request 的資源量的資源都無法得到滿足咏窿,此時就可以使用 --qos-reserved 為高 Qos pod 進行預(yù)留資源斟或,舉個例子,當(dāng)前機器的 allocatable 內(nèi)存資源量為 8G集嵌,當(dāng)為這臺機器的 kubelet 開啟 --qos-reserved 參數(shù)后萝挤,并且設(shè)置為 memory=100%,如果此時創(chuàng)建了一個內(nèi)存 request 為 1G 的 guaranteed level 的 pod根欧,那么需要預(yù)留的資源就是 1G怜珍,此時這臺機器上面的 burstable QoS level cgroup 的 memory.limit_in_bytes 的值將會被設(shè)置為 7G,besteffort QoS level cgroup 的 memory.limit_in_bytes 的值也會被設(shè)置為 7G凤粗。而如果此時又創(chuàng)建了一個 burstable level 的 pod酥泛,它的內(nèi)存申請量為 2G,那么此時需要預(yù)留的資源為 3G,而 besteffort QoS level cgroup 的 memory.limit_in_bytes 的值也會被調(diào)整為 5G揭璃。

由上面的公式也可以看到晚凿,burstable 的 cgroup 需要為比他等級高的 guaranteed 級別的 pod 的內(nèi)存資源做預(yù)留,而 besteffort 需要為 burstable 和 guaranteed 都要預(yù)留內(nèi)存資源瘦馍。

小結(jié)

kubelet 啟動時首先會創(chuàng)建 root cgroups 以及為 Qos 創(chuàng)建對應(yīng)的 level cgroups歼秽,然后當(dāng) pod 調(diào)度到節(jié)點上時,kubelet 也會為 pod 以及 pod 下的 container 創(chuàng)建對應(yīng)的 level cgroups情组。root cgroups 限制節(jié)點上所有 pod 的資源使用量燥筷,Qos level cgroups 限制不同 Qos 下 pod 的資源使用量,Pod level cgroups 限制一個 pod 下的資源使用量院崇,Container level cgroups 限制 pod 下 container 的資源使用量肆氓。

節(jié)點上 cgroup 層級樹如下所示:

$ROOT
  |
  +- Pod1
  |   |
  |   +- Container1
  |   +- Container2
  |   ...
  +- Pod2
  |   +- Container3
  |   ...
  +- ...
  |
  +- burstable
  |   |
  |   +- Pod3
  |   |   |
  |   |   +- Container4
  |   |   ...
  |   +- Pod4
  |   |   +- Container5
  |   |   ...
  |   +- ...
  |
  +- besteffort
  |   |
  |   +- Pod5
  |   |   |
  |   |   +- Container6
  |   |   +- Container7
  |   |   ...
  |   +- ...

QOSContainerManager 源碼分析

kubernetes 版本:v1.16

qos 的具體實現(xiàn)是在 kubelet 中的 QOSContainerManagerQOSContainerManager 被包含在 containerManager 模塊中底瓣,kubelet 的 containerManager 模塊中包含多個模塊還有谢揪,cgroupManagercontainerManager捐凭、nodeContainerManager拨扶、podContainerManagertopologyManager茁肠、deviceManager患民、cpuManager 等。

qosContainerManager 的初始化

首先看 QOSContainerManager 的初始化垦梆,因為 QOSContainerManager 包含在 containerManager 中匹颤,在初始化 containerManager 時也會初始化 QOSContainerManager

k8s.io/kubernetes/cmd/kubelet/app/server.go:471

func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh <-chan struct{}) (err error) {
    ......
    kubeDeps.ContainerManager, err = cm.NewContainerManager(......)
    ......
}

k8s.io/kubernetes/pkg/kubelet/cm/container_manager_linux.go:200

// 在 NewContainerManager 中會初始化 qosContainerManager
func NewContainerManager(......) (ContainerManager, error) {
    ......
    qosContainerManager, err := NewQOSContainerManager(subsystems, cgroupRoot, nodeConfig, cgroupManager)
    if err != nil {
        return nil, err
    }
    ......
}

qosContainerManager 的啟動

在調(diào)用 kl.containerManager.Start 啟動 containerManager 時也會啟動 qosContainerManager托猩,代碼如下所示:

k8s.io/kubernetes/pkg/kubelet/kubelet.go:1361

func (kl *Kubelet) initializeRuntimeDependentModules() {
    ......
    if err := kl.containerManager.Start(node, kl.GetActivePods, kl.sourcesReady, kl.statusManager, kl.runtimeService); err != nil {
        klog.Fatalf("Failed to start ContainerManager %v", err)
    }
    ......
}
cm.setupNode

cm.setupNode 是啟動 qosContainerManager 的方法印蓖,其主要邏輯為:

  • 1、檢查 kubelet 依賴的內(nèi)核參數(shù)是否配置正確站刑;
  • 2另伍、若 CgroupsPerQOS 為 true,首先調(diào)用 cm.createNodeAllocatableCgroups 創(chuàng)建 root cgroup绞旅,然后調(diào)用 cm.qosContainerManager.Start 啟動 qosContainerManager摆尝;
  • 3、調(diào)用 cm.enforceNodeAllocatableCgroups 計算 node 的 allocatable 資源并配置到 root cgroup 中因悲,然后判斷是否啟用了 SystemReserved 以及 KubeReserved 并配置對應(yīng)的 cgroup堕汞;
  • 4、為系統(tǒng)組件配置對應(yīng)的 cgroup 資源限制晃琳;
  • 5讯检、為系統(tǒng)進程配置 oom_score_adj琐鲁;

k8s.io/kubernetes/pkg/kubelet/cm/container_manager_linux.go:568

func (cm *containerManagerImpl) Start(......) {
    ......
    if err := cm.setupNode(activePods); err != nil {
        return err
    }
}

// 在 setupNode 中會啟動 qosContainerManager
func (cm *containerManagerImpl) setupNode(activePods ActivePodsFunc) error {
    f, err := validateSystemRequirements(cm.mountUtil)
    if err != nil {
        return err
    }
    if !f.cpuHardcapping {
        cm.status.SoftRequirements = fmt.Errorf("CPU hardcapping unsupported")
    }
    b := KernelTunableModify
    if cm.GetNodeConfig().ProtectKernelDefaults {
        b = KernelTunableError
    }
    // 1、檢查依賴的內(nèi)核參數(shù)是否配置正確
    if err := setupKernelTunables(b); err != nil {
        return err
    }

    if cm.NodeConfig.CgroupsPerQOS {
        // 2人灼、創(chuàng)建 root cgroup围段,即 kubepods dir
        if err := cm.createNodeAllocatableCgroups(); err != nil {
            return err
        }
        // 3、啟動 qosContainerManager
        err = cm.qosContainerManager.Start(cm.getNodeAllocatableAbsolute, activePods)
        if err != nil {
            return fmt.Errorf("failed to initialize top level QOS containers: %v", err)
        }
    }

    // 4投放、為 node 配置 cgroup 資源限制
    if err := cm.enforceNodeAllocatableCgroups(); err != nil {
        return err
    }
    if cm.ContainerRuntime == "docker" {
        cm.periodicTasks = append(cm.periodicTasks, func() {
            cont, err := getContainerNameForProcess(dockerProcessName, dockerPidFile)
            if err != nil {
                klog.Error(err)
                return
            }
            cm.Lock()
            defer cm.Unlock()
            cm.RuntimeCgroupsName = cont
        })
    }

    // 5奈泪、為系統(tǒng)組件配置對應(yīng)的 cgroup 資源限制
    if cm.SystemCgroupsName != "" {
        if cm.SystemCgroupsName == "/" {
            return fmt.Errorf("system container cannot be root (\"/\")")
        }
        cont := newSystemCgroups(cm.SystemCgroupsName)
        cont.ensureStateFunc = func(manager *fs.Manager) error {
            return ensureSystemCgroups("/", manager)
        }
        systemContainers = append(systemContainers, cont)
    }

    // 6、為系統(tǒng)進程配置 oom_score_adj
    if cm.KubeletCgroupsName != "" {
        cont := newSystemCgroups(cm.KubeletCgroupsName)
        allowAllDevices := true
        manager := fs.Manager{
            Cgroups: &configs.Cgroup{
                Parent: "/",
                Name:   cm.KubeletCgroupsName,
                Resources: &configs.Resources{
                    AllowAllDevices: &allowAllDevices,
                },
            },
        }
        cont.ensureStateFunc = func(_ *fs.Manager) error {
            return ensureProcessInContainerWithOOMScore(os.Getpid(), qos.KubeletOOMScoreAdj, &manager)
        }
        systemContainers = append(systemContainers, cont)
    } else {
        cm.periodicTasks = append(cm.periodicTasks, func() {
            if err := ensureProcessInContainerWithOOMScore(os.Getpid(), qos.KubeletOOMScoreAdj, nil); err != nil {
                klog.Error(err)
                return
            }
            cont, err := getContainer(os.Getpid())
            if err != nil {
                klog.Errorf("failed to find cgroups of kubelet - %v", err)
                return
            }
            cm.Lock()
            defer cm.Unlock()

            cm.KubeletCgroupsName = cont
        })
    }
    cm.systemContainers = systemContainers
    return nil
}
cm.qosContainerManager.Start

cm.qosContainerManager.Start 主要邏輯為:

  • 1灸芳、檢查 root cgroup 是否存在涝桅,root cgroup 會在啟動 qosContainerManager 之前創(chuàng)建;
  • 2烙样、為 BurstableBestEffort 創(chuàng)建 Qos level cgroups 并設(shè)置默認值冯遂;
  • 3、調(diào)用 m.UpdateCgroups 每分鐘定期更新 cgroup 信息谒获;

k8s.io/kubernetes/pkg/kubelet/cm/qos_container_manager_linux.go:80

func (m *qosContainerManagerImpl) Start(getNodeAllocatable func() v1.ResourceList, activePods ActivePodsFunc) error {
    cm := m.cgroupManager
    rootContainer := m.cgroupRoot

    // 1蛤肌、檢查 root cgroup 是否存在
    if !cm.Exists(rootContainer) {
        return fmt.Errorf("root container %v doesn't exist", rootContainer)
    }

    // 2、為 Qos 配置 Top level cgroups
    qosClasses := map[v1.PodQOSClass]CgroupName{
        v1.PodQOSBurstable:  NewCgroupName(rootContainer, strings.ToLower(string(v1.PodQOSBurstable))),
        v1.PodQOSBestEffort: NewCgroupName(rootContainer, strings.ToLower(string(v1.PodQOSBestEffort))),
    }

    // 3批狱、為 Qos 創(chuàng)建 top level cgroups
    for qosClass, containerName := range qosClasses {
        resourceParameters := &ResourceConfig{}
        // 4寻定、為 BestEffort QoS cpu.shares 設(shè)置默認值,默認為 2
        if qosClass == v1.PodQOSBestEffort {
            minShares := uint64(MinShares)
            resourceParameters.CpuShares = &minShares
        }

        containerConfig := &CgroupConfig{
            Name:               containerName,
            ResourceParameters: resourceParameters,
        }

        // 5精耐、配置 huge page size
        m.setHugePagesUnbounded(containerConfig)

        // 6、為 Qos 創(chuàng)建 cgroup 目錄
        if !cm.Exists(containerName) {
            if err := cm.Create(containerConfig); err != nil {
                ......
            }
        } else {
            if err := cm.Update(containerConfig); err != nil {
                ......
            }
        }
    }
    ......

    // 7琅锻、每分鐘定期更新 cgroup 配置
    go wait.Until(func() {
        err := m.UpdateCgroups()
        if err != nil {
            klog.Warningf("[ContainerManager] Failed to reserve QoS requests: %v", err)
        }
    }, periodicQOSCgroupUpdateInterval, wait.NeverStop)

    return nil
}
m.UpdateCgroups

m.UpdateCgroups 是用來更新 Qos level cgroup 中的值卦停,其主要邏輯為:

  • 1、調(diào)用 m.setCPUCgroupConfig 計算 node 上的 activePods 的資源以此來更新 bestEffortburstable Qos level cgroup 的 cpu.shares 值恼蓬,besteffortcpu.shares 值默認為 2惊完,burstable cpu.shares 的計算方式為:max(sum(Burstable pods cpu requests)* 1024 /1000, 2);
  • 2处硬、調(diào)用m.setHugePagesConfig 更新 huge pages小槐;
  • 3、檢查是否啟用了--qos-reserved 參數(shù)荷辕,若啟用了則調(diào)用 m.setMemoryReserve 計算每個 Qos class 中需要設(shè)定的值然后調(diào)用 m.cgroupManager.Update 更新 cgroup 中的值凿跳;
  • 4、最后調(diào)用 m.cgroupManager.Update 更新 cgroup 中的值疮方;

k8s.io/kubernetes/pkg/kubelet/cm/qos_container_manager_linux.go:269

func (m *qosContainerManagerImpl) UpdateCgroups() error {
    m.Lock()
    defer m.Unlock()

    qosConfigs := map[v1.PodQOSClass]*CgroupConfig{
        v1.PodQOSBurstable: {
            Name:               m.qosContainersInfo.Burstable,
            ResourceParameters: &ResourceConfig{},
        },
        v1.PodQOSBestEffort: {
            Name:               m.qosContainersInfo.BestEffort,
            ResourceParameters: &ResourceConfig{},
        },
    }

    // 1控嗜、更新 bestEffort 和 burstable Qos level cgroup 的 cpu.shares 值
    if err := m.setCPUCgroupConfig(qosConfigs); err != nil {
        return err
    }

    // 2、調(diào)用 m.setHugePagesConfig 更新 huge pages
    if err := m.setHugePagesConfig(qosConfigs); err != nil {
        return err
    }

    // 3骡显、設(shè)置資源預(yù)留
    if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.QOSReserved) {
        for resource, percentReserve := range m.qosReserved {
            switch resource {
            case v1.ResourceMemory:
                m.setMemoryReserve(qosConfigs, percentReserve)
            }
        }

        updateSuccess := true
        for _, config := range qosConfigs {
            err := m.cgroupManager.Update(config)
            if err != nil {
                updateSuccess = false
            }
        }
        if updateSuccess {
            klog.V(4).Infof("[ContainerManager]: Updated QoS cgroup configuration")
            return nil
        }

        for resource, percentReserve := range m.qosReserved {
            switch resource {
            case v1.ResourceMemory:
                m.retrySetMemoryReserve(qosConfigs, percentReserve)
            }
        }
    }

    // 4疆栏、更新 cgroup 中的值
    for _, config := range qosConfigs {
        err := m.cgroupManager.Update(config)
        if err != nil {
            return err
        }
    }

    return nil
}
m.cgroupManager.Update

m.cgroupManager.Update 方法主要是根據(jù) cgroup 配置來更新 cgroup 中的值曾掂,其主要邏輯為:

  • 1、調(diào)用 m.buildCgroupPaths 創(chuàng)建對應(yīng)的 cgroup 目錄壁顶,在每個 cgroup 子系統(tǒng)下面都有一個 kubelet 對應(yīng)的 root cgroup 目錄珠洗;
  • 2、調(diào)用 setSupportedSubsystems 更新的 cgroup 子系統(tǒng)中的值若专;

k8s.io/kubernetes/pkg/kubelet/cm/cgroup_manager_linux.go:409

func (m *cgroupManagerImpl) Update(cgroupConfig *CgroupConfig) error {
    ......
    resourceConfig := cgroupConfig.ResourceParameters
    resources := m.toResources(resourceConfig)

    cgroupPaths := m.buildCgroupPaths(cgroupConfig.Name)

    libcontainerCgroupConfig := &libcontainerconfigs.Cgroup{
        Resources: resources,
        Paths:     cgroupPaths,
    }

    if m.adapter.cgroupManagerType == libcontainerSystemd {
        updateSystemdCgroupInfo(libcontainerCgroupConfig, cgroupConfig.Name)
    } else {
        libcontainerCgroupConfig.Path = cgroupConfig.Name.ToCgroupfs()
    }

    if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.SupportPodPidsLimit) && cgroupConfig.ResourceParameters != nil && cgroupConfig.               ResourceParameters.PidsLimit != nil {
        libcontainerCgroupConfig.PidsLimit = *cgroupConfig.ResourceParameters.PidsLimit
    }

    if err := setSupportedSubsystems(libcontainerCgroupConfig); err != nil {
        return fmt.Errorf("failed to set supported cgroup subsystems for cgroup %v: %v", cgroupConfig.Name, err)
    }
    return nil
}
setSupportedSubsystem

setSupportedSubsystems 首先通過 getSupportedSubsystems 獲取 kubelet 支持哪些 cgroup 子系統(tǒng)许蓖,然后調(diào)用 sys.Set 設(shè)置對應(yīng)子系統(tǒng)的值,sys.Set 是調(diào)用 runc/libcontainer 中的包進行設(shè)置的富岳,其主要邏輯是在 cgroup 子系統(tǒng)對應(yīng)的文件中寫入值蛔糯。

k8s.io/kubernetes/pkg/kubelet/cm/cgroup_manager_linux.go:345

func setSupportedSubsystems(cgroupConfig *libcontainerconfigs.Cgroup) error {
    for sys, required := range getSupportedSubsystems() {
        if _, ok := cgroupConfig.Paths[sys.Name()]; !ok {
            if required {
                return fmt.Errorf("failed to find subsystem mount for required subsystem: %v", sys.Name())
            }
            ......
            continue
        }
        if err := sys.Set(cgroupConfig.Paths[sys.Name()], cgroupConfig); err != nil {
            return fmt.Errorf("failed to set config for supported subsystems : %v", err)
        }
    }
    return nil
}

例如為 cgroup 中 cpu 子系統(tǒng)設(shè)置值的方法如下所示:

func (s *CpuGroup) Set(path string, cgroup *configs.Cgroup) error {
    if cgroup.Resources.CpuShares != 0 {
        if err := writeFile(path, "cpu.shares", strconv.FormatUint(cgroup.Resources.CpuShares, 10)); err != nil {
            return err
        }
    }
    if cgroup.Resources.CpuPeriod != 0 {
        if err := writeFile(path, "cpu.cfs_period_us", strconv.FormatUint(cgroup.Resources.CpuPeriod, 10)); err != nil {
            return err
        }
    }
    if cgroup.Resources.CpuQuota != 0 {
        if err := writeFile(path, "cpu.cfs_quota_us", strconv.FormatInt(cgroup.Resources.CpuQuota, 10)); err != nil {
            return err
        }
    }
    return s.SetRtSched(path, cgroup)
}

Pod Level Cgroup

Pod Level cgroup 是 kubelet 在創(chuàng)建 pod 時創(chuàng)建的,創(chuàng)建 pod 是在 kubelet 的 syncPod 方法中進行的窖式,在 syncPod 方法中首先會調(diào)用 kl.containerManager.UpdateQOSCgroups 更新 Qos Level cgroup蚁飒,然后調(diào)用 pcm.EnsureExists 創(chuàng)建 pod level cgroup。

func (kl *Kubelet) syncPod(o syncPodOptions) error {
        ......
        if !kl.podIsTerminated(pod) {
            ......
            if !(podKilled && pod.Spec.RestartPolicy == v1.RestartPolicyNever) {
                if !pcm.Exists(pod) {
                    if err := kl.containerManager.UpdateQOSCgroups(); err != nil {
                        ......
                    }
                    if err := pcm.EnsureExists(pod); err != nil {
                        ......
                    }
                }
            }
        }
        ......
}

EnsureExists 的主要邏輯是檢查 pod 的 cgroup 是否存在萝喘,若不存在則調(diào)用 m.cgroupManager.Create 進行創(chuàng)建淮逻。

k8s.io/kubernetes/pkg/kubelet/cm/pod_container_manager_linux.go:79

func (m *podContainerManagerImpl) EnsureExists(pod *v1.Pod) error {
    podContainerName, _ := m.GetPodContainerName(pod)

    alreadyExists := m.Exists(pod)
    if !alreadyExists {
        containerConfig := &CgroupConfig{
            Name:               podContainerName,
            ResourceParameters: ResourceConfigForPod(pod, m.enforceCPULimits, m.cpuCFSQuotaPeriod),
        }
        if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.SupportPodPidsLimit) && m.podPidsLimit > 0 {
            containerConfig.ResourceParameters.PidsLimit = &m.podPidsLimit
        }
        if err := m.cgroupManager.Create(containerConfig); err != nil {
            return fmt.Errorf("failed to create container for %v : %v", podContainerName, err)
        }
    }
    ......
    return nil
}

Container Level Cgroup

Container Level Cgroup 是通過 runtime 進行創(chuàng)建的,若使用 runc 其會調(diào)用 runc 的 InitProcess.start 方法對 cgroup 資源組進行配置與應(yīng)用阁簸。

k8s.io/kubernetes/vendor/github.com/opencontainers/runc/libcontainer/process_linux.go:282

func (p *initProcess) start() error {
    ......

    // 調(diào)用 p.manager.Apply 為進程配置 cgroup
    if err := p.manager.Apply(p.pid()); err != nil {
        return newSystemErrorWithCause(err, "applying cgroup configuration for process")
    }
    if p.intelRdtManager != nil {
        if err := p.intelRdtManager.Apply(p.pid()); err != nil {
            return newSystemErrorWithCause(err, "applying Intel RDT configuration for process")
        }
    }
    ......
}

總結(jié)

kubernetes 中有三種 Qos爬早,分別為 Guaranteed、Burstable启妹、BestEffort筛严,三種 Qos 以 node 上 allocatable 資源量為基于為 pod 進行分配,并通過多個 level cgroup 進行層層限制饶米,對 cgroup 的配置都是通過調(diào)用 runc/libcontainer/cgroups/fs 中的方法進行資源更新的桨啃。對于 Qos level cgroup,kubelet 會根據(jù)以下事件動態(tài)更新:

  • 1檬输、kubelet 服務(wù)啟動時照瘾;
  • 2、在創(chuàng)建 pod level cgroup 之前丧慈,即創(chuàng)建 pod 前析命;
  • 3、在刪除 pod level cgroup 后逃默;
  • 4鹃愤、定期檢測是否需要為 qos level cgroup 預(yù)留資源;

參考:

https://kubernetes.io/docs/setup/production-environment/container-runtimes/#cgroup-drivers
https://zhuanlan.zhihu.com/p/38359775
https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node/pod-resource-management.md
https://github.com/cri-o/cri-o/issues/842
https://yq.aliyun.com/articles/737784?spm=a2c4e.11153940.0.0.577f6149mYFkTR

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末笑旺,一起剝皮案震驚了整個濱河市昼浦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌筒主,老刑警劉巖关噪,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸟蟹,死亡現(xiàn)場離奇詭異,居然都是意外死亡使兔,警方通過查閱死者的電腦和手機建钥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來虐沥,“玉大人熊经,你說我怎么就攤上這事∮眨” “怎么了镐依?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長天试。 經(jīng)常有香客問我槐壳,道長,這世上最難降的妖魔是什么喜每? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任务唐,我火速辦了婚禮,結(jié)果婚禮上带兜,老公的妹妹穿的比我還像新娘枫笛。我一直安慰自己,他們只是感情好刚照,可當(dāng)我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布刑巧。 她就那樣靜靜地躺著,像睡著了一般无畔。 火紅的嫁衣襯著肌膚如雪海诲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天檩互,我揣著相機與錄音,去河邊找鬼咨演。 笑死闸昨,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的薄风。 我是一名探鬼主播饵较,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼遭赂!你這毒婦竟也來了循诉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤撇他,失蹤者是張志新(化名)和其女友劉穎茄猫,沒想到半個月后狈蚤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡划纽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年脆侮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勇劣。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡靖避,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出比默,到底是詐尸還是另有隱情幻捏,我是刑警寧澤,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布命咐,位于F島的核電站篡九,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏侈百。R本人自食惡果不足惜瓮下,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望钝域。 院中可真熱鬧讽坏,春花似錦、人聲如沸例证。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽织咧。三九已至胀葱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間笙蒙,已是汗流浹背抵屿。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留捅位,地道東北人轧葛。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像艇搀,于是被迫代替她去往敵國和親尿扯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內(nèi)容