一、前言
前面分析了Zookeeper對請求的處理,本篇博文接著分析Zookeeper中如何對底層數(shù)據(jù)進行存儲,數(shù)據(jù)存儲被分為內(nèi)存數(shù)據(jù)存儲于磁盤數(shù)據(jù)存儲。
二、數(shù)據(jù)與存儲
2.1 內(nèi)存數(shù)據(jù)
Zookeeper的數(shù)據(jù)模型是樹結(jié)構(gòu),在內(nèi)存數(shù)據(jù)庫中,存儲了整棵樹的內(nèi)容,包括所有的節(jié)點路徑、節(jié)點數(shù)據(jù)、ACL信息,Zookeeper會定時將這個數(shù)據(jù)存儲到磁盤上。
1. DataTree
DataTree是內(nèi)存數(shù)據(jù)存儲的核心,是一個樹結(jié)構(gòu),代表了內(nèi)存中一份完整的數(shù)據(jù)。DataTree不包含任何與網(wǎng)絡(luò)、客戶端連接及請求處理相關(guān)的業(yè)務(wù)邏輯,是一個獨立的組件。
2. DataNode
DataNode是數(shù)據(jù)存儲的最小單元,其內(nèi)部除了保存了結(jié)點的數(shù)據(jù)內(nèi)容、ACL列表、節(jié)點狀態(tài)之外,還記錄了父節(jié)點的引用和子節(jié)點列表兩個屬性,其也提供了對子節(jié)點列表進行操作的接口。
3. ZKDatabase
Zookeeper的內(nèi)存數(shù)據(jù)庫,管理Zookeeper的所有會話、DataTree存儲和事務(wù)日志。ZKDatabase會定時向磁盤dump快照數(shù)據(jù),同時在Zookeeper啟動時,會通過磁盤的事務(wù)日志和快照文件恢復(fù)成一個完整的內(nèi)存數(shù)據(jù)庫。
2.2 事務(wù)日志
1. 文件存儲
在配置Zookeeper集群時需要配置dataDir目錄,其用來存儲事務(wù)日志文件。也可以為事務(wù)日志單獨分配一個文件存儲目錄:dataLogDir。若配置dataLogDir為/home/admin/zkData/zk_log,那么Zookeeper在運行過程中會在該目錄下建立一個名字為version-2的子目錄,該目錄確定了當(dāng)前Zookeeper使用的事務(wù)日志格式版本號,當(dāng)下次某個Zookeeper版本對事務(wù)日志格式進行變更時,此目錄也會變更,即在version-2子目錄下會生成一系列文件大小一致(64MB)的文件。
2. 日志格式
在配置好日志文件目錄,啟動Zookeeper后,完成如下操作
(1) 創(chuàng)建/test_log節(jié)點,初始值為v1。
(2) 更新/test_log節(jié)點的數(shù)據(jù)為v2。
(3) 創(chuàng)建/test_log/c節(jié)點,初始值為v1。
(4) 刪除/test_log/c節(jié)點。
經(jīng)過四步操作后,會在/log/version-2/目錄下生成一個日志文件,筆者下是log.cec。
將Zookeeper下的zookeeper-3.4.6.jar和slf4j-api-1.6.1.jar復(fù)制到/log/version-2目錄下,使用如下命令打開log.cec文件。
java -classpath ./zookeeper-3.4.6.jar:./slf4j-api-1.6.1.jar org.apache.zookeeper.server.LogFormatter log.cec

