今天运维反馈,生产某个服务的 CPU 持续飙升到 100%,因为该服务已经迁移到新的 k8s 容器中了,没有外网流量进来,我的第一想法是可能有定时任务在执行。
于是全局搜索了下 @Scheduled
,没有发现该相关注解,由于本人没有服务器相关权限,于是配合运维进行了以下排查。
查看线程情况
通过以下命令,查看进程中所有线程情况。
看到 17791
和 17620
两个线程占用 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() { List < Map < String, Object >> goodsIds = productMapper.selectGoodsIdsBySupId(); for (Map < String, Object > goodsMap: goodsIds) { queue.offer(goodsMap.get("goodsId").toString()); } } }).start();
|
可以看到,这里开了一个线程进行扫描,往队列中添加数据,看上去这块业务是用来同步商品信息的。由于这里扫表数据过大,单个单个处理时间较长导致 CPU 持续飙升。
解决问题
由于该项目是中途接手的,问了业务这块功能已经没有用了,于是注释了这块的代码。