上篇回顾Day 84 用 Arthas 在不重启 JVM 的情况下五分钟定位生产故障、热修复走紧急发布。但单点诊断解决的是某个接口为什么慢压测要解决的是整条链路在什么量会崩。这篇给你真正生产可用的全链路压测体系。单接口压测最大的陷阱是假绿勾JMeter 把库存接口单机 QPS 压到 8000报告写满绿勾结果大促 0 点刚过库存先崩、支付超时、订单雪崩——压测一个没压到。根子在三点只压了单接口没跑完整链路、用测试数据而非生产真实分布、没有流量回放机制测的是假负载。一、为什么传统压测不顶用新人做压测有个常见误区JMeter 加并发到目标 QPS看响应时间不超过 SLA 就完事了。但生产事故告诉你这种压测至少漏掉了三层信息链路层订单→库存→支付→风控一条链路每一跳超时都会击穿上游。光压单点看不出雪崩链数据层生产 1 亿用户的真实分布 vs 测试 10 个用户的均分布并发锁竞争、缓存命中率、SQL 执行计划全不一样基线层压完看绝对值没用要看和昨天同时段/上周同时段基线对比才知道是代码退化还是真实压力所以真正的全链路压测至少要解决五个问题┌─────────────────────────────────────────────────────────┐ │ 全链路压测完整度模型 │ ├─────────────────────────────────────────────────────────┤ │ 1. 流量来源 生产流量录制回放 ✓ ← 决定负载真实性 │ │ 2. 数据隔离 影子库/影子表 ✓ ← 决定污染风险 │ │ 3. 链路覆盖 入口→下游全链路 ✓ ← 决定雪崩可见性 │ │ 4. 压测集群 10万级QPS模拟 ✓ ← 决定容量上限 │ │ 5. 监控分析 实时拐点TP99 ✓ ← 决定瓶颈定位速度 │ └─────────────────────────────────────────────────────────┘下文挨个讲解。二、影子库压测数据不污染生产的平行宇宙全链路压测第一个要解决的事是压测流量打出的数据不能污染生产。比如下了一笔订单库存要扣、积分要加、支付要生成流水——这些数据跑进生产表就是脏数据第二天对账要你命。影子库Shadow Table / Shadow DB的思路用同一套表结构物理上隔离到独立数据源压测流量走 shadow_ 开关的 shadow 数据源但业务代码 0 改动。Spring 提供了AbstractRoutingDataSource让这事变得非常简单。// ShadowRoutingDataSource.java — JDK 17 Spring Boot 3.3 // 依赖spring-boot-starter-jdbc // 思路基于 ThreadLocal 的 routing key 决定走主库还是影子库 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class ShadowRoutingDataSource extends AbstractRoutingDataSource { // ThreadLocal 存储当前请求是否压测流量true走 shadow 数据源 private static final ThreadLocalBoolean isShadow new ThreadLocal(); public static void enterShadow() { isShadow.set(Boolean.TRUE); } public static void exitShadow() { isShadow.remove(); } public static boolean isShadow() { return Boolean.TRUE.equals(isShadow.get()); } Override protected Object determineCurrentLookupKey() { // 返回 lookupKey 即可Spring 会根据 key 在 targetDataSources 里找对应 DataSource return isShadow() ? shadow : main; } }数据源配置如下主库和影子库同时注册进 Spring 容器靠 ThreadLocal 决定走哪个链路接入点Spring Cloud Gateway 过滤器或 Servlet Filter识别到X-Shadow:# application-stress.yml — 压测环境专用配置 spring: datasource: main: url: jdbc:mysql://prod-db:3306/order?useSSLtrue username: order_app password: ${PROD_DB_PWD} hikari: pool-name: MainHikariCP maximum-pool-size: 50 shadow: # 同构实例物理隔离压测完直接 TRUNCATE无需清理 url: jdbc:mysql://stress-db:3306/order_shadow?useSSLtrue username: stress_app password: ${STRESS_DB_PWD} hikari: pool-name: ShadowHikariCP maximum-pool-size: 100 # 压测需要更大连接池 # 流量染色用 Header 标记 X-Shadow: true网关层识别后写入 ThreadLocal gateway: shadow: header: X-Shadow enabled: truetrue就调用enterShadow()切换// ShadowTrafficFilter.java — JDK 17 Spring Cloud Gateway 4.x // 关键染色与切换必须在最早的位置完成否则下方调用链已开始走主数据源 Component public class ShadowTrafficFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { boolean isShadow true.equalsIgnoreCase( exchange.getRequest().getHeaders().getFirst(X-Shadow)); if (isShadow) { ShadowRoutingDataSource.enterShadow(); } // Reactor 上下文Reactive 场景下 ThreadLocal 会失效需用 Context 传递 return chain.filter(exchange) .doFinally(signal - ShadowRoutingDataSource.exitShadow()); } Override public int getOrder() { return -100; } // 越早越好 }注意一个坑用了 WebFlux/Reactor 之后ThreadLocal 是失效的因为请求处理可能跨多个线程。要么用Reactor Context传递染色标记要么就在 controller 层用阻塞模式MVC。上例是兼容写法演示流程真实生产用Mono.deferContextual才是稳的。为什么这套设计能成立因为业务代码不变业务 SQL 看的是同一个DataSourceBean虽然里面是 RoutingDataSource不知道下面切了哪天不想压测了把 header 拿掉就回主库。三、流量录制把生产真实流量搬到压测压测最大的谎言是我用的测试数据跟生产差不多——其实生产数据分布永远不会跟测试样例一样。真正的解法是录下生产流量去敏化后回放。Java 生态里生产流量录制主流方案有两种Nginx access log 解析回放与JVM 字节码录制如 jvm-sandbox。后者更准能抓到内部 RPC但成本高。我们用前者——网关层 access log 已经记录了 99% 的请求参数// TrafficRecorder.java — JDK 17 // 思路异步批量写文件避免影响生产 RT // 部署位置网关层 Filter每 30 秒刷盘一次 Component public class TrafficRecorder { private final BlockingQueueString buffer new LinkedBlockingQueue(100_000); private final AtomicBoolean running new AtomicBoolean(true); // 配置开启压测的 URL 前缀防止误抓用户隐私接口 Value(${stress.record.path-prefix:/api/}) private String pathPrefix; PostConstruct public void start() { Executors.newSingleThreadExecutor().submit(this::flushLoop); } public void record(String method, String path, int status, long costMs, MapString, String headers, byte[] body) { // 关键去敏化——身份证、手机号、密码一律打码 String sanitizedBody sanitize(body); // 用 JSON Lines 格式每行一个请求 String line String.format( {\ts\:%d,\m\:\%s\,\p\:\%s\,\s\:%d,\cost\:%d,\h\:%s,\b\:\%s\}, System.currentTimeMillis(), method, path, status, costMs, toJson(headers), Base64.getEncoder().encodeToString(sanitizedBody)); buffer.offer(line); } private void flushLoop() { while (running.get()) { try (BufferedWriter w Files.newBufferedWriter( Paths.get(/data/stress/traffic- System.currentTimeMillis()/1000 .jsonl), StandardOpenOption.CREATE, StandardOpenOption.APPEND)) { // 每批最多 1000 条或 1 秒刷一次 for (int i 0; i 1000; i) { String line buffer.poll(1, TimeUnit.SECONDS); if (line null) break; w.write(line \n); } } catch (Exception e) { log.error(flush traffic log failed, e); } } } private byte[] sanitize(byte[] body) { // 正则替换身份证/手机号——务必接入合规团队的脱敏库 String s new String(body, StandardCharsets.UTF_8); s s.replaceAll(\\d{17}[\\dXx], ***ID***) // 身份证 .replaceAll(1[3-9]\\d{9}, ***MOBILE***) // 手机号 .replaceAll(\password\\\s*:\\s*\[^\]*\, \password\:\***\); // 密码 return s.getBytes(StandardCharsets.UTF_8); } }录制文件天然就是压测脚本不用写压测用例。复现 1 万 QPS 的真实负载比写一个等价的 JMeter 脚本可信度高 10 倍。四、压测集群把 JMeter 从单机推到 10 万 QPS单机 JMeter 极限约 2000 并发就到顶了——JMeter GUI 模式下连本机 socket 都会成为瓶颈。要做 10 万 QPS 量级必须把 JMeter 拆成主从模式Slave 集群用 Docker Compose 一键拉起# docker-compose-stress.yml # 启动 50 个压测节点每节点 2000 并发总 10 万 QPS version: 3.8 services: jmeter-controller: image: apache/jmeter:5.6.2 command: -n -t /scripts/order.jmx -R jmeter-slave-1,jmeter-slave-2,... volumes: - ./scripts:/scripts - ./results:/results jmeter-slave: image: apache/jmeter:5.6.2 command: -s -Jserver_port1099 -Jserver.rmi.ssl.disabletrue deploy: replicas: 50 environment: - JVM_ARGS-Xms2g -Xmx2g -XX:UseG1GC # 关键不限制 CPU让一个 Slave 把 CPU 打满看吞吐上限核心 trickJMeter 用jpgc - Throughput Shaping Timer控制 QPS 增速曲线——比如按5 分钟从 0 线性爬到 50000便于找到系统的拐点。最常犯的错直接 2000 并发起跑。结果要么压挂了没留时间分析要么压不出真实承载上限。要先慢后快让系统有时间预热。五、压测监控3 个核心指标 实时告警压测过程你必须在 Grafana 上盯三样东西吞吐量、响应时间分布、错误率。这三者任何一个先崩那个点就是系统瓶颈。我把这三板斧提炼成一套 PromQL 告警# alert.rules.yml — 压测专用告警规则 groups: - name: stress_test_alerts rules: - alert: P99_ResponseTime_TooHigh # 关键TP99 大于 SLA 阈值 expr: | histogram_quantile(0.99, sum by (le, service) (rate(http_server_requests_seconds_bucket{service~.*}[1m])) ) 0.5 for: 2m labels: { severity: critical, stage: stress } annotations: summary: {{ $labels.service }} TP99 已超过 500ms - alert: ErrorRate_Spike # 错误率大于 1% 持续 1 分钟 expr: | sum by (service) (rate(http_server_requests_seconds_count{status~5..}[1m])) / sum by (service) (rate(http_server_requests_seconds_count[1m])) 0.01 for: 1m labels: { severity: critical, stage: stress } - alert: Throughput_Plateau # 关键吞吐量不再增长但并发还在加 拐点已到 expr: | sum(rate(http_server_requests_seconds_count[30s])) sum(rate(http_server_requests_seconds_count[5m])) * 0.95 and on() sum(http_server_requests_active) 1000 for: 3m annotations: summary: 吞吐量已到拐点Throughput_Plateau这条最值钱——当 QPS 不再随并发上升、错误率却开始抬头那就是系统的真实容量上限比测试报告里写满了的字强得多。六、结果分析找拐点比绝对值更重要测试报告里最该写的不是SLA 满足或SLA 不满足而是系统在什么并发下出现拐点。下面是常见的三种拐点形态每种对应不同的根因判断方法把并发数和 QPS 双轴画图曲线斜率突变的点就是拐点。比如拐点在 8000 QPS可能 DB 连接池满扩容数据库或上连接池分库分表拐点在 5000 QPS可能 Redis 大 Key 拉跨看 Redisslowlog get找 TOP3 大 Key拐点在 3000 QPS可能锁竞争严重jstackdump 看 BLOCKED 线程数下面这段代码自动解析 JMeter JTL 文件输出真实的拐点和 P99 报告// StressReportAnalyzer.java — JDK 17 // 解析 JMeter 的 JTL 输出输出系统真实容量画像 public class StressReportAnalyzer { // JTL 文件示例timeStamp,elapsed,label,responseCode,success,bytes,threadName public CapacityProfile analyze(Path jtlFile) throws IOException { ListLong latencies new ArrayList(); MapInteger, AtomicLong qpsBySecond new TreeMap(); MapInteger, AtomicLong errorsBySecond new TreeMap(); Files.lines(jtlFile).skip(1).forEach(line - { // skip CSV header String[] f line.split(,); if (f.length 6) return; long elapsed Long.parseLong(f[1]); long ts Long.parseLong(f[0]); boolean success true.equalsIgnoreCase(f[4]); int secondBucket (int) (ts / 1000); latencies.add(elapsed); qpsBySecond.computeIfAbsent(secondBucket, k - new AtomicLong()).incrementAndGet(); if (!success) errorsBySecond .computeIfAbsent(secondBucket, k - new AtomicLong()).incrementAndGet(); }); Collections.sort(latencies); long p50 latencies.get((int)(latencies.size() * 0.50)); long p99 latencies.get((int)(latencies.size() * 0.99)); long p999 latencies.get((int)(latencies.size() * 0.999)); // 拐点检测找连续 3 秒 QPS 下降超过 10% 但并发还在涨 int knee detectKneePoint(qpsBySecond); return new CapacityProfile(p50, p99, p999, knee, qpsBySecond.values().stream().mapToLong(AtomicLong::get).sum(), (double) errorsBySecond.values().stream().mapToLong(AtomicLong::get).sum() / Math.max(1, qpsBySecond.values().stream().mapToLong(AtomicLong::get).sum())); } private int detectKneePoint(MapInteger, AtomicLong qps) { ListMap.EntryInteger, AtomicLong sorted new ArrayList(qps.entrySet()); sorted.sort(Map.Entry.comparingByKey()); // 找到 QPS 首次下降超过 10% 的位置即视为拐点 long peak sorted.get(0).getValue().get(); for (int i 1; i sorted.size(); i) { long cur sorted.get(i).getValue().get(); if (cur peak * 0.9) { return sorted.get(i).getKey(); } peak Math.max(peak, cur); } return -1; // 没找到拐点 系统完全健康大胆加压 } public record CapacityProfile( long p50, long p99, long p999, int kneeSecondBucket, // 拐点出现的时间桶 long totalRequests, double errorRate ) { Override public String toString() { return String.format( 压测报告 P50: %d ms | P99: %d ms | P99.9: %d ms 总请求数: %d | 错误率: %.2f%% %s 系统拐点: %s , p50, p99, p999, totalRequests, errorRate * 100, errorRate 0.01 ? ❌ : ✅, kneeSecondBucket 0 ? 在第 kneeSecondBucket 秒出现拐点需排查 : 未出现拐点系统健康); } } }跑一遍直接输出形如 压测报告 P50: 38 ms | P99: 412 ms | P99.9: 1890 ms 总请求数: 12487650 | 错误率: 0.32% ✅ 系统拐点: 在第 387 秒出现拐点需排查我们之前双十一那次压测报告从来没填过拐点——只写了系统通过 SLA结果真出问题只能现场手忙脚乱。七、给团队的三条实战建议压测起步从链路幂等性检查开始别从高并发开始。第一次压测只跑 200 QPS验证链路每一步是否幂等、是否有脏数据。压测生产环境一旦出现非幂等写入重则资金损失。比如我们的红包服务第一次压测就丢了一堆红包——后端用了INSERT ... ON DUPLICATE KEY UPDATE并发下却调用了两遍无可挽回。所以建议先低并发慢压观察数据库增量确认业务幂等再上量。一定要做减压测试不只做加压测试。95% 的团队只测加压时会不会挂但生产事故 30% 发生在流量回退阶段——双十一结束后流量从 50 万 QPS 急速回落到 5 万 QPS缓存预热、连接池收缩、JVM 触发 GC……一系列连锁反应可能造成服务震荡。建议每轮压测最后加个5 分钟降到 0阶段。建立双链路对比基线而非绝对值基线。系统每周都会因为依赖升级、配置变更、SQL 微调导致性能漂移。每月固定时间做一次标准压测5000 QPS维持 5 分钟把 P99、错误率、CPU/内存画像入库上线发版后跑同样测试对比差异 5% 必须复盘。这套基线比加压到极限还重要。压测这件事最容易踩的坑就是把它当测试做——JUnit 写个 case、JMeter 跑一下、出报告完事。但压测本质是生产事故的预演——你要用压测回答的核心问题只有一个线上出问题前我能不能提前 1 个月发现一个系统能扛多大流量不取决于压测峰值而取决于你对拐点了解多深。下篇预告Day86AI 时代架构师的新命题从 CRUD 到 AI 工程的思维跃迁——同样是做技术选型AI 时代多出了大模型依赖这一维度。我会拆解 AI 原生架构与 AI 增强架构的本质差异给你一份加 AI 时应该怎么想的新清单。
阅读完成 · 觉得有帮助?