Skip to content

服务端监控与瓶颈分析 ​

T2-016必学高级约 90 分钟JDK 21测试专项 B:接口·UI·性能 · 性能测试

1. 本节定位 ​

《基线测试、负载测试与压力测试》找到了拐点("吞吐上限约 69 QPS");本节回答下一个问题:

text
"为什么只有 69 QPS?瓶颈在哪?"

没有这一步,压测只完成了一半 ✗ ——因为"知道慢"不等于"知道为什么慢" ✓

本节的核心方法论:

压测的同时,必须看服务端的指标 ✓ ——事后猜瓶颈,等于猜谜 ✗✗

2. 学习目标 ​

  • 说出监控的四个层次(操作系统 / JVM / 应用 / 依赖)★★★;
  • 会用 USE 方法组织排查(利用率 / 饱和度 / 错误)★★;
  • 能说出六类常见瓶颈的特征,并知道看哪个指标 ★★★;
  • 能根据"QPS 卡死 + 延迟上升"这类形态推断瓶颈类型 ★★★;
  • 知道"瓶颈会转移",并知道为什么 ★★★;
  • 会用最少的工具(top / jstat / jstack / Actuator)定位瓶颈 ★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看一个真实的排查循环:

text
压测发现 QPS 上不去
  → "是不是数据库慢?" → 加索引 → 重压 → 还是上不去 ✗
  → "是不是内存不够?" → 扩内存 → 重压 → 还是上不去 ✗
  → "是不是线程池太小?" → 调大 → 重压 → 还是上不去 ✗
  → …………

问题在于:每次都是"猜一个,改一个,再试一次" ✗ ——成本极高,且可能越改越糟 ✓

正确的方式:

在压测进行中,同时采集服务端指标,直接看"哪个资源先饱和" ✓✓

一次就能定位,而不是试错十次 ✓

5. 核心概念 ​

5.1 监控的四个层次 ★★★ ​

从上到下,一层层排除:

层看什么工具
① 操作系统 ★CPU、内存、磁盘 IO、网络、文件句柄top / vmstat / iostat / ss
② JVM ★★堆、GC、线程、类加载jstat / jstack / jmap / Actuator
③ 应用 ★★线程池队列、连接池、缓存命中率、QPS 分布Actuator / Micrometer ★
④ 依赖 ★★数据库、缓存、下游服务慢查询(《执行计划与慢查询定位》)/ 客户端指标

★ 排查顺序(从外到内,但不绝对):

text
先看 ①(最常见、最容易看)
  → 都正常?
再看 ②(Java 应用的头号嫌疑:GC)
  → 正常?
再看 ③(应用内部的等待:池、队列、锁)
  → 正常?
最后看 ④(是不是被下游拖慢)

⚠️ 但"从①开始"不是铁律:

如果现象是"CPU 低、QPS 上不去、延迟高",那基本可以跳过①②,直接看③ ✓

因为"在等"的瓶颈不在计算资源 ✓✓(《进程与端口排查》 的"CPU 低却假死")

5.2 USE 方法 ★★ ​

一个极简的排查框架(每个资源都问这三个问题):

维度问题例子
利用率(Utilization)这个资源有多少时间在忙?CPU 90%、连接池 80/100 ★
饱和度(Saturation)有没有在排队?排队多长?线程池队列长度、DB 连接等待时间 ★★
错误(Errors)有没有出错?超时、拒绝、异常计数 ✓

★ "饱和度"是最容易被忽略、却最能定位瓶颈的一维:

利用率高但不排队 → 还有余量 ✓

利用率不高但排队严重 → 有"隐形的串行点" ✗✗(锁、连接池上限、单线程消费)

本轮的实测现象(QPS 卡死在 69)正是"饱和度到顶"的典型形态 ✓

5.3 JVM 监控:Java 应用的必修课 ★★★ ​

三个最常用的工具(《进程与端口排查》 已讲过用法)★:

工具看什么什么时候用
jstat -gcutil <pid> 1000 ★★GC 频率与耗时怀疑 GC 拖慢
jstack <pid> ★★★线程都在干什么怀疑锁、线程池、等下游
jmap -histo:live <pid> ⚠️对象直方图怀疑内存泄漏(会触发 GC)

