动态

详情 返回 返回

快手 Java 透明協程:實現零代碼修改提升 30%QPS - 动态 详情

摘要:對於開發者而言,傳統線程模型邏輯直觀但性能受限,而異步模型雖性能高卻複雜性大。協程以“同步編程,異步執行”平衡兩者,成為現代語言標配。結合自身業務需求,快手基於社區開源版本自研了 Java17 透明協程技術,實現對業務無侵入的同時,吞吐能力提升 30%以上。本文將深入剖析快手協程技術的背後原理與架構演進。

一、協程技術的發展與挑戰

協程作為計算機領域的一項古老技術,其思想可追溯至 1963 年。然而很遺憾的是在之後的歲月裏,協程並沒有成為併發編程的主流,取而代之的是對用户更加友好的基於搶佔式調度的線程模型。儘管如此,協程並未淡出歷史舞台。進入 21 世紀後,隨着互聯網業務的蓬勃發展,協程因其調度策略的高效性和對吞吐量的友好性而重新受到工業界的青睞,CPP、Lua、Python、Golang、C#等一眾編程語言紛紛開始支持協程,迎來了協程實踐的廣泛應用。相較於其他語言,Java 在協程方面的發展起步較晚。2011 年,JKU 首次提出了 Java 協程的原型,並發表了一系列具有指導意義的論文,為 Java 協程的實現指明瞭方向。自此,Java 協程進入了快速發展階段,各大廠商紛紛推出了各具特色的 Java 協程解決方案,如阿里的 Wisp 協程方案、騰訊的 Fiber 協程方案以及 Oracle 官方的 Loom 協程方案等。其中,Oracle 官方的 Loom 協程方案自 2018 年啓動以來,備受矚目,並在 2023 年的 Java 21 版本中正式發佈,引發了 Java 業界的廣泛關注,被視為 Java 生態中的重要里程碑。

圖片

傳統併發編程存在線程和異步兩種模型,各自特點鮮明:線程模型開發友好,但性能受限;異步模型性能優越,但開發複雜度高。協程則融合了兩者的優點,實現了編程效率與運行效率的平衡。通過簡化應用示例,下圖展示了協程、線程和異步模型之間的關鍵差異。

圖片

儘管協程在性能上具備顯著優勢,但其應用也需考慮特定場景。當業務服務呈現以下特徵時,協程將極大提升服務的極限 QPS 性能:

  • 雲原生高負載環境:服務進程在 CPU 資源受限的情況下頻繁遭遇節流。
  • 線程上下文切換頻繁:服務進程具有 IO 密集或鎖密集的特點,導致線程上下文切換頻繁。

業界普遍認為,協程的主要優勢在於減少了內核線程的上下文切換指令開銷。然而,更為關鍵的收益在於協程顯著改善了內核 CFS 的調度延遲。以下圖為例,在雲原生 k8s 環境中,當服務在單核 CPU 配額不足的高負載工況下運行時,線程的 CFS 公平調度策略可能引發 CPU Throttle,從而嚴重影響響應時間(RT)。相比之下,協程採用的 FIFO(先進先出)調度策略完全消除了 CPU 節流現象,將平均響應時間從 101ms 縮短至 63ms,顯著提升了服務的 QPS 上限。

圖片

二、快手 Java 透明協程技術的演進之路

Java 協程作為一種“輕量級線程”,擁有“同步編程,異步運行”的特性,在提升服務 QPS,優化成本等方面具有較大潛力。而快手的線上業務大量運行在 Java 上,鑑於此,快手於 23 年 4 月份啓動 Java 透明協程項目。此項目對於快手而言意義重大,具體體現在多個方面:

  • 運行效率提升:協程在提升 QPS 方面的卓越表現,結合系統軟件優化的規模化效應,將為快手帶來可觀的成本節省收益。
  • 編程效率提升:快手各業務線層進行了部分不徹底的異步化改造,導致框架代碼複雜度增加,可維護性降低,架構演進受阻。透明協程的引入有助於提升快手高併發架構的開發效率。
  • 雲原生架構演進:協程將補齊 Java 語言的短板,助力快手的技術架構更好地適應雲原生場景,為長遠技術規劃奠定基礎。

2.1 Java 協程方案選型

