Zookeeper 研讀
說明:本文為論文 《 ZooKeeper : Wait-free coordination for Internet-scale systems 》 的個人理解,難免有理解不到位之處,歡迎交流與指正 。
論文地址:Zookeeper Paper
1. Zookeeper 介紹
Zookeeper 是用來協調分布式應用的服務框架,它是一個通過冗余容災的服務器集群,提供 API 給 client ,用以實現一些 原語( 如配置管理、成員管理、領導人選舉、分布式鎖等 ),在這些原語的基礎上可以實現一些分布式應用程序( 如 GFS 、MapReduce 、VM-FT 的 test-and-set server 以及雅虎的 Fetching Service 、Katta 、YMB 等 )。
1.1 Zookeeper 服務實現
Zookeeper 通過在集群中每台服務器上復制 Zookeeper 數據來提供高可用性 。集群由一個 leader 和 多個 follower 組成 ,leader 負責進行投票的發起和決議、更新系統狀態,follower 在選舉 leader 的過程中參與投票。
每個服務器都可以連接客戶端,客戶端連接到一個服務器,建立 Session 。Zookeeper 使用 timeout 來檢測 session 是否還在,如果 client 在一定時間內無法與服務器通信,則連接到其他服務器重新建立 session。
一台服務器上的組件構成如上圖所示 。client 與 服務器通過 TCP 連接來發送請求。
如果是 讀請求 :
- 則直接在該服務器本地讀取數據,可能讀到過時數據
- 若讀請求之前有
sync
,則必然讀到最新數據
如果是 寫請求:
- 將寫請求轉發至 leader
- Request Processor 對請求做准備。leader 使用 Atomic Broadcast 將寫請求廣播給 follower ( 寫請求被排序為 zxid ),具體是使用 ZAB ( 一種原子廣播協議 )
- leader 得到多數回復之后,將寫請求應用到 Replicated Database 中,最后將該寫請求應用到所有 follower 上
- 為了可恢復性,強制在寫入內存數據庫之前將 white-ahead log 寫入磁盤,並周期性地為內存數據庫生成快照
服務器每次處理請求后,都會將 zxid 返回給 client ,若 client 連接一個新的服務器,新服務器會通過檢查 client 的最后一個 zxid 保證服務器的最后一個 zxid 至少和其一樣新,否則服務器在趕上 client 之前不會建立 session 。client 會連接一個具有最新視圖的服務器。
1.2 Zookeeper 數據模型
Repblicated Database 是一個內存數據庫,存儲着一個層次性的文件系統,即一個數據樹,每一個節點稱為 znode 。
znode 並不用於通用數據存儲,而是用來存儲 client 引用的數據( 用於協調的元數據 )。一個 client 可以通過 API 來操縱 znode ,如上圖中 app1 實現了一個簡單的組成員身份協議:每個 client 進程 pi 在 /app1 下創建了一個 znode pi ,只要該進程正在運行,該節點便會持續存在 。
znode 還將元數據與 timestamp 和 version counter 關聯,這使 client 可以跟蹤對 znode 的更改並根據 znode 的版本執行條件更新 。
client 可以創建三種 znode:
Regular
:client 顯式地操縱和刪除 regular znodesEphemeral
:這種節點可以被顯式地刪除,也可以在創建這個節點的 session 斷開時自動刪除。該節點不能擁有子節點Sequential
:client 創建 znode 時設置 sequential flag ,該節點名字后會添加一個單調遞增的序號
1.3 Fuzzy Snapshots
Zookeeper 使用周期定時快照,它不會阻塞地等待快照生成,而是一邊生成快照,一邊應用接收到的新的寫請求 。這些新的寫請求會部分地寫入到快照 ,所以快照生成的時間點是不確定的 。
所以當服務器重新啟動、從快照恢復后,會重復執行一些 log 中的寫請求 。但由於 Zookeeper 狀態變更是冪等的,所以只要按照狀態變更的順序應用狀態改變,就不會產生錯誤的結果 。
1.4 Zookeeper 特性
wait-free data objects
:zookeeper 可執行一個 client 的請求,無需等待別的 client 采取什么行動watch mechanism
:通過 watch 機制,client 不需輪詢就可以接收到某 znode 更新的通知 。watch 表明發生了更改,但不提供更改的內容linearizable writes
:來自所有 client 的所有寫操作都是可線性化的FIFO client order
:zookeeper 對於一個 client 的請求,嚴格按照該 client 發送請求的順序執行
事實上,zookeeper 的 client 經常通過 watch 機制來等待別的 znode 發生更新
對於全局寫操作,所有的寫操作都是可線性化的,即在寫操作上保持了強一致性。這種可線性化實際上是異步可線性化,允許一個 client 有多個未完成的操作 。
但是對於全局讀操作,zookeeper 並未提供可線性化,而是提供了較弱的一致性,允許 client 從它連接的服務器上直接讀取到數據,這樣的數據被允許是過期的。這是 zookeeper 性能高 的關鍵所在,因為大多數分布式應用都是讀操作占比更高,允許 client 從連接服務器的本地數據庫讀取數據,使得性能與集群中服務器數量成正比,大大提高了 zookeeper 系統的伸縮性 。
然而這種弱一致性也並非是無限制的,例如 write(a,1)->write(a,2)->write(a,3)
,假設集群中大多數服務器已將 a 更新為 3,某 client 連接的服務器上 a 仍為 2 ,此時該 client 發送讀請求,讀到 2 是被允許的;但是讀到 2 之后,下一個讀請求讀到 1 的情況是不被允許的,即一個 client 讀的結果順序不能違背數據的更新順序 。所以 zookeeper 保證對於一個 client ,FIFO 地執行其請求,即對於一個 client 的請求實現了可線性化 。
如果想保證一次讀取的數據必須是最新的數據,在讀請求之前發送 sync
即可 。sync
使該服務器所有的更新全部寫入副本。( 類似於 flush )
1.5 Zookeeper API
create(path, data, flags)
:根據路徑名和存儲的數據創建一個 znode ,flags 指定 znode 類型delete(path, version)
:版本號匹配情況下刪除 path 下的 znodeexists(path,watch)
:如果 path 下 znode 存在,返回 true;否則返回 false 。watch 標志可以使 client 在 znode 上設置 watchgetData(path, watch)
:返回 znode 的數據setData(path, data, version)
:版本匹配前提下,將 data 寫入 znodegetChildren(path, watch)
:返回 path 對應 znode 的子節點集合sync(path)
:等待 sync 之前的所有更新應用到 client 連接的服務器上
client 進行更新操作時,會攜帶上次獲取到的 version 值發起請求,若請求的 version 號與 server 的不匹配,說明該數據已被別的 client 更新,則更新將失敗 。
所有的方法在 API 中都有一個同步版本和一個異步版本 。當應用程序執行單個 zookeeper 操作且沒有並發執行的任務時,使用同步 API ;異步 API 可以並行執行多個未完成的 zookeeper 操作 。
2. 原語實現
2.1 配置管理
配置被存儲在 znode zc
中,啟動進程將 watch
標志設為 true 來讀取 zc
以獲得其配置。zc
有任何更新,都會通知進程,進程讀取新配置。
2.2 匯合 (Rendezvous)
client 要啟動一個 master 進程和多個 worker 進程時,因為啟動進程由調度器完成,事先不知道 master 的地址和端口。
client 將 rendevzous znode
整個路徑作為啟動參數傳給 master 和 worker 進程。master 啟動時,把自己的地址和端口信息填充至 zr
,watch 機制通知 worker 這些信息,兩者就可以建立連接了。將 zr
設為 ephemeral
節點,還可以通過 watch 機制判斷 client 連接是否斷開。
2.3 組成員管理
指定一個 znode zg
代表 group ,一個 group 成員啟動時便在 zg
下創建一個 ephemeral znode
。進程可以將進程信息,進入該進程使用的地址、端口等數據存入子 znode 中。
2.4 互斥鎖
client 通過創建 lock znode
來獲取鎖,若該 znode 已存在,則等待別的 client 釋放鎖。client 釋放鎖時即為刪除該 znode 。
lock():
while true:
if create("lf", ephemeral = true), exit
if exists("lf", watch = true)
wait for notification
unlock():
delete("lf")
由於鎖被釋放后,會有多個 client 同時爭奪該鎖,這樣就導致了 Herd Effect
。
2.5 沒有 Herd Effect 的互斥鎖
lock():
1 - n = create(l + "/lock-", EPHEMERAL|SEQUENTIAL)
2 - c = getChildren(l, false)
3 - if n is lowest znode in C, exit
4 - p = znode in C ordered just before n
5 - if exits(p, true) wait for watch event
6 - goto 2
unlock():
1 - delete(n)
排隊所有請求鎖的 client , client a
只 watch 它的前一個 client b
的 znode。當 b
的 znode 刪除后,它可能是釋放了鎖,或者是申請鎖的請求被放棄,此時再判斷 a
是否是隊列中的第一個,若是,則獲取鎖。
釋放鎖則是簡單地刪除對應的 znode
。
2.6 讀寫鎖
write lock():
1 - n = create(l + "write-", EPHEMERAL|SEQUENTIAL)
2 - c = getChildren(l, false)
3 - if n is lowest znode in C, exit
4 - p = znode in C ordered just before n
5 - if exits(p, true) wait for event
6 - goto 2
read lock():
1 - n = create(l + "read-", EPHEMERAL|SEQUENTIAL)
2 - getChildren(l, false)
3 - if no write znodes lower than n in C, exit
4 - p = write znode in C ordered just before n
5 - if exits(p, true) wait for event
6 - goto 3
2.7 Double Barrier
double barrier 使 client 能夠同步計算的開始和結束。當進入 barrier 的進程數量大於一個閾值時,這些進程會同時開始計算,並在計算后離開 barrier 。
在 zookeeper 中用 znode b
表示 barrier ,每個進程 p 在進入 barrier 時在 b
下創建子節點。當子節點數超過閾值后,各進程通過 watch 機制開始計算。在計算結束后刪除該節點 。
3. Zookeeper 的應用
3.1 The Fetching Service
FS 是雅虎爬蟲的一部分,它有 master 進程用來控制頁面爬取。FS 使用 zookeeper 的主要優點是可以使用主備容災提高可用性,並且可以將 client 與服務器分離,允許 client 直接從 zookeeper 讀取狀態即可將請求定向到正常的服務器。因此 FS 主要使用到的原語有:元數據配置 和 領導選舉 。
3.2 Katta
Katta 是使用 zookeeper 進行協調的分布式索引器。Katta 使用 zookeeper 跟蹤 master 和 slave 的狀態( 組成員管理 ),處理 master 故障轉移( 領導選舉 ),並跟蹤分片的分配以及將其傳播給 slave ( 配置管理 )。
3.3 Yahoo! Message Broker
YMB 是一個分布式的 publish-subscribe 系統。該系統管理着數千個 topics,client 可以向其發布消息或從中接收消息。topics 分布在一組服務器之間。
YMB 使用 zookeeper 來管理 topics 的分布( 元數據配置 ),處理系統中機器的故障( 故障檢測 和 組成員管理 )以及控制系統操作。