我不会起名字322 · 后端 / 算法 / 数据库
文章目录
- [K8s 节点磁盘打满导致 Pod 被驱逐:4 类占用与回收顺序](#K8s 节点磁盘打满导致 Pod 被驱逐:4 类占用与回收顺序)
-
- [一、先搞清:kubelet 有两类阈值,不是一个](#一、先搞清:kubelet 有两类阈值,不是一个)
- [二、第 1 类:镜像层堆积](#二、第 1 类:镜像层堆积)
- [三、第 2 类:容器日志](#三、第 2 类:容器日志)
- [四、第 3 类:emptyDir 与临时数据](#四、第 3 类:emptyDir 与临时数据)
- [五、第 4 类:文件系统碎片与 reserve 块](#五、第 4 类:文件系统碎片与 reserve 块)
- [六、回收顺序:kubelet 先杀谁?](#六、回收顺序:kubelet 先杀谁?)
- 七、一个急救清单(按优先级)
- 八、总结
K8s 节点磁盘打满导致 Pod 被驱逐:4 类占用与回收顺序
凌晨三点,告警连响:一批 Pod 被 Evicted。上去一看 kubectl describe node,满屏 DiskPressure------节点根分区使用率 92%。
这是个很典型的"连锁事故":磁盘涨 → kubelet 判定 DiskPressure → 开始驱逐 Pod → 业务抖动。这篇说清 kubelet 的驱逐逻辑,拆 4 类磁盘占用,给出"先回收谁"的顺序表。
一、先搞清:kubelet 有两类阈值,不是一个
很多人以为节点磁盘只有一个"满了就驱逐"的开关。实际上 kubelet 管着两个文件系统,各有软硬两档阈值:
| 信号 | 监控对象 | 影响 |
|---|---|---|
nodefs |
节点根文件系统(/var/lib/kubelet 所在) |
影响 Pod 创建、日志、emptyDir |
imagefs |
容器运行时镜像存储(/var/lib/docker 或 containerd 目录) |
影响镜像拉取、容器启动 |
每类又有两个阈值:
- 软阈值(soft) :达到后 kubelet 开始尝试驱逐 ,但有宽限期(默认
soft 阈值 + 2 分钟); - 硬阈值(hard) :达到后 kubelet 立即驱逐,不给宽限期。
bash
# 看节点当前状况和驱逐条件(含各类阈值)
kubectl describe node <node> | sed -n '/Conditions:/,/Addresses:/p'
kubectl describe node <node> | grep -A12 "Allocatable"
# 直接看 kubelet 的阈值配置
kubectl get --raw "/api/v1/nodes/<node>/proxy/configz" | python -m json.tool | grep -A5 eviction
关键判据 :DiskPressure=True 且 Reason=KubeletHasDiskPressure。而真正决定"先杀谁"的,是下一节的回收顺序。
二、第 1 类:镜像层堆积
最大的隐形占用。每次 CI 构建推一个新 tag,节点就多拉一份镜像;旧镜像不会自动删(除非你配了 GC 阈值)。
bash
# containerd 环境
crictl images | wc -l
crictl images | awk '{print $3}' | head # SIZE 列
# 或直接看目录占用
du -sh /var/lib/containerd/io.containerd.content.v1.content 2>/dev/null
# docker 环境
docker system df
注意:同一个应用的多个历史版本镜像,可能每个 500MB~1GB。如果一天发 5 次版、留 30 天,光镜像就能吃掉几十 G。
三、第 2 类:容器日志
容器 stdout/stderr 被写到节点的 /var/log/pods/ 或 /var/lib/docker/containers/,不受 json-file 的 max-size 以外任何限制。一个疯狂打日志的应用能在一夜之间写满磁盘。
bash
# 找最大的容器日志
du -sh /var/log/pods/* 2>/dev/null | sort -rh | head -10
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -rh | head -10
修法(治本):
yaml
# kubelet 配置里限制单容器日志体积
containerLogMaxSize: "50Mi"
containerLogMaxFiles: 5
以及在应用层降日志级别 ------DEBUG 级别在生产环境打日志,是磁盘杀手。
四、第 3 类:emptyDir 与临时数据
emptyDir 卷落在节点磁盘上(除非指定了 medium: Memory)。它生命周期跟 Pod 走,但如果 Pod 一直在跑、往里写数据不清理,就会持续膨胀。
bash
# 定位哪个 Pod 的 emptyDir 在涨
kubectl get pods --all-namespaces -o json | \
python -c "
import json,sys
d=json.load(sys.stdin)
for p in d['items']:
for v in p['spec'].get('volumes',[]):
if 'emptyDir' in v: print(p['metadata']['namespace'], p['metadata']['name'], v['name'])
"
还有一类是日志文件写到 emptyDir 里(应用自定义日志路径指向挂载点)------排查时别只看容器的 stdout。
五、第 4 类:文件系统碎片与 reserve 块
/var/lib/docker 或 overlay2 目录文件数极多时 (每个镜像层是一堆小文件),会消耗 inode 和元数据空间,导致"df 显示还有空间,但写不进去"。
bash
# 同时看空间和 inode
df -h /var/lib/docker
df -i /var/lib/docker # inode 用满一样写不了
六、回收顺序:kubelet 先杀谁?
这是本篇最有价值的部分。磁盘压力下 kubelet 的顺序是:先驱逐 Pod,再 GC 镜像------很多人以为是反过来的。
原因:镜像 GC 是"低优先级、被动"的,kubelet 认为先腾出 Pod 相关的临时空间更快。所以你会看到 Pod 被杀了、镜像还在。
内置的驱逐排序(kubelet 的 eviction ranking)大致是:
- 超出 requests 最多的 Pod;
- 使用量 > requests 的 Pod;
- 按 Pod 优先级(
priorityClass)低的先杀; - 同优先级下,创建时间晚的先杀。
imagefs 相关的 GC 阈值可以调:
bash
# kubelet 启动参数(或 config 文件)
--image-gc-high-threshold=85 # 使用率超 85% 开始 GC 镜像
--image-gc-low-threshold=80 # GC 到 80% 停止
--eviction-hard=imagefs.available<10%,nodefs.available<10%
--eviction-soft=nodefs.available<15%
--eviction-soft-grace-period=nodefs.available=2m

一个可带走物 :把上面这段"先驱逐后 GC"的顺序记牢------别指望删镜像能立刻止住驱逐,真正的急救是先找出写爆磁盘的那个 Pod/日志。
七、一个急救清单(按优先级)
| 顺序 | 动作 | 命令 |
|---|---|---|
| 1 | 找出吃磁盘的容器日志 | `du -sh /var/log/pods/* |
| 2 | 清理最大的日志文件(确认可删) | truncate -s 0 <logfile> |
| 3 | 手动触发镜像清理 | crictl rmi --prune / docker image prune -a |
| 4 | 检查 emptyDir 大户 | 上面的 Pod 扫描脚本 |
| 5 | 长期:配 containerLogMaxSize + image-gc 阈值 | kubelet config |
八、总结
- kubelet 管 nodefs 和 imagefs 两套阈值,各有软/硬两档;
- 4 类占用:镜像层、容器日志、emptyDir、文件系统碎片;
- 回收顺序是先驱逐 Pod、后 GC 镜像 ------ 删镜像止不住正在发生的驱逐;
- 急救的第一刀是找日志 ,长期要配
containerLogMaxSize+image-gc阈值。
留个问题给你:你们的镜像 GC 阈值用的默认 85%,还是调过?有没有遇到过 Pod 被驱逐了但镜像一个没删的情况?