背景
在排查Java进程CPU使用率过高的时候你是否和我有相似的经历?
🤔 忘记排查步骤只记得要转换什么东西到16进制
😡 忘记top命令参数然后被某SDN的复制粘贴文章搞到崩溃
😩 在搜索引擎上搜索"在线转换16进制"甚至使用计算器
😓 没来及排查完就同事就重启了进程还被他阴阳怪气排查问题速度慢
以往我们排查CPU打满的步骤是这样的:
top -Hp {pid}
: 查看该Java进程内所有线程的资源占用情况- 将pid转换为16进制,Linux高手可以用:
printf "%x\n"{pid}
打印出线程id的16进制 jstack -l <pid> > jstack.txt
:获取此时的所有线程快照并输入到文件中- 查找文件内容包含
nid={16进制id}
的线程的堆栈
原理其实很简单,但是步骤要4步,速度快的人恐怕也需要2min左右才能做完,而且每次只能查看一个线程,要排查多个线程需要重复以上步骤花费大量时间,所以我写了一个shell脚本帮助你快速搞定这一切.
github链接下载(欢迎star👏):github.com/hengyoush/J....
加速链接:mirror.ghproxy.com/github.com/...
使用方法
bash
./cpu100.sh 1221 3
如上命令输出制定进程Id 1221下cpu使用率最高的3个线程堆栈,输出如下:
bash
Thread ID: 11777(0x2e01), CPU Usage: 5.9%
"Cat-RealtimeConsumer-ProblemAnalyzer-16-0" #5935 daemon prio=5 os_prio=0 cpu=33301.45ms elapsed=2423.07s tid=0x00007ff1bc75fbd0 nid=0x2e01 runnable [0x00007ff0e5645000]
9221
java.lang.Thread.State: TIMED_WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@17.0.6/Native Method)
- parking to wait for <0x00000005ffdbd1b0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.parkNanos(java.base@17.0.6/LockSupport.java:252)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(java.base@17.0.6/AbstractQueuedSynchronizer.java:1672)
at java.util.concurrent.ArrayBlockingQueue.poll(java.base@17.0.6/ArrayBlockingQueue.java:435)
at com.dianping.cat.message.io.DefaultMessageQueue.poll(DefaultMessageQueue.java:59)
at com.dianping.cat.analysis.AbstractMessageAnalyzer.analyze(AbstractMessageAnalyzer.java:62)
at com.dianping.cat.analysis.PeriodTask.run(PeriodTask.java:116)
at java.lang.Thread.run(java.base@17.0.6/Thread.java:833)
at org.unidal.helper.Threads$RunnableThread.run(Threads.java:294)
Locked ownable synchronizers:
- None
---------------------------------------------
Thread ID: 11789(0x2e0d), CPU Usage: 5.9%
"Cat-RealtimeConsumer-StateAnalyzer-16-0" #5947 daemon prio=5 os_prio=0 cpu=7448.45ms elapsed=2423.05s tid=0x00007ff1bc309800 nid=0x2e0d runnable [0x00007ff16498d000]
9413
java.lang.Thread.State: TIMED_WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@17.0.6/Native Method)
- parking to wait for <0x00000005ffea5da8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.parkNanos(java.base@17.0.6/LockSupport.java:252)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(java.base@17.0.6/AbstractQueuedSynchronizer.java:1672)
at java.util.concurrent.ArrayBlockingQueue.poll(java.base@17.0.6/ArrayBlockingQueue.java:435)
at com.dianping.cat.message.io.DefaultMessageQueue.poll(DefaultMessageQueue.java:59)
at com.dianping.cat.analysis.AbstractMessageAnalyzer.analyze(AbstractMessageAnalyzer.java:62)
at com.dianping.cat.analysis.PeriodTask.run(PeriodTask.java:116)
at java.lang.Thread.run(java.base@17.0.6/Thread.java:833)
at org.unidal.helper.Threads$RunnableThread.run(Threads.java:294)
Locked ownable synchronizers:
- None
工具源码
工具源码如下,后续可能会有更新,最新的可以关注github仓库。
这个工具实际上就是上述4个步骤的结合:
- 首先jstack获取堆栈
- 然后找到进程下CPU使用率最高的前N个线程的ID
- 在jstack输出中匹配"nid={16进制线程id}",并输出匹配的堆栈
bash
#!/bin/bash
if [ $# -ne 2 ]; then
echo "Usage: $0 <Java_PID> <Number of Threads>"
exit 1
fi
java_pid=$1
n=$2
user=$(ps -o user= -p $java_pid | awk '{print $1}')
# 1. 获取进程的jstack线程堆栈
jstack_output=$(sudo -u $user jstack -l $java_pid)
# 2. 找出进程下CPU使用率最高的前N个线程的ID
top_threads_info=$(top -b -n 1 -H -p $java_pid | awk 'NR>7 && $9 != "0.0" {print $1, $9}' | head -n "$n" )
# 3. 在jstack输出中匹配"nid={16进制线程id}",并输出匹配的堆栈
echo "Top $n CPU-consuming threads for Java PID $java_pid (in hexadecimal):"
while read -r thread_id cpu_usage; do
hex_thread_id=$(printf "0x%x" "$thread_id")
echo "Thread ID: $thread_id($hex_thread_id), CPU Usage: $cpu_usage%"
echo ""
awk -v hex_id="$hex_thread_id" '
BEGIN { flag=0 ;start=0 }
{ lines[NR] = $0 }
$0 ~ "nid=" hex_id { print; flag=1;start=NR;print start }
flag && /^"/ && start!=NR { if (flag) exit }
END { for (i=start; i<=NR; i++) if (flag && lines[i] !~ /^"/) print lines[i] }
' <<< "$jstack_output"
echo "---------------------------------------------"
done <<< "$top_threads_info"
解释下最后匹配堆栈的部分,目的是找到包含特定线程ID的相关信息,并输出该目标线程及其下面的信息直到下一个线程堆栈。
BEGIN { flag=0; start=0 }
:在处理开始之前,初始化两个变量,flag
用于标记是否找到了匹配的线程,start
用于记录找到匹配线程的行号。
{ lines[NR] = $0 }
:对每一行进行处理时,将当前行保存在名为lines
的数组中,数组的索引是当前行的行号NR
。
$0 ~ "nid=" hex_id { print; flag=1; start=NR; print start }
:如果当前行包含与给定hex_thread_id
相匹配的线程ID,则打印当前行,将flag
设置为1,将start
设置为当前行的行号。
flag && /^"/ && start!=NR { if (flag) exit }
:如果flag
为1,当前行以双引号开头,并且start
不等于当前行的行号,则退出awk
脚本。这是为了避免提前退出,确保在找到匹配线程后继续处理直到遇到新的以双引号开头的行。
END { for (i=start; i<=NR; i++) if (flag && lines[i] !~ /^"/) print lines[i] }
:在处理结束后,从start
行开始,输出匹配线程及其下面的行,直到遇到新的以双引号开头的行。
总结
如上,一键就可以看到当前CPU使用率最高的堆栈了。
觉得它有用吗,不妨尝试下,我的仓库里还有另外一个内存泄漏排查工具,也可以看看哦。不妨关注一下我的github,以后会不断增强和开发新的工具!