什麼是線上優雅停機和調整執行緒池參數?
- 2022 年 4 月 24 日
- 筆記
我是3y,一年CRUD經驗用十年的markdown程式設計師👨🏻💻常年被譽為職業八股文選手
好幾天沒更新austin的系列文章啦,主要是一直在寫austin的程式碼。而這篇文章我想了很久標題,最後定為《優雅,不過時》。文章的內容主要由以下部分組成:
- 應用發布重啟了怎麼辦?記憶體數據不是丟失了嗎?
- 什麼是優雅停機?如何實現優雅停機?
- 如何優雅地調整執行緒池的參數?
如果你的項目遇到了類似的問題,也可以借鑒下我今天所講解的內容,讀完我相信你肯定會有些收穫。
01、應用發布重啟了怎麼辦
眾所周知,如果我們系統在運行的過程中,記憶體數據沒存儲起來那就會導致丟失。對於austin項目而言,就會使消息丟失,並且無法下發到用戶上。
這個在我講述完我是如何設計「發送消息消費端」以及「讀取文件」時,尤其問得比較多。為了部分沒有追更的讀者,我再簡單講述下我這邊的設計:

在austin-handler模組,每個渠道的每種消息類型我都用到了執行緒池進行隔離而消費:

在austin-cron模組,我讀取文件是把每一條記錄放至了單執行緒池做LazyPending,目的為了延遲消費做批量下發。

敏感的技術人看到記憶體隊列或執行緒池(執行緒池也需要指定對應的記憶體隊列)就很正常地想:記憶體隊列可能的size為1024,而伺服器在重啟的時候可能記憶體隊列的數據還沒消費完,此時你怎麼辦?數據就丟了嗎?
我們使用執行緒池/記憶體隊列在很多場景下都是為了提高吞吐量,有得就必有失。至於重啟伺服器導致記憶體數據的丟失,就看你評估對自己的業務帶來多少的影響了。
針對這種問題,austin本身就開發好了相關的功能作為「補充」,通過實時計算引擎flink的能力可以實時在後台查看消息下發的情況:

可以在離線hive找到消息下發失敗的userId(離線這塊暫未實現),輸入具體的receiverId 可以查看實時下發時失敗的原因

查明原因之後再通過csv文件上傳的做補發。

不過,這是平台提供做補發的能力,從技術上的角度,還有別的思路盡量避免執行緒池或者記憶體隊列的數據因重啟而丟失的數據嗎?有的,優雅關閉執行緒池
02、優雅停機
所謂「優雅停機」就是關閉的時候先將自己需要處理的內容處理完了,之後才關閉。如果你直接kill -9,是沒有「優雅」這一說法的,神仙都救不了。
1、在網路層:TCP有四次揮手、TCP KeepAlive、HTTP KeepAlive 讓連接 優雅地關閉,避免很多報錯。
2、在Java裡邊通過Runtime.getRuntime().addShutdownHook()註冊事件,當虛擬機關閉的前調用該方法的具體邏輯進行善後。
3、在Spring裡邊執行了ApplicationContext 的close之後,只要我們Bean配置了destroy策略,那Spring在關閉之前也會先執行我們的已實現好的destroy方法
4、在Tomcat容器提供了幾種關閉的姿勢,先暫停請求,選擇等待N秒才完全關閉容器。
5、在Java執行緒池提供了shutdown和shutdownNow供我們關閉執行緒,顯然shutdown是優雅關閉執行緒池的方法。
我們的austin項目是基於SpringBoot環境構造的,所以我們可以重度依賴SpringBoot進行優雅停機。
1、我們設置應用伺服器的停機模式為graceful
server.shutdown=graceful
2、在austin已經引入動態執行緒池而非使用Spring管理下的ThreadPoolTaskExecutor,所以我們可以把自己創建出來的執行緒池在Spring關閉的時候,進行優雅shutdown(想要關閉其他的資源時,也可以類似干這種操作)

