节点 NotReady 是判出来的,不是崩出来的:340 秒

1 阅读10分钟

节点上什么都没崩,Pod 却被删了:NotReady 是一纸判决,不是事故现场

节点失联判定与驱逐|K8s 深入理解

凌晨两点,告警群炸了:worker1 NotReady。你 SSH 上去——kubelet 在跑,containerd 在跑,容器一个个活得好好的。节点上什么都没崩。

但在 apiserver 眼里,这台机器已经失联五分多钟,上面的 Pod 正被逐个删除、在别的节点重建。上一篇我们把 drain 卡住的锅甩给了 PDB,这一篇讲另一条没人申请就自动发动的驱逐:心跳怎么报、40 秒怎么判、Pod 怎么被删——以及为什么说 NotReady 是"判"出来的,不是"崩"出来的。

一、心跳:节点活着的唯一证据

先立一条前提:控制面从不主动检查节点。apiserver 只有 exec/logs 这类操作才会主动连节点的 10250 端口,其余时间只等节点上报——控制面没有"探测"能力,只有"数心跳"的能力。

kubelet 定期干两件事:更新 NodeStatus(Ready 等一揽子条件),以及在 kube-node-lease 命名空间续约一个小小的 Lease 对象。

为什么搞两个通道?【从业者判断】老机制的心跳就是更新 NodeStatus 本体——它带着 capacity、conditions、地址、镜像一整坨,几千节点按秒级上报,etcd 的写入直接被打爆。Lease 只有几个字段、续约近乎免费,心跳于是搬进小对象,NodeStatus 降频为条件变化时才报。

kube-system 里的 Lease 还顺便给 controller-manager、scheduler 选主,一处机制两处复用。这笔账和控制器读本地缓存是同一个思路:高频路径必须便宜,贵的数据低频走。

心跳从"交体检报告"变成"签到",让"活着"说得足够便宜。

眼见为实:

# 每个节点一条 Lease
kubectl -n kube-node-lease get lease
# NAME      HOLDER     AGE
# worker1   worker1    30d

# 看心跳本体:隔几秒再跑一次,renewTime 一直在往前跳
kubectl -n kube-node-lease get lease worker1 -o yaml | grep -E 'renewTime|holderIdentity'
# holderIdentity: worker1
# renewTime:      "2026-10-02T18:00:05.000000Z"

renewTime 的跳动,就是节点在控制面眼里的生命体征。

二、40 秒判决:判的是失联,不是宕机

心跳断了谁来判?不是 apiserver,是 controller-manager 里的 node-lifecycle 控制器——几十个控制循环中专管节点生死的那一个。判据一条:超过 node-monitor-grace-period(默认 40 秒)没收到心跳。

判决内容两件事:Node 的 Ready 条件翻转(get nodes 里的 NotReady 就是外显),同时打上 NoExecute 污点——心跳断时 Ready 翻为 Unknown,污点是 node.kubernetes.io/unreachable;节点自己显式上报 NotReady(Ready=False)时才是 node.kubernetes.io/not-ready。之后,驱逐逻辑介入。

三个推论,个个反直觉。

判的是失联,不是宕机——整条链上没人看过节点一眼。 40 秒里没有任何组件真正检查过节点。网络分区时节点和容器全都健康,照样被判 NotReady、照样被驱逐,依据只有"你没说话"。

第二,kubelet 死了不等于容器死了。【从业者判断】kubelet 挂掉时 containerd 还在,容器继续跑——驱逐删掉的只是 API 里的 Pod 对象,失联节点上的旧容器收不到终止指令,要等节点恢复才被真正清理。节点 NotReady 和服务恢复之间,隔着一整条判决链。

第三,判决会误伤:网络抖 40 秒就足以触发一次。【从业者判断】grace-period 调小恢复快但误判多,调大则相反——默认 40 秒是官方拍的板。

判决书写在 Node 对象上,四条命令看到全文:

kubectl get nodes
# NAME      STATUS     ROLES    AGE   VERSION
# worker1   NotReady   <none>   30d   v1.31.1

# 判决书:心跳停在哪一刻
kubectl describe node worker1 | grep -A5 Conditions
#   Ready   Unknown/False   LastHeartbeatTime: 停在几分钟前

# 污点是执行机关
kubectl describe node worker1 | grep -A3 Taints
#   node.kubernetes.io/unreachable:NoExecute ...
#   (心跳断 → Ready=Unknown,对应 unreachable 污点;
#    节点显式上报 NotReady 时,才是 not-ready)

