背景介紹
本來是可以在文章標題中將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