目前 Java 業界具有代表性的方案有兩類:Oralce 官方的 Loom 協程,阿里 Dragonwell 社區的 Wisp 協程。二者特點如下:

  • 透明性:Loom 不支持透明協程,這意味着業務方在引入 Loom 時需要對原有代碼進行一定的改造與適配。相比之下,Wisp 協程則提供了透明協程的支持,使得業務方能夠在幾乎不感知協程存在的情況下輕鬆使用。
  • 切換性能:Loom 的切換性能相對較低,這主要源於其對棧序列化的處理。而 Wisp 則憑藉高效的切換機制,實現了更高的切換性能。理論上,更高的切換性能意味着能夠支持更高的 QPS,從而帶來更好的系統性能與用户體驗。
  • 併發數:由於 Loom 協程的棧是按需使用的,因此它佔用的物理內存較少,同時對 StopTheWorld 事件的影響也更小。這使得 Loom 能夠支持更高的併發數,並通過結構化併發的策略,進一步降低服務的響應時間(RT)。
    綜合考慮快手 Java 服務的龐大體量以及業務適配改造的成本,最終選擇基於 Dragonwell 社區的 Wisp 協程方案進行改造優化。

2.2 快手 Java 協程架構演進

2.2.1 社區協程架構

Dragonwell 社區的原生協程架構作為快手協程架構的雛形,整體如下:

圖片

社區 Java 協程架構分為調度器、IO 管理模塊、Timer 管理模塊和 Locker 管理模塊 4 個主體。具體來説:調度器的工作線程是 WispCarrier,其數量和 CPU 核數相當,主要職責包括輪詢 RunQueue 獲取任務進行執行,查詢 IO 管理/Timer 管理/Locker 管理模塊獲取就緒任務到 RunQueue,Steal 任務等。如果 WispCarrier 處於空閒狀態則會進入休眠,讓出 CPU 資源;IO 管理模塊主要負責維護所有 FD 和阻塞 Task 的映射關係,基於 Epoll 機制提供 IO 就緒狀態的查詢能力;Timer 管理模塊則專注於定時器的全面管理,每個定時器對應一個阻塞 Task,該模塊提供定時器到期 Task 查詢能力;Locker 管理模塊同樣不可或缺,負責統籌管理所有因鎖而阻塞的任務,並具備高效查詢鎖就緒狀態下相關任務的能力。由於快手的 Java 服務場景相對複雜,上述架構模塊內部的一些機制缺陷在落地過程中逐步暴露出來,成為快手 Java 透明協程規模化落地的主要障礙。缺陷主要集中在如下幾個方面(對應上面架構圖紅色標記部分):

  • 調度器缺陷:原生調度實現策略在低負載工況下 CPU 消耗偏高,無法滿足客户的需求。
  • 搶佔缺陷:原生架構下協程長任務搶佔機制開銷大,且無法實現 JNI 長任務的及時搶佔,導致部分服務的長尾延時高,影響服務可用性。
  • IO 管理缺陷:原生 IO 管理機制在部分場景下 IO 查詢不及時,導致服務的平均延時嚴重劣化。快手需要通過持續的 Java 透明協程架構升級演進,來解決上述一系列制約 Java 透明協程技術規模化落地的障礙。

2.2.2 調度 CPU 優化

Wisp 在線上試點過程中暴露了低負載工況下 CPU 使用率偏高的問題(相比線程模型 CPU 劣化 10%+),儘管高負載時協程有顯著優勢,但低負載時性能表現卻不盡人意。

圖片

為了攻克 Wisp 調度器在低負載下的 CPU 效率難題,核心在於優化 Context-Switch 頻率。然而,我們面臨兩大挑戰:一是 Wisp 原生的認主模式導致任務均勻分散在所有 WispCarrier 上,難以實現任務集中執行以降低切換開銷;二是低負載時,Wisp 需依賴 WispCarrier0 和 WispCarrier1(作為 IO Poller)兩個線程協同工作,這進一步加劇了協程間的 Context-Switch 頻率。針對上述的問題,我們提煉出調度器設計的通用原則:

  • 線程數最小:在及時響應任務調度需求的前提下,保持儘可能少的活躍 WispCarrier 線程數,從而使得能 WispCarrier 儘可能連續執行任務,減少 Context-Switch。為了達到該目的,一方面,對業務的負載需求延遲滿足,喚醒新的 Idle WispCarrier 保持謹慎,避免過度響應需求,而是通過合理策略充分壓榨現有資源潛力,減少活躍線程數;另一方面,打破傳統調度器設計中不同任務類型(如 RunQueue、IO、Timer)各自擁有獨立調度線程的慣例,改為 WispCarrrier 在執行任務間隙兼顧 IO/Timer 調度,減少額外線程帶來的系統資源消耗。
  • 連續執行:儘可能保持活躍 WispCarrier 線程的穩定,假設我們保留 5 個活躍 WispCarrier,那麼調度器需要儘可能保證這 5 個活躍的 WispCarrier 不發生變化,確保工作的連續性,避免頻繁陷入空閒休眠狀態,引入額外的 Context-Switch。為了達到該目的,一方面任務提交到 WispCarrier 時,優先提交到當前或其它活躍的 WispCarrier,僅在必要時喚醒新的 WispCarrier 來 Steal,減少新 WispCarrrier 線程出現的概率,即使是必要的新 WispCarrier 喚醒,也儘量遵循 LIFO(後進先出)原則喚醒 Idle WispCarrier,確保新喚醒的載體更可能是“連續工作”的 WispCarrier;另一方面,WispCarrier 執行完所有任務時,避免立即進入休眠狀態,而是保留少部分作為活躍自旋的 WispCarrier 嘗試 Steal 其它 WispCarrier 的任務/Timer。