# 排除干扰项:维护后忘了解封
kubectl get node worker1
# NotReady,SchedulingDisabled = 有人 cordon/drain 过没恢复,uncordon 即可

三、时间线推演:从最后一次心跳到最后一个 Pod 被删

设 T0 为最后一次成功的 Lease 续约,此后心跳中断:

T0         最后一次心跳成功(kubelet 卡死 / 网络中断)

T0+40s     node-monitor-grace-period 耗尽
           → node-lifecycle 判 NotReady、打污点
           → 调度器不再往这台节点放任何新 Pod

T0+40s     NoExecute 污点生效,多数 Pod 带着默认注入的
           容忍:tolerationSeconds=300,倒计时开始

T0+340s    容忍到期,驱逐逻辑开始删 Pod
           → 控制器在健康节点重建副本

T0+340s+   每个 Pod 再走自己的终止宽限期,流量切到新副本

关键数字就是 340 = 40 + 300:40 秒是控制面的耐心,300 秒是 Pod 的缓冲。

300 秒不是你配置的,是 apiserver 的准入控制(DefaultTolerationSeconds 插件)在 Pod 创建时自动注入的——只要没自己写这条容忍,就默认带上 not-ready 和 unreachable 两条 NoExecute 容忍、各 300 秒:

kubectl run demo --image=nginx:1.27 --restart=Never
kubectl get pod demo -o jsonpath='{.spec.tolerations}' | tr ',' '\n'
#  {"key":"node.kubernetes.io/not-ready"
#   "effect":"NoExecute"
#   "tolerationSeconds":300}
#  {"key":"node.kubernetes.io/unreachable"
#   "effect":"NoExecute"
#   "tolerationSeconds":300}
kubectl delete pod demo

你没写过的容忍,是准入控制替你写的——300 秒就是这么来的。

两个直接结论。

第一,算 SLA 按 340 秒起步:单节点故障,副本从"消失"到"别处 Ready"至少 5 分 40 秒,还要加每个 Pod 的终止宽限期。多副本的恢复预期按这个数算,别按"马上切走"的直觉算。复盘时它也是最有说服力的证据链:告警时间、污点出现时间、Pod 删除时间,三个点连起来就是判决全程。

第二,失联驱逐 PDB 拦不住,唯一缓冲是那 300 秒。上一篇讲过 PDB 只管自愿中断——节点失联属于非自愿中断,驱逐不等预算、直接执行。想给关键服务更长缓冲,显式写一条:

tolerations:
- key: node.kubernetes.io/unreachable
  effect: NoExecute
  tolerationSeconds: 3600   # 给慢启动的有状态服务留一小时

【从业者判断】代价:缓冲越长,真故障时流量切得越慢;但节点若只是网络分区,长容忍能避免无谓搬家——赌的是"多数失联是网络,不是机器"。

四、根因分诊树:五个嫌疑人,一人一条命令

判决链解释"谁删了你的 Pod",不解释"心跳为什么停"。分诊管后者,入口就一问:

Q: 节点纯 NotReady(无 SchedulingDisabled)→ SSH 上去
   第一问:kubelet 活着吗?
 ├─ systemctl status kubelet → inactive/dead
 │    └─ journalctl -u kubelet 查退出原因:
 │         ├─ 检测到 swap → swapoff -a(fstab 没注释净,重启又犯)
 │         └─ x509 报错   → 证书过期,跳嫌疑人四
 ├─ kubelet active,日志刷 pleg is not healthy
 │    └─ 查运行时:sudo timeout 5 crictl ps
 │         ├─ 5 秒不返回 → containerd 卡死(多半是磁盘 IO)
 │         └─ containerd inactive → start 并设自启
 └─ kubelet active,日志干净
      └─ 下沉系统层:df -h / free -h / dmesg
           └─ 磁盘打满(日志/镜像/emptyDir)既是独立根因也是 PLEG 诱因

五个嫌疑人过堂:

嫌疑人第一条命令特征现场修复
kubelet 挂/被停systemctl status kubeletinactive;日志见检测到 swapsystemctl enable --now kubelet;swapoff -a
containerd 挂/卡sudo timeout 5 crictl pscrictl 不返回;或 inactivesystemctl restart containerd,查磁盘 IO
磁盘压力df -h分区 100%,日志/镜像堆积清日志与悬空镜像,扩盘
证书过期sudo kubeadm certs check-expirationjournalctl 刷 x509: certificate has expiredsudo kubeadm certs renew all 后重启组件
PLEG not healthyjournalctl -u kubelet | grep 'pleg'每秒的容器全量 list 超时治 containerd 和磁盘,别查网络

