ACK 节点 NodeHasDiskPressure / EvictionThresholdMet 告警排查:磁盘压力自愈,为何没有 Pod 被驱逐

ACK 节点 NodeHasDiskPressure / EvictionThresholdMet 告警排查:磁盘压力自愈,为何没有 Pod 被驱逐

半夜收到一条「高级」告警:集群驱逐事件 / Attempting to reclaim ephemeral-storage,节点 NodeHasDiskPressure。听起来要出大事——其实不用慌:这类告警里有相当一部分是磁盘压力瞬时越过阈值、几分钟内被 kubelet 自己回收化解,全程没有一个 Pod 被驱逐,业务无损。

本文记录一次完整判定过程:从 SLS 的 k8s-event 还原时间线,到上机用 kubelet 的真实磁盘数据定性,再讲清一个容易搞混的原理——为什么 ACK 报了 EvictionThresholdMet,却没有驱逐任何 Pod。看完你应该能在 5 分钟内判断这条告警「要不要起来处理」。

环境

平台 阿里云 ACK 托管版
告警节点 GPU 裸金属 ecs.ebmgn8t.32xlarge(8×GPU),已运行 539 天
磁盘 系统盘 528 GB,nodefs 与 imagefs 共用同一块盘(关键)
告警链路 ACK 集群事件 → SLS k8s-event logstore → 告警规则
排查工具 aliyun CLI 3.3.14(sls 插件)、kubectl
触发的告警 集群驱逐事件(高级)、集群节点磁盘空间不足(中级)

文中 <CLUSTER_ID>、节点名(示例统一用 192.168.1.1)、<REGION>、业务命名空间等均为占位,替换成你自己的即可。

收到的原始告警(两条)

如果你也是被 ACK 的 SLS 事件告警叫醒搜进来的,大概率收到的是下面这两条之一——关键字段我标在注释里:

① 集群驱逐事件(严重度:高级)

告警名称: 集群驱逐事件
告警状态: 触发
告警严重度: 高级
告警标签:
  - cluster_id: <CLUSTER_ID>
  - namespace:  default
  - node_name:  192.168.1.1
告警标注:
  - __config_app__: sls.app.ack
  - count:   1
  - message: ["Attempting to reclaim ephemeral-storage"]   # ← 核心:在“尝试回收”,不是已驱逐

② 集群节点磁盘空间不足(严重度:中级,通常和上一条同时来)

告警名称: 集群节点磁盘空间不足
告警严重度: 中级
告警标注:
  - __drill_down_query__: eventId.reason : NodeHasDiskPressure
      and "eventId.involvedObject.name": 192.168.1.1
  - message: ["Node 192.168.1.1 status is now: NodeHasDiskPressure"]

两条告警对应的底层事件 reason 分别是 EvictionThresholdMetNodeHasDiskPressure,messageAttempting to reclaim ephemeral-storage——这几个就是该粘进搜索框的关键词。下面先解释它们各自在说什么。

先读懂告警在说什么

告警正文里这几个词是核心:

  • EvictionThresholdMet + message: Attempting to reclaim ephemeral-storage
    kubelet 检测到 ephemeral-storage(即 nodefs) 越过了驱逐软阈值,正在「尝试回收」磁盘空间。注意:这是尝试回收,不等于已经驱逐 Pod。
  • NodeHasDiskPressure
    节点被打上 DiskPressure=True 这个 Condition,新 Pod 不再调度到该节点。
  • 告警的「严重度:高级」是规则配的,和实际影响不挂钩——所以不能只看严重度,要看后续到底有没有 Pod 被驱逐。

判断要不要紧急处理,只需回答两个问题:① 现在磁盘还满不满?② 期间有没有 Pod 真被驱逐? 下面逐个查。

步骤 1:用 SLS k8s-event 还原事件时间线

ACK 把集群事件投递到 SLS,排查第一步不上机,先在 SLS 里把这台节点的事件按时间拉出来。

先确认 logstore 名(注意子命令是 list-log-stores,不是 list-logstore):

aliyun sls list-log-stores --region <REGION> \
  --project k8s-log-<CLUSTER_ID>
{
  "count": 16,
  "logstores": [
    "apiserver-...", "audit-...", "k8s-event",
    "nginx-ingress", "scheduler-...", ...
  ]
}

事件在 k8s-event 里。SLS 查询用 Unix 时间戳,先把告警时间换算出来(告警是 CST,SLS 存的是 UTC 时间戳,用 epoch 不会错):

# 告警时间 2026-05-27 09:42:33 CST 对应的 epoch
TZ=Asia/Shanghai date -j -f "%Y-%m-%d %H:%M:%S" "2026-05-27 09:42:33" "+%s"
# -> 1779846153