基於上述 2 條原則,我們重新設計了協程調度器的架構如下:

圖片

在新的架構下,WispCarrier 的 CPU 資源分佈呈現出一個倒金字塔狀集中分佈,完美契合了我們的設計初衷,成功消除了 Wisp 協程在低負載工況下相對於傳統協程的 CPU 效率劣勢。具體見下圖:

圖片

2.2.3 調度搶佔優化

協程的搶佔長尾延時高一直是業界面臨的難題,協程的切換時機完全依賴於用户代碼行為,如果遇到長任務(用户代碼長時間運行非阻塞代碼不釋放 WispCarrier),就會造成業務長尾延時高。為了緩解該問題,社區 Wisp 協程基於 Safepoint 機制實現了調度搶佔,但該機制存在如下問題:

  • Java 長任務搶佔代價高:為了搶佔一個業務協程,Safepoint 需要打斷所有的用户線程進入昂貴的 StopTheWorld,導致所有線程的業務 RT 都會發生抖動,影響面大。
  • JNI 長任務無法搶佔:快手大量使用 JNI,而 JNI 是無法被 Safepoint 打斷的,因此 JNI 長任務的長尾延時劣化無法解決。

對於 Java 長任務的搶佔,我們拋棄了昂貴的全局 Safepoint,改用 Java17 引入的 Handshake 機制來實現搶佔。Handshake 機制能夠實現特定線程的打斷,將 StopTheWorld 改為 StopTheThread,避免了 StopTheWorld 導致的所有線程的暫停。為了實現 JNI 長任務的搶佔,我們重新思考了搶佔的本質。搶佔的本質目的在於消除長任務執行對其它任務的影響,雖然 JNI 沒有類似 Handshake 的機制能夠打斷特定任務,但如果將受影響的任務及時轉交給其它的 WispCarrier 進行補償,同樣也可以消除長任務的影響。基於思路的轉換,我們針對 JNI 長任務設計了 HandOff 調度搶佔機制(Wisp 社區存在 HandOff 機制的原型,但很遺憾並沒有最終完全實現):當調度器發現某個 JNI 任務執行時間過長需要觸發搶佔時,我們將對應的 WispCarrier 中受影響的任務全部 HandOff 移交給其它空閒 WispCarrier 來執行,這樣 JNI 長任務就被限制在一個單獨的 WispCarrier 裏獨立運行,不會影響其它 WispTask。HandOff 搶佔機制整體架構圖如下:

圖片

對比舊的搶佔機制,新的任務搶佔機制有着顯著的優點:

  • 能夠搶佔 JNI 長任務:HandOff 通過補償空閒線程的方式,巧妙得實現了對於 JNI 長任務的搶佔。
  • 搶佔代價低:對於 Java 長任務,Handshake 搶佔代價顯著優於 Safepoint 搶佔;對於 JNI 長任務,基於一系列關鍵數據結構的重構(WP 分離),HandOff 僅僅是喚醒空閒線程和交換指針,搶佔開銷非常小。
    基於新的調度器搶佔設計,我們解決了 JNI 長任務造成的長尾延時劣化,擴大了協程優化的落地適用範圍,並提升了搶佔的性能。優化效果如下:

圖片

2.2.4 IO 模型優化

