作者: jiangbodev
发布时间: 2026-06-01
技术栈: .NET 10 + Saga + Outbox + PostgreSQL + Kafka/RabbitMQ + OpenTelemetry
阅读时长: 13 分钟
先说一个会被喷的观点:
很多团队做的“多云跨地域容灾”,本质是把单点故障升级成“双倍成本故障”。
因为他们只做了这些表面动作:
- 双活流量切换
- 多地域部署
- 复制数据库
却没有解决真正要命的底层问题:
- 事件跨区复制后重复消费怎么办?
- 幂等键在跨区域如何全局唯一?
- 跨区补偿怎么防止“补偿打架”?
- 演练时怎么证明切流后业务一致性还成立?
这篇我不讲空泛“架构大图”,只讲我把 Saga + Outbox 扩展到多云跨地域后,如何保证:
- 切流不丢账
- 灾备不重放
- 补偿不互撞
- 演练可验收
1. 多云容灾里最容易被忽略的真问题
在单地域里你能靠“本地一致性 + 可靠重试”兜住很多事。
一旦跨地域,问题会立刻复杂化:
- 网络分区下,复制延迟不可避免
- 双写窗口下,事件顺序无法天然保证
- 回切时,旧链路可能重放历史消息
- 两边都触发补偿时,可能互相抵消或重复执行
所以我把目标从“看起来可用”改成“可证明一致”:
- 任意单区故障后,业务最终一致
- 单次业务流程在全局只生效一次
- 回切后不会出现补偿风暴
2. 架构原则:控制面可切,数据面必须可判定
我最终采用的原则只有两条:
- 流量路由可以切,但业务判定规则不能变
- 区域可以降级,但幂等和补偿语义必须全局一致
核心组件:
- 区域内 Saga Runtime(本地执行)
- 区域内 Outbox(本地事务落库)
- 跨区 Event Replicator(异步复制)
- Global Idempotency Store(全局去重判定)
- DR Controller(切换与回切策略)
flowchart LR
A[Region A Services] --> OA[(Outbox A)]
B[Region B Services] --> OB[(Outbox B)]
OA --> RA[Replicator A->B]
OB --> RB[Replicator B->A]
RA --> EB[Event Bus B]
RB --> EA[Event Bus A]
A --> G[(Global Idempotency)]
B --> G
C[DR Controller] --> A
C --> B
3. 第一关键:幂等键全局化(不是区域内唯一)
很多事故都出在“区域内唯一键”被跨区复制打穿。
我用的幂等键规范:
idempotency_key = hash(tenant_id + saga_id + step + business_key + version)
要求:
- 在所有区域共享判定语义
- 写入采用“先判定再执行业务”
- 判定库支持 TTL 与分区归档
伪代码:
var key = IdempotencyKey.Build(tenantId, sagaId, step, businessKey, version);
if (await globalStore.ExistsAsync(key, ct))
return;
await ExecuteBusinessAsync();
await globalStore.MarkAsync(key, ttl: TimeSpan.FromDays(30), ct);
这一步是跨区“只生效一次”的基础,没有它容灾基本不成立。
4. 第二关键:跨区补偿防冲突(Compensation Fencing)
跨区最怕什么?
A 区失败触发补偿,B 区切流后又触发一次补偿,最后把业务状态补到错误方向。
我做了“补偿栅栏”机制:
- 每个 Saga 实例维护全局补偿代号
comp_epoch - 补偿执行前先抢占 fencing token
- 只有 token 持有者可执行补偿
- 失效 token 的补偿请求直接拒绝
简化表结构:
CREATE TABLE saga_fencing (
saga_id VARCHAR(64) PRIMARY KEY,
comp_epoch BIGINT NOT NULL,
owner_region VARCHAR(32) NOT NULL,
token VARCHAR(64) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL
);
这套机制把“重复补偿”问题从业务代码里抽离成平台规则。
5. 第三关键:复制策略要分“事件类型等级”
不是所有事件都需要同等级跨区复制。
我按业务价值分级:
- L1(资金/库存):强一致语义,优先复制,回放窗口长
- L2(订单状态):准实时复制,可短延迟
- L3(通知/统计):可降级,允许批量补偿
策略示例:
eventReplication:
L1:
mode: near-realtime
maxLag: 5s
replayWindow: 72h
L2:
mode: async
maxLag: 30s
replayWindow: 24h
L3:
mode: batch
maxLag: 5m
replayWindow: 6h
这样做的收益很现实:核心链路保真,非核心链路控成本。
6. 切流与回切:必须是“状态机”而不是“人工流程”
我把容灾切换定义成状态机:
NormalPrepareFailoverFailoverStabilizingPrepareFailbackFailback
每一步都绑定门槛:
- outbox 积压阈值
- 复制延迟阈值
- 幂等拒绝率阈值
- 补偿失败率阈值
只要一个指标超阈,状态机禁止前进。
7. 演练策略:每月一次“跨区一致性”强制演练
我把演练固定成 3 个剧本:
剧本 A:单区失联
- 注入:Region A 与消息总线网络中断 10 分钟
- 验收:Region B 接管后核心流程成功率不低于阈值
剧本 B:重复回放
- 注入:强制重放最近 30 分钟 L1/L2 事件
- 验收:全局幂等拒重命中率符合预期,无重复业务生效
剧本 C:双区补偿竞争
- 注入:同时触发同一 Saga 的补偿请求
- 验收:仅一个 fencing token 生效,无双补偿
8. 成本与可靠性平衡:别把容灾做成无限烧钱
跨区后账单会上涨,这很正常。
我控制成本的方法:
- 核心链路高保障,非核心链路分级降配
- 复制保留窗口按事件等级配置
- 观测数据跨区只保留关键标签
- 回切后自动执行“沉积消息清算”避免长期堆积
我关注两个核心指标:
目标不是把 压到最低,而是在 达标前提下最小化 。
9. 我踩过的 6 个真实坑
- 幂等键没带版本,升级后误判重复
- 复制只测延迟,不测重放
- 切流只看入口可用,不看补偿队列
- 回切后没做消息清算,历史消息二次污染
- 只演练单区故障,不演练跨区补偿竞争
- 把“全量双活”当目标,忽略业务分级现实
10. 落地结果
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 单区故障切换恢复时间 | 18 分钟 | 4 分钟 |
| 跨区重复生效率 | 有明显风险 | 接近 0 |
| 补偿冲突事件 | 偶发 | 基本消失 |
| 跨区容灾演练通过率 | 60% | 93% |
| 容灾单位成本 UC_dr | 1.00x | 0.78x |
结论很直接:
多云容灾能不能真抗打,不看拓扑图,看你有没有做全局幂等和补偿栅栏。
持续更新 .NET 云原生实战:
- 公众号: jiangbodev
- 微信小程序: 俭用账本、简用活动助手
- 掘金: juejin.cn/user/640371…