深入了解Zookeeper核心原理
之前的文章Zookeeper基礎(chǔ)原理&應(yīng)用場景詳解中將Zookeeper的基本原理及其應(yīng)用場景做了一個(gè)詳細(xì)的介紹,雖然介紹了其底層的存儲(chǔ)原理、如何使用Zookeeper來實(shí)現(xiàn)分布式鎖。但是我認(rèn)為這樣也僅僅只是了解了Zookeeper的一點(diǎn)皮毛而已。所以這篇文章就給大家詳細(xì)聊聊Zookeeper的核心底層原理。不太熟悉Zookeeper的可以回過頭去看看。
ZNode
這個(gè)應(yīng)該算是Zookeeper中的基礎(chǔ),數(shù)據(jù)存儲(chǔ)的最小單元。在Zookeeper中,類似文件系統(tǒng)的存儲(chǔ)結(jié)構(gòu),被Zookeeper抽象成了樹,樹中的每一個(gè)節(jié)點(diǎn)(Node)被叫做ZNode。ZNode中維護(hù)了一個(gè)數(shù)據(jù)結(jié)構(gòu),用于記錄ZNode中數(shù)據(jù)更改的版本號(hào)以及ACL(Access Control List)的變更。
有了這些數(shù)據(jù)的版本號(hào)以及其更新的Timestamp,Zookeeper就可以驗(yàn)證客戶端請(qǐng)求的緩存是否合法,并協(xié)調(diào)更新。
而且,當(dāng)Zookeeper的客戶端執(zhí)行更新或者刪除操作時(shí),都必須要帶上要修改的對(duì)應(yīng)數(shù)據(jù)的版本號(hào)。如果Zookeeper檢測(cè)到對(duì)應(yīng)的版本號(hào)不存在,則不會(huì)執(zhí)行這次更新。如果合法,在ZNode中數(shù)據(jù)更新之后,其對(duì)應(yīng)的版本號(hào)也會(huì)一起更新。
- 這套版本號(hào)的邏輯,其實(shí)很多框架都在用,例如RocketMQ中,Broker向NameServer注冊(cè)的時(shí)候,也會(huì)帶上這樣一個(gè)版本號(hào),叫DateVersion。
接下來我們來詳細(xì)看一下這個(gè)維護(hù)版本號(hào)相關(guān)數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu),它叫Stat Structure,其字段有:
舉個(gè)例子,通過stat命令,我們可以查看某個(gè)ZNode中Stat Structure具體的值。
關(guān)于這里的epoch、zxid是Zookeeper集群相關(guān)的東西,后面會(huì)詳細(xì)的對(duì)其進(jìn)行介紹。
ACL
ACL(Access Control List)用于控制ZNode的相關(guān)權(quán)限,其權(quán)限控制和Linux中的類似。Linux中權(quán)限種類分為了三種,分別是讀、寫、執(zhí)行,分別對(duì)應(yīng)的字母是r、w、x。其權(quán)限粒度也分為三種,分別是擁有者權(quán)限、群組權(quán)限、其他組權(quán)限,舉個(gè)例子:
- drwxr-xr-x 3 USERNAME GROUP 1.0K 3 15 18:19 dir_name
什么叫粒度?粒度是對(duì)權(quán)限所作用的對(duì)象的分類,把上面三種粒度換個(gè)說法描述就是**對(duì)用戶(Owner)、用戶所屬的組(Group)、其他組(Other)**的權(quán)限劃分,這應(yīng)該算是一種權(quán)限控制的標(biāo)準(zhǔn)了,典型的三段式。
Zookeeper中雖然也是三段式,但是兩者對(duì)粒度的劃分存在區(qū)別。Zookeeper中的三段式為Scheme、ID、Permissions,含義分別為權(quán)限機(jī)制、允許訪問的用戶和具體的權(quán)限。
Scheme代表了一種權(quán)限模式,有以下5種類型:
- world 在此中Scheme下,ID只能是anyone,代表所有人都可以訪問
- auth 代表已經(jīng)通過了認(rèn)證的用戶
- digest 使用用戶名+密碼來做校驗(yàn)。
- ip 只允許某些特定的IP訪問ZNode
- X509 通過客戶端的證書進(jìn)行認(rèn)證
同時(shí)權(quán)限種類也有五種:
- CREATE 創(chuàng)建節(jié)點(diǎn)
- READ 獲取節(jié)點(diǎn)或列出其子節(jié)點(diǎn)
- WRITE 能設(shè)置節(jié)點(diǎn)的數(shù)據(jù)
- DELETE 能夠刪除子節(jié)點(diǎn)
- ADMIN 能夠設(shè)置權(quán)限
同Linux中一樣,這個(gè)權(quán)限也有縮寫,舉個(gè)例子:
getAcl方法用戶查看對(duì)應(yīng)的ZNode的權(quán)限,如圖,我們可以輸出的結(jié)果呈三段式。分別是:
- scheme 使用了world
- id 值為anyone,代表所有用戶都有權(quán)限
- permissions 其具體的權(quán)限為cdrwa,分別是CREATE、DELETE、READ、WRITE和ADMIN的縮寫
Session機(jī)制
了解了Zookeeper的Version機(jī)制,我們可以繼續(xù)探索Zookeeper的Session機(jī)制了。
我們知道,Zookeeper中有4種類型的節(jié)點(diǎn),分別是持久節(jié)點(diǎn)、持久順序節(jié)點(diǎn)、臨時(shí)節(jié)點(diǎn)和臨時(shí)順序節(jié)點(diǎn)。
在之前的文章我們聊到過,客戶端如果創(chuàng)建了臨時(shí)節(jié)點(diǎn),并在之后斷開了連接,那么所有的臨時(shí)節(jié)點(diǎn)就都會(huì)被刪除。實(shí)際上斷開連接的說話不是很精確,應(yīng)該是說客戶端建立連接時(shí)的Session過期之后,其創(chuàng)建的所有臨時(shí)節(jié)點(diǎn)就會(huì)被全部刪除。
那么Zookeeper是怎么知道哪些臨時(shí)節(jié)點(diǎn)是由當(dāng)前客戶端創(chuàng)建的呢?
- 答案是Stat Structure中的**ephemeralOwner(臨時(shí)節(jié)點(diǎn)的Owner)**字段
上面說過,如果當(dāng)前是臨時(shí)順序節(jié)點(diǎn),那么ephemeralOwner則存儲(chǔ)了創(chuàng)建該節(jié)點(diǎn)的Owner的SessionID,有了SessionID,自然就能和對(duì)應(yīng)的客戶端匹配上,當(dāng)Session失效之后,才能將該客戶端創(chuàng)建的所有臨時(shí)節(jié)點(diǎn)全部刪除。
對(duì)應(yīng)的服務(wù)在創(chuàng)建連接的時(shí)候,必須要提供一個(gè)帶有所有服務(wù)器、端口的字符串,單個(gè)之間逗號(hào)相隔,舉個(gè)例子。
- 127.0.0.1:3000:2181,127.0.0.1:2888,127.0.0.1:3888
Zookeeper的客戶端收到這個(gè)字符串之后,會(huì)從中隨機(jī)選一個(gè)服務(wù)、端口來建立連接。如果連接在之后斷開,客戶端會(huì)從字符串中選擇下一個(gè)服務(wù)器,繼續(xù)嘗試連接,直到連接成功。
除了這種最基本的IP+端口,在Zookeeper的3.2.0之后的版本中還支持連接串中帶上路徑,舉個(gè)例子。
- 127.0.0.1:3000:2181,127.0.0.1:2888,127.0.0.1:3888/app/a
這樣一來,/app/a就會(huì)被當(dāng)成當(dāng)前服務(wù)的根目錄,在其下創(chuàng)建的所有的節(jié)點(diǎn)路經(jīng)都會(huì)帶上前綴/app/a。舉個(gè)例子,我創(chuàng)建了一個(gè)節(jié)點(diǎn)/node_name,那其完整的路徑就會(huì)為/app/a/node_name。這個(gè)特性特別適用于多租戶的環(huán)境,對(duì)于每個(gè)租戶來說,都認(rèn)為自己是最頂層的根目錄/。
當(dāng)Zookeeper的客戶端和服務(wù)器都建立了連接之后,客戶端會(huì)拿到一個(gè)64位的SessionID和密碼。這個(gè)密碼是干什么用的呢?我們知道Zookeeper可以部署多個(gè)實(shí)例,如果客戶端斷開了連接又和另外的Zookeeper服務(wù)器建立了連接,那么在建立連接使就會(huì)帶上這個(gè)密碼。該密碼是Zookeeper的一種安全措施,所有的Zookeeper節(jié)點(diǎn)都可以對(duì)其進(jìn)行驗(yàn)證。這樣一來,即使連接到了其他Zookeeper節(jié)點(diǎn),Session同樣有效。
Session過期有兩種情況,分別是:
- 過了指定的失效時(shí)間
- 指定時(shí)間內(nèi)客戶端沒有發(fā)送心跳
對(duì)于第一種情況,過期時(shí)間會(huì)在Zookeeper客戶端建立連接的時(shí)候傳給服務(wù)器,這個(gè)過期時(shí)間的范圍目前只能在2倍tickTime和20倍tickTime之間。
ticktime是Zookeeper服務(wù)器的配置項(xiàng),用于指定客戶端向服務(wù)器發(fā)送心跳的間隔,其默認(rèn)值為tickTime=2000,單位為毫秒
而這套Session的過期邏輯由Zookeeper的服務(wù)器維護(hù),一旦Session過期,服務(wù)器會(huì)立即刪除由Client創(chuàng)建的所有臨時(shí)節(jié)點(diǎn),然后通知所有正在監(jiān)聽這些節(jié)點(diǎn)的客戶端相關(guān)變更。
對(duì)于第二種情況,Zookeeper中的心跳是通過PING請(qǐng)求來實(shí)現(xiàn)的,每隔一段時(shí)間,客戶端都會(huì)發(fā)送PING請(qǐng)求到服務(wù)器,這就是心跳的本質(zhì)。心跳使服務(wù)器感知到客戶端還活著,同樣的讓客戶端也感知到和服務(wù)器的連接仍然是有效的,這個(gè)間隔就是**tickTime**,默認(rèn)為2秒。
Watch機(jī)制
了解完ZNode和Session,我們終于可以來繼續(xù)下一個(gè)關(guān)鍵功能Watch了,在上面的內(nèi)容中也不止一次的提到**監(jiān)聽(Watch)**這個(gè)詞。首先用一句話來概括其作用
- 給某個(gè)節(jié)點(diǎn)注冊(cè)監(jiān)聽器,該節(jié)點(diǎn)一旦發(fā)生變更(例如更新或者刪除),監(jiān)聽者就會(huì)收到一個(gè)Watch Event
和ZNode中有多種類型一樣,Watch也有多種類型,分別是一次性Watch和永久性Watch。
- 一次性Watch 在被觸發(fā)之后,該Watch就會(huì)移除
- 永久性Watch 在被觸發(fā)之后,仍然保留,可以繼續(xù)監(jiān)聽ZNode上的變更,是Zookeeper 3.6.0版本新增的功能
一次性的Watch可以在調(diào)用getData()、getChildren()和exists()等方法時(shí)在參數(shù)中進(jìn)行設(shè)置,永久性的Watch則需要調(diào)用addWatch()來實(shí)現(xiàn)。
并且一次性的Watch會(huì)存在問題,因?yàn)樵赪atch觸發(fā)的事件到達(dá)客戶端、再到客戶端設(shè)立新的Watch,是有一個(gè)時(shí)間間隔的。而如果在這個(gè)時(shí)間間隔中發(fā)生的變更,客戶端則無法感知。
Zookeeper集群架構(gòu)
ZAB協(xié)議
把前面的都鋪墊好之后就可以來從整體架構(gòu)的角度再深入了解Zookeeper。Zookeeper為了保證其高可用,采用的基于主從的讀寫分離架構(gòu)。
- 我們知道在類似的Redis主從架構(gòu)中,節(jié)點(diǎn)之間是采用的Gossip協(xié)議來進(jìn)行通信的,那么在Zookeeper中通信協(xié)議是什么?
答案是**ZAB(Zookeeper Atomic Broadcast)**協(xié)議。
ZAB協(xié)議是一種支持崩潰恢復(fù)的的原子廣播協(xié)議,用于在Zookeeper之間傳遞消息,使所有的節(jié)點(diǎn)都保持同步。ZAB同時(shí)具有高性能、高可用的、容易上手、利于維護(hù)的特點(diǎn),同時(shí)支持自動(dòng)的故障恢復(fù)。
ZAB協(xié)議將Zookeeper集群中的節(jié)點(diǎn)劃分成了三個(gè)角色,分別是Leader、Follower和Observer,如下圖:
總的來說,這套架構(gòu)和Redis主從或者M(jìn)ySQL主從的架構(gòu)類似(感興趣的也可以去看之前的寫的文章,都有聊過)
- Redis主從
- MySQL主從
不同點(diǎn)在于,通常的主從架構(gòu)中存在兩種角色,分別是Leader、Follower(或者是Master、Slave),但Zookeeper中多了一個(gè)Observer。
- 那問題來了,Observer和Follower的區(qū)別是啥呢?
本質(zhì)上來說兩者的功能是一樣的, 都為Zookeeper提供了橫向擴(kuò)展的能力,使其能夠扛住更多的并發(fā)。但區(qū)別在于Leader的選舉過程中,Observer不參與投票選舉。
順序一致性
上文提到了Zookeeper集群中是讀寫分離的,只有Leader節(jié)點(diǎn)能處理寫請(qǐng)求,如果Follower節(jié)點(diǎn)接收到了寫請(qǐng)求,會(huì)將該請(qǐng)求轉(zhuǎn)發(fā)給Leader節(jié)點(diǎn)處理,F(xiàn)ollower節(jié)點(diǎn)自身是不會(huì)處理寫請(qǐng)求的。
Leader節(jié)點(diǎn)接收到消息之后,會(huì)按照請(qǐng)求的嚴(yán)格順序一一的進(jìn)行處理。這是Zookeeper的一大特點(diǎn),它會(huì)保證消息的順序一致性。
- 舉個(gè)例子,如果消息A比消息B先到,那么在所有的Zookeeper節(jié)點(diǎn)中,消息A都會(huì)先于消息B到達(dá),Zookeeper會(huì)保證消息的全局順序。
zxid
那Zookeeper是如何保證消息的順序?答案是通過zxid。
可以簡單的把zxid理解成Zookeeper中消息的唯一ID,節(jié)點(diǎn)之間會(huì)通過發(fā)送**Proposal(事務(wù)提議)**來進(jìn)行通信、數(shù)據(jù)同步,proposal中就會(huì)帶上zxid和具體的數(shù)據(jù)(Message)。而zxid由兩部分組成:
- epoch 可以理解成朝代,或者說Leader迭代的版本,每個(gè)Leader的epoch都不一樣
- counter 計(jì)數(shù)器,來一條消息就會(huì)自增
這也是唯一zxid生成算法的底層實(shí)現(xiàn),由于每個(gè)Leader所使用的epoch都是唯一的,而不同的消息在相同的epoch中,counter的值是不同的,這樣一來所有的proposal在Zookeeper集群中都有唯一的zxid。
恢復(fù)模式
正常運(yùn)行的Zookeeper集群會(huì)處于廣播模式。相反,如果超過半數(shù)的節(jié)點(diǎn)宕機(jī),就會(huì)進(jìn)入恢復(fù)模式。
- 什么是恢復(fù)模式?
在Zookeeper集群中,存在兩種模式,分別是:
- 恢復(fù)模式
- 廣播模式
當(dāng)Zookeeper集群故障時(shí)會(huì)進(jìn)入恢復(fù)模式,也叫做Leader Activation,顧名思義就是要在此階段選舉出Leader。節(jié)點(diǎn)之間會(huì)生成zxid和Proposal,然后相互投票。投票是要有原則的,主要有兩條:
- 選舉出來的Leader的zxid一定要是所有的Follower中最大的
- 并且已有超過半數(shù)的Follower返回了ACK,表示認(rèn)可選舉出來的Leader
如果在選舉的過程中發(fā)生異常,Zookeeper會(huì)直接進(jìn)行新一輪的選舉。如果一切順利,Leader就會(huì)被成功選舉出來,但是此時(shí)集群還不能正常對(duì)外提供服務(wù),因?yàn)樾碌腖eader和Follower之間還沒有進(jìn)行關(guān)鍵的數(shù)據(jù)同步。
此后,Leader會(huì)等待其余的Follower來連接,然后通過Proposal向所有的Follower發(fā)送其缺失的數(shù)據(jù)。
- 至于怎么知道缺失哪些數(shù)據(jù),Proposal本身是要記錄日志,通過Proposal中的zxid的低32位的Counter中的值,就可以做一個(gè)Diff
當(dāng)然這里有個(gè)優(yōu)化,如果缺失的數(shù)據(jù)太多,那么一條一條的發(fā)送Proposal效率太低。所以如果Leader發(fā)現(xiàn)缺失的數(shù)據(jù)過多就會(huì)將當(dāng)前的數(shù)據(jù)打個(gè)快照,直接打包發(fā)送給Follower。
新選舉出來的Leader的Epoch,會(huì)在原來的值上+1,并且將Counter重置為0。
- 到這你是不是以為就完了?實(shí)際上到這還是無法正常提供服務(wù)
數(shù)據(jù)同步完成之后,Leader會(huì)發(fā)送一個(gè)NEW_LEADER的Proposal給Follower,當(dāng)且僅當(dāng)該P(yáng)roposal被過半的Follower返回Ack之后,Leader才會(huì)Commit該NEW_LEADER Proposal,集群才能正常的進(jìn)行工作。
至此,恢復(fù)模式結(jié)束,集群進(jìn)入廣播模式。
廣播模式
在廣播模式下,Leader接收到消息之后,會(huì)向其他所有Follower發(fā)送Proposal(事務(wù)提議),F(xiàn)ollower接收到Proposal之后會(huì)返回ACK給Leader。當(dāng)Leader收到了quorums個(gè)ACK之后,當(dāng)前Proposal就會(huì)提交,被應(yīng)用到節(jié)點(diǎn)的內(nèi)存中去。quorum個(gè)是多少呢?
Zookeeper官方建議每2個(gè)Zookeeper節(jié)點(diǎn)中,至少有一個(gè)需要返回ACK才行,假設(shè)有N個(gè)Zookeeper節(jié)點(diǎn),那計(jì)算公式應(yīng)該是n/2 + 1。
這樣可能不是很直觀,用大白話來說就是,超過半數(shù)的Follower返回了ACK,該P(yáng)roposal就能夠提交,并且應(yīng)用至內(nèi)存中的ZNode。
Zookeeper使用2PC來保證節(jié)點(diǎn)之間的數(shù)據(jù)一致性(如上圖),但是由于Leader需要跟所有的Follower交互,這樣一來通信的開銷會(huì)變得較大,Zookeeper的性能就會(huì)下降。所以為了提升Zookeeper的性能,才從所有的Follower節(jié)點(diǎn)返回ACK變成了過半的Follower返回ACK即可。