一次線上關鍵REST接口調用卡死bug排查

背景介紹

本來是可以在文章標題中將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);

分析到這,直觀的結論有兩類:

  1. ForkJoin.commonPool線程池所有線程被阻塞,導致store2OperFuntion裏的代碼邏輯無線程可用
  2. 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章