最近我在帮某个业务方做稳定性建设,发现一个挺有意思的现象:性能测试团队和混沌工程团队明明是奔着同一个目标去的,但各干各的,一个只关心压力下的响应时间,一个只关心故障下的可用性。结果就是,性能测试从来看不到实例挂掉时候的表现,混沌实验也从来不会在满载流量下做。两边都觉得自己测过了,但系统在“高压力+故障”同时发生时会怎么样,没人说得清。后来我把混沌工程和性能测试揉到了一起,做了一轮融合实验,暴露出来的问题比之前单测半年发现的还多。
这篇就把我的思路和落地细节都梳理一遍,包括为什么融合、怎么设计实验、用什么工具、怎么处理结果,以及我踩过的那些坑。
1. 为什么要把混沌工程和性能测试放在一起做
1.1 两者的目标和盲区
性能测试的核心是“施加可预测的负载”,通过不断抬高并发数、吞吐量,观察系统在不同压力等级下的行为,从而回答“系统能扛多少QPS”、“P99会不会失控”、“资源使用率是否合理”。它的优势是可控、可量化、可重复,问题是它假设所有服务实例和依赖都处于健康状态。哪怕你压到极限,也不会主动把某一个节点杀死、把网络延迟拉高或者让磁盘IO卡死。
混沌工程的核心是“主动注入不可预测的故障”,用来验证系统在真实故障下是否还能提供基本服务。它擅长发现超时没设兜底、重试风暴、依赖故障导致的雪崩等问题。但传统混沌实验通常在空闲或者低负载环境里做,这就有一个很隐蔽的盲区:某些故障在低流量下根本看不出来。比如一台应用实例被重启,如果只有10个并发,剩余实例轻松接住流量,系统几乎无感;但如果正扛着每秒2000次请求,一个实例崩溃可能直接打穿剩余实例的线程池,引发级联雪崩。
把两者翻出来对比,你会发现它们其实是在补对方的短板。性能测试告诉你系统的极限边界,混沌工程告诉你系统在异常下的韧性,但真实生产最危险的时刻是“极限边界和异常同时出现”,所以必须把两者的能力组合成一种新的实验方式。
1.2 融合实验能发现什么不一样的问题
我在实际融合实验里看到过几类很有代表性的问题,单独做性能或混沌测试很难暴露。
第一类是“压力下重试策略反而成了放大器”。单独做性能测试时,下游正常,重试机制基本不触发。单独做混沌实验时,下游故障但流量低,重试次数有限。可是当压测流量高,又逢下游短暂故障,所有请求瞬间超时并触发重试,重试流量像滚雪球一样叠加在已有流量上,导致系统总对外请求量翻了数倍,最终把下游完全打挂。
第二类是“降级策略在高负载下失效”。很多降级逻辑平时表现很好,比如熔断后立刻返回缓存数据。但高负载下缓存重建本身需要消耗大量CPU和数据库连接,降级后的响应路径竟然比原路径还要慢,原本想保护系统,反而拖垮了系统。
第三类是“资源耗尽型故障的放大效应”。内存泄漏这类问题,低负载下可能要跑几小时才让容器OOM,但在压力测试过程中注入一个小的CPU抖动,系统就会因为线程阻塞发生堆积,内存快速增长,最终提前触发OOM。这些现象都告诉我,稳定性不能只靠“单维测试”,必须让压力和故障成为同一场实验里的两个变量。
2. 融合方案的整体设计思路
2.1 融合要解决的核心矛盾
真正落地时,我发现混沌工程和性能测试的理念是冲突的。性能测试追求“可重复、可控、定量”,同一个场景跑三次,结果应该基本一致。混沌工程则强调“随机、不可预测、探索未知”,每次故障注入的位置和时间都可能不同,这样才可能发现意外。如果直接把两套流程拼在一起,实验结果千差万别,根本没法分析。
我的解法是给两者划定边界:压测负责“标准场景”,混沌只负责“受控扰动”。压测的主链路、并发数、步长完全按性能测试的标准脚本来跑;混沌注入的时间点、目标、持续时长、爆炸半径全部提前规划好,不做纯随机的滥用。这样既保留了性能测试的可比性,又能覆盖混沌工程的故障场景。换句话说,把混沌做成压测过程中的一个“可控变量”,而不是来搅局的随机事件。
2.2 从“先压后杀”到“压着杀”实验模型
很多团队做稳定性演练时,习惯“先压后杀”:先跑单独的压测,看性能指标;再停掉压测,注入故障,看容错表现。这种方式有硬伤——它检验的是“压力状态”和“故障状态”两个独立场景,而不是“压力+故障”的叠加场景。
我采用的模型是“压着杀”,具体分成四步:
- 低负载与正常状态:先以低并发跑几分钟,确认系统各组件连通,采集最基础的行为数据。
- 压测稳态:把并发逐步抬升到目标水位,保持一段时间,记录当前负载下的基线指标,确定没有内存泄漏或性能衰减。
- 故障扰动:在压测不停止的前提下,按照实验计划注入指定故障,持续一段设计好的时间,然后恢复。
- 回归观察:解除故障后,继续维持相同压测负载,观察系统能否自愈并回到稳态基线水平。
这个模型的核心逻辑是验证一个完整闭环:系统在压力下能不能发现自身弱点,应对故障后能不能重新站稳。如果解除故障后,系统仍无法回到故障前的性能水平,那说明系统已经产生了不可逆的损伤,这种问题是最值得提前暴露的。
3. 核心细节与实操要点
3.1 怎么设计故障注入的时机
故障注入不能想什么时候打就什么时候打。压测刚开始阶段,应用正在经历JIT预热、连接池初始化、缓存慢加载,这时的指标本来就波动大,如果此时注入故障,测出来的数据很难区分是故障影响还是启动影响。
我常用的时间策略是:
- 先在目标并发下跑至少5到10分钟,等各项指标平缓后再开始注入。
- 如果压测场景有阶梯升压,故障注入放在某一个阶梯稳定阶段的中间,而不是升压过程中。
- 故障持续和观察时间要挂钩。比如设计故障持续3分钟,那解除后至少要观察5到10分钟,因为系统自愈需要时间。我遇到过连接池耗尽的情况,故障解除后花了30分钟才慢慢恢复,观察窗口太短的话,你会误判成故障恢复能力很强。
3.2 爆炸半径和持续时间的“三层设计”
爆炸半径决定了实验的安全边界。融合实验里,我强烈建议从最小粒度开始试。一个很实用的参考配置是这样的:
| 故障层级 | 举例 | 持续时间建议 | 验证目标 |
|---|---|---|---|
| 单实例 | 重启一个订单服务实例 | 30-60秒 | 快速失败与重试机制 |
| 单依赖 | Redis延迟增加300ms | 3-5分钟 | 缓存降级与连接池行为 |
| 资源层 | 某服务CPU占用80% | 5-10分钟 | 资源争抢下的稳定性 |
短故障主要看系统是否能快速失败、超时回调是否生效;中故障重点观察连接池回收、缓存重新加载等中间状态;长故障则用来发现队列积压、内存增长这类慢变问题。如果你一上来就注入10分钟全链路故障,出了问题可能整个环境都炸了,连数据都没法收集。
3.3 性能基线和混沌结果之间要建立联动
很多团队做融合实验,最大的问题是“没有基线”。没有基线,你就无法回答“这个故障到底造成了多大影响”。所以我在项目里建了一个统一的基线库,给每个压测场景记录以下指标:正常状态下的吞吐量、P99响应时间、错误率、CPU/内存/网络占用等。
混沌实验结果出来后,我会计算一个“影响因子”,比如某次故障注入后P99从60ms涨到1200ms,影响因子就是20倍。用这个因子横向对比不同故障的破坏力,能够帮团队排出“哪些故障需要优先防护”。基线数据至少取正常状态下的三轮平均值,避免把偶发抖动当成基线。
注意:基线和实验结果必须放到同一监控体系里,时间轴统一。否则一边用Prometheus,一边用自己的日志,时间对不上,定位问题会非常痛苦。
4. 实操过程:一个完整的融合实验示例
4.1 环境准备与工具选型
我拿一套模拟的订单系统做实验。链路是:端上请求进入网关,由网关转发给订单服务,订单服务依赖Redis做会话缓存,依赖数据库做订单读写,还有一个第三方支付通道。我刻意让测试环境与线上配置保持一致,尤其是资源配额和容器规格,因为配置不同会让实验结论作废。
工具方面,压测用k6,故障注入用ChaosBlade,监控用Prometheus和Grafana。选k6是因为脚本简单,能平缓升降并发,还能直接输出Prometheus指标;选ChaosBlade是因为它的故障场景丰富,支持CPU、内存、网络延迟、进程终止,而且可以精确控制目标服务。
4.2 压测脚本与混沌注入配置
我用k6写了一个阶梯压测场景,先把并发慢慢抬到目标值,再稳定运行一段较长的时间:
import http from 'k6/http'; import { check } from 'k6'; export const options = { scenarios: { load_test: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '2m', target: 50 }, { duration: '5m', target: 100 }, { duration: '10m', target: 200 }, { duration: '5m', target: 200 }, ], }, }, }; export default function () { const res = http.get('http://gateway.test.local/orders/123'); check(res, { 'status is 200': (r) => r.status === 200, 'response under 500ms': (r) => r.timings.duration < 500, }); }混沌注入我选择在压测进入200并发并稳定3分钟之后执行。第一次实验注入的是CPU压力,命令如下:
blade create cpu load --cpu-percent 80 --timeout 3m这条命令会让目标服务的所有可用CPU尝试跑满80%,持续3分钟。第二次实验我换了一种故障,给Redis所在的网络路径注入延迟:
blade create network delay --interface eth0 --latency 300 --offset 10 --timeout 3m注入之前要确认目标、网卡名和本机IP,避免把实验打到别的环境。
4.3 实验执行与数据采集过程
整个实验我按照下面的时间线来执行:
- 第0-15分钟:低并发起步,逐步升压,第12分钟到达200并发。
- 第15-18分钟:保持正常压测,记录基线数据,确认P99在80ms上下浮动。
- 第18分钟:执行CPU故障注入。
- 第18-21分钟:故障持续期间,观察Grafana面板变化。
- 第21分钟:故障自动消除,继续压测,观察恢复情况。
- 第25分钟:等指标稳定后,执行Redis网络延迟注入。
- 第25-28分钟:观察延迟注入下的错误率、重试行为。
- 第28分钟:故障消除,继续观察10分钟。
实验打完,我记录了这样的实际情况:CPU故障持续时,订单服务的响应P99从80ms飙到900ms,但吞吐量下降并没有想象中严重。Redis延迟注入时,P99上涨不如前者,但错误率明显升高,日志里出现大量连接等待超时。这说明系统对CPU资源争抢的容忍度尚可,但对Redis延迟的降级和重试策略设计不够好。后续我们把Redis超时时间缩短,并给热点接口增加了进程内缓存,问题才得到缓解。
这个例子就是想说明,融合实验不是看一个“通过/不通过”的结果,而是要把数据和日志结合,找出系统最薄弱的那个点。
5. 常见问题与排查技巧
5.1 实验结果不稳定,先查压测端而不是被压系统
我第一次做融合实验时,连续跑了两轮,出来的指标差得离谱。后来一查,发现压测客户端和被压系统共用了同一台物理机,故障注入把整机的CPU打满后,压测脚本自己都跑不动了,发出的请求数量波动巨大。所以实验前一定要保证压测机资源充足,并且与被压系统隔离。还有一个细节,监控采样周期至少要小于故障持续时间,不然采不到故障期间的峰值。
5.2 无法区分性能瓶颈和故障注入的影响怎么办
区分的方法是看“突变点”。性能瓶颈通常是渐变的,随着并发升高逐渐恶化;而故障注入的影响是突变的,注入的一瞬间指标会剧烈跳变,解除故障后又会有一个明显回移。我在Grafana上把注入和结束时间都画了标记,然后在同一个时间轴上比对。如果突变点和注入时间严丝合缝,那基本可以确定是故障影响;如果解除故障后指标迟迟不回落,那就要检查是不是故障引发了次生问题,比如连接池没恢复、线程堆积、缓存击穿等。
5.3 融合实验落地时必须避开的几个坑
- 不要第一次就在生产环境做完整融合实验。先测试环境把工具用熟,摸清监控采集、命令超时、故障恢复等细节。
- 故障注入命令必须加超时。没有超时的故障会让你在实验结束后还得手动恢复。ChaosBlade的timeout参数挺好用,但要确保执行安全管理组件同步返回。
- 每次实验只改一个变量。如果你想验证多个故障,就拆成多轮实验,不要同时注入CPU和网络延迟,否则出问题你根本归不了因。
- 实验前通知所有相关方,并准备一封“本次是演练”的公告模板。我见过因为实验没提前通知,故障被当成真实事故,整个值班组被叫起来的尴尬。
- 记录要沉淀成故障履历,不能跑完就结束。我每次实验后都会写一份简报,包含故障场景、预期结果、实际影响、恢复时间、后续整改项。这些材料后来成了稳定性评审里最有说服力的资料。
5.4 一些小技巧
我个人经验是,故障注入结束后不要立刻看数据。先等一个自愈窗口,比如5分钟后,再看指标是否回到基线。如果系统能在窗口内自我修复,说明容错设计有效;如果回不来,才需要人工介入和排查。还有,多关注“全长链路”的错误面,而不要只看单个服务的响应时间。混沌和压测叠加以后,很多故障是跨服务传播的,单看某一段看不出问题。
做融合实验跑了几轮,我的体会是:大多数系统的性能指标在正常状态下都很好看,但在“压力已经很大,又突然出点小故障”的那一刻,才能真正看出设计上有没有给自己留活路。性能测试让我知道系统哪里会不够,混沌工程让我知道系统哪里会倒,把两者放到一起,看到的就是“不够的时候遇到倒下会发生什么”。以后我落到每个版本的稳定性验证里,这都会是必选项。
最后再分享一个小经验:融合实验里,忍耐住“马上看结果”的冲动很重要。故障结束后的自愈观察,比故障期间的那些剧烈数字更能说明系统的健壮性。一个能恢复的系统,和另一个虽然没崩溃但恢复很慢的系统,在真实问题面前是完全不同的命运。