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使用率过高。官方已在后续版本中修复此问题。

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. 深入排查 线程分析 发现 : 线程状态主要为RUN或TIME_ WAITING,无死锁 YGC(Young GC)次数持续增高,提示堆内存可能有问题 堆内存分析 关键发现 : ConcurrentLinkedQueue$Node 对象数量持续增加,无减少趋势 队列长度不断增长,导致性能下降 3. 火焰图分析 使用火焰图工具定位到 MimeTypeUtils 类中过度使用 ConcurrentLinkedQueue 的问题。 4. 源码分析 Spring Boot 2.2.6修复版本中的 ConcurrentLruCache 类代码片段: 问题根源 : ConcurrentLinkedQueue.remove() 方法在并发情况下可能返回false 当remove返回false时,代码会继续执行add操作 导致队列长度无限增长 长队列使remove操作需要遍历整个队列,消耗大量CPU资源 问题复现方法 1. 构造恶意请求 使用JMeter构造包含大量不同Accept头的请求: 示例Accept头: 2. 监控队列增长 观察结果 : ConcurrentLinkedQueue$Node 数量持续增加 队列长度突破限制后CPU使用率急剧上升 解决方案 1. 临时解决方案 升级硬件 :使用多核CPU可减缓问题出现速度 降级版本 :回退到Spring Framework 5.1.x版本 2. 官方修复方案 将 ConcurrentLinkedQueue 替换为 ConcurrentLinkedDeque 该修复已包含在Spring Framework 5.3.x版本中 验证代码 验证 ConcurrentLinkedQueue 在并发下的问题: 总结 该问题是由于Spring Boot 2.2.x中 MimeTypeUtils 使用的 ConcurrentLinkedQueue 在并发环境下remove操作可能失败,导致队列无限增长,最终引发CPU使用率过高。官方已在后续版本中修复此问题。