🔍 CPU不高但响应慢:性能排查的福尔摩斯式推理!

CPU才30%,为什么系统还是慢?让我们一起破解这个谜案!


🎬 问题场景

常见的困惑 😰

erlang 复制代码
运维:老板,系统响应好慢!
老板:快看CPU使用率!
运维:CPU才30%啊...
老板:那为什么慢???
运维:我也不知道啊!😭

典型现象 📊

bash 复制代码
# top命令查看
top - 10:30:15 up 5 days,  2:15,  2 users,  load average: 0.50, 0.45, 0.40
Tasks: 250 total,   1 running, 249 sleeping
%Cpu(s): 30.0 us,  5.0 sy,  0.0 ni, 60.0 id,  5.0 wa  ← 关键!
KiB Mem : 16384000 total,  2048000 free

# 症状:
# - CPU使用率只有30%
# - 但系统响应很慢
# - 用户投诉延迟高
# - 接口超时频繁

🎯 可能的原因分类

scss 复制代码
CPU不高但响应慢的6大原因:
┌────────────────────────────────┐
│ 1. 🔒 锁竞争(Lock Contention) │
│ 2. 💤 IO等待(IO Wait)         │
│ 3. 🌐 网络延迟(Network)       │
│ 4. 🗄️ 数据库慢(DB Slow)       │
│ 5. 🔄 上下文切换(Context Switch)│
│ 6. 🧵 线程状态异常               │
└────────────────────────────────┘

🔒 原因1:锁竞争

症状 🎯

java 复制代码
// 问题代码
public class OrderService {
    // 单例,所有请求共享!
    private static final Object lock = new Object();
    
    public void createOrder(Order order) {
        synchronized(lock) {  // 所有线程都在这里排队!
            // 处理订单(耗时操作)
            processOrder(order);  // 需要100ms
            saveToDatabase(order); // 需要50ms
        }
        // 总耗时:150ms
        // 100个并发请求 = 15秒!💥
    }
}

现象 📉

bash 复制代码
# jstack查看线程状态
"http-nio-8080-exec-1" #25 waiting for monitor entry
  java.lang.Thread.State: BLOCKED (on object monitor)
  
"http-nio-8080-exec-2" #26 waiting for monitor entry
  java.lang.Thread.State: BLOCKED (on object monitor)
  
"http-nio-8080-exec-3" #27 waiting for monitor entry
  java.lang.Thread.State: BLOCKED (on object monitor)

# 大量线程BLOCKED状态!
# CPU在等待锁,不干活!

排查方法 🔍

bash 复制代码
# 1. 使用jstack查看线程堆栈
jstack <pid> > thread.dump

# 2. 分析线程状态
grep "BLOCKED" thread.dump | wc -l  # 统计BLOCKED线程数

# 3. 找到锁竞争的位置
grep -A 10 "waiting for monitor entry" thread.dump

# 4. 使用JProfiler/VisualVM查看锁分析

解决方案 ✅

java 复制代码
// ❌ 问题代码
public synchronized void process() {
    // 所有逻辑都在锁里
    doA();  // 50ms
    doB();  // 50ms
    doC();  // 50ms
}

// ✅ 方案1:缩小锁范围
public void process() {
    doA();  // 无需同步
    doB();  // 无需同步
    
    synchronized(this) {
        doC();  // 只保护必须同步的部分
    }
}

// ✅ 方案2:使用读写锁
private ReadWriteLock lock = new ReentrantReadWriteLock();

public void read() {
    lock.readLock().lock();
    try {
        // 读操作,多个线程可以并发
    } finally {
        lock.readLock().unlock();
    }
}

public void write() {
    lock.writeLock().lock();
    try {
        // 写操作
    } finally {
        lock.writeLock().unlock();
    }
}

// ✅ 方案3:使用并发容器
// 替换:
Map<String, Object> map = new HashMap<>();  // 需要synchronized
// 为:
Map<String, Object> map = new ConcurrentHashMap<>();  // 无锁并发

