werwerwer
DR 容灾Active / Standby Controller 协同

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 把判定细节封装成一个函数:

import "github.com/alauda/astrolabe/pkg/drsyncgate"
 
syncing, err := drsyncgate.IsRunning(ctx, reader)
行为说明
ConfigMap 存在返回 true
ConfigMap 不存在但 60 秒 grace 内曾经存在仍返回 true
进程从未观测到该 ConfigMap返回 false
读取超时(30 秒)返回错误,建议调用方按正常错误重试
读取错误(非 NotFound)返回 (true, err)——保守地按 syncing 处理

完整源码

drsyncgate 是 astrolabe 仓内的私有包,外部仓不会拉到这份代码。完整列出便于 review、移植、或在没接入 astrolabe 的项目里直接照抄一份。

内部 grace period 状态机:

调用方只需要决定:当 syncing 为 true 时,是否要给本对象短路 reconcile。

标准接入姿势

入口在 Reconcile() 顶部。astrolabe 仓的两个示例:

// pkg/cvo/controllers/controller.go:160
syncing, err := drsyncgate.IsRunning(ctx, r.DirectClient)
if err != nil {
    return ctrl.Result{}, err
}
if syncing && !isGlobalCluster(obj.Name) {
    logger.Info(
        "Skipping reconcile while etcd-sync is running",
        "cluster", obj.Name,
        "configMap", metav1.NamespacePublic+"/"+drsyncgate.ConfigMapName,
        "requeueAfter", etcdSyncRequeueAfter,
    )
    return ctrl.Result{RequeueAfter: etcdSyncRequeueAfter}, nil
}
// pkg/cvo/controllers/syncer.go:69
syncing, err := drsyncgate.IsRunning(ctx, c.DirectClient)
if err != nil {
    return ctrl.Result{}, err
}
if syncing && !isGlobalCluster(cluster.Name) {
    return ctrl.Result{RequeueAfter: etcdSyncRequeueAfter}, nil
}

要点:

  • 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;调用方再加一层会延长收敛时间

On this page