演练做了一次,真出事还是手忙脚乱——这是"只做过一次演练"的团队的标准结局。原因不在演练本身,在目标定错了:多数演练在验证"系统能不能扛住",而真出事时决定后果的是人能不能在信息不全时做出正确判断、流程能不能在压力下跑通。系统扛不扛是压测与容量规划的事;演练唯一不可替代的价值,是把人和流程放进真实故障的时序里跑一遍。本文只谈投入决策:多大爆炸半径、多高频率、什么条件下根本不该演练。
1. 演练要回答的问题
| 问题 | 谁能回答 | 演练是否必要 |
|---|---|---|
| 单机能扛多少 QPS | 压测 | 不需要 |
| 依赖挂掉后降级是否生效 | 单点注入 | 需要 |
| 值班能否 5 分钟内定位到正确服务 | 桌面推演 + 告警链路 | 需要 |
| 主库不可用时切换要多久 | 链路故障 | 需要,风险最高 |
| 全站不可用时对外沟通谁负责 | 桌面推演 | 需要,成本最低 |
判据:能被压测回答的问题不要用演练回答。演练的成本是"真实影响用户",压测的成本是"占用测试环境",不在一个量级。用演练验证容量,是最贵的错误用法。
2. 四种强度:成本跃升来自组织,不来自技术
| 强度 | 做法 | 参与人 | 成本 | 风险 |
|---|---|---|---|---|
| 桌面推演 | 念剧本,问"下一步做什么" | 5–10 人,2 小时 | 极低 | 无 |
| 单点注入 | 杀实例、断连接 | 1–2 人 + 值班 | 低 | 低 |
| 链路故障 | 停依赖、注入延迟/错误率 | 团队级,需预案 | 中 | 中 |
| 全站演练 | 多故障叠加、跨团队 | 组织级,需公告 | 高 | 高 |
从单点到链路的成本跃升,原因不是技术复杂度,而是需要跨团队预案:单点失败回滚即可,链路故障失败需要有人判断"降级还是回滚",而这个人往往没被授权。务实路径是桌面推演 → 单点注入 → 链路故障,每一步的缺口清单关闭后才升级。
3. 先有可观测,再有演练
看不见故障的演练不是演练,是制造事故。判据只有一句:注入一个已知故障,告警是否在预期时间内到达值班? 答案是否,当前该补监控。
| 能力 | 缺失时的表现 | 演练会变成什么 |
|---|---|---|
| 指标与看板 | 注入后没人发现异常 | 白做,消耗团队信任 |
| 告警链路 | 异常发生但没通知到人 | 变成"发现告警没配" |
| 日志检索 | 定位靠猜 | 排障时间被算进演练耗时 |
| 链路追踪 | 不知道坏在哪一跳 | 结论只能写"某处有问题" |
演练的价值来自"验证人对信号的响应",信号缺失时它只剩破坏。
4. 三个前置条件
| 条件 | 要求 | 不满足的后果 |
|---|---|---|
| 回滚可行 | 注入可控、有开关、能一键恢复 | 演练故障变真实故障 |
| 影响可控 | 半径明确、有灰度、有时间窗 | 波及真实用户且无法止损 |
| 值班在位 | 有人在岗且知道这是演练 | 没人处理,或处理者不知情 |
最容易忽略的是第三条:"值班在位"不等于"有人在线",而是"知道这是一次演练、知道怎么终止"。曾出现演练注入后被不知情工程师当真实故障扩容、把影响放大的案例——这是知情范围问题,不是技术问题。
# 演练前先验证回滚脚本能跑通,再谈注入
./chaos rollback-check --experiment "$EXP_ID" --timeout 60 \
&& echo "rollback ok" || { echo "abort: rollback broken"; exit 1; }
5. 注入点与可恢复性
| 注入点 | 手段 | 可恢复性 | 风险 |
|---|---|---|---|
| 网络 | 延迟、丢包、分区(tc/iptables) | 高,撤销即恢复 | 中,易误伤 |
| 进程 | 杀实例、暂停(kill/stop) | 高 | 低(有冗余时) |
| 依赖 | 错误率、超时、异常返回 | 中,需确认降级 | 中 |
| 资源 | 占满 CPU/内存/磁盘/连接数 | 低,恢复慢且易残留 | 高 |
| 数据 | 删改记录、制造不一致 | 极低,可能不可逆 | 最高 |
判据:数据层注入只在数据可重建的环境做。生产库的数据注入恢复依赖备份,RTO 通常是小时级,那是事故预演不是演练。资源类要注意"恢复残留":进程被杀后页缓存与交换的影响会持续一段时间,指标恢复不等于系统恢复。
6. 爆炸半径:四道闸缺一不可
experiment:
target: { service: checkout, selector: "zone=b", percent: 5 } # 单可用区 + 小比例
blast_radius:
max_affected_requests_per_min: 50
abort_if_error_rate_over: 0.02 # 越线立即自动终止
abort_if_latency_p99_over_ms: 800
window: { start: "02:00", end: "03:00" } # 低峰 + 硬截止
rollback: { command: "./chaos stop --id $EXP_ID", timeout_s: 60 }
范围、自动止损、时间窗、回滚脚本。自动止损比人工判断重要,因为异常往往先被用户发现;止损条件必须在注入前写死。
7. 与故障响应的联动
演练最有价值的产出常常不是"发现架构缺陷",而是"发现告警发给了错的人"。
# 打标而不是静默:静默会让"告警是否触发"变得不可观测
curl -s -XPOST "$ALERT_API/labels" \
-d '{"match":"env=prod","set":{"chaos":"true","exp":"EXP-042"}}'
curl -s -XPOST "$ALERT_API/notify" \
-d '{"channel":"oncall","text":"02:00-03:00 演练中,告警请人肉响应"}'
| 联动项 | 要验证的 | 常见缺口 |
|---|---|---|
| 告警触发 | 阈值与聚合是否生效 | 依赖指标没上报 |
| 发给谁 | 值班表是否正确 | 值班表半年没更新 |
| 升级路径 | 一级不响应后是否升级 | 升级链断在某人 |
不要用静默掩盖演练,那会让最关键的问题变成不可观测。正确做法是打标签并在通知里明示。
8. 产出物与"通过"的误读
| 产出 | 内容 | 后续 |
|---|---|---|
| 缺口清单 | 现象、根因、影响、建议 | 进排期,指定 owner 与期限 |
| 时间线 | 注入到恢复的每个动作时刻 | 算 MTTD/MTTR |
| 决策记录 | 为何选降级而非回滚 | 补进 runbook |
“完全按计划完成”的演练,几乎必然强度不足。好的演练至少暴露一条预期外现象;什么都没暴露,先怀疑注入无效而不是庆祝健壮。
# 验证注入确实生效:60 秒内指标无可观测变化就中止
sleep 60
rate=$(curl -s "$PROM/api/v1/query" --data-urlencode \
"query=sum(rate(http_requests_total{service=\"checkout\",code=~\"5..\"}[1m]))" | jq -r '.data.result[0].value[1] // 0')
[ "$(echo "$rate > 0" | bc)" -eq 1 ] || { echo "injection ineffective"; ./chaos stop --id "$EXP_ID"; }
9. 频率跟随变更,不跟随日历
| 变更频率 | 建议频率 | 理由 |
|---|---|---|
| 每周多次发布 | 每月单点 + 每季链路 | 变更多,故障面变化快 |
| 每月数次 | 每季一次 | 与变更窗口对齐 |
| 季度级发布 | 半年一次,桌面推演为主 | 形态稳定,回报低 |
| 架构大改前后 | 各一次,必做 | 变更本身是最大风险源 |
新增强依赖、换数据库、改部署拓扑,都比"又过了一年"更值得触发一次演练。
10. 生产与预发的分工
| 维度 | 预发演练 | 生产演练 |
|---|---|---|
| 验证对象 | 降级逻辑、配置正确性 | 告警链路、人的响应、真实流量 |
| 风险 | 低 | 高 |
| 常见误用 | 把预发通过当成生产可用 | 用生产演练验证代码逻辑 |
预发验证"代码是否按设计动作",生产验证"人和流程是否动作"。前者失败是缺陷,后者失败是组织问题。用生产演练发现代码 bug,是把最贵的环境用在最便宜的问题上。
11. 文化障碍与制度解法
怕背锅、怕影响业务、怕暴露问题、怕麻烦、走过场——解法都是制度而非号召。核心是把"发现缺口"从负面事件变成正面产出:缺口由平台团队统一收集,考核修复率而非缺口数量;演练窗口进季度计划并提前公告。缺口清单一旦导致问责,下一季度演练就只剩表演。
12. 度量:只看两个数
-- MTTD/MTTR 按演练与真实故障分开统计,避免互相污染
SELECT kind,
percentile_cont(0.5) WITHIN GROUP (ORDER BY detected_at - injected_at) AS mttd,
percentile_cont(0.5) WITHIN GROUP (ORDER BY recovered_at - detected_at) AS mttr,
count(*) FILTER (WHERE gap_closed)::float / count(*) AS gap_close_rate
FROM incidents
WHERE created_at > now() - interval '180 days'
GROUP BY kind;
| 指标 | 健康信号 |
|---|---|
| MTTD | 逐季下降,且演练值与真实值差距缩小 |
| MTTR | 不追求极小,追求可预测 |
| 缺口关闭率 | 上升;长期低于一半说明治理失效 |
不要用"演练次数"当指标,它只会催生走过场。MTTD 的演练值与真实值接近,才是演练真正有效的证据——说明告警链路在两种情况下表现一致。
13. 排错:演练做完了但没价值
| 现象 | 根因 | 处理 |
|---|---|---|
| 注入后指标毫无变化 | 注入未生效或打到无流量实例 | 先验证生效再走流程 |
| 演练中无人响应 | 告警未配或值班表过期 | 补规则,更新值班表 |
| 缺口清单没人修 | 无 owner、无期限 | 进排期,设回归验证 |
| 复盘变成追责 | 无时间线,只能靠记忆 | 强制记录时间线 |
| 演练影响真实用户 | 无自动止损、时间窗错 | 加止损与低峰窗口 |
| 每次演练都成功 | 剧本过细、强度不足 | 引入未知变量与叠加故障 |
14. 边界:这些阶段先别演练
| 场景 | 该做什么 |
|---|---|
| 系统刚上线 | 先补监控与回滚,桌面推演即可 |
| 无监控/无告警 | 补可观测,演练无意义 |
| 团队不足三人 | 桌面推演 + runbook,不做注入 |
| 单点故障不影响业务 | 记录为已知风险,不值得投入 |
| 无冗余的单实例 | 注入等于制造故障,先做冗余 |
| 强合规/不可中断业务 | 只在预发演练,生产做只读验证 |
最终口径:演练价值 = 缺口修复价值 − 演练影响 − 协调成本。任一项估不出来,说明还没到该演练的阶段。
15. 自测
- 为什么"系统能不能扛住"不该由演练回答?
- 从单点注入升级到链路故障,成本跃升的真实原因是什么?
- 注入一个已知故障后告警没响,下一步该做什么?
- 为什么不能用静默(silence)掩盖演练?
- 一次"完全按计划完成"的演练,应该怀疑什么?
- MTTD 的演练值与真实值接近,说明什么?