服务器跨机房容灾怎么设计?数据复制、故障切换与恢复验证

同城双机房之间相距 30 公里,光纤专线延迟 1.2ms,看起来是理想的容灾距离。某年夏天,市政施工挖断了主干光缆,两个机房同时失联——因为那两条"物理隔离"的专线,实际上走了同一条管道沟。这不是极端的巧合,而是跨机房容灾设计中最常犯的错误:把"地理上的两个机房"误当成"两个独立的故障域"。

跨机房容灾(Geo-Redundancy / DR Site)要解决的是机房级甚至地域级故障:断电、火灾、网络中断、自然灾害、运营商故障。它的技术难度不在于"把数据复制过去",而在于在广域网的不确定延迟下,如何界定一致性、如何决策切换、以及如何证明它真的能用。

一、先确定容灾等级与指标

在动手之前必须回答三个问题,它们决定了整个方案的形态与成本:

  • RPO(恢复点目标):机房整体失效时,能接受丢多少数据?
  • RTO(恢复时间目标):多久之内必须恢复服务?
  • 故障域边界:要抵御的是单机故障、单机房故障,还是单地域故障?
容灾形态抵御范围典型 RPO典型 RTO相对成本
同机房双机单机、单电源、单机柜0 ~ 秒秒 ~ 分钟低
同城双机房单机房、单运营商0 ~ 分钟分钟 ~ 30 分钟中
两地三中心单地域、城市级灾害秒 ~ 分钟30 分钟 ~ 数小时高
异地冷备(备份站点)地域级灾害小时 ~ 24 小时数小时 ~ 数天低 ~ 中

成本说明:上表为相对量级比较,具体支出受机房距离、专线带宽、机型与租期影响极大,需按实际配置测算,本文不给出具体金额。

1.1 故障域的核查要点

选第二个机房时,必须逐项确认以下"独立性",任何一项不独立都会让容灾形同虚设:

  1. 电力:是否接入不同变电站?是否有独立的柴油发电与供油协议?
  2. 网络:专线是否走不同物理路由(不同管道沟、不同方向出城)?上游运营商是否不同?
  3. 制冷与消防:是否独立的空调系统与消防分区?
  4. 运维与管理:是否会被同一次误操作同时影响(例如同一个自动化脚本下发到两个机房)?
  5. 供应商:是否同一家 IDC / 云厂商的同一地域内的不同可用区?可用区之间通常共享部分基础设施。

二、数据复制:三种模式的取舍

跨机房数据复制的核心矛盾是:延迟放大了"一致性"与"可用性"之间的冲突。同城 1-3ms 尚可接受同步复制,跨城 30ms 以上就必须做取舍。

2.1 同步复制(强一致)

主库提交事务前必须等待备机房确认。优点 RPO=0,缺点是写延迟直接叠加一个 RTT(往返时延),且备机房网络抖动会直接拖垮主库。适用:同城、专线质量极高、写量不大。

# MySQL 半同步复制(RPO≈0 的常用折中)
# 主库安装并启用
mysql -e "INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';"
mysql -e "SET GLOBAL rpl_semi_sync_master_enabled = 1;"
mysql -e "SET GLOBAL rpl_semi_sync_master_timeout = 1000;"          # 毫秒
mysql -e "SET GLOBAL rpl_semi_sync_master_wait_for_slave_count = 1;"

# 备库启用
mysql -e "INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';"
mysql -e "SET GLOBAL rpl_semi_sync_slave_enabled = 1;"
mysql -e "STOP SLAVE IO_THREAD; START SLAVE IO_THREAD;"

# 观察半同步状态与退化次数(Rpl_semi_sync_master_no_tx 增长说明在退化)
mysql -e "SHOW STATUS LIKE 'Rpl_semi_sync_master_%';"

关键判断:Rpl_semi_sync_master_timeout 超时后半同步会自动退化为异步,此时 RPO 不再为 0。因此必须监控 Rpl_semi_sync_master_no_tx(退化为异步的事务数)与 Rpl_semi_sync_master_yes_tx 的比值,一旦 no_tx 持续上升,说明网络质量已不满足容灾目标。

