前言
看了很多zookeeper的文章和視頻,幾乎前篇一律先講一遍基本使用再講使用場景,看完還是云里霧里。
所以本文換一個角度講解zookeeper,從它出現(xiàn)的背景,要解決的問題入手,一步步反推它的功能。用大白話的方式試著從設(shè)計者的角度思考zookeeper為何如此設(shè)計。
注:本文只是幫助理解zookeeper,具體的搭建和操作可以參考官方文檔或百度,從設(shè)計者的角度思考也只是一種假設(shè),只是為了方便理解,實際上zookeeper最初的定位可能壓根沒考慮這么多
協(xié)調(diào)者
現(xiàn)如今上網(wǎng)用戶越來越多,單機(jī)服務(wù)無法滿足各種高并發(fā)場景帶來的壓力,所以采用多個物理節(jié)點來共同完成任務(wù),組成一個分布式應(yīng)用,而zookeeper就是分布式應(yīng)用的協(xié)調(diào)者。
什么是協(xié)調(diào)者?你們公司的領(lǐng)導(dǎo)就是協(xié)調(diào)者,負(fù)責(zé)協(xié)調(diào)每個人的工作。
如果把公司比做動物園,每個員工是一個動物,那么負(fù)責(zé)協(xié)調(diào)工作的領(lǐng)導(dǎo)就是動物園管理員(zookeeper的直譯)。
分布式應(yīng)用也是這樣,比如有n個訂單服務(wù)、n個用戶服務(wù),兩種服務(wù)雖然各處理一類事情但總歸是需要配合的,需要配合就有公共的數(shù)據(jù)(比如節(jié)點的信息,公共的配置),那這公共的數(shù)據(jù)存哪里好?單獨交給任何一個服務(wù)都不合適,因為大家都是平等的,如果所有服務(wù)都給,一致性怎么保證,萬一不一樣到底以哪個為準(zhǔn)。
所以就需要一個中間件來協(xié)調(diào),就是zookeeper,它的存在就是給分布式應(yīng)用提供一致性服務(wù)。

服務(wù)
如果我們是zookeeper的設(shè)計者,現(xiàn)在用戶的痛點找到了,產(chǎn)品的定位也有了:協(xié)調(diào)分布式應(yīng)用。
那么接下來就要想了,要提供怎樣的服務(wù),或者說分布式應(yīng)用中哪些工作需要協(xié)調(diào)?
分布式注冊中心
比如分布式應(yīng)用有A服務(wù)、B服務(wù),A服務(wù)需要調(diào)用B服務(wù),那A怎么知道B服務(wù)在哪些臺機(jī)器吶?ip是多少?port是多少?
如果想簡單解決,那就把B服務(wù)的ip和port在A服務(wù)存儲一份就可以了,但是如果有多個B服務(wù)就需要存儲多份,某個B服務(wù)宕機(jī)了還要改。如果又加了個依賴B服務(wù)的C服務(wù),又要再C服務(wù)存一份,甚至以后有更多服務(wù)彼此依賴,那光維護(hù)這些信息就能累死。
zookeeper作為分布式協(xié)調(diào)者,這種事情肯定要管,怎么管吶,所有服務(wù)都到我這里注冊(ip、port等信息),這樣每臺機(jī)器的每個服務(wù)只需注冊一次,將來A想調(diào)用B,先來注冊中心找到一個B的位置,然后去調(diào)用就可以了,某個服務(wù)宕機(jī)了,就把它注冊的信息刪掉,起到一個注冊中心的作用。
文件系統(tǒng)數(shù)據(jù)結(jié)構(gòu)
有了具體的需求,下面就是設(shè)計了,如果自己來設(shè)計zookeeper,這個數(shù)據(jù)結(jié)構(gòu)如何設(shè)計合理?
假設(shè)場景如下:
- 當(dāng)前有多個項目
- 每個項目按功能拆分成多個微服務(wù)
- 每個微服務(wù)有多個節(jié)點,節(jié)點信息包含ip和port等所在機(jī)器信息
如果人工處理,把這些信息交給某個人(注冊員)來用電腦存儲記錄,那么他有兩種選擇:1.建一個文件夾把所有節(jié)點信息存入。2.建多個文件夾把節(jié)點信息分類存儲。
很明顯第一種一但信息多了,找太費勁了,所以一定會用第二種,存儲的信息大概如下:

這樣存的好處一看便知:方便歸納,方便查找
這時候再回頭看zookeeper的數(shù)據(jù)結(jié)構(gòu),就能理解為什么要這么設(shè)計了:

上例中分為
文件夾和txt文件,zookeeper統(tǒng)稱為節(jié)點,任何節(jié)點都可以包含信息(文件),任何節(jié)點也可以包含子節(jié)點(文件夾),節(jié)點本身可以create、delete來創(chuàng)建刪除,也可以通過set、get存放獲取數(shù)據(jù)(所以也是一個key-value模式)。
接下來展示兩個使用zookeeper做注冊中心的數(shù)據(jù)存儲
-
spring使用zookeeper做注冊中心:
spring-zookeeper -
dubbo使用zookeeper做注冊中心:
dubbo-zookeeper
可以看到有些信息直接用節(jié)點名來存儲,有些信息用set存儲,其實隨意,怎么存怎么取唄,但節(jié)點名太長如果再有子節(jié)點就很不方便
問題?
以上只能算是一個基礎(chǔ)版的注冊中心,可以注冊可以獲取僅此而已,但在微服務(wù)開發(fā)中會有這樣的場景,比如1個A服務(wù),2個B服務(wù),A調(diào)用B且從zookeeper處獲取了2個B服務(wù)的位置并緩存起來,可以選一個調(diào)用或者負(fù)載均衡輪番調(diào)用,這時突然有一個B服務(wù)宕機(jī)了,就會出現(xiàn)問題:
- 問題1,此時zookeeper中存儲的兩個B的節(jié)點信息有一個是錯誤信息,因為服務(wù)已經(jīng)停了,你不可能依靠宕機(jī)的B主動來刪除,那么zookeeper如何感知B的掉線?
- 問題2,即使zookeeper中的宕機(jī)節(jié)點信息刪除了,由于A緩存了B的信息,無法知道有一個B服務(wù)宕機(jī)了,那么如何通知A?
長鏈接和臨時節(jié)點
zookeeper的解決問題1是這樣的,作為一個注冊中心,他要求所有客戶端與其保持長鏈接,一次鏈接稱為一次會話,并通過心跳(ping,pong)不斷的檢驗會話是否依然存在,如果某個鏈接長時間不響應(yīng),那就說明服務(wù)掉線了,就可以把其注冊的節(jié)點信息刪除。
實際上刪除的方式并不是主動去刪,而是zookeeper給每個節(jié)點添加一個是否是臨時節(jié)點的屬性,并規(guī)定一個臨時節(jié)點在會話結(jié)束后自動刪除,所以每個服務(wù)注冊的都是臨時節(jié)點信息,如果服務(wù)長時間不響應(yīng)代表會話結(jié)束,臨時節(jié)點就會被自動刪除(后臺刪除,有一定延時)


通過長鏈接和臨時節(jié)點的設(shè)計者,解決了zookeeper如何感知某服務(wù)的掉線,并自動刪除掉線服務(wù)注冊的節(jié)點信息
創(chuàng)建臨時節(jié)點的方法:
create -e /ephemeral data // 其中-e代表臨時節(jié)點,非臨時節(jié)點會一直保存
那么問題1解決了,問題2:如何通知其它服務(wù)某服務(wù)的掉線事件
監(jiān)聽通知機(jī)制
問題2解決也很簡單,既然有了長鏈接保持會話,B服務(wù)掉了,zookeeper直接把掉線事件推送給A就行了。
但關(guān)鍵是zookeeper中有那么多節(jié)點,比如A只依賴B,那A只關(guān)心B服務(wù)的所有節(jié)點掉沒掉,其它節(jié)點跟我也沒關(guān)系啊,推送過多的垃圾信息浪費啊,但zookeeper也不知道誰關(guān)心誰啊,所以就推出了監(jiān)聽通知機(jī)制:
- 監(jiān)聽: 你告訴zookeeper你關(guān)心那些節(jié)點
- 通知: 你關(guān)心的節(jié)點發(fā)生變化,zookeeper負(fù)責(zé)通知你
通過這種方式,A監(jiān)聽B節(jié)點,當(dāng)B節(jié)點下有某個節(jié)點掉了zookeeper通知A,到此問題解決~
為了實現(xiàn)各種維度的監(jiān)控,zookeeper提供了多種監(jiān)聽方法:
- 針對節(jié)點監(jiān)聽
get -w /path // 注冊監(jiān)聽的同時獲取數(shù)據(jù)
stat -w /path // 對節(jié)點進(jìn)行監(jiān)聽,且獲取元數(shù)據(jù)信息
- 針對目錄監(jiān)聽,目錄的變化,會觸發(fā)事件,且一旦觸發(fā),對應(yīng)的監(jiān)聽也會被移除,后續(xù)對節(jié)點的創(chuàng)建沒有觸發(fā)監(jiān)聽事件
ls -w /path
- 針對遞歸子目錄的監(jiān)聽
ls -R -w /path : -R 區(qū)分大小寫,一定用大寫
注:zookeeper的監(jiān)聽是一次性的
總結(jié)
通過文件系統(tǒng)數(shù)據(jù)結(jié)構(gòu),長鏈接,臨時節(jié)點,監(jiān)聽通知機(jī)制,zookeeper就形成了一個完整的注冊中心。
實際使用我們可以用zookeeper配合spring來做微服務(wù)的注冊中心,dubbo也可以使用zookeeper做注冊中心,這兩個都有相關(guān)的支持。
類似的注冊中心還有eureka,nacos,目前主流應(yīng)該是nacos吧,我現(xiàn)在用的也是nacos。
分布式配置中心
這個其實很好理解了,集群里的每個節(jié)點都有一樣的配置,為了避免每個節(jié)點都去維護(hù),還不如直接存在一個配置中心里,zookeeper的文件系統(tǒng)結(jié)構(gòu)可以使用各種配置信息分類存儲,還可以通過監(jiān)聽機(jī)制來實時感知配置的變化。
發(fā)布/訂閱
有了監(jiān)聽通知機(jī)制,自然就支持了分布式發(fā)布/訂閱
分布式鎖
分布式應(yīng)用中還有一個需要協(xié)調(diào)的場景就是分布式鎖,比如給某個商品減庫存,分布式鎖的作用就是保證隸屬于不同服務(wù)所有線程同時要操作商品庫存時,只有拿到鎖的那個線程才可以操作,或者可以理解為只能排著隊一個個操作。
實際應(yīng)用中可能會有多個鎖,通過文件系統(tǒng)可以對鎖歸類整理,如下:

