一次K8s节点磁盘压力导致Pod批量驱逐的排查实录

问题背景

某日上午 10:00 左右,监控平台开始陆续推送 K8s 集群告警:多个 Pod 处于 Pending 或 Evicted 状态,部分服务出现间歇性不可用。

查看集群状态,发现有 3 个工作节点出现 DiskPressure 状态,kubelet 开始自动驱逐节点上的 Pod 以释放空间,导致相关服务的副本数不足,触发了监控告警。

集群为 K8s v1.26,节点操作系统为 CentOS 7,每个节点配置 100GB 系统盘(/ 分区)。


故障现象

  • kubectl get nodes 显示 3 个节点状态带有 DiskPressure 条件
  • 受影响节点上的 Pod 被驱逐(Evicted),重新调度到其他节点后其他节点也逐渐出现磁盘压力
  • kubectl describe node <node-name> 显示:
1
2
3
Conditions:
Type Status Reason Message
DiskPressure True KubeletHasDiskPressure imagefs is available: 2% < 10%
  • 被驱逐的 Pod 状态为 Evicted,大量积压:
1
2
kubectl get pods -A | grep Evicted | wc -l
# 输出:127

排查过程

第一步:查看节点磁盘使用情况

登录出现 DiskPressure 的节点(以 node-03 为例):

1
df -h

输出:

1
2
3
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1 97G 95G 1.8G 99% /
tmpfs 16G 0 16G 0% /dev/shm

系统盘使用率 99%!几乎满了。

第二步:找出磁盘占用大的目录