jstat 的关键列:

列含义异常信号
FGC / FGCT ★★Full GC 次数 / 总耗时持续增长 = 频繁 Full GC ✗
YGC / YGCT新生代 GC增长快但通常无害 ✓
O老年代占用率接近 100% ⚠️

★ 判据:

压测中如果 FGCT 快速上升,且与延迟尖峰对应 → 瓶颈在内存/GC ✓

如果 GC 很安静 → 排除内存方向,往线程与等待方向看 ✓✓

jstack 的三类关键发现:

发现含义
大量线程 WAITING 在同一个池的队列上 ★★池太小 / 上游太慢
大量线程 BLOCKED ★锁竞争
大量 RUNNABLE 卡在 socketRead0等下游响应 ⚠️
Found one Java-level deadlock ★★★死锁(jstack 会直接告诉你)

5.4 六类常见瓶颈 ★★★ ​

这张表是本节的核心 ——"形态 → 嫌疑"的对照:

#瓶颈典型现象看哪个指标
①CPUQPS 卡死、CPU 接近 100%、延迟上升top 的 us/sy ★
②GC / 内存延迟有周期性尖峰、QPS 忽高忽低jstat 的 FGC ★★
③磁盘 IOCPU 不高但 wa 高、写入变慢top 的 wa、iostat ★
④锁竞争 ★并发上不去、CPU 有富余jstack 的 BLOCKED ★★
⑤连接池 / 线程池 ★★QPS 卡在某个整数附近池的活跃数、等待数
⑥下游依赖 ★★线程大量等 IO、CPU 空闲下游的 RT、超时计数

★ 第 ⑤ 条有个很好用的线索:

QPS 卡在一个"看起来很整齐"的数值上(如正好 100、200)→ 强烈怀疑是某个池的上限 ✓✓

因为池的大小是人设的(如 maximumPoolSize=200、HikariCP maximumPoolSize=10)✓

5.5 用本轮的数据推断瓶颈 ★★★ ​

本轮的实测现象(T2-003 第 7 节):

text
并发 16 → 64:QPS 68.7 → 68.8(零增长)★
P50:213 ms → 845 ms(线性上升)★
错误率:0%

★ 这种形态能推出什么:

观察推断
QPS 完全冻结在一个值存在一个"串行资源"到顶了 ★★(不是"资源快用完",是"已经用完")
延迟随并发线性上升多出来的请求在排队 ✓("排队时间 = 等待时间",符合队列模型)
错误率 0%没有崩溃,是"优雅地变慢" ✓ ——说明队列还没满 ✓

嫌疑排序(按本环境):

排序嫌疑依据
1CPU(本机被压测客户端与服务端共享) ★★★同机压测——两侧 JVM 抢同一个 CPU 池
2Tomcat 工作线程数 ★默认 max-threads=200——64 并发不到上限,不太可能是它 ✓
3锁竞争该接口无共享状态(RawServlet 只做字符串拼接)→ 可能性低 ✓
4GC该接口几乎不产生对象 → 可能性低 ✓(但需 jstat 确认)

★ 关键认识:

"推断"只是缩小范围,"确认"必须靠指标 ✓✓

正确做法:压测时同时开 top(看 CPU)与 jstat(看 GC) ✓

本轮没有同时采集这两个指标 ✗ ——这是本节留的实验任务 ✓

5.6 瓶颈会转移 ★★★ ​

这是最容易让人困惑、也最值得记住的一条:

修好一个瓶颈后,压力会立刻转移到下一个瓶颈上 ✓

所以"优化"是一个循环,而不是一次性动作 ✓✓

一个真实的过程:

text
第一轮:QPS 200,瓶颈是 DB 没索引
  → 加索引 → QPS 800

第二轮:QPS 800,瓶颈是 DB 连接池只有 10
  → 调大到 50 → QPS 2000

第三轮:QPS 2000,瓶颈是应用 GC(对象分配太快)
  → 调堆 + 减少对象 → QPS 3500

