Stories

Detail Return Return

Phoenix框架 從0到1設計業務併發框架 併發線程池的核心設計 - Stories Detail

背景

從 0 到 1 設計業務併發框架系列:

  • Phoenix 框架 小米商城產品站革新之路
  • Phoenix 框架 怎麼組織設計一個框架

前兩篇文章已經講述了我設計框架的背景以及抽象設計的細節,今天講一下併發框架最為關鍵的併發線程池的核心設計,主要講一下在設計線程池劃分遇到的問題以及最終我採用了哪種方式實現的。

併發調用組

將存在依賴關係的 Task 進行劃分分組後,依次執行分組就可以拿到所有想要的結果,但是怎麼劃分線程池、設置線程池是面臨的問題。

接下來,我將實際業務中的複雜度簡化設計,將問題具象化呈現給大家。

方案:公用線程池

方案

最開始,我計劃將分配的 Task 公用一個線程池,讓 Task 去線程池競爭資源,如下圖:

公用線程池

但是很快發現,單個線程池一旦請求數量上來,某個 Task 接口變慢就會導致整個接口成功率急速下降,直至不可用的狀態。

為什麼會出現這種情況呢?

效果

公用線程池

  • T1 時刻,第 1 波流量進來,之後率先執行 TaskA 或者 TaskB;
  • TaskA 請求的快速遞增,接口變得越來越慢;
  • T2 時刻,還有兩個 TaskA 並沒有執行完畢,之後第二波流量進來:

    • 第 1 波流量開始執行 TaskC 和 TaskD;
    • 第 2 波流量進來,也有 TaskA 和 TaskB 獲取到線程執行;
  • T3 時刻,此時已經有 4 個 TaskA 還沒有執行完,並且最開始的兩個 TaskA 要面臨着超時情況:

    • 第 1 波流量執行的 TaskA 面臨超時中斷的情況;
    • 第 2 波流量執行的 TaskA 也在運行狀態中;
    • 第 3 波流量進來,情況變得複雜,新的流量,有 TaskA 和 TaskB 進行執行;
    • 此時第 1 波流量前兩層運行完畢,開始執行 TaskE;
    • 此時第 2 波流量的前一層運行完畢,開始執行 TaskC 和 TaskD;
  • 之後按照 TaskA 始終慢的情況繼續發展.......
  • Tn 時刻,此時線程池大部分已經被前 n 波流量的 TaskA 佔據着,並且大量被中斷超時,其他 Task 無法競爭到線程進行執行。

這樣的話,接口的可用性完全取決於 TaskA 的可用性,但是還有一個致命的問題就是其他 Task 無法執行或者由於依賴問題,前置該獲取用作請求參數大部分為空,也無法正常請求,這樣就算是接口返回了數據,也是不全的數據。

這種方案存在共用線程池大量線程等待超時的情況,是不可取的。

方案:分層線程池

方案

公用線程池的情況肯定是有問題的,在此基礎上,嘗試將分層併發劃分不同的併發池,每一層公用線程池,如下圖:

分層線程池

上了分層公用線程池之後,壓力測試發現效果只有小幅的提升,沒有達到預期的目標,甚至來説相差很遠,為啥會出現這個問題?

效果

分層線程池

我們還是假設 TaskA 會隨着請求量上來會大面積超時來舉例。

  • T1 時刻,第 1 波流量進來,之後率先執行 TaskA 或者 TaskB,此次線程池 2、3 沒有執行到;
  • TaskA 請求的快速遞增,接口變得越來越慢;
  • T2 時刻,還有兩個 TaskA 並沒有執行完畢,之後第二波流量進來:

    • 第 1 波流量開始執行線程池 2 的線程 TaskC 和 TaskD;
    • 第 1 波流量存在 TaskC 執行完,陸續開始執行線程池 3 的線程 TaskE;
    • 第 2 波流量進來,也有 TaskA 和 TaskB 獲取到線程執行;
  • T3 時刻,此時已經有 4 個 TaskA 還沒有執行完,並且最開始的兩個 TaskA 要面臨着超時情況:

    • 第 1 波流量執行的線程池 1 TaskA 面臨超時中斷的情況;
    • 第 2 波流量執行的線程池 1 TaskA 也在運行狀態中;
    • 第 3 波流量進來,情況變得相對來説比較複雜,新的流量;
    • 此時第 1 波流量前兩層運行完畢,開始執行線程池 3 TaskE;
    • 此時第 2 波流量的前一層運行完畢,開始執行線程池 2 TaskC 和 TaskD;
  • 之後按照 TaskA 始終慢的情況繼續發展.......
  • Tn 時刻,此時線程池 1 大部分已經被前 n 波流量的 TaskA 佔據着,並且大量被中斷超時,由於依賴於 TaskA 和 TaskB 的結果作為下層的入參數:

    • TaskA 過慢佔據着接近 100% 的線程池 1 的資源;
    • TaskB 競爭不到資源,被超時中斷;

