Web 系统中 CPU 飙高的可能原因分析
CPU 飙高(CPU 使用率持续超过 80%,甚至接近100%)是系统性能中最常见的问题,可能影响系统响应速度、服务可用性甚至导致宕机。
在 Java Web 系统中,若同时使用 Redis、MySQL、ES等中间件,CPU 飙升的原因可能不仅来自 Java 应用本身,也可能由这些外部组件的交互、配置或异常行为引发,需结合相关工具,进行端到端根因分析,才能精准定位和解决。。
Java 线程堆栈(jstack/Arthas)
GC 情况(jstat)
中间件慢查询日志(Redis/MySQL/ES)
连接池/线程池监控
系统资源指标(CPU、上下文切换、I/O)
以下从 Java 应用层、Redis、MySQL、系统交互层 等维度深入分析 CPU 飙升的可能原因。
Java 应用层
高并发、高吞吐的业务消耗较多CPU资源,如果CPU资源未达到瓶颈,属于正常业务场景。
1 | #查看 cpu 占用率高的线程 |
死循环或业务逻辑异常
-
某个 Controller 或 Service 方法陷入死循环(如 while(true)、递归无终止)。
-
复杂业务逻辑(如多重嵌套循环、大 List 遍历、未分页的全表遍历)。
-
递归调用无终止条件或深度过大。
-
正则表达式回溯爆炸(如
.*.*.*匹配长字符串)。
GC 频繁(Full GC / Young GC)
-
内存泄漏 → 对象堆积 → 触发 Full GC → STW + CPU 飙升。
-
大对象分配、缓存未清理、MyBatis 一级缓存过大等。
-
GC 线程本身是 CPU 密集型(尤其是 ParallelGC、G1 并发阶段)。
-
内存泄漏或对象创建过快,导致 Full GC 频繁触发。
1 | # 通过 `jstat`、`jstack`、GC 日志分析。 |
线程池配置不当 / 锁竞争
-
线程池过小 → 请求堆积 → 重试/超时 → CPU 忙等(瞬时大量请求涌入,线程池满负荷运行)。
-
synchronized / ReentrantLock 竞争激烈 → 线程 BLOCKED → 上下文切换频繁 → CPU 飙升。
-
数据库连接池耗尽 → 线程阻塞等待 → 后续请求堆积。
-
未做限流、降级或熔断机制,导致 CPU 被打满。
-
同步阻塞操作过多(如同步 I/O、锁竞争),线程频繁切换。
排查:
jstack查看大量 BLOCKED/WAITING 线程;Arthas
thread -n 3查看最忙线程。
序列化/反序列化开销大
-
大量 JSON(如 FastJSON/Jackson)解析大对象(如 ES 返回万条数据)。
-
对象深度嵌套、循环引用、无缓存的反射调用。
算法复杂度高
-
使用了时间复杂度为 O(n²)、O(n³) 甚至更高的算法处理大数据量。
-
未优化的排序、搜索、正则匹配等操作在高频调用下拖垮 CPU。
Java实例CPU过高的排查方法
1 | top |
-
使用top命令找到占用CPU较高的进程,并记录下 PID(进程号)
ps H -eo user,pid,ppid,tid,time,%cpu,cmd --sort=%cpu得到 4042或者根据
jps -l,或ps -ef | grep java命令找出java应用程序对应的进程ID号 -
根据进程ID查找其下的全部线程
top -Hp PID按照CPU使用率倒序排序 , 如:top -Hp 4042得到 14068或者用jstack 命令,直接查询线程状态
-
将线程ID,转换为16进制
printf "%x\n" PID,如:printf "%x\n" 14068得到 36f4 -
定位到具体的线程
jstack 进程ID| grep -a 16进制线程ID, 如:jstack 4042 | grep -a 36f4可能出错:
Unable to open socket file: target process not responding or HotSpot VM not loaded
原因分析:- jvm运行时会生成目录
/tmp/hsperfdata_<username>/<pid>,用于存放jvm进程信息。 - jps、jstack等工具读取该目录下的pid文件获取连接信息。
- 操作系统为了防止/tmp目录文件过多,有删除管理机制:每天用tmpwatch命令检查并删除 /tmp下超过240小时未访问过的文件和目录。
解决方法:
-
编辑配置,
vim /etc/cron.daily/tmpwatch1
2
3
4
5# 添加排除规则。使用 --exclude 参数来排除 hsperfdata 目录
/usr/sbin/tmpwatch "$flags" 240 /tmp --exclude=/tmp/hsperfdata_*
# 排除所有用户下的该目录:
/usr/sbin/tmpwatch "$flags" 240 /tmp --exclude='^/tmp/hsperfdata_' -
或者 更改 JVM 的临时目录
java -Djava.io.tmpdir=/data/tmpdir -jar demo.jar
- jvm运行时会生成目录
-
将某个进程的全部堆栈信息放入临时文件
jstack PID > /opt/temp.txt,如:jstack 4042 > /opt/temp.txt -
使用vi命令查看该文件,然后输入线程转换后的16进制数字搜索,查看线程状态,如:1006

