问题背景:
网关配置索引出错导致CPU占用过高
排查过程:
首先排查机器CPU占用率,使用top命令
占用CPU最高的进程为19414进程
查看该进程中占用CPU最高的线程 top -Hp 19414
将线程id 19492转换为16进制 为4c24
查询占用cpu最高线程的栈 jstack 19414 |grep 4c24 -A 20
终于定位到哪一个线程出的问题,可以看到该线程是一个nacos工作线程 com.alibaba.nacos.client.Worker.longPolling.fixed-xxxx
这是一个长轮询线程,如果只是单线程执行肯定不会有问题,问题应该是出现在不停的创建了clientWork对象上
排查这个对象是如果创建
这里能够看出来,nacos每次刷新都会触发实例创建,验证想法,通过jamp命令
jmap -histo:live 6853 |grep ClientWorker
果然,有438个work对象,并且随着时间逐渐变大
事情到这里就比较明确了,只需要排查出,为什么会一直不停的创建ClientWorker,我们把断点打到创建ClientWorker的方法上
通过堆栈分析,找到创建ClientWorker的代码堆栈
通过堆栈分析,长轮询获取配置信息时,会定时校验 cacheData.checkListenerMd5()
我们可以看到创建ClientWorker最终源头还是来源于ClientWorker类,以下是调用cacheData.checkListenerMd5()的源码
class LongPollingRunnable implements Runnable {
private int taskId;
public LongPollingRunnable(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
List<CacheData> cacheDatas = new ArrayList<CacheData>();
List<String> inInitializingCacheList = new ArrayList<String>();
try {
// check failover config
for (CacheData cacheData : cacheMap.get().values()) {
if (cacheData.getTaskId() == taskId) {
cacheDatas.add(cacheData);
try {
checkLocalConfig(cacheData);
if (cacheData.isUseLocalConfigInfo()) {
cacheData.checkListenerMd5();
}
} catch (Exception e) {
LOGGER.error("get local config info error", e);
}
}
}
// check server config
List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
此处省略
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
for (CacheData cacheData : cacheDatas) {
if (!cacheData.isInitializing() || inInitializingCacheList
.contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
// 校验本地配置和服务端配置md5
cacheData.checkListenerMd5();
cacheData.setInitializing(false);
}
}
inInitializingCacheList.clear();
executorService.execute(this);
} catch (Throwable e) {
// If the rotation training task is abnormal, the next execution time of the task will be punished
LOGGER.error("longPolling error : ", e);
executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
}
}
}
猜测如果不一致会刷新spring配置容器,应该是这里报错导致
接着分析cacheData.checkListenerMd5()代码
void checkListenerMd5() {
for (ManagerListenerWrap wrap : listeners) {
if (!md5.equals(wrap.lastCallMd5)) {
safeNotifyListener(dataId, group, content, type, md5, wrap);
}
}
}
如果服务端和本地md5不一致,则会调用safeNotifyListener监听方法,并执行job.run方法
上图是job.run最终报错的代码receiveConfigInfo报错,spring绑定配置信息异常,并且导致213行listenerWrap.lastCallMd5 = md5;没有成功执行到,因此会一直死循环