dubbogo
Apache Dubbo是由阿里開源的一個(gè)RPC
框架邻眷,而dubbogo則是相對應(yīng)的go
語言版本:
之前dubbogo
一直沒有優(yōu)雅退出的機(jī)制佃声,終于有小伙伴忍不住了強(qiáng)烈要求我們實(shí)現(xiàn)這個(gè)部分。艱難摸魚了兩周之后阴绢,我才把這個(gè)搞完,該功能的PR
是https://github.com/apache/dubbo-go/pull/255
。
當(dāng)我們討論優(yōu)雅退出的時(shí)候鸡挠,最基本的要求是自動(dòng)無損停機(jī)。它同時(shí)強(qiáng)調(diào)了自動(dòng)和無損兩個(gè)方面搬男。
首先是自動(dòng)拣展,而與自動(dòng)對應(yīng)的則是手動(dòng)了。手工介入的缺陷是顯而易見的缔逛,它要求我們在應(yīng)用下線的時(shí)候手動(dòng)摘掉流量备埃。這一步可以通過網(wǎng)關(guān)、負(fù)載均衡或者注冊中心來實(shí)現(xiàn)褐奴。它還容易忘和出錯(cuò)按脚,如果這個(gè)東西還要求到運(yùn)維身上,那就真的是下個(gè)線都得求爺爺告奶奶敦冬,開發(fā)體驗(yàn)十分不好辅搬。
而無損,關(guān)鍵則是脖旱,正在執(zhí)行的事情要能執(zhí)行完伞辛。這個(gè)“事情”含義會(huì)非常廣泛烂翰,比如說發(fā)出去的請求我要能收回響應(yīng);收到的請求要執(zhí)行完畢并且給人家返回響應(yīng)……如果更加嚴(yán)格的來說蚤氏,那么本地啟動(dòng)的定時(shí)任務(wù)甘耿,或者分布式事務(wù),都應(yīng)該在完成之后才能關(guān)機(jī)竿滨。
設(shè)計(jì)
我們先來看一下佳恬,一般情況下關(guān)機(jī)會(huì)發(fā)生什么:
這里面可以看出來,如果沒有優(yōu)雅退出機(jī)制的話于游,服務(wù)器是很任性的毁葱,誰都不說咔嚓一下關(guān)了。
然后注冊中心隔了一小會(huì)之后贰剥,通過心跳檢測或者監(jiān)聽到服務(wù)器跪了倾剿,心里臥槽一句之后,就趕緊通知客戶端蚌成。這個(gè)過程前痘,客戶端這個(gè)傻白甜還是使勁發(fā)請求。
它收到注冊中心的通知之后担忧,就懵逼了芹缔,心里一萬句MMP飄過之后,終于接受了自己剛才發(fā)出去的一些請求瓶盛,收不到響應(yīng)了的事實(shí)最欠。
如果要是客戶端咔嚓一下關(guān)機(jī)呢?
所以我們可以看到惩猫,不論是客戶端突然關(guān)機(jī)還是服務(wù)端突然關(guān)機(jī)芝硬,都會(huì)造成問題。
于是我們的優(yōu)雅關(guān)機(jī)轧房,就是要解決這么一個(gè)問題拌阴。從前面的那些圖可以看到,如果要優(yōu)雅退出锯厢,關(guān)鍵在于好好商量:
如果客戶端關(guān)機(jī)呢皮官?那就更加簡單了脯倒,稍微停一下实辑,把發(fā)出去的請求的響應(yīng)收完再關(guān)機(jī)。
現(xiàn)實(shí)的情況是藻丢,一個(gè)節(jié)點(diǎn)剪撬,往往既是服務(wù)端,也是客戶端悠反。這種情況下該怎么搞残黑?首先在發(fā)出關(guān)機(jī)的信號(hào)后馍佑,它肯定不能關(guān)掉,至少要等到已接受的請求處理完成梨水,才能關(guān)掉拭荤。往往是,處理一個(gè)請求會(huì)導(dǎo)致它作為客戶端發(fā)起一個(gè)調(diào)用疫诽。于是我們可以看到舅世,在該節(jié)點(diǎn)既是服務(wù)端,又是客戶端的情況下奇徒,要先關(guān)閉作為服務(wù)端的功能雏亚,這樣才能防止因?yàn)橐幚硇碌恼埱蠖坏貌蛔鳛榭蛻舳讼騽e的服務(wù)器發(fā)起請求。
所以最終步驟就是:
- 告知注冊中心摩钙,即將關(guān)閉罢低,此時(shí)等待并處理請求;
- 注冊中心通知?jiǎng)e的客戶端胖笛,別的客戶端停止發(fā)送新請求网持,等待已發(fā)請求的響應(yīng);
- 節(jié)點(diǎn)處理完所有接收到的請求并且返回響應(yīng)后匀钧,釋放作為服務(wù)端相關(guān)的組件和資源翎碑;
- 節(jié)點(diǎn)釋放作為客戶端的組件和資源;
實(shí)現(xiàn)
如何知道關(guān)機(jī)之斯?
不管我們?nèi)绾螌?shí)現(xiàn)優(yōu)雅關(guān)機(jī)日杈,第一個(gè)要解決的就是,我怎么知道這個(gè)節(jié)點(diǎn)要關(guān)機(jī)了佑刷?在Java虛擬機(jī)里面莉擒,有Runtime
提供了addShutdownHook
的方法:
golang就沒這個(gè)便利。好在golang
提供了信號(hào)(Signal)機(jī)制瘫絮。在golang里有一個(gè)os/signal
的包涨冀,它是一個(gè)對操作系統(tǒng)信號(hào)的封裝——所以這是一個(gè)操作系統(tǒng)相關(guān)的東西,不過我這里只考慮Unix-Like
系統(tǒng)麦萤,畢竟我還是不怎么聽說有人在Windows
上部署golang
微服務(wù)的[手動(dòng)狗頭]鹿鳖。
golang
的文檔(https://golang.org/pkg/os/signal/)里面有很詳細(xì)的描述。我大概總結(jié)一下:
-
SIGKILL
和SIGSTOP
可能捕捉不到壮莹; -
SIGHUP
翅帜,SIGINT
和SIGTERM
會(huì)導(dǎo)致系統(tǒng)退出; -
SIGQUIT
命满,SIGILL
涝滴,SIGTRAP
,SIGABRT
,SIGSTKFLT
歼疮,SIGEMT
杂抽,SIGSYS
會(huì)導(dǎo)致系統(tǒng)退出,并且打印此時(shí)的棧韩脏;
所以我們只需要監(jiān)聽這些信號(hào)的處理就可以了缩麸。
釋放資源步驟
前面我們討論了關(guān)機(jī)釋放資源所需要按序執(zhí)行的步驟,那么落地到dubbogo
里面該如何實(shí)現(xiàn)呢赡矢?
從dubbogo
的源碼能夠發(fā)現(xiàn)匙睹,關(guān)鍵的組件就是Registry
和Protocol
。
其中Protocol
從邏輯上來說济竹,可以分成供Provider
使用的Protocol
和供Consumer
使用的Protocol
痕檬。當(dāng)然,Protocol
也可能同時(shí)提供兩者使用送浊。因此我們考慮到這種情況梦谜,在銷毀Provider
的Protocol
的時(shí)候,要把共用的那些Protocol
剔除出來袭景。
按照我們的預(yù)先分析的步驟唁桩,釋放資源的步驟應(yīng)該是:
- 銷毀所有的
Registry
實(shí)例,這也就是從注冊中心里面注銷耸棒。這個(gè)過程荒澡,客戶端因?yàn)橛斜O(jiān)聽注冊中心的事件,所以很快就能知道某個(gè)服務(wù)器已經(jīng)不可用与殃;
- 在步驟1之后单山,理論上來說所有的客戶端都不會(huì)再發(fā)請求過來了。但是還有很多時(shí)候幅疼,一個(gè)是注冊中心通知客戶端的延時(shí)米奸,二是不同的客戶端可能有一些奇怪的緩存機(jī)制,再一個(gè)就是此時(shí)正在發(fā)送的請求爽篷。這幾種情況下悴晰,還會(huì)有部分請求到達(dá)服務(wù)器,所以服務(wù)器還需要接收這部分請求然后處理掉逐工,因而要等待一段時(shí)間铡溪;
- 在步驟2之后,絕大部分情況下泪喊,服務(wù)端就可以直接銷毀掉扮演
Provider
的Protocol
了棕硫。然而,如果步驟2等待時(shí)間過短窘俺,或者說客戶端和注冊中心就服務(wù)器下線這個(gè)事情達(dá)成一致的時(shí)間太長饲帅,那么這個(gè)階段還會(huì)收到請求。這個(gè)時(shí)候我們就只能拒絕請求了瘤泪。此時(shí)灶泵,我們還要判斷一下,當(dāng)前正在處理的請求處理完了沒有对途,如果處理完了赦邻,或者等了一段時(shí)間之后都還沒處理完,就進(jìn)入下一個(gè)階段实檀;
在這個(gè)步驟惶洲,服務(wù)器才真的摧毀作為
Provider
的Protocol
。經(jīng)過步驟4膳犹,服務(wù)器還可能處在一種“雖然我無法響應(yīng)別人恬吕,但是我還在處理點(diǎn)事情,我要等別人的響應(yīng)”的狀態(tài)中须床,所以這個(gè)時(shí)候我們再稍微停下來等一下铐料,如果所有的響應(yīng)都收到請求了,或者超時(shí)豺旬,進(jìn)入下一個(gè)階段钠惩;
- 摧毀掉剩下的
Protocol
。 - 理論上來說族阅,經(jīng)過步驟6篓跛,在框架層面上,所有的資源都釋放了坦刀。但是這個(gè)時(shí)候我們要考慮到開發(fā)者可能在此時(shí)需要釋放他創(chuàng)建的資源愧沟,因此我們要提供一個(gè)回調(diào)機(jī)制,允許他們在這個(gè)時(shí)間節(jié)點(diǎn)回收資源鲤遥;
我們的源碼里面很容易看出來這些步驟:
如何確定每一步的超時(shí)時(shí)間
在實(shí)現(xiàn)這個(gè)優(yōu)雅退出的時(shí)候央渣,有一個(gè)參數(shù)非常關(guān)鍵,就是每一步的退出時(shí)間step_timeout
渴频,它代表的是芽丹,在前面提及的每一個(gè)步驟,如果需要停下來等待卜朗,那么會(huì)在多久以后超時(shí)拔第,結(jié)束等待。
在大大大大大多數(shù)情況下场钉,設(shè)置這個(gè)時(shí)間只需要考慮第一個(gè)停下來等待的步驟蚊俺,即服務(wù)端在宣稱了自己要停機(jī),并且銷毀了Registry
之后停下來等待新請求的時(shí)長逛万。也就是執(zhí)行方法waitAndAcceptNewRequests
的超時(shí)時(shí)間泳猬。
有一個(gè)簡單的式子可以描述這個(gè)時(shí)間:客戶端收到注冊中心通知的時(shí)長+請求響應(yīng)時(shí)長。
第一個(gè)“客戶端收到注冊中心通知的時(shí)長”很好理解,但是也比較難估算得封。這主要取決于注冊中心和客戶端緩存機(jī)制埋心。我個(gè)人經(jīng)驗(yàn)是使用ZK
的情況下,一般不會(huì)超過1秒忙上。
第二個(gè)“請求響應(yīng)時(shí)長”最復(fù)雜了拷呆。首先,這是一個(gè)從客戶端觀察的值疫粥。也就是說茬斧,它不是我們監(jiān)控到的服務(wù)端的服務(wù)響應(yīng)時(shí)間,而是從客戶端發(fā)出一個(gè)請求到它收到完全響應(yīng)的時(shí)長梗逮。于服務(wù)端而言项秉,大概是“請求傳輸時(shí)長+服務(wù)響應(yīng)時(shí)長+響應(yīng)傳輸時(shí)長”。
然后我們又會(huì)面臨一個(gè)問題慷彤,一個(gè)服務(wù)端往往提供多個(gè)服務(wù)伙狐,我該取哪個(gè)服務(wù)的請求響應(yīng)時(shí)長?答案是取決于你具體的業(yè)務(wù)和你的期望瞬欧。開發(fā)者可以基于自己的服務(wù)的重要性贷屎,取比較重要的服務(wù)的999線;又或者全部服務(wù)一起考慮艘虎,取999線唉侄。這里我比較不建議使用平均線,因?yàn)槠骄€意味著有很多的請求無法再這個(gè)時(shí)間內(nèi)返回響應(yīng)野建。
另外一種比較罕見的選擇是属划,使用定時(shí)任務(wù)的執(zhí)行時(shí)間,或者事務(wù)——尤其是分布式事務(wù)——的完成時(shí)間候生。
核心就是同眯,你覺得哪個(gè)東西最重要,你就用那個(gè)東西的執(zhí)行時(shí)間唯鸭。
上面的邏輯也適用于單純是Consumer
的應(yīng)用须蜗。
大多數(shù)情況下,step_timeout
默認(rèn)值10秒足以應(yīng)付了目溉。
未實(shí)現(xiàn)部分
特殊回調(diào)
這個(gè)小標(biāo)題有點(diǎn)不太準(zhǔn)確明肮。大家注意到的是,我只在所有框架資源都被銷毀之后才會(huì)回調(diào)開發(fā)者注冊的回調(diào)缭付。這個(gè)時(shí)候就有這么一些問題:
- 如果開發(fā)者在自定義的回調(diào)里面希望用到
dubbogo
的功能柿估,特別是發(fā)起遠(yuǎn)程調(diào)用,那么顯然是不可能的——雖然我也覺得不會(huì)有人會(huì)這么干陷猫; - 如果開發(fā)者的回調(diào)希望按照順序來執(zhí)行秫舌,那么也是不支持的的妖。我們只會(huì)按照注冊回調(diào)的順序來依次調(diào)用。當(dāng)然開發(fā)者可以通過將多個(gè)回調(diào)按序調(diào)用組成一個(gè)更復(fù)雜的回調(diào)來實(shí)現(xiàn)這個(gè)目標(biāo)足陨。不支持它主要是一個(gè)取舍問題嫂粟。我相信有這種需求的人是少數(shù)以至于幾乎沒有的……
底層支持的優(yōu)雅停機(jī)
前面所有的步驟,都是直接建立在應(yīng)用層面上钠右。實(shí)際上,還有一些業(yè)界的做法忘蟹,是在底層協(xié)議上就直接提供了支持飒房。比如說,通過TCP
連接發(fā)送一個(gè)只讀事件媚值,那么客戶端后續(xù)就自然不會(huì)再把請求發(fā)過來狠毯。
我們的優(yōu)雅停機(jī)并沒有使用到這一種機(jī)制,因?yàn)樵趹?yīng)用層面上就能夠解決褥芒。dubbogo
里面的Registry
和Protocol
的Destroy
都沒采用這種機(jī)制嚼松。
但是這的確是一個(gè)很不錯(cuò)的實(shí)現(xiàn)思路。dubbo
就是采用了這種方式锰扶。