Zeppelin:一個分散式KV存儲平台之存儲節點

系列

Zeppelin:一個分散式KV存儲平台之概述zhuanlan.zhihu.com圖標Zeppelin:一個分散式KV存儲平台之元信息節點zhuanlan.zhihu.com圖標

通過上一篇Zeppelin:一個分散式KV存儲平台之概述的介紹,相信讀者已經對Zeppelin有了大致的了解,這篇就將詳細介紹其中的存儲節點集群(Node Server)。存儲節點負責最終的數據存儲,每個Node Server會負責多個分片副本,每個分片副本對應一個DB和一個Binlog。同一分片的不同副本之間會建立主從關係,進行數據同步,並在主節點異常時自動切換。本文將從請求處理、線程模型、元信息變化、副本同步及故障檢測來展開介紹,最後總結在存儲節點的設計開發過程中的兩點啟發。

請求處理

Node Server會與Client直接連接,接受用戶請求,處理過程會經過如下層級:

Client與存儲節點Node之間用Protobuf協議通信,網路模塊會先進行協議解析;之後進入命令的處理層,區分命令類型,判斷合法性及讀寫屬性;寫請求會先寫Rocksdb,之後寫Binlog,Binlog被用來進行副本主從間的數據同步;Rocksdb是LSM(The Log-Structured Merge-Tree)的優秀實現,數據會先被寫入Rocksdb的內存Memtable及Log,並在之後的Compaction中逐步寫入不同層級的SST文件中去。

線程模型

Zeppelin的線程模型如下圖所示:

通過不同的顏色我們將Zeppelin的線程分為四大塊:

  1. 用戶命令處理模塊:包括紅色顯示的Dispatch Thread和Worker Thread,Dispatch線程接受請求,建立連接,並將連接分發給不同的Worker線程;Worker線程與Client通信,接受請求、處理命令、返回結果。
  2. 副本同步模塊:如圖中藍色所示,TrySync線程為所有本節點負責的Slave分片向Master發起數據同步請求;Binlog Sender線程負責Binlog的發送;Binlog Receiver,Receiver Worker負責Binlog的接受和處理,與用戶命令處理模塊類似。
  3. 元信息模塊:綠色所示,包括與Meta進行心跳的Heartbeat線程及拉取元信息並更新本地狀態的MetaCmd線程。
  4. 後台輔助模塊:Binlog Purge線程定時刪除過期的Binlog以維持較小的空間佔用,BgSave and DbSync線程負責分片的備份及與Slave分片的全量數據發送。

元信息變化

當有節點起宕,節點加入退出或創建刪除表等元信息變化時,存儲節點需要感知並作出對應的改變。正常情況下,存儲節點Node與元信息節點Meta之間維持一個心跳,Meta通過心跳向Node發送當前的元信息版本號Epoch,Node向Meta發送當前負責分片的Binlog偏移量。元信息改變時,Node會從心跳得到更大的Epoch,這時Heartbeat線程通知MetaCmd線程向Meta主動發起Pull請求,獲得最新的元信息,之後Node進行對應的主從遷移,分片添加刪除等操作。

副本同步

Zeppelin的副本之間採用非同步複製的方式,由Slave發起建立主從關係,當存儲節點發現自己所負責的分片有主從關係變化時,會觸發Slave向對應的Master發起TrySync請求,TrySync中攜帶Slave當前的Binlog偏移,Master從該偏移順序發送Binlog信息。下圖所示是主從之間配合數據同步的線程關係。

Binlog

Binlog支持尾部的Append操作,由多個固定大小的文件組成,文件編號和文件內偏移一起標記一個Binlog位置。如下圖所示,每條用戶的寫請求被記錄在一個Record中,Record Header記錄了Value的Length,校驗Checksum及類型Type,Type Full表示Record被完整的記錄在一個Block中,First,Middle,Last表示該Record橫跨多個Block,當前是開頭,中間或是結尾的部分。

可以看出每個Record的解析,十分依賴從Header中讀到的Length,那麼當Binlog文件中有一小段損壞時,就會因為無法找到後一條而損失整個Binlog文件,為了降低這個損失,Binlog被劃分為固定大小的Block,每個Block的開頭都保證是一個Record開頭,Binlog損壞時,只需要略過當前Block,繼續後續的解析。

Binlog發送

當主從關係建立以後,Master副本需要不斷的給Slave副本發送Binlog信息。我們之前提到,一個分片都會對應一個Binlog,當有很多分片時,就沒有辦法給每個Binlog分配一個發送線程。因此Zeppelin採用了如下圖所示機制:當前存儲節點所負責的每個Master分片的Binlog發送任務被封裝為一個Task,Task中記錄其對應的Table,分片號,目標Slave節點地址,當前要發送的Binlog位置(文件號加文件內偏移)。所有的Task被排成一個FIFO隊列,固定個數的Binglog發送線程從隊列頭中取出一個Task,服務固定的時間片長度後將其插回隊列尾部。

針對每個Task,Binlog發送線程會從當前的Binlog偏移量發送順序發送Binlog Record的內容給對應的Slave的接受線程,並更新Binlog偏移。

Binlog接收

對應節點的Binlog Receive線程會接受所有來自不同Master分片的Binlog消息,按照分片號分發給多個Binlog Worker,Binlog Worker順序執行Binlog消息,同樣要寫DB及Binlog,從而完成與Master分片的數據同步。