2.2 异步复制(最终一致)

主库不等待备机房确认,RPO 等于复制延迟。优点是主库性能不受影响、备机房故障不影响生产;缺点是机房级故障时必然丢数据。适用:跨城、写量大、RPO 分钟级可接受。

# 备机房观测复制延迟(RPO 实测依据)
mysql -e "SHOW SLAVE STATUS\G" | grep -E "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running|Last_IO_Error"

# 连续采样 10 分钟取延迟峰值
for i in $(seq 1 60); do
  mysql -N -e "SHOW SLAVE STATUS\G" | awk '/Seconds_Behind_Master/{print $2}'
  sleep 10
done | sort -n | tail -3

# 网络质量基线测试(判断同步复制是否可行)
ping -c 100 -i 0.2 10.1.0.5 | tail -3
iperf3 -c 10.1.0.5 -t 30 -P 4

判断标准:若 RTT P99 稳定低于 3ms 且丢包率为 0,可考虑半同步;若 RTT 超过 10ms 或有抖动,必须走异步并接受分钟级 RPO。

2.3 文件与对象层复制

数据库之外的静态文件、上传内容,通常用存储层复制而非数据库复制:

# 方案 A:rsync + 硬链接多版本(适合中小规模,成本极低)
rsync -az --delete --bwlimit=20480 \
      --link-dest=/backup/www/$(date -d yesterday +%F)-0200/ \
      -e "ssh -i /root/.ssh/id_ed25519_dr -o BatchMode=yes" \
      /var/www/html/ druser@10.1.0.9:/backup/www/$(date +%F)-0200/

# 方案 B:对象存储跨地域复制(由云厂商托管,配置保留策略)
# 开启版本控制 + 对象锁定(WORM),抵御误删除与勒索加密

# 方案 C:分布式文件系统/块设备复制(DRBD 等),适合强一致要求
# 注意:DRBD 的同步协议 C 等价于同步复制,跨城时写延迟会显著放大

# 传输后校验
sha256sum -c /backup/www/$(date +%F).sha256

三、故障切换:最难的不是技术,是决策

跨机房切换的最大风险是误判:主机房只是网络抖动,却误以为整体失效而切换,导致脑裂与数据双写。因此切换机制必须包含"确认"与" fencing(隔离旧主)"两个环节。

3.1 切换决策的检查清单

  1. 多源确认:至少两个独立的探测点(不同运营商、不同城市)都判定主机房不可达,且持续超过阈值(建议 3-5 分钟)。
  2. 区分网络分区与机房失效:如果备机房能连上主机房的带外管理(IPMI/iDRAC)或云 API,说明可能是网络分区而非机房失效。
  3. Fencing 旧主:切换前必须通过带外管理或云 API 将主机房的数据库服务强制停止/隔离,防止旧主恢复后继续写入造成双写。
  4. 记录切换时刻的位点:用于事后数据比对与回补。
  5. 明确决策人:自动切换需预先定义触发条件与人工兜底流程;禁止"谁先发现谁决定"。

3.2 切换执行流程(以 DNS + 数据库为例)

# 1. 停止备机房复制,提升为新主
mysql -e "STOP SLAVE;"
mysql -e "RESET SLAVE ALL;"
mysql -e "SHOW MASTER STATUS;"    # 记录位点,写入事故记录

# 2. 确认数据完整(与预期 RPO 对比)
mysql -e "SELECT COUNT(*), MAX(updated_at) FROM appdb.orders;"

# 3. 应用层指向新主(配置中心下发或改 hosts/连接池配置)
sed -i 's/10.0.0.5/10.1.0.5/' /etc/app/database.yml
systemctl restart app

# 4. 切换流量:DNS 或全局负载均衡
#    DNS 方式务必预先把 TTL 调到 60 秒以内,否则生效极慢
dig +short www.example.com
#    云解析 API 修改记录(示意)
#    aliyun alidns UpdateDomainRecord --RecordId xxx --RR www --Value 10.1.0.100

# 5. Fencing 主机房(关键!防止恢复后双写)
ipmitool -I lanplus -H 10.0.0.50 -U admin -P '***' chassis power off
# 或通过云 API 停止主机房实例 / 移除其安全组出网权限

