4. 性能测试工具:JMH、ab/wrk、gprof/perf
聊到并发瓶颈,最怕的就是凭感觉说话。我见过太多工程师,代码写得行云流水,一问性能数据,只能回一句「我觉得应该很快」—— 这不行。
性能诊断,一定得靠数据说话。而这章,我们就来聊聊我常用的三套工具:JMH、ab/wrk、gprof/perf。它们各自管一块,组合起来,几乎能覆盖所有并发瓶颈场景。
4.1 JMH:写对基准测试
搞国密SDK,你肯定要测某个算法的单次执行耗时。很多新手直接写个 for 循环 打时间戳,结果 JIT 编译一介入,数据全乱套。我个人习惯,这种场景一定用 JMH。
举个例子,你要测 SM4 的 ECB 加密吞吐:
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
public class Sm4Benchmark {
private Sm4Crypto sm4;
private byte[] plaintext;
@Setup
public void init() {
sm4 = new Sm4Crypto();
plaintext = "国密SDK性能测试数据".getBytes();
}
@Benchmark
public byte[] encrypt() {
return sm4.encryptEcb(plaintext);
}
}
跑起来之后,JMH 会输出类似这样的结果:
| Benchmark | Mode | Cnt | Score | Error | Units |
|---|---|---|---|---|---|
| Sm4Benchmark.encrypt | thrpt | 10 | 285.647 | ± 3.218 | ops/ms |
这个 285 ops/ms 就是你在单线程下的真实上限。我在优化一个客户端的国密库时,就是靠这个数据,发现某个实现比预期慢了一半 —— 原来是个无意义的字节拷贝。
4.2 ab / wrk:模拟真实并发压力
单次调用再快,放到高并发下可能直接崩盘。所以第二个工具,就是压测工具。Linux 下我常用 ab(ApacheBench),简单直接。如果你想要更精细的控制,wrk 支持 Lua 脚本,灵活性更高。
咱们先看 ab 的基本用法:
# 模拟 100 个并发,总共发送 10000 次请求
ab -n 10000 -c 100 http://127.0.0.1:8080/sm4/encrypt
输出里的几个核心指标:
- Requests per second:系统每秒能处理的请求数。这个数值越大越好。
- Time per request (mean):平均每个请求的耗时,包括网络传输。
- Connection Times:连接建立、处理、等待的分布情况。
- Failed requests:如果这个不为 0,说明有请求超时或出错,基本就是瓶颈信号。
wrk 的用法也类似,但支持更多自定义:
wrk -t12 -c400 -d30s http://127.0.0.1:8080/sm4/encrypt
-t12 用 12 个线程,-c400 保持 400 个并发连接,持续 30 秒。我个人更习惯用 wrk,因为它能打出更细的延迟分布,比如 Latency Distribution 里的 P50、P75、P99。
注意一个常见的误区:并发连接数不是越大越好。当并发数超过系统处理能力时,你会发现 RPS 反而下降,失败率飙升。我以前给一个国密网关做压测,并发从 200 调到了 800,RPS 从 3200 跌到了 1500 —— 就是因为线程争抢锁太严重。所以并发数要逐步增加,找到拐点。
这里分享我自己的一个经验:压测时,我会同时用 top 或 htop 观察 CPU 和内存。如果 CPU 没跑满,RPS 就上不去了,那说明瓶颈在 I/O 或锁上。
4.3 gprof / perf:定位热点函数
好了,前两步已经帮你确认了「系统确实有瓶颈」。但瓶颈到底在哪行代码里?这时候就需要性能剖析工具上阵了。
gprof 是 GNU 工具链里自带的,用法简单:
- 编译时加
-pg选项。 - 正常运行程序,退出后会自动生成
gmon.out文件。 - 执行
gprof ./your_program gmon.out查看报告。
报告会以表格形式列出每个函数的调用次数、自身耗时、累计耗时。比如下面这个简化输出:
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls us/call us/call name
35.2 1.52 1.52 50000 30.4 86.2 sm4_encrypt_block
20.5 2.41 0.89 100000 8.9 8.9 memcpy
12.1 2.93 0.52 50000 10.4 40.6 key_expansion
...
从这张表里,一眼就能看到 sm4_encrypt_block 占用了 35.2% 的 CPU 时间。那优化目标就很明确了。
但是,gprof 有一个很大的缺陷:它只能分析程序正常退出后的数据。对于服务器这种常年跑着的进程,你不好让它优雅退出。所以我现在更常用 perf。
常用的 perf 命令:
# 记录正在运行的进程(PID 1234)的 CPU 采样,持续 30 秒
perf record -g -p 1234 -- sleep 30
# 生成火焰图风格的调用链报告
perf report -g graph --stdio
输出里会列出各个函数的热度百分比,以及父子调用关系。我曾在某个项目中,用 perf 抓到一个只有 3 行代码的小函数,占用了超过 40% 的 CPU —— 源码里有个 memset 每次清空一块 4KB 的缓冲区,但其实根本不需要。去掉之后,整体吞吐提升了近 50%。
perf record --call-graph dwarf 来展开符号。这种东西,书上不会写,只能自己摔一跤才长记性。
4.4 三个工具的协作流
三套工具不是孤立的。我习惯按这个顺序走:
- JMH 先把每个独立操作的性能基线定下来。比如 SM2 签名耗时多少,SM4 加密吞吐多少。这一步能发现明显的低效代码。
- ab / wrk 把完整链路压起来,看看系统在真实并发下的表现。如果这里的 RPS 与 JMH 基线差太多,说明瓶颈不在单次操作,而在并发协调(锁、队列、线程模型)。
- perf 再钻到进程内部,直接看 CPU 时间花在哪里。是加解密本身,还是上下文切换,还是内存拷贝?
搞懂这套流程,你基本就能应付 90% 的国密 SDK 并发性能问题了。剩下的 10%,可能需要深入系统内核或 JVM 调优——那些是后面章节的内容。