中间件-06-中间件故障注入降级和恢复测试怎么设计

会做中间件高可用建设,也会准备主从、集群、哨兵、代理、监控和告警,但一旦进入真实故障现场,最容易暴露的问题往往不是“没有方案”,而是下面这些落差:

  • 故障注入只停进程,没有覆盖超时、限流、延迟、半连通和错误返回
  • 验证只看组件是否恢复,没有验证业务是否真的恢复
  • 降级策略写在设计文档里,现场却不知道什么时候触发、谁负责解除
  • 恢复动作执行了,但连接池、缓存、消费者位点、任务重试和告警状态没有一起收口

这说明中间件稳定性测试不能只停留在“把节点打坏再拉起来”,而要把下面三个问题串成一条完整链路:

  • 故障是怎么注入的,影响面是否可控
  • 异常发生后,系统是否按预期降级
  • 故障恢复后,业务、数据、流量和证据是否真正收敛

这篇文章只讨论一个核心问题:

中间件故障注入、降级和恢复测试应该怎么分层设计、怎么执行、怎么控风险,才能验证出真实系统在异常条件下是否可用。

一、先把验证目标收清,不要把故障注入做成破坏性演练

中间件故障测试最容易失控的地方,是一开始就把焦点放在“怎么打坏”,而不是“想验证什么”。

更合适的做法是先把目标拆成四组。

1. 可探测性

需要先回答:

  • 中间件异常发生后,监控、日志、告警能否在可接受时间内发现
  • 平台、应用和中间件三层证据能否对上同一个时间窗
  • 故障发生后能否快速判断是连接问题、容量问题、拓扑问题还是数据问题

如果异常发生了却没有证据链,后面的降级和恢复结论都不可靠。

2. 降级有效性

需要验证:

  • 连接失败时,调用方是否快速失败而不是无限阻塞
  • 中间件延迟升高时,业务是否触发限流、熔断、只读、缓存兜底或异步化
  • 非关键链路是否真的能降级,关键链路是否仍能保住最小可用能力

很多系统不是没有降级,而是降级条件、降级粒度和解除条件都不清楚。

3. 恢复收敛性

更需要验证的是:

  • 组件恢复后,流量是否回归
  • 连接池、消费者、任务执行器、代理缓存是否同步收敛
  • 临时降级策略是否能安全撤回
  • 误报告警、残留重试和脏数据是否会继续放大影响

4. 风险边界

要提前说清:

  • 哪些动作允许在共享测试环境做
  • 哪些动作必须单独窗口执行
  • 哪些故障类型只能做模拟,不能直接做破坏
  • 当出现超出预期的影响时,立即止损动作是什么

没有风险边界,故障测试就很容易从“验证稳定性”变成“制造环境事故”。

二、故障注入要分层,不要只会停节点

故障注入的本质不是做戏剧化动作,而是覆盖真实现场最常出现的异常类型。更实用的分层方式通常是下面 5 层。

1. 连接层

这一层关注的是应用能不能连上中间件,常见注入动作包括:

  • 目标端口不可达
  • 安全组或防火墙临时拒绝访问
  • DNS 解析错误
  • 连接建立慢
  • 连接数耗尽

这类问题最适合验证:

  • 调用超时是否合理
  • 重试是否有上限
  • 是否会把线程池、连接池、任务队列一起拖死

2. 协议层

这一层更接近“连得上但用不了”的现场:

  • 建连成功,但请求超时
  • 返回错误码
  • 请求被拒绝
  • 主从角色切换后协议层状态短暂不一致

这类问题最容易诱发误判,因为业务日志里常常只会出现“调用失败”,但真正原因可能是协议握手异常、认证失败或请求超时。

3. 拓扑层

这一层重点验证集群成员变化、主从切换和路由收敛:

  • 单节点下线
  • 主节点切换
  • 哨兵、代理、注册中心状态漂移
  • 分区 leader、slot 或路由元数据更新延迟

会在这里踩坑,因为“拓扑已经切过去”和“业务已经恢复正常”不是一回事。

4. 数据层

更容易造成长尾影响的是数据层异常:

  • 主从复制延迟
  • 消息积压
  • 索引恢复慢
  • 缓存热 key 丢失
  • 持久化或刷盘延迟

这类问题往往不会在第一分钟直接把系统打挂,但会在恢复阶段继续引发旧数据、重复消费、读写不一致和查询抖动。

5. 资源层

这一层关注资源耗尽和慢性退化:

  • CPU 打满
  • 内存紧张
  • 磁盘空间不足
  • IOPS 受限
  • 网络抖动和带宽争抢

资源层故障最适合验证系统有没有提前退化、限流和熔断,而不是等到完全不可用才触发降级。

三、降级测试要验证什么,不要只看“服务还能返回”

中间件异常发生后,业务是否还能返回结果,不足以说明降级策略有效。更实用的验证维度至少有下面四组。

1. 降级触发是否正确