第四轮:QPS 3500,瓶颈是单实例 CPU
  → 水平扩容 → QPS 7000(4 个实例)

每一轮的现象:"和上一轮长得一样"(QPS 卡住 + 延迟上升) ⚠️ ——但原因完全不同 ✗

★ 这就是为什么"看指标"比"猜"重要:

形态相似,原因不同 ——只有指标能区分它们 ✓✓

★ 一个反过来的提醒:

不要在没有指标依据的情况下"提前优化" ✗ ——那是在优化一个可能不存在的瓶颈 ✓

先把当前瓶颈找出来、修掉,再看下一个 ✓

6. 递进示例 ​

6.1 最小示例:压测时同时看三个数 ​

bash
# 三个终端同时开
top -p <服务端 pid>            # ① CPU
jstat -gcutil <pid> 1000       # ② GC
curl -s localhost:8080/actuator/metrics/hikaricp.connections.active   # ③ 连接池

6.2 业务示例:一次"QPS 卡住"的排查 ​

text
① 现象:并发 16 → 64,QPS 冻结在 69
② top:服务端 CPU 只有 40%,客户端 CPU 55%      → ★ 两侧都不满,但加起来 95%
③ jstat:FGC 稳定,FGCT 很小                     → 排除 GC ✓
④ jstack:线程多在 RUNNABLE(正常处理中),无 BLOCKED → 排除锁 ✓
⑤ 结论:瓶颈是「两个 JVM 抢同一台机器的 CPU」★
⑥ 修法:压测机与服务分离([T2-003](./T2-003) 的 5.6 ①)

★ 第 ② 步就是本轮"应该做但没做"的事 ✓

6.3 反例:不看指标就"优化" ​

text
"QPS 上不去 → 先加内存"          ✗ 没依据
"再调大线程池"                    ✗ 没依据
"再加索引"                        ✗ 没依据(可能是写操作)

结果:可能把环境改得更复杂,而问题依旧 ✗✗

7. 实测与实验设计 ​

本节没有新增代码——它是一个"实验任务" ✓

★ 诚实说明:

T2-003 的压测只采集了客户端侧的指标(QPS、延迟分位), 没有同时采集服务端的 CPU / GC / 连接池 ✗ —— 所以那一轮"瓶颈在哪"只有推断,没有确认 ✓

留给你的实验(这正是本节要训练的能力):

text
① 启动服务,记下它的 PID
② 另开终端:top -p <pid> 与 jstat -gcutil <pid> 1000
③ 再开一个终端跑压测(并发 64 那一档)
④ 观察:
     · top 的 CPU% 是多少?有没有到 100%?(Java 进程的 CPU 可以是 100% × 核数)
     · jstat 的 FGC 有没有增长?FGCT 增长多少?
     · 压测客户端的 CPU 又是多少?
⑤ 得出结论:瓶颈是 CPU、GC,还是别的?

★ 判读提示:

观察结论
服务端 CPU ≈ 单核 100%单线程瓶颈(锁 / 单线程消费)★
服务端 CPU ≈ 核数 × 100%CPU 打满 → 扩容或优化算法 ✓
服务端 CPU 不高、客户端 CPU 很高压测机才是瓶颈 ✗✗(本轮最可能的情况)
FGC 持续增长GC 瓶颈 → 调堆或减少对象 ★

★ 这个实验的价值:它把"推断"变成"证据" ✓✓

8. Debug 观察点 ​

  1. top -H -p <pid>:看线程级的 CPU(找出最耗 CPU 的那个线程)★,再用 jstack 把它的十六进制 nid 对上去 ✓;
  2. jstat -gcutil 与延迟曲线的对应:看 FGC 的时刻是否是延迟尖峰的时刻 ★;
  3. Actuator 的指标端点(《Spring Boot 自动配置与项目结构》):/actuator/metrics 里有 jvm.memory.used、hikaricp.connections.active、http.server.requests ✓;
  4. 观察"瓶颈转移":把压测客户端换成另一台机器后重压,看 QPS 上限是否变了 ★★。

9. 常见错误 ​

