dubbogo
Apache Dubbo是由阿里開源的一個RPC框架,而dubbogo則是相對應的go語言版本:

之前dubbogo一直沒有優(yōu)雅退出的機制,終于有小伙伴忍不住了強烈要求我們實現(xiàn)這個部分。艱難摸魚了兩周之后,我才把這個搞完,該功能的PR是https://github.com/apache/dubbo-go/pull/255。
當我們討論優(yōu)雅退出的時候,最基本的要求是自動無損停機。它同時強調(diào)了自動和無損兩個方面。

首先是自動,而與自動對應的則是手動了。手工介入的缺陷是顯而易見的,它要求我們在應用下線的時候手動摘掉流量。這一步可以通過網(wǎng)關、負載均衡或者注冊中心來實現(xiàn)。它還容易忘和出錯,如果這個東西還要求到運維身上,那就真的是下個線都得求爺爺告奶奶,開發(fā)體驗十分不好。
而無損,關鍵則是,正在執(zhí)行的事情要能執(zhí)行完。這個“事情”含義會非常廣泛,比如說發(fā)出去的請求我要能收回響應;收到的請求要執(zhí)行完畢并且給人家返回響應……如果更加嚴格的來說,那么本地啟動的定時任務,或者分布式事務,都應該在完成之后才能關機。
設計
我們先來看一下,一般情況下關機會發(fā)生什么:




這里面可以看出來,如果沒有優(yōu)雅退出機制的話,服務器是很任性的,誰都不說咔嚓一下關了。
然后注冊中心隔了一小會之后,通過心跳檢測或者監(jiān)聽到服務器跪了,心里臥槽一句之后,就趕緊通知客戶端。這個過程,客戶端這個傻白甜還是使勁發(fā)請求。

它收到注冊中心的通知之后,就懵逼了,心里一萬句MMP飄過之后,終于接受了自己剛才發(fā)出去的一些請求,收不到響應了的事實。

如果要是客戶端咔嚓一下關機呢?

所以我們可以看到,不論是客戶端突然關機還是服務端突然關機,都會造成問題。
于是我們的優(yōu)雅關機,就是要解決這么一個問題。從前面的那些圖可以看到,如果要優(yōu)雅退出,關鍵在于好好商量:




如果客戶端關機呢?那就更加簡單了,稍微停一下,把發(fā)出去的請求的響應收完再關機。
現(xiàn)實的情況是,一個節(jié)點,往往既是服務端,也是客戶端。這種情況下該怎么搞?首先在發(fā)出關機的信號后,它肯定不能關掉,至少要等到已接受的請求處理完成,才能關掉。往往是,處理一個請求會導致它作為客戶端發(fā)起一個調(diào)用。于是我們可以看到,在該節(jié)點既是服務端,又是客戶端的情況下,要先關閉作為服務端的功能,這樣才能防止因為要處理新的請求而不得不作為客戶端向別的服務器發(fā)起請求。
所以最終步驟就是:
- 告知注冊中心,即將關閉,此時等待并處理請求;
- 注冊中心通知別的客戶端,別的客戶端停止發(fā)送新請求,等待已發(fā)請求的響應;
- 節(jié)點處理完所有接收到的請求并且返回響應后,釋放作為服務端相關的組件和資源;
- 節(jié)點釋放作為客戶端的組件和資源;
實現(xiàn)
如何知道關機?
不管我們?nèi)绾螌崿F(xiàn)優(yōu)雅關機,第一個要解決的就是,我怎么知道這個節(jié)點要關機了?在Java虛擬機里面,有Runtime提供了addShutdownHook的方法:

golang就沒這個便利。好在golang提供了信號(Signal)機制。在golang里有一個os/signal的包,它是一個對操作系統(tǒng)信號的封裝——所以這是一個操作系統(tǒng)相關的東西,不過我這里只考慮Unix-Like系統(tǒng),畢竟我還是不怎么聽說有人在Windows上部署golang微服務的[手動狗頭]。