ZooKeeper Transactional Log File with dbid 0 txnlog format version 2 。是文件頭信息,主要是事務(wù)日志的DBID和日志格式版本號。
...session 0x159...0xcec createSession 30000。表示客戶端會話創(chuàng)建操作。
...session 0x159...0xced create '/test_log,... 。表示創(chuàng)建/test_log節(jié)點,數(shù)據(jù)內(nèi)容為#7631(v1)。
...session 0x159...0xcee setData ‘/test_log,...。表示設(shè)置了/test_log節(jié)點數(shù)據(jù),內(nèi)容為#7632(v2)。
...session 0x159...0xcef create ’/test_log/c,...。表示創(chuàng)建節(jié)點/test_log/c。
...session 0x159...0xcf0 delete '/test_log/c。表示刪除節(jié)點/test_log/c。
3. 日志寫入
FileTxnLog負(fù)責(zé)維護事務(wù)日志對外的接口,包括事務(wù)日志的寫入和讀取等。Zookeeper的事務(wù)日志寫入過程大體可以分為如下6個步驟。
(1) 確定是否有事務(wù)日志可寫。當(dāng)Zookeeper服務(wù)器啟動完成需要進行第一次事務(wù)日志的寫入,或是上一次事務(wù)日志寫滿時,都會處于與事務(wù)日志文件斷開的狀態(tài),即Zookeeper服務(wù)器沒有和任意一個日志文件相關(guān)聯(lián)。因此在進行事務(wù)日志寫入前,Zookeeper首先會判斷FileTxnLog組件是否已經(jīng)關(guān)聯(lián)上一個可寫的事務(wù)日志文件。若沒有,則會使用該事務(wù)操作關(guān)聯(lián)的ZXID作為后綴創(chuàng)建一個事務(wù)日志文件,同時構(gòu)建事務(wù)日志的文件頭信息,并立即寫入這個事務(wù)日志文件中去,同時將該文件的文件流放入streamToFlush集合,該集合用來記錄當(dāng)前需要強制進行數(shù)據(jù)落盤的文件流。
(2) 確定事務(wù)日志文件是否需要擴容(預(yù)分配)。Zookeeper會采用磁盤空間預(yù)分配策略。當(dāng)檢測到當(dāng)前事務(wù)日志文件剩余空間不足4096字節(jié)時,就會開始進行文件空間擴容,即在現(xiàn)有文件大小上,將文件增加65536KB(64MB),然后使用"0"填充被擴容的文件空間。
(3) 事務(wù)序列化。對事務(wù)頭和事務(wù)體的序列化,其中事務(wù)體又可分為會話創(chuàng)建事務(wù)、節(jié)點創(chuàng)建事務(wù)、節(jié)點刪除事務(wù)、節(jié)點數(shù)據(jù)更新事務(wù)等。
(4) 生成Checksum。為保證日志文件的完整性和數(shù)據(jù)的準(zhǔn)確性,Zookeeper在將事務(wù)日志寫入文件前,會計算生成Checksum。
(5) 寫入事務(wù)日志文件流。將序列化后的事務(wù)頭、事務(wù)體和Checksum寫入文件流中,此時并為寫入到磁盤上。
(6) 事務(wù)日志刷入磁盤。由于步驟5中的緩存原因,無法實時地寫入磁盤文件中,因此需要將緩存數(shù)據(jù)強制刷入磁盤。
4. 日志截斷
在Zookeeper運行過程中,可能出現(xiàn)非Leader記錄的事務(wù)ID比Leader上大,這是非法運行狀態(tài)。此時,需要保證所有機器必須與該Leader的數(shù)據(jù)保持同步,即Leader會發(fā)送TRUNC命令給該機器,要求進行日志截斷,Learner收到該命令后,就會刪除所有包含或大于該事務(wù)ID的事務(wù)日志文件。
2.3 snapshot-數(shù)據(jù)快照
數(shù)據(jù)快照是Zookeeper數(shù)據(jù)存儲中非常核心的運行機制,數(shù)據(jù)快照用來記錄Zookeeper服務(wù)器上某一時刻的全量內(nèi)存數(shù)據(jù)內(nèi)容,并將其寫入指定的磁盤文件中。
1. 文件存儲
與事務(wù)文件類似,Zookeeper快照文件也可以指定特定磁盤目錄,通過dataDir屬性來配置。若指定dataDir為/home/admin/zkData/zk_data,則在運行過程中會在該目錄下創(chuàng)建version-2的目錄,該目錄確定了當(dāng)前Zookeeper使用的快照數(shù)據(jù)格式版本號。在Zookeeper運行時,會生成一系列文件。
2. 數(shù)據(jù)快照
FileSnap負(fù)責(zé)維護快照數(shù)據(jù)對外的接口,包括快照數(shù)據(jù)的寫入和讀取等,將內(nèi)存數(shù)據(jù)庫寫入快照數(shù)據(jù)文件其實是一個序列化過程。針對客戶端的每一次事務(wù)操作,Zookeeper都會將他們記錄到事務(wù)日志中,同時也會將數(shù)據(jù)變更應(yīng)用到內(nèi)存數(shù)據(jù)庫中,Zookeeper在進行若干次事務(wù)日志記錄后,將內(nèi)存數(shù)據(jù)庫的全量數(shù)據(jù)Dump到本地文件中,這就是數(shù)據(jù)快照。其步驟如下
(1) 確定是否需要進行數(shù)據(jù)快照。每進行一次事務(wù)日志記錄之后,Zookeeper都會檢測當(dāng)前是否需要進行數(shù)據(jù)快照,考慮到數(shù)據(jù)快照對于Zookeeper機器的影響,需要盡量避免Zookeeper集群中的所有機器在同一時刻進行數(shù)據(jù)快照。采用過半隨機策略進行數(shù)據(jù)快照操作。
(2) 切換事務(wù)日志文件。表示當(dāng)前的事務(wù)日志已經(jīng)寫滿,需要重新創(chuàng)建一個新的事務(wù)日志。
(3) 創(chuàng)建數(shù)據(jù)快照異步線程。創(chuàng)建單獨的異步線程來進行數(shù)據(jù)快照以避免影響Zookeeper主流程。
(4) 獲取全量數(shù)據(jù)和會話信息。從ZKDatabase中獲取到DataTree和會話信息。
(5) 生成快照數(shù)據(jù)文件名。Zookeeper根據(jù)當(dāng)前已經(jīng)提交的最大ZXID來生成數(shù)據(jù)快照文件名。
(6) 數(shù)據(jù)序列化。首先序列化文件頭信息,然后再對會話信息和DataTree分別進行序列化,同時生成一個Checksum,一并寫入快照數(shù)據(jù)文件中去。
2.4 初始化
在Zookeeper服務(wù)器啟動期間,首先會進行數(shù)據(jù)初始化工作,用于將存儲在磁盤上的數(shù)據(jù)文件加載到Zookeeper服務(wù)器內(nèi)存中。
1. 初始化流程
Zookeeper的初始化過程如下圖所示

