手繪了11張圖,幫你看明白 Zookeeper 如何實(shí)現(xiàn)服務(wù)注冊(cè)發(fā)現(xiàn)
對(duì)微服務(wù)稍有了解的小伙伴應(yīng)該都聽(tīng)說(shuō)過(guò) Zookeeper,我們來(lái)看看在官網(wǎng)上是如何介紹的:
Zookeeper 是一個(gè)分布式的、開(kāi)源的分布式應(yīng)用程序協(xié)調(diào)服務(wù)。
作為一個(gè)協(xié)調(diào)服務(wù),常常用來(lái)配合其他中間件來(lái)用,比如:Dubbo + Zookeeper,Hadoop + Zookeeper等,Zookeeper可以實(shí)現(xiàn):服務(wù)注冊(cè)發(fā)現(xiàn)、分布式鎖、配置中心等功能。
今天我們重點(diǎn)來(lái)學(xué)習(xí)一下 Zookeeper 是如何實(shí)現(xiàn)服務(wù)注冊(cè)發(fā)現(xiàn)的。
分布式帶來(lái)的問(wèn)題
先正式介紹 Zookeeper 之前,我們先引入一個(gè)業(yè)務(wù)場(chǎng)景:訂單服務(wù)需要調(diào)用用戶服務(wù)的接口。
要實(shí)現(xiàn)這個(gè)功能非常簡(jiǎn)單,我們只需要知道用戶服務(wù)的 ip 和 port 就可以了。
突然有一天,用戶數(shù)量激增,用戶服務(wù)扛不住了,這個(gè)時(shí)候只能進(jìn)行擴(kuò)容,多部署幾個(gè)實(shí)例,這個(gè)時(shí)候問(wèn)題就來(lái)了,訂單服務(wù)該調(diào)用哪個(gè)用戶服務(wù)的實(shí)例?
最簡(jiǎn)單的辦法就是在訂單服務(wù)中配置所有的用戶服務(wù)實(shí)例,然后使用某種算法(比如說(shuō)輪詢)從配置列表中選擇一個(gè)就可以了。
看似問(wèn)題解決了,其實(shí)隱患很大:
- 用戶服務(wù)的實(shí)例數(shù)會(huì)根據(jù)負(fù)載進(jìn)行動(dòng)態(tài)調(diào)整,每次調(diào)整完都要更新配置列表,非常麻煩,也容易出錯(cuò)。
- 某些服務(wù)實(shí)例 down 掉了,如果沒(méi)來(lái)得及從配置列表中清除掉,就會(huì)造成調(diào)用者請(qǐng)求接口報(bào)錯(cuò)。
如何解決呢?往往解決這類(lèi)分布式問(wèn)題都需要一塊公共的區(qū)域來(lái)保存這些信息。
用 Redis 解決
需要一塊公共的區(qū)域保存這些信息,那利用 Redis 是否可以實(shí)現(xiàn)?
每個(gè)服務(wù)實(shí)例啟動(dòng)之后都向 Redis 注冊(cè)信息,停止時(shí)也從 Redis 中刪除數(shù)據(jù)。
存放在 Redis 中的信息簡(jiǎn)單來(lái)說(shuō)就是服務(wù)實(shí)例的 ip + port,訂單服務(wù)需要調(diào)用用戶服務(wù)時(shí)直接從 Redis 中獲取即可。
簡(jiǎn)單流程如下圖所示:
每次調(diào)用的時(shí)候都去 Redis 查詢一次,頻繁的查詢可能會(huì)導(dǎo)致性能瓶頸,為了解決這個(gè)問(wèn)題我們可以在查詢之后在本地緩存一份數(shù)據(jù),這樣每次調(diào)用可以優(yōu)先從本地獲取數(shù)據(jù)。
但這樣又會(huì)出現(xiàn)新的問(wèn)題,本地緩存如何刷新呢,如果服務(wù)提供者某些實(shí)例 down 掉了或者擴(kuò)容新增了一批實(shí)例,那服務(wù)消費(fèi)者如何才能快速感知到呢?
要想解決這個(gè)問(wèn)題,最先想到的一個(gè)辦法就是讓服務(wù)消費(fèi)者定時(shí)輪詢 Redis,發(fā)現(xiàn)有更新了就去更新本地緩存,看起來(lái)也能解決本地緩存刷新的問(wèn)題,但是多久輪詢一次呢,1 秒或者10 秒?
輪詢時(shí)間太短依然有性能瓶頸問(wèn)題,這樣本地緩存也沒(méi)有存在的必要了;輪詢時(shí)間太長(zhǎng),本地緩存來(lái)不及更新,就會(huì)存在 "臟" 數(shù)據(jù)。
以上的方案都不完美,并且不優(yōu)雅,主要有以下幾點(diǎn):
基于定時(shí)任務(wù)會(huì)導(dǎo)致很多無(wú)效的查詢。
定時(shí)任務(wù)存在周期性,沒(méi)法做到實(shí)時(shí),這樣就可能存在請(qǐng)求異常。
如果服務(wù)被強(qiáng)行 kill,沒(méi)法及時(shí)清除 Redis,這樣這個(gè)看似可用的服務(wù)將永遠(yuǎn)不可用!
所以我們需要一個(gè)更加靠譜的解決方案。
用 Zookeeper 解決
用過(guò) Dubbo 的小伙伴對(duì)這張圖肯定很熟悉,步驟 0 到 4 是服務(wù)注冊(cè)發(fā)現(xiàn)的核心流程。
這個(gè)流程與我們上面討論的不謀而合,那 Dubbo 是如何實(shí)現(xiàn)的呢?實(shí)際上 Dubbo 作為一個(gè)通用的框架提供了多種解決方案,如:Zookeeper、Nacos等。
不管是哪種方案,總結(jié)起來(lái)都是一種套路,基本流程如下:
- 每個(gè)服務(wù)實(shí)例啟動(dòng)之后將自己的信息(ip+port)寫(xiě)入公共區(qū)域;
- 調(diào)用者訂閱自己感興趣的服務(wù)實(shí)例,獲取服務(wù)實(shí)例信息列表后緩存在自己本地;
- 服務(wù)實(shí)例停止或者 down 調(diào)后將公共區(qū)域自己的信息清除掉;
- 公共區(qū)域通知調(diào)用者你感興趣的信息已經(jīng)發(fā)生變更,請(qǐng)更新一下本地的緩存。
Zookeeper的重點(diǎn)特性
(1)樹(shù)狀目錄結(jié)構(gòu)
Zookeeper是一個(gè)樹(shù)狀的文件目錄結(jié)構(gòu),與 Unix 文件系統(tǒng)很類(lèi)似。樹(shù)中每個(gè)節(jié)點(diǎn)可以稱(chēng)作為一個(gè)ZNode,每一個(gè)ZNode都可以通過(guò)其路徑唯一標(biāo)識(shí),最重要的是我們可以對(duì)每個(gè) ZNode 進(jìn)行增刪改查。
(2)持久節(jié)點(diǎn)(Persistent)
客戶端與Zookeeper服務(wù)端斷開(kāi)連接后,節(jié)點(diǎn)仍然存在不會(huì)被刪除,這樣的節(jié)點(diǎn)就叫做持久節(jié)點(diǎn)。
(3)持久有序節(jié)點(diǎn)(Persistent_sequential)
持久有序節(jié)點(diǎn)是在上面持久節(jié)點(diǎn)的特性上加上了有序性,有序性的意思是服務(wù)向Zookeeper注冊(cè)信息時(shí),Zookeeper 根據(jù)注冊(cè)順序給每個(gè)節(jié)點(diǎn)編號(hào)。
(4)臨時(shí)節(jié)點(diǎn)(Ephemeral)
客戶端與Zookeeper服務(wù)端斷開(kāi)連接后,該節(jié)點(diǎn)被刪除。
注意:臨時(shí)節(jié)點(diǎn)下不存在子節(jié)點(diǎn);持久節(jié)點(diǎn)下可以存在臨時(shí)節(jié)點(diǎn)。
(5)臨時(shí)有序節(jié)點(diǎn)(Ephemeral_sequential)
臨時(shí)有序節(jié)點(diǎn)是在臨時(shí)節(jié)點(diǎn)的基礎(chǔ)上再加上有序性,跟持久有序節(jié)點(diǎn)類(lèi)似。
(6)節(jié)點(diǎn)監(jiān)聽(tīng)(Wacher)
節(jié)點(diǎn)監(jiān)聽(tīng)是Zookeeper最重要的特性之一,客戶端可以監(jiān)聽(tīng)任意節(jié)點(diǎn),節(jié)點(diǎn)有任何變化 Zookeeper 可以通過(guò)回調(diào)的方式通知給客戶端,這樣客戶端不用輪詢就可以及時(shí)感知節(jié)點(diǎn)變化。
如下圖所示,客戶端(client)開(kāi)始監(jiān)聽(tīng)臨時(shí)節(jié)點(diǎn) 1,因某種原因臨時(shí)節(jié)點(diǎn) 1 被刪除了,Zookeeper 通過(guò)回調(diào)將變化通知給 client 了。
Zookeeper 實(shí)現(xiàn)服務(wù)注冊(cè)發(fā)現(xiàn)
了解了Zookeeper的一些重要特性,我們?cè)賮?lái)看下 Zookeeper 是如何實(shí)現(xiàn)服務(wù)注冊(cè)和發(fā)現(xiàn)的。還是以訂單服務(wù)、用戶服務(wù)的場(chǎng)景為例。
服務(wù)提供方(用戶服務(wù))啟動(dòng)成功后將服務(wù)信息注冊(cè)到Zookeeper,服務(wù)信息包括實(shí)例的 ip、端口等元信息。注冊(cè)成功 Zookeeper 還可以通過(guò)心跳監(jiān)測(cè)來(lái)動(dòng)態(tài)感知實(shí)例變化,詳細(xì)的過(guò)程這里不展開(kāi)。
服務(wù)消費(fèi)方(訂單服務(wù))需要調(diào)用用戶服務(wù)的接口,但因?yàn)椴恢缹?shí)例的 ip、端口等信息,只能從 Zookeeper 中獲取調(diào)用地址列表,然后進(jìn)行調(diào)用,這個(gè)過(guò)程成為服務(wù)的訂閱。
訂閱成功后服務(wù)消費(fèi)方可以將調(diào)用列表緩存在本地,這樣不用每次都去調(diào)用 Zookeeper 獲取。一旦 Zookeeper感知到用戶服務(wù)實(shí)例變化后就會(huì)通知給服務(wù)消費(fèi)方,服務(wù)消費(fèi)方拿到結(jié)果后就會(huì)更新本地緩存,這個(gè)過(guò)程稱(chēng)之為通知。
服務(wù)注冊(cè)原理
服務(wù)啟動(dòng)后會(huì)自動(dòng)向 Zookeeper 注冊(cè),注冊(cè)的規(guī)則如下:
每個(gè)服務(wù)會(huì)創(chuàng)建一個(gè)持久節(jié)點(diǎn)和若干個(gè)臨時(shí)節(jié)點(diǎn)。比如:用戶服務(wù)首先創(chuàng)建一個(gè)持久節(jié)點(diǎn) user,然后每個(gè)服務(wù)實(shí)例會(huì)在持久節(jié)點(diǎn)下創(chuàng)建一個(gè)臨時(shí)有序節(jié)點(diǎn)。
服務(wù)動(dòng)態(tài)發(fā)現(xiàn)原理
由于訂單服務(wù)需要調(diào)用用戶服務(wù)的接口,所以訂單服務(wù)會(huì)訂閱 user 節(jié)點(diǎn),一旦用戶服務(wù)有變化(增加實(shí)例或者減少實(shí)例),Zookeeper 都會(huì)將最新的列表信息推送給訂單服務(wù),這個(gè)過(guò)程就是服務(wù)動(dòng)態(tài)發(fā)現(xiàn)的基本原理。少用文字描述,大家直接看圖:
小結(jié)
文章首先引入訂單服務(wù)和用戶服務(wù)的例子,說(shuō)明了分布式場(chǎng)景下可能存在的問(wèn)題:服務(wù)提供者實(shí)例越多,維護(hù)的成本越高。
經(jīng)過(guò)分析,我們得出結(jié)論:需要使用一塊公共的區(qū)域存儲(chǔ)實(shí)例信息。
如何提供公共的區(qū)域?我們先想到了Redis。
經(jīng)過(guò)實(shí)踐發(fā)現(xiàn) Redis 確實(shí)可以解決服務(wù)注冊(cè)和服務(wù)發(fā)現(xiàn)的問(wèn)題,但是同時(shí)又引入了其他問(wèn)題:
- 基于定時(shí)任務(wù)會(huì)導(dǎo)致很多無(wú)效的查詢。
- 定時(shí)任務(wù)存在周期性,沒(méi)法做到實(shí)時(shí),這樣就可能存在請(qǐng)求異常。
- 如果服務(wù)被強(qiáng)行 kill,沒(méi)法及時(shí)清除 Redis,這樣這個(gè)看似可用的服務(wù)將永遠(yuǎn)不可用!
當(dāng)我們一籌莫展的時(shí)候,我們發(fā)現(xiàn)強(qiáng)大的 Dubbo 框架使用了 Zookeeper 來(lái)實(shí)現(xiàn)服務(wù)注冊(cè)和發(fā)現(xiàn)的功能。為了更好的學(xué)習(xí) Zookeeper 是如何實(shí)現(xiàn)服務(wù)注冊(cè)和發(fā)現(xiàn)功能的,我們了解到 Zookeeper 的一些重要特性:
- 樹(shù)狀目錄結(jié)構(gòu)
- 持久節(jié)點(diǎn)
- 持久有序節(jié)點(diǎn)
- 臨時(shí)節(jié)點(diǎn)
- 臨時(shí)有序節(jié)點(diǎn)
- 節(jié)點(diǎn)監(jiān)控
這些重要的特性為最后實(shí)現(xiàn)服務(wù)注冊(cè)和動(dòng)態(tài)發(fā)現(xiàn)打下了堅(jiān)實(shí)的基礎(chǔ)。
文章的最后,我們?cè)俅我杂唵畏?wù)和用戶服務(wù)為例通過(guò)兩張圖生動(dòng)的詮釋了服務(wù)注冊(cè)和動(dòng)態(tài)發(fā)現(xiàn)的流程和原理。