最近在查看一个 kubernetes 集群中 node not ready 的奇怪现象, 顺便阅读了一下 kubernetes kube-controller-manager 中管理 node 健康状态的组件 node lifecycle controller. 我们知道 kubernetes 是典型的 master-slave 架构, master node 负责整个集群元数据的管理, 然后将具体的启动执行 pod 的任务分发给各个 salve node 执行, 各个 salve node 会定期与 master 通过心跳信息来告知自己的存活状态. 其中 slave node 上负责心跳的是 kubelet 程序, 他会定期更新 apiserver 中 node lease 或者 node status 数据, 然后 kube-controller-manager 会监听这些信息变化, 如果一个 node 很长时间都没有进行状态更新, 那么我们就可以认为该 node 发生了异常, 需要进行一些容错处理, 将该 node 上面的 pod 进行安全的驱逐, 使这些 pod 到其他 node 上面进行重建. 这部分工作是由 node lifecycel controller 模块负责.
在目前的版本 (v1.16) 中, 默认开启了 TaintBasedEvictions, TaintNodesByCondition 这两个 feature gate, 则所有 node 生命周期管理都是通过 condition + taint 的方式进行管理. 其主要逻辑由三部分组成:
不断地检查所有 node 状态, 设置对应的 condition
不断地根据 node condition 设置对应的 taint
不断地根据 taint 驱逐 node 上面的 pod
一. 检查 node 状态
检查 node 状态其实就是循环调用 monitorNodeHealth 函数, 该函数首先调用 tryUpdateNodeHealth 检查每个 node 是否还有心跳, 然后判断如果没有心跳则设置对应的 condtion.
node lifecycle controller 内部会维护一个 nodeHealthMap 数据结构来保存所有 node 的心跳信息, 每次心跳之后都会更新这个结构体, 其中最重要的信息就是每个 node 上次心跳时间 probeTimestamp, 如果该 timestamp 很长时间都没有更新(超过 --node-monitor-grace-period 参数指定的值), 则认为该 node 可能已经挂了, 设置 node 的所有 condition 为 unknown 状态.
gracePeriod, observedReadyCondition, currentReadyCondition, err = nc.tryUpdateNodeHealth(node)
tryUpdateNodeHealth 传入的参数为每个要检查的 node, 返回值中 observedReadyCondition 为当前从 apiserver 中获取到的数据, 也就是 kubelet 上报上来的最新的 node 信息, currentReadyCondition 为修正过的数据. 举个例子, 如果 node 很长时间没有心跳的话, observedReadyCondition 中 nodeReadyCondion 为 true, 但是 currentReadyCondion 中所有的 conditon 已经被修正的实际状态 unknown 了.
如果 observedReadyCondition 状态为 true, 而 currentReadyCondition 状态不为 true, 则说明 node 状态状态发生变化, 由 ready 变为 not-ready. 此时不光会更新 node condition, 还会将该 node 上所有的 pod 状态设置为 not ready, 这样的话, 如果有对应的 service 资源选中该 pod, 流量就可以从 service 上摘除了, 但是此时并不会直接删除 pod.
node lifecycle controller 会根据 currentReadyCondition 的状态将该 node 加入到 zoneNoExecuteTainter 的队列中, 等待后面设置 taint. 如果此时已经有了 taint 的话则会直接更新. zoneNoExecuteTainter 队列的出队速度是根据 node 所处 zone 状态决定的, 主要是为了防止出现集群级别的故障时, node lifecycle controller 进行误判, 例如交换机, loadbalancer 等故障时, 防止 node lifecycle controller 错误地认为所有 node 都不健康而大规模的设置 taint 进而导致错误地驱逐很多 pod, 造成更大的故障.
设置出队速率由 handleDisruption 函数中来处理, 首先会选择出来各个 zone 中不健康的 node, 并确定当前 zone 所处的状态. 分为以下几种情况:
Initial: zone 刚加入到集群中, 初始化完成.
Normal: zone 处于正常状态
FullDisruption: 该 zone 中所有的 node 都 notReady 了
PartialDisruption: 该 zone 中部分 node notReady, 此时已经超过了 unhealthyZoneThreshold 设置的阈值
对于上述不同状态所设置不同的 rate limiter, 从而决定出队速度. 该速率由函数 setLimiterInZone 决定具体数值, 具体规则是:
当所有 zone 都处于 FullDisruption 时, 此时 limiter 为 0
当只有部分 zone 处于 FullDisruption 时, 此时 limiter 为正常速率:
--node-eviction-rate
如果某个 zone 处于 PartialDisruption 时, 则此时 limiter 为二级速率:
--secondary-node-eviction-rate
二. 设置 node taint
根据 node condition 设置 taint 主要由两个循环来负责, 这两个循环在程序启动后会不断执行:
doNodeProcessingPassWorker
中主要的逻辑就是:
doNoScheduleTaintingPass
, 该函数会根据 node 当前的 condition 设置 unschedulable 的 taint, 便于调度器根据该值进行调度决策, 不再调度新 pod 至该 node.
doNoExecuteTaintingPass
会不断地从上面提到的
zoneNoExecuteTainter
队列中获取元素进行处理, 根据 node condition 设置对应的 NotReady 或 Unreachable 的 taint, 如果 NodeReadycondition 为 false 则 taint 为 NotReady, 如果为 unknown, 则 taint 为 Unreachable, 这两种状态只能同时存在一种!
上面提到从 zoneNoExecuteTainter 队列中出队时是有一定的速率限制, 防止大规模快速驱逐 pod. 该元素是由 RateLimitedTimedQueue 数据结构来实现:
- // RateLimitedTimedQueue is a unique item priority queue ordered by
- // the expected next time of execution. It is also rate limited.
- type RateLimitedTimedQueue struct {
- queue UniqueQueue
- limiterLock sync.Mutex
- limiter flowcontrol.RateLimiter
- }
从其定义就可以说明了这是一个 去重的优先级队列, 对于每个加入到其中的 node 根据执行时间 (此处即为加入时间) 进行排序, 优先级队列肯定是通过 heap 数据结构来实现, 而去重则通过 set 数据结构来实现. 在每次 doNoExecuteTaintingPass 执行的时候, 首先尽力从 TokenBucketRateLimiter 中获取 token, 然后从队头获取元素进行处理, 这样就能控制速度地依次处理最先加入的 node 了.
三. 驱逐 pod
在 node lifecycle controller 启动的时候, 会启动一个 NoExecuteTaintManager. 该模块负责不断获取 node taint 信息, 然后删除其上的 pod.
首先会利用 informer 会监听 pod 和 node 的各种事件, 每个变化都会出发对应的 update 事件. 分为两类: 1. 优先处理 nodeUpdate 事件; 2. 然后是 podUpdate 事件
对于 nodeUpdate 事件, 会首先获取该 node 的 taint, 然后获取该 node 上面所有的 pod, 依次对每个 pod 调用 processPodOnNode: 判断是否有对应的 toleration, 如果没有则将其加入到对应的 taintEvictionQueue 中, 该 queue 是个定时器队列, 对于队列中的每个元素会有一个定时器来来执行, 该定时器执行时间由 toleration 中的 tolerationSecond 进行设置. 对于一些在退出时需要进行清理的程序, toleration 必不可少, 可以保证给容器退出时留下足够的时间进行清理或者恢复. 出队时调用的是回调函数 deletePodHandler 来删除 pod.
对于 podUpdate 事件则相对简单, 首先获取所在的 node, 然后从 taintNode map 中获取该 node 的 taint, 最后调用 processPodOnNode, 后面的处理逻辑就同 nodeUpdate 事件一样了.
为了加快处理速度, 提高性能, 上述处理会根据 nodename hash 之后交给多个 worker 进行处理.
上述就是 controller-manager 中心跳处理逻辑, 三个模块层层递进, 依次处理, 最后将一个异常 node 上的 pod 安全地迁移.
来源: https://www.cnblogs.com/gaorong/p/12312590.html