TT Lab
开始
学习 学习路径 课程

Kubernetes 运维实务

节点死亡后的五分钟是谁定的

在 TT Lab 中继续学习

一句话总结

节点出问题时,节点控制器会加上 NoExecute 污点,从那一刻起,Pod 何时消失由 Pod 的 tolerationSeconds 决定。大多数 Pod 从来没有写过这个值,它们靠准入(admission)自动加入的 300 秒在运行。

为什么要了解这个值

drain 是人来执行的。时间和对象都由我们来选。然而凌晨被叫起来处理的事故,往往恰恰相反—— 没有人敲过任何命令,Pod 却开始到处迁移,有的工作负载迁得太快,丢光了本地缓存, 有的工作负载则在已经死掉的节点上挂得太久,导致流量流向了空的地方。这两件事之所以会在同一个集群里同时发生, 原因很简单。因为这两个工作负载用的是同一个值。

这个值是 300 秒。Kubernetes 在创建 Pod 时,会自动为 node.kubernetes.io/not-ready 和 node.kubernetes.io/unreachable 这两个键加上 tolerationSeconds 为 300 的 NoExecute 容忍度。 只有在你或控制器没有明确指定这些容忍度时才会如此。正如官方文档所说, 由于这个自动添加的容忍度,Pod 在检测到问题之后会被绑在节点上 5 分钟。

工作原理

先看节点一侧。kubelet 会定期更新自己的 Lease,以告知自己还活着。如果这个更新中断, 节点控制器会把节点的 Ready 状况改为 Unknown,并加上与之对应的污点。 状况与污点是这样配对的。

节点状况 所加的污点 默认效果
Ready = False node.kubernetes.io/not-ready NoExecute
Ready = Unknown node.kubernetes.io/unreachable NoExecute
MemoryPressure node.kubernetes.io/memory-pressure NoSchedule
DiskPressure node.kubernetes.io/disk-pressure NoSchedule
PIDPressure node.kubernetes.io/pid-pressure NoSchedule
NetworkUnavailable node.kubernetes.io/network-unavailable NoSchedule

重要的是,调度器看的不是节点状况,而是污点。如果让它直接看状况,放置决策会按状况的种类分裂, 而一旦先把状况翻译成污点,放置和驱逐就可以用同一条规则来处理。

接下来是效果的区别。NoSchedule 只阻止放置新的 Pod,不会动已经在运行的 Pod。 PreferNoSchedule 是它较弱的版本。只有 NoExecute 会把已经在运行的 Pod 赶出去。这时 容忍度分成三种情况。没有能容忍它的容忍度,就立即驱逐;有能容忍的容忍度但没有 tolerationSeconds, 就永远留下;写了秒数,则撑过那么多秒后被驱逐。如果污点在此之前被撤掉,驱逐就不会发生。

DaemonSet 是例外。由 DaemonSet 控制器创建的 Pod,会被加上针对 not-ready 和 unreachable 这两个键的、 不带 tolerationSeconds 的 NoExecute 容忍度。因为如果节点一有风吹草动,连节点代理也被赶走, 观测或恢复该节点的手段也就随之消失了。

还有一点。为了避免驱逐一下子扎堆发生,控制平面会限制给节点加新污点的速度。 这是一种在大规模网络中断时,防止整个集群瞬间被重新放置的装置。 并且从 1.29 起,这个驱逐的实现从节点控制器中分离出来,由名为 taint-eviction-controller 的 独立控制器负责。

在现场相遇的样子

最常见的事故是网络短暂抖动时,有状态工作负载整体迁走。 明明只是 40 秒的交换机重启,300 秒之后 Pod 已经在另一个节点上面对着空的本地磁盘,从头开始 接收数据了。对这类工作负载,把 tolerationSeconds 设长一些更好——官方文档也写道, 对于本地状态较多的应用,你可能希望在网络中断时让它们在节点上绑得更久, 并举了 6000 秒的例子。

反方向的事故也来自同一个值。节点真的死了,服务副本却在 300 秒内一直处于不足的状态运行。 像前端这样在哪里启动都一样的工作负载,这 5 分钟就是整段的损失。不过这种地方, 与其只缩小这个值,通常不如增加副本并分散拓扑——只缩小这个值,每次抖动 都会导致频繁重新放置,反而更不稳定。

第三种是忘记撤掉污点。为了检修而加上 NoExecute 把 Pod 清空,检修结束后如果不删除污点, 这个节点就会一直空着。集群容量减少了三分之一、就这样过去几周,这并不少见。

本实验环境的局限

这个集群的节点是 kwok 创建的模拟节点,没有真正的 kubelet。所以无法通过切断心跳来 让节点控制器自己加上污点。而且这里我们亲自测量了一件事—— 即使手动给 Ready 的节点加上 node.kubernetes.io/unreachable:NoExecute,它也会在几秒内消失。 这两个键的污点由节点控制器根据节点状况直接管理, 所以加在状况正常的节点上的污点会被当作错误状态而撤走(直接修改节点 status、把 Ready 改为 False, kwok 也会立刻改回来)。因此实验中用自定义键来设置同样的 NoExecute 污点。只是键不同,驱逐规则 与字面上完全相同,而随后发生的驱逐——谁在什么时候消失——是真正的控制器做出的真实行为。

另外,kwok 的 Pod 不是容器,所以既没有容器日志,也没有 exec,也没有 OOM。Pod 迁移后重新接收数据 的场景在这个环境里看不到,我们能看到的是对象消失的时刻。 本实验处理的,恰恰就是这个时刻。

下一项实验要做什么

在一个节点上放置不去动的对比组,亲眼确认没有人写过的默认容忍度。 然后把仅容忍时间不同的两个 Pod 放到另一个节点上,看到 NoSchedule 下什么都不会发生, 而 NoExecute 下其中一个真的消失了。接着在另一个节点上设置模拟链路中断的 NoExecute 污点,测量 20 秒与 3600 秒的差异,并编写一个脚本,计算整个命名空间的驱逐时间表。 最后,为有状态的工作负载确定要使用的值,应用到集群中,并留下依据。

参考文档: