# 别再盯着 CPU 看弹性了:混沌工程真正该测的是“扛揍能力”
做运维这些年,我越来越觉得一件事挺有意思:有些系统,平时监控面板漂亮得很,CPU 不高,内存不满,接口响应也很快。可一旦 Redis 出点问题、下游接口卡上几秒,或者某个实例突然挂掉,整个业务就开始排队、超时,甚至直接瘫痪。
这时候我们经常会说一句话:“系统的弹性不太行。”
但问题来了:弹性到底怎么衡量?拿什么证明一个系统真的有弹性?
看 CPU 利用率?看服务器有没有自动拉起?还是看故障发生后,服务能不能自动恢复?
这些指标都有用,但单看任何一个,都可能把我们带沟里。
比如,某个服务的 CPU 利用率只有 30%,看起来从容得很。可它依赖的数据库连接池已经耗尽,所有请求都在等待连接。此时 CPU 再低,也救不了业务。
再比如,某个服务实例挂掉后,Kubernetes 十几秒就把它重新拉起来了,监控上还显示恢复成功。可在这十几秒里,用户提交的订单失败了一批,消息队列积压了一大堆,恢复之后又因为重试风暴把数据库打崩了。
你说这个系统有弹性吗?
我的看法很明确:弹性不是机器能不能活下来,而是故障发生时,业务能不能尽量不停、损失能不能控制住,以及故障过去后能不能真正恢复正常。
所以,混沌工程设计指标,不能只围着机器转。我们真正要测的,是系统在异常条件下维持业务、限制故障扩散和恢复服务的能力。
一、做混沌实验之前,先搞清楚系统的“正常”是什么
混沌工程有个很重要的思路:先定义系统正常运行时应该是什么状态,再主动注入故障,观察系统有没有偏离预期。混沌工程的原则也强调,应该关注系统可衡量的外部表现,而不只是内部属性。
听起来有点抽象,我举个例子。
假设你维护的是一个订单系统,平时的运行情况大致如下:
- 每秒处理 1000 个订单请求。
- 订单提交成功率 99.95%。
- 99% 的请求能在 300 毫秒内完成。
- 订单数据没有丢失,也没有重复创建。
那么,这几项就可以成为你的基线。
接下来,我们主动让 Redis 响应变慢,或者直接让一个应用实例下线。此时我们不应该只问“Redis 有没有恢复”“应用有没有重新启动”,而应该问:
订单成功率下降了多少?有多少用户受到了影响?多长时间恢复到可接受的状态?故障期间有没有丢单?
这才是混沌实验真正需要回答的问题。
还有一个细节:实验期间的正常基线必须考虑业务流量变化。双十一的高峰期和凌晨三点的业务量完全不同,不能直接拿两个时段的数据比较。可以选择相近时段、相近流量作为参照,也可以设置对照组,尽量排除其他变量的影响。Netflix 的混沌工程实践就曾使用业务指标和对照组来观察故障注入的实际影响。
二、哪些指标才值得重点关注?
我认为,不需要一上来就设计几十个指标。先把下面几类指标测清楚,比堆一张密密麻麻的监控大屏有意义得多。
1. 业务成功率:故障期间,事情到底办成了多少?
这是我最看重的一类指标。
运维人员习惯看 HTTP 200、接口调用量、服务器在线率,但这些指标不一定等于业务成功。
例如,一个接口返回了 HTTP 200,响应内容却是“订单处理中,请稍后查询”。对于普通查询,这可能没问题;对于要求立即完成的支付业务,它未必代表交易成功。
所以,指标设计一定要结合具体业务。
对于订单系统,可以关注成功创建的订单数、支付成功率;对于消息系统,可以关注成功处理的消息数;对于文件处理系统,可以关注完整处理的文件数量。
例如,使用 Prometheus 统计接口请求成功率:
sum(rate(http_requests_total{
route="/orders",
status=~"2.."
}[5m]))
/
sum(rate(http_requests_total{
route="/orders"
}[5m]))
这里假设应用已经按接口和 HTTP 状态码暴露了指标。实际项目中,标签名称需要根据自身的埋点调整。如果 HTTP 200 不代表业务成功,还应该单独统计订单创建成功、支付成功等业务事件。
还要注意,成功率不能脱离流量看。
一个系统平时每秒处理 1000 个请求,故障期间只剩 100 个请求,而且成功率仍然是 99%,表面看起来似乎没什么问题。但如果剩下的流量是因为大量请求超时、主动失败而被系统丢弃的,那么这个成功率就具有误导性。
因此,最好同时观察:
- 业务成功率。
- 成功请求的实际吞吐量。
- 总请求量、拒绝请求量和超时请求量。
我的观点是,系统可以降级,但不能把“少干活”伪装成“干得很好”。
2. P95、P99 延迟:系统是不是已经开始吃力了?
平均响应时间是一个很容易让人误判的指标。
假设一个接口有 1000 次请求,其中 990 次在 100 毫秒内完成,剩下 10 次却耗时 10 秒。平均值可能依然不算特别夸张,但那 10 个用户的体验已经非常糟糕。
所以,混沌实验不能只盯着平均值,应该同时关注 P95、P99 等高分位延迟。Google SRE 的监控指导也强调了延迟、流量、错误和饱和度这四类关键观测信号,并提醒我们应当区分成功请求和失败请求的延迟。
如果接口已经暴露了 Prometheus Histogram 指标,可以这样统计 P99:
histogram_quantile(
0.99,
sum by (le) (
rate(http_request_duration_seconds_bucket{
route="/orders"
}[5m])
)
)
然后,在混沌实验中观察三个阶段:故障前、故障注入期间、故障解除后。
这里有个很容易忽略的问题:故障解除,不代表延迟马上恢复。
例如,数据库连接池已经耗尽,即使数据库恢复了,等待中的请求也未必能立即获得连接。超时重试还可能进一步放大请求量,导致系统迟迟无法回归正常。
因此,我们不只看故障发生时延迟升高了多少,还要看故障解除后,P95 和 P99 需要多长时间才能回到目标范围。
3. 故障影响范围:到底有多少请求和用户受了影响?
同样是一个实例宕机,两套系统的表现可能完全不同。
系统 A 有 10 个实例,一个实例挂掉之后,其余实例迅速接管流量,成功率几乎没有变化。
系统 B 虽然也有 10 个实例,但所有实例都依赖同一个连接池服务。一个实例发生异常后,大量请求堵塞,最终其他实例也受到影响。
只看“宕机了一个实例”,两者好像差不多;看业务影响,区别就很大了。
所以,混沌实验还应该关注故障的影响范围,也就是故障半径。
例如:
- 受影响请求占总请求的比例。
- 受影响用户数占活跃用户数的比例。
- 受影响的业务分区、租户或服务实例数量。
- 一个依赖故障是否引发其他服务连锁异常。
其中,受影响请求比例可以按下面的思路计算:
\[ \text{影响比例}= \frac{\text{故障期间失败或超标的请求数}} {\text{故障期间总请求数}} \times 100\% \]
要注意,不能把所有错误都一股脑归因于混沌实验。实际统计时,应尽量区分实验造成的异常和原本就存在的错误。
我个人很看重这个指标,因为它能反映一个系统有没有做好隔离。
真正有弹性的系统,不一定永远不出故障,但应该尽量做到:一个地方出了问题,不要拖着整条业务链一起陪跑。
4. 恢复时间:不是进程启动了,而是业务恢复了
很多团队做故障演练时,会把恢复时间简单理解为实例重启时间。
比如 Kubernetes 发现 Pod 异常,随后自动创建新 Pod,几秒后 Pod 状态变成 Running。于是演练报告写上“自动恢复成功”。
但这个结论下得太早了。
Pod 变成 Running,只能说明它达到了相应的运行状态,未必意味着应用已经准备好接收流量,更不代表业务已经正常。
应用可能还在加载缓存,数据库连接池可能尚未建立,消费端可能仍然落后于消息队列,甚至监控都还没来得及发现业务已经出错。
所以,建议把恢复时间拆开衡量:
| 指标 | 关注的问题 |
|---|---|
| 故障发现时间 | 多久发现系统异常? |
| 故障定位时间 | 多久识别出主要原因? |
| 故障缓解时间 | 多久停止故障继续扩大? |
| 业务恢复时间 | 多久恢复到可接受的业务状态? |
| 完全恢复时间 | 多久清理积压、恢复数据和依赖状态? |
对于自动化程度较高的系统,故障发现、切换和流量恢复可以自动完成,不一定需要人工介入。但无论采用什么方式,最终都要以事先定义的业务健康条件作为判断依据。
我更愿意把最后的两个指标作为重点:业务恢复时间和完全恢复时间。
因为用户不关心某个 Pod 是几点启动的,用户关心的是订单什么时候能提交成功。
5. 队列积压与排空时间:别让“恢复正常”只停留在表面
这一点在消息队列、异步任务、数据同步以及批处理系统里尤其重要。
假设我们有一个订单异步处理服务,平时每秒消费 500 条消息。现在主动停止一个消费者,持续 60 秒。
如果这段时间生产者仍然以每秒 500 条的速度发送消息,那么理论上就会新增约 3 万条待处理消息。
消费者恢复以后,服务监控可能很快显示正常,但队列里还有几万条消息等待处理。
这意味着什么?
新订单也许能够提交成功,但后续库存同步、物流通知或者 ERP 数据传输仍然存在延迟。对用户而言,业务链路未必真的恢复了。
因此,建议关注:
- 队列积压峰值。
- 最老消息的等待时间。
- 故障解除后积压下降的速度。
- 积压恢复到正常区间所需的时间。
- 积压期间是否产生消息丢失、重复消费或处理乱序。
这里还有一个很实际的经验:不要只看积压数量,还要看积压的增长趋势。
如果故障解除后,消息消费速率仍低于生产速率,积压就会持续扩大。即便消费者处于运行状态,也谈不上恢复。
一个系统能不能恢复,不只是看它重新开始工作,还要看它能不能把故障留下的“烂摊子”收拾干净。
6. 数据一致性:系统可以慢一点,但不能把账算错
如果业务涉及交易、库存、支付、生产数据或者跨系统同步,那么数据正确性必须是混沌工程的重要判定条件。
举个简单的例子。
我们模拟数据库短暂不可用,应用随后进行了重试。结果,客户端第一次请求实际上已经创建了订单,只是响应在返回途中失败;重试之后,系统又创建了一笔订单。
从接口监控来看,第二次请求成功了,服务也恢复正常了。
但用户可能发现,自己下了一次单,系统却生成了两笔订单。
这显然不能算弹性好。
因此,针对不同业务,需要设置相应的数据校验指标,例如:
- 订单请求数与有效订单数之间是否符合预期。
- 支付金额与订单金额是否一致。
- 消息生产数量与最终成功处理数量是否一致。
- 故障期间是否产生重复记录、数据丢失或非法状态转换。
- 恢复后是否存在需要人工修复的数据。
当然,部分异步业务本来就存在合理的时间差,所以不能要求所有数据在任意时刻都立即一致。关键是先定义业务允许的延迟和一致性边界,再检查故障是否让系统超出这个边界。
对于这类指标,我的态度比较明确:性能下降可以被接受到一定程度,数据正确性却不能随意妥协。
三、不要只列指标,还要把“合格线”写出来
前面这些指标都很重要,但光有指标还不够。
如果一场实验结束之后,大家盯着图表讨论半天,最后得出一句“看起来问题不大”,那这场演练就很难真正指导改进。
混沌实验在开始之前,就应该定义假设、判定条件和停止条件。
例如,我们要验证订单服务在一个 Redis 实例不可用时,能不能维持基本业务。
可以先写出类似这样的实验规则:
experiment:
name: redis-failure-resilience
target: order-service
fault:
type: redis-unavailable
duration: 60s
steady_state:
order_success_rate_min: 0.99
p99_latency_max_ms: 1000
order_data_loss_allowed: 0
duplicate_order_allowed: 0
recovery:
business_recovery_max_seconds: 120
queue_drain_max_seconds: 300
abort:
order_success_rate_below: 0.95
critical_data_integrity_failure: true
这里的数值只是演示用的假设,不是通用行业标准。实际阈值应当结合现有 SLO、业务容忍度、峰值负载和数据风险确定。
这段配置想表达的核心是:在注入故障之前,就明确什么叫成功、什么叫失败,以及发生什么情况必须提前停止实验。
例如:
如果订单成功率短暂下降,但仍然处于可接受范围,故障解除后也能在目标时间内恢复,那么实验可能通过。
如果 Redis 短暂不可用就让订单成功率跌到不可接受的水平,说明系统可能过度依赖缓存,或者缺少合理的超时与降级机制。
如果业务服务已经恢复,但是消息积压超过预期恢复时间,那么实验也不应该直接判定为完全通过。
如果发生数据丢失或重复订单,更不能因为其他指标很漂亮就把结果算作成功。
这里还需要补充一点:停止条件必须配合真正有效的控制措施。配置文件写着 abort,并不代表实验平台就会自动停止故障注入;需要由执行工具、监控检查和实验编排逻辑共同实现。
四、CPU、内存、重启次数这些指标是不是没用了?
当然不是。
问题不在于这些指标不重要,而在于它们更适合帮助我们解释故障,而不是单独用来证明弹性。
举个例子:注入 CPU 压力以后,CPU 利用率达到 95%,这是非常有价值的诊断信息。但如果此时业务成功率依旧稳定、延迟没有明显恶化,那么我们还需要进一步判断系统是否仍有足够的资源余量,以及这种状态能持续多久。
反过来,如果 CPU 只有 30%,但数据库连接池使用率已经达到上限,线程池等待时间不断增长,接口 P99 已经飙升,那么系统实际上已经处于明显的退化状态。
因此,CPU、内存、磁盘 IO、网络丢包、连接池使用率、线程池等待队列、自动扩缩容耗时等指标,仍然是很好的诊断工具。Google SRE 的监控指导也建议关注服务所依赖的各类资源,包括线程池和等待队列等可能先于硬件资源达到瓶颈的信号。
我的建议是,把指标分成两层:
第一层是结果指标:业务成功率、有效吞吐量、P99 延迟、影响范围、业务恢复时间、数据一致性。
第二层是原因指标:CPU、内存、连接池、线程池、队列深度、重启次数、扩缩容情况。
先通过第一层判断系统扛不扛得住,再通过第二层分析为什么扛不住。这样的思路,比单纯要求 CPU 低于某个百分比更有价值。
五、最后说说我认为最容易踩的三个坑
第一个坑:只看平均值。
平均值很容易掩盖少数请求的严重退化。混沌实验至少应该把成功率、吞吐量和高分位延迟结合起来看,而不是拿一个平均响应时间就下结论。
第二个坑:只验证故障发生时,不验证故障解除后。
有些系统确实能够撑过故障,但故障过去后却留下积压、重试风暴、缓存失效或者数据不一致。完整的实验必须覆盖故障前、故障中和恢复后,直到关键指标回到约定的状态。
第三个坑:把每次演练都做成一次表演。
实验之前就知道一定会通过,最后只截几张监控截图,这样的演练很难发现真正的问题。
更有价值的方式是提出一个可以被证伪的假设:例如“某个 Redis 节点不可用时,订单服务可以通过降级继续处理核心请求,业务成功率不会低于约定阈值”。随后主动注入故障,认真检查结果。如果不符合预期,就查清楚原因、修复问题,再进行同样的实验验证修复是否有效。
这才是混沌工程真正有价值的地方:不是制造故障给大家看,而是把那些平时不敢验证、也不确定是否有效的韧性机制,放到可控环境里检验。
写在最后
我一直觉得,运维指标最大的价值,不是让监控大屏看起来更专业,而是让我们对系统的真实状态有更准确的判断。
一台服务器运行得很稳定,不代表业务链路有弹性;一个 Pod 能够自动重启,不代表用户没有受到影响;一次故障没有导致服务彻底宕机,也不代表系统已经经受住了考验。
设计混沌工程指标时,我更倾向于抓住几个核心问题:故障期间业务损失有多大,影响范围能否被控制,服务多久可以恢复,积压和数据问题能否彻底解决,以及我们能不能用重复实验验证修复效果。
不要拿机器的健康,代替业务的健康;也不要拿故障后的自动恢复,代替真正的弹性。
我们做混沌工程,不是为了证明系统永远不会出问题,而是为了在问题真的发生时,少丢一些订单,少影响一些用户,少让值班的人在凌晨手忙脚乱。
对运维来说,能把这些事情变得可衡量、可验证、可持续改进,才算是真正把弹性做到了系统里。