針對 Wisp IO 模型在生產環境中推廣時所暴露的缺陷,我們進行了 IO 模型的重構,旨在解決以下問題:

  • 查詢不及時:Wisp 進行 IO 查詢的響應速度不足,導致響應時間過長。
  • 設計低效:Wisp 原生的 IO 管理模塊採用基於 HashMap 的集中式 FD 到 WispTask 的關係映射設計存在激烈臨界態競爭。並且 Epoll 採用 EPOLLONESHOT 模式,系統調用過多。
  • 堆外內存膨脹:Wisp 的 Socket 劫持實現完全拷貝 Java8 的舊的 Socket,相比於 Java17 的線程模型下 NIOSocket,其 ThreadLocal DirectBuffer 堆外緩存不設容量上限,堆外內存資源消耗較多。

這些缺陷在某些服務工況下,使得 Wisp 的 RT 相對於線程模型顯著劣化。為了克服這些挑戰,我們遵循以下協程 IO 模型的設計原則進行了優化:

  • 非阻塞 IO 補償:在適當時機進行非阻塞 IO 補償,以規避 timedEpoll 執行優先級較低可能帶來的 IO 延時風險。
  • 複用內核結構:儘可能複用內核數據結構,簡化用户態設計,同時採用邊沿觸發代替 EPOLLONESHOT 模式,一次性為 FD 註冊所有 IO 事件,從而實現系統調用作用範圍的複用,大幅度減少 epoll_ctl 系統調用的頻率。
  • 資源緩存限定:對於 ThreadLocal DirectBuffer 等緩存資源,設定合理的上限,以防止資源消耗失控。

下圖是基於上述原則重構後的 IO 架構圖:

圖片

通過對 IO 模型的改進,我們解決了部分服務下 Wisp 延遲增加的問題,業務延時效果對比如下,優化後的 Wisp 在響應時間上有了顯著提升,與線程模型的性能差距明顯縮小,甚至在某些場景下實現了超越。

圖片

2.2.5 快手 Java 協程架構

通過上述一系列調度器、IO 管理、任務搶佔等關鍵架構模塊的深入優化和重構,我們最終完成了快手 Java 協程架構的整體升級,新架構如下:

圖片

在新架構下,之前困擾快手 Java 協程規模化落地的幾個關鍵問題都得到了很好的解決:

  1. 調度器缺陷:通過調度策略的重新設計,我們有效控制了低負載工況下協程的 CPU 消耗,滿足了客户的需求。
  2. 搶佔機制缺陷:通過新的 Handshake 和 HandOff 機制,我們顯著降低了 Java 長任務搶佔開銷,並實現了 JNI 長任務的及時搶佔,降低了服務可用性風險。
  3. IO 管理缺陷:通過 IO 管理模塊關鍵數據結構的重構改造,消除了 IO 查詢不及時導致的服務平均延時劣化。

上述架構已經在快手的 Java17 版本中得以實現,填補了 Dragonwell 社區協程特性在 Java17 上的空白。

三、協程落地成果與未來展望

通過和 Dragonwell 社區的深入合作,快手成功在 Java17 上實現了透明協程,並陸續解決了性能、穩定性、業務功能適配等一系列問題。如今,該技術已趨於成熟穩定,為業界提供了一個在大規模生產環境中成功應用協程的實踐範例。目前,協程已在快手全面部署上線,其服務極限 QPS 實現了 30%以上的顯著提升,有力推動了快手 Java 服務的降本增效。據統計,協程技術已為快手節省了數千萬的服務器成本。展望未來,隨着 Java 協程覆蓋率的持續提高,其帶來的服務性能提升將進一步降低快手的服務資源成本,實現更為顯著的經濟效益。在協程技術的未來發展中,我們還將致力於以下幾方面的探索與改進:

  • 與 Loom 的深度融合:Loom 作為 OpenJDK 社區的官方協程實現,我們將努力推動其與 Wisp 協程的和諧共存,以實現技術的互補與協同。
  • 調度策略與調度器的解耦:為了提供更加靈活高效的協程管理,我們將進一步將調度策略與調度器設計進行解耦,允許用户根據自身需求自定義協程策略。這將有助於用户實現更具針對性的、性能更優的調度方案,從而進一步提升服務效能。
user avatar xialeistudio 头像 haoqingwanqiandesigua 头像 lamazhenyuan 头像 yuanfang_648a85b26d85e 头像 binghe001 头像 headofhouchang 头像 tekin_cn 头像 hunter_58d48c41761b8 头像 feixianghelanren 头像 hantianfeng 头像 mrbone11 头像
点赞 11 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.