4. 性能指标定义:吞吐量、延迟、QPS、内存占用、CPU开销的度量方法

做性能优化,第一步就是知道怎么量。我见过太多团队,上来就撸袖子改代码,改完后「感觉快了」,结果一上压测被打回原形。说白了,没有度量就没有优化。

这一章我们把 PKI 证书 SDK 最核心的五个指标掰开揉碎,讲清楚它们的定义、怎么采集,以及我踩过的坑。

4.1 吞吐量 (Throughput)

吞吐量是最直观的指标。它回答一个问题:系统在单位时间内能处理多少笔证书操作?

  • 定义:单位时间(通常是秒)内成功完成的请求数。
  • 单位:TPS (Transactions Per Second) 或 OPS (Operations Per Second)。
  • 针对 PKI SDK:每秒完成的签名次数、验签次数、证书链验证次数。
我的度量习惯: 我一般不用「并发数」代指吞吐量。并发高不代表吞吐高,如果每个请求排队等锁,并发一万也是白搭。

度量方法:

  1. 选定一个压力工具(如 wrk, ab 或自研压测客户端)。
  2. 设定恒定并发数(例如 100 线程),持续压测 60 秒。
  3. 统计总完成请求数 ÷ 总耗时(秒),得到平均吞吐量。
// 伪代码示例:统计吞吐量
long start = System.nanoTime();
int completed = 0;
while (elapsedTime < 60_000) {
    sdk.sign(data);   // 执行一次签名
    completed++;
}
long elapsed = (System.nanoTime() - start) / 1_000_000_000;
double tps = completed / (double) elapsed;
System.out.println("吞吐量: " + tps + " TPS");
我曾经踩过的坑: 有一次我统计吞吐量时只跑 10 秒,结果 JIT 还没预热完就结束了,数据偏低 30%。后来我强制跑 60 秒以上,先跑 30 秒预热,再采集后 30 秒的数据。

4.2 延迟 (Latency)

延迟就是单个请求从发起到收到响应的时间。用户感知的「慢」,其实大部分是延迟问题。

度量维度:

维度 含义 PKI 场景的关注点
均值 (Avg) 所有请求延迟的算术平均 容易被极值拉偏,参考意义有限
P50 (中位数) 一半请求比这个值快 日常操作的典型延迟
P99 (99 分位) 99% 的请求比这个值快 尾延迟,证书链深或 CRL 大的情况
P999 极限尾延迟 垃圾回收、临时降级时的体验

度量方法:

  • 在每个请求前记录时间戳,请求完成后计算差。
  • 不只看平均值。我建议至少看 P99。
  • 使用 HdrHistogram 或 Micrometer Timer 做百分位统计。
// 使用 HdrHistogram 记录延迟
Histogram histogram = new Histogram(3);
for (int i = 0; i < 100_000; i++) {
    long start = System.nanoTime();
    sdk.verify(certificate, rootCert);
    long elapsed = System.nanoTime() - start;
    histogram.recordValue(elapsed / 1_000);  // 转为微秒
}
System.out.printf("P50: %d μs%n", histogram.getValueAtPercentile(50));
System.out.printf("P99: %d μs%n", histogram.getValueAtPercentile(99));

关键认知: 吞吐量和延迟是跷跷板。你强行把并发推到极限,单个请求延迟会急剧恶化。优化时要看两者的平衡。

4.3 QPS (每秒查询数)

QPS 是查询类指标的吞吐量变体。在 PKI 场景里,它多用于查询证书状态、CRL、OCSP 响应。

与 TPS 的异同:

TPS QPS
操作类型 写/改/签 读/查/验
数据变更 有副作用 一般无副作用
度量方式 与吞吐量一致 与吞吐量一致

度量方法: 和吞吐量一样,计数除以时间。但有一个细节:QPS 容易被缓存干扰。如果你加了一层本地缓存,从缓存命中返回的请求算不算 QPS?

我个人的做法是:分开统计。命中缓存的叫「有效 QPS」,走真实查询的叫「后端 QPS」。这样你能看到缓存的真实收益。

一个小建议: 度量 QPS 时要明确时间窗口。我见过用 1 秒窗口测出 5000 QPS,但换成 100 毫秒窗口只有 300 QPS —— 因为请求不均匀。建议用滑动窗口(如 5 秒)来平滑毛刺。

