CPU 只有 30%,系统却慢到不可用?
这是我们排查最多的
“假健康事故”
这是一次非常典型的事故场景。
业务反馈系统响应明显变慢,
用户投诉集中在「接口卡顿」「页面转圈」。
首先打开系统监控进行排查:
CPU Usage: 30%Memory Usage: 55%Load Average: 0.8Disk IO: 正常
Grafana指标全都正常。
从资源视角看,系统甚至谈不上有压力。
但事实是——
请求已经慢到用户无法接受。
CPU不高≠系统在“正常工作”
这是很多团队在监控体系上踩的第一个认知坑。
CPU 只代表一件事:
CPU 正在消耗多少计算资源
它完全不能反映:
请求是否被阻塞
线程是否在等待
I/O 是否成为瓶颈
业务链路是否已经“局部瘫痪”
一句话总结:
CPU 低,只能说明系统没在算,不代表系统没在等。
CPU 低系统慢,原因何在?
1️⃣线程池被 I/O 阻塞“悄悄吃满”
我们先看一段最常见的 Java 服务结构:
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 / 50Waiting 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重点不是线程数量,而是状态分布:
RUNNABLEBLOCKEDWAITINGTIMED_WAITING
在“CPU 低但系统慢”的事故中,我们最常看到的是:
RUNNABLE 很少
WAITING / TIMED_WAITING 占大多数
典型线程栈长这样:
"HTTP-8080-exec-124" prio=5 tid=0x00007f8c940 waitingat java.util.concurrent.locks.LockSupport.park()at java.util.concurrent.FutureTask.get()
这说明什么?
线程没有在算,而是在等结果。
等什么?
数据库返回
下游 RPC
锁释放
线程池资源
2️⃣ 看线程池:不是没线程,是用不上线程
很多团队只关心线程池大小,却不看运行状态。
如果你用的是 ThreadPoolExecutor,重点看这几个指标:
activeCountqueueSizecompletedTaskCount
一个非常危险的组合是:
activeCount ≈ maxPoolSizequeueSize 持续增长
这意味着:
线程已经被慢任务占满
新请求只能排队
延迟开始指数级放大
而 CPU?
依然不高。
3️⃣ 再看 GC:不是 Full GC,但“轻微抖动”很要命
很多人一看到系统慢,就下意识否定 GC:
“没有 Full GC,应该不是 GC 问题。”
但真实情况是:
频繁 Young GC
Stop The World 很短,但次数极多
你会在 GC 日志里看到类似:
[]15ms 不长,但如果:
每秒 20 次那对延迟型服务来说,就是灾难。
尤其是:
接口本身就慢
请求已经在排队
GC 抖动会直接放大用户感知延迟。
4️⃣ 堆没满,但对象“活得太久”
这是非常容易被忽略的一点。
jmap -histo <pid> | head -20你可能会看到:
num#instances#bytesclass 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_totalnode_memory_MemAvailable_bytes
但真正该关注的,是这些:
http_server_requests_seconds_bucketjvm_threads_state{state="BLOCKED"}hikaricp_connections_activemysql_global_status_threads_running
如果你没有:
接口分位延迟(P95 / P99)
线程池状态
连接池使用情况
关键依赖的响应时间
那么监控只能告诉你一句话:
“服务器还活着。”
但业务是否健康,它不知道。
中小团队常忽视的“慢性事故”
我们复盘过大量事故后发现:
这类问题很少第一时间报警
通常是用户先感知
再由人肉排查发现
原因只有一个:
监控体系没有覆盖“用户体验劣化”的早期信号
等到 CPU 真正升高时,
系统往往已经处在雪崩边缘。
一个更靠谱的判断逻辑
与其问:
“CPU 高不高?”
不如问这三个问题:
请求在系统中卡在哪一层?
哪个资源正在成为隐形瓶颈?
如果现在继续变慢,谁能第一时间发现?
真正成熟的运维体系,
不是等系统挂了再报警,
而是能在**“慢”刚开始出现时就介入**。
写在最后
CPU 只有 30%,系统却慢到不可用,
从来不是一个偶发问题。
它往往意味着:
系统已经进入亚健康状态
只是还没触发致命阈值
真正的分水岭,不在于是否出过事故,而在于:
系统开始变慢的那一刻,
你能不能看见?
所以、单纯的监控系统层面的cpu、内存、磁盘等等,
是远远不够的。
线程在等什么?
连接池还有多少空闲?
GC 暂停是否隐形拖垮了延迟?
数据库/Redis 调用是否在异常?
只有把以下这些 JVM 核心亚健康指标
实时采集、可视化、设置阈值告警,
你才能在“页面刚开始卡”而不是“系统彻底挂”的时候发现问题。



