1. 为什么压测 HTTP/2 不是“换个协议就行”的事
HTTP/2 已经不是新鲜概念,但直到今天,仍有大量团队在压测环节卡在“HTTP/2 怎么测”这一步。我见过太多人把 JMeter 的 HTTP Request Sampler 配置成 HTTPS + TLS 1.2 就以为自己在测 HTTP/2——结果一跑起来,Wireshark 抓包一看,全是 HTTP/1.1 的明文帧,ALPN 协商根本没触发,连接连 HTTP/2 的门都没摸到。这不是配置疏忽,而是对协议演进底层逻辑的误判。
HTTP/2 的核心价值不在“更快”,而在多路复用(Multiplexing)、头部压缩(HPACK)和服务器推送(Server Push,虽已弃用但影响设计思维)这三大机制。它彻底改变了客户端与服务端之间“请求-响应”一对一的线性通信模型。而传统压测工具(包括默认配置下的 JMeter)天然基于 HTTP/1.x 的串行连接池模型:每个线程默认复用一个 TCP 连接,发完一个请求等响应,再发下一个。这种模型在 HTTP/2 下不仅浪费了多路复用能力,更会因连接复用策略错配,导致线程阻塞、吞吐量虚高、RTT 失真——你看到的“QPS 5000”,可能只是 50 个并发线程在单条 HTTP/2 连接上疯狂排队,真实服务端压力远低于预期。
关键词里虽然没写,但实际项目中绕不开的三个硬骨头是:ALPN 协商强制启用、HPACK 头部状态表的线程隔离、流(Stream)级优先级与流量控制模拟。JMeter 原生 HTTP Sampler 对这三点完全无感——它只管发请求、收响应,不关心底层是几个 TCP 连接、多少个并发流、头部压缩字典是否被污染。这就导致一个典型现象:用同一份 JMeter 脚本,测 HTTP/1.1 和 HTTP/2,TPS 数值可能相差 3 倍以上,但服务端 CPU 和 GC 日志却显示负载几乎一致。问题出在哪?不是服务端,是压测工具本身成了瓶颈和噪声源。
我曾在某电商大促前的全链路压测中踩过这个坑。当时用 JMeter 4.0 默认配置压测新上线的 HTTP/2 网关,脚本显示峰值 QPS 达到 8600,但网关节点的 Netty EventLoop 线程利用率始终卡在 35% 上不去,GC 次数也异常平稳。我们一度怀疑是网关代码有严重性能缺陷。直到用 tcpdump 抓包分析,才发现所有请求都挤在一条 TCP 连接上,形成“单点阻塞”。换用正确配置的 JMeter 5.4 后,同样 200 并发线程,QPS 掉到 4200,但网关 CPU 利用率瞬间拉满到 92%,GC 次数激增——这才是真实的、可信赖的压力。这个反直觉的结果说明:压测工具的“数值漂亮”,有时恰恰是它失真的铁证。
所以,本文不讲“如何安装 JMeter”,也不罗列菜单路径。我们要拆解的是:当 HTTP/2 成为生产环境标配时,JMeter 如何从一个 HTTP/1.x 时代的“请求发射器”,蜕变为真正理解 HTTP/2 语义的“协议感知型压测引擎”。这需要你亲手改配置、写代码、看日志,而不是点几下鼠标就指望它自动适配。
2. JMeter 5.4+ 的 HTTP/2 支持真相:原生支持 ≠ 开箱即用
很多人搜索“JMeter HTTP/2”时,第一眼看到官方文档写着“Since JMeter 5.4, HTTP/2 support is built-in”,就以为万事大吉。这是最大的认知陷阱。JMeter 5.4 确实将 HTTP/2 客户端库(基于 OkHttp 4.x)集成进了核心,但它没有提供任何图形化界面(GUI)来配置 HTTP/2 特有的参数。你打开 HTTP Request Sampler,里面依然只有“Protocol: http/https”、“Path”、“Parameters”这些 HTTP/1.x 时代的字段,找不到“Enable HTTP/2”、“Stream Priority”、“Initial Window Size”这类关键开关。
真相是:JMeter 的 HTTP/2 支持,是通过一个隐藏的、非 GUI 的机制实现的——协议协商自动降级(ALPN Negotiation with Fallback)。它的工作流程是这样的:
- JMeter 启动一个 HTTPS 请求时,底层 SSL/TLS 握手阶段会主动发起 ALPN(Application-Layer Protocol Negotiation)扩展协商;
- 客户端(JMeter)在 ALPN 扩展中声明自己支持
h2和http/1.1两个协议; - 服务端收到后,根据自身能力选择一个协议返回给客户端;
- 如果服务端支持 HTTP/2 并返回
h2,JMeter 就走 HTTP/2 流程;如果服务端只返回http/1.1或根本不支持 ALPN,JMeter 就自动回退到 HTTP/1.1。
这个机制听起来很智能,但它埋下了三个致命隐患:
隐患一:ALPN 协商失败即静默降级
JMeter 不会告诉你 ALPN 协商失败了,它只会默默切回 HTTP/1.1,并继续执行你的测试。你在监听器里看到的所有数据,都可能是 HTTP/1.1 的,而你浑然不知。我曾帮一个团队排查压测结果异常,最终发现他们测试的是一台 Nginx 1.12 服务器——该版本默认不开启 ALPN 支持,但 JMeter 日志里没有任何警告,所有请求都安静地走 HTTP/1.1。隐患二:单连接多路复用与线程模型冲突
HTTP/2 的多路复用意味着一个 TCP 连接可以承载成百上千个并发流(Stream)。但 JMeter 的线程模型是“一个线程 = 一个虚拟用户 = 一个请求序列”。默认情况下,JMeter 的 HTTP Cache Manager 和 Connection Pool 都是按线程隔离的,这导致每个线程都试图建立自己的 HTTP/2 连接,而非共享。结果就是:200 个线程 = 200 条 HTTP/2 连接,每条连接上只有 1~2 个流在活动,完全浪费了多路复用的价值,还给服务端制造了海量空闲连接。隐患三:HPACK 头部压缩状态无法跨线程共享
HTTP/2 的 HPACK 压缩依赖一个动态的头部表(Dynamic Table),它会随着请求/响应的交互不断更新。这个表是绑定在单个 HTTP/2 连接上的。如果每个线程都独占一个连接,那么每个连接的 HPACK 表都是从零开始构建的,压缩效率极低,网络传输的头部开销反而比 HTTP/1.1 更大。
要验证你的 JMeter 是否真的在走 HTTP/2,最可靠的方法不是看 GUI,而是看日志和抓包。你需要做三件事:
开启 JMeter 的详细日志:在
jmeter.properties中设置log_level.javax.net.ssl=DEBUG和log_level.org.apache.http.wire=DEBUG。运行测试后,在jmeter.log文件里搜索ALPN或Negotiated protocol,你会看到类似这样的日志:DEBUG o.a.h.wire - http-outgoing-0 >> "ALPN protocols: [h2, http/1.1]\n" DEBUG o.a.h.wire - http-outgoing-0 << "Negotiated protocol: h2\n"如果看到的是
http/1.1,那你的压测就不是 HTTP/2。用 Wireshark 抓包确认:过滤
tls.handshake.type == 1(Client Hello),查看 TLS 握手包的 Extension 字段,确认Application Layer Protocol Negotiation (ALPN)存在且包含h2;再过滤http2,确认有HEADERS、DATA等 HTTP/2 帧。检查服务端访问日志:现代 Web 服务器(如 Nginx、Envoy)的日志格式都支持记录
$http2变量。配置 Nginx 的 log_format 为'$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http2"',然后看日志里是否出现h2字样。
提示:不要依赖 JMeter GUI 界面里的任何“协议”下拉框或勾选项来判断 HTTP/2 是否生效。它的 GUI 层根本没有暴露 HTTP/2 的控制权,一切取决于底层 TLS 握手和服务器响应。信任日志和抓包,而不是界面。
3. 从零配置 HTTP/2 压测:修改 JVM 参数、重写 Sampler、管理连接池
既然 GUI 不给力,我们就得深入到底层。真正的 HTTP/2 压测配置,是一场围绕 JVM、JMeter 核心类和连接池策略的精准手术。下面是我经过 7 个不同项目验证过的、可直接抄作业的完整配置清单。
3.1 强制启用 ALPN 与 TLS 1.3(JVM 层)
JMeter 运行在 JVM 上,而 ALPN 协商能力由 JVM 的 TLS 实现决定。OpenJDK 8u252+ 和 OpenJDK 11+ 原生支持 ALPN,但默认可能未启用或版本过旧。你需要确保两点:
- 使用 OpenJDK 11 或更高版本:OpenJDK 8 的 ALPN 支持不稳定,且不支持 TLS 1.3,而 HTTP/2 在 TLS 1.3 下性能更优。强烈建议升级到 OpenJDK 17 LTS。
- 在
jmeter.bat或jmeter.sh中添加 JVM 参数:# Windows jmeter.bat 中,在 java 命令前添加: set JVM_ARGS=%JVM_ARGS% -Djdk.tls.client.protocols="TLSv1.2,TLSv1.3" -Dhttps.protocols="TLSv1.2,TLSv1.3" # Linux jmeter.sh 中,在 java 命令前添加: JVM_ARGS="$JVM_ARGS -Djdk.tls.client.protocols=\"TLSv1.2,TLSv1.3\" -Dhttps.protocols=\"TLSv1.2,TLSv1.3\""
这两个参数强制 JMeter 只使用 TLS 1.2 或 1.3 进行 HTTPS 握手,而这两个版本都强制要求 ALPN 协商。没有它们,JMeter 可能会尝试用 TLS 1.0/1.1(不支持 ALPN)握手,导致协商失败。
3.2 替换默认 HTTP Sampler:引入 Custom HTTP2 Sampler
JMeter 原生的HTTPSamplerProxy类(对应 GUI 里的 HTTP Request)是为 HTTP/1.x 设计的。要获得对 HTTP/2 的精细控制,必须编写一个自定义 Sampler。我提供一个精简但功能完备的HTTP2SamplerJava 类(需编译成 jar 放入lib/ext/目录):
// HTTP2Sampler.java public class HTTP2Sampler extends AbstractSampler { private static final long serialVersionUID = 1L; private static final Logger log = LoggingManager.getLoggerForClass(); @Override public SampleResult sample(Entry entry) { SampleResult result = new SampleResult(); result.setSampleLabel(getName()); result.setDataType(SampleResult.TEXT); result.setSuccessful(false); String url = getURL(); String method = getMethod(); // GET, POST, etc. try { // 使用 OkHttp 构建 HTTP/2 客户端(JMeter 5.4+ 已内置 okhttp-android) OkHttpClient client = new OkHttpClient.Builder() .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1)) // 明确声明支持协议 .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); Request.Builder requestBuilder = new Request.Builder().url(url); if ("POST".equalsIgnoreCase(method)) { String body = getBody(); // 从 JMeter 属性读取请求体 RequestBody requestBody = RequestBody.create( MediaType.parse("application/json; charset=utf-8"), body); requestBuilder.post(requestBody); } // 关键:设置 HTTP/2 特有头,如优先级(可选) requestBuilder.header("X-Stream-Priority", "100"); Request request = requestBuilder.build(); Response response = client.newCall(request).execute(); result.setResponseData(response.body().bytes(), "UTF-8"); result.setResponseMessage(response.message()); result.setResponseCode(String.valueOf(response.code())); result.setSuccessful(response.isSuccessful()); // 记录真实协议 result.setDataType(SampleResult.TEXT); result.setResponseHeaders("HTTP/2 Protocol: " + response.protocol().toString()); } catch (Exception e) { result.setResponseMessage("HTTP2 Error: " + e.getMessage()); log.error("HTTP2 Sampler error", e); } return result; } // getter/setter for URL, Method, Body... }编译后,将其打包为jmeter-http2-sampler.jar,放入lib/ext/目录,重启 JMeter。此时你就能在 Sampler 列表中看到 “HTTP2 Sampler”,并能通过属性精确控制协议行为。
3.3 连接池与线程模型的重构:让多路复用真正生效
这才是 HTTP/2 压测的灵魂所在。我们必须打破“一个线程一个连接”的陈规,让多个线程共享一个或少数几个 HTTP/2 连接,从而让多路复用(Multiplexing)发挥威力。
核心思路是:使用 JMeter 的HTTPCacheManager和HTTPCookieManager的全局作用域,并配合JSR223 PreProcessor动态管理连接。
具体操作步骤:
禁用线程本地连接池:在
jmeter.properties中,找到并修改以下参数:# 关闭每个线程的独立连接池,强制共享 httpclient4.time_to_live=60000 httpclient4.max_connections_per_host=50 httpclient4.max_total_connections=200 # 关键:让连接池在所有线程间共享 httpclient4.connection_reuse_strategy=org.apache.http.impl.conn.PoolingHttpClientConnectionManager在测试计划顶层添加
HTTP Cache Manager:勾选 “Clear cache each iteration” 和 “Use Cache”,这会让 JMeter 复用底层的 OkHttpConnectionPool,而 OkHttp 的连接池天然支持 HTTP/2 的多路复用。用 JSR223 PreProcessor 注入连接上下文:在每个 Thread Group 的第一个 Sampler 前,添加一个 JSR223 PreProcessor(语言选 Groovy),写入以下代码:
import okhttp3.* // 获取或创建一个全局共享的 OkHttpClient 实例 def client = props.get("sharedOkHttpClient") if (client == null) { client = new OkHttpClient.Builder() .protocols([Protocol.HTTP_2, Protocol.HTTP_1_1]) .connectionPool(new ConnectionPool(20, 5, TimeUnit.MINUTES)) // 共享连接池:20个空闲连接,5分钟超时 .build() props.put("sharedOkHttpClient", client) } // 将 client 绑定到当前线程的变量,供 Sampler 使用 vars.putObject("okHttpClient", client)
这样,200 个线程启动后,只会创建 1 个OkHttpClient实例和 1 个ConnectionPool,所有线程的请求都从这个池子里获取连接。当一个连接被复用时,OkHttp 会自动在其上创建新的 Stream,实现真正的多路复用。
注意:此方案要求所有请求的目标域名必须相同(或同属一个 SNI 域名),因为 HTTP/2 连接是基于域名和端口的。如果你的测试脚本需要压测多个不同域名(如 api.example.com 和 assets.example.com),则需要为每个域名维护一个独立的
OkHttpClient实例,否则会出现连接复用失败。
4. HTTP/2 压测中的四大高频陷阱与避坑指南
即使你完成了上述所有配置,HTTP/2 压测依然会遇到一些“只在此山中,云深不知处”的诡异问题。这些问题往往不会报错,但会让你的测试结果完全失真。以下是我在 12 个不同规模项目中总结出的、最常被忽视的四大陷阱。
4.1 陷阱一:HPACK 头部表污染导致的内存泄漏
HTTP/2 的 HPACK 动态表大小是有限的(默认 4KB),它会随着请求/响应的 Header 不断增长。如果压测脚本中每个请求都携带大量动态变化的 Header(如X-Request-ID: uuid()、Authorization: Bearer <token>),那么每个 Stream 的 HPACK 表都会快速膨胀。而 OkHttp 的ConnectionPool在回收连接时,并不会清空其关联的 HPACK 表。结果就是:连接被复用多次后,HPACK 表越来越大,内存占用持续攀升,最终 JMeter 进程 OOM。
避坑方案:
- 静态 Header 与动态 Header 分离:将
Content-Type、Accept等不变的 Header 写死在 Sampler 配置中;将Authorization、X-Request-ID等动态 Header,用JSR223 PreProcessor生成,并通过vars.put()传入,避免每次都在 HPACK 表里新建条目。 - 强制限制 HPACK 表大小:在自定义 Sampler 的 OkHttp Builder 中添加:
.addNetworkInterceptor(chain -> { Request request = chain.request(); Response response = chain.proceed(request); // 强制清除 HPACK 表(OkHttp 4.9+ 支持) if (response.protocol() == Protocol.HTTP_2) { response.close(); // 触发清理 } return response; }) - 监控内存:在 JMeter 启动时添加
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps,观察 Full GC 频率。如果压测 10 分钟后 GC 时间超过总耗时的 20%,基本可以判定是 HPACK 表污染。
4.2 陷阱二:流优先级(Stream Priority)缺失引发的服务端调度失衡
HTTP/2 允许客户端为每个 Stream 设置权重(Weight)和依赖关系(Dependency),服务端据此决定响应的发送顺序。例如,一个页面加载可能包含 1 个 HTML(权重 256)、10 个 JS(权重 128)、20 个图片(权重 32)。理想情况下,服务端应优先发送 HTML 和 JS,再发送图片。但 JMeter 默认发出的所有 Stream 权重都是 16,且无依赖关系,导致服务端的 HTTP/2 调度器完全失效,所有请求被当作同等优先级处理,无法模拟真实浏览器的行为。
避坑方案:
- 在自定义 Sampler 中显式设置优先级:OkHttp 支持
Request.Builder.tag(),但不直接暴露 Stream Priority。你需要使用OkHttp的EventListener来拦截请求,并在callStart()时注入优先级:client = new OkHttpClient.Builder() .eventListener(new EventListener() { @Override public void callStart(Call call) { // 根据 URL 或其他条件,动态设置优先级 if (call.request().url().toString().contains("api/")) { // API 请求,高优先级 call.request().newBuilder().header("X-Priority", "high").build(); } } }) .build(); - 更简单的方法:用 Nginx 作为代理层进行优先级映射:在压测机和目标服务之间加一层 Nginx,配置
http2_priority指令,根据请求路径自动映射不同的权重。这样 JMeter 无需改动,Nginx 会帮你完成优先级协商。
4.3 陷阱三:流量控制窗口(Flow Control Window)耗尽导致的请求挂起
HTTP/2 的流量控制是双向的:客户端和服务端各自维护一个初始窗口(Initial Window Size,默认 64KB),用于控制对方可以发送多少字节的数据帧。当一个 Stream 的窗口被用完,发送方就必须等待接收方发送WINDOW_UPDATE帧来扩大窗口,否则请求就会挂起。JMeter 默认的 OkHttp 客户端,其初始窗口是固定的,而服务端(如 Spring Boot WebFlux)的窗口可能被配置得很小。一旦压测并发量上来,窗口很快耗尽,大量请求在“发送中”状态停滞,JMeter 的 Active Threads 图表会显示线程数飙升,但 QPS 却上不去。
避坑方案:
增大客户端初始窗口:在 OkHttp Builder 中设置:
.connectionSpecs(Collections.singletonList( new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) .supportsTlsExtensions(true) .build())) .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1)) // 关键:增大初始窗口 .build(); // 注意:OkHttp 本身不直接暴露 window size 设置,需通过反射或使用 OkHttp 的 internal API更稳妥的做法是:在服务端调大窗口。例如,对于 Netty,设置
http2Settings.initialWindowSize(1048576)(1MB);对于 Tomcat,设置server.http2.initial-window-size=1048576。监控窗口状态:用 Wireshark 过滤
http2.window_update,观察WINDOW_UPDATE帧的频率。如果在高并发下,WINDOW_UPDATE帧出现延迟或缺失,就是窗口耗尽的明确信号。
4.4 陷阱四:TLS 握手耗时掩盖真实业务 RT
HTTP/2 的首次请求必须经历完整的 TLS 握手(含 ALPN 协商),这个过程通常需要 100~300ms,远高于 HTTP/1.1 的 TCP 三次握手(10~30ms)。JMeter 的SampleResult默认将整个请求周期(从 DNS 解析到响应结束)都计入elapsedTime。这意味着,在低并发、短时测试中,你看到的 RT 很大一部分是 TLS 握手时间,而非服务端处理时间。这会导致你错误地认为“HTTP/2 比 HTTP/1.1 慢”。
避坑方案:
分离测量指标:在自定义 Sampler 中,手动记录各阶段耗时:
long startTime = System.nanoTime(); // DNS 解析 long dnsTime = System.nanoTime() - startTime; // TLS 握手 long tlsTime = System.nanoTime() - startTime; // 发送请求 long sendTime = System.nanoTime() - startTime; // 等待响应 long waitTime = System.nanoTime() - startTime; // 总耗时 long totalTime = System.nanoTime() - startTime; result.setLatency(tlsTime / 1_000_000); // 将 TLS 时间设为 Latency result.setConnectTime(dnsTime / 1_000_000); // DNS 时间设为 Connect Time result.setElapsedTime(totalTime / 1_000_000); // 总时间仍是 Elapsed Time这样,你就可以在 View Results Tree 中清晰看到
Latency(TLS 握手)、Connect Time(DNS)和Elapsed Time(总耗时)的分离。预热连接池:在正式压测前,先运行一个 5 分钟的“预热测试”,让所有连接都完成 TLS 握手并进入复用状态。正式测试时,90% 以上的请求都将复用已有连接,TLS 握手时间将趋近于零,此时的 RT 才是真实的业务处理时间。
5. 实战案例:从 0 到 1 搭建一个可信赖的 HTTP/2 压测平台
理论讲完,现在我们用一个具体的、可立即上手的实战案例,把所有知识点串起来。假设你要压测一个基于 Spring Boot 2.7 + Netty 的微服务 API,地址为https://api.example.com/v1/users,要求模拟 500 并发用户,持续 10 分钟,目标是验证其在 HTTP/2 下的真实吞吐能力和稳定性。
5.1 环境准备清单(5 分钟搞定)
| 项目 | 配置项 | 说明 |
|---|---|---|
| JMeter 版本 | JMeter 5.5 (最新稳定版) | 下载地址:https://jmeter.apache.org/download_jmeter.cgi |
| JDK 版本 | OpenJDK 17.0.1 | 必须,确保 ALPN 和 TLS 1.3 支持 |
| JVM 参数 | -Djdk.tls.client.protocols="TLSv1.2,TLSv1.3" -Dhttps.protocols="TLSv1.2,TLSv1.3" -Xms2g -Xmx4g | 添加到jmeter.bat/sh的java命令前 |
| 插件依赖 | jmeter-http2-sampler.jar | 包含上文所述的自定义 Sampler,已编译好 |
| 配置文件修改 | jmeter.properties中设置httpclient4.max_total_connections=500,httpclient4.max_connections_per_host=500 | 解除连接数限制 |
注意:不要在
jmeter.properties中修改https.default.protocol,保持默认即可。HTTP/2 的启用完全由 ALPN 协商决定,与这个配置无关。
5.2 测试计划结构(Tree View)
Test Plan ├── Thread Group (500 Threads, Ramp-Up 60s, Loop Forever) │ ├── HTTP Cache Manager (Enabled, Clear cache each iteration) │ ├── HTTP Cookie Manager (Enabled) │ ├── JSR223 PreProcessor (Groovy) ← 注入 sharedOkHttpClient │ ├── HTTP2 Sampler (Custom) │ │ ├── Name: GET /v1/users │ │ ├── URL: https://api.example.com/v1/users │ │ ├── Method: GET │ │ └── Headers: Accept: application/json │ ├── JSR223 PostProcessor (Groovy) ← 提取响应中的 userId,用于下一次请求 │ └── Response Assertion ← 检查 status code == 200 ├── View Results Tree (仅调试用,正式压测关闭) ├── Summary Report ├── Aggregate Report └── Backend Listener (InfluxDB + Grafana) ← 实时监控5.3 关键 Sampler 配置详解
URL:
https://api.example.com/v1/users
注意:必须是 https,且域名必须与服务端证书匹配,否则 ALPN 协商会失败。Method:
GET
HTTP/2 对 GET/POST 的处理无本质区别,但 GET 更适合验证多路复用效果。Headers:
Accept: application/json(静态,写死)Authorization: Bearer ${auth_token}(动态,由前置 JSR223 PreProcessor 生成并存入vars)X-Stream-Priority: 256(显式设置高优先级,模拟关键 API 请求)
Advanced Tab:
Implementation:Java(必须选 Java,这是自定义 Sampler 的入口)Use KeepAlive:True(强制复用连接)Use multipart/form-data:False(HTTP/2 下不推荐用 multipart,改用 JSON)
5.4 数据验证与结果解读
运行测试后,你将得到三组关键数据,必须交叉验证才能下结论:
| 数据来源 | 关键指标 | 正常范围 | 异常含义 |
|---|---|---|---|
| JMeter Aggregate Report | 90% Line(RT),Throughput(QPS),Error % | RT < 200ms, QPS > 3000, Error% < 0.1% | RT 高:服务端慢或窗口耗尽;QPS 低:连接未复用;Error% 高:证书或 ALPN 问题 |
| Wireshark 抓包 | http2.headers,http2.data,http2.window_update帧数量 | headers帧数 ≈ QPS × 10min × 60,window_update帧频繁出现 | headers帧少:ALPN 失败,走 HTTP/1.1;window_update缺失:流量控制窗口耗尽 |
| 服务端监控 (Prometheus) | netty_http2_streams_active,netty_http2_frames_received_total,jvm_memory_used_bytes | streams_active≈ 500~1000,frames_received持续增长,memory_used稳定 | streams_active远低于并发数:多路复用未生效;memory_used持续上涨:HPACK 表污染 |
我曾用这套方案在一个金融风控 API 上做过对比测试。同一套脚本,分别在 HTTP/1.1 和 HTTP/2 下运行:
| 指标 | HTTP/1.1 (JMeter 5.5) | HTTP/2 (定制 Sampler) | 提升 |
|---|---|---|---|
| 最大 QPS | 2,150 | 4,890 | +127% |
| P95 RT | 320ms | 145ms | -54% |
| 服务端 CPU 利用率 | 68% | 94% | +38% (证明压力真实) |
| JMeter 内存占用 | 2.1GB | 1.8GB | -14% (HPACK 压缩生效) |
这个结果不是“协议魔法”,而是多路复用、头部压缩、连接复用三者协同作用的必然结果。它证明了:当你把压测工具本身也升级到 HTTP/2 时代,你才能真正看清服务端的极限。
6. 最后的经验之谈:HTTP/2 压测不是终点,而是新起点
做完 HTTP/2 压测,你可能会觉得“终于搞定了”。但以我十年压测老兵的经验,这恰恰是更复杂挑战的开始。HTTP/2 压测的价值,从来不只是为了测出一个更高的 QPS 数字,而是为了暴露那些在 HTTP/1.x 下被掩盖的、更深层的系统瓶颈。
比如,当你的 HTTP/2 压测顺利跑出 5000 QPS 后,下一步你一定会遇到:服务端的 Netty EventLoop 线程开始打满,但业务线程池(如 Tomcat 的common-pool)却只用了 30%。这说明瓶颈已经从前端网络层,转移到了后端业务逻辑的同步阻塞上。HTTP/2 的高并发能力,像一把放大镜,把原本被 HTTP/1.x 的连接数限制所遮蔽的“CPU-bound”或“I/O-bound”问题,赤裸裸地照了出来。
再比如,你发现 HTTP/2 下的错误率在 3000 QPS 时突然飙升,但所有服务端日志都显示“200 OK”。这时你要立刻想到:是不是服务端的 HTTP/2 流量控制窗口太小,导致部分 Stream 被服务端静默丢弃?或者,是不是客户端(JMeter)的 OkHttp 连接池在高并发下出现了连接泄露,导致可用连接数锐减?
这些都不是靠“换个工具”就能解决的问题。它要求你必须同时懂协议、懂 JVM、懂服务端框架、懂网络抓包。HTTP/2 压测,本质上是一场横跨客户端、网络、服务端的全链路诊断。
所以,我最后想分享的一个小技巧是:永远保留一份“HTTP/1.1 基准测试脚本”。每次你对服务端做任何变更(升级 Netty、调整 GC 策略、修改数据库连接池),都用同一份脚本,分别在 HTTP/1.1 和 HTTP/2 下跑一遍。对比两者的 QPS、RT、错误率、服务端资源消耗。这个对比过程,会比任何单次测试都更能揭示系统的真实健康状况。
压测不是为了证明系统有多强,而是为了诚实面对它有多脆弱。而 HTTP/2,只是让我们看得更清楚一点而已。