💤 原因2:IO等待

什么是IO Wait?🤔

diff 复制代码
CPU的状态:
┌──────────────────────────────┐
│ us(user):用户态CPU占用     │
│ sy(system):系统态CPU占用   │
│ id(idle):空闲              │
│ wa(wait):等待IO完成 ← 关键!│
└──────────────────────────────┘

IO Wait高的原因:
- 磁盘读写慢
- 网络IO慢
- CPU在等待IO完成,无法执行其他任务

症状 📊

bash 复制代码
# top命令
%Cpu(s):  5.0 us,  2.0 sy,  0.0 ni, 73.0 id, 20.0 wa ← IO等待20%!
                                              ↑
                                         问题在这里!

# iostat查看磁盘IO
iostat -x 1

Device:  r/s   w/s  rkB/s  wkB/s  await  util
sda     1000  500  50000  25000   150   100% ← await高,util满!
                                   ↑      ↑
                              等待时间  磁盘满负荷

常见场景 🎪

场景1:日志打印太多

java 复制代码
// ❌ 问题代码
public void process(Order order) {
    logger.info("开始处理订单: {}", order);  // 同步写磁盘
    // ... 处理逻辑
    logger.info("订单处理中: {}", order);    // 同步写磁盘
    // ... 更多处理
    logger.info("订单处理完成: {}", order);  // 同步写磁盘
}

// 问题:
// - 每条日志都要写磁盘
// - 大量并发 = 大量磁盘IO
// - CPU等待磁盘,响应变慢

// ✅ 解决方案
// 1. 使用异步日志
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="FILE"/>
</appender>

// 2. 减少日志级别
logger.debug("详细信息");  // 生产环境关闭debug

// 3. 只记录关键信息
logger.info("订单处理完成: orderId={}", order.getId());  // 不打印整个对象

场景2:频繁读取文件

java 复制代码
// ❌ 问题代码
public String getConfig(String key) {
    // 每次都读取文件!
    Properties props = new Properties();
    props.load(new FileInputStream("config.properties"));
    return props.getProperty(key);
}

// ✅ 解决方案:使用缓存
public class ConfigManager {
    private static final Properties props = new Properties();
    
    static {
        // 启动时加载一次
        try {
            props.load(new FileInputStream("config.properties"));
        } catch (Exception e) {
            // 处理异常
        }
    }
    
    public static String getConfig(String key) {
        return props.getProperty(key);  // 直接从内存读取
    }
}

场景3:大量数据库查询

java 复制代码
// ❌ 问题代码
public List<User> getUsers(List<Long> userIds) {
    List<User> users = new ArrayList<>();
    for (Long id : userIds) {
        User user = userDao.selectById(id);  // N次数据库查询!
        users.add(user);
    }
    return users;
}

// 问题:
// - 100个ID = 100次数据库查询
// - 每次查询 = 网络IO + 磁盘IO
// - 响应时间 = 100 * 单次查询时间

// ✅ 解决方案:批量查询
public List<User> getUsers(List<Long> userIds) {
    return userDao.selectByIds(userIds);  // 1次查询!
}

排查方法 🔍

bash 复制代码
# 1. 查看IO Wait
top
# 关注 %wa(IO等待)

# 2. 查看磁盘IO详情
iostat -x 1
# 关注:
# - await:平均等待时间(ms)
# - util:磁盘利用率

# 3. 查看进程的IO情况
iotop
# 实时显示哪个进程在读写磁盘

# 4. 查看文件打开情况
lsof -p <pid> | grep -E "(log|tmp|data)"
# 查看进程打开了哪些文件

# 5. 使用JProfiler查看IO操作
# 可以看到每个IO操作的耗时

🌐 原因3:网络延迟

症状 🎯

java 复制代码
// 调用远程服务
public Order getOrder(Long orderId) {
    // 调用订单服务(远程)
    Order order = orderClient.getOrder(orderId);  // 网络IO
    
    // 调用用户服务(远程)
    User user = userClient.getUser(order.getUserId());  // 网络IO
    
    // 调用商品服务(远程)
    List<Product> products = productClient.getProducts(order.getProductIds());  // 网络IO
    
    return enrichOrder(order, user, products);
}