重点说第五个,它最迷惑人。kubelet 不收运行时的事件推送,而是每秒全量 list 一次容器状态、跟上次对比,自己生成"谁启动了、谁退出了"——这就是 PLEG。containerd 一卡顿,relist 就超时,日志开始刷 pleg is not healthy,节点随之 NotReady。

所以 PLEG 报的病,根子多半在 containerd——先查运行时,别查网络。五秒定罪:sudo timeout 5 crictl ps,五秒不返回,运行时罪名坐实。

【从业者判断】证书和 swap 各提醒一句:kubeadm 证书默认一年,过期是周期性事故,把到期日上日历;节点重启后 fstab 里的 swap 复活、kubelet 检测到直接退出,是"重启后莫名 NotReady"的头号解释。

五、和 drain 对照:一个是判决,一个是搬家申请

同样是"节点上的 Pod 被搬走",drain 和这条链性质完全不同:

被动判定链(本文)drain 主动维护
发起者node-lifecycle 控制器人
触发条件40 秒没心跳你敲 kubectl drain
是否等 PDB不等,非自愿中断拦不住等,Eviction API 预算不足就卡住重试
时间线40+300 秒,默认值说了算你说了算

drain 是搬家申请,NotReady 是缺席判决。 drain 内部自带 cordon,先保证"删一个不会被调度回本节点"再动手;判决链的对应动作是污点,调度器自动绕开。上一篇 drain 卡 40 分钟,是 PDB 在替你把关;这一篇的驱逐 PDB 拦不住——集群已认定节点没了,预算让位于自愈。两条路合起来,才是节点上 Pod 离开的完整地图。

还要会认混合态:NotReady,SchedulingDisabled = 判决链和维护链撞在同一台节点。先修 NotReady(走第四节),再 uncordon,顺序别反。

六、修复动作清单

# [master] 1. 先看判决书:心跳停在哪一刻、污点是什么
kubectl describe node worker1 | grep -A5 Conditions
kubectl describe node worker1 | grep -A3 Taints

# [worker1] 2. 修根因(对照第四节),两个服务一起看
sudo systemctl status kubelet containerd --no-pager

# [master] 3. 心跳恢复后 Ready 自动翻转——这条链不需要 uncordon
kubectl get node worker1
# STATUS 回到 Ready;被驱逐的副本早被控制器重建,无需搬回

# [master] 4. 只有出现过 SchedulingDisabled 才需要解封
kubectl uncordon worker1

# [master] 5. 回归验证:副本分布与服务健康
kubectl get pods -A -o wide | grep worker1

三个容易翻车的收尾细节:

  • 修完仍 NotReady,回第四节换嫌疑人,kubelet 和 containerd 都要查;
  • 证书续期后组件不自动换证,控制面静态 Pod 要重启才生效;
  • 被驱逐的副本不用手动搬回,uncordon 只恢复调度——回流靠滚动更新,急验证可 rollout restart。

修复的终点不是节点 Ready,是服务在别处恢复接客。

现在就能做的事

  • 零门槛档:跑 kubectl -n kube-node-lease get lease 看心跳;再挑个 Pod 看 tolerations,找到那条你没写过的 tolerationSeconds: 300;
  • 动手档:可糟蹋的节点上 sudo systemctl stop kubelet,掐表看三件事——约 40 秒时 Conditions 变化、污点出现、约 340 秒时 Pod 被删,然后 start 复原。六分钟,比读十篇文档都牢。

30 秒自检三问:核心服务的 tolerationSeconds 是多少?上次 NotReady 你 SSH 下去的第一条命令是什么?节点证书下次到期是哪天?

评论区聊两件事:一,你见过最冤的一次 NotReady——节点明明没坏,凶手最后是谁;二,默认 300 秒你们动过吗,调大还是调小。

这些内容整理在我维护的学习仓库,GitHub 搜 sre-learning-hub:架构与节点维护两章是底稿,scripts/faults 里还有 break-kubelet 故障注入脚本,可以在测试集群亲手复现这条 340 秒链路。觉得有用点个 star。