高可用(二):容灾、多活与故障转移
高可用(二):容灾、多活与故障转移
导语:前面《分布式(一)》讲过"脑裂与多数派仲裁"的理论(第 10 题),《Redis(三)》讲过缓存与集群的自动故障转移。本篇把视角拔到机房级容灾:RTO/RPO 怎么定、同城双活与异地多活怎么选、异地多活的核心难点与单元化改造、多机房数据同步与冲突解决、流量调度与"一键切流"的坑、容量水位与容灾演练,共 9 题。
一、容灾目标与架构选型
1. RTO 与 RPO 是什么?如何确定容灾等级?
答: 两个指标定义了"灾难发生后,业务能承受多大的损失":
| 指标 | 含义 | 通俗理解 |
|---|---|---|
| RTO(Recovery Time Objective) | 恢复服务所需的时间目标 | "多久能重新对外服务" |
| RPO(Recovery Point Objective) | 可容忍的数据丢失量(时间维度) | "最多能丢多久的数据" |
|<-- RPO -->| |<-- RTO -->|
上次可用数据点 故障发生 服务恢复容灾等级(从低到高):
| 等级 | 架构 | RTO | RPO | 成本 |
|---|---|---|---|---|
| L1 | 冷备(只备份数据,不备机) | 小时~天 | 分钟~小时 | 最低 |
| L2 | 温备(备机但不接管流量) | 分钟~小时 | 秒~分钟 | 低 |
| L3 | 热备 / 主备(备机可快速接管) | 分钟级 | 秒级(同步复制可为 0) | 中 |
| L4 | 同城双活(两机房同时服务) | 秒~分钟 | ≈ 0 | 高 |
| L5 | 异地多活(多地域同时服务) | 秒级 | ≈ 0(单元内) | 极高 |
怎么定级(架构决策):
业务损失 = 停机损失(RTO) + 数据丢失损失(RPO) + 容灾成本
→ 选"总成本最低"的那一档,而不是无脑选最高档- 支付、交易类核心链路 → 通常要求 RPO ≈ 0、RTO 分钟级 → 同城双活(异地多为"冷备 + 数据异步同步")。
- 内容、社交、搜索 → 可容忍分钟级 RPO → 异地多活(体验优先,就近接入)。
加分点:RTO/RPO 是业务指标,不是技术指标——架构师要做的第一件事是和业务对齐"能接受丢多少、停多久",再倒推技术方案。
2. 同城双活、两地三中心、异地多活怎么对比?
答:
| 架构 | 拓扑 | 特点 | 主要解决的问题 | 局限 |
|---|---|---|---|---|
| 同城双活 | 同城 A / B 两机房同时承载流量 | 低延迟(< 2ms)、可同步复制实现 RPO≈0 | 单机房故障、机房级断电 | 无法应对城市级灾难 |
| 两地三中心 | 同城双活 + 异地灾备(一主两备) | 兼顾"机房故障"与"城市级灾难" | 城市级灾难恢复 | 异地中心多为只读/冷备,接管慢 |
| 异地多活 | 多地域机房同时对外服务 | 就近接入、单元自治、故障单元可切走 | 城市级灾难 + 体验最优 | 数据同步与冲突是最大难题,改造成本极高 |
架构选型路径:
起步:单机房多副本(解决机器/机架故障)
↓ 有预算
同城双活(解决机房故障,RPO≈0)
↓ 有城市级容灾要求
两地三中心(加异地灾备)
↓ 业务量极大 + 要求就近接入
异地多活(单元化改造)一句话:同城双活解决"机房挂",异地多活解决"城市挂 + 体验";而"两地三中心"是两者的折中——异地只做灾难兜底,不承担日常流量。
3. 异地多活的核心难点是什么?单元化(Set 化)如何解决?
答: 异地多活的本质难题是——同一个用户的数据在多地可写,就会出现多点写冲突与一致性灾难。如果只是"多地部署 + 数据互相同步",会立刻遇到:
- 同一订单在两个地域同时被修改 → 写写冲突;
- 跨地域同步延迟 → 读到旧值、重复扣款、超卖;
- 全量双向同步 → 网络带宽与延迟爆炸。
单元化(Set 化)的解法——"让数据只有一个写入口":
| 设计要点 | 说明 |
|---|---|
| 流量按维度分片路由 | 按 userId(或 sellerId/orderId)哈希,把用户固定路由到某个单元(Set) |
| 单元封闭(Cell Isolation) | 一个单元内是完整的业务闭环:应用 + 缓存 + DB + MQ 都在本单元,单元内调用是本地调用 |
| 单元内写唯一 | 该用户的所有写操作只发生在它所属的单元 → 天然无写冲突 |
| 异地只读 | 其他单元最多保留该用户的只读副本(用于查询/容灾) |
| 少量全局数据 | 全局唯一的数据(如全局配置、跨单元交易)单独放在"中心单元",或用"异地双写 + 冲突合并"特殊处理 |
用户A ──hash──▶ 单元1(华东) 写/读都在单元1(自闭环)
用户B ──hash──▶ 单元2(华北) 写/读都在单元2(自闭环)
两地之间只同步"只读副本"与"需要跨单元的数据"四个关键收益:写冲突消失(单元内唯一写)、故障可切(单元故障 → 把该单元流量切到其他单元)、延迟降低(就近接入)、扩容线性(加单元即可)。
代价:应用、DB、MQ 都要按单元部署;跨单元操作(如用户 A 给用户 B 转账) 需要特殊设计(异步 + 补偿 / 中心化处理),这是异地多活最难啃的部分。
4. 多机房数据同步有哪些方案?数据冲突怎么解决?
答:
同步方案谱系:
| 层级 | 方案 | 说明 |
|---|---|---|
| 存储层 | MySQL 主从 / MGR、双向复制、Oracle GoldenGate | 单主多从最安全;双主/双向必须解决冲突 |
| 中间件层 | Canal / Debezium 订阅 binlog → MQ 分发 | 变更事件驱动,可跨异构系统,可控性好 |
| 业务层 | 业务双写、单元内单写 + 跨单元事件同步 | 侵入大,但语义最清晰 |
| 全局协调 | 全局时钟 / 逻辑时钟(TSO、HLC) | 为"谁先谁后"提供统一顺序(《分布式(一)》第 7 题) |
冲突解决策略(双向同步时必答题):
| 策略 | 说明 | 适用 |
|---|---|---|
| 避免冲突(最优) | 单元化:数据只有一个写入口 | 优先选择 |
| Last-Write-Wins(LWW) | 用时间戳/版本号,后写覆盖先写 | 对冲突不敏感的数据(如用户资料) |
| 业务规则合并 | 计数器合并(求和)、购物车合并(并集) | 可交换操作(CRDT 思想) |
| 人工/对账兜底 | 冲突落表 + 人工处理 | 资金类等不可自动合并 |
核心结论:冲突解决的最好办法是"不让冲突发生"——单元化之所以是多活首选,就是因为它在架构层面消灭了双写。
5. 流量调度与故障转移有哪些手段?
答: 故障转移 = "发现故障" + "把流量导走",分两层看:
(1)流量层(入口引流)
| 手段 | 说明 | 特点 |
|---|---|---|
| DNS / GSLB | 按地域、健康状态返回不同机房 IP | 生效受 TTL 与本地 DNS 缓存影响,切换慢(分钟级) |
| HTTP DNS | 客户端 SDK 直接请求 DNS 服务 | 绕开 LocalDNS,切换快、可精准调度 |
| VIP + Keepalived(VRRP) | 主备 VIP 漂移 | 同机房内秒级;跨机房需打通二层 |
| Anycast | 同一 IP 在多机房宣告,路由就近 | 天然就近 + 故障自动收敛 |
| 负载均衡 / 网关 | LVS / Nginx / 云 SLB:健康检查 + 剔除节点 | 服务级快速失败转移 |
| 服务发现 | Nacos/Consul 健康检查 + 实例下线 | 微服务内部调用级转移 |
| 配置中心开关 | 一键切流(改路由规则/权重) | 人工可控,用于大促/故障 |
(2)数据层(保证切过去数据可用)
- DB 主从/MGR 自动选主、Redis Sentinel/Cluster、MQ 副本切换(《Redis(三)》等)。
- 缓存与状态:切流后新建连接打到新机房,但要处理"会话粘性、缓存预热、瞬时回源压力"。
答题结构:发现(健康检查/监控)→ 决策(自动阈值/人工确认)→ 切流(DNS/VIP/网关/服务发现)→ 数据确认(新主可用、数据到位)→ 流量确认(成功率恢复)——缺任何一环都可能"流量切过去了,数据没跟上"。
二、故障转移的可靠性与演练
6. 数据库故障转移时,如何保证"切主不丢数据、不双写"?
答: 这是故障转移中最危险的一步——老主没死透、新主已接管 → 双写 → 数据永久错乱。要点三件套:
(1)不丢数据:依靠共识与复制级别
| 机制 | 说明 |
|---|---|
| 同步复制(半同步/MGR) | 至少 N 个副本确认后才算提交 → RPO≈0,但写延迟上升、可用性下降(副本挂会阻塞写) |
| 多数派仲裁 | 只有拿到多数派的节点才能成为新主(Raft/MGR/Paxos 系,见《分布式(一)》第 11、12 题) |
| 日志位点校验 | 切换前比对 GTID / binlog 位点,确保新主不比老主旧(否则老主恢复后会"凭空多出数据") |
(2)不双写:先隔离,再切换
| 手段 | 说明 |
|---|---|
| Fencing(围栏)/ STONITH | 切主前强制隔离老主(关机、断网、吊销存储访问),确保"老主绝对写不进去" |
| 租约(Lease) | 主节点持有有期限的领导权,到期未续期自动失效,防止"老主自认为还是主" |
| Proxy 层单点写路由 | 写请求统一走代理(如 MyCat/ShardingSphere/中间件),由代理保证"只有一个主" |
(3)兜底:切换后必须校验
切换完成 → 校验新主数据完整(位点/行数/对账) → 老主恢复后作为从库接入(先补齐数据)一句话:"先隔离旧主,再让多数派选新主"——这两步的顺序不能颠倒;脑裂的根因几乎都是"旧主还活着又开始接受写入"(详见《分布式(一)》第 10 题)。
7. 单机房故障时,剩余机房容量怎么保证够用?
答: 这是最容易被忽略的容灾前提——双活不是"两台机器",而是"每一台都要能扛住全部流量":
| 要点 | 说明 |
|---|---|
| 容量水位 | 常态做 N+1:假设任一机房失效,剩余机房要有能力承接 100% 流量 |
| 水位红线 | 日常 CPU/连接数水位控制在 50% 左右(两个机房任一挂掉,另一个翻倍后仍有余量);若常态 80%,故障时必然雪崩 |
| 降级联动 | 单机房故障后,同步启动降级(关闭非核心功能)以释放容量(见《高可用(一)》) |
| 弹性扩容 | 云上通过 HPA/弹性伸缩快速补机器(但扩容有分钟级延迟,不能只靠它) |
| 依赖项也要双活 | 缓存、MQ、注册中心、DB 都必须是多副本,否则"单个依赖挂"依然会全站故障 |
架构原则:容灾规划要按"最坏情况"算——同城双活按"剩一个机房"算容量,异地多活按"剩 N-1 个单元"算容量,并在日常用压测验证这个假设。
8. 如何设计"一键切流"与回切?有哪些坑?
答: 切流的目标是"可控、可逆、可观测":
切流设计:
| 环节 | 要点 |
|---|---|
| 决策 | 自动(监控阈值/健康检查)+ 人工(值班决策)双通道;重大切流需双人确认 |
| 执行 | 配置中心一键下发路由规则(按比例灰度放量:1% → 10% → 50% → 100%) |
| 数据前置 | 切流前确认新机房数据已同步到位(尤其缓存的预热、DB 的主从延迟) |
| 观测 | 成功率、RT、错误码、DB 连接数、缓存命中率;有明确回滚阈值 |
五个常见的坑:
- 一次切 100% → 新机房瞬间被打爆;必须灰度放量。
- 缓存未预热 → 切过去后缓存全空,全部回源打垮 DB(必须预热 + 回源限流)。
- DNS 缓存导致"切不干净" → 部分流量仍在老机房(用 HTTP DNS / 短 TTL / 网关层收敛)。
- 状态未迁移 → 会话、本地缓存、定时任务(同一任务在两机房同时跑)造成重复执行。
- 回切比切流更危险 → 老机房恢复后数据可能落后,回切前必须先补齐数据、再小流量验证、最后放量。
一句话:切流是"熵减"操作,必须灰度、可回滚、带观测;演练不足的切流脚本,等于一颗定时炸弹。
9. 容灾演练怎么做才有效?云原生时代有什么新思路?
答:
有效演练的四个原则:
| 原则 | 说明 |
|---|---|
| 无预告 | 提前通知的演练只能验证"文档",无预告才验证"真实能力" |
| 真故障注入 | 拔网线、杀节点、注入地域延迟、模拟机房断网(ChaosBlade / Chaos Mesh / Chaos Monkey) |
| 全链路参与 | 不只是运维,业务方、值班、客服一起走完"发现→决策→切流→恢复"全流程 |
| 有复盘与改进 | 记录 MTTR(平均恢复时间)、暴露的问题;每次演练后更新预案与容量 |
云原生 / 多活时代的新思路:
| 思路 | 说明 |
|---|---|
| 多集群 K8s + 多活控制面 | 集群级容灾:一个 K8s 集群整体故障,流量切换到另一个集群(如 Karmada / 云多集群方案) |
| Service Mesh 流量治理 | 用 Sidecar(Istio/Envoy)做熔断、重试、超时、故障注入、流量镜像,把容灾能力下沉到基础设施(见《高可用(一)》) |
| 混沌工程平台化 | 把"故障注入"变成常态化能力(演练即代码),在预发/生产小流量持续验证 |
| 单元化 + 流量调拨 | 故障单元流量按比例、按用户维度精准调拨到健康单元 |
| 多活数据层 | 全局 TSO / 逻辑时钟、CRDT 等,尝试从"避免冲突"走向"自动合并冲突" |
本章小结:容灾的完整链条是「目标(RTO/RPO)→ 架构(双活/多活/单元化)→ 数据(同步与冲突)→ 切流(灰度可回滚)→ 容量(N+1 水位)→ 演练(无预告混沌)」。其中最难的不是技术方案,而是"容量是否真的够"和"预案是否真的演练过"——这两点决定了纸面架构能否在真实故障中活下来。
