压力测试全绿,线上照样崩——这种事我遇到过不止一次。
负载工具把 QPS 打到顶峰,CPU、内存、连接池全部在安全水位,RT 曲线平得像一条直线。可等到真实用户涌进来,服务器照样卡死、报错、雪崩,运维群里一片混乱。反复复盘之后,我逐渐意识到一个被忽略的变量:用户不是负载工具里的虚拟线程,用户有情绪。用户情绪会影响系统稳定性,这句话听起来像玄学,但落到技术上,它意味着完全不同的流量特征和故障模式。今天这篇就聊聊压力测试之外的东西:情绪是怎么变成系统压力的,以及系统层面能做些什么。
1. 一次“全绿”压测后的线上事故
1.1 事故现场还原
先讲一个某电商平台的案例。大促当天下午,订单接口突然超时率飙升,网关层开始大量 504。当时团队的第一反应是“流量超预估了”,可监控面板显示 QPS 并没有超过压测水位,甚至只有压测值的七成。
诡异的是,数据库 CPU 也不算高,Redis 的命中率也稳定,但从应用日志看,同一个订单号、同一个用户 ID 的请求在几秒内重复出现了十几次。单看 QPS 不夸张,但看“真实用户失败重试”这个维度,请求量被放大了将近一倍,而且大量请求打在了库存扣减这种需要加锁的写路径上。
这次事故最后定位为:商品详情页出现价格显示异常,用户反复刷新、反复提交订单,产生了大量的重复写请求和锁等待。压测时我们的脚本是匀速打流,但真实用户是带着情绪在点击,他们的行为模式和压测模型完全不一样。
1.2 复盘发现的“情绪盲区”
事后把监控数据拉出来看,有几个数字很有意思。详情页接口的 P99 延迟从正常的 80ms 拉高到 3s,但 P50 一直很稳定——也就是说,大部分请求是快的,但有一小撮请求被某条慢链路拖死了。这些“慢请求”来自同一个特征群体:他们在一个页面上反复操作,每次操作都在等待,等待期间又发出了更多请求。
我们管这个叫“情绪放大效应”。一个正常用户一次购买行为,大概产生 20 个请求;但一个愤怒的用户在抢购、付款失败、页面卡顿的场景下,一次行为可以产生 100 个以上的请求,且请求高度集中,还自带重试和并发。更麻烦的是,这一类用户一旦遇到错误提示,会更频繁地重试,而不是停下来退出。
传统的压力测试设计了峰值、毛刺、阶梯加压,但很少设计“用户反复点同一个按钮”这种场景。负载测试模拟的是理性的并发用户,而现实里的用户会焦虑、会愤怒、会恐慌,这些情绪直接转化为特有的流量模式。情绪是压测模型之外的一个维度,我把它称为“情绪流量”。
2. 用户情绪为什么能击穿系统防线
2.1 非理性行为的技术特征
把情绪翻译成技术语言,其实可以拆成几种可观察、可模拟的行为模式。
第一类是焦虑式刷新。用户打开页面发现没反应,第一反应就是刷新,刷新不行就退出重进。这类行为对应到技术上就是高频、偶发、全路径的重复请求,会绕过前端缓存,打穿 CDN 或者 SSR 服务。
第二类是愤怒式重试。遇到报错、超时、验证码失败,用户的典型行为是狂点按钮。前端如果没有做防抖,后端如果没有做幂等,同一个写操作就可能以每秒几次的频率重复提交。重试这种行为的可怕之处在于,它会和超时时间形成共振:请求超时了,用户立刻重试;重试请求又超时,用户再重试,同时前一个请求还没完全断开——连接池、线程池、数据库连接就这样被占满。
第三类是恐慌式扩散。部分平台故障期间,用户会去社交平台发帖、去客服通道投诉,同时反复尝试刷新。这种“外部渠道回流”会在短时间内带来额外的流量尖峰,而且这些流量往往集中在出问题的接口上,形成局部热点。
我把这些行为统称为“用户情绪的流量投影”。它们不是正常的业务流量,也不可预测,但可以被设计防御。
2.2 情绪流量的放大效应
情绪流量和普通流量最大的区别在于放大系数。压测里的虚拟用户是克制的:每个用户按脚本执行,请求间隔均匀、请求内容固定、失败后按预期退出或重试。而真实用户在情绪之下,请求量可以放大的倍数远超想象。
我实测过一个典型场景:一个支付结果查询接口,正常用户每笔订单只查一次;但当支付结果延迟超过 2 秒时,用户会在一分钟内查询 20 到 30 次。某支付系统高峰期,这类查询请求占了总流量的四成,而其中一半来自重复查询。查询类接口还好,如果是下单、锁库存、发消息这类写操作,放大倍数的破坏力是几何级数的。
更要命的是,情绪流量往往是瞬间到达的。正常用户行为是泊松分布,情绪用户的请求则更像尖峰脉冲——几百个愤怒用户可以在同一秒内同时点击,直接把系统打到峰值。这些尖峰从监控上看起来像流量突刺,但单看数字你根本分不清是正常的集中访问还是情绪爆发。
情绪放大效应是压力测试模型没有覆盖、但真实环境中杀伤力最强的东西。要处理它,不是提高系统容量就行,得从流量特征入手。
3. 情绪压力的技术放大机制
3.1 重试风暴与雪崩链路
情绪流量进入系统后,如果系统本身有超时重试机制,问题会进一步恶化。很多基础组件默认开启重试:HTTP 客户端超时会重试一次,RPC 框架连不上会换个节点再试,消息队列消费失败会重投。正常情况这套机制是可靠性保障,但在流量异常时,它成了放大器。
举个具体的链路:前端用户狂点 → 网关转发超时 → 网关自动重试一次 → 后端线程池排队 → 等待线程超时 → 前端再次刷新。这个链路下,原本一个请求会变成四五个请求,而每个请求都在消耗线程和数据库连接。
当系统开始出现超时,用户的重试加上框架的重试,会在短时间内把后端打成一个“重试放大器”。这时系统的表现不是慢慢变差,而是断崖式雪崩。我曾经遇到过一个案例,一个慢 SQL 把某一个接口的平均响应时间拖到 3 秒,结果该接口的请求量自动翻了三倍,之后整个应用的线程池被打满,连健康检查都过不去,服务直接被摘掉。
重试风暴之所以可怕,是因为它把“系统变慢”这个结果,变成了“系统过载”的原因。这是情绪压力触发、技术机制放大的典型路径。
3.2 热点与排队:情绪用户的“聚光灯效应”
日常流量分散在不同的商品、不同接口上,系统可以从容处理。但情绪流量有一个显著特征:聚光灯效应——所有愤怒的用户都盯着同一个东西。
热门的秒杀商品、刚出现故障的支付渠道、突然无法访问的登录入口,这些“舆论焦点”会吸引大量用户集中操作。于是热点数据出现,缓存击穿、分布式锁竞争、数据库行锁等待,全都跟着来了。
我之前排查过一个抢票系统的问题:某个热门场次的余票查询接口,缓存失效的瞬间,几千个请求同时穿透到数据库。平时这个接口 QPS 不高,数据库完全没压力;但热点场景下,同一行数据被几千个线程同时查询,数据库连接池瞬间耗尽。
热点问题在压测中很难暴露,因为压测的流量模型往往是均匀的。但在真实世界里,一个社会热点、一个故障通告,就能把流量集中在少数几个 key 上。情绪把用户的注意力集中,系统则在这种集中面前暴露出所有并发弱点。
3.3 监控与告警的“自激振荡”
还有一个更隐蔽的因素:监控系统本身会放大情绪压力的影响。
当用户开始遇到错误,错误率升高,监控告警触发,值班人员上线,登录线上环境查看,执行各种排查操作。这些排查动作本身会产生额外负载——重放日志、抓包、执行诊断 SQL、连接线上 Redis。在系统已经过载的情况下,运维人员的排查操作会像一个外部的“并发用户”一样,继续给系统施加压力。
更典型的是告警风暴:某个错误率指标触发告警,告警引起恐慌,更多人同时上线看问题,而这可能会触发更多的诊断任务和链路追踪采样,把系统的性能进一步拉低。
这就形成了一个自激振荡回路:系统慢了 → 用户重试 → 错误率上升 → 告警触发 → 人工介入 → 系统更慢。压测阶段没人会模拟“运维人员上线查问题”这个场景,但这个场景在情绪事件中几乎必然发生。
4. 面向情绪的系统设计
4.1 快速失败:别让用户把请求挂在半空
既然情绪无法避免,系统设计就必须从“尽力成功”转向“尽快失败”,这里有一句我反复强调的话:在系统过载时,最快的成功是快速失败。
很多系统的问题不是能力不够,而是不愿意承认自己不行。下游超时了,请求还在队列里傻等;连接池满了,新的请求还在排队;数据库锁等待超时了,线程还在阻塞。这些“半死不活”的状态,比直接报错更消耗资源,因为用户的请求没有立刻返回,用户就会重试,从而带来更多负载。
正确的做法是分级超时控制:网关层设置一个总超时时间,内部 RPC 调用设置更短的超时时间,比如外部调用限制在 800ms,数据库操作限制在 1s。一旦超时,立刻返回明确错误,而不是继续等待。这样请求被快速“杀掉”,线程和连接被释放,用户的后续重试至少不会叠加在已经卡死的请求之上。
快速失败还会带来一个额外好处:错误信息可以及时反馈给用户,用户的等待时间变短,情绪反而会更稳定。我见过很多线上事故,用户卡在一个转圈页面 10 秒没有任何反馈,最终愤怒地开始狂点刷新;而同样情况下,如果 2 秒内明确告诉用户“系统繁忙,请稍后再试”,大部分用户会离开而不是继续施压。
4.2 安抚式反馈:排队页面与预期管理
技术手段之外,用户体验层面的“情绪降级”也很关键。
一个典型的失败页,比如“500 Internal Server Error”,会让用户感到不知所措,随之而来的就是反复刷新。而一个好的系统,在过载时会展示明确的排队状态、预计等待时间、或者一个可操作的“稍后提醒”按钮。
排队机制的应用场景很广:秒杀场景的排队、故障场景的安抚页、大促高峰期的等待室。它们的技术本质是把瞬时流量转化为平滑的异步流量。用户进入等待队列,前端轮询查询排队状态,而不是直接请求核心业务接口。
这样做的好处有几个:第一,核心系统的压力可控,系统的请求速率由排队器决定,而不是由用户的情绪决定;第二,用户的等待有了预期,不再是无尽的转圈,用户的重试行为会减少;第三,排队页本身就是一层天然的熔断墙,当系统恢复时,再放行等待中的用户。
设计排队页面的技术细节是:排队状态查询接口要独立部署,不能和后端业务共享线程池。否则用户疯狂查询排队状态,会把排队系统本身也打挂,那就真的陷入了死循环。
4.3 防重复与幂等设计
情绪流量最危险的部分是写请求重复提交。所以,幂等设计不是“加分项”,而是系统的“安全底线”。
接口幂等有几个层面。前端层面,提交按钮提交后立即置灰并附带 loading,防止用户重复点击。但前端防不住暴力刷新、客户端重放、或者绕过页面的请求,所以后端必须兜底。
后端幂等的核心是幂等键。客户端生成一个唯一的业务请求 ID,服务端在收到请求后先检查这个 ID 是否已处理过;处理过则直接返回之前的结果。Redis SETNX 是最常见的实现方式:请求进来,先去 Redis 里尝试写入幂等键,写成功则继续处理,写失败则说明已有相同请求在处理或已完成。
库存扣减这类关键操作,除了接口幂等,还要在数据层面做防重。例如,扣库存的 SQL 加上条件判断stock > 0,操作记录表加唯一索引,保证同一个订单号只能成功一次。防重做得好,用户再怎么狂点,系统也不会状态错乱。防重做得不好,一次误操作就能把库存扣成负数,引发更大范围的数据事故。
4.4 软降级与兜底体验
系统承载力终归是有限的,总有扛不住的时候。这时候考验的是“当系统不行时,怎么让用户体面退场”。
降级设计分几个层次。功能性降级:用户头像、推荐位、搜索联想这类次要功能可以先挂掉,保证主链路可用。渲染降级:页面上的动态模块如果失败,可以渲染缓存版本或静态兜底内容,不能出现整页白屏。接口降级:核心接口超时后,可以不返回完整数据,而是返回简化的静态版本,让用户还能“用起来”,而不是报错。
还有一种降级叫请求范围收敛。比如一个查询接口的正常逻辑要查 6 张表,高负载时可以改成只查 2 张核心表,返回精简数据。这种“牺牲完整性、保住可用性”的思路,在情绪压力场景下效果非常明显。
兜底体验的设计也很容易被忽视。系统高负载时的错误提示,不要只写“系统繁忙”,还要引导用户下一步做什么。比如“当前访问人数过多,请 5 分钟后再试”要比“系统错误”好得多;提供一个“订阅恢复通知”的选项,可以避免用户反复刷新。
情绪在系统层面的本质是“失去预期”。如果系统能持续给用户一个“我很忙但我没有坏”的信号,用户的重试行为会大幅减少。安抚式设计不是产品经理的事,它直接影响系统稳定性。
5. 把情绪量化:情绪感知与情绪压测
5.1 情绪指数怎么做
情绪流量虽然行为复杂,但可以被量化。我的做法是把情绪做成一套可监控的指标体系,叫做“用户焦虑指数”。
前端埋点采集几个信号:按钮重复点击次数、输入框重复提交次数、页面刷新频率、同一会话的错误弹窗次数、页面停留时间与滑动速度等。这些信号综合起来,可以算一个前端情绪分数。
后端采集另外几个信号:重试请求占比、同一个用户 ID 的请求频率、非 2xx 响应占比、幂等键重复命中率、超时请求占比。其中“幂等键重复命中率”是一个非常敏感的情绪指标——用户如果疯狂点提交,这个数值会瞬间升高。
把这些指标汇总后,系统可以按分钟粒度生成一个“情绪水位”。正常业务场景下情绪水位在 10% 以下;超过 30%,说明有部分用户处于焦躁状态;超过 60%,预示着可能有重试风暴的风险。情绪水位不是用来精确判断用户心理的,而是用来提前发现“用户行为模式突变”的信号。
情绪感知的目的不是给用户做心理分析,而是给系统发出早期预警:当用户行为开始异常时,系统需要提前进入防御模式,而不是等到线程池被打满才反应。
5.2 情绪压测场景设计
如果说传统压测模拟的是“理性的用户”,那么情绪压测模拟的就是“不理性的用户”。情绪压测场景不是在原有压测模型上简单提高 QPS,而是要构造特殊的行为模式。
我建议在常规压测之外,增加几类情绪场景。第一类:尖峰重试场景,模拟大量用户在 10 秒内同时刷新页面,并对同一写接口重复提交,观察幂等和限流是否生效。第二类:超时恶化场景,人为制造慢下游,看系统在响应变慢时是否触发快速失败和降级,而不是无限排队。第三类:热点穿透场景,把流量集中在少数几个 key 上,看缓存击穿时数据库是否能扛住。第四类:监控自激场景,在系统已经高负载的情况下,模拟运维排查行为,确认排查操作不会给系统带来二次伤害。
这类压测的目的不是测试系统在最优状态下的极限容量,而是测试系统在“最坏的心理状态”下能否有尊严地失败。压测报告除了“最高支持多少 QPS”之外,还应该多一行数据:“情绪压测下,系统是否能保持快速失败、不会雪崩、数据不会错乱。”能扛住情绪压测的系统,抗真实事故的能力才会强。
5.3 压测之外的容量水位
还有一个常被忽略的点:容量规划必须预留“情绪余量”。
很多团队把系统容量打到 80% 才告警,这在平稳流量下没问题,但在情绪事件下,80% 的水位可能瞬间失控。因为情绪流量不是均匀分布在所有节点上的,它往往集中在某些特定节点,比如某个热门商品详情页所在的容器实例。同一个服务下的不同实例,负载可能相差好几倍。
所以,我的建议是核心链路的容量水位控制在 60% 以下,留出 30% 以上的缓冲区间给情绪尖峰。同时,针对可能导致情绪集中的业务模块(抢购、支付、登录),单独做容量规划,而不是靠整体均值去评估。你要问自己的问题是:当全系统 60% 负载时,某个热门模块突然涌入 5 倍流量,它的独立容量是否扛得住?
容量不是平均数游戏,是短板游戏。情绪流量恰恰最擅长攻击最短板的那块板。
6. 常见问题与排查技巧实录
6.1 常见问题速查
| 现象 | 情绪层面的根因 | 技术排查要点 | 预防方案 |
|---|---|---|---|
| QPS 不高但错误率飙升 | 用户集中重试,写请求被放大 | 查看幂等键命中率、重试请求占比 | 快速失败 + 幂等设计 |
| 接口 P99 高但 P50 正常 | 少数热点用户拖慢整体 | 分析慢请求特征、是否集中在少数 ID | 读写分离 + 热点缓存 |
| 线程池满但连接数不高 | 请求排队阻塞,线程得不到释放 | 查看队列深度、超时分布 | 分级超时 + 快速失败 |
| 数据库锁等待严重 | 同一行数据被高频更新 | 查看慢 SQL、热点行、行锁等待 | 异步化 + 数据拆分 |
| 缓存穿透导致回源压力 | 热点数据被集中请求 | 查看缓存命中率、key 分布 | 本地缓存 + 布隆过滤器 |
| 告警风暴加剧故障 | 排查操作叠加到过载系统上 | 查看排查期间负载变化 | 告警收敛 + 只读复本 |
6.2 实战排查心得
情绪防御做得不好的系统,在故障时通常有一个共同特征:错误反馈不明确。用户不知道发生了什么,就只能靠反复操作来试探。所以排查情绪相关问题时,第一个要查的是“用户在界面上看到了什么”,而不是“后端到底发生了什么”。
线上实操时,遇到事故我第一件事是看网关层面的“客户端重试比例”。如果这个数值在快速上升,基本可以断定用户情绪正在恶化。第二步是看“幂等键重复命中率”,如果这个指标高,说明用户正在疯狂提交写请求,这时必须立刻开启网关限流或直接进入降级模式。第三步才是定位后端资源瓶颈。
还有一个行之有效的经验:给系统设计一个“急刹车”开关。平时这个开关是关闭的,一旦情绪水位异常飙升,紧急值班人员可以一键进入“限流模式”:非核心接口全部拒绝、核心接口排队、写请求强制幂等校验、静态资源全部走 CDN。这个开关比临时改配置、拆代码快得多,在真实事故中能救命。
6.3 最后再分享一个小技巧
从我个人的实践来看,情绪感知这套体系不需要一开始就很复杂。先从前端埋点开始,把“重复点击次数”和“同一会话报错次数”这两个指标采集起来,后端把“幂等键重复命中率”和“重试请求占比”加到监控大屏上。不需要构建精确的用户心理模型,只需要感知到“用户行为突变”这个信号就够了。
情绪流量是一种必然存在、但很少被纳入传统压测模型的流量形态。它不可预测、突然爆发、自带放大器,但它的破坏路径是有规律的:焦虑导致刷新,刷新导致请求放大,请求放大导致过载,过载导致超时,超时导致更多重试。只要把这些规律拆解出来,系统完全可以在设计层面做出针对性防御。
我在做过几次线上事故复盘之后,现在评估系统稳定性时一定会额外问一个问题:当用户生气的时候,这套系统还能不能保持住底线?这个问题,比最高 QPS 能扛到多少,更能反映一个系统的真实韧性。