然后按关键词捞这台节点相关的磁盘/驱逐事件(--from/--to 给一个覆盖告警前后的窗口):

aliyun sls get-logs-v2 --region <REGION> \
  --project k8s-log-<CLUSTER_ID> --logstore k8s-event \
  --from 1779843600 --to 1779847200 \
  --query 'reclaim or DiskPressure or FreeDiskSpace or "ephemeral-storage" or Evicted' \
  --line 100

把返回的 eventId(JSON)解析出 reason / type / lastTimestamp / message,得到这条时间线:

[2026-05-27 09:42:33] reason=EvictionThresholdMet  type=Warning  count=1
    obj=Node/192.168.1.1
    msg=Attempting to reclaim ephemeral-storage

关键:在整个窗口里 Evicted 命中数为 0——没有任何 Pod 被驱逐。单独再查一次坐实:

aliyun sls get-logs-v2 --region <REGION> \
  --project k8s-log-<CLUSTER_ID> --logstore k8s-event \
  --from 1779843600 --to 1779847200 \
  --query 'reason: Evicted' --line 100
# count: 0

到这里已经有一半结论:触发了回收,但没驱逐 Pod。 接着上机看现在还满不满。

步骤 2:上机看节点 Condition 与真实磁盘占用

上机用一台有集群读权限的运维机执行 kubectl。先看节点 Condition 现在是什么状态:

kubectl describe node 192.168.1.1 | grep -A2 -iE "DiskPressure|MemoryPressure|Ready"
MemoryPressure   False   ...   KubeletHasSufficientMemory
DiskPressure     False   ...   KubeletHasNoDiskPressure   <- 已恢复
Ready            True    ...   KubeletReady

DiskPressure=False 说明压力已经过去了。最准的磁盘数字不是 df,而是 kubelet 自己用于驱逐判定的 stats/summary(它统计的 nodefs/imagefs 才是 kubelet 真正看的口径):

kubectl get --raw "/api/v1/nodes/192.168.1.1/proxy/stats/summary" \
  | python3 -c '
import sys,json
d=json.load(sys.stdin)
fs=d["node"]["fs"]
print("nodefs : cap=%.0fGB used=%.0fGB avail=%.0fGB used%%=%.1f%%" % (
    fs["capacityBytes"]/1e9, fs["usedBytes"]/1e9,
    fs["availableBytes"]/1e9, 100*fs["usedBytes"]/fs["capacityBytes"]))
imfs=d["node"]["runtime"]["imageFs"]
print("imagefs: used=%.0fGB (镜像层占用)" % (imfs["usedBytes"]/1e9))
'
nodefs : cap=528GB used=184GB avail=322GB used%=35.0%
imagefs: used=104GB (镜像层占用)

现在只用了 35%、还空着 322 GB。 可 16 分钟前却越过了驱逐阈值——意味着当时短时间内被写满了上百 GB,又被快速回收。问题出在哪类数据?拆开看。

步骤 3:确认没有 Pod 被驱逐 + 找占用大头

再次确认节点上没有 Evicted 状态的 Pod(SLS 看的是事件,这里看的是当前对象):

kubectl get pods -A --field-selector spec.nodeName=192.168.1.1,status.phase=Failed \
  -o wide | grep -i evicted
# (无输出,0 个被驱逐)

列出该节点所有 Pod 的临时存储占用,看 nodefs 大头是不是 Pod 写的:

kubectl get --raw "/api/v1/nodes/192.168.1.1/proxy/stats/summary" \
  | python3 -c '
import sys,json
d=json.load(sys.stdin)
rows=[]
for p in d.get("pods",[]):
    eph=p.get("ephemeral-storage",{}).get("usedBytes",0)
    rows.append((eph, p["podRef"]["namespace"]+"/"+p["podRef"]["name"]))
for eph,name in sorted(rows,reverse=True)[:5]:
    print("%6.1f GB  %s" % (eph/1e9, name))
'
   3.7 GB  monitoring/arms-prom-gpu-exporter-xxxx
   1.4 GB  kube-system/csi-provisioner-xxxx   (日志)
   0.9 GB  prod/<GPU 推理服务>-xxxx
   ...

所有 Pod 的 ephemeral-storage 加起来才约 10 GB,而 imagefs 占了 104 GB。结论很清楚:撑满磁盘的不是 Pod 临时文件,而是容器镜像

步骤 4:根因 —— 大镜像顶到阈值,kubelet 镜像 GC 自愈

