昨天去了grafa 线上的监控看了看项目 突然发现cpu达到了70% (好久没关注) 然后开启了排查

现象图如下

java怎么刷cpu java线上cpu飙升_线程池


这个项目没有其他的很耗时的操作 没有大量计算 而且接口数量 以及并发数量均没有很大 怀疑有异常 本来想着从grafa 监控中看下最近几个月 CPU使用率的变化 可惜因为数据量太大 线上没有保留这么长时间

出问题后临时的解决方案

  • 加大堆内存 之前1g 后来加到2g 问题并未解决 反而出来内存占用大于90%的告警
  • 之前怀疑是skywalking 引起的CPU过高 (后面才上线的)新建 临时服务节点 下掉了skywalking CPU过高问题还是存在

开始进行深度代码排查

  • 申请线上跳板机只读权限 略
  • 进行操作
#1.获取到pods  
kubectl get pods 
#2.进入到容器 
kubectl exec -it 容器名 bash
  • 执行top 然后 M
  • java怎么刷cpu java线上cpu飙升_缓存_02

  • 占用CPU最大的进程pid为 然后 查询进程中消耗CPU最大线程
  • top -H -p 1

java怎么刷cpu java线上cpu飙升_java怎么刷cpu_03

  • 可以看到进程中占用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 慎用