GitOps 多集群规模化与灾备:从注册扇出到控制面重建
八十多个目标集群同时恢复连接时,GitOps 控制面会把积压的 revision、健康检查和漂移修复一起送向 Kubernetes API,产生大量 429 Too Many Requests。控制器 Pod Ready 不能说明积压已经消化;注册身份、放置集合、并发预算与恢复顺序必须作为同一套系统设计。
中央 Argo CD 管理集群不可恢复时,Git 仓库和业务 Pod 仍在,也不代表 Application、Project、cluster credential、差异规则与插件可以从仓库完整重建。新控制面在旧 inventory、字段所有权和 prune 候选对账前打开自动同步,可能把恢复动作变成跨集群批量删除。
先选拓扑,再讨论能管理多少集群
多集群 GitOps 至少有三种常见控制路径,它们的网络方向、状态位置和故障半径不同。
| 拓扑 | 控制路径 | 优点 | 主要代价 |
|---|---|---|---|
| 中央直连目标 API | 中央控制器读取事实源并直连每个目标 API 写入 | 策略、审计和界面集中 | cluster credential 集中,中央网络和缓存成为共同故障域 |
| 每集群自治拉取 | 每个目标集群内的控制器自行拉取 Git/OCI 并写本地 API | 网络边界清楚,单集群故障较独立 | 控制器副本、升级、凭证和观测成本随集群数增长 |
| 管理面加代理 | 管理面生成发布单元,下游 agent 主动拉取并在本地安装 | 适合 NAT 后集群,中央不必主动拨入 | manager/agent 协议、注册状态和下游 release ownership 增加一层 |
Argo CD 的经典部署由中央 application-controller 持有目标集群凭证、维护 watch cache,并可按 cluster shard 分摊;它从事实源拉取期望状态,却从中央控制面主动连接目标 API 写入,不能只用一个“pull”标签概括网络方向。Flux 的常见做法是每个集群 bootstrap 一套控制器,也支持 remote kubeconfig 和按标签分片;Fleet 则采用 manager 拉取 Git 并生成 Bundle、下游 agent 从 manager 拉取 BundleDeployment、再通过 Helm 落地的 two-stage pull。Fleet 的 manager-initiated 注册会在初始阶段访问下游 API,稳态才由 agent 主动访问 manager;注册网络与调谐网络必须分别画清楚。三者都能管理多个集群,但“断开中央网络以后谁还能继续调谐”“凭证集中在哪里”“恢复时谁持有 inventory”的答案完全不同。Argo CD High Availability;Flux sharding;Fleet Architecture
选型时先画四条线:source 到控制器、控制器到目标 API、状态回传、凭证签发。跨隔离区不能由中央主动访问时,优先自治拉取或 agent;需要统一 Project、Application 和审计入口时,中央实例更直接;不同租户不能共享凭证和缓存故障域时,应拆实例或拆管理集群,而不是只加一个标签。
注册是授权过程,不是把 kubeconfig 塞进 Secret
目标集群进入交付体系前,应建立唯一 clusterId、环境、区域、故障域、租户、合规等级和生命周期状态。显示名可以变化,稳定 ID 不应复用。注册对象只能由资产系统或受控工作流创建,ApplicationSet generator、Fleet target 或其他放置器只消费已经批准的属性,不能把任意业务标签都当成发布开关。
中央推送模式通常需要目标侧 ServiceAccount、最小 Role/ClusterRole、短期 Token 或云工作负载身份,以及受信 CA。先做允许和拒绝矩阵,再交给控制器:
TARGET_NS=payments
SA=gitops-reconciler
kubectl auth can-i --as="system:serviceaccount:${TARGET_NS}:${SA}" \
create deployments.apps -n "${TARGET_NS}"
kubectl auth can-i --as="system:serviceaccount:${TARGET_NS}:${SA}" \
patch services -n "${TARGET_NS}"
# 这两项预期返回 no;任一返回 yes 都应停止注册。
kubectl auth can-i --as="system:serviceaccount:${TARGET_NS}:${SA}" \
create clusterrolebindings.rbac.authorization.k8s.io
kubectl auth can-i --as="system:serviceaccount:${TARGET_NS}:${SA}" \
get secrets -n kube-system正向证据是允许清单全部为 yes,拒绝清单全部为 no,并且审计日志中的主体与新 ServiceAccount 一致。停止条件包括:目标 context 无法唯一确认、CA 校验被关闭、令牌没有过期或撤销路径、控制器凭证仍是 cluster-admin、注册 Secret 会进入普通 Git 仓库。不要为了通过连接测试扩大权限;forbidden 应落到缺失的 GVR、verb 和 namespace 上逐项处理。
每集群自治模式则把 Git/OCI/SOPS/KMS 身份放在目标集群。它减少跨集群 kubeconfig,却没有消除高价值凭证;需要分别轮换 deploy key、registry pull、KMS decrypt 和 controller ServiceAccount。Fleet agent 注册还要追踪 agent heartbeat、ClusterRegistration 与 manager 端对象;断联后本地 workload 可能继续运行,而中央状态会陈旧,二者必须分开告警。
分组与放置需要稳定属性和候选集预览
集群分组不是导航分类,而是批量写入边界。建议把属性分成三类:资产属性由平台维护,如 region、provider、stage;调度属性由发布治理维护,如 wave、maintenance-window、canary;临时状态只用于阻断,如 quarantined、draining。成本中心、展示名称或排障临时标签不应参与自动放置。
放置规则至少经过“候选集合、增量集合、减量集合”三次计算。任何减项都可能触发 Application 删除、BundleDeployment 清理或资源 prune。合并前应导出旧新集合并设置变化阈值,例如一次新增不超过 5 个集群、减少必须为 0,超过阈值改为人工批准。ApplicationSet、Fleet ClusterGroup/target 和 RHACM Placement 的对象模型不同,不能只复制选择器语法。ApplicationSet generators;Fleet rollout
一个安全的放置清单应把决策和模板分开:
apiVersion: platform.example.io/v1alpha1
kind: ReleasePlacement
metadata:
name: payments-prod
spec:
selector:
matchLabels:
environment: production
gitops.example.com/managed: "true"
exclude:
matchLabels:
gitops.example.com/quarantined: "true"
rollout:
cohorts: [canary, region-a, region-b]
maxConcurrentClusters: 5
maxUnavailableClusters: 1
pauseAfterCohort: true这是平台决策模型示例,不是某个产品可直接提交的 CRD。落地时映射到目标产品的已安装 API,并在 CI 中打印集群稳定 ID,不打印 kubeconfig、token、内部 API 地址。候选为空、候选数突增、生产集群落入未批准 cohort、同一集群被两个互斥放置命中时,流水线必须拒绝继续。
扇出不是越快越好,而是受预算约束的队列
一次 Git commit 可能展开为 集群数 × 应用数 × 对象数 次比较与写入。真正的容量主轴还包括渲染耗时、对象字节数、CRD 比例、watch/relist、admission 延迟、健康等待、status 写放大和 drift 比例。以“控制器能连 500 个集群”作为容量结论没有意义;500 个小型 ConfigMap 与 500 组含 CRD、hook 和大型 Helm chart 的应用不是同一负载。
扇出要有四层预算:source 拉取并发、render 并发、集群并发、单集群 API QPS。批次之间既要看控制器队列,也要看目标 API 的 429、P99 延迟和 admission 拒绝。Argo CD 官方 HA 文档中的 processor 数值是调优示例,不是容量保证;Fleet 的 rollout 参数也必须显式设置,尤其不能让宽松的 unavailable 默认值替代生产门禁。Argo CD High Availability;Fleet Rollout Strategy
可观测的批次状态至少包含:候选总数、已开始、已成功、已失败、等待中、当前 revision、每集群对象数、队列等待、目标 API 请求率与 429。停止条件可以定义为“任一 cohort 失败集群超过 1、业务错误率超过阈值、API 429 连续三个窗口上升、队列年龄超过发布窗口的一半”。停止后冻结新 cohort,已经开始的集群继续还是回滚要由发布策略明确,不能靠 controller 重启碰运气。
把管理面、目标集群和外部依赖拆成故障域
中央控制器多副本只解决进程或节点故障,不等于跨站点灾备。Argo CD 的 Kubernetes 对象最终在管理集群 etcd,Redis 主要是可重建缓存;三个副本若位于同一集群、同一云区域或共享同一 Git 出口,仍属于共同故障域。repo-server 的临时磁盘、插件和远程依赖也能让“控制器健康”与“无法渲染”同时发生。
故障域图至少标出:
Git、OCI、Helm 仓库及其 DNS、CA、身份提供方。管理集群 etcd、CRD/CR、控制器、缓存、插件和通知组件。到各区域目标 API 的网络、代理、证书和速率限制。
目标集群 workload、Helm release Secret、PVC、外部负载均衡与数据库。独立保存的审计、指标、最后成功 revision 和恢复手册。
Flux 每集群自治能缩小中央故障半径,但共享 Git、KMS 或镜像仓库仍可能同时阻断全部集群。Fleet 在稳态调谐时由下游 agent 主动访问 manager,不代表 manager 丢失没有影响:新 Bundle、注册和中央状态都会受阻;manager-initiated 注册还存在一段 manager 到下游 API 的初始访问。架构结论必须来自分别切断注册链、agent 上行链和事实源链的实验,而不是从“pull”这个词推断永远可用。
用 RTO 与 RPO 定义能恢复到什么状态
RPO 不是只有 Git 提交丢失量。至少分别定义事实源 RPO、控制面对象 RPO、凭证状态 RPO、业务数据 RPO。Git 可以是零丢失,但最近一次 Project 权限、cluster Secret 轮换、custom health、plugin 配置或下游 Helm release 状态若未备份,控制面仍不可重建。
RTO 也要分段:取得事实源、重建控制器、恢复身份、完成只读对账、恢复首个 cohort、恢复全部集群、确认业务请求。团队常说的“两小时恢复”如果只指 Pod Ready,而没有包含凭证签发、inventory 接管和真实入口检查,就不是可执行目标。
Argo CD 提供 argocd admin export/import;官方提醒,在错误 namespace 执行 export 不一定失败,所以备份任务必须检查对象数量、关键 Application/AppProject/ConfigMap/Secret 摘要,并在隔离集群做 import。该导出是 all Argo CD data,不能因示例只检查元数据就把文件当作无密资产;它要在生成时即加密、限权和设置保留期。Argo CD Disaster Recovery Flux 可从 Git 中的 bootstrap 清单重建控制器,但 Git/OCI 凭证、SOPS/KMS 身份、目标 RBAC 和 inventory 接管仍需恢复。Flux installation Fleet 官方没有等价的单命令完整灾备承诺;按官方对象模型推导,至少要备份和重建 manager 中的注册对象、GitRepo、Bundle、BundleDeployment、策略与 Secret,并用隔离恢复实验验证 agent 与下游 Helm release 的接管,不能把这一治理推论写成产品内置保证。
正向演练:从空管理面恢复到受控首批
演练应使用隔离管理集群和可销毁目标 namespace,不能把生产作为第一次恢复环境。下面的骨架以 Argo CD 为例,Flux 或 Fleet 也要保留相同的“备份检查、只读对账、分批接管”状态机:
# 旧控制面:固定 CLI/服务端版本,限制备份文件权限并完成全量导出。
umask 077
argocd version
argocd admin export -n argocd > argocd-export.yaml
kubectl -n argocd get applications,applicationsets,appprojects --no-headers | wc -l
grep -c '^kind: Application$' argocd-export.yaml
age -r "${BACKUP_RECIPIENT}" -o argocd-export.yaml.age argocd-export.yaml
rm argocd-export.yaml
sha256sum argocd-export.yaml.age > argocd-export.sha256
# 隔离控制面:reconciler 保持停止且目标 API 出口被阻断,再导入全量数据。
sha256sum -c argocd-export.sha256
age -d -i "${BACKUP_IDENTITY}" argocd-export.yaml.age |
argocd admin import - -n argocd
kubectl -n argocd get applications,applicationsets,appprojects
argocd app list导出文件可能包含 repository 和 cluster credential 等敏感数据,演练副本应使用专用密钥加密;BACKUP_IDENTITY 只能在隔离恢复作业中短时可用。导入会恢复备份中的凭证对象,所以不能把“尚未手工配置凭证”当成隔离;必须先停 reconciler、阻断到生产目标 API 的出口,并确认旧凭证已撤销,导入后再按测试环境身份替换。演练结束时销毁备份副本、解密身份与临时存储快照。接下来先恢复 repo 只读身份和一个只读目标身份,固定到备份记录中的 revision,只对一个 canary 集群执行 refresh/diff;候选删除归零或逐项批准后,才进入 fencing 与最小写入。预期证据是:对象数与摘要符合备份、source revision 可解析、live UID/managedFields 与旧清单可关联、首批真实请求成功。只有这些证据成立,才按 cohort 授予写权限并逐批恢复自动同步。
停止条件包括:导出对象数为零或异常下降、导入后 Application 指向错误集群、首次 diff 出现未批准的大量删除、同一对象已有另一 field manager/Helm owner、旧 revision 无法取得、业务依赖的数据 RPO 尚未满足。遇到任一条件应保持控制器只读或 suspend,不得通过 force、删除 finalizer、全局忽略差异来“推进恢复”。
反向演练:断联恢复与调谐风暴
选择三个可销毁目标集群,分别标为 canary、batch-a、batch-b。先让它们消费 revision A,再阻断目标 API 或暂停 agent;在断联期间连续提交 B、C,确保 C 是最终期望状态。恢复网络时只开放 canary,并观察它是否直接收敛到 C,而不是依次执行所有中间副作用。
# 采集控制器和目标 API 证据;具体指标名按安装版本确认。
kubectl -n argocd get applications -o custom-columns='NAME:.metadata.name,SYNC:.status.sync.status,HEALTH:.status.health.status,REV:.status.sync.revision'
kubectl get --raw '/metrics' > apiserver-metrics.txt
kubectl get events -A --sort-by=.lastTimestamp | tail -n 100
# 恢复后只触发 canary,其他批次保持暂停。
# 不传 --prune;显式确认该 Application 的自动 prune 也处于关闭状态。
argocd app sync payments-canary --revision <revision-c>
argocd app wait payments-canary --sync --health --timeout 300预期看到 canary 的 source、sync revision 和运行对象都指向 C,API 延迟和 429 在预算内,幂等 Job 不重复产生外部副作用。反例证据包括:队列无限增长、恢复即全量扇出、旧 revision B 覆盖 C、hook 重复扣费或迁移、控制器因重启丢失批次状态。任一业务错误率越线、API 429 持续上升、失败集群超过批次预算或 Job 幂等性不明时,应停止开放后续批次。
实验结束要解除网络故障、恢复正常放置策略、删除测试 Application/Bundle/Kustomization,并确认测试凭证、临时 namespace、Job、告警静默和外部资源均已清理。只删除 Git 目录不等于清理完成,尤其要复查 finalizer 和 orphan workload。
凭证撤销是灾备切换的最后一道门
恢复新控制面时,复制旧长期 token 最快,也最危险:失效管理集群若重新上线,两套控制器可能同时写目标集群。正确顺序是创建只有读权限的新身份,让新控制面完成 source 读取、refresh 与 diff;随后冻结旧控制器并撤销旧 RoleBinding、token、client certificate 或云角色信任,确认旧主体写入已被拒绝;最后才给新身份授予最小写权限并执行 canary apply。Git deploy key、registry 与 KMS 身份可以按各自读写能力设置短暂轮换窗口,但目标 API 的写身份不能并行有效。
撤销证据必须包含负面请求。旧主体访问 Git 应被拒绝,旧 ServiceAccount 对目标 namespace 的写入应返回 no 或 403,旧 agent/principal 不再心跳,审计中不再出现旧 fingerprint。仅删除管理集群里的 Secret 不能证明外部 token 已撤销;仅等待 token 过期也不适用于可长期续签的云角色信任。
如果 RTO 要求快速恢复,可以预先准备禁用状态的灾备身份和最小 RBAC 模板,但不要长期启用第二写控制面。切换流程应包含 fencing:先停止旧控制器出队并吊销它到目标 API 的写权限,用旧主体发起负向请求证明隔离,再为新控制面授予写权限。无法确认旧控制面已隔离、旧云角色仍可换取新令牌或审计仍出现旧主体写请求时,停止自动接管。
用容量账单和责任矩阵维持长期可恢复性
多集群成本不仅是控制器 CPU。还包括每集群 agent、管理集群 etcd、repo 缓存和临时盘、跨区 API 与制品流量、日志指标基数、凭证系统调用、备份存储、隔离演练环境和工程值守。按 cluster、Application/Bundle/Kustomization、render 秒数、API 请求和日志字节建立成本标签,才能看出自治模式的副本成本与中央模式的共享容量成本。
职责要落实到可核对对象:平台团队维护控制器、注册、身份、放置策略和容量;应用团队维护声明、健康判据和幂等 hook;安全团队维护信任、轮换、撤销和审计;SRE 维护 RTO/RPO、故障注入和业务恢复;资产负责人批准集群加入、隔离和退役。没有 owner 的集群不得自动进入生产放置。
季度演练至少回答这些问题:备份能否在隔离环境读取;对象数和摘要能否发现空备份;控制器能否只读启动;首批接管是否零意外 prune;旧凭证能否被明确拒绝;断联集群是否消费最新 revision;API 限流是否阻断后续批次;业务数据恢复是否满足独立 RPO;所有临时资源和费用是否核销。只有这些答案持续有证据,多集群 GitOps 才从“批量部署工具”变成可恢复的交付系统。
