詳解 ZooKeeper 資料持久化

削微寒發表於2021-03-18

本文作者:HelloGitHub-老荀

Hi,這裡是 HelloGitHub 推出的 HelloZooKeeper 系列,免費開源、有趣、入門級的 ZooKeeper 教程,面向有程式設計基礎的新手。

專案地址:https://github.com/HelloGitHub-Team/HelloZooKeeper

前一篇文章我們介紹了 ZK 是如何進行選舉的,這篇我們開始學習 ZK 是如何將資料持久化到磁碟中的。

一、優秀員工小S(Sync)

我們通過之前的文章有介紹過,小S(Sync) 負責對辦事處的資料進行歸檔,所以今天他就是我們的主角,讓我們一起深入瞭解他的日常工作吧

為了喚醒大家的遠古記憶,我放一張之前的圖片

今天我們會重點講一下圖中的藍色部分,不過在此之前還是得先從整體架構上介紹下 ZK 的資料管理,ZK 的資料大致是分為了兩部分,一個是記憶體,一個就是磁碟檔案。

1.1 記憶體

雖然今天我們的主角是磁碟檔案,但是記憶體還是稍微再提一下下,幫助大家記憶的同時也能有一個比較全面的視角去認知 ZK 整體的資料管理。

ZK 在記憶體中的儲存就是之前故事中有提到的兩個賬本:小紅本和小黃本。如果排除作為回撥通知記錄的小黃本,那 ZK 的記憶體中就是小紅本對應的雜湊表而已,但是小黃本中的資料依然非常重要,所以需要將兩者作為整體一起看待,以及之前我有說過 小F(Final) 掌管了這兩個賬本,作為業務處理的最後一個負責人,小S(Sync) 從時間上來說是優先於 小F(Final) 先處理的,所以 ZK 的設計是優先將資料存入磁碟,再去修改記憶體中的資料保證儘可能的提升資料的可靠性。下面我們繼續瞭解磁碟檔案(還真就提一下下!)

1.2 磁碟檔案

ZK 的開發者給 ZK 設計了兩種磁碟檔案,對應的路徑分別是 zoo.cfg 配置中的 dataDirdataLogDir 這兩專案錄的配置。為了之後的描述清楚,我給這兩種磁碟檔案起了名字: dataDir 對應 snapshot,dataLogDir 對應 log,log 就是的是 小S(Sync) 工作中的歸檔,snapshot 就是的是 小S(Sync) 工作中的快照。

log 是負責順序記錄每一個寫請求到檔案,snapshot 則是直接將整個記憶體物件持久化至檔案中。假設我現在 zoo.cfg 的配置是這樣:

dataDir=/tmp/zookeeper/snapshot
dataLogDir=/tmp/zookeeper/log

當 ZK 啟動後會基於上面兩個路徑繼續建立 version-2 子路徑,之後的檔案都會在該子路徑下建立

/tmp
└── zookeeper
    ├── snapshot
    		└── version-2
    				└── ...
    └── log
    		└── version-2
    				└── ...

二、檔案的建立和寫入

兩種檔案分別是在什麼時候被寫入磁碟的呢?寫入的內容又是哪些呢?我們接下來對兩種檔案一一進行分析。

2.1 log 檔案

log 檔名的格式是這樣 log.{zxid} zxid 對應當時建立該檔案時的最大 zxid,假設現在建立時 zxid 為 0,那目錄結構會是這樣:

/tmp
└── zookeeper
    └── log
    		└── version-2
    				└── log.0

這個 log.0 檔案建立的時機你也可以簡單的理解為當服務端收到第一個寫請求的時候,而且當建立完成後,並不能直接將資料寫入,而是要先寫一些檔案頭的欄位,比如大名鼎鼎的魔數,版本號等元資訊。

而 log 檔案的魔數是 ZKLG(4 個位元組),版本號固定為 2(4 個位元組),還要記錄一個 dbId 固定為 0(8 個位元組) (當前沒用,可能之後會派用處吧),所以前 16 個位元組是固定這樣的:

 Z K L G        2                 0
5A4B4C47 00000002 00000000 00000000

那之後的業務資料是如何記錄的呢?

每一個寫請求都可以分為四個部分:校驗和、請求頭、請求資料、簽名,校驗和是通過後面三個欄位計算出來的,小S 每次收到寫請求後都會按照這樣的順序將對應請求的四個欄位寫入 log 檔案,由於不同的業務請求資料不固定,而且資料長度也比較大,這裡就不給大家展示具體的值(如果大家想要知道這硬核的儲存過程,不妨給我留言,我以後單獨做下,嘗試逐個位元組解釋)

然後是 zookeeper.txnLogSizeLimitInKb 這個環境變數配置,預設是 -1,這個配置限制了 log 單個檔案大小(單位是 KB),每次 小S(Sync) 歸檔的時候(圖中右下角粉色部分“是否歸檔”),將資料統一刷到磁碟後,如果使用者手動配置了該引數,就會檢查當前 log 檔案大小是否超過了該引數大小,如果超過了就會進行 rollLog,相當於下一次的寫請求會建立一個新的 log 檔案。除此之外,當 小S(Sync) 每次快照的時候會強制執行一次 rollLog。

2.2 snapshot 檔案

