全链路压测:从影子库到流量染色的实战 — editorial cover photo

全链路压测:不压一遍,你怎么知道大促会不会挂

大促前的生死线,全链路压测用真实流量模型压满生产,提前暴露所有瓶颈。

2025年7月21日·返回文章列表
文章大纲

大促前的容量评估单靠单接口压测是远远不够的。你把下单接口压到两万 QPS,单看数字漂亮,但这只能证明这个接口本身扛得住。真正的瓶颈从来不在单点上,而是藏在服务间的调用链路里。数据库连接池够、缓存够、单个服务 QPS 够,但链路上某个不起眼的依赖被打挂,整条链路会一起瘫痪。单接口压测发现不了这种链路级瓶颈,只有用真实流量把整条链路压满,问题才会暴露出来。

全链路压测就是干这件事的,用还原过的真实流量模型,在生产环境上把整条业务链路压到预期峰值,提前把大促当天才会爆的雷现在就踩响。这套东西做起来有两个绕不开的硬骨头,一个是压测流量绝不能污染生产数据,一个是染色标记必须全链路一滴不漏地透传。这两件事任何一件做砸,全链路压测就变成一场生产事故。

核心矛盾,要在生产环境跑,又不能碰生产数据

全链路压测必须在生产环境跑,因为只有生产的环境、生产的数据规模、生产的依赖拓扑,才能还原真实的瓶颈。但生产环境跑压测,最大的红线是压测数据绝不能污染生产库。压测订单不能进真实订单表,压测支付不能真扣钱,压测用户不能出现在真实的用户体系里。一旦压测数据混进生产,对账会乱、报表会错、真实用户的体验会被污染。

解法是给压测流量打一个标记,系统里每个环节都识别这个标记,把压测数据路由到一套隔离的影子存储里,和生产数据物理分开。这个思路简单,难在执行,因为标记要在整条链路的所有调用方式下都正确透传,漏一处就污染一处。

流量染色,标记的全链路透传

PlantUML
正在加载图表…
PlantUML diagram

图:压测染色标记从网关入口打入,经 HTTP/RPC/MQ/线程池各自载体全链路透传,最终路由到影子存储,漏一处即污染生产库

压测请求在入口网关被打上一个标记,常见的做法是往 HTTP header 里塞一个 x-pt-flag=true。从这个入口开始,这个标记要在整条链路里一路透传下去,经过网关、各业务服务、RPC 调用、MQ 消息、异步线程池,每个环节都得把这个标记认出来再传给下一个环节。

不同调用方式透传的载体不一样。HTTP 靠 header,Dubbo 这类 RPC 靠框架的 attachment 上下文,MQ 消息靠消息属性里带一个压测标记字段,异步线程池靠 ThreadLocal 加任务上下文显式传递。每种调用方式都得单独适配一遍,这是全链路压测框架最重的工程量。

最容易漏的是异步和跨线程的场景。一个同步请求里带了标记,进到一个服务后丢到线程池里异步处理,ThreadLocal 在线程切换时会丢,标记就没了,这个异步任务写的数据就会落到生产库。所以跨线程的地方必须用显式的任务上下文把标记传递过去,不能依赖 ThreadLocal 的隐式继承。MQ 消费也一样,生产端带了标记,消费端得在反序列化时把标记恢复到当前上下文,否则消费侧处理的数据又混进生产。

染色标记漏传是压测数据污染的根因

全链路压测翻车,十次有九次是某个异步任务或者某个 MQ 消费者忘了带标记,压测数据悄悄混进了生产库。这种污染很难当场发现,往往是对账时才看出来。框架层要把标记透传做成默认行为,业务代码无感知,而不是让每个开发者自己记得传。每接入一个新的中间件或者新的异步框架,都得第一时间补上标记透传的适配。

影子存储,物理隔离压测数据

PlantUML
正在加载图表…
PlantUML diagram

图:影子存储三种方案——影子库独立实例最彻底但成本高、影子表同库加后缀成本低、影子 Redis 加 pt_ 前缀隔离 key 空间

识别了压测标记,下一步是把压测数据路由到独立的影子存储。常见的几种方案各有取舍。

影子库是起一套独立的 DB 实例,压测数据完全物理隔离,最彻底,但成本高,得起一整套和生产同规格的实例。影子表是在同一个库里给压测数据单独建表,表名加一个 _pt 后缀,比如 order_pt。成本低,但压测流量还是占用了生产 DB 的 CPU 和 IO,压测本身可能把生产库压慢。影子 Redis 是给压测的 key 加一个 pt_ 前缀,独立一套 key 空间,避免覆盖生产缓存。

