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

CRD 与 Operator

调谐跑了两次 - 租约过期、接管与事故诊断

在 TT Lab 中继续学习

目标

亲手操作 Lease 的五个字段,制造续期、过期和接管;配好领导者选举所需的最小权限;然后通过字段管理器冲突证明出现两个领导者时实际会发生什么,并汇总成运维检查表。

为什么重要

启动两个以上 Operator 的原因是可用性。但调谐循环是修改集群状态的代码,不能同时运行两个。所以 Kubernetes 在一个非常小的对象上写下“现在谁是领导者”,由领导者定期续期,表明自己还活着。这里重要的是,这个对象上没有“已过期”之类的字段。是否存活,取决于把 renewTime + leaseDurationSeconds 与当前时间比较的计算,而这个计算是各个实例按自己的时钟做的。如果时钟出现偏差,或者 API 服务器响应变慢,就会出现两个实例同时相信自己是领导者的时间段。在那个时间段内,如果两个调谐循环试图创建同一个资源,会发生什么,以及到哪里去找它留下的痕迹,就是本实验的主题。

步骤

  1. 创建命名空间 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。
  2. 亲手做三次领导者所做的事——以 1 秒的间隔运行三次只把 renewTime 更新为当前时间的 patch,每次把更新后的 renewTime 各用一行记录到 /root/op-leader/renew-log.txt(共三行)。不要碰 holderIdentity 和 leaseTransitions。
  3. 再创建两张租约——/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。
  4. 假设 widget-operator 租约被另一个实例接管了,用一次 patch 修改四个值——把 holderIdentity 改为 widget-operator-1,把 acquireTime 和 renewTime 改为当前时间,把 leaseTransitions 改为 1。在 /root/op-leader/takeover.txt 中留下四行——before-holder=、after-holder=、before-transitions=、after-transitions=。
  5. 尝试用精确到秒的时间续期——用 2026-01-01T00:00:00Z 这样不带小数点的值给 renewTime 打补丁。把结果汇总到 /root/op-leader/microtime-error.txt——第一行是 patch-rc=<종료 코드>(占位符为退出码),下面原样贴上服务器给出的语句。
  6. 在 /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。
  7. 重现两个实例同时相信自己是领导者的情形——在 /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=<지금 값>(占位符为当前值)。
  8. 创建 /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 三个值的名称,并包含为什么续期期限必须短于租约时长,以及切换次数不断增加时应该怀疑什么。

参考

一张租约决定领导者

创建命名空间 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 秒)为依据来说明。