场景描述
在生产环境中,服务器CPU使用率超高的情况时有发生。尤其是在重大版本发布后,一旦出现内存泄露,CPU使用率极易飙升,严重时甚至可能达到100%。目前我们所使用的服务器均为多核CPU,当CPU告警触发时,我们必须迅速定位问题代码并加以处理,否则在极端情况下,服务器可能会因不堪重负而宕机。
近期,我们在生产环境中就遭遇了这样的问题:某个程序由于代码存在缺陷,致使CPU占用率居高不下。因此,当务之急是立即展开排查工作。首先,需要确定是哪个程序引发了问题,并精准定位具体代码所在位置。在此过程中,我们需要借助 jstack
、jmap
等命令来定位具体的线程,进而查看线程堆栈的详细信息,为解决问题提供有力依据 。
问题模拟
为了模拟生产环境问题,需要写点代码,让AI写吧,但是不要oom,只是让cpu高就行,AI写的代码,接口传一个比较大的值,比如10000
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping(value = "/test")
public void test(@RequestParam(value = "num", defaultValue = "10000")Integer num) {
// 定义线程数量,这里设置为CPU核心数的两倍,以确保高CPU使用率
int numberOfThreads = Runtime.getRuntime().availableProcessors() * 2;
Thread[] threads = new Thread[numberOfThreads];
// 创建并启动线程
for (int i = 0; i < numberOfThreads; i++) {
threads[i] = new Thread(new IntensiveTask(num));
threads[i].start();
}
}
static class IntensiveTask implements Runnable {
private Integer num = 10000;
public IntensiveTask(Integer num) {
this.num = num;
}
@Override
public void run() {
while (true) {
// 创建一个大型数组
int[] array = new int[num];
// 使用计算密集型操作,如排序
java.util.Arrays.sort(array);
// 短暂休眠以避免完全占用CPU
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
}
}
将代码丢到开发环境,java启动程序
nohup java -jar demo-0.0.1-SNAPSHOT.jar > /opt/logs/output.log 2>&1 &
调用接口:
curl http://127.0.0.1:8080/test?num=10000
问题处理
在window系统有任务管理器这些可视化界面可以看,在Linux服务器也有,比如 top
命令
定位CPU占用高的进程
在Linux服务器上,可利用top命令来定位CPU占用高的进程。其中,%CPU
代表CPU占用率,%MEM
表示内存占用率 。
top命令的常用参数
top [-] [d] [p] [q] [c] [C] [S] [n]
各参数含义如下:
- -d:用于指定每两次屏幕信息刷新的时间间隔,用户也能通过s交互命令对其进行更改。
- -p:通过指定要监控的进程ID,从而仅对特定进程的状态进行监控。
- q:此选项可让top无延迟地进行刷新。若调用程序具备超级用户权限,top将以最高优先级运行。
- S:指定累计模式 。
- s:使top命令在安全模式下运行,可消除交互命令可能带来的潜在风险。
- i:让top不显示任何闲置或僵死进程。
- c:显示完整的命令行,而非仅显示命令名。
- H:显示线程 。
先执行下 top
命令看下输出结果:
top
直接使用 top
来看有时候不能很直观,所以加点命令,
top -b -n 1 | head -n 16
-b选项表示批处理模式,-n 1表示只运行一次top命令。head -n 16表示打印16行,这个行数根据具体情况加,因为我想打印出前10的进程
想要持续监控CPU和内存占用最高的前10个进程,加上 watch
命令,每1s会更新一次
watch -n 1 "top -b -n 1 | head -n 16"
也可以使用命令:
top -b -n 1 | head -n 20 | awk 'NR>1 {print $1, $2, $9, $10, $12}'
定位进程中高CPU占用的线程
在前面的方法中使用top命令定位到进程后,需要再定义进程中具体的线程,可以使用命令,其中pid就是前面定位到的进程ID
top -Hp pid
比如前面定位到2540这个进程,监控一下进程下面具体的线程,发现线程2568、2569比较占cpu
嫌弃不够直观,直接打印一下前10,2540是进程ID
ps -mp 2540 -o THREAD,tid | gawk 'NR!=1 && NR!=2 { printf "%s %x\n",$2,$8 }' | sort -rn | head -10
将线程ID转换为十六进制
将前面的线程ID转为十六进制
printf "%x\n" 2569
获取线程堆栈信息
jstack 2540 | grep "a09" -A 30
通过线程堆栈信息就可以定位到具体的代码
找到具体问题代码,原来写了个while(true)循环
监控GC情况
当然生产环境的问题不一定这么容易定位,所以需要监控gc情况,使用jstat -gcutil <进程号>命令查看GC持续变化情况,如果发现Full GC次数过多,可能需要进一步分析内存使用情况
导出堆内存文件
如果出现频繁Full GC的问题,使用命令导出堆内存文件:
jmap -dump:format=b,file=heapdump.hprof pid
分析堆内存文件
使用MAT工具打开导出的heapdump.hprof文件,分析内存泄漏和内存分配情况,找出占用大量内存的对象
优化代码
根据MAT的分析结果,定位代码中的问题,优化代码
监控生产环境
重新部署修改后的代码,并使用top命令监控CPU占用情况,确保问题得到解决,同时可以部署一下监控平台,比如zabbix等对生产环境服务器进行监控,及时发现问题