性能工程:容量、基准、剖析与 JVM 诊断

优化之前,先把“慢”说具体:哪条操作,在多大负载下,等待了多久?然后保留相同条件,一次改一个因素。下面把容量测试和 JVM 诊断连起来,避免把偶然波动当成优化效果。

优化目标应是可度量的吞吐、延迟分位数、资源与成本,而不是抽象的“更快”。

1. 本文覆盖范围

  • SLO 与容量模型
  • 正确基准测试
  • CPU/内存/锁/IO 剖析
  • JFR、jcmd、GC 日志与压测闭环

2. 核心知识详解

1. 预算与排队

吞吐、并发与平均响应时间可用 Little 定律在稳定系统中关联。端到端延迟预算分配给网关、排队、服务、数据库和网络。

  • 使用 p50/p95/p99 和错误率,不只看平均值。
  • 容量测试逐步升压,识别饱和点和拐点。
  • 稳态、突发、故障和恢复分别验证。

这里容易混淆的是: Little 定律要求稳定长期平均;系统积压持续增长时直接套用会误导。

2. 基准测试方法

JVM 有 JIT、逃逸分析、GC 和预热,微基准使用 JMH 避免死代码消除、常量折叠和错误计时。

  • 端到端压测与微基准回答不同问题。
  • 固定数据、环境、版本和负载生成器容量。
  • 报告置信区间、样本和火焰图证据。

这里容易混淆的是: 一次 System.nanoTime 循环不是可靠 JVM 微基准。

3. 证据驱动剖析

CPU profile 找热点,allocation profile 找分配,heap dump 找保留路径,线程转储找阻塞,GC 日志解释暂停与堆行为。

  • 先复现并建立基线,再改一个变量。
  • 区分 CPU 饱和、锁等待、IO 等待和下游排队。
  • 生产优先低开销 JFR,敏感数据采集有权限和留存控制。

这里容易混淆的是: 对象数量多不等于泄漏;从 GC Roots 的意外保留路径才是关键证据。

4. 优化与验证

优化顺序通常是删除不必要工作、改算法/查询、批量与缓存、减少分配/锁,最后才微调 JVM 参数。

  • 每次优化验证正确性和尾延迟。
  • GC 选择依据堆、暂停目标和吞吐。
  • 变更设回滚阈值和线上观测。

这里容易混淆的是: 增加堆可能减少 GC 频率,也可能增加内存成本和故障转储时间;需要测量。

3. 工程链路

flowchart LR A["定义 SLO/负载"] --> B["建立基线"] B --> C["剖析定位瓶颈"] C --> D["提出单一假设"] D --> E["实现 + 正确性测试"] E --> F["同负载复测"] F --> G{"达到目标且无回归?"} G -->|否| C G -->|是| H["灰度与持续观测"]

4. 操作练习:给真实慢请求收集一份 JFR

前置条件是本机已有运行中的 HotSpot Java 服务,你有权附加到该进程,磁盘有足够空间。以下命令使用 JDK 自带工具;把 12345 换成 jcmd -l 列出的目标 PID,不要把它当成固定端口。

Shell
jcmd -l jcmd 12345 JFR.start name=slow-request settings=profile duration=60s filename=slow-request.jfr jcmd 12345 JFR.check jfr summary slow-request.jfr

在这 60 秒内用同样的输入重复那条慢操作。录制完成后再运行 jfr summary,并用 JDK Mission Control 打开文件。先看线程是在运行还是等待,再检查 CPU 样本、分配、锁和 GC 事件。采样事件是统计证据,某个栈没有出现不代表它从未执行;录制策略和事件阈值也会影响可见性。

举例:如果请求慢时线程主要等数据库连接,先查池等待和慢 SQL;盲目改 Java 循环不会解决等待。若 CPU 样本集中在解析同一份大配置,才有理由研究重复解析。记录优化前后的负载、成功率、p95 和资源占用,不要只保存“感觉变快”的截图。

settings=profile 有额外开销,应先在测试环境估计影响。采集文件可能含业务路径、线程名等信息,分享前检查内容。

5. 实践与验证

  1. 用 JMH 比较两种集合查找,解释预热和 Blackhole。
  2. 对一个接口采集 JFR 并区分 CPU、锁与分配热点。
  3. 写出峰值、实例数、连接池和数据库容量表。

6. 掌握检查

  • 能使用分位数和饱和点。
  • 能写可信 JMH。
  • 能读基础 JFR/线程/堆证据。
  • 能验证优化收益与回归。

参考资料