"我们上了双机房"这句话,在故障演练里经常被一句话击穿:主库挂了,从库在另一个机房,但切过去之后全站写失败——因为连接串写死在一个机房、缓存没有预热、异步任务没有消费端。机房数量不是容灾能力,能通过切换验证的拓扑才是。
部署拓扑的决策本质是把 RTO/RPO 换算成"每多一毫秒可容忍的停机,要付多少复杂度与钱"。这篇给出判据:容灾等级怎么定、多活的前置条件、单元化的收益与代价、以及什么情况下多活根本不该做。
1. 先把容灾等级的口径钉死
四种等级常被混着说,判据落在两个指标上:RTO(恢复要多久) 与 RPO(能丢多少数据)。
| 等级 | RTO | RPO | 典型做法 | 什么时候够用 |
|---|---|---|---|---|
| 备份恢复 | 小时级 | 上一次备份到故障点 | 定期全备 + binlog 归档 | 内部系统、可容忍回滚 |
| 冷备 | 10–30 分钟 | 分钟级 | 待机机器、按需拉起 | 流量低谷可接受短断 |
| 热备 | 分钟级 | 秒级 | 从库实时同步、随时切换 | 多数对外业务 |
| 多活 | 秒级 / 近零 | 近零 | 多机房同时承载流量 | 支付、登录等不可中断面 |
比等级更重要的是:RTO/RPO 是谁承诺的。工程侧自拍的 5 分钟,与写进合同、带赔付条款的 5 分钟是两种约束:前者决定架构,后者决定投入。没有承诺方时,最常见的结局是按"越快越好"建设,再为一年用不到一次的能力持续付账单。
2. 多活的三个前置条件
多活不是"多机房部署",而是三件事同时成立。缺一件,故障时就是全站挂。
| 前置条件 | 内容 | 缺失时的失效形态 |
|---|---|---|
| 流量调度 | 能在秒级把流量从 A 切到 B,且切得回来 | 故障时手动改 DNS,等 TTL 生效 |
| 数据同步 | 双向或单向复制,延迟可控 | 切过去读到旧数据或写冲突 |
| 冲突处理 | 同一条记录两边同时写时的裁决规则 | 数据被覆盖,事后靠人工对账 |
可用性检验(不是设计检验):
流量能切 × 数据能同步 × 冲突能裁决 × 切回来也是通的 = 多活
任一为否 → 实际是"多机房的热备",按热备的 RTO 承诺
冲突处理的规则通常只有三种:以时间戳为准(要求时钟可比)、以机房优先级为准(简单但会丢另一侧写入)、按业务键分片归属(正确但要改数据模型)。第三种是唯一在强约束业务里站得住的,代价是路由逻辑进了业务代码。
3. 单元化的收益与代价
单元化的思路是:按用户维度切片,每个单元自成闭环,故障只影响一个单元。
| 维度 | 收益 | 代价 |
|---|---|---|
| 故障隔离半径 | 单单元故障只影响其用户 | 公共资源(配置、鉴权)仍是全局单点 |
| 扩缩粒度 | 按单元加机器,粒度细 | 容量规划按单元做,峰谷不均 |
| 发布影响面 | 可分单元灰度 | 版本必须全员兼容,不能跨单元调用 |
| 数据一致性 | 单元内事务简单 | 跨单元查询几乎不可行,需要汇总层 |
单元化卖的是隔离半径,不是性能。隐性成本在于"任何跨单元需求都要重新设计":全局排行榜、跨单元搜索、总后台报表,这三类会在之后反复被提出。
4. 同城双活与异地多活
两者常被当成量级差异,其实差在物理距离带来的延迟下限。
| 维度 | 同城(数十公里内) | 异地(千公里级) |
|---|---|---|
| 单向 RTT | 亚毫秒到数毫秒 | 数十毫秒 |
| 同步复制可行性 | 可行(代价是写放大) | 不可行,只能异步 |
| 数据一致性 | 可做到较强一致 | 最终一致,必须处理中间态 |
| 运维成本 | 中等 | 高,跨地域发布与排障复杂 |
| 适用 | 机房级故障、可用区故障 | 区域级灾难 |
判据很直接:需要强一致写的地方,异地多活就要付"跨地域确认"的延迟税。一笔支付要等异地确认,就多出几十毫秒,这几十毫秒往往比故障概率更贵。因此常见组合是"同城做强一致双活 + 异地做异步只读"。
5. 数据层的四种方案与失效模式
| 方案 | 机制 | 切换能力 | 主要失效模式 |
|---|---|---|---|
| 主备 | 单向复制,从库只读 | 手动/半自动提升 | 提升后旧主回连造成脑裂 |
| 双主 | 双向复制 | 两侧都可写 | 自增冲突、写覆盖、复制环路 |
| 分片多副本 | 每片一组副本 | 片级切换 | 片间不一致,聚合结果漂移 |
| 全局一致性层 | 共识协议或分布式事务 | 自动选主 | 多数派依赖,跨地域时延迟高 |
# 数据层切换必须显式声明:谁是权威、切换后如何阻止旧主回连
failover:
mode: manual-confirm # 自动提升风险高,先人工确认
quorum: 2/3 # 多数派门槛
fence: revoke-old-primary # 切换后立即撤销旧主写权限
max-lag-seconds: 5 # 超过则判定为不满足 RPO,拒绝提升
fence 这一步最常被省略,也最容易造成事故:旧主恢复后仍认为自己是主,两边同时写,数据在复制层互相覆盖。提升不是终点,隔离旧主才是。
6. 三层流量切换:切得动与切得回来
流量切换能力分布在三个层次,任一层的"回切"能力缺失,都会把一次演练变成一次事故。
| 层 | 切换机制 | 生效速度 | 回切难点 |
|---|---|---|---|
| DNS | 改解析、调 TTL | 分钟级到小时级 | TTL 缓存、递归服务器缓存 |
| 网关 / 负载均衡 | 改后端池、权重 | 秒级 | 会话粘性、健康检查误判 |
| 服务发现 | 注册中心摘除实例 | 秒级 | 客户端缓存、长连接未断开 |
DNS 是最不可靠的一层:TTL 设得再短,递归缓存也会让部分用户长时间指向旧地址。"切得动但回不来"的常见原因是回切时新机房已有写入,两侧都认为自己该接管。
# 演练时同时验证"切过去"与"切回来",两步都要计时
time dig +short +tcp app.example.com
time curl -so /dev/null -w '%{time_total}\n' https://app.example.com/health
7. 故障演练的真实价值
演练的价值不在于"证明能切",而在于暴露流程里没有自动化的那部分。经验上,第一次演练的时间几乎都花在"谁有权按按钮""密码在哪""监控看哪个面板"上,而不是技术切换本身。
| 演练类型 | 验证什么 | 常见结果 |
|---|---|---|
| 单实例摘除 | 负载均衡与健康检查 | 通常顺利,无信息量 |
| 主库切换 | 数据层 fence 与提升流程 | 暴露连接池不重连、DNS 未生效 |
| 机房级断流 | 流量调度 + 容量余量 | 暴露新机房容量只有 30% |
| 反向切换 | 回切路径 | 暴露"回来的路没人走过" |
# 一次完整演练必须走完四段,每段单独计时并留痕
# 1 决策 2 切过去 3 观察 4 切回来
date +%s > /tmp/drill.t0
curl -so /dev/null -w '%{http_code}' https://app.example.com/health # 切后
dig +short app.example.com # 解析是否生效
mysql -N -e "SELECT MAX(delay) FROM monitor.heartbeat" # 数据面
date +%s > /tmp/drill.t4
必须承认的差异:演练环境不等于生产。数据量、连接数、缓存热度、第三方依赖的真实超时都不同。因此演练结论的正确读法是"流程能否走通",而不是"切换耗时等于 X 秒"。
8. 成本结构与渐进路径
| 成本项 | 构成 | 量级特征 |
|---|---|---|
| 机房 / 云资源 | 冗余算力常年空闲 | 与冗余比例线性 |
| 跨地域带宽 | 数据同步 + 跨区调用 | 常被低估,按流量计费时增长快 |
| 人力 | 双活运维、跨区发布、演练 | 至少一人力冗余 |
| 复杂度税 | 每次需求都要考虑路由与一致性 | 长期、隐性、最贵 |
渐进路径只有一条现实顺序:
第 0 步:备份恢复演练(先证明备份能恢复)
第 1 步:同城机房切换(热备,分钟级 RTO)
第 2 步:单元化拆分(隔离半径)
第 3 步:异地多活(只对必须多活的核心面)
跳过第 0 步直接做多活是最常见的浪费:备份不可恢复时,任何容灾等级都是纸面数字。
9. 度量:三个可验证的数字
| 指标 | 定义 | 采集方式 |
|---|---|---|
| 切换耗时 | 从决策到流量全部落到新机房的墙钟时间 | 演练计时,含人工确认时间 |
| 数据丢失量 | 切换点前后差额(按业务键比对) | 对账任务,而非估算 |
| 演练频率 | 每季度有效演练次数 | 有效的标准是"有回切" |
第一个指标要包含人工时间。很多团队报的"3 分钟切换"只算了执行脚本的时间,没算从发现故障到有人做决定的那 10 分钟——而后者才是 RTO 的真实组成部分。
10. 排错:五种"切换了仍全站挂"
| 现象 | 原因 | 处理 |
|---|---|---|
| 双机房已上线,故障时仍全站挂 | 数据层单主,从库从未提升 | 先演练数据层切换 |
| 切过去后大量报错 | 连接串、密钥、缓存未随流量迁移 | 列"跟随流量"的资源清单 |
| DNS 改了,部分用户仍打旧机房 | 递归缓存未过期 | 网关层切流,DNS 只做兜底 |
| 切换成功,回切后数据分裂 | 两侧同时写,没有 fence | 提升后立即撤销旧主写权限 |
| 演练 3 分钟,真实恢复 40 分钟 | 未计入决策与确认时间 | 计时含人工环节 |
11. 边界:什么情况本就不该多活
| 场景 | 判断 | 理由 |
|---|---|---|
| RTO 要求小时级 | 热备足够 | 多活的复杂度换不到收益 |
| 单区域业务(用户集中一地) | 同城热备即可 | 异地多活无用户收益 |
| 数据强一致要求 | 多活不可行 | 跨地域确认的延迟税高于收益 |
| 团队少于 5 人 | 不做多活 | 运维复杂度超出承受能力 |
| 第三方依赖单点 | 先解决依赖 | 依赖挂了多活也白搭 |
| 流量规模小 | 备份恢复足够 | 故障窗口内损失有限 |
最后两条常被忽略:多活能解决"我的机房挂了",不能解决"支付渠道挂了""云厂商的区域级故障"。容灾等级的瓶颈永远在最弱的那个依赖上,先找它,再谈拓扑。
12. 自测
- 多活的三个前置条件分别缺失时,故障表现是什么?
- 为什么说"多机房部署"不等于多活?
- 单元化真正的收益是隔离半径还是性能?它的隐性成本在哪类需求上暴露?
- 异地多活做不了强一致写的根本原因是什么物理约束?
- 数据层切换里
fence这一步省略会导致什么? - RTO 指标为什么必须包含人工决策时间?