天天看點

kubernetes 中 Evicted pod 是如何産生的

線上被驅逐執行個體資料

最近線上上發現很多執行個體處于 Evicted 狀态,通過 pod yaml 可以看到執行個體是因為節點資源不足被驅逐,但是這些執行個體并沒有被自動清理,平台的大部分使用者在操作時看到服務下面出現 Evicted 執行個體時會以為服務有問題或者平台有問題的錯覺,影響了使用者的體驗。而這部分 Evicted 狀态的 Pod 在底層關聯的容器其實已經被銷毀了,對使用者的服務也不會産生什麼影響,也就是說隻有一個 Pod 空殼在 k8s 中儲存着,但需要人為手動清理。本文會分析為什麼為産生 Evicted 執行個體、為什麼 Evicted 執行個體沒有被自動清理以及如何進行自動清理。

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 執行個體狀态:

status:
  message: 'Pod The node had condition: [DiskPressure]. '
  phase: Failed
  reason: Evicted
  startTime: "2021-09-14T10:42:32Z"           

執行個體被驅逐的原因

kubelet 預設會配置節點資源不足時驅逐執行個體的政策,當節點資源不足時 k8s 會停止該節點上執行個體并在其他節點啟動新執行個體,在某些情況下也可通過配置

--eviction-hard=

參數為空來禁用驅逐政策,在之前的生産環境中我們也确實這麼做了。

節點資源不足導緻執行個體被驅逐

k8s 中産生 Evicted 狀态執行個體主要是因為節點資源不足執行個體主動被驅逐導緻的,kubelet eviction_manager 子產品會定期檢查節點記憶體使用率、inode 使用率、磁盤使用率、pid 等資源,根據 kubelet 的配置當使用率達到一定門檻值後會先回收可以回收的資源,若回收後資源使用率依然超過門檻值則進行驅逐執行個體操作。

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 資料一部分是通過 cAdvisor 接口擷取到的,一部分是通過 CRI runtimes 的接口擷取到的。

memory.available:目前節點可用記憶體,計算方式為 cgroup memory 子系統中 memory.usage_in_bytes 中的值減去 memory.stat 中 total_inactive_file 的值;

nodefs.available:nodefs 包含 kubelet 配置中

--root-dir

指定的檔案分區和 /var/lib/kubelet/ 所在的分區磁盤使用率;

nodefs.inodesFree:nodefs.available 分區的 inode 使用率;

imagefs.available:鏡像所在分區磁盤使用率;

imagefs.inodesFree:鏡像所在分區磁盤inode使用率;

pid.available:

/proc/sys/kernel/pid_max

中的值為系統最大可用 pid 數;

kubelet 可以通過參數

--eviction-hard

來配置以上幾個參數的門檻值,該參數預設值為

imagefs.available<15%,memory.available<100Mi,nodefs.available<10%,nodefs.inodesFree<5%

,當達到門檻值時會驅逐節點上的容器。

kubelet 驅逐執行個體時與資源處理相關的已知問題

1、kubelet 不會實時感覺到節點記憶體資料的變化

kubelet 定期通過 cadvisor 接口采集節點記憶體使用資料,當節點短時間内記憶體使用率突增,此時 kubelet 無法感覺到也不會有 MemoryPressure 相關事件,但依然會調用 OOMKiller 停止容器。可以通過為 kubelet 配置

--kernel-memcg-notification

參數啟用 memcg api,當觸發memory 使用率門檻值時 memcg 會主動進行通知;

memcg 主動通知的功能是 cgroup 中已有的,kubelet 會在

/sys/fs/cgroup/memory/cgroup.event_control

檔案中寫入 memory.available 的門檻值,而門檻值與 inactive_file 檔案的大小有關系,kubelet 也會定期更新門檻值,當 memcg 使用率達到配置的門檻值後會主動通知 kubelet,kubelet 通過 epoll 機制來接收通知。

2、kubelet memory.available 不會計算 active page

kubelet 通過記憶體使用率驅逐執行個體時,記憶體使用率資料包含了 page cache 中 active_file 的資料,在某些場景下會因 page cache 過高導緻記憶體使用率超過門檻值會造成執行個體被驅逐,

由于在記憶體緊張時 inactive_file 會被核心首先回收,但在記憶體不足時,active_file 也會被核心進行回收,社群對此機制也有一些疑問,針對核心回收記憶體的情況比較複雜,社群暫時還未進行回應,詳情可以參考

kubelet counts active page cache against memory.available (maybe it shouldn't?)

kubelet 計算節點可用記憶體的方式如下:

#!/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"           

驅逐執行個體未被删除原因分析

源碼中對于 Statefulset 和 DaemonSet 會自動删除 Evicted 執行個體,但是對于 Deployment 不會自動删除。閱讀了部分官方文檔以及 issue,暫未找到官方對 Deployment Evicted 執行個體未删除原因給出解釋。

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、團隊裡面有了一套 k8s 叢集事件采集的鍊路,我們通過消費 k8s 中 pod 的相關事件來進行處理,消費事件時過濾 pod 中與 Evicted 執行個體相關的事件然後處理即可。

Evicted 執行個體判斷邏輯:

const (
    podEvictedStatus = "Evicted"
)

// 判斷如果為 Evicted 狀态的執行個體且 Pod 中容器數為 0 時直接删除 pod
if strings.ToLower(pod.Status.Reason) == strings.ToLower(podEvictedStatus) && pod.Status.Phase == v1.PodFailed &&
        len(pod.Status.ContainerStatuses) == 0 {

}           

2、社群有人提供通過在 kube-controller-manager 中配置 podgc controller --terminated-pod-gc-threshold 參數來自動清理:

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)           

該參數配置的是保留的異常執行個體數,預設值為 12500,但 podgc controller 回收 pod 時使用強殺模式不支援執行個體的優雅退出,是以暫不考慮使用。

3、其他處理方式可以參考社群中提供的

Kubelet does not delete evicted pods

總結

由于在之前的公司中對于穩定性的高度重視,線上節點并未開啟驅逐執行個體的功能,是以也不會存在 Evicted 狀态的執行個體,當節點資源嚴重不足時會有告警人工介入處理,以及還會有二次排程、故障自愈等一些輔助處理措施。本次針對 Evicted 相關執行個體的分析,發現 k8s 與作業系統之間存在了很多聯系,如果要徹底搞清楚某些機制需要對作業系統的一些原理有一定的了解。

參考:

https://github.com/kubernetes/kubernetes/issues/55051 https://ieevee.com/tech/2019/05/23/ephemeral-storage.html https://github.com/kubernetes/kubernetes/issues/43916 https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/

繼續閱讀