很多团队并不缺备份、复制、监控或云服务,真正缺的是在压力下把它们用对的能力。灾难恢复,简称 DR,最容易被误解成一套工具清单:有备份、有只读副本、有对象存储、有告警,就算准备好了。但真正出事时,决定 RTO 的往往不是你买了什么,而是谁来指挥、命令是否还能跑、权限是否还有效、团队有没有练过。
这篇文章讨论的是 DR 的后半段:如何把恢复能力从文档变成流程,从流程变成演练过的能力。
Runbook 的目标不是解释系统,而是消除犹豫
很多 runbook 写得像系统百科:架构图、背景说明、数据库版本历史、各种边界情况塞在一个巨大 wiki 页面里。平时看起来完整,凌晨三点就会变成负担。
一个能救命的 runbook 应该更像操作手册:
- 当前适用场景是什么;
- 执行前要检查什么;
- 运行哪条命令;
- 成功是什么样;
- 失败是什么样;
- 失败后停止、回滚还是升级给谁。
“检查日志是否有错误”不是好步骤。好的步骤应该写清楚日志在哪里、查什么关键字、看到什么结果才继续。
比如 PostgreSQL 复制故障的 runbook,不应该只写“必要时提升只读副本”。它至少要回答:提升哪一个副本?怎么确认它是最新的候选?提升后怎么验证?应用流量何时切回?如果提升失败,谁有权决定接受数据损失?
常见反模式:文档看着齐,事故时跑不动
runbook 失败通常不是因为没人写,而是因为写法默认了一个不存在的环境:人清醒、信息完整、专家在线、权限齐全。
几个高风险信号值得专门检查:
- 命令过期:数据库版本、Kubernetes 资源名、云资源组、主机名都在变,命令不复测就会腐烂。
- 没有回滚条件:只告诉人“执行 promote”,却没写 promotion 卡住、失败或产生分叉时怎么办。
- 所有权模糊:大家都能改,最后等于没人负责。可靠性工程或平台团队通常需要显式拥有 runbook 的维护节奏。
- 权限假设错误:演练时管理员在场,事故时值班工程师没有 VPN、堡垒机、证书或云账号权限。
- 沟通角色缺失:敲键盘的人同时负责对外解释,恢复节奏很容易被打断。
一个成熟的 DR 流程至少要拆出两个角色:incident commander 负责协调恢复,communications owner 负责状态页、邮件、客户群、内部通报等沟通。技术恢复和沟通都重要,但它们不应该压在同一个人身上。
可以这样实践:把 runbook 写成可执行检查单
下面是一个可以改造的 PostgreSQL 故障演练 runbook 片段。它不是通用真理,里面的主机名、服务名、端口和命令都需要替换成你的环境;重点是结构:每一步都有检查、命令、判定和升级路径。
#!/usr/bin/env bash
set -euo pipefail
# Assumptions to change before running:
# - STANDBY_HOST points to the replica you selected before the incident.
# - PGDATA matches the PostgreSQL data directory on that host.
# - APP_HEALTHCHECK is an application endpoint reachable from this machine.
# - You have SSH and sudo permissions for the standby.
STANDBY_HOST="postgres-standby-1.example.internal"
PGDATA="/var/lib/postgresql/16/main"
APP_HEALTHCHECK="https://app.example.com/healthz"
MAX_REPLAY_LAG_BYTES="16777216" # 16 MiB example threshold
log() {
printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*"
}
log "Checking standby replay lag"
LAG_BYTES=$(ssh "$STANDBY_HOST" "sudo -u postgres psql -tAc \"SELECT COALESCE(pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn()), 0)::bigint;\"" | tr -d ' ')
log "Replay lag bytes: ${LAG_BYTES}"
if [ "$LAG_BYTES" -gt "$MAX_REPLAY_LAG_BYTES" ]; then
log "STOP: replay lag is above threshold. Escalate to incident commander for risk authorization."
exit 2
fi
log "Promoting standby"
ssh "$STANDBY_HOST" "sudo -u postgres /usr/lib/postgresql/16/bin/pg_ctl promote -D '$PGDATA'"
log "Verifying the promoted node is writable"
ssh "$STANDBY_HOST" "sudo -u postgres psql -tAc \"CREATE TABLE IF NOT EXISTS dr_validation(ts timestamptz DEFAULT now()); INSERT INTO dr_validation DEFAULT VALUES; SELECT count(*) FROM dr_validation;\""
log "At this point, update DNS/load balancer according to your environment."
log "Waiting 20 seconds before app health check"
sleep 20
HTTP_CODE=$(curl -sS -o /dev/null -w '%{http_code}' "$APP_HEALTHCHECK")
if [ "$HTTP_CODE" != "200" ]; then
log "FAIL: application health check returned HTTP ${HTTP_CODE}. Keep traffic disabled and escalate."
exit 3
fi
log "SUCCESS: promoted standby is writable and application health check passed."
这个脚本故意没有自动切 DNS 或负载均衡,因为不同组织的入口层差异很大。你可以把那一步接到 Terraform、云 CLI、Kubernetes Service 或数据库代理上,但 runbook 里必须写清楚:谁批准、改哪里、怎么确认流量真的切过去。
如果你的服务跑在 Kubernetes 里,可以把演练场景写成一个明确的 Game Day 配置,而不是临场口头描述。例如,用一个独立 namespace 还原数据库和应用,演练“副本提升后应用恢复”:
apiVersion: v1
kind: Namespace
metadata:
name: dr-gameday
---
apiVersion: v1
kind: ConfigMap
metadata:
name: dr-drill-plan
namespace: dr-gameday
data:
objective: "Restore service within 30 minutes after simulated primary database loss"
failure_mode: "Primary PostgreSQL instance unavailable"
success_criteria: |
1. Standby promoted successfully
2. Application health endpoint returns HTTP 200
3. Incident update sent every 15 minutes
4. Actual RTO and decision time recorded
rollback: |
Stop application writes, keep promoted standby isolated, and escalate to incident commander.
这个 YAML 本身不会完成恢复,它的价值是让演练目标、失败模式、成功标准和回滚路径变成可审查的工程资产。
Game Day:别等真事故才第一次恢复
DR 计划不是因为存在而可信,而是因为被反复执行过才可信。Game Day 的作用就是在可控环境里制造故障,测试系统、流程、协作和隐藏假设。
演练可以分层推进:
- 从简单场景开始:还原备份、提升 standby、切流量、验证应用。
- 加入时间约束:比如 10:00 破坏环境,30 分钟内恢复,测出真实 RTO。
- 再加入现实压力:关键工程师不在、证书过期、权限缺失、监控信号不完整。
几个实践规则很朴素,但能避免演练变成新事故:
- 从 staging 或 standby 开始,不要一上来碰生产 primary。
- 提前宣布演练日期。要测试的是流程,不是突袭团队。
- 初期一次只测一种故障模式。
- 开始前定义成功标准,比如“30 分钟内恢复写入并通过健康检查”。
- 两天内做复盘,趁记忆还热,把 runbook 改掉。
真正有价值的指标也不只是“恢复成功了吗”。你应该拆开看:检测用了多久,决策用了多久,执行用了多久,验证用了多久,沟通是否按 15 分钟节奏发生,哪些步骤让人停下来问“这是什么意思”。每一个停顿,都是 runbook 的 bug。
文化会直接影响 RTO
事故中最危险的不是某个人犯错,而是大家不敢说自己不确定。责备文化会制造沉默:没人愿意承认命令没看懂、权限没有、假设可能错了。结果就是升级变晚、错误被掩盖、风险决策被单人仓促做出。
好的复盘应该盯系统,不盯人。问题应该被写成可修复项:命令失效、权限缺失、角色不清、沟通节奏没有负责人、RTO 目标和真实能力不匹配。
落地清单:从下个月的一次演练开始
降低 RPO 往往可以花钱买:复制、更好的硬件、更多云资源。降低 RTO 更像训练:runbook、演练、复盘、角色分工和稳定的沟通节奏。
可以从这份短清单开始:
- 选一个下个月的固定日期做第一次 DR 演练。
- 只选一个故障:杀掉安全环境里的一个 replica 或模拟 primary 不可用。
- 指定 incident commander 和 communications owner。
- 写清楚成功标准和停止条件。
- 记录每个阶段耗时,而不是只记录总耗时。
- 演练后 48 小时内更新 runbook。
第一次演练通常最有信息量。它会暴露过期命令、失效权限、没人知道的依赖,以及那些平时看起来“应该没问题”的假设。发现这些问题不是失败,等真实灾难发生时才发现,才是失败。