-
线程名称:SimplePauseDetectorThread_1
-
线程类型:daemon
-
优先级prio: 5,默认是5
-
jvm线程id:tid=0x00007f…,jvm内部线程的唯一标识(通过java.lang.Thread.getId()获取,通常用自增方式实现。)
-
对应系统线程id(NativeThread ID):nid=0xbd2,和top命令查看的线程pid对应,不过一个是10进制,一个是16进制。(通过命令:top -H -p pid,可以查看该进程的所有线程信息)
-
线程状态:waiting on condition / Object.wait() / waiting for monitor entry
-
起始栈地址:[0x00007f…]
对于thread dump信息,主要关注的是线程的状态和其执行堆栈,堆栈信息应该逆向解读(从下往上)
-
DB 层相关原因
在使用 MySQL 的过程中会遇到各种瓶颈问题,常见的是 IO 瓶颈,但是有时候会出现服务器 CPU 使用率超过 100%,应用页面访问慢,登录服务器负载很高。
慢查询导致线程阻塞
-
未加索引、索引失效、关联查询复杂 → SQL 执行慢 → Java 线程阻塞等待 → 请求堆积 → CPU 飙升(忙等/重试)。
-
MyBatis 生成 N+1 查询,循环中查数据库。
MySQL
slow_query_log、SHOW PROCESSLISTArthas
monitor或trace拦截 Mapper 方法耗时
EXPLAIN分析执行计划
连接池耗尽 / 事务未提交
-
Druid/HikariCP 连接池 maxActive 设置过小 → 获取连接阻塞。
-
事务未提交(如忘记 commit 或异常未回滚)→ 连接占用 → 后续线程阻塞。
监控活跃连接数、等待线程数。
日志搜索 “wait millis”、“getConnection timeout”。
大结果集查询 + Java 处理开销
-
SELECT * FROM table无分页 → 返回 10 万行 → Java 端循环处理 → CPU 飙升。 -
ResultSet 未及时关闭 → 内存 + CPU 双重压力。
DB实例CPU过高的排查方法
SQL 问题导致 CPU 使用率过高是最常见的现象,比如 group by、order by、join 等,这些很大程度影响 SQL 执行效率,从而占用大量的系统资源。
-
SQL分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20# 查看 MySQL 进程(两种方式)
show full processlist;
select * from information_schema.processlist;
# command:执行的数据库操作类型
# sleep:休眠状态
# Query:查询状态
# connect:连接状态
# time:已经执行的时间,单位秒
# info:已经执行的 SQL
# state:SQL 执行的状态,结果是 SQL 语句整个执行状态中的一个,其中包含很多状态
# 根据问题线程 id 定位 MySQL 中的 SQL:
select a.user,a.host,a.db,b.thread_os_id,b.thread_id,a.id processlist_id,a.command,a.time,a.state,a.info from information_schema.processlist a,performance_schema.threads b
where a.id = b.processlist_id and b.thread_os_id=32272;
#根据问题线程 id 查看其他监控指标:
select *
from performance_schema.events_statements_current
where thread_id in
(select thread_id from performance_schema.threads where thread_os_id = 32272)\G; -
慢查询日志
开启慢日志记录
1
2
3
4
5# 开启慢日志记录
set global slow_query_log=ON;
# 查看慢日志信息,日志中记录了慢SQl
show variables like 'slow_query_log%'; -
排查日志
1
2
3
4
5
6# 更改排查日志为 TABLE 方式,默认为 FILE 方式
set global log_output='TABLE';
set global general_log=ON;
# 查询排查日志内容:
SELECT * from mysql.general_log ORDER BY event_time DESC; -
其他配置
-
检查 MySQL 连接数当前使用是否超过限制。如果超出限制,而且之前的连接没有得到释放,那新的连接肯定会连接不到,造成连接延迟,影响效率。
-
MySQL 的 timeout 参数设置问题
-
wait_timeout:MySQL 在关闭一个非交互的连接之前所要等待的秒数,其取值范围在 windows 系统中为 1-2147483,linux 系统中为 1-31536000,默认值 28800。
-
interactive_time:MySQL 在关闭一个交互的连接之前所要等待的秒数(如 mysql gui tool 中的连接),其取值范围随 wait_timeout 变动,默认值 28800。
交互连接:即在 mysql_real_connect() 函数中使用了 CLIENT_INTERACTIVE 选项。通过 MySQL 客户端连接数据库。
非交互式连接:通过 jdbc 连接数据库。
在 MySQL 默认设置下,当一个连接的空闲时间超过 8 小时后,当业务出现了高峰期,肯定会造成有太多的 TCP 连接没关闭,数据库连接数会不够用。从而会产生 CPU 占用过高,服务器告警等问题。
访问一次对数据库操作量不大,查询完数据就完成 ok 了,wait_timeout 设置在 120s 内。
在 MySQL 的官网,修改以上两个参数必须修改 interactive_timeout。如果修改 interactive_timeout,则 wait_timeout 也发生变化,如果只修改 wait_timeout,不生效。
-
Redis 相关原因
使用不当,例如高消耗命令、热Key、大Key等,导致CPU使用率异常升高。当平均CPU使用率高于70%、连续5分钟内的CPU平均峰值使用率高于90%时。
CPU使用率高,主要分为以下三种现象:
某个时间段,CPU使用率突然升高。高并发,数据查询操作频繁。
某个数据节点的CPU使用率较高。可能是数据热点和倾斜问题。
某个Proxy节点的CPU使用率较高。负载均衡问题,可能是某个IP的攻击或爬虫。
导致CPU使用率异常的主要因素:
高消耗命令:即时间复杂度为O(N)的命令,其中N为较大值。通常情况下,命令的时间复杂度越高,在执行时会消耗越多的资源,从而导致CPU使用率上升。例如KEYS、HGETALL或使用MGET、MSET、HMSET、HMGET一次操作大量Key等。
由于命令执行单元为单线程的特性,实例在执行高消耗命令时会引发排队导致应用响应变慢。极端情况下,甚至可能导致实例被整体阻塞,引发应用超时中断或流量跳过缓存层直接到达后端的数据库侧,引发雪崩效应。
热Key:某个或某部分Key的请求访问次数显著超过其他Key时,代表此时可能产生了热Key。热Key将会消耗实例的大量CPU资源,从而影响其他Key的访问时延。并且,在集群架构中,如果热Key较为集中地分布在部分数据分片节点,可能会导致CPU使用率倾斜(个别分片的CPU使用率远超其他分片)。
大Key:大Key会占用更多的内存,同时,对大Key的访问会显著增加实例的CPU负载和流量。大Key在一定程度上更容易形成热点从而造成CPU使用率高。如果大Key较为集中地分布在部分数据分片节点,可能会导致CPU使用率倾斜、带宽使用率倾斜及内存使用率倾斜。
短连接:频繁地建立连接,导致实例的大量资源消耗在连接处理上。
AOF:实例默认开启了AOF(append-only file),当实例处于高负载状态时,AOF的写盘行为将会导致CPU使用率升高及实例整体的响应时延增加。
大 Key / 热 Key 操作阻塞
-
单个 Key 数据过大(如 List 有 100 万元素),执行
LRANGE、HGETALL等命令阻塞 Redis 单线程。 -
导致 Java 客户端线程长时间等待 → 超时重试 → CPU 飙升。
Redis
SLOWLOG GET 10
redis-cli --bigkeys监控 QPS、响应时间突增
连接泄露 / 连接池耗尽
-
Jedis/Lettuce 连接未正确释放 → 连接池耗尽 → 获取连接阻塞 → 线程堆积 → CPU 忙等。
-
Lettuce 异步回调未处理异常 → 回调线程池被打满。
日志中是否有
Timeout waiting for connection。监控 Redis 连接数(
INFO clients)。Arthas
watch拦截 getConnection 方法。
频繁 Pipeline / 批量操作未优化
-
一次 Pipeline 执行上千条命令,虽高效但若数据量大,序列化/网络开销仍高。
-
客户端 CPU 消耗在网络编解码或数据组装上。
Redis实例CPU过高的排查方法
业务运行超预期,Redis开源版实例的CPU资源无法满足业务需求,可通过增加分片数、副本数。
启用监控
可以使用metricbeat+filebeat+monitori进行监控。也可以通过redis proxy 进行代理监控。
CPU使用率突然升高
-
排查并禁用高消耗命令
-
通过性能监控功能,确认CPU使用率高的具体时间段。
-
通过下述方法,找出高消耗的命令:
慢日志功能会记录执行超过指定时间阈值的命令。根据指定时间段和节点的慢查询语句和执行时长,可找出执行时间较长的高消耗命令。redis.conf 慢日志配置 和 查询
1
2
3
4
5
6
7
8
9# 执行时间大于多少微秒(microsecond,1秒 = 1,000,000 微秒)的查询进行记录。
slowlog-log-lower-than 1000
#最多能保存多少条日志
slowlog-max-len 200
#打印所有 slow log ,最大长度取决于 slowlog-max-len 选项的值
SLOWLOG GET
# 只打印指定数量的 slow log日志。
SLOWLOG GET number -
评估并禁用高风险命令和高消耗命令,例如FLUSHALL、KEYS、HGETALL等。
1
2
3
4
5# 修改配置文件redis.conf,添加如下内容,禁用命令
rename-command KEYS ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG "" -
根据业务情况,调整实例为读写分离架构,对高消耗命令或应用进行分流。
-
-
排查并优化短连接
- 通过性能监控功能,确认CPU使用率高的具体时间段。
- 在性能监控页面,查看是否有CPU使用率较高,连接数较高,但QPS(每秒访问次数)未达到预期的现象。如果有,说明可能存在短连接
- 将短连接调整为长连接,例如使用JedisPool连接池连接。
-
关闭AOF
实例默认开启了AOF(append-only file),当实例处于高负载状态时,频繁地执行AOF会一定程度上导致CPU使用率升高。
在业务允许的前提下,您可以考虑关闭持久化。另外将实例的数据备份时间设定到低访问/维护时间窗口内,降低影响。
节点CPU使用率不一致
如果实例为集群架构或读写分离架构,实例的部分数据分片节点的CPU使用率高,而其他数据分片节点的CPU使用率较低
-
排查并禁用高消耗命令
方法如上
-
排查并优化热点Key
- 通过性能监控功能,确认CPU使用率高的具体时间段。
- 在实时Top Key统计的历史页面,选择CPU使用率高的数据节点,可看到CPU使用率高的时间段内有哪些热点Key。
-
启用代理查询缓存功能(Proxy Query Cache),代理节点会缓存热点Key对应的请求和查询结果,当在有效时间内收到同样的请求时直接返回结果至客户端,无需和后端的数据分片交互,可改善对热点Key的发起大量读请求导致的访问倾斜。
-
如果热Key的产生来自于读请求,您可以将实例改造成读写分离架构来降低每个数据分片的读请求压力。
-
排查并优化大Key
-
Redis自带的
BIGKEYS命令可以查询当前Redis中所有key的信息,对整个数据库中的键值对大小情况进行统计分析。会输出每种数据类型中最大的 big key 的信息,对于 String 类型来说,会输出最大 big key 的字节长度,对于集合类型来说,会输出最大 big key 的元素个数。
BIGKEYS命令会扫描整个数据库,这个命令本身会阻塞Redis,找出所有的大键,并将其以一个列表的形式返回给客户端。
redis-cli --bigkeys -
Redis4.0之前,使用命令
debug object key查看某个key的详细信息,包括该key的value大小等1
2
3
4
5
6
7
8redis 127.0.0.1:6379> DEBUG OBJECT key1
Value at:0xb6838d20 refcount:1 encoding:raw serializedlength:9 lru:283790 lru_seconds_idle:150
# Value at:0xb6838d20:key 所在的内存地址。
# refcount:1:引用计数,表示该对象被引用的次数。
# encoding:raw:编码类型,这里是 raw ,表示这个字符串对象的编码类型。
# serializedlength:9:序列化后的长度。
# lru:283790:LRU (Least Recently Used)信息,即最近最少使用算法的相关信息,在内存淘汰策略中会用到。
# lru_seconds_idle:150:该 key 已空闲多久(单位为秒),也就是自从最后一次访问已经过去多少秒。 -
4.0版本及以上,更推荐使用
memory usag命令。memory usage命令采用抽样的方式,默认抽样5个元素,所以计算是近似值,我们也可以手动指定抽样的个数。1
2
3
4
5127.0.0.1:6379> memory usage k1
(integer) 57 # 这里k1 value占用57字节内存
127.0.0.1:6379> MEMORY usage hkey SAMPLES 1000
(integer) 617977753 # 指定SAMPLES为1000,分析hkey键内存占用617977753字节 -
通过工具 rdbtools 分析大key,可以分析静态rdb文件并生成csv格式的内存报告
rdb -c memory dump.rdb > memory.csv
解决方法
避免使用过大的value。如果需要存储大量的数据,可以将其拆分成多个小的value。
避免使用不必要的数据结构。例如,如果只需要存储一个字符串,就不要使用Hash或者List等数据结构。
定期清理过期的key。如果Redis中存在大量的过期key,就会导致Redis的性能下降。
对象压缩。
当发现存在Big Key问题时,我们需要及时采取措施来解决这个问题。下面列出几种可行的解决思路:
-
分割大key
将Big Key拆分成多个小key。这个方法比较简单,但是需要修改应用程序的代码。就像是把一个大蛋糕切成小蛋糕一样。或者尝试将Big Key转换成Redis的其他数据结构。例如,将Big Key转换成Hash,List或者Set等数据结构。 -
对象压缩
如果大key的产生原因主要是由于对象序列化后的体积过大,可以考虑使用压缩算法来减小对象的大小。需要在客户端使用一些压缩算法对数据进行压缩和解压缩操作,例如LZF、Snappy等。 -
直接删除
Redis 4.0+的版本,直接使用unlink命令异步删除大key。
4.0以下的版本 可以考虑使用scan命令,分批次删除。
系统交互与架构层面
缓存穿透 / 缓存雪崩
-
Redis 缓存穿透 → 大量请求打到 MySQL → MySQL 压力大 → 响应慢 → Java 线程阻塞 → CPU 飙升。
-
缓存雪崩 → 同时失效 → 所有请求查 DB → DB 崩 → 应用重试 → CPU 打满。
解决方式:布隆过滤器、空值缓存、随机过期时间。
服务雪崩 / 级联故障
-
ES 查询慢 → 接口超时 → 重试 → MySQL 被拖慢 → Redis 连接池耗尽 → 整个系统线程阻塞 → CPU 飙升。
解决方式:熔断(Hystrix/Sentinel)、降级、超时控制。
异步任务堆积
-
使用
@Async或线程池处理异步任务(如写 ES、发消息),若任务执行慢 → 任务堆积 → 线程持续运行 → CPU 飙升。
排查方式:监控线程池队列大小、活跃线程数。
诊断工具链
| 场景 | 工具 |
|---|---|
| Java 线程分析 | jstack、Arthasthread、`jstack |
| GC 分析 | jstat -gcutil、GC 日志 + GCeasy 分析 |
| 方法耗时 | Arthastrace、monitor、watch |
| Redis 诊断 | redis-cli --bigkeys、SLOWLOG、INFO、RedisInsight |
| MySQL 诊断 | slow_query_log、SHOW PROCESSLIST、EXPLAIN、Arthas 拦截 SQL |
| ES 诊断 | Kibana Monitoring、慢日志、_nodes/hot_threads |
| 系统级 | top -H、pidstat -t -p <java_pid>、perf record、vmstat 1 |
典型排查流程(实战步骤)
-
top→ 找到 Java 进程 PID,确认 CPU 高的是 Java 进程。 -
top -H -p <pid>→ 找到最耗 CPU 的线程 TID。 -
printf "%x\n" <tid>→ 转为 16 进制。 -
查看线程堆栈:
1
jstack <pid> | grep <hex_tid> -A 30
-
如果是
GC task thread→ 查 GC。 -
如果是业务线程 → 看是否在循环、等锁、处理大对象。
-
-
检查 GC:
jstat -gcutil <pid> 1000→ 看 YGC/FGC 频率。 -
检查外部调用:
-
Arthas
trace com.xxx.service.* *→ 看哪个方法耗时高。 -
检查 Redis/MySQL/ES 慢日志。
-
检查连接池、线程池监控指标(如 Druid 监控页、HikariCP JMX)。
-
检查系统日志、业务日志是否有异常堆栈或超时记录。
优化与预防建议
-
代码层:避免大循环、大对象、深递归;合理分页;异步化耗时操作。
-
缓存层:避免大 Key;设置过期时间;使用本地缓存(Caffeine)减轻 Redis 压力。
-
数据库层:SQL 优化 + 索引;避免 SELECT *,避免深度分页;使用连接池监控。
-
架构层:
- 加入熔断降级(Sentinel)。
- 设置合理的超时(Feign、RestTemplate、Redis、DB)。
- 异步化 + 削峰填谷(MQ)。
-
监控层:
- 接入 Prometheus + Grafana + AlertManager。
- APM(SkyWalking/Pinpoint)监控全链路性能。
- 日志中心(ELK)分析异常模式。