SpringBoot2.2.x 版本CPU增高BUG
字数 1106 2025-08-15 21:31:03
Spring Boot 2.2.x CPU增高问题分析与解决方案
问题概述
在Spring Boot 2.2.x版本中,存在一个导致CPU使用率持续增高的严重BUG。该问题在应用运行24小时后突然出现,迫使运维人员不得不重启机器。即使经过压力测试和增加机器资源,问题依然存在。
问题分析过程
1. 初步排查
-
环境信息:
- 测试机配置:1核2GB
- 正式环境配置:比测试机高4倍
-
测试结果:
- 使用JMeter进行压力测试(QPS=90),CPU短暂升高至40%后稳定在10%
- 初步排除接口性能问题
2. 深入排查
线程分析
# 查看Java进程PID
top
# 查看指定PID下的高占用线程
top -Hp pid
# 将高占用线程ID转换为16进制
printf "%x\n" tid
# 查看线程栈信息
jstack pid|grep -A 2000 tid的16进制 > xx.log
# 查看GC情况
jstat -gcutil pid 2000 10
发现:
- 线程状态主要为RUN或TIME_WAITING,无死锁
- YGC(Young GC)次数持续增高,提示堆内存可能有问题
堆内存分析
# 查看堆内存概况
jmap pid
jmap -heap pid
# 查看堆中对象统计(带live参数触发GC)
jmap -histo:live pid|more
关键发现:
ConcurrentLinkedQueue$Node对象数量持续增加,无减少趋势- 队列长度不断增长,导致性能下降
3. 火焰图分析
使用火焰图工具定位到MimeTypeUtils类中过度使用ConcurrentLinkedQueue的问题。
4. 源码分析
Spring Boot 2.2.6修复版本中的ConcurrentLruCache类代码片段:
private static class ConcurrentLruCache<K, V> {
private final int maxSize;
private final ConcurrentLinkedQueue<K> queue = new ConcurrentLinkedQueue<>();
public V get(K key) {
this.lock.readLock().lock();
try {
if (this.queue.size() < this.maxSize / 2) {
V cached = this.cache.get(key);
if (cached != null) {
return cached;
}
} else if (this.queue.remove(key)) {
this.queue.add(key);
return this.cache.get(key);
}
} finally {
this.lock.readLock().unlock();
}
this.lock.writeLock().lock();
try {
if (this.queue.remove(key)) {
this.queue.add(key);
return this.cache.get(key);
}
if (this.queue.size() == this.maxSize) {
K leastUsed = this.queue.poll();
if (leastUsed != null) {
this.cache.remove(leastUsed);
}
}
V value = this.generator.apply(key);
this.queue.add(key);
this.cache.put(key, value);
return value;
} finally {
this.lock.writeLock().unlock();
}
}
}
问题根源:
ConcurrentLinkedQueue.remove()方法在并发情况下可能返回false- 当remove返回false时,代码会继续执行add操作
- 导致队列长度无限增长
- 长队列使remove操作需要遍历整个队列,消耗大量CPU资源
问题复现方法
1. 构造恶意请求
使用JMeter构造包含大量不同Accept头的请求:
示例Accept头:
application/stream+x-jackson-smile,
application/vnd.spring-boot.actuator.v3+json,
application/vnd.spring-boot.actuator.v2+json,
application/json,
multipart/form-data; boundary=----WebKitFormBoundaryVHfecvFDYeDEjhu4,
[更多MIME类型...]
2. 监控队列增长
jmap -histo:live pid | grep java.util.concurrent.ConcurrentLinkedQueue
观察结果:
ConcurrentLinkedQueue$Node数量持续增加- 队列长度突破限制后CPU使用率急剧上升
解决方案
1. 临时解决方案
- 升级硬件:使用多核CPU可减缓问题出现速度
- 降级版本:回退到Spring Framework 5.1.x版本
2. 官方修复方案
- 将
ConcurrentLinkedQueue替换为ConcurrentLinkedDeque - 该修复已包含在Spring Framework 5.3.x版本中
验证代码
验证ConcurrentLinkedQueue在并发下的问题:
import java.util.concurrent.ConcurrentLinkedQueue;
public class Main {
private static ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
Thread thread1 = new QueueThread(String.valueOf(i));
thread1.start();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end");
}
static class QueueThread extends Thread {
private int value = 0;
private String name;
public QueueThread(String name) {
this.name = name;
queue.add(value);
}
@Override
public void run() {
for (int i = 1; i < 1000; i++) {
try {
boolean flag = queue.remove(value);
System.out.println("remove: " + value + " " + flag);
queue.add(value);
value++;
} catch (Exception e) {
System.out.println(e);
}
}
}
}
}
总结
该问题是由于Spring Boot 2.2.x中MimeTypeUtils使用的ConcurrentLinkedQueue在并发环境下remove操作可能失败,导致队列无限增长,最终引发CPU使用率过高。官方已在后续版本中修复此问题。