4. 性能指标定义:吞吐量、延迟、QPS、内存占用、CPU开销的度量方法
做性能优化,第一步就是知道怎么量。我见过太多团队,上来就撸袖子改代码,改完后「感觉快了」,结果一上压测被打回原形。说白了,没有度量就没有优化。
这一章我们把 PKI 证书 SDK 最核心的五个指标掰开揉碎,讲清楚它们的定义、怎么采集,以及我踩过的坑。
4.1 吞吐量 (Throughput)
吞吐量是最直观的指标。它回答一个问题:系统在单位时间内能处理多少笔证书操作?
- 定义:单位时间(通常是秒)内成功完成的请求数。
- 单位:TPS (Transactions Per Second) 或 OPS (Operations Per Second)。
- 针对 PKI SDK:每秒完成的签名次数、验签次数、证书链验证次数。
度量方法:
- 选定一个压力工具(如 wrk, ab 或自研压测客户端)。
- 设定恒定并发数(例如 100 线程),持续压测 60 秒。
- 统计总完成请求数 ÷ 总耗时(秒),得到平均吞吐量。
// 伪代码示例:统计吞吐量
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");
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」。这样你能看到缓存的真实收益。
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。
4.5 CPU 开销
CPU 开销决定了你的 SDK 单机能撑多少业务。说白了,它回答一个核心问题:每次签名/验签到底用了多少 CPU 时间?
度量方式:
- 用户态 CPU + 内核态 CPU:用
OperatingSystemMXBean.getProcessCpuTime()获取。 - 每操作 CPU 耗时:连续执行 N 次操作,记录前后 CPU 时间差。
- 热点识别:用
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)。
- 线程上下文切换(线程数远大于核数时)。
4.6 五个指标的层级关系
这五个指标不是孤立的。我把它们画成一张观测体系图,你看看就明白了。
有了这张图,你应该清楚了:上层指标告诉你业务效果,下层指标告诉你根因。我建议你做性能分析时,先从上层(吞吐/延迟)发现问题,然后下沉到内存和 CPU 去定位。
核心总结: 五个指标定义清楚后,后面每一章的性能优化都会围绕它们展开。每改一个参数、每调一段代码,都要问自己:吞吐变了吗?延迟降了吗?内存和 CPU 的代价是什么?