全链路压测
问题背景
2023年双十一前两周,某电商平台的 SRE 团队信心满满地发布了全链路压测报告:系统在 10 万 QPS 下一切正常,P99 延迟稳定在 120ms。
双十一零点,流量峰值冲到 15 万 QPS(比压测时高 50%)。系统在第 7 分钟开始出现连锁反应:Redis 连接池耗尽 → 商品服务超时 → 订单服务降级 → 支付服务雪崩。20 分钟后系统才逐步恢复。
事后复盘发现两个关键问题:
- 压测时的 Redis 集群是新扩容的节点,连接数上限比生产环境低 40%,但压测报告没有记录这个差异
- 压测流量只有"下单"一个链路,但生产流量中"查询订单"占了 60%,查询链路的并发度被严重低估
这次事故说明:单链路压测不够,全链路压测做错了方向。
【架构权衡】
全链路压测的成本很高——需要压测平台、影子库、专门的压测时间窗口。建议先用单链路压测定位瓶颈,全链路压测作为上线前的最终验证。把钱花在刀刃上:与其做一次覆盖全链路的"面子工程"压测,不如做 10 次精准定位瓶颈的单链路压测。
问题定义
全链路压测(Full-Link Pressure Testing)是对整个请求链路(从入口网关到后端数据库)进行真实的流量压测,验证系统在生产环境或接近生产环境的压力下的真实表现。
它要回答的问题:
- 当前系统能承受的最大 QPS 是多少?
- 在峰值流量下,P50/P90/P99 延迟是多少?
- 哪个子系统是瓶颈?
- 扩容方案是否有效?
graph TD
A[压测流量构造器] -->|构造并发用户| B[压测请求]
B --> C[网关层]
C --> D[应用服务链]
D --> E[数据库层]
D --> F[缓存层]
D --> G[消息队列]
C -->|隔离| H[影子库]
D -->|隔离| H
E -->|隔离| H
subgraph 数据隔离
I[影子库<br/>压测数据独立]
J[影子表<br/>表名前缀标识]
K[流量染色<br/>header标记]
end
为什么单链路压测不够
单链路压测只测试单个服务或子系统。但系统性能取决于整条链路中最薄弱的一环。
graph LR
subgraph 单链路压测["单链路压测(不够)"]
A1[服务A: 10万QPS] --> B1[服务B: 8万QPS]
B1 --> C1[服务C: 12万QPS]
A1 --> B1
end
subgraph 实际表现["实际链路表现"]
A2[服务A: 10万QPS] --> B2[服务B: 8万QPS]
B2 --> C2[服务C: ?]
A2 --> B2
end
Note over B2: 实际QPS被限制在8万<br/>瓶颈在服务B
木桶效应:系统的最大吞吐量由最短板决定。单链路压测可以找到每个服务的瓶颈,但无法验证整条链路在叠加压力下的真实表现。
全链路压测的实现原理
核心组件
graph TD
subgraph 流量构造["流量构造层"]
A[流量构造器<br/>JMeter/Gatling/自研]
B[流量回放工具<br/>TCPCopy/goreplay]
end
subgraph 流量标记["流量标记与隔离"]
C[流量染色<br/>Header: X-Test-Request: true]
D[影子库/影子表<br/>test_xxx]
E[Mock服务<br/>外部依赖模拟]
end
subgraph 执行层["压测执行层"]
F[压测任务调度<br/>定时/立即执行]
G[施压集群<br/>分布式JMeter节点]
end
subgraph 数据采集["数据采集层"]
H[Prometheus<br/>Metrics采集]
I[Jaeger<br/>Trace采集]
J[日志采集<br/>Elasticsearch]
end
A --> C
B --> C
C --> F
F --> G
G --> H
G --> I
G --> J
1. 流量标记(流量染色)
在请求 Header 中注入压测标记,整个链路透传这个标记:
// 网关层注入压测标记
@Component
public class TestTrafficFilter extends ZuulFilter {
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
// 如果是压测请求,注入标记
if (isTestTraffic()) {
ctx.addZuulRequestHeader("X-Test-Request", "true");
ctx.addZuulRequestHeader("X-Test-Timestamp", String.valueOf(System.currentTimeMillis()));
}
return null;
}
}
// 业务层识别压测流量
public class TestTrafficInterceptor {
public boolean isTestTraffic(HttpServletRequest request) {
String testFlag = request.getHeader("X-Test-Request");
return "true".equals(testFlag);
}
}
2. 影子库(Shadow Database)
压测数据写入独立的数据库,避免污染生产数据:
@DataSourceRouter
public class DataSourceSelector {
public String selectDataSource(JoinPoint point) {
// 如果是压测流量,切换到影子库
if (TestTrafficContext.isTestTraffic()) {
return "shadow-datasource";
}
return "primary-datasource";
}
}
// 影子库表命名规范
// 生产表: orders
// 影子表: orders_test 或 shadow_orders
3. Mock 外部依赖
第三方支付、短信网关等不可压测的外部服务需要 Mock:
@ConditionalOnProperty(name = "test.mode", havingValue = "true")
@Service
public class MockPaymentService implements PaymentService {
@Override
public PaymentResult pay(PaymentRequest request) {
// 模拟支付成功,延迟 50~100ms
try {
Thread.sleep(50 + new Random().nextInt(50));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return PaymentResult.success(
"TEST_TXN_" + UUID.randomUUID().toString().substring(0, 8),
request.getAmount()
);
}
}
数据隔离方案
数据隔离是全链路压测中最核心也最容易出错的部分。
graph TD
subgraph 生产环境
P1[生产库] --> P2[orders表<br/>1000万数据]
P1 --> P3[users表<br/>500万数据]
end
subgraph 影子库
S1[影子库] --> S2[orders_test表<br/>10万数据]
S1 --> S3[users_test表<br/>5万数据]
end
subgraph 隔离策略
T1[流量染色] --> T2[路由到影子库]
T1 --> T3[或路由到影子表]
end
【架构权衡】
不建议使用生产数据做全链路压测。即使做了脱敏,生产数据的字段分布、关联关系和真实压测场景也有差异。更推荐的做法是:构造一个和生产数据结构相同但规模更小的影子数据集(通常为生产数据的 1%~10%),通过影子库或影子表隔离存储。
压测场景设计
基准测试
确定系统在正常负载下的性能基线。
场景:基准测试
目标:获取系统在 1万 QPS 下的 P50/P90/P99 延迟
持续时间:30分钟
预热时间:10分钟
峰值测试
模拟大促、秒杀等流量峰值。
场景:峰值测试
目标:验证系统在 15万 QPS 下的表现
施压策略:阶梯加压(5万 → 10万 → 15万 → 20万,每阶段5分钟)
终止条件:P99延迟 > 500ms 或 错误率 > 1%
异常测试
验证系统在故障场景下的降级和恢复能力。
graph TD
A[基准压测] --> B[注入故障: 杀一个Redis节点]
B --> C[观察: 降级是否生效]
C --> D{系统稳定?}
D -->|是| E[继续加压]
D -->|否| F[记录问题<br/>停止压测]
E --> G[注入故障: 数据库主从切换]
G --> H[观察: 切换是否平滑]
H --> I[继续...]
F --> J[输出故障场景测试报告]
E --> K[输出峰值测试报告]
容量规划
从压测数据推导机器数量,是全链路压测的核心价值之一。
核心计算公式
最大支持QPS = 单机QPS × 机器数量 × 安全系数
安全系数 = 0.7(预留30%的冗余容量)
单机 QPS 推导
假设压测结果:
- 5台机器支撑 10万 QPS
- 单机 QPS = 2万
- P99 延迟 = 120ms
则:
- 目标 20万 QPS → 需要 20万 / 2万 × 1.43 ≈ 15台机器
分层容量规划
graph TD
A[目标: 20万 QPS] --> B[网关层]
B --> C{需要多少网关节点?}
C --> D[单节点承载: 5万QPS<br/>安全系数: 0.7<br/>→ 需要: 6台]
A --> E[应用层]
E --> F{需要多少应用节点?}
F --> G[单节点承载: 1万QPS<br/>安全系数: 0.7<br/>→ 需要: 29台]
A --> H[缓存层]
H --> I{需要多少Redis节点?}
I --> J[单节点QPS: 30万<br/>评估连接数瓶颈<br/>→ 至少2节点]
A --> K[数据库层]
K --> L{需要多少DB节点?}
L --> M[主从架构<br/>写: 1主1从<br/>读: 2只读从库]
全链路压测的风险
- 对生产环境的影响:即使有数据隔离,压测流量仍会占用网络带宽、连接池、CPU 等资源。压测时间窗口要避开业务高峰期。
- 第三方依赖被压垮:如果压测流量打到了真实的短信、支付网关,会产生真实费用或触发风控。
- 数据残留:压测数据如果清理不彻底,可能在生产环境留下垃圾数据。
- 监控误判:压测期间的大量超时/错误可能被监控误判为真实故障,触发不必要的告警。
⚠️
全链路压测不要在生产环境做,除非你已经做好了完整的隔离和应急回滚预案。推荐在预发布环境(灰度环境)进行,该环境与生产环境配置相同但不处理真实流量。
工程代价
【架构权衡】
全链路压测的价值不在于"测出了多少 QPS",而在于发现那些单链路压测无法发现的系统性瓶颈——比如连接池配置不合理、线程池饥饿、热点 key 竞争、跨服务调用链路过长等等。这些问题只有在全链路叠加压力下才会暴露。
生产避坑
- 压测前必须检查依赖:确认所有外部依赖(支付、短信)已 Mock,确保不会产生真实费用。
- 压测数据必须脱敏:用户手机号、身份证号、银行卡号等敏感信息必须脱敏或使用虚构数据。
- 压测时关闭定时任务:压测期间的定时任务(如数据清理、报表生成)可能与压测流量产生干扰。
- 压测后必须清理:压测结束后立即清理影子库中的测试数据,避免积累垃圾数据影响后续压测准确性。
- 压测结果要可重现:记录压测环境的精确配置(机器规格、网络拓扑、依赖版本),下次压测才能对比。
落地 Checklist