1
du -sh /* 2>/dev/null | sort -rh | head -20

输出:

1
2
3
42G     /var
38G /var/log
...

/var/log 占用了 38GB!进入查看:

1
du -sh /var/log/* | sort -rh | head -20

输出:

1
2
3
35G     /var/log/containers
2.1G /var/log/pods
890M /var/log/messages

/var/log/containers 占用 35GB,这是容器日志目录。

第三步:分析容器日志目录

1
ls -lhS /var/log/containers/ | head -20

发现有几个日志文件异常巨大:

1
2
3
-rw-r----- 1 root root 18G  Mar 12 09:55 business-api-xxx_default_business-api-xxxxxxx.log
-rw-r----- 1 root root 9G Mar 12 09:48 data-sync-xxx_default_data-sync-xxxxxxx.log
-rw-r----- 1 root root 6.2G Mar 12 08:30 log-collector-xxx_kube-system_log-collector-xxxxxxx.log

business-api 这个容器的日志文件高达 18GB,data-sync 的日志 9GB。

第四步:查看 kubelet 日志轮转配置

K8s 的 kubelet 支持对容器日志进行自动轮转,相关参数为:

1
cat /etc/kubernetes/kubelet-config.yaml | grep -i log

输出:

1
2
containerLogMaxSize: "10Mi"
containerLogMaxFiles: 5

等等——配置明明设置了单个日志文件最大 10MB,为什么实际文件有 18GB?

第五步:确认 kubelet 是否真正加载了该配置

1
ps aux | grep kubelet

输出:

1
/usr/bin/kubelet --config=/etc/kubernetes/kubelet-config.yaml ...

看起来配置文件是加载了的,但再仔细检查配置文件:

1
cat /etc/kubernetes/kubelet-config.yaml
1
2
3
4
5
6
7
8
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
containerLogMaxSize: "10Mi"
containerLogMaxFiles: 5
evictionHard:
memory.available: "200Mi"
nodefs.available: "10%"
imagefs.available: "10%"

配置看起来正确……但为什么没有生效?

第六步:深入排查日志轮转未生效的原因

查看 kubelet 的实际运行时日志:

1
journalctl -u kubelet --since "2 hours ago" | grep -i "log"

发现一条警告:

1
W0312 09:23:15.123456    1234 kubelet.go:567] "Failed to rotate container log" err="error truncating file: ..." containerID="..." 

日志轮转失败!原因是 error truncating file,进一步查看:

1
journalctl -u kubelet --since "2 hours ago" | grep -i "truncat"
1
W0312 ... "Failed to truncate log file" path="/var/log/containers/business-api-xxx.log" err="truncate /var/log/containers/business-api-xxx.log: read-only file system"

关键信息:read-only file system,磁盘挂载为只读了!

1
dmesg | tail -50 | grep -i "error\|readonly\|remount"
1
2
[1234567.890] EXT4-fs error (device sda1): ext4_journal_check_start:61: Detected aborted journal
[1234567.891] EXT4-fs (sda1): Remounting filesystem read-only

根本原因找到了:磁盘写满 → 文件系统出错(journal abort)→ 内核自动将文件系统 remount 为只读 → kubelet 无法轮转日志文件 → 日志文件不断增大(等等,如果文件系统只读,为什么日志还能写入?

再仔细看:发现这台节点上 business-api 容器使用了 hostPath 挂载,日志输出路径是 /data/logs/,挂载的是另一块数据盘(/dev/sdb),而 /dev/sdb 挂载的 /data 目录没有配置日志轮转,数据盘的日志一直增长,而我们之前找到的 18GB 日志文件是软链接,指向了 /data/logs/ 下的实际文件。

1
2
ls -la /var/log/containers/business-api-xxx.log
# lrwxrwxrwx ... /var/log/containers/business-api-xxx.log -> /data/logs/business-api/app.log

数据盘 /dev/sdb 也满了,但报错发生在系统盘 /dev/sda1。这是两个叠加的问题。


解决方案

立即处理

清理已驱逐的 Pod 记录(Evicted 状态的 Pod 不会自动清除):

1
kubectl get pods -A | grep Evicted | awk '{print $1, $2}' | xargs -n2 kubectl delete pod -n

清理超大日志文件(谨慎操作,先确认不影响业务):

1
2
3
4
5
# 对于仍在写入的日志,不要直接 rm,使用截断方式
> /data/logs/business-api/app.log

# 或使用 truncate
truncate -s 0 /data/logs/business-api/app.log

修复系统盘只读问题(需要重启或 remount):

1
2
3
# 运行 fsck(需要先 umount 或在 rescue 模式下操作)
# 在线 remount(临时恢复读写,不修复 journal 问题)
mount -o remount,rw /

长期修复

1. 为应用容器的 hostPath 日志添加 logrotate 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建 /etc/logrotate.d/business-api
cat > /etc/logrotate.d/business-api << 'EOF'
/data/logs/business-api/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
size 500M
postrotate
# 发送 SIGHUP 给应用重新打开日志文件(如应用支持)
kill -HUP $(cat /var/run/business-api.pid) 2>/dev/null || true
endscript
}
EOF

2. 对 K8s 容器日志设置合理的 size 限制和 TTL:

在应用的 K8s Deployment 中,使用 terminationMessagePolicy 和日志驱动层面控制。或推荐将应用日志输出到 stdout/stderr,由 kubelet 统一管理轮转(不使用 hostPath)。

3. 扩容数据盘并规划磁盘告警:

在磁盘使用率达到 70% 时告警,不等到 99% 再发现问题。


根因分析

两个问题叠加:

  1. 应用日志通过 hostPath 写入数据盘,且没有配置 logrotate,导致日志文件无限增长,最终耗尽数据盘空间
  2. 数据盘写满后的错误日志写到系统盘,导致系统盘也快速被填满,触发 EXT4 journal abort 和 read-only remount,引发 kubelet 日志轮转失败,进一步加速了磁盘占用

预防措施

  1. 所有使用 hostPath 挂载的应用必须配置 logrotate
  2. 容器日志优先使用 stdout/stderr + kubelet 自动轮转,不自行挂载日志目录
  3. 节点磁盘使用率告警阈值设置为 70%/85%/95% 三档
  4. 定期(每周)扫描所有节点上 /var/log/containers/ 和应用日志目录的大小

总结

K8s 集群中,Pod 驱逐事件往往是表象,真正的根因要深入到节点操作系统层面去排查。这次事件表明,日志管理是 K8s 运维中容易被忽视但影响显著的环节。无论是 kubelet 自带的日志轮转还是应用侧的 logrotate,都需要主动配置和验证,不能假设”默认会管好日志”。