順序編號
如何實現(xiàn)吶,比如有A,B兩個線程都要操作商品1,B看/商品1鎖節(jié)點下沒有子節(jié)點,好,那B建一個臨時子節(jié)點B排,此時A線程又來了,看到/商品1鎖下有一個節(jié)點了,那說明B在操作商品1了,那就建一個臨時節(jié)點A排,并監(jiān)聽B排節(jié)點,B線程操作完之后刪除了B排,此時A線程感知到了,證明A排上號獲得鎖了,A開始操作商品1,操作完了刪除A排。

但是問題出現(xiàn)了,又來了個線程C,此時下面兩個節(jié)點A排,B排都在,線程C進(jìn)來監(jiān)聽誰啊,我們都只到肯定是A,但問題是C不知道這些信息,A排、B排對它來講都是一樣的,沒有辦法選擇了。
問題的根源是C不知道A和B誰先誰后,所以如果我們是zookeeper設(shè)計者,那么就要給節(jié)點加順序了:一個類似mysql自增主鍵的可以標(biāo)識先后的順序值。
這樣的話,C進(jìn)程根據(jù)順序就知道他要跟在A后面了

實際上這種結(jié)構(gòu)就好比排隊買票,排隊就有了順序,隊伍的第一個人(順序號最小)就是正在買票的人(獲取到了鎖),隊伍里的每個人都只關(guān)注前面的人即可,如果前面的人走了,證明排到自己了(獲得鎖),而自己買完票自然也要離開隊伍(刪除節(jié)點)。
總結(jié)
以上通過注冊中心,配置中心,分布式鎖等場景介紹了zookeeper主要特性:文件系統(tǒng)數(shù)據(jù)結(jié)構(gòu),CS長鏈接,監(jiān)聽通知機(jī)制,臨時節(jié)點,有序節(jié)點等主要特性,相信到此就對zookeeper有個基本的概念了。
粗暴總結(jié)一下不過就是一個:可監(jiān)控的文件系統(tǒng)結(jié)構(gòu)的數(shù)據(jù)庫,當(dāng)你的分布式應(yīng)用有數(shù)據(jù)要統(tǒng)一存儲,只要它的結(jié)構(gòu)合適,就可以用它存。
其他
除此之外zookeeper還有一些其它使用場景和特性,簡單提一下就不細(xì)說了:
持久化
zookeeper的持久化是日志+快照,這點和redis很相似
集群
zookeeper作為配置中心肯定不能輕易掛掉,為了高可用,提供了集群功能,并可以實現(xiàn)選舉,這也是一個大課題,篇幅有限,以后單說
樂觀鎖
節(jié)點信息包含版本號,做樂觀鎖再合適不過了(mysql為了實現(xiàn)樂觀鎖往往還要建一個版本號字段)
TTL
類似redis那種可以設(shè)置過期時間的節(jié)點,但是好像不準(zhǔn)確
權(quán)限控制
zookeeper作為數(shù)據(jù)存儲中心,如果暴露在非局域網(wǎng)中那就有點太可怕了,所以加了針對節(jié)點的權(quán)限控制,有些節(jié)點需要登錄才訪問,有些節(jié)點需要秘鑰,有些限制固定ip訪問,總之就是像要保證數(shù)據(jù)的安全

