昨天去了grafa 线上的监控看了看项目 突然发现cpu达到了70% (好久没关注) 然后开启了排查
现象图如下
这个项目没有其他的很耗时的操作 没有大量计算 而且接口数量 以及并发数量均没有很大 怀疑有异常 本来想着从grafa 监控中看下最近几个月 CPU使用率的变化 可惜因为数据量太大 线上没有保留这么长时间
出问题后临时的解决方案
- 加大堆内存 之前1g 后来加到2g 问题并未解决 反而出来内存占用大于90%的告警
- 之前怀疑是skywalking 引起的CPU过高 (后面才上线的)新建 临时服务节点 下掉了skywalking CPU过高问题还是存在
开始进行深度代码排查
- 申请线上跳板机只读权限 略
- 进行操作
#1.获取到pods
kubectl get pods
#2.进入到容器
kubectl exec -it 容器名 bash
- 执行top 然后 M
- 占用CPU最大的进程pid为 然后 查询进程中消耗CPU最大线程
- top -H -p 1
- 可以看到进程中占用CPU最大的线程ID为49
- 将线程ID转化为16进制 (0x开头)
- printf “0x%x\n” 49
- 16进制的线程ID 0x31
- 查询线程的栈信息 并匹配该线程ID 同时向下打印30行 并将匹配到的值 染色
- jstack 1 | grep 0x31 -A 30 --color
可以看到直接定位到了关键的代码行数 我们来看业务代码
//等待被更新的缓存map
private static final Map<Long, Integer> userCacheMap = new ConcurrentHashMap<>();
//可用线程数
private static final Integer availableProcessors = Runtime.getRuntime().availableProcessors();
//任务执行线程池
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(availableProcessors, availableProcessors + 1, 3, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(1024), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "定时任务执行线程");
}
});
private class TaskThread extends Thread implements Runnable {
@Override
public void run() {
for (; ; ) {
//死循环 遍历map 存在数据进行处理 每处理一个数据 remove
Iterator<Map.Entry<Long, Integer>> iterator = userCacheMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Long, Integer> next = iterator.next();
Long userId = next.getKey();
Integer expireTime = next.getValue();
//丢入任务到线程池子
EXECUTOR.execute(new Task(userId, expireTime));
//任务数量
long taskCount = EXECUTOR.getTaskCount();
//完成任务数量
long completedTaskCount = EXECUTOR.getCompletedTaskCount();
//存活数量
int activeCount = EXECUTOR.getActiveCount();
LogUtil.info(log, LogUtil.LogType.INFO, "CacheUserServiceImpl.EXECUTOR",
"存活数量: {},任务数量: {},完成任务数量: {}", activeCount, taskCount, completedTaskCount);
iterator.remove();
}
}
}
}
复盘下 当时的设计方案
- 其他的业务调用包装的获取缓存的方法
- 缓存方法中 判断是否存在缓存 不存在进行获取 存在的话 进行 检测是否业务时间已经失效 失效的话 存入map 等待被更新
- 同时启动中会开启一个线程 死循环 使用iterator 方式遍历map 如果存在等待被更新数据 进行遍历
- 将每个遍历的结果包装成Task 丢入到线程池中 等待被调度 去重建缓存 同时remove掉
- static修饰的线程池 进行执行任务
替换方案
- 去掉死循环迭代map的方式 当缓存业务失效时 直接包装成Task 丢到线程池 异步执行完成
- for( ; ; ) 循环中循环不到数据 睡眠一会 几秒钟 这样就不会一直查询
总结: for(; ; ) 会占用大量CPU 尤其是for(; ; ) 里面遍历map 慎用