13 | 優雅關閉:如何避免服務停機帶來的業務損失?
我們在RPC架構下,需要考慮當服務重啟時,如何做到讓呼叫方系統不出問題,
當服務提供方要上線時,一般是通過部署系統完成實體重啟,在這個程序匯總,服務提供方不會事先告訴呼叫方哪些實體會被重啟,從而讓呼叫方切換流量,而對呼叫方來說,它也無法預測服務提供方哪些實體會重啟,因此負載均衡還是有可能降正在重啟的實體挑選出來,這樣導致請求被分發到正在重啟的服務實體中,造成呼叫方無法拿到正確的回應結果,
在服務重啟的時候,對于呼叫方來說,有以下2種情況:
- 呼叫方發請求前,目標服務已經下線,對于呼叫方來說,跟目標節點的連接會斷開,這時呼叫可以立刻感知到,并在其健康串列中將該實體洗掉,這樣就不會被負載均衡選中,
- 呼叫方發請求時,目標服務正在關閉,但呼叫方并不知道它正在關閉,而且兩者之間的連接沒有斷開,所以這個節點還會存在健康串列里面,有可能會被負載均衡選中,
我們可以通過服務發現來實時通知服務呼叫方關于服務提供方是否可用嗎?
不可以,這樣做的話,整個程序會依賴兩次RPC呼叫:一次是服務提供方通知注冊中心下線操作,一次是注冊中心通知服務呼叫方下線節點操作,注冊中心通知服務呼叫方都是異步的,服務發現只保證最終一致性,并不保證實時性,所以當注冊中心收到服務提供方下線的時候,并不能保證把這次要下線的節點推送給所有呼叫方,這樣,呼叫方還是有可能將請求發送給錯誤的服務提供方節點,
如何做到優雅關閉服務?
我們可以嘗試讓服務提供方來通知呼叫方,RPC里面呼叫方和提供方之間是長連接,我們可以在提供方應用記憶體中維護一份呼叫方連接集合,當服務關閉時,挨個通知呼叫方去下線相關實體,這樣整個呼叫鏈路就變短了,對于每個呼叫方來說只一次RPC,可以確保呼叫的成功率很高,
但是上述方法不能徹底解決問題,因為有時出問題請求的時間點和收到提供方關閉通知的時間點很接近,再加上網路延遲,還是有可能在服務提供方關閉服務后再接收到新的請求,
解決辦法是我們在關閉的時候,在服務提供方設定一個請求“擋板”,它的作用是告訴呼叫方,我已經進入關閉流程,不能再處理新的請求了,
當服務提供方正在關閉,如果在之后還收到了新的業務請求,服務提供方直接回傳一個特定的例外給呼叫方(ShutdownException),這個例外就是告訴呼叫方“我已經收到這個請求了,但是我正在關閉,沒有處理這個請求”,然后呼叫方收到這個例外回應后,RPC框架就把這個節點從健康串列中挪出,并把請求自動重試到其他節點,因為這個請求沒有被服務提供方處理過,所以可以安全的重試到其他節點,這樣可以實作對業務無損,
我們還可以加上主動通知流程,讓服務提供方給相關呼叫方發送關閉通知,這樣既可以保證實時性,也可以避免通知失敗的情況,
在Java語言中,我們可以使用Runtime.addShudownHook方法,來注冊關閉的鉤子,在RPC啟動的時候,我們提前去注冊關閉鉤子,并在里面添加連個處理程式:一個復雜開啟關閉標識,一個負責安全關閉服務物件,服務物件在關閉的時候會通知呼叫方下線節點,同時我們需要再呼叫鏈里面加上擋板處理器,當新的請求進來時,會判斷關閉標識,如果正在關閉,就拋出特定例外,
對于關閉程序中還在處理的請求,我們可以根據參考計數器,等待正在處理的請求全部結束后再真正關閉服務,同時還可以設定一個超時控制,當超過指定時間,請求還沒有處理完,就強制退出應用,
總結一下,關于如何優雅關閉服務,包括以下步驟:
- 開啟關閉擋板,拒絕新的請求
- 利用參考計數器確保正在執行的請求處理完
- 設定超時時間,保證服務可以正常關閉
- 執行關閉時,服務提供方通知服務呼叫方下線相關節點
服務優雅關閉的示意圖如下,

