背景介绍
本来是可以在文章标题中将bug现象说的更具体一点,但介于聪明TX可能一眼就知道问题所在,相对来说没有了挑战性。问题背景如下 :
微服务架构中,其中的一个提供协议拓扑的微服务组件,在线上运行时,突然无法查询拓扑数据,手动执行拓扑查询,相关接口调动也一直阻塞并且无数据返回。如图所示:
初步排查
在线上第一次出现该问题时,就进行了常规的日志分析和jstack线程dump操作,命令如下 :
jstack -l pid > a1.txt
当时急于恢复,进行了多次重启,最终才恢复正常。
针对该问题,给出一个大概的定位方向
组织攻关排查
继续分析现场的线程dump文件,查询拓扑的线程,都阻塞在了同一个地方:
定位到关键代码如下(忽略不忍直视的代码规范):
private Set<Link> segmentOperToStore(List<List<Object>> lists) {
Set<Link> sets = ImmutableSet.of();
if (lists != null && lists.size() > 0) {
List<List<List<Object>>> splitLists = QueryUtils.splitListForListList(lists);
List<CompletableFuture<List<Link>>> domainFeature =
splitLists.stream().map(list -> CompletableFuture.supplyAsync(() -> {
return list.stream().map(store2OperFuntion).collect(Collectors.toList());
})).collect(Collectors.toList());
List<Link> listsLinks = domainFeature.stream().map(CompletableFuture::join).reduce(new ArrayList<>(),
(all, item) -> {
all.addAll(item);
return all;
});
sets = Sets.newHashSet(listsLinks);
}
return sets;
}
从调用栈看到线程都阻塞在:
domainFeature.stream().map(CompletableFuture::join)
熟悉JAVA8同学应该知道对于JAVA8的parallelStream和CompletableFutrue在不显示指定线程池的前提下,使用是默认的线程池ForkJoin线程,具体名称为:
ForkJoinPool.commonPool-worker-6]
ps.注意如果是自己实例化的ForkJoin对象,其线程名称为:
/**
* 该线程池取代parallelStream默认线程池,大于也等于CPU核心数
* 名称为ForkJoinPool-" + nextPoolId() + "-worker-
*/
static ForkJoinPool forkJoinPool = new ForkJoinPool(CPU_CORE);
分析到这,直观的结论有两类:
- ForkJoin.commonPool线程池所有线程被阻塞,导致store2OperFuntion里的代码逻辑无线程可用
- store2OperFuntion方法中代码逻辑自身阻塞
但通过走查代码,发现store2OperFunction方法中其并无复杂操作。
此外还有其它各种异常现象,如kafka rebance、消息无法消费等。
此外kafka的主题定阅,也一直无法收到消息和正常处理消息。
乱查一通
虽然前面已经给出正确思路和方向(后来发现),但出于领导先入为主,其主观上认为是拓扑组件本身的问题,直接忽略第一封邮件的判断。即使在第二天的复现排查中,相关同事已经走查到怀疑点,依旧没有重视。
还有同事直接给出了与其无关的结论。这期间经历了kafka服务器问题、CPU性能不够等一系列奇怪结论。
这期间,对上述代码进行了简单的重构,放弃使用默认的forkJoin线程,作为万不得已的修复方案。
//超过400条链路需要组装,则启用多线程方案
if (storeLinks.size() > 400) {
ExecutorService executorService = Executors.newFixedThreadPool(16, UserThreadFactory.build("combine-store-links"));
List<CompletableFuture<List<Link>>> futures = Lists.partition(storeLinks, 200).stream().map(p ->
CompletableFuture.supplyAsync(() -> {
return p.stream().map(store2OperFuntion).collect(toList());
}, executorService)
).collect(toList());
Set<Link> result = futures.stream().map(p -> {
try {
return p.get();
} catch (Exception e) {
logger.error("error when get result.", e);
}
return null;
}).filter(Objects::nonNull).flatMap(List::stream).filter(Objects::nonNull).collect(toSet());
executorService.shutdown();
return result;
} else {
for (List<Object> obj : storeLinks) {
Link link = store2OperFuntion.apply(obj);
linkSet.add(link);
}
}
经过一天的复现过程,在最终将CPU核数改为4核,出现了kafka rebance的问题,导致kafka无法继续拉取消息。结论一度成为:CPU核心数据不够,压力过大。
现场再次复现
现场再次出现拓扑无法刷新,无法查询的问题。在申请保留一晚定位时间,再次定位。一顿操作后,在jstack中的ForkJoin线程发现了关键代码线索:
在咨询相关开发同事,发现其在远程调用Pcep进行远程下发,存在阻塞的超时等待。至此真相大白,拓扑的查询与kafka消费都是受害者:
ForkJoinPool.commonPool里19个线程都在做远程调度,线程池中线程占满。其它使用了该公共线程的的工作都在等待而无法执行。
总结
1、对于公共线程池的使用,要小心,建议还是自己定义线程池进行。
2、自己定义的线程池,必须命名,且命名规则为query-node-3-thread-3,不能丢失线程池ID号
4、本地线程要用完一定要关闭,全局线程要定义成static