文章目录
-
- 这是我们排查最多的
- CPU不高≠系统在"正常工作"
- [CPU 低系统慢,原因何在?](#CPU 低系统慢,原因何在?)
- [Java 应用怎么查?](#Java 应用怎么查?)
- [Prometheus + Grafana 为啥看不出问题?](#Prometheus + Grafana 为啥看不出问题?)
- 中小团队常忽视的"慢性事故"
- 一个更靠谱的判断逻辑
- 写在最后
这是我们排查最多的
这是一次非常典型的事故场景。
业务反馈系统响应明显变慢,
用户投诉集中在「接口卡顿」「页面转圈」。
首先打开系统监控进行排查:
CPU Usage: 30%
Memory Usage: 55%
Load Average: 0.8
Disk IO: 正常
Grafana指标全都正常。
从资源视角看,系统甚至谈不上有压力。
但事实是------
请求已经慢到用户无法接受。
CPU不高≠系统在"正常工作"
这是很多团队在监控体系上踩的第一个认知坑。
CPU 只代表一件事:
CPU 正在消耗多少计算资源
它完全不能反映:
- 请求是否被阻塞
- 线程是否在等待
- I/O 是否成为瓶颈
- 业务链路是否已经"局部瘫痪"
一句话总结:
CPU 低,只能说明系统没在算,不代表系统没在等。
CPU 低系统慢,原因何在?
1️⃣线程池被 I/O 阻塞"悄悄吃满"
我们先看一段最常见的 Java 服务结构:
@RestController
public Result queryOrder() {
Order order = orderService.getOrder(id);
return Result.ok(order);
}
在代码层面,它看起来是同步、顺序、可控的。
但在运行时,真实路径可能是:
HTTP Request
↓
Tomcat Worker Thread
↓
数据库连接池获取连接(阻塞)
↓
执行 SQL(慢查询)
↓
等待下游 RPC 返回
如果其中任意一步变慢:
- Tomcat 工作线程被占用
- 新请求开始排队
- 响应时间指数级上升
而 CPU 使用率呢?
几乎不变。
因为线程大多数时间都在
WAITING / TIMED_WAITING。
2️⃣连接池耗尽,比 CPU 打满更致命
我们在事故中经常看到这样的监控组合:
DB CPU: 40%
DB QPS: 正常
Application TPS: 下降
问题在哪?
HikariCP Active Connections: 50 / 50
Waiting Threads: 持续增长
这意味着:
- 数据库还能扛
- 但应用已经拿不到连接
- 请求在 getConnection()阶段就被阻塞
从 JVM 角度看:
HTTP-8080-exec-123" waiting on condition
系统没有崩溃,
但已经无法提供有效服务。
3️⃣一个慢接口,拖垮整个系统吞吐
很多团队忽略了一个事实:
系统吞吐 ≈ 最慢路径的能力
假设你的接口响应时间从 50ms 变成 300ms:
原 QPS ≈ 1000
实际 QPS ≈ 160
CPU 依然很低,
但线程池开始排队,延迟开始堆积。
这类问题的典型特征是:
- CPU 不高
- 内存不满
- 但 P95 / P99 延迟持续升高
如果你只盯着平均值和 CPU,是完全感知不到的。
Java 应用怎么查?
当你确认:
- CPU 不高
- 内存正常
- 但请求明显变慢
第一件事,别再盯 Grafana 了。
你需要直接进入 JVM 内部,看它到底在"忙什么"。
1️⃣ 先看线程:CPU 不高,线程在干嘛?
第一步,永远是线程状态。
jstack <pid> > jstack.log
重点不是线程数量,而是状态分布:
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
在"CPU 低但系统慢"的事故中,我们最常看到的是:
- RUNNABLE 很少
- WAITING / TIMED_WAITING 占大多数
典型线程栈长这样:
"HTTP-8080-exec-124" prio=5 tid=0x00007f8c940 waiting
at java.util.concurrent.locks.LockSupport.park()
at java.util.concurrent.FutureTask.get()
这说明什么?
线程没有在算,而是在等结果。
等什么?
- 数据库返回
- 下游 RPC
- 锁释放
- 线程池资源
2️⃣ 看线程池:不是没线程,是用不上线程
很多团队只关心线程池大小,却不看运行状态。
如果你用的是 ThreadPoolExecutor,重点看这几个指标:
activeCount
queueSize
completedTaskCount
一个非常危险的组合是:
activeCount ≈ maxPoolSize
queueSize 持续增长
这意味着:
- 线程已经被慢任务占满
- 新请求只能排队
- 延迟开始指数级放大
而 CPU?
依然不高。
3️⃣ 再看 GC:不是 Full GC,但"轻微抖动"很要命
很多人一看到系统慢,就下意识否定 GC:
"没有 Full GC,应该不是 GC 问题。"
但真实情况是:
- 频繁 Young GC
- Stop The World 很短,但次数极多
你会在 GC 日志里看到类似:
[GC (Allocation Failure) 256M->128M(512M), 15ms]
15ms 不长,但如果:
每秒 20 次
那对延迟型服务来说,就是灾难。
尤其是:
- 接口本身就慢
- 请求已经在排队
GC 抖动会直接放大用户感知延迟。
4️⃣ 堆没满,但对象"活得太久"
这是非常容易被忽略的一点。
> jmap -histo <pid> | head -20
你可能会看到:
num
#instances
#bytes
class name
---------------------------------------
1: 8,000,000 640MB byte[]
2: 2,300,000 184MB java.lang.String
这说明:
- 对象在堆里大量堆积
- GC 清不掉
- 线程在分配内存时越来越慢
CPU 不高,
但 JVM 已经开始效率衰减。
5️⃣ 最后看一个致命点:同步与锁
如果线程栈里频繁出现:
java.lang.Object.wait()
java.util.concurrent.locks.AbstractQueuedSynchronizer
那你基本可以确认:
系统慢,不是因为算得慢,而是锁抢不过来。
这类问题的特点是:
- CPU 利用率低
- 吞吐下降明显
- 延迟突然拉长
而且,扩容几乎无效。
Prometheus + Grafana 为啥看不出问题?
因为大多数监控只做了资源观测,没有做系统行为观测。
常见指标是:
node_cpu_seconds_total
node_memory_MemAvailable_bytes
但真正该关注的,是这些:
http_server_requests_seconds_bucket
jvm_threads_state{state="BLOCKED"}
hikaricp_connections_active
mysql_global_status_threads_running
如果你没有:
- 接口分位延迟(P95 / P99)
- 线程池状态
- 连接池使用情况
- 关键依赖的响应时间
那么监控只能告诉你一句话:
"服务器还活着。"
但业务是否健康,它不知道。
中小团队常忽视的"慢性事故"
我们复盘过大量事故后发现:
这类问题很少第一时间报警
通常是用户先感知
再由人肉排查发现
原因只有一个:
监控体系没有覆盖"用户体验劣化"的早期信号
等到 CPU 真正升高时,
系统往往已经处在雪崩边缘。
一个更靠谱的判断逻辑
与其问:
"CPU 高不高?"
不如问这三个问题:
- 请求在系统中卡在哪一层?
- 哪个资源正在成为隐形瓶颈?
- 如果现在继续变慢,谁能第一时间发现?
真正成熟的运维体系,
不是等系统挂了再报警,
而是能在**"慢"刚开始出现时就介入**。
写在最后
CPU 只有 30%,系统却慢到不可用,
从来不是一个偶发问题。
它往往意味着:
- 系统已经进入亚健康状态
- 只是还没触发致命阈值
真正的分水岭,不在于是否出过事故,而在于:
系统开始变慢的那一刻, 你能不能看见?
所以、单纯的监控系统层面的cpu、内存、磁盘等等,
是远远不够的。
线程在等什么?
连接池还有多少空闲?
GC 暂停是否隐形拖垮了延迟?
数据库/Redis 调用是否在异常?
只有把以下这些 JVM 核心亚健康指标
实时采集、可视化、设置阈值告警,
你才能在"页面刚开始卡"而不是"系统彻底挂"的时候发现问题。