2. 性能指标与度量体系
性能调优最容易走偏的地方,不是不会调参数,而是不知道该看哪个数。同一个系统,运维说“CPU 才 40%,很闲”,用户却抱怨“页面转圈三秒”;压测报告写着“平均响应时间 80ms”,上线后却频繁超时。原因往往在于:大家看的指标不是同一个维度,或者只看了平均值,忽略了分布。
一套可用的度量体系,至少要同时回答四个问题:系统单位时间能处理多少请求(吞吐量)、单个请求要等多久(延迟)、为了达到这个吞吐花了多少机器资源(资源利用率)、以及这些延迟在请求之间是怎么分布的(响应时间分布)。这四个维度互相牵制,单独看任何一个都会得出错误结论。
吞吐量与延迟:一对互相拉扯的指标
吞吐量(Throughput)指单位时间内系统成功处理的请求数,常用单位是 RPS(Requests Per Second)或 TPS(Transactions Per Second)。延迟(Latency)指从发出请求到收到响应所经历的时间,通常用毫秒表示。二者并非线性关系:在低负载区,增加并发能同时提升吞吐、延迟几乎不变;进入饱和区后,吞吐趋于平台,延迟却急剧上升;再往上加压,吞吐反而下降,系统进入崩溃区。
因此调优的目标不是“吞吐越高越好”,而是找到延迟可接受前提下的最大吞吐。例如某订单查询接口,业务要求 P99 延迟不超过 200ms,那么压测时就要观察:并发从 50 加到 200 的过程中,RPS 是否还在增长,而 P99 是否已经越过 200ms。一旦越过,即使 RPS 还能再涨一点,这个并发点也已经不是可用的工作点。
延迟还要区分口径。服务端处理时间只统计应用内部耗时;客户端观测延迟还包含网络往返、DNS、连接建立、排队等。排查问题时必须明确说的是哪一个,否则会出现“监控显示 30ms,用户体感 2s”的割裂。
资源利用率:吞吐的成本侧
资源利用率描述 CPU、内存、磁盘 IO、网络带宽、连接池、线程池等资源被占用的比例。它决定了“这个吞吐是用多少成本换来的”。常见误区是认为利用率越低越好——空闲资源就是浪费;但利用率过高同样危险,因为排队延迟随利用率非线性增长。
一个实用的经验判断是:对于 CPU 密集型服务,CPU 利用率长期高于 70%~80% 时,延迟对负载波动会非常敏感;对于 IO 密集型服务,更该关注的是线程池活跃线程数、连接池等待时间、磁盘 await 等“排队类”指标,而不是 CPU。下面这张图把四个维度放在同一条请求链路上,便于理解它们各自的观测位置与相互关系。
响应时间分布:平均值掩盖的真相
假设 100 个请求中 95 个耗时 20ms,5 个耗时 2000ms,平均延迟是 119ms,看起来“还行”,但那 5% 的用户体验极差。这就是为什么必须看分位数:P50 代表中位体验,P95/P99 代表尾部体验,P999 则常用于核心交易链路。长尾往往来自 GC 停顿、锁竞争、连接池耗尽、慢 SQL、缓存击穿等偶发但影响巨大的因素。
除了分位数,直方图能更直观地暴露分布形态:是单峰集中,还是双峰(一部分快、一部分慢),或者长尾拖得很长。压测工具通常能导出延迟直方图,调优时应把它和分位数一起看。
常用度量工具与观测方式
度量工具按层次大致分为三类:
- 系统层:
top/vmstat/iostat/pidstat看 CPU、内存、磁盘、上下文切换;ss看连接状态。 - JVM 层:
jstat -gcutil <pid> 1000观察 GC 频率与耗时;jstack抓线程栈定位阻塞;JFR(Java Flight Recorder)可低开销采集方法级耗时与分配。 - 应用与链路层:Micrometer + Prometheus + Grafana 采集 RPS、延迟分位数、线程池指标;SkyWalking、Zipkin 等做分布式链路追踪,把端到端延迟拆到每个服务与每个 span。
下面给出一段可直接参考的 Micrometer 埋点示例,用于同时暴露吞吐与延迟分位数:
// 依赖:micrometer-core + micrometer-registry-prometheus
Timer requestTimer = Timer.builder("http.server.requests")
.tag("uri", "/api/order/query")
.publishPercentiles(0.5, 0.95, 0.99) // 暴露 P50/P95/P99
.publishPercentileHistogram() // 生成直方图桶,便于聚合
.register(meterRegistry);
// 在请求处理前后打点
Timer.Sample sample = Timer.start(meterRegistry);
try {
return orderService.query(orderId);
} finally {
sample.stop(requestTimer);
}
采集到数据后,判断逻辑可以归纳为一张简单的决策表:
| 现象 | 优先怀疑 | 下一步动作 |
|---|---|---|
| 吞吐上不去,CPU 不高 | 线程池/连接池排队、锁竞争、下游阻塞 | 看线程栈、连接池等待时间、下游 RT |
| P99 远高于 P50 | GC 停顿、慢 SQL、缓存击穿、偶发锁 | 对齐 GC 日志与慢请求时间点 |
| CPU 利用率长期 > 85% | 计算密集或频繁 GC | 看 GC 占比、热点方法、是否需要扩容 |
| 吞吐随并发增加反而下降 | 已过饱和点,排队与切换开销反噬 | 回退并发,定位瓶颈资源 |
最后要强调:指标是手段,不是目的。真正要回答的是“在当前业务约束下,系统是否健康、瓶颈在哪、改动是否有效”。把吞吐、延迟、利用率、分布四个维度放在同一张观测面板上,再配合链路追踪下钻,才能从“感觉慢”走到“知道慢在哪、为什么慢、改完是否真的变快”。