// 问题:
// - 3次远程调用,串行执行
// - 每次调用:50ms网络延迟
// - 总耗时:至少150ms
// - CPU只能等待,利用率低

排查方法 🔍

bash 复制代码
# 1. 查看网络连接
netstat -an | grep ESTABLISHED | wc -l  # 当前连接数

# 2. 查看网络延迟
ping <remote-server>  # ICMP延迟
curl -w "@curl-format.txt" -o /dev/null -s http://api.example.com
# 查看DNS、TCP连接、传输时间

# 3. 使用tcpdump抓包
tcpdump -i eth0 host <remote-ip>

# 4. 使用JProfiler查看网络调用
# 可以看到每个远程调用的耗时

解决方案 ✅

java 复制代码
// ✅ 方案1:并行调用
public Order getOrder(Long orderId) {
    CompletableFuture<Order> orderFuture = 
        CompletableFuture.supplyAsync(() -> orderClient.getOrder(orderId));
    
    CompletableFuture<User> userFuture = 
        orderFuture.thenCompose(order -> 
            CompletableFuture.supplyAsync(() -> userClient.getUser(order.getUserId())));
    
    CompletableFuture<List<Product>> productsFuture = 
        orderFuture.thenCompose(order -> 
            CompletableFuture.supplyAsync(() -> productClient.getProducts(order.getProductIds())));
    
    // 等待所有结果
    Order order = orderFuture.join();
    User user = userFuture.join();
    List<Product> products = productsFuture.join();
    
    return enrichOrder(order, user, products);
}
// 耗时:50ms(最慢的那个调用)

// ✅ 方案2:使用缓存
@Cacheable("users")
public User getUser(Long userId) {
    return userClient.getUser(userId);  // 第一次远程调用,之后走缓存
}

// ✅ 方案3:批量接口
public List<User> getUsers(List<Long> userIds) {
    return userClient.batchGetUsers(userIds);  // 1次调用获取多个用户
}

🗄️ 原因4:数据库慢查询

症状 📊

sql 复制代码
-- 慢查询
SELECT * FROM orders 
WHERE user_id = 123 
  AND status = 'PENDING' 
  AND create_time > '2024-01-01'
ORDER BY create_time DESC;

-- 耗时:3秒!💥
-- 原因:没有合适的索引

排查方法 🔍

bash 复制代码
# 1. 开启慢查询日志
# MySQL配置:
slow_query_log = 1
long_query_time = 1  # 超过1秒的查询记录下来

# 2. 分析慢查询日志
mysqldumpslow -s t /var/log/mysql/slow.log

# 3. 使用EXPLAIN分析
EXPLAIN SELECT * FROM orders WHERE user_id = 123;

# 4. 查看当前执行的查询
SHOW FULL PROCESSLIST;

# 5. 使用Druid监控
# 可以看到每个SQL的执行次数、耗时等

解决方案 ✅

sql 复制代码
-- ✅ 方案1:添加索引
CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time);

-- ✅ 方案2:优化查询
-- 不要SELECT *
SELECT id, user_id, status, create_time 
FROM orders 
WHERE user_id = 123 AND status = 'PENDING';

-- ✅ 方案3:使用分页
SELECT * FROM orders 
WHERE user_id = 123 
LIMIT 20 OFFSET 0;  -- 不要一次查询所有数据

-- ✅ 方案4:读写分离
// 写操作走主库
masterDataSource.update("INSERT INTO orders ...");

// 读操作走从库
slaveDataSource.query("SELECT * FROM orders ...");

🔄 原因5:上下文切换过多

什么是上下文切换?🤔

css 复制代码
CPU从一个线程切换到另一个线程:
线程A执行 → 保存线程A的状态 → 加载线程B的状态 → 线程B执行

