网络层面的问题是分布式系统里最隐蔽的一类故障。机器看起来一切正常,进程还活着,CPU和内存都不高,但服务就是莫名其妙地超时、重试、甚至误触发主备切换。等到事后排查,往往只能从零散的日志里猜个大概。与其被动等待故障发生,不如主动在测试环境甚至预发环境中注入网络延迟和丢包,观察集群的真实反应,这就是网络故障演练的核心价值。本文将从原理、工具、实操流程和结果分析几个方面,完整讲一次如何做好这类演练。

一、为什么要专门针对网络做故障演练
很多团队做过宕机演练,比如直接 kill 掉某个节点,看集群是否能自动恢复。但宕机演练覆盖的场景其实很有限。分布式系统中大量的一致性协议、超时设计、重试策略,其行为分支只有在网络异常时才会被触发,而节点宕机属于相对“干净”的故障——其他节点能明确感知到对端不可达,逻辑处理反而简单。
网络故障则完全不同。延迟是渐进的、波动的,丢包是概率性的。一个 Raft 或 Paxos 集群在 300ms 的网络延迟下,可能因为选举超时参数设置不合理而疯狂发生选主抖动;一个依赖同步调用的链路,在 5% 的丢包率下如果没有合理的重试退避策略,很容易演变成线程池打满、故障向上游蔓延的雪崩。这些问题不通过真实注入是无法提前发现的。
更重要的是,网络演练能帮你回答几个关键问题:集群的超时阈值是否留了足够的余量?心跳检测的容忍窗口设多少合适?客户端的重试是否幂等?降级和熔断策略在网络抖动下是否真的生效?这些问题的答案直接决定了线上网络故障发生时系统的表现。
二、常用注入工具对比与选择
网络故障注入工具不少,选型时主要考虑部署方式、支持的场景类型和对业务的侵入性。下面这张表列出了几个主流方案的核心差异:
| 工具 | 注入层面 | 支持场景 | 侵入性 | 适用环境 |
|---|---|---|---|---|
| tc netem | 内核流量控制 | 延迟、丢包、乱序、抖动、带宽限制 | 无侵入 | 物理机、虚拟机 |
| toxiproxy | TCP 代理 | 延迟、超时、断连、带宽限制 | 需代理转发 | 任意环境,含容器 |
| Chaos Mesh | Kubernetes CRD | 网络延迟、丢包、分区、DNS 异常等 | 无侵入 | K8s 集群 |
| pumba | 容器层面 | 网络延迟、丢包、容器停止 | 无侵入 | Docker 环境 |
最经典的当属 Linux 自带的 tc netem,它直接工作在内核的流量控制层,能力最全面,除了延迟丢包还能模拟包乱序、包重复和带宽限制。缺点是命令参数较复杂,且只能影响出方向的流量,如果要做双向注入,需要在两端机器上分别配置。下面是一个典型用法:
# 在 eth0 出方向注入 100ms 固定延迟 + 10ms 抖动,并附加 5% 丢包 tc qdisc add dev eth0 root netem delay 100ms 10ms loss 5% # 模拟包乱序(25% 概率,相关系数 50%) tc qdisc add dev eth0 root netem delay 10ms reorder 25% 50% # 查看当前规则 tc qdisc show dev eth0 # 清除所有规则,恢复网络 tc qdisc del dev eth0 root
如果目标服务部署在容器或 K8s 环境中,Chaos Mesh 是更好的选择。它通过 CRD 描述故障,能精确选择注入目标(按 Pod 标签、命名空间筛选),并且支持定时自动恢复,不会因为演练脚本报错导致故障规则一直残留。而 toxiproxy 适合针对特定中间件连接做精细控制,比如只对 MySQL 主从之间的连接注入延迟,观察半同步复制的表现,这种细粒度在全局工具里反而不容易做到。
三、演练前的准备与实操流程
演练绝不是上去就敲一条 tc 命令。一次有价值的网络演练,事前准备至少要占一半的工作量。首先是明确演练目标和范围:这次要验证的是集群选主的稳定性,还是业务链路的降级策略?目标不同,注入的位置和参数差别很大。前者应该在节点间心跳链路上注入,后者则在服务调用的关键路径上注入。
其次是建立基线。注入故障之前,先记录正常状态下的核心指标:P99 延迟、错误率、QPS、主从切换次数、消息积压量等。没有基线,演练后的数据就没有参照意义。同时要确认观测手段就绪,Prometheus 指标、链路追踪、关键日志的采集都必须正常工作,否则故障发生时你只能看到表象,看不到内部状态。
第三点是制定爆炸半径控制和回滚预案。明确注入的时长上限、自动恢复机制(比如 Chaos Mesh 的 duration 字段)、以及一旦出现不可控影响时的人工介入步骤。建议首次演练一律从低强度开始,比如先注入 20ms 延迟观察一轮,再逐步加码到 100ms、300ms,而不是一上来就模拟机房级网络分区。
下面以 Chaos Mesh 的网络延迟故障为例,展示一个对指定 Pod 注入 200ms 延迟的配置:
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: delay-on-storage-node
spec:
action: delay
mode: one
selector:
namespaces:
- storage
labelSelectors:
"app": "redis-cluster"
delay:
latency: "200ms"
correlation: "50"
jitter: "30ms"
duration: "5m"
scheduler:
cron: "@once"
这个配置会对 storage 命名空间下被选中的 Redis 集群节点注入 200ms 的网络延迟,附带 30ms 抖动和 50% 的相关性,持续 5 分钟后自动恢复。cron 调度器保证故障只执行一次,适合手工触发的演练场景。
四、演练中的观测要点与结果分析
故障注入后,观测要分三层进行。第一层是基础设施层:确认注入是否生效,比如用 ping 或 mtr 验证实际延迟是否符合预期值,netem 的规则是否还在。经常出现的情况是命令敲了但没生效,比如 tc 规则被网卡重启冲掉,演练半天其实测的是正常网络。
第二层是中间件和集群行为层,这是重点。观察一致性协议的状态变化:Raft 集群是否发生了不必要的重新选举,Term 值是否持续增长;Redis Sentinel 是否因为主观下线判定误触发切换;Kafka 消费者的 rebalance 是否频繁发生。这些行为直接反映超时参数与网络延迟之间的匹配度。举个例子,如果心跳超时设为 500ms,而你注入了 300ms 延迟,再加上抖动,个别心跳包就会超过阈值,这种边界情况正是参数调优的依据。
第三层是业务层:面向用户的接口错误率、响应时间、以及下游依赖的连锁反应。特别要关注重试风暴——一次网络抖动后,客户端的重试请求叠加原始请求,可能让上游压力瞬间翻倍。如果观测到错误率在故障结束后仍长时间不恢复,往往说明熔断器的恢复条件设置有问题,或者连接池中的连接失效后没有被正确重建。
演练结束后,输出一份结构化的报告很重要,至少包含:注入参数与实际生效值、各层指标在故障期间的曲线截图、发现的问题清单、以及对应的改进项。问题要区分等级,比如“选举超时参数过小导致频繁切主”属于必须修复的高优先级问题,而“某次要链路重试无退避”可以排入后续迭代。每一次演练发现的问题修复后,建议在下一次演练中针对性复验,形成闭环。
五、把网络演练常态化
一次性的演练价值有限,真正有威慑力的是常态化。比较成熟的实践是分级推进:测试环境可以随时随机注入低强度网络故障,让系统长期生活在“不完美的网络”中;预发环境在每次大版本发布前执行固定的网络演练用例集,覆盖延迟、丢包、分区三类场景;生产环境则谨慎地选择低峰期,对个别可用区做受控演练,并提前报备和准备好紧急回滚。
另一个建议是把演练场景代码化。用 Chaos Mesh 的 CRD 或自动化脚本管理故障用例,纳入版本控制,这样每次演练都是可重复、可审计的。团队还可以基于历史线上故障建立“故障剧本库”,把真实发生过的网络问题转化为演练用例,定期回放。长此以往,系统对网络异常的免疫力会显著提升,值班同学再遇到半夜的网络告警时,心里也会踏实很多。