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

CRD 与 Operator

出现两个领导者的那一刻

在 TT Lab 中继续学习

一句话总结

领导者选举是这样一种约定:把“现在谁是领导者”写在一个小对象(Lease)上,并定期续期。是否过期并不写在对象上,而是由各个实例按自己的时钟计算,所以从原理上说,存在出现两个领导者的时间段。

为什么需要这个机制

如果只启动一个 Operator,那个 Pod 死掉期间就没有人做调谐。所以要启动两个以上。但调谐循环是修改集群状态的代码。两个同时运行,就会试图把同一个资源创建两次,互相删除对方的结果,交替把扩缩容的值改回去。

最简单的办法是“只让一个工作”。让多个实例都在运行,但只有其中一个做调谐,其余的待命。问题在于怎么确定这一个,如果另设一个共识系统,那个系统本身又会成为一个新的故障点。Kubernetes 用的是已有的东西——API 服务器的乐观并发控制。多个方同时更新同一个对象时,只有 resourceVersion 匹配的一方会成功。在这个性质之上叠加一个非常薄的约定,就成了领导者选举。

工作原理

约定的载体是 coordination.k8s.io/v1 的 Lease。只有五个字段。

字段 含义
holderIdentity 当前声称自己是领导者的实例的名称
leaseDurationSeconds 续期中断之后,经过这么长时间就视为过期
acquireTime 当前持有者获取租约的时间
renewTime 持有者最后一次续期的时间
leaseTransitions 持有者变更的次数

acquireTime 和 renewTime 不是普通的 Time,而是 MicroTime,所以需要小数点后六位。格式不同的话,不是在值的阶段,而是在解析阶段就会被拒绝。

领导者要做的只是定期改写 renewTime。持有者和切换次数都不碰。候选者要做的,是定期读取租约,看 renewTime + leaseDurationSeconds 是否早于当前时间。如果是,就视为过期,以自己的名字接管,同时重新写入 acquireTime,并把 leaseTransitions 加一。所以切换次数是运维指标。如果持有者不断变化,这个数字就会不断增长。

kube-controller-manager 的三个标志决定了这个节奏——--leader-elect-lease-duration(默认 15 秒)、--leader-elect-renew-deadline(默认 10 秒)、--leader-elect-retry-period(默认 2 秒)。它们之间的关系很重要。续期期限必须短于租约时长。领导者如果在续期期限内没能续期,会自己放弃领导者的位置,而如果这个期限比租约时长还长,就会出现“领导者仍然相信自己是领导者,而其他候选者已经视为过期并夺走了”的时间段。重试周期要设得短得多,以便在那个期限内可以尝试多次。

所需权限小得惊人。只要对 coordination.k8s.io 组的 leases 拥有 get、create、update 这三项就够了。不需要删除,也不需要监视(watch)——候选者不是通过监视,而是通过定期查询来确认过期的。

在现场相遇的样子

第一,出现两个领导者的时间段无法消除。网络短暂中断的领导者仍然相信自己是领导者,继续调谐,而在此期间,另一个实例接管了租约并开始调谐。要消除这个时间段,需要分布式系统中更强的保证,而 Kubernetes 的领导者选举并不承诺这一点。所以调谐循环本身必须是幂等的。领导者选举是降低概率的机制,而不是保证排他的机制。

第二,由时钟偏差引起的事故。如果各节点的时钟相差几秒,过期判定在各节点上就会得出不同结果。症状是“领导者不断更换”,界面上能看到的只有 leaseTransitions 以分钟为单位不断上升。调查时只看租约对象,是找不到原因的——必须同时查看节点时钟和 API 服务器的延迟。

第三,两个领导者留下的痕迹。如果两个调谐循环试图创建同一个资源,其中一个会因名称冲突而失败;如果它们试图用服务端应用来写同一个字段,就会发生字段管理器冲突。后者尤其有用——API 服务器会连名字一起告诉你“这个字段属于另一个管理器”,所以一条冲突消息就能暴露出谁在一起写入。如果用服务端应用来编写 Operator,这种事故就不会悄悄过去。

本实验环境的局限

在实验 Pod 中,无法启动两个真实的控制器来竞争。因为 kwok 的 Pod 是假的,容器不会运行。所以续期、过期、接管要由人用 patch 来模拟,时钟偏差也通过把 renewTime 写成过去的时间来重现。反过来,API 服务器是真的,所以 MicroTime 格式被拒绝、RBAC 判定、服务端应用的字段管理器冲突,都能按实际行为确认。

下一项实验要做什么

创建租约并尝试续期,编写判定已过期租约与仍然有效的租约的脚本。用 patch 模拟接管,确认切换次数上升,并看看违反 MicroTime 格式时续期会怎样被阻止。亲自配好领导者选举所需的最小权限,然后在两个实例试图写同一个资源时,接收 API 服务器留下的冲突,最后汇总成运维检查表。