1、實踐背景
現在java主流的微服務技術棧毫無疑問是SpringCloud,這也是經銷商技術部微服務實踐采用的技術棧。注冊中心采用公司技術部的nacos。在SpringCloud實踐中大家普遍遇到的問題是應用默認是無法做到無損下線的,需要更多的輔助措施才能得到無損下線的效果,本文主要分享我們團隊解決應用無損下線的一些實踐。
2、有損下線
有損下線指的是應用在下線過程中,部分請求沒有被妥善處理,出現請求異常進而影響應用可用性,影響用戶使用。有損下線原因分析:
損失原因1:springboot實例默認接收到停止信號TERM時,馬上停止服務。如下圖
當springboot實例收到TERM信號立即關閉的時候,很有可能請求隊列中還有請求,還有一部分正在處理的請求。如果立即關閉這些請求都會損失掉。
解決方案:在springboot 2.3版本以上,提供了優雅關閉(Graceful Shutdown)配置。如下:
server:
shutdown: graceful
配置了優雅關閉后,實例關閉過程如下圖所示:
springboot實例在收到TERM信號后,不會立即關閉應用,而是進入優雅處理階段,這個優雅時間段時長可以配置,默認為30秒。
springboot在網絡層拒絕新的請求進入的同時,會等隊列中的請求和正在處理請求都處理完成或者優雅時間耗盡才關閉應用。這樣只要給應用配置合適的優雅關閉階段時長就可以避免這類請求損失。
損失原因2:在使用注冊中心的默認情況下,服務下線狀態無法實時通知給調用方。
在微服務引入注冊中心的場景下,provider(服務提供方),registry(注冊中心),consumer(服務調用方)正常服務調用過程如下:
服務下線時的時序圖如下:
當provider收到TERM信號進入Graceful shutdown階段的時候也就是圖中的1時間點,provider在網絡層便不再接收新的請求,然后直到時間點4,consumer才停止向provider發送請求。所以損失時間 = 時間點4 – 時間點1。
3、解決方案
方案1:provider下線盡快通知到consumer。
Ribbon默認采用輪詢拉取服務列表的方式,時間間隔默認為30秒,也就是說
時間點3 – 時間點2 = 30秒 (最長)
對一個服務來說,如果30秒不可用,情況是相當糟糕的,改進方法有兩個,一是縮短輪詢間隔到5秒左右;二是可以實現注冊中心事件推送方式通知consumer更新服務列表,盡量縮短損失時長。但是這只能治標不治本,因為雖然損失時長縮短了,但是仍然還有幾秒損失,每次上線都需要損失請求仍然是不可接受的。
方案2:在provider關閉前通知到consumer下線。
如果是可以人工干預下線過程,我們大概會這么干:首先把要下線的provider實例從負載中摘除掉確保不再有流量打到下線實例后,才真正執行下線命令。這樣就不會有任何流量損失了。只需要將這個流程自動化就可以完美解決了。
好在k8s提供了preStop配置,在k8s平臺中,preStop和TERM信號關系是這樣的:preStop會先執行完,然后k8s才會給pod發送TERM信號,k8s會給pod的停止設置一個寬限時長,超過寬限時長會強殺掉,這個寬限時長從preStop調用開始計時。采用這個方案的話,服務下線時序圖如下:
可以看到當consumer不再往provider打流量后,provider才開始執行shutdown,這樣不會有任何損失。
4、實現步驟
1) 縮短輪詢間隔
在springboot應用中增加如下配置:
ribbon:
ServerListRefreshInterval: 5000 # 服務列表刷新間隔
2)springboot應用增加preStop hook實現
擴展springboot actuator節點,具體代碼如下。
其中sleepSeconds可以根據項目配置。
3)在云平臺配置preStop
4)安全加固
由于暴露了prestop hook接口,如果服務直接暴露至公網,有可能被惡意掃描調用,對接口進行安全加固是很有必要的。我們在prestop hook接口增加token參數,例如:
prestop-hook-api?token=xxxxxxxxxxxxxx
每個項目可以約定不同token值,并對token值進行校驗,token值不對的訪問將被拒絕,這樣惡意掃描就無法訪問到prestop hook 接口了。
代碼實現我們已經封裝在harmless-starter中并集成到項目模板里,這樣使用者只需要配置sleepSeconds即可。
5、總結
本文主要記錄了經銷商技術部在保證SpringCloud應用無損下線的一些實踐探索和總結,其中對SpringCloud服務下線造成損失的原因進行了比較全面的分析并提供了最終解決方案和實現,希望對其他人也有所幫助。