線上被驅逐實例數據
最近在線上發現很多實例處於 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?)[1]。
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(status) == strings.ToLower(podEvictedStatus) && 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[2]。
總結
由於在之前的公司中對於穩定性的高度重視,線上節點並未開啟驅逐實例的功能,因此也不會存在 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/
引用鏈接
[1]kubelet counts active page cache against memory.available (maybe it shouldn’t?): https://github.com/kubernetes/kubernetes/issues/43916
[2]Kubelet does not delete evicted pods: https://github.com/kubernetes/kubernetes/issues/55051