36.4 两地三中心与多活
一句话
两地三中心解决“机房没了怎么办”,多活解决“机房没了业务还在跑”;拓扑每加一层,成本翻倍、写延迟按光速上涨,而真正的门槛不是机房数量,是数据冲突方案、流量调度和演练能力。
先分清五个东西
| 概念 | 部署 | 故障域 | 主要解决 |
|---|---|---|---|
| 单机房多可用区 | 1 地域、3 AZ | 单 AZ 故障 | 服务器/机架/电力故障 |
| 备份 + 冷备 | 1 主 + 异地存储 | 机房整体故障 | 数据不丢(RTO 小时级) |
| 同城双活 | 2 个机房,距离 < 50km | 单机房故障 | RTO 分钟级、RPO≈0 |
| 两地三中心 | 同城 2 + 异地 1 | 城市级故障 | 城市级灾难 |
| 多活 | ≥ 2 个地域同时承载流量 | 地域级故障 | 业务连续,用户无感 |
“两地三中心”是拓扑,“多活”是流量的使用方式:三个中心都待命但只有一套在跑,那是灾备;三套都在接流量,才是多活。
四种拓扑的账:延迟、RPO、可用性、成本
| 拓扑 | 写延迟 | RPO | RTO | 年停机 | 年可用性 | 成本 | 单中心故障后 |
|---|---|---|---|---|---|---|---|
| 单中心 + 异地冷备 | 0.5ms | 24h | 522.6min | 34.8h | 99.6023% | 1.0× | 整体不可用 |
| 同城双活 + 异地冷备 | 1.5ms | 30s | 30min | 2.0h | 99.9772% | 2.1× | 切到同城另一中心 |
| 两地三中心(异地同步) | 30ms | 0 | 1min | 0.1h | 99.9992% | 3.1× | 切到同城或异地 |
| 三地五中心 | 30ms | 0 | 0.5min | ≈0 | 99.9996% | 5.2× | 存活地域各担 50% |
三个读数:
- 写延迟是物理代价:同城双活只多 1ms;异地同步直接到 30ms。这不是软件优化能消掉的——光在光纤里约
200km/ms,30ms 往返意味着两个机房相距约 3000 公里,往返一趟就是 30ms。 - RPO=0 与低延迟不可兼得:要么同步(吃 RTT),要么异步(丢最后一段)。异步复制的 RPO 由积压 ÷ 带宽决定:4 GiB 积压、200 MB/s 带宽 = 滞后 20 秒。
- 成本不是线性买 9:从 2.4 个 9 到 3.6 个 9 只要多花 1.1×(结构改动),从 5.1 个 9 到 5.4 个 9 要多花 2.1×(五中心)。最后半个 9 最贵。
9 的边际成本:什么时候该停
| 从 → 到 | 可用性 | 多买的 9 | 成本变化 | 每多一个 9 约花 |
|---|---|---|---|---|
| 单中心 → 同城双活 | 99.60% → 99.977% | +1.24 | +1.1× | 0.9× |
| 同城双活 → 两地三中心 | 99.977% → 99.9992% | +1.48 | +1.0× | 0.7× |
| 两地三中心 → 三地五中心 | 99.9992% → 99.9996% | +0.30 | +2.1× | 7.0× |
🔑 结论很直接:同城双活是性价比最高的那一步(花 1.1 倍成本,年停机从 34.8h 降到 2h);两地三中心再花 1 倍换来“城市级灾难”的保险;三地五中心只多 0.3 个 9 却要 2.1 倍成本,只有“每分钟停机都按金额计损”的业务才值得。
RPO=0 的实现代价:同步、多数派与写延迟
| 复制方式 | 写延迟 | RPO | 可用性影响 |
|---|---|---|---|
| 异步(主→备) | 本地写,最低 | 滞后量(秒~分钟) | 主挂了可能丢最后一段 |
| 同步(主→备,备确认才返回) | +1 个 RTT | 0 | 备挂了写不进去(除非降级) |
| 半同步(多数派确认) | + 最快的多数派 RTT | 0(不丢已提交) | 容忍少数派故障 |
| 三副本跨 3 AZ,法定多数 | + 同城 RTT | 0 | 丢 1 个 AZ 仍可写 |
关键约束:跨地域同步写,等于把异地 RTT 加到每一次写操作上。30ms 的 RTT 对下单接口是灾难(一次请求 3 次写 = +90ms),所以异地通常只能异步;要 RPO=0 又不想吃延迟,就得把“写”收敛在同城,异地只承接读和灾备切换。
多活的三道门槛
门槛一:数据冲突
两个地域同时接受写,同一份数据就可能被两边改:
| 方案 | 做法 | 代价 |
|---|---|---|
| 单元化(Set) | 按用户/租户分片,同一用户永远落同一单元 | 跨单元查询与转账要专门设计 |
| 单写多读 | 某类数据只在一个地域写 | 写请求要路由,延迟上升 |
| 冲突检测(版本号/LWW) | 后写覆盖先写 | 静默丢更新 |
| CRDT | 设计成可合并的数据类型 | 只适合计数、集合、点赞等 |
| 业务补偿 | 冲突后人工/自动对账 | 复杂,且不适合资金 |
⚠️ 没有单元化的“多活”往往等于双写:同一个用户的订单可能落在两个地域,库存各扣一次,最后靠对账修。多活的复杂度主要在这里,不在机房。
门槛二:流量调度
| 层 | 手段 | 关键参数 |
|---|---|---|
| DNS / GSLB | 按地域解析、按健康状态切 | TTL 60–300s(TTL 太长,切流慢;太短,DNS 压力大) |
| 入口网关 | 按 header/地域/灰度比例路由 | 需要能“一键切回” |
| 数据层 | 按分片键路由 | 路由表要能热更新 |
切流最容易出事的不是“切过去”,而是切回来:主机恢复后流量回切,缓存冷启动 + 连接风暴 + 数据追上,往往比故障本身更糟。
门槛三:容量与演练
- 容量必须 N+1:双活各承载 50%,一台挂了另一台要扛 100% → 容量按 2× 预留;三活各 33%,一个地域挂了存活地域各扛 50% → 容量按 150% 预留(每地域多留 50%)。
- 演练:不演练的多活,切换时长的真实值是估算的 2–3 倍;且“部分切换”(只切 10% 流量)才是可验证的方式。
异地多活怎么落地:四步 + 四技巧
多活的难点不在机房,而在决定放弃什么。一套可执行的顺序:
| 步 | 做什么 | 判据 |
|---|---|---|
| ① 业务分级 | 只给核心业务做多活 | 三个标准:访问量最大、核心链路、产生收入;三者冲突时以 “影响用户数” 为准 |
| ② 数据分类 | 逐类数据打 5 个标签 | 数据量、唯一性、实时性、可丢失性、可恢复性 |
| ③ 数据同步 | 按标签选同步方式 | 存储同步 / 消息队列 / 重复生成 / 回源读取 |
| ④ 异常处理 | 假设同步一定会失败 | 多通道、同步+访问结合、日志记录、只保核心 + 安抚 |
② 数据分类:五个维度决定方案
| 维度 | 问什么 | 影响 |
|---|---|---|
| 数据量 | 增量(增删改)有多大 | 越大,同步延迟的几率越高 |
| 唯一性 | 两个机房能否产生同类数据 | 要求唯一 → 只能单点产生,或引入全局 ID(44.24 发号器与全局 ID) |
| 实时性 | 改完多久必须到对端 | 越高,方案越复杂(同城同步 vs 异地异步) |
| 可丢失性 | 丢了业务会不会出事 | session 可丢,账户不可丢 |
| 可恢复性 | 丢了能不能补回来 | 微博可重发、密码可找回、账号不可恢复 |
③ 四种同步方式
| 方式 | 适合 | 例子 | 代价 |
|---|---|---|---|
| 存储系统同步 | 有事务/时序要求、改动不频繁 | 密码、用户信息(MySQL 主从) | 通常只有一条同步通道,量大时延迟到分钟级 |
| 消息队列同步 | 无事务、无时序要求 | 新注册账号 | 顺序与幂等要自己做 |
| 重复生成 | 可再生数据 | session、验证码、缓存 | 用户需要重新登录/重新获取 |
| 回源读取 | 数据有明确归属机房 | 跨机房读 session / 账号 | 多一次跨机房调用,依赖路由正确 |
🔑 最典型的一次选择:消息队列适合 “账号”(只创建,先后无所谓),不适合 “密码”(必须先 m 后 n)。同一套系统里不同数据走不同通道,才是常态。
四技巧:每一条都是主动放弃
| 技巧 | 放弃什么 | 换到什么 | 具体做法 |
|---|---|---|---|
| 只保核心业务 | 注册、资料修改的多活 | 登录 100% 可多活 | 登录只需账号密码全量复制到每个中心;注册受 “手机号唯一” 约束,跨机房无法判重,硬做会产生不可修复的冲突 |
| 只保核心数据最终一致 | 实时一致 | 方案可落地 | session 不同步(重登即可);只同步核心表;一致性窗口按分钟算 |
| 多种手段同步 | “只依赖存储同步” 的简单 | 极端场景仍可用 | 账号走 MQ + 数据库双通道;本地读不到时按路由二次读取对端;session 走回源 |
| 只保绝大部分用户 | 100% 可用 | 整体方案可行 | 接受千分之几的受损用户;强一致业务(转账)改成 “申请 + 异步重试”;挂公告 + 事后补偿 |
算例一:为什么 “保登录、弃注册” 是算出来的
| 业务 | 每天的量级(日活 1000 万的系统) | 多活难度 |
|---|---|---|
| 登录 | ≈ 1000 万次 | 低:每个中心都有全量账号密码,换个中心重登即可 |
| 注册 | 几万次 | 高:A 机房注册未同步、A 挂掉后用户在 B 注册,A 恢复即冲突,且不能 “以后一次为准” |
| 改资料 | 不到 1 万次 | 中:可用 LWW,但要求多机房时钟误差 < 1 秒,异地做不到 |
结论:把有限的复杂度投在 “影响 1000 万人” 的登录上,而不是 “影响几万人” 的注册上。顺带一提,为了多活去改 “一个手机号只能注册一个账号” 这条核心业务规则,代价远大于收益。
算例二:银行转账为什么不能实时多活
小明有 1 万元,北京中心转给张三 1 万、上海中心转给李四 1 万——两地同时成功就是双花。所以 “实时转账” 只能单中心写。
可行的降级是把它变成 “转账申请”:上海中心只记录申请 → 后台异步向北京中心发起 → 北京不可用就重试等待 → 恢复后余额不足则置为失败。代价是用户体验从 “一次操作” 变成 “提交 + 等结果”,补救手段是短信通知结果。
④ 异常处理清单
| 手段 | 做法 | 关键点 |
|---|---|---|
| 多通道同步 | DB 同步 + MQ 同步并行 | 两条通道不能走同一条网络(一公网一内网);数据必须可重复覆盖(账号可以,密码不行) |
| 同步 + 访问结合 | 本地优先,读不到按路由访问对端接口 | 接口通道与同步通道分离;路由规则要能推断数据归属 |
| 日志记录 | 关键操作前后记流水 | 用于事后修正数据,日志本身要跨机房可取 |
| 只保核心 + 安抚 | 挂公告、事后补偿(代金券/礼包)、补充体验(短信通知) | 要明确写清 “哪部分用户会被牺牲” |
可运行的拓扑账本
package main
import (
"fmt"
"math"
)
type topo struct {
name string
writeRTT float64 // 一次同步写要等的跨节点 RTT (ms)
rpoSec float64
rtoHour float64
cost float64 // 相对成本(单中心 = 1)
after string // 单中心/单地域故障后的状态
}
func rpo(sec float64) string {
if sec >= 3600 {
return fmt.Sprintf("%.0fh", sec/3600)
}
return fmt.Sprintf("%.0fs", sec)
}
func main() {
const incidents = 4.0
const hours = 365 * 24.0
list := []topo{
{"单中心 + 异地冷备", 0.5, 24 * 3600, 8.71, 1.00, "整体不可用"},
{"同城双活 + 异地冷备", 1.5, 30, 0.50, 2.10, "切到同城另一中心"},
{"两地三中心(异地同步)", 30, 0, 1.0 / 60, 3.10, "切到同城或异地"},
{"三地五中心", 30, 0, 0.5 / 60, 5.20, "存活地域各担 50%"},
}
fmt.Printf("%-24s %7s %6s %7s %7s %9s %5s %s\n",
"拓扑", "写延迟", "RPO", "RTO", "年停机", "可用性", "成本", "故障后")
for _, t := range list {
down := t.rtoHour * incidents
avail := 1 - down/hours
fmt.Printf("%-24s %5.1fms %6s %6.1fmin %6.1fh %8.4f%% %4.1fx %s\n",
t.name, t.writeRTT, rpo(t.rpoSec), t.rtoHour*60, down, avail*100, t.cost, t.after)
}
// 每多买一个 9 的边际成本
fmt.Println("\n每多买一个 9 的边际成本:")
prevNines, prevCost := 0.0, 0.0
for i, t := range list {
avail := 1 - t.rtoHour*incidents/hours
nines := -math.Log10(1 - avail)
if i == 0 {
fmt.Printf(" %-24s %.2f 个 9,成本 %.1fx\n", t.name, nines, t.cost)
prevNines, prevCost = nines, t.cost
continue
}
dn, dc := nines-prevNines, t.cost-prevCost
fmt.Printf(" %-24s %.2f 个 9(+%.2f),成本 %.1fx(+%.1fx)→ 每多一个 9 约 %.1fx\n",
t.name, nines, dn, t.cost, dc, dc/dn)
prevNines, prevCost = nines, t.cost
}
// 异地异步复制的滞后:突增的积压 / 跨域带宽
backlogGiB, bwMBs := 4.0, 200.0
lag := backlogGiB * 1024 / bwMBs
fmt.Printf("\n跨域异步复制:%0.0f GiB 积压、带宽 %.0f MB/s → 滞后 %.0f 秒(这段时间的数据会丢)\n",
backlogGiB, bwMBs, lag)
// 单地域 33% 流量的三活:故障后每个存活地域从 33% 涨到 50%
fmt.Printf("三活各承载 33%%:一个地域故障后,存活地域各承载 50%%,需按正常负载的 %.0f%% 预留容量(每地域多留 50%% 余量)\n",
50.0/33.3*100)
}
输出:
拓扑 写延迟 RPO RTO 年停机 可用性 成本 故障后
单中心 + 异地冷备 0.5ms 24h 522.6min 34.8h 99.6023% 1.0x 整体不可用
同城双活 + 异地冷备 1.5ms 30s 30.0min 2.0h 99.9772% 2.1x 切到同城另一中心
两地三中心(异地同步) 30.0ms 0s 1.0min 0.1h 99.9992% 3.1x 切到同城或异地
三地五中心 30.0ms 0s 0.5min 0.0h 99.9996% 5.2x 存活地域各担 50%
每多买一个 9 的边际成本:
单中心 + 异地冷备 2.40 个 9,成本 1.0x
同城双活 + 异地冷备 3.64 个 9(+1.24),成本 2.1x(+1.1x)→ 每多一个 9 约 0.9x
两地三中心(异地同步) 5.12 个 9(+1.48),成本 3.1x(+1.0x)→ 每多一个 9 约 0.7x
三地五中心 5.42 个 9(+0.30),成本 5.2x(+2.1x)→ 每多一个 9 约 7.0x
跨域异步复制:4 GiB 积压、带宽 200 MB/s → 滞后 20 秒(这段时间的数据会丢)
三活各承载 33%:一个地域故障后,存活地域各承载 50%,需按正常负载的 150% 预留容量(每地域多留 50% 余量)
注意表中的“每多一个 9 约 0.7×”看着划算,是因为这一段同时减少了同城的切换时间——拓扑的收益常常来自“结构”,不是“更多副本”。而最后一行每 9 花 7 倍,才是多活的真实成本墙。
参数怎么给
| 你的情况 | 选 | 关键动作 |
|---|---|---|
| 只要求数据不丢 | 单中心 + 异地备份 | 备份跨区、不可变、定期恢复演练 |
| 机房级故障要 30 分钟内恢复 | 同城双活 | 同步复制 + 自动切流 + 容量 2× |
| 城市级灾难也要能开门 | 两地三中心 | 异地异步(RPO 秒级),定期演练切异地 |
| 全国用户、地域级故障无感 | 单元化多活 | 按用户分片、单元内闭环、跨单元异步 |
| 全球用户、写延迟敏感 | 多地域单元化 + 本地写 | 数据按用户归属地域,跨地域只读 |
| 资金/库存类强一致 | 单写多读 + 同城同步 | 不要用 LWW 做多活写 |
反模式
| 反模式 | 后果 |
|---|---|
| 三个中心都在跑,但没有单元化 | 等于双写,库存/余额靠对账修 |
| 异地同步写为了 RPO=0 | 每次写 +30ms,下单/支付接口直接变慢一个数量级 |
| DNS TTL 设 1 小时 | 切流要等 1 小时才全量生效 |
| 只演练“切过去” | 回切时缓存冷启动 + 数据追上,二次故障 |
| 双活各按 50% 容量建设,故障时才发现不够 | 单边扛 100% 直接崩 |
| 用 LWW 处理库存/资金 | 静默丢更新,账对不上 |
| 多活但监控按单机房看 | 一个地域的异常被平均掉,没人发现 |
| 把“两地三中心”当成“两地三活” | 预算按灾备批,承诺按多活给 |
| 给注册/资料修改也做多活 | 手机号唯一性跨机房无法判重,冲突不可修复 |
| 想用 LWW 合并用户资料 | 多机房时钟误差 > 1 秒就乱序,先改的反而不生效 |
| 两条同步通道走同一条专线 | 专线一断,两条通道同时失效,等于单通道 |
| 追求 100% 多活 | 为最后千分之几做完美方案,把 99% 拖下水 |
自检
Q1 同城双活能把 RPO 做到 0、写延迟只多 1ms;两地三中心用异地同步,写延迟立刻变 30ms。这 30ms 能靠优化网络消掉吗?
▸ 答案:消不掉,它是物理下限。光在光纤中的速度约 200km/ms,30ms 是往返时间,对应单程约 3000km——北京到广州、上海到新加坡都是这个量级。优化的空间只有“减少同步跳数”(只同步到一个异地副本,而不是所有副本)和“把同步写收敛到少数关键数据”。要本地写延迟又要 RPO=0,只能把写流量按用户归属收敛到某个地域(单元化),让异地同步发生在后台。
Q2 同城双活 + 异地冷备:RPO=30s 是怎么来的?如果跨域带宽从 200 MB/s 降到 50 MB/s,RPO 会变成多少?
▸ 答案:异步复制下 RPO ≈ 积压 ÷ 带宽。4 GiB 积压、200 MB/s → 20 秒(取整说 30s 是留了余量)。带宽降到 50 MB/s → 4×1024/50 ≈ 82 秒,RPO 变成约 1.5 分钟;如果写入峰值更高,积压会继续涨,RPO 还会恶化。所以 RPO 不是配置项,是带宽与写入速率的函数,大促前要按峰值重算。
Q3 三地五中心只比两地三中心多 0.3 个 9(99.9992% → 99.9996%),成本却从 3.1× 涨到 5.2×。什么业务值得?什么业务不值得?
▸ 答案:值得的是“停机按分钟计损且损失与地域强相关”的业务:支付清算、证券交易、大型电商的核心交易(每分钟损失可量化到几十万以上),且已有单元化与自动化演练能力。不值得的是:内部系统、内容站、SaaS 的非核心模块——它们的 4 个 9 已经足够,多出来的预算放在“减少故障次数”(稳定性工程)上回报更高。
Q4 单元化多活和“两个地域都接流量”有什么区别?为什么说后者常常等于双写?
▸ 答案:单元化按 分片键(用户/租户) 把流量和数据绑定到单元:同一用户的请求永远进同一单元,单元内部是一个完整闭环(网关、服务、分库),跨单元只有异步同步。这样单地域故障只影响 1/N 用户,且不存在同一用户被两地同时写。“两个地域都接流量”如果没有绑定规则,同一个用户的两次请求可能落到不同地域,各自写各自的库,库存/余额就靠对账修——这就是双写。
Q5 双活按 50/50 分流,为什么容量要按 2 倍建?如果按 1.2 倍建会发生什么?
▸ 答案:单机房故障后,存活机房要承接 100% 流量。如果每个机房只按 60% 容量建(总计 1.2 倍),故障瞬间存活侧要扛 100%,超出其 60% 的上限 → 过载 → 超时 → 重试 → 雪崩,故障从“一个机房不可用”变成“两个机房都不可用”。所以双活的容量冗余必须是“任一机房能独立承载全量”,即 2 倍;三活是 1.5 倍(存活两地域各 50%)。
Q6 为什么 “登录” 能做异地多活,“注册” 却做不了?
▸ 答案:因为约束不同。登录只需要全量账号密码已复制到每个中心,用户在哪个中心都能登;注册要满足 “一个手机号只能注册一个账号” 这条唯一性约束,而 A 机房的新注册还没同步到 B 机房时,B 机房无法判重——让用户注册会冲突,不让注册则等于没多活,而冲突一旦发生无法用 “以后一次为准” 修复。所以合理选择是:登录多活、注册单点(或只在主中心可用)。
Q7 银行转账要做异地多活,为什么只能做成 “申请 + 异步” 而不是实时转账?
▸ 答案:实时转账在两地都接写时会出现双花(同一笔 1 万元在北京和上海各转一次都成功)。所以实时写必须收敛到单一中心;多活只能通过 “先记录转账申请、后台异步向归属中心发起、失败重试” 实现,把实时一致降级为最终一致。代价是用户要多一步确认,因此需要短信通知等体验补偿。
跨篇相关:RTO/RPO 的换算见 36.1 RTO / RPO;冷温热三种形态的恢复步骤见 36.3 冷备 / 温备 / 热备;按用户分片的实现见 36.5 单元化 / Set 化架构;切换时的数据一致性见 36.6 切换时的数据一致性;跨地域读写的一致性模型见 72.5 跨地域读写与一致性(待补)。