Controller 集成手册
主备协同接入步骤与 drsyncgate 全源码
Controller 集成手册
面向需要在 Global 集群上跑 Controller / Operator 并接入 DR 体系的开发者。
背景
ACP 的 Global + 业务集群(行业里常说的 Hub-Spoke)形态下,Global 集群同时承担两个角色:
- 当 Global 是 Active 时,正常 reconcile,写入它自己的 etcd
- 当 Global 是 Standby 时,etcd-sync 把 Active 的状态同步过来,覆盖本地 etcd 中相应 key 段
如果 Standby Global 上的 Controller 也照常 reconcile,可能出现两类问题:
| 问题 | 原因 |
|---|---|
| 状态抖动 | Controller 写入的字段被 etcd-sync 立即盖掉,下一轮观测又触发新一轮写入 |
| 决策不可靠 | Controller 依赖的状态正在被覆盖,决策基于"瞬时拍下"的旧值,结论可能不一致 |
下面这张时序图对比了有 / 没有 drsyncgate 时 Standby 上发生的事:
正确做法:
- Standby 期间短路 reconcile,让出对象给 etcd-sync
- Active 期间正常工作
- 切换期间留 grace period,避免单次 informer 抖动触发 false positive
角色信号
判定信号是 kube-public/etcd-sync ConfigMap:
| ConfigMap | 当前角色 |
|---|---|
| 存在 | Standby(etcd-sync 正在写入本集群) |
| 不存在 | Active |
ConfigMap 的内容不重要;只看存在性。它由 etcd-sync 自身在它生效的一侧维护。
drsyncgate 库
astrolabe/pkg/drsyncgate 把判定细节封装成一个函数:
| 行为 | 说明 |
|---|---|
| ConfigMap 存在 | 返回 true |
| ConfigMap 不存在但 60 秒 grace 内曾经存在 | 仍返回 true |
| 进程从未观测到该 ConfigMap | 返回 false |
| 读取超时(30 秒) | 返回错误,建议调用方按正常错误重试 |
| 读取错误(非 NotFound) | 返回 (true, err)——保守地按 syncing 处理 |
完整源码
drsyncgate 是 astrolabe 仓内的私有包,外部仓不会拉到这份代码。完整列出便于 review、移植、或在没接入 astrolabe 的项目里直接照抄一份。
内部 grace period 状态机:
调用方只需要决定:当 syncing 为 true 时,是否要给本对象短路 reconcile。
标准接入姿势
入口在 Reconcile() 顶部。astrolabe 仓的两个示例:
要点:
etcdSyncRequeueAfter = 30 * time.Second,与 grace period 量级匹配isGlobalCluster(name)是仓内的小帮手name == globalClusterName——参见下文豁免说明- 读取使用
DirectClient(绕过 cache),避免 informer 还没观测到 marker 变化时漏判
把上面的代码翻译成判断流程:
"Global 豁免"为什么需要
仓内对 cluster name 等于 global 的对象不短路。原因:
| 对象类别 | Standby 期间是否短路 | 理由 |
|---|---|---|
| 业务集群的 ClusterVersionShadow | 短路 | 这些字段本身在被 etcd-sync 同步覆盖 |
global 自身的 ClusterVersionShadow | 不短路 | Global 集群自身管理的对象,etcd-sync 不会盖掉它的 spec/status,必须持续 reconcile 才能让 Global 自己保持就位 |
如果你写的 Controller 不区分 Global 集群自身 vs 它管理的业务集群,那就直接 if syncing { requeue } 不带豁免;如果区分,请套用同样的 carve-out。
日志与可观测
判定结果本身没有 metric / event。建议 Controller 在短路分支记一行 Info 日志,至少包含:
- 对象名
- marker 的 namespace/name(
kube-public/etcd-sync,可用drsyncgate.ConfigMapName拼出) - 选择的 requeue 间隔
避免只打"skipping reconcile"——排障时无法区分是 gate 触发还是别的逻辑。
测试姿势
astrolabe/pkg/drsyncgate/gate_test.go 用 fake client + 注入 ConfigMap 的方式覆盖了三类场景:
| 场景 | 构造方式 |
|---|---|
| 当前正在同步 | fake client 中放入 kube-public/etcd-sync ConfigMap |
| 同步刚停止(grace 内) | 先标记 running=true,再让 fake client 返回 NotFound,注入时间在 60 秒以内 |
| 长期未同步 | fake client 中没有 marker,进程内状态从未 markRunning |
测试用的内部钩子:
resetStateForTest(t, clockFn):重置库内状态并替换时钟resetClockForTest(t):单独替换时钟
钩子是非导出的,只能在同包测试或本仓内的 wrapper test 中使用;从外部包测试自己 Controller 时,直接用 fake client 注入 / 删除 ConfigMap 就够了。
反模式
| 反模式 | 为什么不要 |
|---|---|
| 通过自定义 Lease / leader election 判断角色 | 当前 DR 角色由 etcd-sync 控制;任何额外信号源会与 marker 漂移,造成"双 leader" |
| 在 Active 上直接禁止写入特定对象 | Active 上 Controller 必须正常写入;禁止后 Active 自己也会停摆 |
用 cluster name 是不是 global 来判断角色 | name 只决定"我是不是 Global 集群",不决定"我是 Active 还是 Standby" |
| 在 reconcile 主循环中只判一次 syncing 后无 requeue 直接返回 | 角色翻转后没人触发再次 reconcile;务必带 RequeueAfter |
| 把判定结果缓存到内存里跨 reconcile 复用 | drsyncgate 内部已经有 grace period;调用方再加一层会延长收敛时间 |