成熟的做法是数据库用影子库彻底隔离,缓存用前缀隔离,MQ 用独立的压测 topic 或者压测消费者组。路由的判断要下沉到数据访问层,由中间件或者 ORM 拦截器识别染色标记自动切到影子存储,业务代码完全不感知。

public Order createOrder(Order order) {
if (PtContext.isPressureTest()) {
return shadowOrderMapper.insert(order);
} else {
return orderMapper.insert(order);
}
}

这段判断逻辑不该散落在每个业务方法里,应该收敛到数据访问中间件统一拦截。ShardingSphere 这类中间件就支持基于上下文自动路由影子库,业务代码只管调 insert,中间件根据上下文决定写哪张表。

流量模型,真实才有意义

压测流量必须模拟真实用户行为,否则测出来的容量数字是假的。大促当天的流量不是均匀的,用户操作是有比例的,浏览商品占大头,加购次之,下单支付是少数,退款更少。一个粗略的模型大概是浏览七成、加购两成、下单支付不到一成、退款百分之一二。

这个比例要基于历史大促的真实数据来还原,不能拍脑袋。比例失真会导致容量评估偏差。比如只压下单接口不压浏览接口,可能漏掉浏览接口在真实流量下的瓶颈,而大促当天浏览接口往往是 QPS 最高的。流量模型还得包括用户的前后操作路径,比如加购之后多久下单,下单之后多久支付,这些时间分布会影响链路上各服务的并发压力。

数据准备也要贴近真实。压测用的商品库存、用户账户余额、优惠券,这些状态数据要足够支撑压测流量的消耗,不能压到一半因为压测账户余额不足而大面积失败,这种失败和容量瓶颈无关,会污染压测结论。

四个阶段把容量摸清楚

PlantUML
正在加载图表…
PlantUML diagram

图:全链路压测四阶段递进——基线找拐点、容量验证目标水位、稳定性长跑抓慢性病、极限压到崩溃找真实上限

压测不是一次性跑一个数字,是分阶段递进的。

基线压测先做,单接口或者核心链路逐步加压,找到响应时间开始剧增的那个拐点。拐点之前服务是线性的,拐点之后性能急剧恶化。这个拐点告诉你的是一条临界线,过了它再往上压,系统随时会崩。

容量压测压到预估的目标 QPS,看整条链路能不能在这个水位上稳定运行。这一步验证的是容量规划成不成立。稳定性压测是在目标 QPS 上持续跑几个小时甚至十几小时,专门抓内存泄漏、连接泄漏、日志撑爆磁盘这类慢性病。这些毛病短时间压测发现不了,只有长时间运行才暴露。极限压测是继续加压直到系统崩溃,找到真实的容量上限,这个上限反过来给降级阈值提供依据,知道扛到多少该开始降级。

我在大促压测里见过的典型瓶颈有这么几类。数据库连接池打满,压测暴露的根因通常是某个慢查询把连接占住不释放,连接数配置反而够用。缓存击穿,热点 key 在压测期间过期,瞬间大量请求穿透到 DB。线程池阻塞,某个下游依赖变慢,调用它的线程池被占满,整条链路雪崩。这些问题单接口压测一个都发现不了,只有全链路真实流量同时跑才会浮出水面。

监控要跟着压测一起看

压测时全链路监控必须开足,应用层的 CPU、内存、GC、QPS、响应时间,依赖层的 DB 和 Redis 连接数和耗时,链路追踪定位慢调用,资源层的网络和磁盘。压测的目的不是把系统压崩,是在压的过程中把瓶颈精确定位出来。

监控要能区分压测流量和生产流量,不然压测期间的真实生产告警会被压测噪声淹没。这就又回到染色标记,监控指标按标记分维度统计,压测的归压测,生产的归生产。这套可观测性是全链路压测能不能沉淀出结论的关键,压完了只剩一堆崩掉的报错,定位不出瓶颈在哪,这次压测就白做了。

Continue Reading

关联文档推荐

查看全部
容量评估:从 QPS 推导到资源规划的全流程 — editorial cover photo多级缓存:从浏览器到 DB 的全链路缓存设计 — vibrant cover for technical blog article读写分离与主从延迟:DB 性能优化的基石 — vibrant cover for technical blog article
作者:archy.shawn
声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。