數(shù)據(jù)的初始化工作是從磁盤上加載數(shù)據(jù)的過程,主要包括了從快照文件中加載快照數(shù)據(jù)和根據(jù)實物日志進行數(shù)據(jù)修正兩個過程。
(1) 初始化FileTxnSnapLog。FileTxnSnapLog是Zookeeper事務(wù)日志和快照數(shù)據(jù)訪問層,用于銜接上層業(yè)務(wù)和底層數(shù)據(jù)存儲,底層數(shù)據(jù)包含了事務(wù)日志和快照數(shù)據(jù)兩部分。FileTxnSnapLog中對應(yīng)FileTxnLog和FileSnap。
(2) 初始化ZKDatabase。首先構(gòu)建DataTree,同時將FileTxnSnapLog交付ZKDatabase,以便內(nèi)存數(shù)據(jù)庫能夠?qū)κ聞?wù)日志和快照數(shù)據(jù)進行訪問。在ZKDatabase初始化時,DataTree也會進行相應(yīng)的初始化工作,如創(chuàng)建一些默認(rèn)結(jié)點,如/、/zookeeper、/zookeeper/quota三個節(jié)點。
(3) 創(chuàng)建PlayBackListener。其主要用來接收事務(wù)應(yīng)用過程中的回調(diào),在Zookeeper數(shù)據(jù)恢復(fù)后期,會有事務(wù)修正過程,此過程會回調(diào)PlayBackListener來進行對應(yīng)的數(shù)據(jù)修正。
(4) 處理快照文件。此時可以從磁盤中恢復(fù)數(shù)據(jù)了,首先從快照文件開始加載。
(5) 獲取最新的100個快照文件。更新時間最晚的快照文件包含了最新的全量數(shù)據(jù)。
(6) 解析快照文件。逐個解析快照文件,此時需要進行反序列化,生成DataTree和sessionsWithTimeouts,同時還會校驗Checksum及快照文件的正確性。對于100個快找文件,如果正確性校驗通過時,通常只會解析最新的那個快照文件。只有最新快照文件不可用時,才會逐個進行解析,直至100個快照文件全部解析完。若將100個快照文件解析完后還是無法成功恢復(fù)一個完整的DataTree和sessionWithTimeouts,此時服務(wù)器啟動失敗。
(7) 獲取最新的ZXID。此時根據(jù)快照文件的文件名即可解析出最新的ZXID:zxid_for_snap。該ZXID代表了Zookeeper開始進行數(shù)據(jù)快照的時刻。
(8)** 處理事務(wù)日志**。此時服務(wù)器內(nèi)存中已經(jīng)有了一份近似全量的數(shù)據(jù),現(xiàn)在開始通過事務(wù)日志來更新增量數(shù)據(jù)。
(9) 獲取所有zxid_for_snap之后提交的事務(wù)。此時,已經(jīng)可以獲取快照數(shù)據(jù)的最新ZXID。只需要從事務(wù)日志中獲取所有ZXID比步驟7得到的ZXID大的事務(wù)操作。
(10) 事務(wù)應(yīng)用。獲取大于zxid_for_snap的事務(wù)后,將其逐個應(yīng)用到之前基于快照數(shù)據(jù)文件恢復(fù)出來的DataTree和sessionsWithTimeouts。每當(dāng)有一個事務(wù)被應(yīng)用到內(nèi)存數(shù)據(jù)庫中后,Zookeeper同時會回調(diào)PlayBackListener,將這事務(wù)操作記錄轉(zhuǎn)換成Proposal,并保存到ZKDatabase的committedLog中,以便Follower進行快速同步。
(11) 獲取最新的ZXID。待所有的事務(wù)都被完整地應(yīng)用到內(nèi)存數(shù)據(jù)庫中后,也就基本上完成了數(shù)據(jù)的初始化過程,此時再次獲取ZXID,用來標(biāo)識上次服務(wù)器正常運行時提交的最大事務(wù)ID。
(12) 校驗epoch。epoch標(biāo)識了當(dāng)前Leader周期,集群機器相互通信時,會帶上這個epoch以確保彼此在同一個Leader周期中。完成數(shù)據(jù)加載后,Zookeeper會從步驟11中確定ZXID中解析出事務(wù)處理的Leader周期:epochOfZxid。同時也會從磁盤的currentEpoch和acceptedEpoch文件中讀取上次記錄的最新的epoch值,進行校驗。
2.5 數(shù)據(jù)同步
整個集群完成Leader選舉后,Learner會向Leader進行注冊,當(dāng)Learner向Leader完成注冊后,就進入數(shù)據(jù)同步環(huán)節(jié),同步過程就是Leader將那些沒有在Learner服務(wù)器上提交過的事務(wù)請求同步給Learner服務(wù)器,大體過程如下

