如何實現線上優雅停機和調整執行緒池引數?

Java3y發表於2022-04-24

我是3y,一年CRUD經驗用十年的markdown程式設計師??‍?常年被譽為職業八股文選手

好幾天沒更新austin的系列文章啦,主要是一直在寫austin的程式碼。而這篇文章我想了很久標題,最後定為《優雅,不過時》。文章的內容主要由以下部分組成:

  • 應用釋出重啟了怎麼辦?記憶體資料不是丟失了嗎?
  • 什麼是優雅停機?如何實現優雅停機?
  • 如何優雅地調整執行緒池的引數?

如果你的專案遇到了類似的問題,也可以借鑑下我今天所講解的內容,讀完我相信你肯定會有些收穫。

01、應用釋出重啟了怎麼辦

眾所周知,如果我們系統在執行的過程中,記憶體資料沒儲存起來那就會導致丟失。對於austin專案而言,就會使訊息丟失,並且無法下發到使用者上。

這個在我講述完我是如何設計「傳送訊息消費端」以及「讀取檔案」時,尤其問得比較多。為了部分沒有追更的讀者,我再簡單講述下我這邊的設計:

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

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

敏感的技術人看到記憶體佇列或執行緒池(執行緒池也需要指定對應的記憶體佇列)就很正常地想:記憶體佇列可能的size1024,而伺服器在重啟的時候可能記憶體佇列的資料還沒消費完,此時你怎麼辦?資料就丟了嗎?

我們使用執行緒池/記憶體佇列在很多場景下都是為了提高吞吐量,有得就必有失。至於重啟伺服器導致記憶體資料的丟失,就看你評估對自己的業務帶來多少的影響了。

針對這種問題,austin本身就開發好了相關的功能作為「補充」,通過實時計算引擎flink的能力可以實時在後臺檢視訊息下發的情況:

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

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

不過,這是平臺提供做補發的能力,從技術上的角度,還有別的思路儘量避免執行緒池或者記憶體佇列的資料因重啟而丟失的資料嗎?有的,優雅關閉執行緒池

02、優雅停機

所謂「優雅停機」就是關閉的時候先將自己需要處理的內容處理完了,之後才關閉。如果你直接kill -9,是沒有「優雅」這一說法的,神仙都救不了。

1、在網路層:TCP有四次揮手、TCP KeepAliveHTTP KeepAlive 讓連線 優雅地關閉,避免很多報錯。

2、在Java裡邊通過Runtime.getRuntime().addShutdownHook()註冊事件,當虛擬機器關閉的前呼叫該方法的具體邏輯進行善後

3、在Spring裡邊執行了ApplicationContextclose之後,只要我們Bean配置了destroy策略,那Spring在關閉之前也會先執行我們的已實現好的destroy方法

4、在Tomcat容器提供了幾種關閉的姿勢,先暫停請求,選擇等待N秒才完全關閉容器。

5、在Java執行緒池提供了shutdownshutdownNow供我們關閉執行緒,顯然shutdown是優雅關閉執行緒池的方法。

我們的austin專案是基於SpringBoot環境構造的,所以我們可以重度依賴SpringBoot進行優雅停機。

1、我們設定應用伺服器的停機模式為graceful

server.shutdown=graceful

2、在austin已經引入動態執行緒池而非使用Spring管理下的ThreadPoolTaskExecutor,所以我們可以把自己建立出來的執行緒池在Spring關閉的時候,進行優雅shutdown(想要關閉其他的資源時,也可以類似幹這種操作)

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

03、如何優雅地調整執行緒池的引數?

austin在整個專案裡邊,還是有挺多地方是用到了執行緒池,特別重要的是從MQ裡消費所建立的執行緒池。

有小夥伴當時給過建議:有沒有打算引入動態執行緒池,不用釋出就調整執行緒池的引數從而臨時提高消費能力。順便在這給大家推薦美團的執行緒池文章:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html,如果沒讀過這篇文章的,建議都去讀下,挺不錯的。

美團這篇文章講述了動態執行緒池的思路,但應該是未官方開源,所以有很多小夥伴基於文章的思路造了好用的輪子。比如 Hippo4Jdynamic-tp 都是比較優秀的輪子了。

這兩個倉庫我都看了下原始碼, Hippo4J無依賴中介軟體實現動態執行緒池,也有預設實現NacosApollo的版本,並有著管理後臺,而dynamic-tp 預設實現依賴NacosApollo。大佬們的程式碼都寫得很不錯,我推薦大家都可以去學學。

我在最初的時候接的是dynamic-tp的程式碼,因為我本身austin就接入了Apollo,也感覺暫時不太需要管理後臺。後來 Hippo4J 作者找我聊了下,希望我能接入Hippo4J

我按照我目前的使用場景對著程式碼看了一把,我是需要通過在建立執行緒池後再動態調參的場景。於是跟 Hippo4J 作者反饋了下,他果斷說晚上或明天就給我實現(:恐怖如斯,太肝了

不過,週三我反饋完,週四晚上我差不多就將 dynamic-tp 快接入完了。我目前現在打算先跑著(畢竟切換API其實也是需要時間成本的),後續看有沒有遇到痛點或者空的時候再遷移到Hippo4J再體驗體驗

也不為別的,就看中龍臺大佬比我還肝(自己提出的場景,開源作者能很快地反饋並實現,太強了,絲毫不擔心有大坑要我自己搞)

04、總結

對於austin而言,正常的重啟發布我們通過優雅停機來儘可能減少系統的處理資料時的丟失。如果訊息是真的非常重要而且需要做補發,在austin中也可以通過上傳檔案的方式再做補發,且能看到實時推送的資料鏈路統計和某個使用者下發訊息失敗的原因。

我相信,這已經能覆蓋線上絕大多數的場景了。

或許後續也可以針對某些場景在消費端做exactly once + 冪等 來解決kill -9的窘境,但要知道的是:想要保證資料不丟失、不重複傳送給使用者,一定會帶來效能的損耗,這是需要做平衡的。

在專案很少使用執行緒池之前,一直可能認為執行緒池的相關面試題就是八股文。但當你專案系統真的遇到執行緒池優雅關閉的問題、執行緒池引數動態調整的問題,你就會發現之前看的內容其實是很有意義的。

阿,原來可以設定引數讓核心執行緒數也會回收的(之前一直都沒有注意過呢)

阿,原來都大多數框架都有提供對應的擴充套件介面給我們監聽關閉,預設的實現都有優雅停機的機制咯,之前一直都不知道呢。

....

austin還在持續優化和更新中,歡迎大佬們給點意見和想法一起討論,對該專案感興趣的同學也可以到我的GitHub上逛逛,或許有可能這個季度的KPI就有了咯。

動態執行緒池的倉庫地址:

都看到這裡了,點個贊一點都不過分吧?我是3y,下期見。

關注我的微信公眾號【Java3y】除了技術我還會聊點日常,有些話只能悄悄說~ 【對線面試官+從零編寫Java專案】 持續高強度更新中!求star!!原創不易!!求三連!!

austin專案原始碼Gitee連結:gitee.com/austin

austin專案原始碼GitHub連結:github.com/austin

相關文章