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优化、合理设置线程数!" 🎓
🎉 掌握性能排查,做系统的福尔摩斯! 🎉