# 多区域 GitOps 架构:从 2 到 25 个区域的扩展
## 引言:规模爆炸的问题
如果产品需要覆盖 25 个区域怎么办?突然间,原本只有两个目标的流水线就变成了 25 个目标的爆炸半径:
- 一个错误的图表版本不会只影响一个区域,而是会影响整个大陆
- 回滚 `ap-south-1` 不应该因为 `eu-west-1` 部署卡住而停滞不前
- 但在串行流水线模型中,这种情况却不得不发生
- 运行手册已经无法完整显示在屏幕上
- "部署到所有区域" 按钮悄然成为了公司里最危险的按钮
本文介绍的架构是为了在不担心区域数量的情况下,将区域数量从 2 个增加到 25 个而构建的:**一个 GitOps 优先的模型**,其中每个区域都是一个独立的、自我修复的控制循环,部署是声明式提交,回滚是作用于一个区域的 `git revert` ——与其他所有区域完全隔离。
---
## 一、核心问题:耦合
多区域规模带来的所有问题都源于一个根本原因 —— **耦合**。以下三种失效模式至关重要:
### 1.1 编排耦合
如果使用单一流水线按顺序部署到所有区域,则意味着区域 25 会等待区域 1 的部署,任何区域的挂起都会导致所有区域停滞。**故障无法隔离**。
### 1.2 配置耦合
复制粘贴 25 个几乎相同的 `values.yaml` 文件意味着全局更改需要提交一个包含 25 个文件的拉取请求,并且很容易出现配置漂移错误。
### 1.3 状态耦合
如果"部署位置"的信息仅存在于持续集成日志中,那么就没有真实数据源。区域会发生变化,而协调工作就像是手动考古一样。
### 1.4 目标架构的策略
目标架构攻击了所有这两个方面:
- **GitOps 解耦了编排** —— 每个区域自行协调
- **分层的 Helm 值模型解耦了配置** —— 一个基础,轻量级覆盖层
- **Git 成为状态的唯一真理来源**
---
## 二、目标架构概述
该设计采用 **中心辐射式 GitOps 拓扑结构**。中央 Git 仓库是数据源。25 个区域各自运行自己的 GitOps 协调器(Argo CD),每个协调器只监控其负责的仓库切片,并将本地集群的状态与声明的状态进行协调。**部署的关键路径中没有中心组件** —— 中心发布部署意图,辐射区域拉取并应用。
附件:架构图 见底部
### 2.1 三个核心属性
从这种拓扑结构中可以得出三个属性,它们直接对应于约束条件:
| 属性 | 实现方式 | 说明 |
|------|---------|------|
| **单区域部署** | 仅支持一个 `regions/<region>/` 覆盖网络 | 只有该区域的协调器才能看到更改 |
| **全区域部署** | 版本号升级 `global/` | 所有 25 个协调器都会独立地应用该版本 |
| **故障隔离** | 每个协调器都是独立集群上的独立进程 | `eu-west-1` 卡住的情况对外部不可见 `ap-south-1`,没有共享队列,没有串行管道,没有中央锁 |
---
## 三、GitOps 与集中式 CI/CD 编排
在确定拓扑结构之前,需要做出一个核心架构决策:**谁实际将更改应用到集群** —— 基于推送的 CI 流水线,还是基于拉取的 GitOps 代理?
### 3.1 集中式 CI/CD(推送模式)
一条流水线(Jenkins、GitHub Actions、Spinnaker)持有所有 25 个集群的凭据,并 `helm upgrade` 针对每个集群运行。
**优点:**
- 易于理解和排序
**缺点:**
- 流水线是单点故障
- 必须持有 25 套集群管理员凭据(影响范围很大)
- 串行扇出会加剧区域延迟
- 挂起的区域会阻塞队列
- 没有持续的漂移纠正机制 —— 如果有人 `kubectl edit` 部署失败,直到下一个版本发布才会发现问题
### 3.2 GitOps(拉取模型)
每个集群运行一个代理,从 Git 拉取所需状态并持续进行同步。
**优点:**
- 凭证永远不会离开集群(CI 系统对集群没有访问权限)
- 状态偏差会自动纠正
- 各个区域完全独立
**缺点:**
- 学习曲线更陡峭
- 需要管理 25 个代理的安装(不过这本身也是自动化的)
### 3.3 决策
对于 25 个具有严格隔离要求的区域,**GitOps 优势明显**。推送模型的核心流水线恰恰是我们试图消除的耦合。
---
## 四、GitOps 代码库结构
配置耦合问题通过 **分层值模型** 解决:一个基础配置表、共享的全局值以及轻量级的区域覆盖层。区域覆盖层应仅包含真正特定于区域的信息(副本数、区域名称、端点、ACM 证书 ARN)—— 通常只有几十行,而不是完整复制。
### 4.1 代码库布局
```
fleet-config/
├── base/
│ └── charts/
│ └── payments-service/ # Helm chart(版本化,不可变)
│ ├── Chart.yaml
│ ├── values.yaml # 合理的默认值
│ └── templates/
├── global/
│ └── payments-service/
│ └── values.yaml # 镜像标签,共享配置(适用于所有)
├── regions/
│ ├── us-east-1/
│ │ └── payments-service/
│ │ └── values.yaml # 副本数、区域、证书 ARN、端点
│ ├── eu-west-1/
│ │ └── payments-service/
│ │ └── values.yaml
│ └── ... (还有 23 个)
├── waves/
│ └── payments-service.yaml # wave 成员资格:canary → wave1 → ... → waveN
└── applicationsets/
└── payments-service.yaml # Argo 应用程序集(生成 25 个应用程序)
```
> **参考:** [github.com/tulasinadhvaka/fleet-config-lab](https://github.com/tulasinadhvaka/fleet-config-lab)
### 4.2 值分层机制
应用程序集生成器会遍历区域列表,并按顺序为每个区域分层设置值:
```
base(默认值) < global < regions/<region>
```
- 区域覆盖层覆盖全局覆盖层
- 全局覆盖层又覆盖基础覆盖层
**镜像标签的位置决定部署范围:**
| 位置 | 部署范围 |
|------|---------|
| `global/` | 更改一次,所有区域都会更新 |
| `regions/<region>/` | 仅更改一个区域 |
正是这一个选择 —— 版本信息放置的位置 —— 使得同一个代码库能够同时支持单区域部署和全区域部署。
### 4.3 ApplicationSet 示例
```yaml
# applicationsets/payments-service.yaml(精简版)
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: payments-service
spec:
generators:
- list:
elements: # 区域列表 + 每个区域的集群 + wave
- { region: us-east-1, cluster: https://eks-use1..., wave: "0" }
- { region: eu-west-1, cluster: https://eks-euw1..., wave: "1" }
# ... 25 个条目
template:
metadata:
name: 'payments-{{region}}'
spec:
sources:
- repoURL: github.com/org/fleet-config
path: base/charts/payments-service
helm:
valueFiles:
- $values/global/payments-service/values.yaml
- $values/regions/{{region}}/payments-service/values.yaml
destination:
server: '{{cluster}}'
namespace: payments
```
---
## 五、Helm 管理策略
三条规则让 Helm 在 25 倍放大倍率下保持理智:
### 5.1 Chart 是不可变的,并且有版本控制
打包 Chart 后,将其推送到 OCI 注册表(Amazon ECR 原生支持 OCI Helm 工件)。`Chart.yaml` version 每次发布都会更新版本号;已部署的 Chart 版本不会被直接修改。回滚操作会指向注册表中仍然存在的先前 Chart 版本。
### 5.2 应用版本是数据,而非图表结构
容器镜像标签和发布配置位于 `global/` 或区域覆盖层中 —— 它们是值,而非图表模板。这意味着发布新版本只需更改一行值,而无需编辑图表。
### 5.3 绝对不要用 `latest`
每个镜像都绑定到一个不可更改的摘要或语义化版本标签上。`latest` 使得 "实际运行的是什么" 变得无法得知,也使得回滚变得毫无意义。
### 5.4 分离原则
> ECR 中的不可变图表,Git 值中的可变意图 —— 使得单个图表可以服务于 25 个区域,而每个区域都可以精确地锁定其想要的版本。
---
## 六、部署工作流程
### 6.1 单区域部署
**场景:** 仅向 `eu-west-1`(例如,区域金丝雀)发布新的支付版本。
1. CI 构建镜像 → `payments-service:1.8.0` → 推送至 ECR(不可变摘要)
2. 工程师仅编辑 `regions/eu-west-1/payments-service/values.yaml` 文件(`image.tag: 1.8.0`),并创建 PR
3. PR 合并至主分支
4. Argo CD 协调器检测到 `eu-west-1` 区域的差异(仅 `eu-west-1` 区域的应用显示 OutOfSync,其他 24 个区域未受影响)
5. Argo Rollouts 运行金丝雀发布:`5% → 25% → 50% → 100%`,并根据指标进行筛选
6. `eu-west-1` 区域报告运行正常。其他区域未观察到任何变化
### 6.2 全区域部署
**场景:** `payments:1.8.0` 安全地推广到所有 25 个地区 —— 绝不搞"大爆炸"。
1. ECR 中已存在相同的镜像
2. PR 编辑 `global/payments-service/values.yaml` 文件(`image.tag: 1.8.0`)
3. 已合并。所有 25 个由 ApplicationSet 生成的应用均处于 OutOfSync 状态
4. **基于波次的发布**(由生成器中的 `wave` 字段驱动):
| 波次 | 区域范围 | 行为 |
|------|---------|------|
| wave 0 | 金丝雀区域(如 `us-east-1`) | 同步 + 金丝雀分析 |
| wave 1 | 2-3 个低流量区域 | 同步,监控 SLO |
| wave 2 | 5 个区域 | 同步 |
| wave 3 | 剩余约 16 个区域 | 同步 |
5. 每个波次都取决于前一个波次的运行状况(同步窗口/PromQL 分析)。**任何波次的失败都会停止后续波次的推进,但不会回滚运行正常的波次**
### 6.3 关于同步部署
"同时部署到所有 25 个应用" 的要求可以通过机制实现 —— 所有应用可以同时同步 —— 但**推荐的模式是分阶段部署**,因为真正的全球同步部署没有安全阀。
> **同步同步仅用于紧急情况**(例如,安全补丁,此时等待的风险大于部署失败的风险)。务必确保同步同步的可行性,并认真考虑其必要性。
---
## 七、回滚工作流程
**硬性要求:** 能够在任何一个区域立即回滚,且完全不依赖任何其他区域的状态。GitOps 让这一切变得简单明了,因为回滚仅仅是声明一个先前的状态。
### 7.1 单区域回滚
1. `eu-west-1` 错误率在 1.8.0 版本后飙升
2. 回滚 `eu-west-1` 的 overlay 提交(或将 `image.tag` 设置回 `1.7.0`):
```bash
git revert <sha> # 仅修改 regions/eu-west-1/.../values.yaml
```
3. 合并。`eu-west-1` 的 Argo CD 应用出现 OutOfSync 异常;自动同步回滚到 1.7.0 版本
4. 其他 24 个区域:未发生任何更改,未观察到任何异常,也未出现阻塞
### 7.2 隔离原则
每个区域都拥有独立的协调器,并与自身的覆盖网络进行对接,因此一个区域的部署卡住 `ap-south-1` 不会阻碍另一个区域的回滚 `eu-west-1` —— 它们之间没有共享队列和锁。这直接满足了 **"一个区域的故障不得阻碍其他区域的操作"** 这一约束,而串行 CI 流水线从根本上无法保证这一点。
### 7.3 紧急破玻璃回滚
对于亚秒级的紧急情况,要预留一个应急方案:
```bash
argocd app rollback payments-eu-west-1
# 或针对特定集群:
helm rollback
```
**立即回滚当前状态,然后再进行 Git 同步**。将此操作记录为紧急回滚,而非常规操作 —— 常规回滚应始终通过 Git 进行,以确保数据源的准确性。
---
## 八、可观测性、跟踪和故障隔离
你无法操作你看不见的 25 个区域。三层结构:
### 8.1 部署状态
Argo CD 的 UI/API 实时显示所有 25 个应用程序的状态(同步/不同步,健康/降级)。**只需一个屏幕即可解答 "哪些应用程序部署在何处,以及它们的运行状况如何"**。
### 8.2 运行时 SLO
基于 Prometheus 的区域级 SLO,并联合到 AMP/Thanos 以实现全局视图和区域级细化。Argo Rollouts 的金丝雀分析会查询这些 PromQL SLO 以自动提升或自动中止部署 —— **可观测性已融入部署控制循环,而不仅仅是事后查看的仪表盘**。
### 8.3 故障隔离已验证
告警范围限定在特定区域。`payments-eu-west-1 Degraded` 告警会指明区域,值班人员只能针对该区域采取行动。
**定期在故障发生当天验证隔离状态:** 在一个区域的同步过程中终止部署,并确认其他 24 个区域未受影响。
---
## 九、权衡取舍:集中式与分散式
这一决定决定了运营模式。
### 9.1 集中式(一条流水线驱动所有区域)
| 维度 | 评价 |
|------|------|
| 优点 | 集中式排序和观察点;更简单的思维模型;一套编排逻辑 |
| 缺点 | 单点故障、凭证集中、串行耦合、无漂移校正 |
### 9.2 分散式(每个区域自行协调 —— 纯粹的 GitOps)
| 维度 | 评价 |
|------|------|
| 优点 | 完全隔离、无中心单点故障、最小权限凭证、漂移自动校正 |
| 缺点 | 需要管理 25 个代理、学习曲线更陡峭、难以强制执行严格的全局顺序 |
### 9.3 混合方案(推荐)
诚实的答案是混合方案:
- **去中心化执行** —— 每个区域的 Argo CD 独立应用状态(这是隔离要求的硬性规定)
- **集中式意图和编排** —— 一个 Git 仓库、一个 ApplicationSet、Wave 逻辑和可观测性集中整合
> 这样既能获得去中心化的隔离性,又能获得集中化的协调性。**中心发布应该为真的状态;各个分支独立地实现它**。
---
## 十、推荐参考架构
| 组件 | 推荐方案 |
|------|---------|
| **集群** | 25 个 EKS 集群,每个区域一个,每个集群运行 Argo CD + Argo Rollouts |
| **代码库** | 一个 `fleet-config` Git 仓库:不可变的基础图表、`global/` 值层、每个区域的薄覆盖层,以及将单个应用程序定义扩展到所有 25 个区域的 ApplicationSets |
| **工件** | ECR (OCI) 中的图表和图像是不可变的,并且有版本控制;CI 推送工件并合并 Git —— 并且不持有任何集群凭证 |
| **部署** | 一次提交:单区域编辑一个覆盖层;全区域编辑 `global/` 并分批次推出,由金丝雀 SLO 分析控制 |
| **回滚** | `git revert`,作用域限定于一个区域,独立于所有其他区域,并具有 `argocd app rollback` 破玻璃功能 |
| **可观测性** | Argo CD UI 用于部署状态,AMP/Thanos + Grafana 用于 SLO,区域范围警报,可观测性集成到发布控制循环中 |
---
## 十一、总结
从一两个区域扩展到多个区域并非扩展性问题,而是耦合性问题。
**解决耦合性问题的方法:**
1. 将执行分散到每个区域的协调器中
2. 将意图集中到一个声明式代码库中
3. 将每次部署和回滚都作为作用域内的 Git 提交
那么 25 个区域的部署就相当于 3 个区域的部署,只需独立运行 25 次即可。
> "部署到所有区域" 按钮不再是公司里最令人担忧的按钮,因为它所做的任何操作都可以回滚 —— 一次回滚一个区域,独立回滚,只需一次提交即可。