调谐跑了两次 - 租约过期、接管与事故诊断
目标
亲手操作 Lease 的五个字段,制造续期、过期和接管;配好领导者选举所需的最小权限;然后通过字段管理器冲突证明出现两个领导者时实际会发生什么,并汇总成运维检查表。
为什么重要
启动两个以上 Operator 的原因是可用性。但调谐循环是修改集群状态的代码,不能同时运行两个。所以 Kubernetes 在一个非常小的对象上写下“现在谁是领导者”,由领导者定期续期,表明自己还活着。这里重要的是,这个对象上没有“已过期”之类的字段。是否存活,取决于把 renewTime + leaseDurationSeconds 与当前时间比较的计算,而这个计算是各个实例按自己的时钟做的。如果时钟出现偏差,或者 API 服务器响应变慢,就会出现两个实例同时相信自己是领导者的时间段。在那个时间段内,如果两个调谐循环试图创建同一个资源,会发生什么,以及到哪里去找它留下的痕迹,就是本实验的主题。
步骤
- 创建命名空间
op-leader,并在/root/op-leader/lease-widget.yaml中编写coordination.k8s.io/v1Leasewidget-operator——holderIdentity为widget-operator-0,leaseDurationSeconds为 15,acquireTime和renewTime为当前时间,leaseTransitions为 0。应用之后,把kubectl -n op-leader get lease widget-operator -o yaml的输出保存到/root/op-leader/lease-initial.yaml。 - 亲手做三次领导者所做的事——以 1 秒的间隔运行三次只把
renewTime更新为当前时间的 patch,每次把更新后的renewTime各用一行记录到/root/op-leader/renew-log.txt(共三行)。不要碰holderIdentity和leaseTransitions。 - 再创建两张租约——
/root/op-leader/lease-stale.yaml是stale-operator(holder 为stale-operator-0,leaseDurationSeconds为 15,renewTime设为 10 分钟之前),/root/op-leader/lease-fresh.yaml是fresh-operator(holder 为fresh-operator-0,leaseDurationSeconds为 86400,renewTime为当前时间)。在/root/op-leader/leases.tsv中写入两行<임차이름><탭><기대: EXPIRED 또는 LIVE>(占位符依次为租约名称、制表符、预期结果 EXPIRED 或 LIVE),让/root/op-leader/expiry.sh把每张租约的renewTime + leaseDurationSeconds与当前时间比较并分类,符合预期时只向标准输出打印OK …,不符合时只向标准输出打印MISMATCH …,只要有一行不符就以非 0 的退出码结束。把输出保存到/root/op-leader/expiry.txt。 - 假设
widget-operator租约被另一个实例接管了,用一次 patch 修改四个值——把holderIdentity改为widget-operator-1,把acquireTime和renewTime改为当前时间,把leaseTransitions改为 1。在/root/op-leader/takeover.txt中留下四行——before-holder=、after-holder=、before-transitions=、after-transitions=。 - 尝试用精确到秒的时间续期——用
2026-01-01T00:00:00Z这样不带小数点的值给renewTime打补丁。把结果汇总到/root/op-leader/microtime-error.txt——第一行是patch-rc=<종료 코드>(占位符为退出码),下面原样贴上服务器给出的语句。 - 在
/root/op-leader/rbac.yaml中放入三个对象并应用——ServiceAccountwidget-operator、Roleleader-election(仅对coordination.k8s.io组的leases授予get、create、update),以及把两者连接起来的 RoleBindingleader-election。然后对get、create、update、delete四个动词各运行一次kubectl auth can-i <동사> leases.coordination.k8s.io --as=system:serviceaccount:op-leader:widget-operator -n op-leader(占位符为动词),把结果以<동사>=<yes 또는 no>四行(占位符依次为动词、yes 或 no)保存到/root/op-leader/rbac-check.txt。 - 重现两个实例同时相信自己是领导者的情形——在
/root/op-leader/child-a.yaml中编写 ConfigMaporder-1(命名空间op-leader,data.phase为A),并用kubectl apply --server-side --field-manager=widget-operator-0应用。然后在/root/op-leader/child-b.yaml中编写同名的 ConfigMap,把data.phase写成B,用--field-manager=widget-operator-1试着应用。把结果汇总到/root/op-leader/split-brain.txt——apply-b-rc=<종료 코드>(占位符为退出码)、服务器给出的语句,以及最后一行final-phase=<지금 값>(占位符为当前值)。 - 创建
/root/op-leader/lease-audit.sh——把op-leader中的所有租约,以<이름> holder=<홀더> transitions=<전환 횟수> duration=<임차 길이>一行的形式(占位符依次为名称、持有者、切换次数、租约时长)排序后只输出到标准输出(不要放入存在时长或时刻)。把输出保存到/root/op-leader/lease-audit.txt,并在/root/op-leader/runbook.txt中写下运维规则——必须出现--leader-elect-lease-duration、--leader-elect-renew-deadline、--leader-elect-retry-period三个值的名称,并包含为什么续期期限必须短于租约时长,以及切换次数不断增加时应该怀疑什么。
参考
acquireTime和renewTime是 MicroTime,所以需要小数点后六位。- 续期只修改
renewTime,接管则同时修改持有者、acquireTime和leaseTransitions。 - 领导者选举所需的动词只有
get、create、update三个。 - 可以用
kubectl auth can-i --as=代替其他主体询问权限。 - 常见错误:续期时同时增加
leaseTransitions,使切换次数失去意义。 - 常见错误:想从对象的某个字段中找出租约是否过期——必须自己计算。
- 参考:https://kubernetes.io/docs/concepts/architecture/leases/
- 参考:https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/lease-v1/
- 参考:https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/
一张租约决定领导者
创建命名空间 op-leader,并在 /root/op-leader/lease-widget.yaml 中编写 coordination.k8s.io/v1 Lease widget-operator——holderIdentity 为 widget-operator-0,leaseDurationSeconds 为 15,acquireTime 和 renewTime 为当前时间,leaseTransitions 为 0。应用之后,把 kubectl -n op-leader get lease widget-operator -o yaml 的输出保存到 /root/op-leader/lease-initial.yaml。
两个时间字段不是普通的 Time,而是 MicroTime,所以需要小数点后六位(例如 2026-09-17T13:37:24.807518Z)。如果是 GNU date,可以直接用 date -u +%Y-%m-%dT%H:%M:%S.%6NZ 生成。leaseTransitions 是“领导者更换的次数”,所以一开始是 0。
领导者不停地续期
亲手做三次领导者所做的事——以 1 秒的间隔运行三次只把 renewTime 更新为当前时间的 patch,每次把更新后的 renewTime 各用一行记录到 /root/op-leader/renew-log.txt(共三行)。不要碰 holderIdentity 和 leaseTransitions。
续期只是“我还活着”的信号,所以持有者和切换次数都不会改变。如果切换次数每次续期都增加,意味着领导者每次都在更换,在生产环境中这是值得设置告警的状态。请确认这三行的时间是否依次增大。
是否存活由计算决定
再创建两张租约——/root/op-leader/lease-stale.yaml 是 stale-operator(holder 为 stale-operator-0,leaseDurationSeconds 为 15,renewTime 设为 10 分钟之前),/root/op-leader/lease-fresh.yaml 是 fresh-operator(holder 为 fresh-operator-0,leaseDurationSeconds 为 86400,renewTime 为当前时间)。在 /root/op-leader/leases.tsv 中写入两行 <임차이름><탭><기대: EXPIRED 또는 LIVE>(占位符依次为租约名称、制表符、预期结果 EXPIRED 或 LIVE),让 /root/op-leader/expiry.sh 把每张租约的 renewTime + leaseDurationSeconds 与当前时间比较并分类,符合预期时只向标准输出打印 OK …,不符合时只向标准输出打印 MISMATCH …,只要有一行不符就以非 0 的退出码结束。把输出保存到 /root/op-leader/expiry.txt。
租约对象上没有“已过期”之类的字段。只是各个候选者按自己的时钟去计算——所以如果时钟出现偏差,即使看的是同一个对象,判定也会不同。计算时,用 date -u -d "<시각>" +%s(占位符为时刻)转换成秒再相加即可。
接管通过切换次数体现出来
假设 widget-operator 租约被另一个实例接管了,用一次 patch 修改四个值——把 holderIdentity 改为 widget-operator-1,把 acquireTime 和 renewTime 改为当前时间,把 leaseTransitions 改为 1。在 /root/op-leader/takeover.txt 中留下四行——before-holder=、after-holder=、before-transitions=、after-transitions=。
区分接管和续期的是 acquireTime 和 leaseTransitions。续期只改变 renewTime,而接管时持有者变了,所以要重新写入获取的时间,并把切换次数加一。关注这个数字,就能看出领导者更换得有多频繁。
一个时间格式就能阻止续期
尝试用精确到秒的时间续期——用 2026-01-01T00:00:00Z 这样不带小数点的值给 renewTime 打补丁。把结果汇总到 /root/op-leader/microtime-error.txt——第一行是 patch-rc=<종료 코드>(占位符为退出码),下面原样贴上服务器给出的语句。
这个字段是 MicroTime,不是 Time。格式不同,不是在值的阶段,而是在解析时就被拦下,错误语句中会原样写出期望的格式。自己编写控制器时,如果直接使用标准库的默认格式,就会在这里被拦下,导致续期整个失败。
领导者选举所需的最小权限
在 /root/op-leader/rbac.yaml 中放入三个对象并应用——ServiceAccount widget-operator、Role leader-election(仅对 coordination.k8s.io 组的 leases 授予 get、create、update),以及把两者连接起来的 RoleBinding leader-election。然后对 get、create、update、delete 四个动词各运行一次 kubectl auth can-i <동사> leases.coordination.k8s.io --as=system:serviceaccount:op-leader:widget-operator -n op-leader(占位符为动词),把结果以 <동사>=<yes 또는 no> 四行(占位符依次为动词、yes 或 no)保存到 /root/op-leader/rbac-check.txt。
领导者选举所需的动词只有三个——读取、不存在则创建、定期续期。不需要删除。也不需要 watch,因为候选者不是通过监视,而是通过定期查询来确认过期的。权限给得宽,就会打开不小心删除别人租约的途径。
出现两个领导者,就会争抢同一个字段
重现两个实例同时相信自己是领导者的情形——在 /root/op-leader/child-a.yaml 中编写 ConfigMap order-1(命名空间 op-leader,data.phase 为 A),并用 kubectl apply --server-side --field-manager=widget-operator-0 应用。然后在 /root/op-leader/child-b.yaml 中编写同名的 ConfigMap,把 data.phase 写成 B,用 --field-manager=widget-operator-1 试着应用。把结果汇总到 /root/op-leader/split-brain.txt——apply-b-rc=<종료 코드>(占位符为退出码)、服务器给出的语句,以及最后一行 final-phase=<지금 값>(占位符为当前值)。
服务端应用会为每个字段记录“谁在管理这个值”。如果另一个管理器试图把同一个字段写成不同的值,API 服务器不会悄悄覆盖,而是会通知冲突。当领导者选举失效时,两个调谐循环互相抹掉对方的结果,这个机制会让这件事变得肉眼可见。
汇总成检查表和运维规则
创建 /root/op-leader/lease-audit.sh——把 op-leader 中的所有租约,以 <이름> holder=<홀더> transitions=<전환 횟수> duration=<임차 길이> 一行的形式(占位符依次为名称、持有者、切换次数、租约时长)排序后只输出到标准输出(不要放入存在时长或时刻)。把输出保存到 /root/op-leader/lease-audit.txt,并在 /root/op-leader/runbook.txt 中写下运维规则——必须出现 --leader-elect-lease-duration、--leader-elect-renew-deadline、--leader-elect-retry-period 三个值的名称,并包含为什么续期期限必须短于租约时长,以及切换次数不断增加时应该怀疑什么。
检查表是把“看到什么就说明不正常”浓缩出来的东西。如果持有者始终相同,只有切换次数在增加,就说明接管在反复发生,原因通常是时钟偏差或 API 服务器延迟。三个值之间的关系,请以官方文档的默认值(15 秒、10 秒、2 秒)为依据来说明。