線(xiàn)上被驅(qū)逐實(shí)例數(shù)據(jù)
最近在線(xiàn)上發(fā)現(xiàn)很多實(shí)例處于 Evicted 狀態(tài),通過(guò) pod yaml 可以看到實(shí)例是因?yàn)楣?jié)點(diǎn)資源不足被驅(qū)逐,但是這些實(shí)例并沒(méi)有被自動(dòng)清理,平臺(tái)的大部分用戶(hù)在操作時(shí)看到服務(wù)下面出現(xiàn) Evicted 實(shí)例時(shí)會(huì)以為服務(wù)有問(wèn)題或者平臺(tái)有問(wèn)題的錯(cuò)覺(jué),影響了用戶(hù)的體驗(yàn)。而這部分 Evicted 狀態(tài)的 Pod 在底層關(guān)聯(lián)的容器其實(shí)已經(jīng)被銷(xiāo)毀了,對(duì)用戶(hù)的服務(wù)也不會(huì)產(chǎn)生什么影響,也就是說(shuō)只有一個(gè) Pod 空殼在 k8s 中保存著,但需要人為手動(dòng)清理。本文會(huì)分析為什么為產(chǎn)生 Evicted 實(shí)例、為什么 Evicted 實(shí)例沒(méi)有被自動(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í)例,在某些情況下也可通過(guò)配置 --eviction-hard= 參數(shù)為空來(lái)禁用驅(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 使用率、磁盤(pán)使用率、pid 等資源,根據(jù) kubelet 的配置當(dāng)使用率達(dá)到一定閾值后會(huì)先回收可以回收的資源,若回收后資源使用率依然超過(guò)閾值則進(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ù)一部分是通過(guò) cAdvisor 接口獲取到的,一部分是通過(guò) 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ū)磁盤(pán)使用率;
nodefs.inodesFree:nodefs.available 分區(qū)的 inode 使用率;
imagefs.available:鏡像所在分區(qū)磁盤(pán)使用率;
imagefs.inodesFree:鏡像所在分區(qū)磁盤(pán)inode使用率;
pid.available:/proc/sys/kernel/pid_max 中的值為系統(tǒng)最大可用 pid 數(shù);
kubelet 可以通過(guò)參數(shù) --eviction-hard 來(lái)配置以上幾個(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)的已知問(wèn)題
1、kubelet 不會(huì)實(shí)時(shí)感知到節(jié)點(diǎn)內(nèi)存數(shù)據(jù)的變化
kubelet 定期通過(guò) cadvisor 接口采集節(jié)點(diǎn)內(nèi)存使用數(shù)據(jù),當(dāng)節(jié)點(diǎn)短時(shí)間內(nèi)內(nèi)存使用率突增,此時(shí) kubelet 無(wú)法感知到也不會(huì)有 MemoryPressure 相關(guān)事件,但依然會(huì)調(diào)用 OOMKiller 停止容器??梢酝ㄟ^(guò)為 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 文件中寫(xiě)入 memory.available 的閾值,而閾值與 inactive_file 文件的大小有關(guān)系,kubelet 也會(huì)定期更新閾值,當(dāng) memcg 使用率達(dá)到配置的閾值后會(huì)主動(dòng)通知 kubelet,kubelet 通過(guò) epoll 機(jī)制來(lái)接收通知。
2、kubelet memory.available 不會(huì)計(jì)算 active page
kubelet 通過(guò)內(nèi)存使用率驅(qū)逐實(shí)例時(shí),內(nèi)存使用率數(shù)據(jù)包含了 page cache 中 active_file 的數(shù)據(jù),在某些場(chǎng)景下會(huì)因 page cache 過(guò)高導(dǎo)致內(nèi)存使用率超過(guò)閾值會(huì)造成實(shí)例被驅(qū)逐,
由于在內(nèi)存緊張時(shí) inactive_file 會(huì)被內(nèi)核首先回收,但在內(nèi)存不足時(shí),active_file 也會(huì)被內(nèi)核進(jìn)行回收,社區(qū)對(duì)此機(jī)制也有一些疑問(wèn),針對(duì)內(nèi)核回收內(nèi)存的情況比較復(fù)雜,社區(qū)暫時(shí)還未進(jìn)行回應(yīng),詳情可以參考 kubelet counts active page cache against memory.available (maybe it shouldn't?)。
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 集群事件采集的鏈路,我們通過(guò)消費(fèi) k8s 中 pod 的相關(guān)事件來(lái)進(jìn)行處理,消費(fèi)事件時(shí)過(guò)濾 pod 中與 Evicted 實(shí)例相關(guān)的事件然后處理即可。
Evicted 實(shí)例判斷邏輯:
const (
podEvictedStatus = "Evicted"
)
// 判斷如果為 Evicted 狀態(tài)的實(shí)例且 Pod 中容器數(shù)為 0 時(shí)直接刪除 pod
if strings.ToLower(pod.Status.Reason) == strings.ToLower(podEvictedStatus) && pod.Status.Phase == v1.PodFailed &&
len(pod.Status.ContainerStatuses) == 0 {
}
2、社區(qū)有人提供通過(guò)在 kube-controller-manager 中配置 podgc controller --terminated-pod-gc-threshold 參數(shù)來(lái)自動(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。
總結(jié)
由于在之前的公司中對(duì)于穩(wěn)定性的高度重視,線(xiàn)上節(jié)點(diǎn)并未開(kāi)啟驅(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)的一些原理有一定的了解。
參考:
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/