每次切换:
- 保存寄存器
- 保存程序计数器
- 刷新TLB(Translation Lookaside Buffer)
- 耗时:几微秒

如果频繁切换:
1000次/秒 * 5微秒 = 5ms
10000次/秒 * 5微秒 = 50ms
100000次/秒 * 5微秒 = 500ms ← CPU浪费在切换上!

症状 📊

bash 复制代码
# vmstat查看上下文切换
vmstat 1

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 4  0      0 2048000 100000 500000    0    0     0     0 5000 50000 10  5 80  5  0
                                                             ↑
                                                      上下文切换次数

# cs(context switch):50000次/秒 ← 太高了!
# 正常值:1000-5000次/秒

常见原因 🎯

java 复制代码
// 原因1:线程过多
ExecutorService executor = Executors.newFixedThreadPool(1000);  // 太多了!
// 问题:
// - 1000个线程竞争8个CPU核心
// - 频繁的线程调度
// - 大量上下文切换

// ✅ 解决:合理设置线程数
int coreNum = Runtime.getRuntime().availableProcessors();
// CPU密集型
ExecutorService executor = Executors.newFixedThreadPool(coreNum + 1);
// IO密集型
ExecutorService executor = Executors.newFixedThreadPool(coreNum * 2);

// 原因2:锁竞争激烈
synchronized(lock) {
    // 100个线程抢一把锁
    // 获得锁的线程执行
    // 未获得锁的线程阻塞
    // 反复切换!
}

// ✅ 解决:减少锁竞争(见前面的锁优化)

// 原因3:大量短任务
for (int i = 0; i < 100000; i++) {
    executor.submit(() -> {
        // 1ms的任务
    });
}
// 问题:线程池不断切换任务

// ✅ 解决:批量处理
List<Task> batch = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
    batch.add(new Task());
    if (batch.size() >= 100) {
        executor.submit(() -> processBatch(batch));
        batch = new ArrayList<>();
    }
}

🧵 原因6:线程状态异常

排查线程状态 🔍

bash 复制代码
# 1. 使用jstack
jstack <pid> > thread.dump

# 2. 分析线程状态分布
grep "java.lang.Thread.State" thread.dump | sort | uniq -c

# 输出示例:
  50 java.lang.Thread.State: RUNNABLE
  10 java.lang.Thread.State: WAITING
 100 java.lang.Thread.State: TIMED_WAITING ← 异常!太多了!
  20 java.lang.Thread.State: BLOCKED

异常状态1:大量WAITING 💤

java 复制代码
// 可能原因:线程池队列满,线程在等待
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10, 10, 0, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(10)  // 队列只有10个位置
);

// 提交100个任务
for (int i = 0; i < 100; i++) {
    executor.submit(() -> {
        // 任务
    });
}
// 结果:
// - 10个线程在执行
// - 10个任务在队列中
// - 80个任务被拒绝或等待

// ✅ 解决:调整队列大小
new LinkedBlockingQueue<>(1000)  // 增大队列

异常状态2:大量TIMED_WAITING ⏰

java 复制代码
// 可能原因:Thread.sleep()太多
public void process() {
    while (true) {
        Task task = queue.poll();
        if (task == null) {
            Thread.sleep(100);  // 睡眠等待
            continue;
        }
        task.run();
    }
}

// 问题:
// - 100个线程
// - 都在sleep
// - CPU利用率低

// ✅ 解决:使用阻塞队列
BlockingQueue<Task> queue = new LinkedBlockingQueue<>();

public void process() {
    while (true) {
        Task task = queue.take();  // 阻塞等待,不消耗CPU
        task.run();
    }
}

🛠️ 综合排查流程

步骤1:查看系统概况 🔍

bash 复制代码
# 1. CPU使用率
top
# 关注:us, sy, id, wa

# 2. 负载
uptime
# load average: 0.50, 0.45, 0.40

# 3. 内存
free -h

# 4. 磁盘IO
iostat -x 1

# 5. 网络
netstat -s  # 网络统计

步骤2:定位Java进程 🎯

