CPU 使用率在系统监控中是一个非常重要的指标。对于大多数 Web 应用来说,它们往往是 IO 密集型的,因此只会在某些时刻可能会出现 CPU 突然飙升的情况,随后很快就恢复正常。然而,当收到报警并想要排查问题时,CPU 飙升的情况可能已经过去,无法直接查看当时 Java 进程内的线程堆栈信息。
为了解决这个问题,本文实现了一个小工具,它在 CPU 飙升时自动保存堆栈信息,方便后续问题的追踪。
该工具实现了两个主要功能:
- 当 CPU 使用率达到预设的阈值时,自动保存当前 Java 进程的线程堆栈信息;
- 输出占用 CPU 使用率最高的线程 ID。
本文选择在 Linux 系统上实现这个工具,因为大多数应用都是部署在 Linux 环境中(本来想实现一个 MacOS 版本的脚本,但捣鼓了半天,由于格式以及函数存在差异,所以就没继续纠结了)
脚本如下:
#!/bin/bash
if [ $# -ne 1 ]; then
echo "Usage: $0 <j_pid>"
exit 1
fi
j_pid=$1
threshold=90
while true; do
cpu_info=$(ps -p "$j_pid" -o %cpu=)
cpu_usage=$(sed 's/%//g' <<< "$cpu_info")
current_time=$(date +'%Y-%m-%d %H:%M:%S')
echo "[-----$current_time] Current CPU Usage: $cpu_usage%"
if [ $(echo "$cpu_usage > $threshold" | bc -q) -eq 1 ]; then
thread_id=$(top -b -n 1 -H -p $j_pid | grep -E "^\s*[0-9]+" | sort -k9 -r | head -n 1 | awk '{print $1}')
echo "[-----$current_time] Detected! PID: $j_pid ; Thread ID: $thread_id"
jstack_output=$(jstack -l $j_pid)
jstack_filename="jstack_$(date +'%Y%m%d%H%M%S').txt"
echo "$jstack_output" > "$jstack_filename"
echo "[$current_time] jstack_filename: $jstack_filename"
fi
sleep 10
done
可以看看效果,这是模拟的消耗 CPU 的 Java 代码:
/**
* @author dongguabai
* @date 2023-07-21 13:11
*/
public class Test {
public static void doSth() {
while (true) {
double x = Math.random() * Math.random();
}
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
doSth();
}
}, "highcpu_thread").start();
}
}
执行 Java 代码:
[root@MiWiFi-R4CM-srv javaTest]# javac Test.java
[root@MiWiFi-R4CM-srv javaTest]# java Test
执行脚本:
[root@MiWiFi-R4CM-srv javaTest]# sh monitor.sh 9581
[-----2023-07-21 01:37:12] Current CPU Usage: 99.3%
[-----2023-07-21 01:37:12] Detected! PID: 9581 ; Thread ID: 9590
[2023-07-21 01:37:12] jstack_filename: jstack_20230721013713.txt
查看线程 ID:
查看线程堆栈:
"Attach Listener" #9 daemon prio=9 os_prio=0 tid=0xb768fc00 nid=0x25a0 waiting on condition [0x00000000]
java.lang.Thread.State: RUNNABLE
"DestroyJavaVM" #8 prio=5 os_prio=0 tid=0xb7607400 nid=0x256e waiting on condition [0x00000000]
java.lang.Thread.State: RUNNABLE
"highcpu_thread" #7 prio=5 os_prio=0 tid=0xb768ec00 nid=0x2576 runnable [0xa4bb8000]
java.lang.Thread.State: RUNNABLE
at Test.doSth(Test.java:13)
at Test$1.run(Test.java:20)
at java.lang.Thread.run(Thread.java:748)
可以看到,效果还是可以的。
注意事项
在生产环境中使用此工具时,需要注意以下几点:
jstack
命令可能对系统性能产生一定影响,需要谨慎使用并避免频繁执行- 如果 Java 进程非常大,有很多线程,那么收集线程堆栈信息的操作可能会耗费较多的系统资源
- 触发脚本时 Java 进程本身已经处于高负载状态,运行
jstack
命令可能会导致 Java 进程的响应时间增加 - 最好设置一个触发阈值,频繁地运行
jstack
命令收集线程堆栈信息,可能会在一定程度上增加系统负担
- 脚本最好保存在一个不容易被意外杀掉的地方,以确保在 CPU 飙升时能正常执行。
- 在应用刚启动的时候,可能由于初始化等操作导致短暂的 CPU 飙升,因此脚本可设置延迟执行,避免误报。