今天运维反馈,生产某个服务的 CPU 持续飙升到 100%,因为该服务已经迁移到新的 k8s 容器中了,没有外网流量进来,我的第一想法是可能有定时任务在执行。
于是全局搜索了下 @Scheduled,没有发现该相关注解,由于本人没有服务器相关权限,于是配合运维进行了以下排查。

查看线程情况

通过以下命令,查看进程中所有线程情况。

1
top -Hp pid

看到 1779117620 两个线程占用 100%。然后我们通过管道符查询该线程名,获取十六进制线程id printf "%x\n" 线程ID;执行 jstack 进程号 | grep 线程HEX ID -B1 -A10,然后得到下面的结果。

1
2
3
4
5
6
7
8
9

"Thread-8" #37 prio=5 os_prio=0 tid=0x00007f7bad58f800 nid=0x457f runnable [0x00007f7b84e71000]
java.lang.Thread.State: RUNNABLE
at com.xxxx.runner.CommandLineRunnerImpl$1.run(CommandLineRunnerImpl.java:35)
at java.lang.Thread.run(Thread.java:745)

"scheduling-1" #36 prio=5 os_prio=0 tid=0x00007f7badb62800 nid=0x457e runnable [0x00007f7b84f70000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)

这样就定位到线程调用栈了,可以看到代码在 CommandLineRunnerImpl 该类中。

分析代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Service
public class CommandLineRunnerImpl implements ApplicationListener<ContextRefreshedEvent> {

...

@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {

ArrayBlockingQueue<String> queue = ItemServiceImpl.queue;

new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
if (queue.size() == 0) {
continue;
}
String productId = queue.take();
goodsService.syncProduct(productId);
}
} catch (InterruptedException e) {

}
}
}).start();

}
}

可以看到线程 Thread-8 是一个 while(true),每次都会去消费队列中的数据,如果队列为空,则继续循环。接着我们寻找生产数据的地方:

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping("/addQueue")

new Thread(new Runnable() {
@Override
public void run() {
// 查询商品id
List < Map < String, Object >> goodsIds = productMapper.selectGoodsIdsBySupId();
for (Map < String, Object > goodsMap: goodsIds) {
queue.offer(goodsMap.get("goodsId").toString());
}
}
}).start();

可以看到,这里开了一个线程进行扫描,往队列中添加数据,看上去这块业务是用来同步商品信息的。由于这里扫表数据过大,单个单个处理时间较长导致 CPU 持续飙升。

解决问题

由于该项目是中途接手的,问了业务这块功能已经没有用了,于是注释了这块的代码。