一段自旋代码导致CPU飙升至200%!
问题引入
项目开发完成后上线,但是只要一启动,不管有没有业务在跑,CPU马上飙升至200%,非常诡异。
定位原因
首先明确需求,该服务通过自定义的topic创建消费者实例,所以初步判定问题出现在了创建的消费者实例上。
- 首先通过
top
命令查看CPU的占用情况,发现进程已经达到了近200%。 - 通过
top -H -p 进程号
配合jstack
发现如果启动两个消费者实例每个就高达100%,启动3个每个达到近70%、4个时每个达到50%、…,反正创建的消费者实例总是要把CPU占满到200%左右。
追踪代码进入公司的组件内,发现了如下的逻辑:
创建消费者实例的部分重要代码如下:
public class MqListenerFactory {
public void createListener(String topic, String groupId, MessageListener listener) {
// ...
Worker worker = new Worker(topic,listener, messageInterceptor, transportManagerBuilder.getTransportManager(),
new ConsumerConfig(), null, async);
worker.setName(topic + "-worker");
worker.setDaemon(true);
worker.start();
if (log.isDebugEnabled()) {
log.debug("启动MQ监听器,主题:{} 监听器:{}", topic, listener.getClass().getName());
}
workerMap.add(topic, worker);
}
}
Worker的部分重要代码如下:
public class Worker extends Thread {
public void run() {
// 创建消费者
createConsumer();
// 创建线程池
createExecutor();
// 开始运行
running();
}
private void running() {
// ...
while (messageConsumer.isStarted()) {
TimeWatch watch = new TimeWatch();
try {
String tempPath = messageConsumer.getTransportManager().getConfig().getTempPath();
// 如果配置了临时文件地址,则是广播模式
if (!StringUtils.isEmpty(tempPath)){
messageConsumer.subscribe(topic,list -> {
doListenMessage(list,statistic,watch);
});
}else {
messageConsumer.pull(topic, list -> {
doListenMessage(list,statistic,watch);
});
}
} catch (Exception e) {
log.error("消费消息异常, topic={}, 异常:", topic, e);
}
}
log.warn("已停止拉取消息,请关注!");
messageConsumer.stop();
log.warn("已关闭,请关注!");
}
}
初步判定是因为running
方法中的自旋导致
解决
自旋中可以进行适当的睡眠等待,可以让CPU的占用大大下降:Thread.sleep(500)
总结
- 自旋代码中如果含有I/O、内存操作等的代码,CPU的占用可能不会很高;但是如果类似于上面的纯自旋空转CPU,会导致CPU资源的大量占用。
- 若场景符合,可以考虑通过加锁替代自旋,以减少CPU资源的过多占用,如上面组件的另一个版本中是通过使用读写锁解决了CPU资源的大量占用。