最後接口還是發展到不可用的狀態,其實和公用線程池的問題一樣,也還是存在大量線程等待超時
的情況。

這種公用線程池的現狀是不可取的,那麼該如何劃分線程池來執行呢?其實分而治之的思想就可以解決這個問題,也就帶來了 3.0 版本,獨立 Task 線程池的方案

方案:獨立線程池

無論怎麼公用線程池,都會出現被擠佔的情況,只有將每個 Task 劃分單獨的線程池,才不會出現搶佔等待的問題,那麼如何設計的呢?

方案

獨立線程池

每個 Task 單獨創建線程池來承接流量,各個線程池互相不干擾,同時承接流量交給 CPU 搶佔資源進行調度運行。

效果

獨立線程池

由於是單獨承接流量,這種設計滿足了高可用的目標,還是依照 TaskA 接口隨着併發請求的提升,接口越來越慢直至不可用,之後再加入一個條件,就是 TaskC 的執行條件是 TaskA 執行完畢的結果

  • T1 時刻,第一波流量進來,所有線程池的線程都佔滿,開始進入核心調度執行;
  • T2 時刻,第二波請求進來,第一波請求的 2 個 TaskA 還沒有執行完畢,其他線程池的線程逐漸承接第二波請求等待調度;
  • T3 時刻,第三波請求進來,這時候情況比較複雜:

    • 第一波流量的 2 個 TaskA 已經超時被中斷了,對應的 TaskC 的線程池的兩個 TaskC 線程等待 Task 的執行結果失敗,結束任務;
    • 第二波流量的 2 個 TaskA 還沒有執行完畢,也瀕臨超時;
    • 其他線程池執行均正常運行;
  • 就這樣過了一段時間 ...
  • Tn 時刻:

    • TaskA 已經達到了不可用的狀態;
    • 對此有依賴關係的 TaskC 也逐漸達到不可用狀態;
    • 其他線程執行正常;

這麼看,針對於一個接口調用幾十個上百個接口的場景,不會因為一個接口或者有依賴關係的接口可用性降低而影響整個接口的可用性,同時只要對單個線程池做好監控,加上報警即可動態感知哪些上游接口失敗而及時通知到對應的系統維護同學,這樣就大大的降低了維護成本。

這個版本作為線上生產環境的第一個版本推了上去,單台 8C 8G (k8s) 的配置空跑框架達到了 QPS 在 1.4w,接口可用性在 99.96%(結果僅供參考,根據公司集羣部署策略、機器性能等問題會有浮動)。

但是,這種目前還是存在着顯而易見的問題,就是每個 Task 執行的接口的接口響應都不是一致的,有的在 50ms 內、有的在 100ms 內、有的比較慢 500ms 內,分配相同的線程池數量是不合理的,因為這樣就會造成 CPU 調度不公平,那麼怎麼讓調度運行的比較公平呢?

優化

針對於這個問題,將線程池大小按照權重創建,像是比較慢的接口但是多等待一定時間可以返回的,我們就多分配線程池大小,接口響應很快的,我們就相對減少線程池大小,這樣的設計可以在保證接口的可用性兼顧接口返回字段的完整性。

寫在最後

本篇文章主要講框架設計中怎麼將劃分好的分層併發執行,最終我們採用了獨立線程池的方案,並且按照耗時、CPU 核數等權重評估分配每個 Task 任務線程池的大小,讓 CPU 線程調度來確保線程都儘可能的公平執行到,最終保證接口的併發需求及高可用的場景。

如果你感興趣,推薦關注公眾號或訂閲本站,歡迎互動與交流,讓我們一起變得更強~

WeChat

user avatar mannayang Avatar aliterminal Avatar u_16502039 Avatar xuxueli Avatar u_16769727 Avatar lenglingx Avatar u_11365552 Avatar u_15702012 Avatar jump_and_jump Avatar yizhidanshendetielian Avatar chaochenyinshi Avatar lvweifu Avatar
Favorites 35 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.