#错误说明
1不看服务端指标就优化 ✗✗试错十次(第 4 节)✗✗
2只看 CPU 不看饱和度 ⚠️利用率低但排队严重 = 有隐形串行点 ✗
3忽略压测机自身的负载 ✗✗它也可能满载(本轮就是)✓
4以为优化一次就够 ✗瓶颈会转移 ✓(5.6)
5在测试环境调优后直接套用到生产 ✗数据量、网络、硬件都不同 ⚠️
6只看 GC 次数不看 GC 耗时 ⚠️"1000 次新生代 GC"可能比"1 次 Full GC"代价小 ✓

10. 动手实验 ​

目标:把"推断瓶颈"变成"确认瓶颈"。

步骤:

  1. 完整做一遍第 7 节的实验(top + jstat + 压测同时进行)★;
  2. 记录数据:服务端 CPU%、客户端 CPU%、FGC 增量、QPS;
  3. 下结论:本轮 QPS 卡在 69 的真正原因是什么?✓;
  4. 验证"瓶颈转移":把压测总请求数调大、并发调更大,看能否找到"下一个瓶颈" ★;
  5. 用 Actuator 采集一次应用层指标(线程池、连接池)✓。

验收方式:能说出"服务端 CPU 与客户端 CPU 各是多少",并据此判断瓶颈归属。

扩展任务:给 LoadTestLesson 加"服务端指标采集"能力:

  • 压测前/后各采集一次 jstat -gcutil,输出 FGC 增量 ✓;
  • 说明:如果 FGC 增量为 0,能排除哪个方向的嫌疑?✓

11. 练习题 ​

  1. 判断:优化性能时,应该先把所有可疑点都优化一遍。(对 / 错)
  2. 填空:USE 方法看三个维度:、、______;其中最能暴露"隐形串行点"的是 ______。
  3. 简答:为什么"QPS 卡在一个整数附近"要怀疑是池的上限?
  4. (代码阅读)
text
压测并发 100:QPS 500,CPU 服务端 35%,客户端 30%
jstat:FGC 0 次
jstack:大量线程 WAITING 在 "HikariPool-1" 相关栈上

瓶颈在哪?怎么修? 5. (编程)写一个"压测前后服务端指标对比"的脚本:启动压测前记录 jstat 与 top 快照,压测后再次记录并输出增量。

参考答案(要点)
  1. 错。先测出瓶颈,再优化它 ✓ ——否则是在优化不存在的瓶颈 ✗(5.6)
  2. 利用率、饱和度、错误;饱和度 ✓
  3. 因为池的大小是人设的 ✓ ——它会形成一个"硬上限",快到上限时请求开始排队,表现为 QPS 卡死 + 延迟上升 ✓✓
  4. 瓶颈是数据库连接池(HikariPool 上大量等待)✗ ——CPU 都不高、GC 正常,说明不是计算资源问题 ✓ 修法:先确认池大小(如 maximumPoolSize=10)→ 适度调大(同时要确认数据库能承受更多连接)⚠️ → 更根本的是看 SQL 是否够快(连接持有时间越长,池越容易被占满)✓
  5. 要点:快照命令 + 解析输出 + 计算增量 ✓ ——本质就是简易的监控系统(T2-016 的核心)✓

12. 掌握标准 ​

  • [ ] 能说出监控的四个层次 ★★★;
  • [ ] 会用 USE 方法组织排查,并知道"饱和度"的价值 ★★;
  • [ ] 会用 jstat / jstack 判断 GC 与线程问题 ★★★;
  • [ ] 能说出六类瓶颈的特征,并对应到具体指标 ★★★;
  • [ ] 能根据"QPS 卡死 + 延迟上升"推断瓶颈类型 ★★;
  • [ ] 知道"瓶颈会转移",所以优化是一个循环 ★★★;
  • [ ] 知道压测时必须同时采集服务端指标(否则只能推断)✓。

13. 扩展资源 ​

14. 下一步学习 ​

性能测试五节(T2-003 / T2-013 / T2-014 / T2-015 / T2-016)到此完成。TB 剩余:

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引