golang的文檔(https://golang.org/pkg/os/signal/)里面有很詳細的描述。我大概總結一下:
-
SIGKILL和SIGSTOP可能捕捉不到; -
SIGHUP,SIGINT和SIGTERM會導致系統(tǒng)退出; -
SIGQUIT,SIGILL,SIGTRAP,SIGABRT,SIGSTKFLT,SIGEMT,SIGSYS會導致系統(tǒng)退出,并且打印此時的棧;
所以我們只需要監(jiān)聽這些信號的處理就可以了。

釋放資源步驟
前面我們討論了關機釋放資源所需要按序執(zhí)行的步驟,那么落地到dubbogo里面該如何實現(xiàn)呢?
從dubbogo的源碼能夠發(fā)現(xiàn),關鍵的組件就是Registry和Protocol。
其中Protocol從邏輯上來說,可以分成供Provider使用的Protocol和供Consumer使用的Protocol。當然,Protocol也可能同時提供兩者使用。因此我們考慮到這種情況,在銷毀Provider的Protocol的時候,要把共用的那些Protocol剔除出來。
按照我們的預先分析的步驟,釋放資源的步驟應該是:
- 銷毀所有的
Registry實例,這也就是從注冊中心里面注銷。這個過程,客戶端因為有監(jiān)聽注冊中心的事件,所以很快就能知道某個服務器已經(jīng)不可用;

- 在步驟1之后,理論上來說所有的客戶端都不會再發(fā)請求過來了。但是還有很多時候,一個是注冊中心通知客戶端的延時,二是不同的客戶端可能有一些奇怪的緩存機制,再一個就是此時正在發(fā)送的請求。這幾種情況下,還會有部分請求到達服務器,所以服務器還需要接收這部分請求然后處理掉,因而要等待一段時間;

- 在步驟2之后,絕大部分情況下,服務端就可以直接銷毀掉扮演
Provider的Protocol了。然而,如果步驟2等待時間過短,或者說客戶端和注冊中心就服務器下線這個事情達成一致的時間太長,那么這個階段還會收到請求。這個時候我們就只能拒絕請求了。此時,我們還要判斷一下,當前正在處理的請求處理完了沒有,如果處理完了,或者等了一段時間之后都還沒處理完,就進入下一個階段;

在這個步驟,服務器才真的摧毀作為
Provider的Protocol。經(jīng)過步驟4,服務器還可能處在一種“雖然我無法響應別人,但是我還在處理點事情,我要等別人的響應”的狀態(tài)中,所以這個時候我們再稍微停下來等一下,如果所有的響應都收到請求了,或者超時,進入下一個階段;

- 摧毀掉剩下的
Protocol。 - 理論上來說,經(jīng)過步驟6,在框架層面上,所有的資源都釋放了。但是這個時候我們要考慮到開發(fā)者可能在此時需要釋放他創(chuàng)建的資源,因此我們要提供一個回調(diào)機制,允許他們在這個時間節(jié)點回收資源;

我們的源碼里面很容易看出來這些步驟:

如何確定每一步的超時時間
在實現(xiàn)這個優(yōu)雅退出的時候,有一個參數(shù)非常關鍵,就是每一步的退出時間step_timeout,它代表的是,在前面提及的每一個步驟,如果需要停下來等待,那么會在多久以后超時,結束等待。
在大大大大大多數(shù)情況下,設置這個時間只需要考慮第一個停下來等待的步驟,即服務端在宣稱了自己要停機,并且銷毀了Registry之后停下來等待新請求的時長。也就是執(zhí)行方法waitAndAcceptNewRequests的超時時間。
有一個簡單的式子可以描述這個時間:客戶端收到注冊中心通知的時長+請求響應時長。
第一個“客戶端收到注冊中心通知的時長”很好理解,但是也比較難估算。這主要取決于注冊中心和客戶端緩存機制。我個人經(jīng)驗是使用ZK的情況下,一般不會超過1秒。
第二個“請求響應時長”最復雜了。首先,這是一個從客戶端觀察的值。也就是說,它不是我們監(jiān)控到的服務端的服務響應時間,而是從客戶端發(fā)出一個請求到它收到完全響應的時長。于服務端而言,大概是“請求傳輸時長+服務響應時長+響應傳輸時長”。

然后我們又會面臨一個問題,一個服務端往往提供多個服務,我該取哪個服務的請求響應時長?答案是取決于你具體的業(yè)務和你的期望。開發(fā)者可以基于自己的服務的重要性,取比較重要的服務的999線;又或者全部服務一起考慮,取999線。這里我比較不建議使用平均線,因為平均線意味著有很多的請求無法再這個時間內(nèi)返回響應。
另外一種比較罕見的選擇是,使用定時任務的執(zhí)行時間,或者事務——尤其是分布式事務——的完成時間。
核心就是,你覺得哪個東西最重要,你就用那個東西的執(zhí)行時間。
上面的邏輯也適用于單純是Consumer的應用。
大多數(shù)情況下,step_timeout默認值10秒足以應付了。
未實現(xiàn)部分
特殊回調(diào)
這個小標題有點不太準確。大家注意到的是,我只在所有框架資源都被銷毀之后才會回調(diào)開發(fā)者注冊的回調(diào)。這個時候就有這么一些問題:
- 如果開發(fā)者在自定義的回調(diào)里面希望用到
dubbogo的功能,特別是發(fā)起遠程調(diào)用,那么顯然是不可能的——雖然我也覺得不會有人會這么干; - 如果開發(fā)者的回調(diào)希望按照順序來執(zhí)行,那么也是不支持的。我們只會按照注冊回調(diào)的順序來依次調(diào)用。當然開發(fā)者可以通過將多個回調(diào)按序調(diào)用組成一個更復雜的回調(diào)來實現(xiàn)這個目標。不支持它主要是一個取舍問題。我相信有這種需求的人是少數(shù)以至于幾乎沒有的……
底層支持的優(yōu)雅停機
前面所有的步驟,都是直接建立在應用層面上。實際上,還有一些業(yè)界的做法,是在底層協(xié)議上就直接提供了支持。比如說,通過TCP連接發(fā)送一個只讀事件,那么客戶端后續(xù)就自然不會再把請求發(fā)過來。
我們的優(yōu)雅停機并沒有使用到這一種機制,因為在應用層面上就能夠解決。dubbogo里面的Registry和Protocol的Destroy都沒采用這種機制。
但是這的確是一個很不錯的實現(xiàn)思路。dubbo就是采用了這種方式。