需要确认:

  • 触发条件是按错误率、时延、连接失败还是依赖不可用判定
  • 触发阈值是否过高,导致已经拖垮线程后才开始降级
  • 触发阈值是否过低,导致轻微波动就频繁抖动

2. 降级粒度是否合理

要看清:

  • 是全局降级还是接口级降级
  • 是单租户降级还是全量降级
  • 是读链路降级还是读写一起降级
  • 是只关闭非核心功能,还是把关键业务也一并打掉

3. 降级后的结果是否可解释

需要验证:

  • 用户看到的是兜底结果、缓存结果、默认结果还是明确错误提示
  • 平台、日志、告警和报告里是否能看出已经进入降级态
  • 降级结果是否会污染后续业务,例如空数据写回、错误状态缓存、重复任务补偿

4. 降级解除是否安全

很多故障不是坏在“没有降级”,而是坏在“恢复后没有退出降级”:

  • 组件恢复后是否自动撤回限流和熔断
  • 手工开关是否有人负责关闭
  • 恢复后是否先小流量试探再全量恢复
  • 残留缓存、队列积压和补偿任务是否被重新核对

四、恢复验证骨架:先恢复组件,再恢复流量,最后恢复结论

中间件恢复测试如果只看进程起来、端口通了、监控变绿,通常是不够的。更适合落地的一套恢复验证骨架可以拆成 5 个阶段。

1. 阶段一:确认故障源已经解除

先确认最基础的事实:

  • 注入动作是否已完全撤销
  • 节点状态、角色状态、路由状态是否恢复
  • 资源限制是否解除

如果故障源还在,后面的恢复动作只会制造更多噪音。

2. 阶段二:确认组件层恢复

这一阶段看的是组件自身:

  • 集群成员是否齐全
  • 主从、leader、slot、分区、consumer group 是否收敛
  • 复制、同步、刷盘、索引恢复是否追平

3. 阶段三:确认调用链恢复

这一阶段要拉到业务侧观察:

  • 应用连接池是否重新建连
  • 失败重试是否停止膨胀
  • 接口时延、错误率、任务堆积是否回落
  • 调度、回调、异步消费是否重新推进

4. 阶段四:确认降级策略撤回

需要明确核对:

  • 限流、熔断、只读、缓存兜底、开关降级是否撤回
  • 平台、配置中心和发布记录里的临时开关是否归零
  • 告警是否自动清除,或进入待人工确认状态

5. 阶段五:确认结果和证据收口

最后要把恢复结论真正收住:

  • 恢复后的关键业务用例是否回归通过
  • 是否产生脏数据、重复数据、漏消费或补偿残留
  • 故障窗口、恢复窗口、触发阈值和止损动作是否已记录

没有最后一步,很多所谓“恢复成功”只是现场感觉恢复了,实际上问题还在尾部继续扩散。

五、最小执行骨架:一套可以直接落地的中间件故障测试方式

如果需要把中间件故障注入、降级和恢复测试做成固定动作,至少应该准备下面这些内容。

1. 故障场景清单

按中间件类型不同,清单可以不同,但至少要包含:

  • 连接失败类
  • 请求超时类
  • 拓扑切换类
  • 数据滞后类
  • 资源耗尽类

每个场景都要带上:

  • 注入动作
  • 预期影响面
  • 预期降级结果
  • 预期恢复标准
  • 止损动作

2. 执行前检查

执行前至少要确认:

  • 监控、日志、告警是否可用
  • 回滚和恢复动作是否演练过
  • 关键业务观察人是否到位
  • 当前环境是否允许做这类注入
  • 是否有正在进行的发布、迁移或压测活动

3. 执行中记录

建议至少记录一张最小记录表:

时间点 注入动作 影响对象 业务现象 降级动作 关键指标 当前结论
20:30:00 阻断 Redis 主节点访问 订单查询链路 接口 RT 上升 缓存兜底开启 错误率 3.2% 进入降级
20:31:20 切主完成 Redis 集群 RT 开始回落 熔断未解除 命中率 78% 组件恢复,业务未完全恢复

4. 恢复后回归

恢复后不要只做健康检查,还要至少跑下面三类动作:

  • 核心接口回归
  • 数据一致性核对
  • 告警与开关复位核对

5. 结论输出

一轮有效的故障测试结论,至少应该输出:

  • 这次验证了什么故障类型
  • 降级是否按预期触发
  • 恢复是否按预期收敛
  • 发现了哪些配置、阈值、脚本、流程和证据链问题
  • 下一轮需要补哪些注入场景

六、风险控制:故障测试不能只追求“打出问题”

中间件故障测试的价值,在于提前收敛风险,而不是把环境打穿。所以至少要守住下面 5 条底线。

1. 先做低风险场景,再做高风险场景

更合理的推进顺序通常是:

  • 单实例只读观察
  • 单链路限速或超时模拟
  • 单节点摘除
  • 拓扑切换
  • 资源耗尽和混合异常