Binlog壓縮及全同步

可以看出Binlog同樣需要佔用大量的磁碟空間,為了不使這種消耗無限增長,Zeppelin設置保留Binlog的時間和個數,並定時清理不需要的Binlog文件,稱為Binlog壓縮。

這帶來了新的問題,當Master收到Trysync請求時,發現Slave的Binlog 偏移量指向的Binlog文件已經被刪除,正常的部分同步無法建立。這時就需要全同步的過程,Master分片會先將當前的DB打一個快照,並將這個快照及快照對應的Binlog位置發送給Slave,Slave替換自己的DB,並用新的Binlog位置發起新的Trysync過程。

Zeppelin利用LSM引擎所有文件寫入後只會刪除不會修改的特性,通過硬鏈實現秒級的快照,同時快照本身也不會佔用過多空間。相關內容可以參考Rocksdb Checkpoint。

Binlog一致

需要注意的是分片副本間主從關係並不穩定,會由於節點的起宕或網路的中斷自動切換,為了保證新的主從關係可以正常建立,我們要求每個Binlog Record的位置在所有的副本看來是一致的,也就是副本間的Binlog一致。Zeppelin採取了如下策略:

  • Binlog檢查拒絕機制:Slave副本檢查Binlog的發送方地址、發送方元信息版本及前一條Binlog的偏移,拒絕錯誤的Binlog請求。這些信息也需要在Master副本所發送的Binlog請求中攜帶。
  • Trysync偏移回退機制:當Master副本收到Trysync的偏移大於自己或者不合法時,需要通知對方回退到一個指定的合法的位置,以完成主從關係的正常建立。這種情況會發生在頻繁的副本主從切換。
  • Master觸發Skip機制:Master副本發現Binlog損壞時,會略過一個或多個Block,為了保證Binlog一致,此時Master需要強制要求所有的Slave略過同樣長度的Binlog。通過特殊的Skip命令來完成這個任務。Slave的Binlog會填充同樣長度的一段類型為Empty的空白內容。

故障檢測

節點異常

節點異常時,元信息節點會感知並完成需要的主從切換,並通知所有的存儲節點,發生變化的節點會進行狀態遷移並建立新的主從關係。

主從鏈接異常檢測及恢復

為了副本複製的高效,Binlog的發送採用單向傳輸,避免了等待Slave的確認信息,但這樣就無法檢測到主從之間鏈接的異常。Zeppelin復用了BInlog發送鏈路來進行異常檢測,如下圖所示,左邊為Master節點,右邊為Slave節點:

  • Slave副本維護一個Master的超時時間和上一次通信時間,收到合法的Binlog請求或Lease命令會更新通信時間。否則,超時後觸發TrySync Moudle發起新的主動同步請求。Master在收到新的TrySync請求後會用新的Binlog發送任務替換之前的,從而恢復Binlog同步過程。
  • Master動態更新Slave超時時間:由於我們用固定數量的Binlog Sender負責所有分片的Binlog發送,上面提到,當某個發送任務的時間片用完後會被放回到任務隊列等待下一次處理,當Master負載較高時這個間隙就會變長。為了不讓Slave無效的觸發TrySync,每次時間片用完被放回任務隊列前,Master都會向Slave發送Lease命令,向Slave刷新自己的超時時間。這個超時是通過Master節點的當前負載動態計算的:

Timeout = MIN((TaskCount * TimeSlice / SenderCount + RedundantTime), MinTime)

Lessons We Learn

1,限制資源線性增長

分片個數是Zeppelin的一個很重要的參數,為了支持更大的集群規模,需要更多的分片數。而因為分片是數據存儲,同步,備份的最小單位,分片數的增多勢必會導致資源的膨脹,Zeppelin中做了很多設計來阻止這種資源隨分片數的線性增長:

  • 減少Binlog發送線程數:通過上面介紹Task Pool及Slave的動態租約來限制Binlog發送線程數;
  • 限制Rocksdb實例增多帶來的資源壓力:通過多實例公用Option來實現共享Flush,Compact線程,內存配額等;
  • 減少心跳信息:通過DIFF的方式來減少Node與Meta之間交互的分片Binlog Offset信息。

2,非同步比同步帶來更多的成本

無論是副本同步還是請求處理,非同步方式都會比同步方式帶來更好的性能或吞吐。而通過上面副本同步部分的介紹可以看出,由於採用了非同步的副本同步方式,需要增加額外的機制來保證Binlog一致,檢測鏈路異常,這些都是在同步場景下不需要的。給了我們一個啟發就是應該更慎重的考慮非同步選項。

之後在Zeppelin不是飛艇之元信息節點中,將詳細介紹Zeppelin的另一個重要角色Meta。

參考

Zeppelin

淺談分散式存儲系統數據分布方法

Zeppelin不是飛艇之概述

Zeppelin不是飛艇之元信息節點

The Log-Structured Merge-Tree

Rocksdb Checkpoint


推薦閱讀:

分散式資源管理
ZAB協議在Zookeeper中的實現
搞分散式,大數據都寫些什麼代碼,是不是寫不了幾行?
到現在為止,NoSQL運動給資料庫系統留下什麼寶貴的思想?

TAG:分布式系统 | 分布式存储 | 主从复制 |