《 ZooKeeper : Wait-free coordination for Internet-scale systems 》論文研讀
Zookeeper 研讀
說明:本文為論文 《 ZooKeeper : Wait-free coordination for Internet-scale systems 》 的個人理解,難免有理解不到位之處,歡迎交流與指正 。
論文地址:Zookeeper Paper
1. Zookeeper 介紹
Zookeeper 是用來協調分散式應用的服務框架,它是一個通過冗餘容災的伺服器叢集,提供 API 給 client ,用以實現一些 原語( 如配置管理、成員管理、領導人選舉、分散式鎖等 ),在這些原語的基礎上可以實現一些分散式應用程式( 如 GFS 、MapReduce 、VM-FT 的 test-and-set server
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 的分佈( 元資料配置 ),處理系統中機器的故障( 故障檢測 和 組成員管理 )以及控制系統操作。