“優雅關閉”的概念除了在RPC里面,在其他很多框架中也很常見,例如Tomcat在關閉的時候,也是先從外層到里層逐層進行關閉,先保證不接收新的請求,然后再處理關閉前收到的請求,
14 | 優雅啟動:如何避免流量達到沒有啟動完成的節點?
為什么Java程式運行一段時間會執行速度會變快?
這是因為在Java里面,在運行程序中,JVM虛擬機會把高頻的代碼編譯成機器碼,被加載過的類也會被快取到JVM快取中,再次使用的時候就不會觸發臨時加載,這樣就使得
“熱點”代碼的執行不用每次都通過解釋,從而提升執行速度,
什么是啟動預熱?
啟動預熱就是讓剛啟動的服務提供方應用不承擔全部的流量,而是讓它被呼叫的次數隨著時間的移動慢慢增加,最終讓流量緩和地增加到跟已經運行一段時間后的水平一樣,
服務呼叫方應用通過服務發現能夠取得服務提供方的IP地址,然后每次發送請求前,都需要通過負載均衡演算法從連接池中選擇一個可用連接,我們可以讓負載均衡在選擇連接的時候,區分一下是不是剛啟動的應用,如果是剛啟動的應用,我們可以調低它的權重值,這樣它被選中的概率會很低,隨著時間推移,我們逐漸增大它的權重值,從而實作一個動態增加流量的效果,
我們如何獲取服務提供方應用的啟動時間?有兩種方法:
- 服務提供方在啟動的時候,把自己啟動的時間告訴注冊中心,
- 注冊中心收到的服務提供方請求注冊的時間,
啟動越熱更多是從呼叫方的角度出發,去解決服務提供方應用冷啟動的問題,讓呼叫方的請求量通過一個時間視窗過渡,慢慢達到一個正常的水平,從而實作平滑上線,
從服務提供方的角度來說,有什么優化方案嗎?服務提供方可以使用延遲暴露的方法來優化熱啟動程序,
問題:服務提供方應用在沒有完成啟動的時候,呼叫方的請求就過來了,而呼叫方請求過來的原因,在于服務提供方應用啟動程序中把決議到的RPC服務注冊到了注冊中心,這就導致了后續加載沒有完成的情況下,服務提供方地址就被服務呼叫方感知到了,
解決辦法:我們在應用啟動加載、決議Bean的時候,如果遇到了RPC服務的Bean,只先把這個Bean注冊到Spring-BeanFactory里面,而不把這個Bean對應的介面注冊到注冊中心,只有等應用啟動完成后,才被介面注冊到注冊中心用于服務發現,從而實作讓服務呼叫方延遲獲取到服務提供方地址,
我們還可以利用服務啟動完成到注冊到注冊中心的那段時間,預留一個Hook,讓用戶可以擴展Hook邏輯,在Hook里面模擬業務呼叫邏輯,從而使得JVM指令能夠預熱起來,同時還可以在Hook中預先加載一些資源,只有等所有快取和資源都加載完成后,才把介面注冊到注冊中心,這樣也就完成了熱啟動整個流程,
如果我們有大批量的服務都需要重啟,如何避免同時重啟造成請求被分發到新啟動的應用實體而造成超時錯誤?
我們可以采取一些措施:
- 分時分批啟動,就像灰度發布一樣,
- 根據重啟比例來設定重啟服務的權重,
- 在請求低峰重啟應用,
- 在重啟程序中,如有必要,對服務進行限流處理,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/542472.html
標籤:其他
