準備工作
爲什麼要閱讀這篇文章
Zookeeper是廣泛使用的主從複製狀態機服務
受Chubby(Google的全局鎖定服務)啓發
最初在Yahoo得到應用,後來在Mesos, HBase廣泛使用
Apache 開源項目
主從複製的案例研究
API支持廣泛的用例
性能優越
發展Zookeeper的動機
服務集羣中的許多應用程序需要協調通信
例如對於GFS服務需要master存儲每一個塊服務器的列表,master決定哪一個塊是primary等。
應用程序之間需要相互發現
MapReduce架構中需要了解GFS master的ip和port
性能優越
以lab3中3個節點的raft算法作對比,需要執行2次磁盤寫入和一次消息往返,對於磁盤,大約50 msg/sec。對於ssd,大約200 msg/sec
但是Zookeeper能處理大約21,000 msg/sec,源於client允許異步調用機制,以及pipelining消息處理。
Zookeeper的替代方案:爲每一個應用程序開發容錯的master服務
根據DNS標識ip與端口
處理容錯
高性能
Zookeeper設計: 通用的協調服務
設計挑戰
API的設計
如何使master容錯
如何獲得良好的表現
基本設計
主從狀態機
主從複製的是znodes對象
znode是客戶端通過ZooKeeper API處理的數據對象,znodes以路徑命名,其通過分層的名稱空間組織,類似於unix的文件系統
znode 分層命名空間
分層的名稱空間是組織數據對象的一種理想方式,因爲用戶已經習慣了這種抽象,並且可以更好地組織應用程序元數據。
znodes包含應用程序的元數據(配置信息、時間戳、版本號)
znodes的類型:Regular(客戶端通過顯式創建和刪除常規znode來操作它們),empheral(客戶端創建了此類znode,它們要麼顯式刪除它們,要麼讓系統在創建它們的會話終止時(故意或由於失敗)自動將其刪除)
爲了引用給定的znode,我們使用標準的UNIX符號表示文件系統路徑。 例如,我們使用/A/B/C表示 znode C的路徑,其中C的父節點爲B,B的父節點爲A,除empheral節點外,所有節點都可以有子節點
znode命名規則: name + 序列號。 如果n是新的znode,p是父znode,則n的序列值永遠不會小於在p下創建的任何其他znode名稱中的序列值
ZooKeeper的數據模型本質上是一個具有簡單API且只能讀取和寫入完整數據的文件系統,或者是具有層次結構鍵的鍵/值表。 分層名稱空間對於爲不同應用程序的名稱空間分配子樹以及設置對這些子樹的訪問權限很有用。
會話(session)
客戶端連接上zookeeper時會初始化會話
會話允許在故障發生時,客戶端請求轉移到另一個服務(client知道最後完成操作的術語和索引)
會話有時間限制,client必須持續刷新會話(通過心跳檢測)
znodes上的操作
create(path, data, flags)
delete(path, version) if znode.version = version, then delete
exists(path, watch)
getData(path, watch)
setData(path, data, version) if znode.version = version, then update
getChildren(path, watch)
sync() 除此操作的其他操作是異步的,每個client的所有操作均按FIFO順序排序。同步會一直等到之前的所有操作都認可爲止
順序保證
所有寫操作都是完全有序的
ZooKeeper會對所有client發起的寫操作做全局統一排序
每一個client的操作都是FIFO順序的。
read操作能夠感知到相同客戶端的其他寫操作
read操作能夠感知之前的寫操作針對相同的znode
Zookeeper用例:ready znode與配置改變
ZooKeeper中,新的leader可以將某個path指定爲ready znode。 其他節點將僅在該znode存在時使用配置。
當leader 重建配置之後,會通知其他副本重建配置,並新建ready znode.
副本爲了防止出現不一致,必須在重建配置時,處理完其之前的所有事務。保證所有服務的狀態一致。
任何一個副本更新失敗,都不能夠應用都需要進行重試。
Zookeeper用例:鎖
下面的僞代碼向我們鎖的實現。通過create試圖持有鎖,如果鎖已經被其他的client持有,則通過watch方式監控鎖的釋放。
acquire lock:
retry:
r = create("app/lock", "", empheral)
if r:
return
else:
getData("app/lock", watch=True)
watch_event:
goto retry
release lock:
delete("app/lock")
由於上面的僞代碼可能會出現羊羣效應,可以嘗試下面的方式
znode下方的children中,序號最低的的是持有鎖的
其他在等待的client只watch前一個znode的變化,避免了羊羣效應
acquire lock:
n = create("app/lock/request-", "", empheral|sequential)
retry:
requests = getChildren(l, false)
if n is lowest znode in requests:
return
p = "request-%d" % n - 1
if exists(p, watch = True)
goto retry
watch_event:
goto retry
Zookeeper簡化程序構建但其不是最終的解決方案
應用程序還有許多需要解決的問題
例如如果我們要在GFS中使用Zookeeper,那麼我們還需要
chunks的副本方案
primary失敗的協議
…
但是使用了Zookeeper,至少可以使master容錯,不會發生網絡分區腦裂的問題
Zookeeper實現細節
和lab3相似,具有兩層
ZooKeeper 服務層 (K/V 層)
ZAB 層 (Raft 層)
Start() 在底層執行插入操作
隨後,ops從每個副本服務器的底層彈出,這些操作按照彈出的順序提交(commited),在lab3中使用apply channel,在ZAB層中,通過調用abdeliver()
挑戰:處理重複的客戶端請求
場景:primary收到客戶端請求後,返回失敗,客戶端進行重試
在lab3中,我們使用了map來解決重複的請求問題,但是每一個客戶端是堵塞的,只能夠等待完成才能進行下一個
在Zookeeper中,在一段時間內的操作是冪等的,以最後一次操作爲準
挑戰: 讀取操作的效率
大部分的操作都是讀取操作,他們不修改狀態
讀取操作是否必須通過ZAB層?
任何副本服務器都可以執行讀取操作?
如果讀取操作通過Raft/ZAB層,則性能會降低
讀取操作如果不通過Raft/ZAB層、可能會返回過時的數據
Zookeeper解決方案:允許返回過時的數據
讀取可以由任何副本執行
讀取吞吐量隨着服務器數量的增加而增加
讀取返回它看到的最後一個zxid
只有sync-read() 保證數據不過時
read操作會返回zxid,zxid包含了部分客戶端對於此read請求相對應的write請求的順序,從而客戶端能夠了解此server操作是否落後於client。
心跳檢測以及建立session時都會返回zxid,當客戶端連接服務器時,通過zxid對比保證client與server的狀態足夠接近
總結
Zookeeper通過將wait-free對象(znode對象)暴露給客戶端解決分佈式系統中的進程協調問題
Zookeeper保證了write操作的線性一致性以及客戶端操作的FIFO順序
Zookeeper通過允許讀取操作返回過時數據實現了每秒數十萬次操作的吞吐量值,適用於多讀而少些的場景
Zookeeper仍然提供了保證讀一致性的sync操作
Zookeeper具有強大的API功能用於多樣的應用場景,並且內在提供主從容錯機制,在包括雅虎在內的多家公司廣泛應用