註:如果是使用Spring封裝過的執行緒池ThreadPoolTaskExecutor,默認就會優雅關閉,因為它是實現了DisposableBean介面的

03、如何優雅地調整執行緒池的參數?
austin在整個項目裡邊,還是有挺多地方是用到了執行緒池,特別重要的是從MQ里消費所創建的執行緒池。

有小夥伴當時給過建議:有沒有打算引入動態執行緒池,不用發布就調整執行緒池的參數從而臨時提高消費能力。順便在這給大家推薦美團的執行緒池文章://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html,如果沒讀過這篇文章的,建議都去讀下,挺不錯的。
美團這篇文章講述了動態執行緒池的思路,但應該是未官方開源,所以有很多小夥伴基於文章的思路造了好用的輪子。比如 Hippo4J 和dynamic-tp 都是比較優秀的輪子了。
這兩個倉庫我都看了下源碼, Hippo4J 有無依賴中間件實現動態執行緒池,也有默認實現Nacos和Apollo的版本,並有著管理後台,而dynamic-tp 默認實現依賴Nacos或Apollo。大佬們的程式碼都寫得很不錯,我推薦大家都可以去學學。
我在最初的時候接的是dynamic-tp的程式碼,因為我本身austin就接入了Apollo,也感覺暫時不太需要管理後台。後來 Hippo4J 作者找我聊了下,希望我能接入Hippo4J。
我按照我目前的使用場景對著程式碼看了一把,我是需要通過在創建執行緒池後再動態調參的場景。於是跟 Hippo4J 作者回饋了下,他果斷說晚上或明天就給我實現(:恐怖如斯,太肝了


不過,周三我回饋完,周四晚上我差不多就將 dynamic-tp 快接入完了。我目前現在打算先跑著(畢竟切換API其實也是需要時間成本的),後續看有沒有遇到痛點或者空的時候再遷移到Hippo4J再體驗體驗
也不為別的,就看中龍台大佬比我還肝(自己提出的場景,開源作者能很快地回饋並實現,太強了,絲毫不擔心有大坑要我自己搞)
04、總結
對於austin而言,正常的重啟發布我們通過優雅停機來儘可能減少系統的處理數據時的丟失。如果消息是真的非常重要而且需要做補發,在austin中也可以通過上傳文件的方式再做補發,且能看到實時推送的數據鏈路統計和某個用戶下發消息失敗的原因。
我相信,這已經能覆蓋線上絕大多數的場景了。
或許後續也可以針對某些場景在消費端做exactly once + 冪等 來解決kill -9的窘境,但要知道的是:想要保證數據不丟失、不重複發送給用戶,一定會帶來性能的損耗,這是需要做平衡的。
在項目很少使用執行緒池之前,一直可能認為執行緒池的相關面試題就是八股文。但當你項目系統真的遇到執行緒池優雅關閉的問題、執行緒池參數動態調整的問題,你就會發現之前看的內容其實是很有意義的。
阿,原來可以設置參數讓核心執行緒數也會回收的(之前一直都沒有注意過呢)
阿,原來都大多數框架都有提供對應的擴展介面給我們監聽關閉,默認的實現都有優雅停機的機制咯,之前一直都不知道呢。
....
austin還在持續優化和更新中,歡迎大佬們給點意見和想法一起討論,對該項目感興趣的同學也可以到我的GitHub上逛逛,或許有可能這個季度的KPI就有了咯。
動態執行緒池的倉庫地址:
- Hippo4J://github.com/acmenlt/dynamic-threadpool
- DynamicTp://gitee.com/yanhom/dynamic-tp
都看到這裡了,點個贊一點都不過分吧?我是3y,下期見。

關注我的微信公眾號【Java3y】除了技術我還會聊點日常,有些話只能悄悄說~ 【對線面試官+從零編寫Java項目】 持續高強度更新中!求star!!原創不易!!求三連!!
austin項目源碼Gitee鏈接:gitee.com/austin
austin項目源碼GitHub鏈接:github.com/austin