bash 复制代码
# 1. 找到Java进程
jps -lv

# 2. 查看进程详情
top -H -p <pid>  # 查看线程

步骤3:分析线程状态 🧵

bash 复制代码
# 1. 生成线程dump
jstack <pid> > thread.dump

# 2. 分析线程状态
grep "java.lang.Thread.State" thread.dump | sort | uniq -c

# 3. 查找BLOCKED线程
grep -A 10 "BLOCKED" thread.dump

# 4. 查找WAITING线程
grep -A 10 "WAITING" thread.dump

步骤4:分析IO情况 💾

bash 复制代码
# 1. 查看打开的文件
lsof -p <pid> | wc -l  # 打开的文件数

# 2. 查看网络连接
lsof -p <pid> | grep TCP

# 3. 查看数据库连接
lsof -p <pid> | grep mysql

步骤5:使用性能分析工具 🔬

bash 复制代码
# 1. JProfiler
# - CPU分析:找到耗时方法
# - 线程分析:找到阻塞点
# - 内存分析:找到内存泄漏

# 2. Arthas
# - dashboard:总览
# - thread:查看线程
# - trace:追踪方法耗时
# - monitor:监控方法调用

# 3. Async-profiler
# 生成火焰图,直观显示性能瓶颈

🎯 解决方案总结

原因 症状 排查工具 解决方案
锁竞争 大量BLOCKED线程 jstack 减小锁粒度、读写锁、ConcurrentHashMap
IO等待 %wa高 iostat, iotop 异步IO、缓存、批量操作
网络延迟 接口响应慢 tcpdump, curl 并行调用、缓存、批量接口
数据库慢 SQL耗时长 slow log, EXPLAIN 索引优化、读写分离、缓存
上下文切换 cs值高 vmstat 减少线程数、批量处理
线程异常 WAITING多 jstack 调整线程池、阻塞队列

🎓 总结口诀

arduino 复制代码
┌────────────────────────────────────┐
│   CPU不高但响应慢的排查口诀         │
├────────────────────────────────────┤
│ 一看锁:jstack找BLOCKED             │
│ 二看IO:iostat看wa值                │
│ 三看网:tcpdump抓延迟                │
│ 四看库:慢查询找瓶颈                 │
│ 五看切:vmstat看cs值                │
│ 六看线:thread dump看状态            │
└────────────────────────────────────┘

下次面试官问"CPU不高但响应慢",你就说

"CPU使用率低但响应慢,说明CPU在等待某些资源。主要有6种可能:1)锁竞争导致大量线程BLOCKED;2)IO等待,wa值高;3)网络延迟,远程调用慢;4)数据库慢查询;5)上下文切换过多;6)线程状态异常。排查流程是:先用top看wa值,再用jstack看线程状态,然后用iostat看磁盘IO,用慢查询日志看数据库,用vmstat看上下文切换。针对性优化:锁优化、异步IO、并行调用、SQL优化、合理设置线程数!" 🎓

🎉 掌握性能排查,做系统的福尔摩斯! 🎉

相关推荐
用户904706683573 小时前
java hutool 工具库
后端
鄃鳕3 小时前
Flask【python】
后端·python·flask
渣哥3 小时前
你以为 Bean 只是 new 出来?Spring BeanFactory 背后的秘密让人惊讶
javascript·后端·面试
桦说编程3 小时前
CompletableFuture API 过于复杂?选取7个最常用的方法,解决95%的问题
java·后端·函数式编程
冲鸭ONE3 小时前
新手搭建Spring Boot项目
spring boot·后端·程序员
Moonbit3 小时前
MoonBit Pearls Vol.10:prettyprinter:使用函数组合解决结构化数据打印问题
前端·后端·程序员
世界哪有真情4 小时前
Trae 蓝屏问题
前端·后端·trae
Moment4 小时前
NestJS 在 2025 年:对于后端开发者仍然值得吗 😕😕😕
前端·后端·github
Java水解4 小时前
【Spring】Spring事务和事务传播机制
后端·spring