把数据串起来,根因就定了:

  • 这台节点跑的是 GPU 推理服务,镜像普遍很大(动辄数 GB,含 CUDA / 模型权重)。
  • nodefs 与 imagefs 共用同一块 528 GB 盘,镜像涨,nodefs 的可用空间就跟着掉。
  • 09:42 一次发版/扩缩容拉取大镜像,imagefs 短时把磁盘顶到 nodefs 驱逐软阈值 → kubelet 报 EvictionThresholdMetDiskPressure=True
  • 但 kubelet 的回收顺序是先删能删的:未被任何容器引用的镜像、已退出的容器 —— 镜像 GC 一跑,几分钟就把 104 GB 量级的旧镜像清掉一批,磁盘掉回 184 GB,DiskPressure 恢复 False
  • 整个过程不需要驱逐 Pod。

为什么报了 EvictionThresholdMet 却没驱逐 Pod?

这是最容易误解的点。kubelet 驱逐是分级、按顺序的:

  1. 磁盘越过软阈值(默认 nodefs.available<10%imagefs.available<15%)→ 先回收:删无用镜像、删已退出容器。Attempting to reclaim ephemeral-storage 就是这一步。
  2. 回收后空间够了 → 到此为止,不驱逐任何 Pod。 ← 本次就停在这。
  3. 只有回收完仍不达标,才会按 QoS / 优先级真正驱逐 Pod(此时才会看到 Evicted 事件)。

所以「EvictionThresholdMet 告警」≠「发生了驱逐」。判定有没有真出事,认准有没有 Evicted 事件 / Evicted 状态的 Pod 就行,这也是为什么前面两步都在反复确认 Evicted 计数为 0。

验证:这是这台节点的老毛病,且每次自愈

最后看历史,确认不是新增风险。统计这台节点 NodeHasNoDiskPressure(恢复事件)在节点生命周期里的次数:

kubectl describe node 192.168.1.1 | grep -iE "DiskPressure"
NodeHasNoDiskPressure   ...   (x27 over 539d)   kubelet has no disk pressure

539 天里反复进出磁盘压力 27 次,每次都自己恢复,从没造成驱逐。这就把性质坐实了:周期性、瞬时、可自愈,无需紧急处理

真正该做的是治本(降低复发),而不是每次半夜爬起来:

  • 给镜像盘扩容,或把 imagefs 拆到独立数据盘,别和 nodefs 抢空间;
  • 收敛镜像体积 / 减少同节点镜像版本堆积;
  • imagefs.available 的 kubelet GC 阈值调得更激进,让它更早回收、不要逼近驱逐线才动;
  • 告警规则上,把「EvictionThresholdMet 软阈值事件」与「真实 Evicted 驱逐事件」分级——前者降级或加「持续 N 分钟未恢复才告警」的抑制,后者才作为高级告警。

快速参考

一句话判定流程

收到磁盘/驱逐类告警 → 查 Evicted 事件数。为 0 → 自愈,不慌;>0 → 真驱逐,立即处理

kubelet 默认磁盘驱逐阈值(软)

信号 默认阈值 越过后的动作
nodefs.available < 10% 先删已退出容器,再不够才驱逐 Pod
imagefs.available < 15% 触发镜像 GC(删无用镜像),通常不驱逐 Pod

命令速查

# 1. SLS 列 logstore(注意是 list-log-stores)
aliyun sls list-log-stores --region <REGION> --project k8s-log-<CLUSTER_ID>

# 2. 拉某节点磁盘/驱逐事件时间线
aliyun sls get-logs-v2 --region <REGION> --project k8s-log-<CLUSTER_ID> \
  --logstore k8s-event --from <EPOCH> --to <EPOCH> \
  --query 'reclaim or DiskPressure or "ephemeral-storage" or Evicted' --line 100

# 3. 节点当前 Condition
kubectl describe node 192.168.1.1 | grep -A2 -iE "DiskPressure|Ready"

# 4. kubelet 真实磁盘口径(比 df 准)
kubectl get --raw "/api/v1/nodes/192.168.1.1/proxy/stats/summary"

# 5. 该节点有没有被驱逐的 Pod
kubectl get pods -A --field-selector spec.nodeName=192.168.1.1,status.phase=Failed | grep -i evicted

三条铁律

  1. 严重度 ≠ 影响:告警标的「高级」是规则配的,以 Evicted 实际计数为准。
  2. 磁盘看 kubelet stats,不要只看 df:驱逐判定用的是 nodefs/imagefs 口径。
  3. nodefs 与 imagefs 同盘的节点,镜像就是头号嫌疑:GPU/AI 业务尤甚,治本靠拆盘 + 控镜像。
posted @ 2026-05-28 14:51  Hello_worlds  阅读(8)  评论(0)    收藏  举报