4.4 内存占用

内存占用是 SDK 内部数据结构的驻留空间。PKI 证书的加载、CRL 的全量缓存、中间证书链的构建……这些东西吃内存吃得很凶。

关注哪些区域:

  • 堆内存 (Heap):证书对象、CRL 列表、OCSP 响应缓存。
  • 堆外内存 (Off-heap):Bouncy Castle 的一些 native 操作,JNI 调用的缓冲区。
  • 元空间 (Metaspace):大量 ASN.1 解析类的类加载。

度量方法:

工具/方法 用途 注意事项
Runtime.getRuntime().totalMemory() 快速估算堆使用 不精确,含预留未分配的空间
jmap -histo:live 查看对象数量和大小 会触发 Full GC,生产慎用
Java Flight Recorder (JFR) 连续记录堆分配和 GC 性能影响小,推荐
使用 Instrumentation.getObjectSize() 估算单个证书对象大小 不包含内部引用的递归展开

我在项目中遇到过一个场景:内存一直涨,但堆分析找不到大对象。后来才发现是 Bouncy Castle 在解析大 CRL 时用了堆外 DirectBuffer,一块 8 MB 没释放。从那以后,我每次做内存分析都强制带上 -XX:NativeMemoryTracking=summary。

避坑指南: 不要只看堆最大值。频繁 GC 但堆大小变化不大时,可能是内存泄漏 —— 存活对象越来越多,GC 回收不掉。我曾经在一个 CRL 缓存模块里忘了设最大条目,一周后堆涨到 4 GB。加个 LRU 就解决了。

4.5 CPU 开销

CPU 开销决定了你的 SDK 单机能撑多少业务。说白了,它回答一个核心问题:每次签名/验签到底用了多少 CPU 时间?

度量方式:

  1. 用户态 CPU + 内核态 CPU:用 OperatingSystemMXBean.getProcessCpuTime() 获取。
  2. 每操作 CPU 耗时:连续执行 N 次操作,记录前后 CPU 时间差。
  3. 热点识别:用 perf 或 async-profiler 采样火焰图。
// 获取进程 CPU 时间(纳秒)
OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
long startCpu = osBean.getProcessCpuTime();
for (int i = 0; i < 10_000; i++) {
    sdk.createCertificateChain(subject, issuer);
}
long endCpu = osBean.getProcessCpuTime();
long cpuPerOp = (endCpu - startCpu) / 10_000;
System.out.println("每次创建链消耗 CPU: " + cpuPerOp / 1_000 + " μs");

常见的 CPU 消耗在哪里:

  • 大数运算(RSA 私钥操作最重)。
  • ASN.1 编解码(DER/BER 解析)。
  • 哈希计算(SHA-256/384/512)。
  • 线程上下文切换(线程数远大于核数时)。
我记得有一个线上问题: 证书验证每次只多花了 200 微秒 CPU,但业务方叫慢。后来发现是用了软件实现的 AES,换成硬件加速 AES-NI,CPU 开销降了 60%。所以 CPU 开销不只看原始耗时,还要看有没有走硬件加速。

4.6 五个指标的层级关系

这五个指标不是孤立的。我把它们画成一张观测体系图,你看看就明白了。

PKI 证书 SDK 性能度量体系 吞吐量 / QPS 单位时间能处理多少笔 延迟 (P50/P99) 单次操作要等多久 QPS(查询类) 证书查询/CRL 查询 内存占用 堆 / 堆外 / 缓存驻留量 CPU 开销 每操作 CPU 耗时 / 热点 工具:火焰图 / JFR / HdrHistogram / NMT 多个指标交叉验证,才能定位真实瓶颈 吞吐量 & 延迟决定「业务能跑多快」;内存 & CPU 决定「机器能撑多少」。缺一不可。

有了这张图,你应该清楚了:上层指标告诉你业务效果,下层指标告诉你根因。我建议你做性能分析时,先从上层(吞吐/延迟)发现问题,然后下沉到内存和 CPU 去定位。

核心总结: 五个指标定义清楚后,后面每一章的性能优化都会围绕它们展开。每改一个参数、每调一段代码,都要问自己:吞吐变了吗?延迟降了吗?内存和 CPU 的代价是什么?