混沌工程入门:主动故障注入找出"纸糊的高可用"

“我们的系统高可用 99.9%”——这句话在依赖挂掉之前都是信仰。混沌工程的本质:主动把依赖杀了,看系统表现和宣称的是不是一回事。这篇讲测试团队怎么落地混沌工程——不是造 chaos monkey 那种平台,而是把故障注入变成可重复的测试场景。

一、混沌测试 vs 传统故障测试:区别在"假设驱动"

1
2
3
4
传统故障测试:  我们知道 DB 会挂 → 测"DB 挂了系统怎么办"
混沌测试: 我们假设"系统能在任何单点依赖失效时保持核心可用"
→ 对每个依赖验证这个假设
→ 假设被证伪的地方,就是真实的风险敞口

混沌实验三要素(照搬业界标准,落地时一个都不能少):

  1. 稳态定义:实验前明确"正常是什么样"(核心接口 P99 < 200ms、错误率 < 0.1%、下单成功率 > 99.9%)——没有稳态定义,"系统挂没挂"就是吵架;
  2. 爆炸半径控制:注入范围最小化(一个 pod/一个节点/一个依赖),从 staging 单实例开始,逐步放大
  3. 停止条件:提前写死"什么指标越界就立刻停止注入并恢复"——混沌实验本身也是风险,必须有一键刹车。

二、故障场景库:按"失效模式"分类,不按"服务"分类

场景按失效模式组织(跨服务复用),每个场景 = 注入参数 + 稳态观测 + 预期行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# chaos/scenarios/dependency_down.yaml
name: dependency_down
desc: 单依赖完全不可用(进程级杀死)
inject:
target: "{{dep}}" # 参数化:user-service / order-service / redis ...
mode: kill # kill / partition / delay
scope: single_instance # 爆炸半径:单实例
duration: 300s
expect:
core_flow: # 核心链路必须可用(下单/支付/登录)
success_rate: ">= 0.99"
p99_latency: "<= 800ms" # 允许变慢,不允许不可用
degraded_flow: # 非核心允许降级,但必须"优雅"
- feature: 个性化推荐
behavior: "fallback to 默认列表" # 明确预期:降级到 X,不是报错
- feature: 积分展示
behavior: "hide or show default"
hard_stop: # 停止条件
- core_success_rate < 0.95
- error_rate > 1%
- 任何 P0 告警触发

场景库的完整清单(起步 12 个就够)

类别 场景
依赖失效 单依赖 kill / 依赖超时(延迟 5s)/ 依赖返回 5xx
网络 分区(节点间丢包 100%)/ 延迟 300ms / 丢包 20%
基础设施 单 pod kill / 节点 kill / 磁盘满(写满到 95%)
数据 主从切换(DB failover)/ 缓存全失效(清空 Redis)
时间 时钟偏移 5 分钟 / 证书过期
客户端 弱网(复用 mitmproxy 场景)/ 断网重连

三、注入工具选型:能进程级就别网络级

1
2
3
4
5
6
7
8
注入粒度由细到粗(能细就别粗):
1. 进程级:kill -9 / 停容器(ChaosBlade、k8s delete pod)
✅ 最贴近真实故障,工具简单
2. 接口级:sidecar 注入延迟/错误(ChaosBlade network、toxiproxy)
✅ 造"依赖变慢"这种 kill 造不出的状态
3. 网络级:tc netem / 交换机策略
⚠️ 真实但难清理,内网要审批
4. 数据级:DB failover(主从切换是真实故障,但演练窗口要协调)

我们的实践:ChaosBlade 做进程级和网络级(K8s 环境原生支持),toxiproxy 做接口级(依赖间插代理,按接口精确注入),DB failover 走 DBA 的演练流程。工具不是重点,场景库 + 停止条件 + 稳态观测才是。

四、和测试体系的关系:混沌是"压测/长稳/回归"的补充维度

测试维度 回答的问题 触发条件
回归 功能对不对 代码改动
压测 容量够不够 流量增长
长稳 时间久不久得住 泄漏类 bug
混沌 坏的时候坏得体不得体 架构变更/依赖变更后

混沌实验的触发时机(不是天天跑):

  1. 架构变更后必跑:加了新依赖、换了中间件、调整了超时配置——"新依赖"就是新的失效模式;
  2. 重大版本前:和发版回归并行;
  3. 季度全量:12 个场景全过一遍,出"韧性报告"(哪些依赖失效是"优雅降级",哪些是"雪崩")。

五、一次真实实验的复盘格式

1
2
3
4
5
6
7
8
9
10
11
12
实验:dependency_down / user-service(单实例 kill,300s)
稳态:下单 P99 180ms,成功率 99.95%
结果:
✅ 下单成功率 99.7%(> 95% 停止线)
✅ 登录走本地缓存,正常
❌ 订单详情页"用户昵称"位置 5s 后白屏——未做降级(预期:显示"用户")
❌ 错误率冲到 2.3%——重试风暴:客户端对 user-service 的重试无退避
发现:
1. 订单详情缺 user-service 降级方案 → 需求(P1)
2. 重试无指数退避 → 缺陷(P1,重试风暴会放大故障)
3. 稳态定义里"白屏"没定义观测手段 → 补 UI 探针
结论:user-service 不是"高可用",是"假设 user-service 不挂的高可用"

复盘的产出不是"系统挂了",是三个精确的改进项——混沌实验的价值密度取决于复盘质量。

六、落地节奏(6 个月路线)

1
2
3
4
M1-2:稳态定义 + 场景库前 4 个(依赖 kill/超时)+ staging 单实例
M3-4:扩到 12 场景 + 停止条件自动化(接监控告警)
M5: 生产金丝雀环境试跑(最小半径:1 个 pod,业务低峰)
M6: 季度韧性报告机制 + 架构变更触发规则写进发布流程

七、小结

混沌工程落地的四个关键:稳态定义(没有它实验就是吵架)、场景库按失效模式组织(可复用可累积)、爆炸半径 + 停止条件(实验本身是风险)、复盘出改进项(价值在产出不在过程)。它不替代回归/压测/长稳,回答的是它们都不回答的问题:"系统坏的时候,坏得体面吗?"