你所知道的服務(wù)發(fā)現(xiàn)
服務(wù)注冊(cè)和發(fā)現(xiàn)不是一個(gè)新的概念利耍,你在之前的實(shí)際項(xiàng)目中也一定了解過(guò)蚌本,只是你可能沒怎么注意罷了盔粹。比如說(shuō),你知道 Nginx 是一個(gè)反向代理組件程癌,那么 Nginx 需要知道應(yīng)用服務(wù)器的地址是什么舷嗡,這樣才能夠?qū)⒘髁客競(jìng)鞯綉?yīng)用服務(wù)器上,這就是服務(wù)發(fā)現(xiàn)的過(guò)程嵌莉。
那么 Nginx 是怎么實(shí)現(xiàn)的呢进萄?它是把應(yīng)用服務(wù)器的地址配置在了文件中。
這固然是一種解決的思路锐峭,實(shí)際上中鼠,我在早期的項(xiàng)目中也是這么做的。那時(shí)沿癞,項(xiàng)目剛剛做了服務(wù)化拆分援雇,RPC 服務(wù)端的地址就是配置在了客戶端的代碼中,不過(guò)椎扬,這樣做之后出現(xiàn)了幾個(gè)問題:
- 首先在緊急擴(kuò)容的時(shí)候惫搏,就需要修改客戶端配置后,重啟所有的客戶端進(jìn)程盗舰,操作時(shí)間比較長(zhǎng)晶府;
- 其次,一旦某一個(gè)服務(wù)器出現(xiàn)故障時(shí)钻趋,也需要修改所有客戶端配置后重啟川陆,無(wú)法快速修復(fù),更無(wú)法做到自動(dòng)恢復(fù)蛮位;
- 最后较沪,RPC 服務(wù)端上線無(wú)法做到提前摘除流量,這樣在重啟服務(wù)端的時(shí)候失仁,客戶端發(fā)往被重啟服務(wù)端的請(qǐng)求還沒有返回尸曼,會(huì)造成慢請(qǐng)求甚至請(qǐng)求失敗。
因此萄焦,我們考慮使用注冊(cè)中心來(lái)解決這些問題控轿。
目前業(yè)界有很多可供你來(lái)選擇的注冊(cè)中心組件,比如說(shuō)老派的 ZooKeeper拂封、Kubernetes 使用的 ETCD茬射、阿里的微服務(wù)注冊(cè)中心 Nacos、Spring Cloud 的 Eureka 等等冒签。
這些注冊(cè)中心的基本功能有兩點(diǎn):
其一是提供了服務(wù)地址的存儲(chǔ)在抛;
其二是當(dāng)存儲(chǔ)內(nèi)容發(fā)生變化時(shí),可以將變更的內(nèi)容推送給客戶端萧恕。
第二個(gè)功能是我們使用注冊(cè)中心的主要原因刚梭。因?yàn)闊o(wú)論是當(dāng)我們需要緊急擴(kuò)容肠阱,還是在服務(wù)器發(fā)生故障時(shí)需要快速摘除節(jié)點(diǎn),都不用重啟服務(wù)器就可以實(shí)現(xiàn)了朴读。使用了注冊(cè)中心組件之后屹徘,RPC 的通信過(guò)程就變成了下面這個(gè)樣子:
從圖中你可以看到一個(gè)完整的服務(wù)注冊(cè)和發(fā)現(xiàn)的過(guò)程:
- 客戶端會(huì)與注冊(cè)中心建立連接,并且告訴注冊(cè)中心磨德,它對(duì)哪一組服務(wù)感興趣缘回;
- 服務(wù)端向注冊(cè)中心注冊(cè)服務(wù)后,注冊(cè)中心會(huì)將最新的服務(wù)注冊(cè)信息通知給客戶端典挑;
- 客戶端拿到服務(wù)端的地址之后就可以向服務(wù)端發(fā)起調(diào)用請(qǐng)求了酥宴。
從這個(gè)過(guò)程中可以看出,有了注冊(cè)中心之后您觉,服務(wù)節(jié)點(diǎn)的增加和減少對(duì)于客戶端就是透明的拙寡。這樣除了可以實(shí)現(xiàn)不重啟客戶端就能動(dòng)態(tài)地變更服務(wù)節(jié)點(diǎn)以外,還可以實(shí)現(xiàn)優(yōu)雅關(guān)閉的功能琳水。
優(yōu)雅關(guān)閉是你在系統(tǒng)研發(fā)過(guò)程中必須要考慮的問題肆糕。因?yàn)槿绻┝Φ赝V狗?wù),那么已經(jīng)發(fā)送給服務(wù)端的請(qǐng)求在孝,來(lái)不及處理服務(wù)就被刪掉了诚啃,就會(huì)造成這部分請(qǐng)求失敗,服務(wù)就會(huì)有波動(dòng)私沮。所以服務(wù)在退出的時(shí)候始赎,都需要先停掉流量再停止服務(wù),這樣服務(wù)的關(guān)閉才會(huì)更平滑仔燕。比如造垛,消息隊(duì)列處理器就是要將所有已經(jīng)從消息隊(duì)列中讀出的消息,處理完之后才能退出晰搀。
對(duì)于 RPC 服務(wù)來(lái)說(shuō)五辽,我們可以先將 RPC 服務(wù)從注冊(cè)中心的服務(wù)列表中刪除掉,然后觀察 RPC 服務(wù)端沒有流量之后外恕,再將服務(wù)端停掉杆逗。有了優(yōu)雅關(guān)閉之后,RPC 服務(wù)端再重啟的時(shí)候鳞疲,就會(huì)減少對(duì)客戶端的影響罪郊。
在這個(gè)過(guò)程中,服務(wù)的上線和下線是由服務(wù)端主動(dòng)向注冊(cè)中心注冊(cè)和取消注冊(cè)來(lái)實(shí)現(xiàn)的建丧,這在正常的流程中是沒有問題的排龄〔ㄊ疲可是翎朱,如果某一個(gè)服務(wù)端意外故障橄维,比如說(shuō)機(jī)器掉電,網(wǎng)絡(luò)不通等情況拴曲,服務(wù)端就沒有辦法向注冊(cè)中心通信争舞,將自己從服務(wù)列表中刪除,那么客戶端也就不會(huì)得到通知澈灼,它就會(huì)繼續(xù)向一個(gè)故障的服務(wù)端發(fā)起請(qǐng)求竞川,也就會(huì)有錯(cuò)誤發(fā)生了。那這種情況如何來(lái)避免呢叁熔?其實(shí)委乌,這種情況是一個(gè)服務(wù)狀態(tài)管理的問題。
服務(wù)狀態(tài)管理如何來(lái)做
針對(duì)上面我提到的問題荣回,我們一般會(huì)有兩種解決思路遭贸。
第一種思路是主動(dòng)探測(cè),方法是這樣的:
你的 RPC 服務(wù)要打開一個(gè)端口心软,然后由注冊(cè)中心每隔一段時(shí)間(比如 30 秒)探測(cè)這些端口是否可用壕吹,如果可用就認(rèn)為服務(wù)仍然是正常的,否則就可以認(rèn)為服務(wù)不可用删铃,那么注冊(cè)中心就可以把服務(wù)從列表里面刪除了耳贬。
微博早期的注冊(cè)中心就是采用這種方式,但是后面出現(xiàn)的兩個(gè)問題猎唁,讓我們不得不對(duì)它做改造咒劲。
第一個(gè)問題是:所有的 RPC 服務(wù)端都需要開放一個(gè)統(tǒng)一的端口給注冊(cè)中心探測(cè),那時(shí)候還沒有容器化胖秒,一臺(tái)物理機(jī)上會(huì)混合部署很多的服務(wù)缎患,你需要開放的端口很可能已經(jīng)被占用,這樣會(huì)造成 RPC 服務(wù)啟動(dòng)失敗阎肝。
還有一個(gè)問題是:如果 RPC 服務(wù)端部署的實(shí)例比較多挤渔,那么每次探測(cè)的成本也會(huì)比較高,探測(cè)的時(shí)間也比較長(zhǎng)风题,這樣當(dāng)一個(gè)服務(wù)不可用時(shí)判导,可能會(huì)有一段時(shí)間的延遲,才會(huì)被注冊(cè)中心探測(cè)到沛硅。
因此眼刃,我們后面把它改造成了心跳模式。
這也是大部分注冊(cè)中心提供的檢測(cè)連接上來(lái)的 RPC 服務(wù)端是否存活的方式摇肌,比如 Eureka擂红、ZooKeeper,在我來(lái)看围小,這種心跳機(jī)制可以這樣實(shí)現(xiàn):
注冊(cè)中心為每一個(gè)連接上來(lái)的 RPC 服務(wù)節(jié)點(diǎn)記錄最近續(xù)約的時(shí)間昵骤,RPC 服務(wù)節(jié)點(diǎn)在啟動(dòng)注冊(cè)到注冊(cè)中心后树碱,就按照一定的時(shí)間間隔(比如 30 秒),向注冊(cè)中心發(fā)送心跳包变秦。注冊(cè)中心在接收到心跳包之后成榜,會(huì)更新這個(gè)節(jié)點(diǎn)的最近續(xù)約時(shí)間。然后蹦玫,注冊(cè)中心會(huì)啟動(dòng)一個(gè)定時(shí)器定期檢測(cè)當(dāng)前時(shí)間和節(jié)點(diǎn)最近續(xù)約時(shí)間的差值赎婚,如果達(dá)到一個(gè)閾值(比如說(shuō) 90 秒),那么認(rèn)為這個(gè)服務(wù)節(jié)點(diǎn)不可用樱溉。
在實(shí)際的使用中挣输,心跳機(jī)制相比主動(dòng)探測(cè)的機(jī)制,適用范圍更廣福贞,如果你的服務(wù)也需要檢測(cè)是否存活歧焦,那么也可以考慮使用心跳機(jī)制來(lái)檢測(cè)。
接著說(shuō)回來(lái)肚医,有了心跳機(jī)制之后绢馍,注冊(cè)中心就可以管理注冊(cè)的服務(wù)節(jié)點(diǎn)的狀態(tài)了,也讓你的注冊(cè)中心成為了整體服務(wù)最重要的組件肠套,因?yàn)橐坏┧霈F(xiàn)問題或者代碼出現(xiàn) Bug舰涌,那么很可能會(huì)導(dǎo)致整個(gè)集群的故障,給你舉一個(gè)真實(shí)的案例你稚。
在我之前的一個(gè)項(xiàng)目中瓷耙,工程是以“混合云”的方式部署的,也就是一部分節(jié)點(diǎn)部署在自建機(jī)房中刁赖,一部分節(jié)點(diǎn)部署在云服務(wù)器上搁痛,每一個(gè)機(jī)房都部署了自研的一套注冊(cè)中心,每套注冊(cè)中心中都保存了全部節(jié)點(diǎn)的數(shù)據(jù)宇弛。
這套自研的注冊(cè)中心使用 Redis 作為最終的存儲(chǔ)鸡典,而在自建機(jī)房和云服務(wù)器上的注冊(cè)中心,共用同一套 Redis 存儲(chǔ)資源枪芒。由于“混合云”還處在測(cè)試階段彻况,所以,所有的流量還都在自建機(jī)房舅踪,自建機(jī)房和云服務(wù)器之前的專線帶寬還比較小纽甘,部署結(jié)構(gòu)如下:
在測(cè)試的過(guò)程中系統(tǒng)運(yùn)行穩(wěn)定,但是某一天早上五點(diǎn)抽碌,我突然發(fā)現(xiàn)悍赢,所有的服務(wù)節(jié)點(diǎn)都被摘除了,客戶端因?yàn)槟貌坏椒?wù)端的節(jié)點(diǎn)地址列表全部調(diào)用失敗,整體服務(wù)宕機(jī)左权。經(jīng)過(guò)排查我發(fā)現(xiàn)瞒斩,云服務(wù)器上部署的注冊(cè)中心竟然將所有的服務(wù)節(jié)點(diǎn)全部刪除了!進(jìn)一步排查之后涮总,原來(lái)是自研注冊(cè)中心出現(xiàn)了 Bug。
在正常的情況下祷舀,無(wú)論是自建機(jī)房瀑梗,還是云服務(wù)器上的服務(wù)節(jié)點(diǎn),都會(huì)向各自機(jī)房的注冊(cè)中心注冊(cè)地址信息裳扯,并且發(fā)送心跳抛丽。而這些地址信息以及服務(wù)的最近續(xù)約時(shí)間,都是存儲(chǔ)在 Redis 主庫(kù)中饰豺,各自機(jī)房的注冊(cè)中心亿鲜,會(huì)讀各自機(jī)房的從庫(kù)來(lái)獲取最近續(xù)約時(shí)間,從而判斷服務(wù)節(jié)點(diǎn)是否有效冤吨。
Redis 的主從同步數(shù)據(jù)是通過(guò)專線來(lái)傳輸?shù)妮锪霈F(xiàn)故障之前,專線帶寬被占滿漩蟆,導(dǎo)致主從同步延遲垒探。這樣一來(lái),云上部署的 Redis 從庫(kù)中存儲(chǔ)的最近續(xù)約時(shí)間就沒有得到及時(shí)更新怠李,隨著主從同步延遲越發(fā)嚴(yán)重圾叼,最終云上部署的注冊(cè)中心發(fā)現(xiàn)了當(dāng)前時(shí)間與最近續(xù)約時(shí)間的差值超過(guò)了摘除的閾值,所以將所有的節(jié)點(diǎn)摘除捺癞,從而導(dǎo)致了故障夷蚊。
有了這次慘痛的教訓(xùn),我們給注冊(cè)中心增加了保護(hù)的策略:如果摘除的節(jié)點(diǎn)占到了服務(wù)集群節(jié)點(diǎn)數(shù)的 40%髓介,就停止摘除服務(wù)節(jié)點(diǎn)惕鼓,并且給服務(wù)的開發(fā)同學(xué)和運(yùn)維同學(xué)報(bào)警處理(這個(gè)閾值百分比可以調(diào)整,保證了一定的靈活性)唐础。
據(jù)我所知呜笑,Eureka 也采用了類似的策略,來(lái)避免服務(wù)節(jié)點(diǎn)被過(guò)度摘除彻犁,導(dǎo)致服務(wù)集群不足以承擔(dān)流量的問題叫胁。如果你使用的是 ZooKeeper 或者 ETCD 這種無(wú)保護(hù)策略的分布式一致性組件,那你可以考慮在客戶端汞幢,實(shí)現(xiàn)保護(hù)策略的邏輯驼鹅,比如說(shuō)當(dāng)摘除的節(jié)點(diǎn)超過(guò)一定比例時(shí),你在 RPC 客戶端就不再處理變更通知,你可以依據(jù)自己的實(shí)際情況來(lái)實(shí)現(xiàn)输钩。
除此之外豺型,在實(shí)際項(xiàng)目中,我們還發(fā)現(xiàn)注冊(cè)中心另一個(gè)重要的問題就是“通知風(fēng)暴”买乃。你想一想姻氨,變更一個(gè)服務(wù)的一個(gè)節(jié)點(diǎn),會(huì)產(chǎn)生多少條推送消息剪验?假如你的服務(wù)有 100 個(gè)調(diào)用者肴焊,有 100 個(gè)節(jié)點(diǎn),那么變更一個(gè)節(jié)點(diǎn)會(huì)推送 100 * 100 = 10000 個(gè)節(jié)點(diǎn)的數(shù)據(jù)功戚。那么如果多個(gè)服務(wù)集群同時(shí)上線或者發(fā)生波動(dòng)時(shí)娶眷,注冊(cè)中心推送的消息就會(huì)更多,會(huì)嚴(yán)重占用機(jī)器的帶寬資源啸臀,這就是我所說(shuō)的“通知風(fēng)暴”届宠。那么怎么解決這個(gè)問題呢?你可以從以下幾個(gè)方面來(lái)思考:
- 首先乘粒,要控制一組注冊(cè)中心管理的服務(wù)集群的規(guī)模豌注,具體限制多少?zèng)]有統(tǒng)一的標(biāo)準(zhǔn),你需要結(jié)合你的業(yè)務(wù)以及注冊(cè)中心的選型來(lái)考慮灯萍,主要考察的指標(biāo)就是注冊(cè)中心服務(wù)器的峰值帶寬幌羞;
- 其次,你也可以通過(guò)擴(kuò)容注冊(cè)中心節(jié)點(diǎn)的方式來(lái)解決竟稳;
- 再次属桦,你可以規(guī)范一下對(duì)于注冊(cè)中心的使用方式,如果只是變更某一個(gè)節(jié)點(diǎn)他爸,那么只需要通知這個(gè)節(jié)點(diǎn)的變更信息即可聂宾;
- 最后,如果是自建的注冊(cè)中心诊笤,你也可以在其中加入一些保護(hù)策略系谐,比如說(shuō)如果通知的消息量達(dá)到某一個(gè)閾值就停止變更通知。
其實(shí)讨跟,服務(wù)的注冊(cè)和發(fā)現(xiàn)歸根結(jié)底是服務(wù)治理中的一環(huán)纪他,服務(wù)治理(service governance),其實(shí)更直白的翻譯應(yīng)該是服務(wù)的管理晾匠,也就是解決多個(gè)服務(wù)節(jié)點(diǎn)組成集群的時(shí)候產(chǎn)生的一些復(fù)雜的問題茶袒。為了幫助你理解,我來(lái)做個(gè)簡(jiǎn)單的比喻凉馆。
你可以把集群看作是一個(gè)微型的城市薪寓,把道路看作是組成集群的服務(wù)亡资,把行走在道路上的車看作是流量,那么服務(wù)治理就是對(duì)于整個(gè)城市道路的管理向叉。
如果你新建了一條街道(相當(dāng)于啟動(dòng)了一個(gè)新的服務(wù)節(jié)點(diǎn))锥腻,那么就要通知所有的車輛(流量)有新的道路可以走了;你關(guān)閉了一條街道母谎,你也要通知所有車輛不要從這條路走了瘦黑,這就是服務(wù)的注冊(cè)和發(fā)現(xiàn)。
我們?cè)诘缆飞习惭b監(jiān)控奇唤,監(jiān)視每條道路的流量情況幸斥,這就是服務(wù)的監(jiān)控。
道路一旦出現(xiàn)擁堵或者道路需要維修冻记,那么就需要暫時(shí)封閉這條道路,由城市來(lái)統(tǒng)一調(diào)度車輛来惧,走不堵的道路冗栗,這就是熔斷以及引流。
道路之間縱橫交錯(cuò)四通八達(dá)供搀,一旦在某條道路上出現(xiàn)擁堵隅居,但是又發(fā)現(xiàn)這條道路從頭堵到尾,說(shuō)明事故并不是發(fā)生在這條道路上葛虐,那么就需要從整體鏈路上來(lái)排查事故究竟處在哪個(gè)位置胎源,這就是分布式追蹤。
不同道路上的車輛有多有少屿脐,那么就需要有一個(gè)警察來(lái)疏導(dǎo)涕蚤,在某一個(gè)時(shí)間走哪一條路會(huì)比較快,這就是負(fù)載均衡的诵。