1. 程式人生 > >讀《從Paxos到Zookeeper 分散式一致性原理與實踐》筆記之資料與儲存

讀《從Paxos到Zookeeper 分散式一致性原理與實踐》筆記之資料與儲存

1.1. 資料與儲存

1.1.1.  記憶體資料

        資料結構:


ZooKeeper的資料模型是一棵樹,而從使用角度看, Zookeeper就像一個記憶體資料庫一樣。在這個記憶體資料庫中,儲存了整棵樹的內容,包括所有的節點路徑、節點資料及其ACL資訊等,Zookeeper會定時將這個資料儲存到磁碟上。

1.1.2.  事物日誌

那麼ZooKeeper 在執行過程中會在事物日誌目錄下建立一個名字為version-2的子目錄,該目錄確定了當前ZooKeeper使用的事務日誌格式版本號。也就足說,等到下次某個ZooKeeper版本對事務日誌格式進行變更時,這個目錄也會有所變更。

事物日誌檔案目錄截圖


檔案內容按照如下的順序寫入:

fileheader
   magic //"ZKSN"
   version //2
   dbid //-1
fileheader
crcvalue1--校驗和,校驗下面的txtEntry
txtEntry1
   hdr
     clientId
     cxid
     zxid
     time
     type(包括createSession、closeSession、create、create2、createContainer、delete、deleteContainer、reconfig、setData、setACL、error、multi)
   hdr
   --不通的hdr.type,有不同的txn
   txn(createSession)
       timeOut
   txn
   txn(closeSession、create、create2)
       path
       data
       acl
       ephemeral
       parentCVersion//父節點的版本號cversion
   txn
   txn(createContainer)
       path
       data
       acl
       parentCVersion
   txn
   txn(delete、deleteContainer)
       path
   txn
   txn(reconfig、setData)
       path
       data
       version
   txn
   txn(setACL)
       path
       acl
       version
   txn
txtEntry1
EOR 0x42--B,標識一個事物記錄結束
crcvalue2--校驗和,校驗下面的txtEntry
txtEntry2
   hdr
    
   hdr
   txn
      
   txn
txtEntry2

使用二進位制編輯器開啟事物日誌檔案,可以看到類似如下所示檔案內容—這就是序列化之後的事務日誌檔案。


使用工具類(org.apache.zookeeper.server.LogFormatter)解析事物日誌檔案,輸出內容如下:



第一行:

ZooKeeper Transactional Log File with dbid 0 txnlog format version 2

這一行是事物日誌的檔案頭資訊,這裡輸出的主要是事物日誌的DBID和日誌格式版本號。

16-11-19 下午01時12分48秒 session 0x15554779e59002c cxid 0x0 zxid 0x600000075 createSession 40000

這一行就是一次客戶端會話建立的事務操作日誌,其中我們不難看出,從左向右分別記錄事物操作時間、客戶端會話ID、CXID (客戶端的操作序列號)、ZXID、操作型別和會話超時時間。

