跳转至

多活、异地容灾与 RPO/RTO

30 秒版(开场)

RPO = 可丢多少数据(Recovery Point Objective);RTO = 恢复要多久。多活是双城同时接流量;冷备 RTO 长、成本低。生产关键词:同城双活、异地多活、单元化、数据复制 lag。

3 分钟版(精讲深度)

  1. 是什么:DR = 机房/地域故障后业务连续性;多活 = 多机房同时服务;RPO/RTO 是业务 SLA 指标。
  2. 为什么:单机房火灾、光缆切断、云区域故障——年度可用性 99.99% 仍可能宕数小时。
  3. 怎么做:同城可用同步/半同步复制把目标 RPO 压低,异地常用异步复制;但 RPO=0 必须由端到端写入确认、日志/MQ 和故障切换协议共同保证,不能只凭“同步复制”下结论。配套单元化路由、GSLB/客户端多端点与定期演练。

10 分钟版(原理 + 图示)

flowchart TB
  GSLB[Global DNS / GSLB]
  GSLB --> AZ1[可用区 A - 50% 流量]
  GSLB --> AZ2[可用区 B - 50% 流量]
  AZ1 --> Svc1[Go 服务]
  AZ2 --> Svc2[Go 服务]
  Svc1 --> DB1[(MySQL 主)]
  Svc2 --> DB2[(MySQL 从/双主)]
  DB1 <-->|同步/异步复制| DB2

架构层级

模式 RPO RTO 成本 适用
冷备 小时~天 小时 低 非核心
温备 分钟 30min 中 内部系统
热备(主从) 秒~0 分钟 中高 核心交易
同城双活 可设计为接近 0 秒~分钟,取决于检测和切换 高 金融同城
异地多活 秒~分钟 秒 很高 超大厂

容量估算(双活)

  • 单城容量 10 万 QPS,双活每城常态 5 万 QPS,故障时单城扛 10 万(需 100% 冗余)。
  • 跨城复制带宽:写 1 万 TPS × 2KB = 20 MB/s 异步复制;同步则 RT 增加 20~40ms。

单元化(Set 化)

  • 用户按 user_id % N 绑定单元,单元内闭环 DB+缓存,跨单元调用走 RPC。
  • 故障切单元比切整城粒度更细。

生产场景

  • 支付核心:同城双活 + 异地冷备,RPO=0,RTO<5min。
  • 内容 Feed:异地多活,RPO 1min 可接受,CDN 兜底读。
  • 可观测:复制 lag、GSLB 健康检查、切流演练记录。

排查与工具

工具 用途
SHOW REPLICA STATUS(MySQL 8+) 复制 lag;旧版本命令为 SHOW SLAVE STATUS
混沌工程(机房隔离) 验证 RTO
对账任务 双活数据漂移
多活路由配置 流量比例

架构取舍

方案 适用 不适用
同城双活 低延迟同步 跨地震带
异地多活 全局用户 强一致写
单元化 超大规模 小团队
冷备 成本敏感 RTO<15min 要求

深挖问答

  1. 双活写冲突? → 单元化避免;或 LWW + 业务合并;金融用主备非双写。
  2. DNS 切流慢? → 降 TTL 只能缩短部分缓存窗口,递归 DNS/客户端可能不严格遵守;关键客户端还应支持多 endpoint、健康探测和受控重试。
  3. Redis 多活? → CRDT/异步复制,接受短暂不一致;或分片各城独立。
  4. Go 服务如何无状态多活? → 会话 Redis;配置中心多活;对象存储跨区复制。
  5. 如何量化 RPO? → 用已确认但尚未复制/持久化的日志位置或事务数衡量;“lag 秒数 × 平均 TPS”只能做粗估,峰值和事务大小会让它失真。

反模式与事故

  • 备份从未恢复演练,真灾备磁盘损坏。
  • 双活共用一个 Redis,机房隔离后脑裂。
  • 异步复制 lag 10min 未告警,切城丢 10min 订单。
  • 把「多副本 K8s」当异地 DR。

代码示例

// 单元路由 — 按 user_id 选 DB 单元
func RouteUnit(userID int64, unitCount int) int {
    return int(userID % int64(unitCount))
}

func (p *DBPool) Get(ctx context.Context, userID int64) *sql.DB {
    unit := RouteUnit(userID, len(p.units))
    return p.units[unit]
}

// 健康检查 — GSLB 探活
func HealthHandler(w http.ResponseWriter, r *http.Request) {
    if err := checkLocalDB(r.Context()); err != nil {
        http.Error(w, "unhealthy", http.StatusServiceUnavailable)
        return
    }
    w.WriteHeader(http.StatusOK)
}

延伸阅读