第 18 章 · 跟练三:跨机房切换与 Postmortem
类型:灾备演练工作坊(约 3~4 小时)
场景:贤紫社区电商 同城双活 —— 机房 AZ-A 网络中断,执行流量切换、数据库 Failover 与业务恢复。
前置:ch12 多活容灾、ch13 SLO/RTO、ch09 容量(B 机房扩容)、ch14 ADR/Runbook 模板。
环境要求(二选一):
| 方案 | 说明 |
|---|---|
| 推荐 | 两套小紫云计算集群 cluster-a / cluster-b,外置 MySQL 主从 |
| 最小 | 单集群两 Namespace + 模拟 GSLB 权重(教学降级) |
产出物(内嵌本章):
- DR 演练执行记录表(§18.4)
- 完整 Postmortem(§18.7 模板 + §18.8 示范)
- ADR-018 Failover 自动化(§18.9)
- Failover ConfigMap 与 Runbook(§18.5~18.6)
18.0 演练目标与成功标准
| 指标 | 目标值 | 测量方式 |
|---|---|---|
| RTO | < 15 min(业务恢复下单) | 演练记录表时间戳 |
| RPO | < 30 s(异步复制) | 主从延迟 + 停写窗口 |
| 下单成功率 | 恢复后 ≥99.9% | Prometheus SLI |
| 数据一致性 | 无重复扣款、无丢单 | 对账脚本 |
| 文档 | Postmortem + ADR 归档 | ch14 目录 |
18.1 拓扑与常态配置(给定)
| 机房 | K8s 集群 | 常态流量权重 | order-api 副本 | MySQL 角色 |
|---|---|---|---|---|
| AZ-A | cluster-a | 60% | 6 × 2C4G | 主库 (write) |
| AZ-B | cluster-b | 40% | 4 × 2C4G | 从库 (async replica) |
入口:api.shop.example.com → GSLB 加权轮询 + 健康检查(HTTP /health,间隔 10s,失败 3 次摘流)
数据复制:MySQL 异步复制,常态延迟 0.3~2s;极端 <10s
交叉引用
- ch12 §12.2 同城双活架构
- ch16 大促 6 副本 —— 本演练 B 机房仅 4 副本,切流后 容量不足 为已知风险
- ch13:RTO 超标消耗可用性预算
- ch17:若叠加缓存击穿,切流后更难恢复
flowchart TB
DNS[GSLB api.shop.example.com]
DNS -->|60%| A[cluster-a AZ-A]
DNS -->|40%| B[cluster-b AZ-B]
subgraph AZ-A
A --> OA[order-api ×6]
OA --> MYA[(MySQL 主)]
end
subgraph AZ-B
B --> OB[order-api ×4]
OB --> MYB[(MySQL 从)]
end
MYA -.->|async replication| MYB
18.2 故障注入剧本(仅授权测试环境)
严禁在未授权生产环境执行。演练前完成变更窗口审批与回滚预案。
剧本 A:AZ-A 入口网络中断(推荐)
# 模拟:在 AZ-A 边界防火墙 DROP GSLB 探针与业务流量
# 或由网络组执行「A 机房上联中断」沙盘
剧本 B:仅 scale order-api(最小环境)
kubectl config use-context cluster-a
kubectl scale deployment/order-api -n prod-app --replicas=0
剧本 C:MySQL 主库不可达(进阶)
# 阻断 A 机房应用到 MySQL 主的 3306(保留复制专线观察延迟)
本次标准剧本:A 失联 45 分钟(预计恢复不确定)→ 决策 B 从库 promote + DNS 100% B
18.3 决策树(演练中必须书面勾选)
AZ-A 不可达
│
├─ 预计恢复 < 30min 且 从库延迟 < 10s 且 主库内网仍可达写
│ → DNS 100% B;写仍指向 A 主(若网络路径 OK)
│ → 风险:A 完全失联时不可选
│
├─ 预计恢复 > 30min 或 主从断连或 从库延迟 > 30s
│ → 停写 2min → promote B 从库 → 应用 DSN 切 B → DNS 100% B
│ → 公告:短暂维护
│
└─ A、B 均不可用
→ ch12 §12.5 异地冷备;RPO 升至最近备份点
本次演练路径:第二分支(promote + 停写 + DSN 切换)
| 决策点 | 你的选择 | 理由 |
|---|---|---|
| 是否只切流量不切写? | 否 | A 完全失联,写落 A 导致脑裂 |
| 是否 promote? | 是 | 恢复时间未知 >45min |
| 停写窗口? | 2 min | 等从库追平 + 避免双写分叉 |
18.4 操作 Runbook 与执行记录表
# DR Runbook · AZ-A 失联 → AZ-B 接管
## 前置检查
- [ ] 确认 P1 事故,War room 建立(企业微信/钉钉群)
- [ ] 确认非误报:A 健康检查失败、多地域用户反馈
- [ ] 通知:客服公告模板、管理层报备
## 执行步骤(填写「实际时间」列)
| 步骤 | 操作 | 负责人 | 计划 T+ | 实际时间 | 结果 | 证据 |
|------|------|--------|---------|----------|------|------|
| 1 | 宣布 P1,冻结非紧急发布(ch13) | 事故指挥 | 0 | | | |
| 2 | GSLB A 权重 → 0 | 网络/SRE | 3min | | | 控制台截图 |
| 3 | 观察 B 错误率、P99、CPU | SRE | 5min | | | Grafana |
| 4 | `SHOW REPLICA STATUS\G` 查延迟 | DBA | 6min | | | 延迟 ___s |
| 5 | **业务降级**:关秒杀/推荐(ch16 ADR-016-03) | 开发 | 8min | | | |
| 6 | **停写**:order-api 只读模式 ConfigMap | 开发 | 10min | | | |
| 7 | DBA promote B 为新主 | DBA | 12min | | | binlog 位点 |
| 8 | 应用 DSN 切 B(§18.5 ConfigMap) | SRE | 14min | | | rollout 完成 |
| 9 | 核心冒烟:下单/支付回调 | QA | 15min | | | 用例 #1-5 |
| 10 | 对外公告恢复 | 客服 | 20min | | | |
| 11 | A 恢复后反向同步评估 | DBA | — | | | 另案 |
## 冒烟用例
| # | 用例 | 期望 |
|---|------|------|
| 1 | POST /api/v1/orders | 201 |
| 2 | GET /api/v1/orders/{id} | 200 |
| 3 | 支付回调模拟 | success |
| 4 | 健康检查 | 200 |
| 5 | 跨 AZ 延迟 P99 | <500ms |
18.5 Failover ConfigMap 一键切换(ADR-018 核心)
设计目标:消除 DSN 变更 人工审批 5min 延误(上次演练教训)。
# db-endpoint-config.yaml — 常态
apiVersion: v1
kind: ConfigMap
metadata:
name: db-endpoint
namespace: prod-app
data:
DB_HOST: "mysql-master.az-a.internal"
DB_READ_HOST: "mysql-replica.az-b.internal"
DB_FAILOVER_MODE: "normal" # normal | readonly | failover-b
---
# 停写阶段 apply
# kubectl patch configmap db-endpoint --type merge -p '{"data":{"DB_FAILOVER_MODE":"readonly"}}'
# 应用滚动重启:kubectl rollout restart deploy/order-api -n prod-app
---
# Failover 后 apply(promote 完成后)
# data:
# DB_HOST: "mysql-master.az-b.internal"
# DB_READ_HOST: "mysql-master.az-b.internal"
# DB_FAILOVER_MODE: "failover-b"
Django settings 读取
# settings/prod.py 片段
FAILOVER = os.environ.get("DB_FAILOVER_MODE", "normal")
if FAILOVER == "readonly":
MIDDLEWARE.insert(0, "apps.core.middleware.ReadOnlyModeMiddleware")
DATABASES = {
"default": {
"HOST": os.environ["DB_HOST"],
# ...
}
}
ReadOnlyModeMiddleware:对 POST/PUT/PATCH/DELETE 返回 503,Retry-After: 120。
18.6 小紫云计算操作速查
| 步骤 | 小紫控制台 / kubectl |
|---|---|
| 切集群上下文 | kubectl config use-context cluster-b |
| 更新 ConfigMap | 应用部署 → 配置 → db-endpoint 编辑 |
| 滚动重启 | 应用部署 → order-api → 重启 |
| 扩容 B | 副本 4→6(ch09 公式:承接 100% 流量) |
| 观察 | 监控告警 → order-api RED + MySQL |
B 机房扩容计算(引用 ch09)
原 B 承担 40% 流量 ×4 副本 → 每副本 10% 全站
切 100% 后需 10× 单副本承载 → 至少 10 副本理论
教学决议:4→6 + HPA max 10,接受短暂 CPU 88%(见 §18.8)
18.7 演练实测数据(教学样本,填入你的实测)
| 指标 | 切流前 | 切流中峰值 (T+5~8) | 恢复后 (T+20) |
|---|---|---|---|
| 下单成功率 | 99.97% | 94.2% | 99.92% |
| order-api P99 | 180ms | 890ms | 220ms |
| B 集群 CPU | 45% | 88% | 62% |
| MySQL 主从延迟 | 0.5s | 断连 | N/A(已 promote) |
| 5xx 率 | 0.03% | 5.8% | 0.08% |
未达标项
| 指标 | 目标 | 实际 | 主因 |
|---|---|---|---|
| RTO | 15min | 20min | DSN ConfigMap 变更需人工审批 5min |
| RPO | 30s | ~0(停写 2min 内) | promote 前停写有效 |