16-11-19 下午12時04分00秒 session 0x15554779e59000f cxid 0x1 zxid 0x600000027 create '/zookeeper/test,#2f7a6f6f6b65657065722f74657374,v{s{31,s{'digest,'foo:Jfg7TYUBs/6KEtdDWd5OB6bdD2Q=}}},F,2
這一行是節點建立操作的事務操作日誌,從左向右分別記錄了事物操作時間、客戶端會話ID、CXID、ZXID、操作型別、節點路徑、節點資料內容(#2f7a6f6f6b65657065722f74657374,在LogFormatter中使用如下格式輸出節點內容:#+內容的ASCII碼值)、節點的ACL資訊、是否是臨時節點(F代表持久節點,T代表臨時節點)和父節點的子節點版本號。

日誌寫入

1.建立事物日誌檔案

當ZooKeeper伺服器啟動完成需要進行第一次事務日誌的寫入,或是上一個事務日誌寫滿的時候,都會處於與事務日誌檔案斷開的狀態,即ZooKeeper伺服器沒有和任意一個日誌檔案相關聯。因此,在進行事務日誌寫入前,Zookeeper首先會判斷FileTxnLog元件是否已經關聯了一個可寫的事物日誌檔案。如果沒有關聯上事務日誌檔案,那麼就會使用與該事務操作關聯的ZXID作為字尾建立一個事物日誌檔案,同時構建事務日誌檔案頭資訊(包含魔數magic、事務日誌格式版本version和dbid),並立即寫入這個事物日誌檔案中去。

2.確定事務日誌檔案是否需要擴容(預分配)。

當檢測到當前務日誌檔案剩餘空間不足4096位元組(4KB)時,就會開始進行檔案空間擴容。在現行檔案大小的基礎上,將檔案大小增加65536KB (64MB),然後使用“0”(\0)填充這些被擴容的檔案空間。

那麼ZooKeeper為什麼要進行事務日誌檔案的磁碟空間預分配呢?對客戶端的每一次事務操作,ZooKeeper都會將其寫人事務日誌檔案中。因此,事物日誌的寫入效能直接決定了ZooKeeper伺服器對事務請求的響應,也就是說,事務寫入近似可以被看作是一個磁碟I/O的過程。嚴格地講,檔案的不斷追加寫入操作會觸發底層磁碟I/O為檔案開闢新的磁碟塊,即磁碟Seek。因此,為了避免磁碟Seek的頻率,提高磁碟I/O的效率,ZooKeeper在建立事物日誌的時候就會進行檔案空間“預分配”——在檔案建立之初就向作業系統預分配一個很大的磁碟塊,預設是64MB,而一旦已分配的檔案空間不足4KB 時,那麼將會再次“預分配”,以避免隨著每次事物的寫入過程中檔案大小增長帶來的Seek開銷,直至建立新的事務日誌。

3.事物序列化

事務序列化包括對事務頭和事務體的序列化,分別是對TxnHeader(事務頭)和Record (事務體)的序列化。其中事務體又可分為會話建立事務(CreateSessionTxn)、節點建立事務(CreateTxn)、節點刪除事務(DeleteTxn)和節點資料更新事務 (SetDataTxn)等。

4.生成Checksum。

為了保證事務日誌檔案的完整性和資料的準確性,ZooKeeper在將事務日誌寫入檔案前,會根據步驟3中序列化產生的位元組陣列來計算Checksum。ZooKeeper預設使用Adler32演算法來計算Checksum值。

5.寫入事務日誌檔案流。

將序列化後的事務頭、事務體及Checksum值寫入到檔案流中去。此時由於 ZooKeeper使用的足BufferedOutputStream,因此寫人的資料並非真正被寫入到磁碟檔案上。

6.事務日誌刷入磁碟。

在步驟5中,已經將事務操作寫人檔案流中,但是由於快取的原因,無法實時地寫入磁碟檔案中,因此我們需要將快取資料強制刷入磁碟中,在步驟1中我們已經將每個事物日誌檔案對應的檔案流放入streamsToFlush,因此這裡會從 streamsToFlush 中提取出檔案流,並呼叫FileChannel.force(boolean metaData)介面來強制將資料刷入磁碟檔案中去。force介面對應的其實是底層的fsync介面,是一個比較耗費磁碟I/O資源的介面,因此ZooKeeper允許使用者控制是否需要主動呼叫該介面,以通過系統屬性zookeeper.forceSync來設定。

1.1.3.  資料快照

和事物日誌類似,zookeeper的快照資料也是使用特定的磁碟目錄進行儲存,讀者也可以通過dataDir屬性進行配置。在執行的過程中會在該目錄下建立一個名為version-2的目錄,改目錄切丁了當前zookeeper使用的快照資料格式版本號。

快照檔案目錄截圖


檔案內容按照如下的順序寫入:

fileheader
    magic //"ZKSN"
    version //2
    dbid //-1
fileheader
count(會話數量)
id //會話ID
timeout //會話超時
id
timeout
...
map//int acls數量
long//acl的key
acls
    acl
    acl
acls
long//acl的key
acls
    acl
    acl
acls
path1
node1
    data
    acl
    stat
        cZxid:建立節點時的事物ID
      ctime:建立節點的時間
      mZxid:節點最新一次更新時的事物ID
      mtime:最近一次節點更新的時間
      pZxid:該節點的子節點列表最後一次被修改時的事物ID,如果沒有子節點,則為當前節點的cZxid
      cversion:子節點更新次數
      dataVersion:節點資料更新次數
      aclVersion:節點acl更新次數
      ephemeralOwner:如果節點為ephemeral節點則該值為sessionid,否則為0
      dataLength:該節點資料的長度
    stat
node1
path2
node2
 
node2
/zookeeper/quota
val--以上資料的校驗和
path:/ --path的值為'/',結束檔案

使用工具類(org.apache.zookeeper.server.SnapshotFormatter)解析快照檔案,輸出內容如下:

輸出的僅僅是資料節點的元資訊,並沒有輸出每個節點的資料內容。

資料結構:

 

將記憶體資料庫寫入快照資料檔案中,其實就是一個序列化過程。

針對客戶端的每一次事務操作,ZooKeeper都會將它們記錄到事物日誌中,當然,ZooKeeper同時也會將資料變更應用到記憶體資料庫中。另外,ZooKeeper會在進行若干次事務日誌記錄之後,將記憶體資料庫的全最資料Dump到本地檔案中,這個過程就是資料快照。可以使用snapCount引數來配置每次資料快照之間的事物操作次數,即ZooKeeper會在snapCount次事務日誌記錄後進行一個數據快照。

資料快照的過程

1.確定是否需要進行資料快照。

每進行一次事務日誌記錄之後,ZooKeeper都會檢測當前是否需要進行資料快照。 理論上進行snapCount次事務操作後就會開始資料快照,但是考慮到資料快照對ZooKeeper所在機器的整體效能的影響,需要儘量避免ZooKecper叢集中的所 有機器在間一時刻進行資料快照。因此ZooKeeper在具體的實現中,並不是嚴格地按照這個策略執行的,而是採取“過半隨機”策略,即符合如下條件就進行資料快照:

logCount> (snapCount / 2 + randRoll)

其中logCount代表了當前已經紀錄的事務日誌數雖,randRoll為1~snapCount/2 之間的隨機數,因此上面的條件就相當於:如果我們配置的snapCount值為預設的100000,那麼ZooKeeper會在50000~100000次事務日誌記錄後進行一次資料快照。

2.切換事物日誌檔案。

滿足上述條件之後,Zookeeper就要開始進行資料快照首先是進行事務日誌檔案的切換。所謂的事務日誌檔案切換是指當前的事務日誌已經“寫滿”(已經寫入了snapCount個事務日誌),需要重新建立一個新的事務日誌。

3.建立資料快照非同步執行緒。

為了保證資料快照過程不影響ZooKeeper的主流程,這裡需要建立一個單獨的非同步執行緒來進行資料快照。

4.獲取全最資料和會話資訊。

資料快照本質上就是將記憶體中的所有資料節點資訊(DataTree)和會話資訊儲存到本地磁碟中去。因此這裡會先從ZKDatabase中獲取到DataTree和會話資訊。

5.生成快照資料檔名。

在“檔案儲存”部分,我們已經提到快照資料檔名的命名規則。在這一步中, ZooKeeper會根據當前已提交的最大zxid來生成資料快照檔名。

6.數椐序列化。

接下來就幵始真正的資料序列化了。在序列化時首先會序列化檔案頭資訊,這裡的檔案頭和事物務日誌中的一致,同樣也包含魔數、版本號和dbid資訊。然後在對會話資訊和DataTree分別進行序列化,同時生成一個Checksum, —並寫入快照資料檔案中去。

1.1.4.  初始化

資料載入的整體流程

 

快照檔案載入流程

 


1.處理快照檔案

完成記憶體資料庫的初始化之後,zookeeper就可以開始從磁碟中恢復資料了。每一個快照資料檔案中都儲存了zookeeper伺服器近似全量的資料,因此首先從這些快照檔案開始載入。

2. 獲取最新的100個快照檔案

—般在ZooKeeper伺服器執行一段時間之後,磁碟上都會保留許多個快照檔案。另外由於毎次資料快照過程中,ZooKeeper都會將全最資料Dump到磁碟快照檔案中,因此往往更新時間最晚的那個檔案包含最新的全量資料。那麼是否我們只需要這個最新的快照檔案就可以了呢?在ZooKeeper的實現中,會獲取最新的至多100個快照檔案(如果磁碟上僅存在不到100個快照檔案,那麼就獲取所有這些快照檔案)。

3.解析快照檔案

獲取到這至多100個檔案之後,ZooKeeper會開始“逐個”進行解析。每個快照檔案都是記憶體資料序列化到磁碟的二進位制檔案,因此在這裡需要對其進行反序列化,生成DataTree物件和sessionsWithTimeouts集合。同時在這個過程中,還會進行檔案的checksum校驗以確定快照檔案的正確性。

需要注意的一點是,雖然獲取到的是100個快照檔案,但其實在“逐個”解析過程中,如果正確性校驗通過的話,那麼通常只會解析最新的那個快照檔案。換句話說,只有當最新的快照檔案不可用的時候,才會逐個進行解析,直到將這100個檔案全部解析完。如果所有快照檔案都解析完後還是無法成功恢復一個完整的DataTree和sessionsWithTimeouts, 則認為無法從磁碟中載入資料,伺服器啟動失敗。

4. 獲取最新的ZXID

完成上面的步驟後,就已經基於快照檔案構建了一個完整的DataTree例項 和sessionsWithTimeouts集合了。此時根據這個快照檔案的檔名就可以解析出一個最新的ZXID: zxid_for_snap,該ZXID代表了ZooKeeper開始進行資料快照的時刻。

5.處理事務日誌

在經過上面的處理過程,此時ZooKeeper伺服器記憶體中已經有了一份近似全量的資料,現在開始就要通過事務日誌來更新增量資料。

6.獲取所有zxid_for_snap之後提交的事務

到這裡,我們已經獲取到快照資料的最新ZXID。ZooKeeper中數椐的快照機制決定了快照檔案中並非包含了所有的事務操作。但是未被包含在快照檔案中的那部分事務操作是可以通過資料訂正來實現的。因此這裡我們只需要從事物日誌中獲取所有ZXID比上步得到的zxid_for_snap大的事務操作。

7.事務應用

獲取到所有ZXID大於zxid_for_snap的事務後,將其逐個應用到之前基於快照資料檔案恢復出來的DataTree和sessionsWithTimeouts中去。

在事務應用的過程中,還有一個細節需要我們注意,每當有一個事務被應用到記憶體資料庫中去後,ZooKeeper同時會回撥PlayBackListener監聽器,將這一事務操作記錄轉換成Proposal,並儲存到ZKDatabase.committedLog中,以便Follower進行快速同步。

1.1.5.  資料同步

ZooKeeper叢集伺服器啟動的過程中提到,整個叢集完成 Leader選舉之後,Learner會向Leader伺服器進行註冊。當Learner伺服器向Leader完成註冊後,就進入資料同步環節,簡單地講,資料同步過程就是Leader伺服器將那些沒存在Learner伺服器上提交過的事務請求同步給Learner伺服器,大體上如下圖所示。


獲取Learner狀態

在註冊Learner的最後階段,Learner伺服器會發送給Leader伺服器一個ACKEPOCH資料包,Leader會從這個資料包中解析出該Learner的currentEpoch和lastZxid。

資料同步初始化

在開始資料同步之前,Leader伺服器會進行資料同步初始化,首先會從ZooKeeper的記憶體資料庫中提取出事務請求對應的提議快取佇列(下面我們用“提議快取佇列”來指代該佇列):proposals,同時完成對以下三個ZXID值的初始化。

l  peerLastZxid:該Learner伺服器最後處理的ZXID。

l  minCommittedLog: Leader伺服器提議快取佇列 committedLog中的最小ZXID。

l  maxConmmittedLog: Leader伺服器提議快取佇列committedLog中的最大ZXID。

ZooKeeper叢集資料同步通常分為四類,分別是直接差異化同步(DIFF同步)、先回滾在差異化同步(TRUNC+DIFF同步)、僅回滾同步(TRUNC同步)和全量同步(SNAP 同步)在初始化階段,Leader伺服器會優先初始化以全量同步方式來同步資料,當然,這並非最終的資料同步方式,在以下步驟中,會根據Leader和Learner伺服器之間的資料差異情況來決定最終的資料同步方式。

直接差異化同步(DIFF同步)

場景:peerLastZxid介於minCommittedLog 和maxCommittedLog 之間

對於這種場景,就使用差異化同步(DIFF同步)方式即可。Leader伺服器會首先向這個Learner傳送一個DIFF指令,用於通知Learner進入差異化資料同步階段,Leader 伺服器即將把一些Proposal同步給自己”。在實際Proposal同步過程中,針對每個Proposal, Leader伺服器都會通過傳送兩個資料包來完成,分別是PROPOSAL內容資料包和COMMIT指令資料包,這和ZooKeeper執行時Leader和Follower之間的事務請求的提交過程是一致的。

舉個例子來說,假如某個時刻Leader伺服器的提議快取佇列對應的ZXID依次是:

0x500000001, 0x500000002, 0x500000003, 0x500000004. 0x500000005

而Learner伺服器最後處理的ZX1D為0x500000003, 於是Leader伺服器就會依次將 0x500000004和0x500000005兩個提議同步給Learner伺服器,同步過程中的資料包傳送順序如下表所示。

傳送順序

資料包型別

對應ZXID

1

PROPOSAL

0x500000004

2

COMMIT

0x500000004

3

PROPOSAL

0x500000005

4

COMMIT

0x500000005

通過以上四個資料包的傳送,Learner伺服器就可以接收到自己和Leader伺服器的所有差異資料。隨後Leader還會立即傳送一個 NEWLEADER指令,用於通知Learner,已經將提議快取佇列中的Proposal都同步給自己了。

下面我們再來看Learner對Leader傳送過來的資料包的處理。根據上面講解的Leader 伺服器的資料包傳送順序,Learner會首先接收到一個DIFF指令,於是便確定了接下來進入DIFF同步階段。然後依次收到上表中的四個資料包,Learner會依次將其應用到記憶體資料庫中。Learner還會接收到來自Leader的NEWLEADER指令,此時Learner就會反饋給Leader —個ACK訊息,表明自己確實完成了對提議快取佇列中Proposal的同步。

Leader在接收到來Learner的這個ACK訊息以後,就認為當前Learner已經完成資料同步,同時進入“過半策略”等待階段——Leader會和其他Learner伺服器進行上述同樣的資料同步流程,直到叢集中有過半的Learner機器響應了Leader這個ACK訊息。一但滿足“過半策略”後,Leader伺服器就會向所有已經完成資料同步的Learner傳送一個UPTODATE指令,用來通知Learner已經完成資料同步,同時叢集中已經有過半機器完成資料同步,叢集已經具備了對外服務的能力了。

Learner在接收到這個來自Leader的UPTODATE指令後,會終止資料同步流程,然後向 Leader再次反饋一個ACK訊息。

這個差異化同步過程中涉及的Leader和Learner之間的資料包通訊如下圖所示。


先回滾再差異化同步(TRUNC + DIFF 同步)

場景:針對上面的場景,我們已經介紹了直接差異化同步的詳細過程。但是在這種場景中,會有一個罕見但是確實存在的特殊場景:設有 A 、 B 、 C 三臺機器,假如某一時刻 B 是 Leader 伺服器,此時的Leader_Epoch為 5 ,同時當前已經被叢集中絕大部分機器都提交的 ZXID 包括: 0x500000001 和 0x500000002 。此時,Leader 正要處理 ZXID : 0x50000003,並且已經將該事務寫入到了 Leader 本地的事務日誌中去,就在 Leader 恰好要將該 Proposal 傳送給其他 Follower 機器進行投票的時候,Leader 伺服器掛了,Proposal沒有被同步出去。此時 zooKeeper 叢集會進行新一輪的 Leader 選舉,假設此次選舉產生的新的 Loader 是 A ,同時Leader_Epoch 變更為 6 ,之後 A 和 C 兩臺伺服器繼續對外進行服務,又提交了 0x600000001 和 0x600000002 兩個事務。此時,伺服器 B 再次啟動,井開始資料同步。

簡單地講,上面這個場景就是 Leader 伺服器在已經將事務記錄到了本地事務日誌中,但是沒有成功發起Proposal流程的時候就掛了。在這個特殊場景中,我們看到, peerLastzxid 、minCommittedLog 和 maxCommittedLog 的值分別是 0x500000003 、 0x500000001和0x600000002.顯然, peerLastzxid介於 minConlmittcdLog 和maxCommittedLog之間。對於這個特殊場景,就使用先回滾再差異化同步(TRUNC +DIFF 同步)的方式。當 Leader 伺服器發現某個 Learner 包含了一條自己沒有的事務記錄,那麼就需要讓該 Learner進行事務回滾,回滾到 Leader 伺服器上存在的,同時也是最接近於 peerLastzxid的ZXID 。在上面這個例子中, Leader 會需要Learner 回滾到 ZXID為0x500000002的事務記錄。

先回滾再差異化同步的資料同步方式在具體實現上和差異化同步是一樣的,都是會將差異化的Proposal傳送給 Learner 。同步過程中的資料包傳送順序如下表所示。

傳送順序

資料包型別

對應ZXID

1

TRUNC

0x500000002

2

PROPOSAL

0x600000001

3

COMMIT

0x600000001

4

PROPOSAL

0x600000002

5

COMMIT

0x600000002

僅回滾同步(RUNC同步)

場景:peerLastzxid大於maxCommittedLog 。這種場景其實就是上述先回滾再差異化同步的簡化模式, Leader 會要求 Learner 回滾到 ZXID 值為maxCommittedLog對應的事務操作,這裡不再對該過程詳細展開講解。

全量同步(SNAP同步)

場景1:peerLastZxid小於minCommittedLog 。

場景2 :Leader伺服器上沒有提議快取佇列, peerLastZxid不等幹 lastProcessedZxid ( Leader伺服器資料恢復後得到的最大ZXID)。上述這兩個場景非常類似,在這兩種場景下, Leader伺服器都無法直接使用提議快取佇列和 Learner進行資料同步,因此只能進行全量同步(SNAP 同步)。

所謂全量同步就是 Leader伺服器將本機上的全量記憶體資料都同步給Learner。Leader伺服器首先向Learner傳送一個 SNAP指令,通知Learner即將進行全量資料同步。隨後, Leader 會從記憶體資料庫中獲取到全量的資料節點和會話超時時間記錄器,將它們序列化後傳輸給Learner 。Learner伺服器接收到該全最資料後,會對其反序列化後載入到記憶體資料庫中。

以上就是ZooKeeper 叢集間機器的資料同步流程了。整個資料同步流程的程式碼實現主要在LearnerHandler和Learner 兩個類中。