snapshot 檔名的格式是這樣 snapshot.{zxid} zxid 對應當是建立該檔案時的最大 zxid,假設現在建立是最大 zxid 是 0,那目錄結構會是這樣:

/tmp
└── zookeeper
    └── snapshot
    		└── version-2
    				└── snapshot.0

而關於是否快照(圖中中間區域粉色部分“是否快照”),之前有簡單介紹過是和隨機數有關,這次我們深入瞭解下。

首先有兩個配置 zookeeper.snapCount (預設 100000)和 zookeeper.snapSizeLimitInKb(預設 4194304 單位是KB,相當於 4 GB)在啟動後會基於這兩個配置分別生成兩個隨機數,假設上述的配置是按照預設的設定,這兩個隨機數的範圍就是:

randRoll = [0, 50000]
randSize = [0, 4194304 * 1024 / 2]

可以簡單的認為就是上述兩個配置的一半之內的隨機數,至於 randSize 為什麼要乘以 1024 因為最終檔案計算大小是以 byte 作為單位的。

而是否快照就是取決於上面兩個隨機數,有兩個條件:

  • 當前寫請求的數量達到了 zookeeper.snapCount 的一半並加上 randRoll 的數量
  • 當前 log 檔案的大小達到了 zookeeper.snapSizeLimitInKb 的一半並加上 randSize 的大小

上述條件滿足任意一個條件後就會重置上面的兩個隨機數,並開始生成快照,生成快照這個過程是啟動一個子執行緒去建立的。

snapshot 和 log 還有個不同的地方就是,snapshot 檔案 ZK 提供了三種不同的壓縮實現,GZIP、SNAPPY、CHECKED,通過 zookeeper.snapshot.compression.method 進行配置,預設是 CHECKED,就是原始按照位元組順序寫入,另外兩個這裡就不展開了。那我們接下來看看 snapshot 檔案是怎麼記的吧。

和 log 檔案一樣,也要先記一些檔案的頭部欄位,而 snapshot 檔案的魔數是 ZKSN(4 個位元組),版本號固定為 2(4 個位元組),還要記錄一個 dbId 固定為 -1(8 個位元組) (當前沒用,可能之後會派用處吧),所以前 16 個位元組是固定這樣的:

 Z K S N        2								 -1
5A4B534E 00000002 FFFFFFFF FFFFFFFF

然後緊跟其後的部分客戶端的會話資訊,客戶端的數量,然後迴圈記錄每一個客戶端的 sessionId、超時時間,然後是小紅本里的所有資訊了包括但不限於 ACL,節點的統計資料,節點的資料,子節點的資訊等。最後一部分就是校驗和和簽名。和 log 一樣,如果大家有興趣的話,我之後單獨再做一篇逐個位元組講解的。

三、從檔案中恢復

如果只是單單存檔案,那這檔案也沒什麼用,所以檔案另一個重要用途就是幫助 ZK 恢復服務端的資訊。

在 ZK 啟動的時候就會嘗試讀取 dataDirdataLogDir 這兩個目錄下的檔案,假設在這兩個路徑下的檔案是:

/tmp
└── zookeeper
    ├── snapshot
    		└── version-2
    				└── snapshot.5
    				└── snapshot.37
    				└── snapshot.100
    └── log
    		└── version-2
    				└── log.0
    				└── log.6
    				└── log.38
    				└── log.90
    				└── log.108

我這裡例子中的檔名的字尾數字是我隨便舉例只是為了說明恢復的過程,實際未必是這樣,切記。

現在 ZK 服務端啟動後,會先從 snapshot 的目錄中找到 zxid 最大的那個檔案,然後根據它的內容恢復 小紅本

恢復完後就會去 log 檔案目錄下尋找所有比 100 要大的 log 檔案以及比 100 要略小一點的 log 檔案,本例子中就是 log.90log.108 這兩個檔案。

你可能會問為什麼要找小於 100 的 log.90 這個檔案呢?因為檔名中的 90 只是說明這個檔案建立的時候,最大的 zxid 是 90,但是檔案中記錄的寫請求是很有可能會大於 100 的,所以 log.90 也需要被找到。

然後就是從 log.90 這個檔案開始恢復,先從 zxid 比 100 大的寫請求開始讀取並執行該寫請求,然後繼續讀取 log.108,等待所有符合條件的 log 檔案讀取後,整個 ZK 的資料就恢復完成了。

四、總結

今天我們介紹了關於 ZK 持久化的知識:

  • ZK 會持久化到磁碟的檔案有兩種:log 和 snapshot
  • log 負責記錄每一個寫請求
  • snapshot 負責對當前整個記憶體資料進行快照
  • 恢復資料的時候,會先讀取最新的 snapshot 檔案
  • 然後在根據 snapshot 最大的 zxid 去搜尋符合條件的 log 檔案,再通過逐條讀取寫請求來恢復剩餘的資料

今天的內容還是比較簡單的,為我們下一篇文章打好了基礎~下一篇我們開始介紹之前選舉中沒有介紹的內容:選舉完成後,Follower 和 Observer 是如何同 Leader 同步資料的?

老規矩,如果你有任何對文章中的疑問也可以是建議或者是對 ZK 原理部分的疑問,歡迎來倉庫中提 issue 給我們,或者來語雀話題討論。

地址:https://www.yuque.com/kaixin1002/yla8hz

相關文章