作系統升級擴容,停服務時候最頭疼的時候就是業務數據錯亂,數據包的丟失,哪咱們如何避免服務停機帶來的業務損失?
安全
關閉爲何有問題?
服務器
咱們知道,在「單體應用」複雜到必定程度後,咱們通常會進行系統拆分,也就是時下流行的微服務架構。服務拆分以後,天然就須要協同,因而 RPC 框架就出來了,它用來解決各個子系統之間的通訊問題。
架構
我再倒回來問你一個很是基礎的問題?你以爲系統爲啥非要拆分呢?從個人角度,若是隻說一個緣由,我以爲拆分以後咱們能夠更方便、更快速地迭代業務。那麼問題來了,更快速地迭代業務,說人話不就是我會常常更新應用系統,時不時還老要重啓服務器嗎?
app
那具體到咱們的 RPC 體系裏,你就要考慮,在重啓服務的過程當中,RPC 怎麼作到讓調用方系統不出問題呢?負載均衡
要想說明白這事,咱們先要簡述下上線的大概流程:當服務提供方要上線的時候,通常是經過部署系統完成實例重啓。在這個過程當中,服務提供方的團隊並不會事先告訴調用方他們須要操做哪些機器,從而讓調用方去事先切走流量。而對調用方來講,它也沒法預測到服務提供方要對哪些機器重啓上線,所以負載均衡就有可能把要正在重啓的機器選出來,這樣就會致使把請求發送到正在重啓中的機器裏面,從而致使調用方不能拿到正確的響應結果框架
在服務重啓的時候,對於調用方來講,這時候可能會存在如下幾種狀況:運維
調用方發請求前,目標服務已經下線。對於調用方來講,跟目標節點的鏈接會斷開,這時候調用方能夠立馬感知到,而且在其健康列表裏面會把這個節點挪掉,天然也就不會被負載均衡選中。ide
調用方發請求的時候,目標服務正在關閉,但調用方並不知道它正在關閉,並且二者之間的鏈接也沒斷開,因此這個節點還會存在健康列表裏面,所以該節點就有必定機率會被負載均衡選中。微服務
關閉流程spa
知道了根本緣由,問題就很好解決了。由於服務提供方已經開始進入關閉流程,那麼不少對象就可能已經被銷燬了,關閉後再收到的請求按照正常業務請求來處理,確定是無法保證能處理的。因此咱們能夠在關閉的時候,設置一個請求「擋板」,擋板的做用就是告訴調用方,我已經開始進入關閉流程了,我不能再處理你這個請求了。
若是你們常常去銀行辦理業務,就會很熟悉這個流程。在交接班或者有其餘要事情處理的時候,銀行櫃檯工做人員會拿出一個紙板,放在窗口前,上面寫到「該窗口已關閉」。在該窗口排隊的人雖然有一萬個不肯意,也只能換到其它窗口辦理業務,由於櫃檯工做人員會把當前正在辦理的業務處理完後正式關閉窗口。
基於這個思路,咱們能夠這麼處理:當服務提供方正在關閉,若是這以後還收到了新的業務請求,服務提供方直接返回一個特定的異常給調用方(好比 ShutdownException)。這個異常就是告訴調用方「我已經收到這個請求了,可是我正在關閉,並無處理這個請求」,而後調用方收到這個異常響應後,RPC 框架把這個節點從健康列表挪出,並把請求自動重試到其餘節點,由於這個請求是沒有被服務提供方處理過,因此能夠安全地重試到其餘節點,這樣就能夠實現對業務無損。
但若是隻是靠等待被動調用,就會讓這個關閉過程總體有點漫長。由於有的調用方那個時刻沒有業務請求,就不能及時地通知調用方了,因此咱們能夠加上主動通知流程,這樣既能夠保證明時性,也能夠避免通知失敗的狀況。
說到這裏,我知道你確定會問,那要怎麼捕獲到關閉事件呢?
在個人經驗裏,能夠經過捕獲操做系統的進程信號來獲取,在 Java 語言裏面,對應的是 Runtime.addShutdownHook 方法,能夠註冊關閉的鉤子。在 RPC 啓動的時候,咱們提早註冊關閉鉤子,並在裏面添加了兩個處理程序,一個負責開啓關閉標識,一個負責安全關閉服務對象,服務對象在關閉的時候會通知調用方下線節點。同時須要在咱們調用鏈裏面加上擋板處理器,當新的請求來的時候,會判斷關閉標識,若是正在關閉,則拋出特定異常。
看到這裏,感受問題已經比較好地被解決了。但在關閉過程當中已經在處理的請求會不會受到影響呢?
若是進程結束過快會形成這些請求尚未來得及應答,同時調用方會也會拋出異常。爲了儘量地完成正在處理的請求,首先咱們要把這些請求識別出來。這就比如平常生活中,咱們常常看見停車場指示牌上提示還有多少剩餘車位,這個是如何作到的呢?若是仔細觀察一下,你就會發現它是每進入一輛車,剩餘車位就減一,每出來一輛車,剩餘車位就加一。咱們也能夠利用這個原理在服務對象加上引用計數器,每開始處理請求以前加一,完成請求處理減一,經過該計數器咱們就能夠快速判斷是否有正在處理的請求。
服務對象在關閉過程當中,會拒絕新的請求,同時根據引用計數器等待正在處理的請求所有結束以後纔會真正關閉。但考慮到有些業務請求可能處理時間長,或者存在被掛住的狀況,爲了不一直等待形成應用沒法正常退出,咱們能夠在整個 ShutdownHook 裏面,加上超時時間控制,當超過了指定時間沒有結束,則強制退出應用。超時時間我建議能夠設定成 10s,基本能夠確保請求都處理完了。整個流程以下圖所示。
總結
在 RPC 裏面,關閉雖然看似不屬於 RPC 主流程,但若是咱們不能處理得很好的話,可能就會致使調用方業務異常,從而須要咱們加入不少額外的運維工做。一個好的關閉流程,能夠確保使用咱們框架的業務實現平滑的上下線,而不用擔憂重啓致使的問題。
其實「優雅關閉」這個概念除了在 RPC 裏面有,在不少框架裏面也都挺常見的,好比像咱們常常用的應用容器框架 Tomcat。Tomcat 關閉的時候也是先從外層到裏層逐層進行關閉,先保證不接收新請求,而後再處理關閉前收到的請求。