2. 先小范围,再扩大影响

先从单服务、单租户、单接口、单环境开始,再考虑更大影响面。不要第一次演练就直接全量打共享环境。

3. 故障注入和恢复脚本要成对准备

每个注入动作都要提前准备对应的恢复动作、验证命令和责任人。否则现场一旦偏离预期,就只能手工抢修。

4. 要有明确止损阈值

例如:

  • 核心接口错误率超过阈值立即回退
  • 积压增长超过阈值立即停止注入
  • 关键业务超时超过阈值立即触发人工接管

5. 故障窗口和非故障窗口要分清

有些异常是注入动作造成的,有些异常是环境本来就有的问题。时间窗不对齐,结论就很容易错。

七、最容易踩的 6 个坑

1. 只验证中间件恢复,不验证业务恢复

节点恢复、角色恢复、监控变绿,不代表业务已经恢复。更需要看调用链、任务链和结果链是否一起收敛。

2. 只做节点下线,不做慢、抖、半连通

真实现场更常见的是延迟升高、短暂抖动、部分请求失败,而不是整台机器直接消失。

3. 降级策略存在,但没有解除路径

限流、只读、缓存兜底开了之后,如果没有回收机制,就会把临时策略变成长期隐患。

4. 恢复后不核对残留数据

消息重复、缓存旧值、索引未追平、主从延迟未消除,都会在恢复后继续制造假象。

5. 只看单点指标,不看证据链

只看错误率、只看组件监控或只看应用日志,都容易误判。更有效的做法是把注入时间、组件状态、业务现象和告警时间对成一条线。

6. 只在单组件视角做结论

中间件故障影响的通常不是一个点,而是一条链。缓存故障可能放大数据库压力,消息积压可能拖慢回调,代理失真可能让健康实例也被误伤。

八、真实案例:一次故障注入后组件恢复了,但业务没有恢复

1. 场景

某测试平台在执行回归任务时依赖 Redis 做任务状态缓存和短期结果聚合。平台设计上已经有降级策略:

  • Redis 不可用时,任务状态先落本地内存队列
  • 查询接口优先返回最近一次成功快照
  • 告警在错误率持续升高时触发

这套方案在设计评审里看起来完整,但一直没有做过完整的故障注入和恢复验证。

2. 执行

一次稳定性演练里,先对 Redis 主节点做网络阻断,再观察:

  • 平台任务执行
  • 状态查询接口
  • 告警触发
  • 主从切换后的恢复情况

演练动作分成三步:

  • 第一步,阻断应用到主节点的访问
  • 第二步,等待哨兵切主
  • 第三步,解除阻断并观察业务恢复

3. 现象

现场最先看到的是:

  • 任务执行没有完全中断,但状态查询接口开始明显变慢
  • 平台日志出现大量重试
  • 告警按预期触发
  • 哨兵切主成功后,Redis 监控恢复正常

问题出在恢复阶段:

  • 组件监控已恢复
  • 任务执行吞吐仍明显偏低
  • 查询接口仍持续命中旧快照
  • 部分任务状态长时间停留在“执行中”

4. 排查

沿着时间线回看后,问题被拆成了三层:

  • 第一层,Redis 拓扑已经恢复,但应用连接池没有快速刷新,仍有旧连接在重试
  • 第二层,降级时写入本地内存队列的状态,没有在恢复后及时回灌
  • 第三层,查询接口命中的“最近成功快照”缺少过期控制,恢复后仍继续返回旧值

进一步核对日志和指标后,又确认了两个事实:

  • 告警触发和解除只关注了组件可用性,没有关注业务结果恢复
  • 演练记录里没有单独检查“降级策略是否撤回”这一项

5. 修复

后续调整分成四步:

  • 给连接池增加切主后的主动刷新和失败上限
  • 给本地兜底队列补回灌逻辑和积压告警
  • 给快照结果加过期时间和恢复后的强制失效动作
  • 在演练清单里新增“降级解除核对”和“恢复后业务回归”两项

调整之后再次演练,现场结论才真正收住:

  • 组件恢复后,业务结果在可接受时间内回归
  • 临时降级策略会自动撤回
  • 告警不再只盯组件状态,而是同时盯业务恢复状态

九、结语

中间件故障注入、降级和恢复测试真正要验证的,不是某个组件会不会挂,而是系统在异常条件下能不能保持最小可用、能不能按预期退化、恢复后能不能把流量、数据、开关和证据一起收住。

如果只做停节点和看监控,最终拿到的通常只是“组件扛过一次故障”的结论。更有价值的测试结果,应该能回答下面这些问题:

  • 哪类故障已经被覆盖
  • 哪类降级真正有效
  • 哪些恢复动作只是把组件拉起来,哪些恢复动作能让业务真正收敛
  • 下一轮最该补的风险点是什么

做到这一步,中间件稳定性测试才不是一场表演,而是一套能持续降低线上风险的验证机制。