生產(chǎn)環(huán)境 Kubernetes 中出現(xiàn)了很多 Evicted Pod,我該怎么辦呢?
線上被驅(qū)逐實(shí)例數(shù)據(jù)
最近在線上發(fā)現(xiàn)很多實(shí)例處于 Evicted 狀態(tài),通過 pod yaml 可以看到實(shí)例是因?yàn)楣?jié)點(diǎn)資源不足被驅(qū)逐,但是這些實(shí)例并沒有被自動(dòng)清理,平臺(tái)的大部分用戶在操作時(shí)看到服務(wù)下面出現(xiàn) Evicted 實(shí)例時(shí)會(huì)以為服務(wù)有問題或者平臺(tái)有問題的錯(cuò)覺,影響了用戶的體驗(yàn)。而這部分 Evicted 狀態(tài)的 Pod 在底層關(guān)聯(lián)的容器其實(shí)已經(jīng)被銷毀了,對(duì)用戶的服務(wù)也不會(huì)產(chǎn)生什么影響,也就是說只有一個(gè) Pod 空殼在 k8s 中保存著,但需要人為手動(dòng)清理。本文會(huì)分析為什么為產(chǎn)生 Evicted 實(shí)例、為什么 Evicted 實(shí)例沒有被自動(dòng)清理以及如何進(jìn)行自動(dòng)清理。
kubernetes 版本:v1.17
- $ kubectl get pod | grep -i Evicted
- cloud-1023955-84421-49604-5-deploy-c-7748f8fd8-hjqsh 0/1 Evicted 0 73d
- cloud-1023955-84421-49604-5-deploy-c-7748f8fd8-mzd8x 0/1 Evicted 0 81d
- cloud-1237162-276467-199844-2-deploy-7bdc7c98b6-26r2r 0/1 Evicted 0 18d
Evicted 實(shí)例狀態(tài):
- status:
- message: 'Pod The node had condition: [DiskPressure]. '
- phase: Failed
- reason: Evicted
- startTime: "2021-09-14T10:42:32Z"
實(shí)例被驅(qū)逐的原因
kubelet 默認(rèn)會(huì)配置節(jié)點(diǎn)資源不足時(shí)驅(qū)逐實(shí)例的策略,當(dāng)節(jié)點(diǎn)資源不足時(shí) k8s 會(huì)停止該節(jié)點(diǎn)上實(shí)例并在其他節(jié)點(diǎn)啟動(dòng)新實(shí)例,在某些情況下也可通過配置 --eviction-hard= 參數(shù)為空來禁用驅(qū)逐策略,在之前的生產(chǎn)環(huán)境中我們也確實(shí)這么做了。
節(jié)點(diǎn)資源不足導(dǎo)致實(shí)例被驅(qū)逐
k8s 中產(chǎn)生 Evicted 狀態(tài)實(shí)例主要是因?yàn)楣?jié)點(diǎn)資源不足實(shí)例主動(dòng)被驅(qū)逐導(dǎo)致的,kubelet eviction_manager 模塊會(huì)定期檢查節(jié)點(diǎn)內(nèi)存使用率、inode 使用率、磁盤使用率、pid 等資源,根據(jù) kubelet 的配置當(dāng)使用率達(dá)到一定閾值后會(huì)先回收可以回收的資源,若回收后資源使用率依然超過閾值則進(jìn)行驅(qū)逐實(shí)例操作。
Eviction Signal | Description |
---|---|
memory.available | memory.available := node.status.capacity[memory] - node.stats.memory.workingSet |
nodefs.available | nodefs.available := node.stats.fs.available |
nodefs.inodesFree | nodefs.inodesFree := node.stats.fs.inodesFree |
imagefs.available | imagefs.available := node.stats.runtime.imagefs.available |
imagefs.inodesFree | imagefs.inodesFree := node.stats.runtime.imagefs.inodesFree |
pid.available | pid.available := node.stats.rlimit.maxpid - node.stats.rlimit.curproc |
kubelet 中 pod 的 stats 數(shù)據(jù)一部分是通過 cAdvisor 接口獲取到的,一部分是通過 CRI runtimes 的接口獲取到的。
- memory.available:當(dāng)前節(jié)點(diǎn)可用內(nèi)存,計(jì)算方式為 cgroup memory 子系統(tǒng)中 memory.usage_in_bytes 中的值減去 memory.stat 中 total_inactive_file 的值;
- nodefs.available:nodefs 包含 kubelet 配置中 --root-dir 指定的文件分區(qū)和 /var/lib/kubelet/ 所在的分區(qū)磁盤使用率;
- nodefs.inodesFree:nodefs.available 分區(qū)的 inode 使用率;
- imagefs.available:鏡像所在分區(qū)磁盤使用率;
- imagefs.inodesFree:鏡像所在分區(qū)磁盤 inode 使用率;
- pid.available:/proc/sys/kernel/pid_max 中的值為系統(tǒng)最大可用 pid 數(shù);
kubelet 可以通過參數(shù) --eviction-hard 來配置以上幾個(gè)參數(shù)的閾值,該參數(shù)默認(rèn)值為 imagefs.available<15%,memory.available<100Mi,nodefs.available<10%,nodefs.inodesFree<5%,當(dāng)達(dá)到閾值時(shí)會(huì)驅(qū)逐節(jié)點(diǎn)上的容器。
kubelet 驅(qū)逐實(shí)例時(shí)與資源處理相關(guān)的已知問題
1、kubelet 不會(huì)實(shí)時(shí)感知到節(jié)點(diǎn)內(nèi)存數(shù)據(jù)的變化
kubelet 定期通過 cadvisor 接口采集節(jié)點(diǎn)內(nèi)存使用數(shù)據(jù),當(dāng)節(jié)點(diǎn)短時(shí)間內(nèi)內(nèi)存使用率突增,此時(shí) kubelet 無法感知到也不會(huì)有 MemoryPressure 相關(guān)事件,但依然會(huì)調(diào)用 OOMKiller 停止容器??梢酝ㄟ^為 kubelet 配置 --kernel-memcg-notification 參數(shù)啟用 memcg api,當(dāng)觸發(fā) memory 使用率閾值時(shí) memcg 會(huì)主動(dòng)進(jìn)行通知;
memcg 主動(dòng)通知的功能是 cgroup 中已有的,kubelet 會(huì)在 /sys/fs/cgroup/memory/cgroup.event_control 文件中寫入 memory.available 的閾值,而閾值與 inactive_file 文件的大小有關(guān)系,kubelet 也會(huì)定期更新閾值,當(dāng) memcg 使用率達(dá)到配置的閾值后會(huì)主動(dòng)通知 kubelet,kubelet 通過 epoll 機(jī)制來接收通知。
2、kubelet memory.available 不會(huì)計(jì)算 active page
kubelet 通過內(nèi)存使用率驅(qū)逐實(shí)例時(shí),內(nèi)存使用率數(shù)據(jù)包含了 page cache 中 active_file 的數(shù)據(jù),在某些場景下會(huì)因 page cache 過高導(dǎo)致內(nèi)存使用率超過閾值會(huì)造成實(shí)例被驅(qū)逐,
由于在內(nèi)存緊張時(shí) inactive_file 會(huì)被內(nèi)核首先回收,但在內(nèi)存不足時(shí),active_file 也會(huì)被內(nèi)核進(jìn)行回收,社區(qū)對(duì)此機(jī)制也有一些疑問,針對(duì)內(nèi)核回收內(nèi)存的情況比較復(fù)雜,社區(qū)暫時(shí)還未進(jìn)行回應(yīng),詳情可以參考 kubelet counts active page cache against memory.available (maybe it shouldn’t?)[1]。
kubelet 計(jì)算節(jié)點(diǎn)可用內(nèi)存的方式如下:
- #!/bin/bash
- #!/usr/bin/env bash
- # This script reproduces what the kubelet does
- # to calculate memory.available relative to root cgroup.
- # current memory usage
- memory_capacity_in_kb=$(cat /proc/meminfo | grep MemTotal | awk '{print $2}')
- memory_capacity_in_bytes=$((memory_capacity_in_kb * 1024))
- memory_usage_in_bytes=$(cat /sys/fs/cgroup/memory/memory.usage_in_bytes)
- memory_total_inactive_file=$(cat /sys/fs/cgroup/memory/memory.stat | grep total_inactive_file | awk '{print $2}')
- memory_working_set=${memory_usage_in_bytes}
- if [ "$memory_working_set" -lt "$memory_total_inactive_file" ];
- then
- memory_working_set=0
- else
- memory_working_set=$((memory_usage_in_bytes - memory_total_inactive_file))
- fi
- memory_available_in_bytes=$((memory_capacity_in_bytes - memory_working_set))
- memory_available_in_kb=$((memory_available_in_bytes / 1024))
- memory_available_in_mb=$((memory_available_in_kb / 1024))
- echo "memory.capacity_in_bytes $memory_capacity_in_bytes"
- echo "memory.usage_in_bytes $memory_usage_in_bytes"
- echo "memory.total_inactive_file $memory_total_inactive_file"
- echo "memory.working_set $memory_working_set"
- echo "memory.available_in_bytes $memory_available_in_bytes"
- echo "memory.available_in_kb $memory_available_in_kb"
- echo "memory.available_in_mb $memory_available_in_mb"
驅(qū)逐實(shí)例未被刪除原因分析
源碼中對(duì)于 Statefulset 和 DaemonSet 會(huì)自動(dòng)刪除 Evicted 實(shí)例,但是對(duì)于 Deployment 不會(huì)自動(dòng)刪除。閱讀了部分官方文檔以及 issue,暫未找到官方對(duì) Deployment Evicted 實(shí)例未刪除原因給出解釋。
statefulset:pkg/controller/statefulset/stateful_set_control.go
- // Examine each replica with respect to its ordinal
- for i := range replicas {
- // delete and recreate failed pods
- if isFailed(replicas[i]) {
- ssc.recorder.Eventf(set, v1.EventTypeWarning, "RecreatingFailedPod",
- "StatefulSet %s/%s is recreating failed Pod %s",
- set.Namespace,
- set.Name,
- replicas[i].Name)
- if err := ssc.podControl.DeleteStatefulPod(set, replicas[i]); err != nil {
- return &status, err
- }
- if getPodRevision(replicas[i]) == currentRevision.Name {
- status.CurrentReplicas--
- }
- if getPodRevision(replicas[i]) == updateRevision.Name {
- status.UpdatedReplicas--
- }
- ......
daemonset:pkg/controller/daemon/daemon_controller.go
- func (dsc *DaemonSetsController) podsShouldBeOnNode(
- ......
- ) (nodesNeedingDaemonPods, podsToDelete []string) {
- ......
- switch {
- ......
- case shouldContinueRunning:
- ......
- for _, pod := range daemonPods {
- if pod.DeletionTimestamp != nil {
- continue
- }
- if pod.Status.Phase == v1.PodFailed {
- // This is a critical place where DS is often fighting with kubelet that rejects pods.
- // We need to avoid hot looping and backoff.
- backoffKey := failedPodsBackoffKey(ds, node.Name)
- ......
解決方案
1、團(tuán)隊(duì)里面有了一套 k8s 集群事件采集的鏈路,我們通過消費(fèi) k8s 中 pod 的相關(guān)事件來進(jìn)行處理,消費(fèi)事件時(shí)過濾 pod 中與 Evicted 實(shí)例相關(guān)的事件然后處理即可。
Evicted 實(shí)例判斷邏輯:
- const (
- podEvictedStatus = "Evicted"
- )
- // 判斷如果為 Evicted 狀態(tài)的實(shí)例且 Pod 中容器數(shù)為 0 時(shí)直接刪除 pod
- if strings.ToLower(status) == strings.ToLower(podEvictedStatus) && len(pod.Status.ContainerStatuses) == 0 {
- }
2、社區(qū)有人提供通過在 kube-controller-manager 中配置 podgc controller –terminated-pod-gc-threshold 參數(shù)來自動(dòng)清理:
- Podgc controller flags:
- --terminated-pod-gc-threshold int32
- Number of terminated pods that can exist before the terminated pod garbage collector starts deleting terminated pods. If
- <= 0, the terminated pod garbage collector is disabled. (default 12500)
該參數(shù)配置的是保留的異常實(shí)例數(shù),默認(rèn)值為 12500,但 podgc controller 回收 pod 時(shí)使用強(qiáng)殺模式不支持實(shí)例的優(yōu)雅退出,因此暫不考慮使用。
3、其他處理方式可以參考社區(qū)中提供的 Kubelet does not delete evicted pods[2]。
總結(jié)
由于在之前的公司中對(duì)于穩(wěn)定性的高度重視,線上節(jié)點(diǎn)并未開啟驅(qū)逐實(shí)例的功能,因此也不會(huì)存在 Evicted 狀態(tài)的實(shí)例,當(dāng)節(jié)點(diǎn)資源嚴(yán)重不足時(shí)會(huì)有告警人工介入處理,以及還會(huì)有二次調(diào)度、故障自愈等一些輔助處理措施。本次針對(duì) Evicted 相關(guān)實(shí)例的分析,發(fā)現(xiàn) k8s 與操作系統(tǒng)之間存在了很多聯(lián)系,如果要徹底搞清楚某些機(jī)制需要對(duì)操作系統(tǒng)的一些原理有一定的了解。