(1) 獲取Learner狀態(tài)。在注冊Learner的最后階段,Learner服務(wù)器會發(fā)送給Leader服務(wù)器一個ACKEPOCH數(shù)據(jù)包,Leader會從這個數(shù)據(jù)包中解析出該Learner的currentEpoch和lastZxid。
(2)** 數(shù)據(jù)同步初始化**。首先從Zookeeper內(nèi)存數(shù)據(jù)庫中提取出事務(wù)請求對應(yīng)的提議緩存隊列proposals,同時完成peerLastZxid(該Learner最后處理的ZXID)、minCommittedLog(Leader提議緩存隊列commitedLog中最小的ZXID)、maxCommittedLog(Leader提議緩存隊列commitedLog中的最大ZXID)三個ZXID值的初始化。
對于集群數(shù)據(jù)同步而言,通常分為四類,直接差異化同步(DIFF同步)、先回滾再差異化同步(TRUNC+DIFF同步)、僅回滾同步(TRUNC同步)、全量同步(SNAP同步),在初始化階段,Leader會優(yōu)先以全量同步方式來同步數(shù)據(jù)。同時,會根據(jù)Leader和Learner之間的數(shù)據(jù)差異情況來決定最終的數(shù)據(jù)同步方式。
· 直接差異化同步(DIFF同步,peerLastZxid介于minCommittedLog和maxCommittedLog之間)。Leader首先向這個Learner發(fā)送一個DIFF指令,用于通知Learner進入差異化數(shù)據(jù)同步階段,Leader即將把一些Proposal同步給自己,針對每個Proposal,Leader都會通過發(fā)送PROPOSAL內(nèi)容數(shù)據(jù)包和COMMIT指令數(shù)據(jù)包來完成,
· 先回滾再差異化同步(TRUNC+DIFF同步,Leader已經(jīng)將事務(wù)記錄到本地事務(wù)日志中,但是沒有成功發(fā)起Proposal流程)。當(dāng)Leader發(fā)現(xiàn)某個Learner包含了一條自己沒有的事務(wù)記錄,那么就需要該Learner進行事務(wù)回滾,回滾到Leader服務(wù)器上存在的,同時也是最接近于peerLastZxid的ZXID。
· 僅回滾同步(TRUNC同步,peerLastZxid大于maxCommittedLog)。Leader要求Learner回滾到ZXID值為maxCommittedLog對應(yīng)的事務(wù)操作。
· 全量同步(SNAP同步,peerLastZxid小于minCommittedLog或peerLastZxid不等于lastProcessedZxid)。Leader無法直接使用提議緩存隊列和Learner進行同步,因此只能進行全量同步。Leader將本機的全量內(nèi)存數(shù)據(jù)同步給Learner。Leader首先向Learner發(fā)送一個SNAP指令,通知Learner即將進行全量同步,隨后,Leader會從內(nèi)存數(shù)據(jù)庫中獲取到全量的數(shù)據(jù)節(jié)點和會話超時時間記錄器,將他們序列化后傳輸給Learner。Learner接收到該全量數(shù)據(jù)后,會對其反序列化后載入到內(nèi)存數(shù)據(jù)庫中。
三、總結(jié)
本篇博文主要講解了Zookeeper的數(shù)據(jù)與存儲,包括內(nèi)存數(shù)據(jù),快照數(shù)據(jù),以及如何進行數(shù)據(jù)的同步等細(xì)節(jié),至此,Zookeeper的理論學(xué)習(xí)部分已經(jīng)全部完成,之后會進行源碼分析