# 6. 业务冒烟验证
curl -sS -o /dev/null -w "%{http_code} %{time_total}\n" https://www.example.com/health
mysql -e "SELECT COUNT(*) FROM appdb.orders WHERE created_at >= NOW() - INTERVAL 5 MINUTE;"

3.3 回切(Failback)比切换更危险

主机房恢复后,不能直接把流量切回去——因为主机房的数据是旧的。正确顺序是:先把主机房作为从库接入新主,追平数据,确认一致后再切回。整个过程需要在低峰期进行,且全程保留回退方案。

# 回切:主机房作为从库追平新主
mysql -e "CHANGE MASTER TO MASTER_HOST='10.1.0.5', MASTER_USER='repl',
          MASTER_PASSWORD='***', MASTER_AUTO_POSITION=1;"
mysql -e "START SLAVE;"
mysql -e "SHOW SLAVE STATUS\G" | grep -E "Seconds_Behind_Master|Slave_IO_Running"

# 延迟为 0 且稳定后,在低峰期反向切换
pt-table-checksum --host=10.1.0.5 --databases=appdb    # 比对一致性

四、恢复验证:证明方案真的能用

跨机房容灾最大的敌人是"长期不用的链路悄悄失效"。验证必须制度化:

验证层级频率内容停机影响
链路健康检查每日(自动)复制延迟、专线质量、备机存活无
数据可恢复性验证每周(自动)备机房还原备份到隔离实例并抽样校验无
只读切换演练每月把读流量切到备机房,验证备机可服务极低
完整切换演练每半年低峰期真实切换全量流量,验证 RTO有,需公告
#!/bin/bash
# /usr/local/bin/dr_verify.sh  —— 每日容灾链路自检
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LOG=/var/log/dr_verify.log
ALERT=ops@example.com

{
  echo "=== $(date '+%F %T') DR verify ==="
  # 1. 复制状态
  mysql -e "SHOW SLAVE STATUS\G" | grep -E "Slave_IO_Running|Slave_SQL_Running|Seconds_Behind_Master|Last_Error"
  # 2. 专线质量
  ping -c 20 -i 0.2 10.1.0.5 | tail -2
  # 3. 备机房服务可达性
  curl -sS -o /dev/null -w "dr-site:%{http_code} %{time_total}s\n" https://dr.example.com/health
  # 4. 最新备份存在且校验通过
  LATEST=$(ls -t /backup/mysql/all-*.sql.gz | head -1)
  sha256sum -c "$LATEST.sha256"
} >> "$LOG" 2>&1 || echo "DR VERIFY FAILED, see $LOG" | mail -s "DR alert" "$ALERT"

4.1 演练必须产出的四个数字

  • 实际 RPO:备机房数据最新时间戳与切换时刻的差值。
  • 实际 RTO:从宣布切换到冒烟验证通过的墙钟时间。
  • 数据完整率:抽样校验通过项占比,要求 100%。
  • 步骤成功率:Runbook 中无需修正即可执行的步骤占比,低于 90% 需重写。

小结

跨机房容灾的设计次序是:先界定故障域与 RPO/RTO,再选择复制模式(同城可用半同步,跨城必须异步并接受分钟级 RPO),然后设计带 Fencing 的切换流程,最后用分级演练制度保证链路长期有效。真正的难点从来不是"把数据复制过去",而是在网络分区面前做出正确决策、并确保回切时不会因为数据倒流造成二次事故。

避坑清单

  • 两个机房必须核查电力、网络路由、运营商、消防是否真正独立,共沟光缆是高频陷阱。
  • 半同步复制超时会自动退化为异步,必须监控 Rpl_semi_sync_master_no_tx。
  • 切换前必须 Fencing 旧主,否则旧主恢复后会造成双写。
  • DNS 切换前务必提前把 TTL 调到 60 秒以内,否则生效极慢。
  • 回切比切换更危险:必须先把主机房作为从库追平,再低峰期反向切换。
  • 容灾链路必须每日自检,长期不用会静默失效。
  • 演练要产出实际 RPO/RTO 并与目标对比,不达标就改设计而不是改目标。