http://www.cnblogs.com/leesf456/p/6179118.html
一、前言
前面分析了Zookeeper對請求的處理,本篇博文接著分析Zookeeper中如何對底層數據進行存儲,數據存儲被分為內存數據存儲于磁盤數據存儲。
二、數據與存儲
2.1 內存數據
Zookeeper的數據模型是樹結構,在內存數據庫中,存儲了整棵樹的內容,包括所有的節點路徑、節點數據、ACL信息,Zookeeper會定時將這個數據存儲到磁盤上。
1. DataTree
DataTree是內存數據存儲的核心,是一個樹結構,代表了內存中一份完整的數據。DataTree不包含任何與網絡、客戶端連接及請求處理相關的業務邏輯,是一個獨立的組件。
2. DataNode
DataNode是數據存儲的最小單元,其內部除了保存了結點的數據內容、ACL列表、節點狀態之外,還記錄了父節點的引用和子節點列表兩個屬性,其也提供了對子節點列表進行操作的接口。
3. ZKDatabase
Zookeeper的內存數據庫,管理Zookeeper的所有會話、DataTree存儲和事務日志。ZKDatabase會定時向磁盤dump快照數據,同時在Zookeeper啟動時,會通過磁盤的事務日志和快照文件恢復成一個完整的內存數據庫。
2.2 事務日志
1. 文件存儲
在配置Zookeeper集群時需要配置dataDir目錄,其用來存儲事務日志文件。也可以為事務日志單獨分配一個文件存儲目錄:dataLogDir。若配置dataLogDir為/home/admin/zkData/zk_log,那么Zookeeper在運行過程中會在該目錄下建立一個名字為version-2的子目錄,該目錄確定了當前Zookeeper使用的事務日志格式版本號,當下次某個Zookeeper版本對事務日志格式進行變更時,此目錄也會變更,即在version-2子目錄下會生成一系列文件大小一致(64MB)的文件。
2. 日志格式
在配置好日志文件目錄,啟動Zookeeper后,完成如下操作
(1) 創建/test_log節點,初始值為v1。
(2) 更新/test_log節點的數據為v2。
(3) 創建/test_log/c節點,初始值為v1。
(4) 刪除/test_log/c節點。
經過四步操作后,會在/log/version-2/目錄下生成一個日志文件,筆者下是log.cec。
將Zookeeper下的zookeeper-3.4.6.jar和slf4j-api-1.6.1.jar復制到/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 。是文件頭信息,主要是事務日志的DBID和日志格式版本號。
...session 0x159...0xcec createSession 30000。表示客戶端會話創建操作。
...session 0x159...0xced create '/test_log,... 。表示創建/test_log節點,數據內容為#7631(v1)。
...session 0x159...0xcee setData ‘/test_log,...。表示設置了/test_log節點數據,內容為#7632(v2)。
...session 0x159...0xcef create ’/test_log/c,...。表示創建節點/test_log/c。
...session 0x159...0xcf0 delete '/test_log/c。表示刪除節點/test_log/c。
3. 日志寫入
FileTxnLog負責維護事務日志對外的接口,包括事務日志的寫入和讀取等。Zookeeper的事務日志寫入過程大體可以分為如下6個步驟。
(1) 確定是否有事務日志可寫。當Zookeeper服務器啟動完成需要進行第一次事務日志的寫入,或是上一次事務日志寫滿時,都會處于與事務日志文件斷開的狀態,即Zookeeper服務器沒有和任意一個日志文件相關聯。因此在進行事務日志寫入前,Zookeeper首先會判斷FileTxnLog組件是否已經關聯上一個可寫的事務日志文件。若沒有,則會使用該事務操作關聯的ZXID作為后綴創建一個事務日志文件,同時構建事務日志的文件頭信息,并立即寫入這個事務日志文件中去,同時將該文件的文件流放入streamToFlush集合,該集合用來記錄當前需要強制進行數據落盤的文件流。
(2) 確定事務日志文件是否需要擴容(預分配)。Zookeeper會采用磁盤空間預分配策略。當檢測到當前事務日志文件剩余空間不足4096字節時,就會開始進行文件空間擴容,即在現有文件大小上,將文件增加65536KB(64MB),然后使用"0"填充被擴容的文件空間。
(3) 事務序列化。對事務頭和事務體的序列化,其中事務體又可分為會話創建事務、節點創建事務、節點刪除事務、節點數據更新事務等。
(4) 生成Checksum。為保證日志文件的完整性和數據的準確性,Zookeeper在將事務日志寫入文件前,會計算生成Checksum。
(5) 寫入事務日志文件流。將序列化后的事務頭、事務體和Checksum寫入文件流中,此時并為寫入到磁盤上。
(6) 事務日志刷入磁盤。由于步驟5中的緩存原因,無法實時地寫入磁盤文件中,因此需要將緩存數據強制刷入磁盤。
4. 日志截斷
在Zookeeper運行過程中,可能出現非Leader記錄的事務ID比Leader上大,這是非法運行狀態。此時,需要保證所有機器必須與該Leader的數據保持同步,即Leader會發送TRUNC命令給該機器,要求進行日志截斷,Learner收到該命令后,就會刪除所有包含或大于該事務ID的事務日志文件。
2.3 snapshot-數據快照
數據快照是Zookeeper數據存儲中非常核心的運行機制,數據快照用來記錄Zookeeper服務器上某一時刻的全量內存數據內容,并將其寫入指定的磁盤文件中。
1. 文件存儲
與事務文件類似,Zookeeper快照文件也可以指定特定磁盤目錄,通過dataDir屬性來配置。若指定dataDir為/home/admin/zkData/zk_data,則在運行過程中會在該目錄下創建version-2的目錄,該目錄確定了當前Zookeeper使用的快照數據格式版本號。在Zookeeper運行時,會生成一系列文件。
2. 數據快照
FileSnap負責維護快照數據對外的接口,包括快照數據的寫入和讀取等,將內存數據庫寫入快照數據文件其實是一個序列化過程。針對客戶端的每一次事務操作,Zookeeper都會將他們記錄到事務日志中,同時也會將數據變更應用到內存數據庫中,Zookeeper在進行若干次事務日志記錄后,將內存數據庫的全量數據Dump到本地文件中,這就是數據快照。其步驟如下
(1) 確定是否需要進行數據快照。每進行一次事務日志記錄之后,Zookeeper都會檢測當前是否需要進行數據快照,考慮到數據快照對于Zookeeper機器的影響,需要盡量避免Zookeeper集群中的所有機器在同一時刻進行數據快照。采用過半隨機策略進行數據快照操作。
(2) 切換事務日志文件。表示當前的事務日志已經寫滿,需要重新創建一個新的事務日志。
(3) 創建數據快照異步線程。創建單獨的異步線程來進行數據快照以避免影響Zookeeper主流程。
(4) 獲取全量數據和會話信息。從ZKDatabase中獲取到DataTree和會話信息。
(5) 生成快照數據文件名。Zookeeper根據當前已經提交的最大ZXID來生成數據快照文件名。
(6) 數據序列化。首先序列化文件頭信息,然后再對會話信息和DataTree分別進行序列化,同時生成一個Checksum,一并寫入快照數據文件中去。
2.4 初始化
在Zookeeper服務器啟動期間,首先會進行數據初始化工作,用于將存儲在磁盤上的數據文件加載到Zookeeper服務器內存中。
1. 初始化流程
Zookeeper的書初始化過程如下圖所示

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

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