Apache > ZooKeeper
 

ZooKeeper 程式設計人員指南

開發使用 ZooKeeper 的分散式應用程式

簡介

這份文件是給希望建立分散式應用程式以利用 ZooKeeper 協調服務的開發人員的指南。它包含概念性和實務資訊。

本指南的前四個章節介紹了各種 ZooKeeper 概念的高階討論。這些對於了解 ZooKeeper 的運作方式以及如何使用它都是必要的。它不包含原始碼,但假設讀者熟悉與分散式運算相關的問題。第一組中的章節包括

接下來的四個章節提供實用的程式設計資訊。這些包括

本書最後附有 附錄,其中包含其他有用的 ZooKeeper 相關資訊的連結。

本文檔中的大部分資訊都寫成可作為獨立參考文件存取。但是,在開始您的第一個 ZooKeeper 應用程式之前,您可能至少應該閱讀有關 ZooKeeper 資料模型ZooKeeper 基本操作 的章節。

ZooKeeper 資料模型

ZooKeeper 具有階層式命名空間,很像分散式檔案系統。唯一的差別是命名空間中的每個節點都可以有與之關聯的資料以及子節點。這就像有一個檔案系統,允許檔案同時也是目錄。節點的路徑總是表示為標準、絕對、以斜線分隔的路徑;沒有相對參照。路徑中可以使用任何 Unicode 字元,但須符合下列限制

ZNodes

ZooKeeper 樹中的每個節點稱為「znode」。Znode 維護一個統計結構,其中包括資料變更的版本號和 ACL 變更。統計結構也有時間戳記。版本號與時間戳記一起允許 ZooKeeper 驗證快取並協調更新。每次 znode 的資料變更時,版本號就會增加。例如,每當客戶端擷取資料時,也會收到資料的版本。當客戶端執行更新或刪除時,它必須提供要變更的 znode 的資料版本。如果它提供的版本與資料的實際版本不符,更新將會失敗。(可以覆寫此行為。)

注意

在分散式應用程式工程中,字詞節點可以指一般的主機、伺服器、整體的成員、用戶端程序等。在 ZooKeeper 文件中,z 節點是指資料節點。伺服器是指組成 ZooKeeper 服務的機器;法定人數對等節點是指組成整體的伺服器;用戶端是指使用 ZooKeeper 服務的任何主機或程序。

z 節點是程式設計師存取的主要實體。它們有幾個值得在此處提到的特徵。

監控

用戶端可以在 z 節點上設定監控。對該 z 節點的變更會觸發監控,然後清除監控。當監控觸發時,ZooKeeper 會傳送通知給用戶端。有關監控的更多資訊,請參閱區段 ZooKeeper 監控

資料存取

儲存在名稱空間中每個 z 節點的資料會以原子方式讀取和寫入。讀取會取得與 z 節點關聯的所有資料位元組,而寫入會取代所有資料。每個節點都有存取控制清單 (ACL),用於限制誰可以執行什麼動作。

ZooKeeper 並非設計為一般資料庫或大型物件儲存。相反地,它管理協調資料。此資料可以組態、狀態資訊、集合等形式呈現。各種協調資料的共同屬性是它們相對較小:以千位元組為單位。ZooKeeper 用戶端和伺服器實作有健全性檢查,以確保 z 節點的資料少於 1M,但平均而言資料應該遠小於此。操作相對較大的資料大小會導致某些作業花費的時間遠多於其他作業,而且會影響某些作業的延遲,因為需要額外時間才能將更多資料移至網路和儲存媒體上。如果需要大型資料儲存,處理此類資料的通常模式是將其儲存在大量儲存系統上,例如 NFS 或 HDFS,並將指向儲存位置的指標儲存在 ZooKeeper 中。

短暫節點

ZooKeeper 也有短暫節點的概念。這些 z 節點存在於建立 z 節點的階段處於活動狀態的時間內。當階段結束時,z 節點會被刪除。由於此行為,短暫 z 節點不允許有子節點。可以使用 getEphemerals() API 擷取階段的短暫清單。

getEphemerals()

擷取由特定路徑的階段性節點所建立的階段性節點清單。如果路徑為空,它將列出該階段性節點的所有階段性節點。使用案例 - 範例使用案例可能是,如果需要收集階段性節點清單以進行重複資料輸入檢查,且節點是以順序建立,因此您不知道重複檢查的名稱。在這種情況下,getEphemerals() api 可用於取得階段性節點清單。這可能是服務發現的典型使用案例。

序列節點 -- 唯一命名

在建立 znode 時,您也可以要求 ZooKeeper 將單調遞增的計數器附加至路徑的結尾。此計數器是父 znode 所獨有的。計數器的格式為 %010d -- 也就是 10 個數字,並以 0 (零) 補齊(計數器以這種方式格式化以簡化排序),亦即「0000000001」。請參閱 佇列配方 以取得此功能的範例使用方式。注意:用於儲存下一個順序號碼的計數器是由父節點維護的已簽署 int (4 位元組),當計數器遞增超過 2147483647 時,將會溢位(導致名稱為「-2147483648」)。

容器節點

已在 3.5.3 中新增

ZooKeeper 具有容器 znode 的概念。容器 znode 是特殊用途的 znode,對於領導者、鎖定等配方很有用。當容器的最後一個子項被刪除時,容器將成為伺服器在未來某個時間點刪除的候選對象。

根據此屬性,您應該準備好在容器 znode 內建立子項時取得 KeeperException.NoNodeException。亦即,在容器 znode 內建立子 znode 時,請務必檢查 KeeperException.NoNodeException,並在發生時重新建立容器 znode。

TTL 節點

已在 3.5.3 中新增

在建立 PERSISTENT 或 PERSISTENT_SEQUENTIAL znode 時,您可以選擇為 znode 設定 TTL(以毫秒為單位)。如果 znode 在 TTL 內未修改且沒有子項,它將成為伺服器在未來某個時間點刪除的候選對象。

注意:TTL 節點必須透過系統屬性啟用,因為它們在預設情況下會被停用。請參閱 管理員指南 以取得詳細資訊。如果您嘗試在未設定適當系統屬性的情況下建立 TTL 節點,伺服器將擲回 KeeperException.UnimplementedException。

ZooKeeper 中的時間

ZooKeeper 以多種方式追蹤時間

ZooKeeper 統計結構

ZooKeeper 中每個 znode 的 Stat 結構由下列欄位組成

ZooKeeper 會話

ZooKeeper 應用程式透過使用語言繫結建立服務的控制代碼,與 ZooKeeper 服務建立階段。一旦建立,控制代碼會從 CONNECTING 狀態開始,而應用程式程式庫會嘗試連線到組成 ZooKeeper 服務的其中一個伺服器,此時它會切換到 CONNECTED 狀態。在正常運作期間,應用程式控制代碼會處於這兩個狀態的其中一個。如果發生無法復原的錯誤,例如階段到期或驗證失敗,或者如果應用程式明確關閉控制代碼,控制代碼將移至 CLOSED 狀態。下圖顯示 ZooKeeper 應用程式的可能狀態轉換

State transitions

若要建立應用程式階段,應用程式程式碼必須提供連線字串,其中包含以逗號分隔的 host:port 配對清單,每個配對都對應到 ZooKeeper 伺服器(例如「127.0.0.1:4545」或「127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002」)。ZooKeeper 應用程式程式庫會挑選一個任意的伺服器並嘗試連線到它。如果此連線失敗,或者如果應用程式因任何原因與伺服器斷開連線,應用程式會自動嘗試清單中的下一個伺服器,直到連線(重新)建立。

3.2.0 中新增:也可以將選用的「chroot」字尾附加到連線字串。這會在將所有路徑相對於此根目錄來詮釋時執行應用程式指令(類似於 unix chroot 指令)。如果使用,範例會如下所示:「127.0.0.1:4545/app/a」或「127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/app/a」,其中應用程式會根植於「/app/a」,而所有路徑都會相對於此根目錄 - 例如取得/設定/等等...「/foo/bar」會導致在「/app/a/foo/bar」上執行操作(從伺服器觀點來看)。此功能在多租戶環境中特別有用,其中特定 ZooKeeper 服務的每個使用者都可以根植於不同的位置。這使得重新使用變得更簡單,因為每個使用者都可以將他的/她的應用程式編碼為根植於「/」,而實際位置(例如 /app/a)可以在部署時確定。

當客戶端取得 ZooKeeper 服務的控制權時,ZooKeeper 會建立一個以 64 位元數字表示的 ZooKeeper 會話,並將其指定給客戶端。如果客戶端連線到不同的 ZooKeeper 伺服器,它會將會話 ID 作為連線握手的一部分傳送。作為安全措施,伺服器會為任何 ZooKeeper 伺服器都能驗證的會話 ID 建立密碼。當客戶端建立會話時,密碼會隨會話 ID 一起傳送給客戶端。每當客戶端使用新的伺服器重新建立會話時,它會將此密碼與會話 ID 一起傳送。

建立 ZooKeeper 會話的 ZooKeeper 函式庫呼叫參數之一是會話逾時(以毫秒為單位)。客戶端傳送請求的逾時,伺服器回應它能給予客戶端的逾時。目前的實作要求逾時至少為 tickTime 的 2 倍(如伺服器設定中所設定),且最多為 tickTime 的 20 倍。ZooKeeper 函式庫 API 允許存取協商的逾時。

當客戶端(會話)與 ZK 服務叢集分隔時,它會開始搜尋會話建立期間指定的伺服器清單。最後,當客戶端與至少一個伺服器之間的連線重新建立時,會話將會再次轉換為「已連線」狀態(如果在會話逾時值內重新連線)或轉換為「已過期」狀態(如果在會話逾時後重新連線)。不建議為中斷建立新的會話物件(新的 ZooKeeper.class 或 c 繫結中的 zookeeper 控制權)。ZK 函式庫會為您處理重新連線。特別是,我們已在函式庫中內建啟發法,以處理「羊群效應」等事項。只有在收到會話過期通知時(強制性)才建立新的會話。

會話過期是由 ZooKeeper 叢集本身管理,而非客戶端。當 ZK 客戶端與叢集建立會話時,它會提供上述的「逾時」值。叢集使用此值來判斷客戶端的會話何時過期。當叢集在指定的會話逾時期間未收到客戶端的回應時(即無心跳),就會發生過期。在會話過期時,叢集會刪除該會話所擁有的任何/所有暫時節點,並立即通知任何/所有已連線客戶端變更(任何正在監控這些 z 節點的客戶端)。此時,已過期會話的客戶端仍與叢集中斷連線,直到/除非它能夠重新建立與叢集的連線,否則它不會收到會話過期通知。客戶端會保持中斷狀態,直到與叢集重新建立 TCP 連線,此時已過期會話的監控程式會收到「會話已過期」通知。

已過期會話的監控程式所見的已過期會話狀態轉換範例

  1. '已連線':已建立工作階段,且客戶端正在與叢集通訊(客戶端/伺服器通訊正常運作)
  2. .... 客戶端已與叢集分區
  3. '已斷線':客戶端已失去與叢集的連線
  4. .... 時間流逝,在'逾時'期間後,叢集會使工作階段過期,客戶端不會看到任何訊息,因為它已與叢集斷線
  5. .... 時間流逝,客戶端重新獲得與叢集的網路層級連線
  6. '已過期':最後,客戶端重新連線到叢集,然後會收到過期通知

ZooKeeper 工作階段建立呼叫的另一個參數是預設監控器。當客戶端發生任何狀態變更時,會通知監控器。例如,如果客戶端失去與伺服器的連線,客戶端會收到通知,或者如果客戶端的工作階段過期,等等... 此監控器應將初始狀態視為斷線(即在客戶端程式庫將任何狀態變更事件傳送給監控器之前)。對於新連線,傳送給監控器的第一個事件通常是工作階段連線事件。

客戶端傳送的請求會讓工作階段保持作用中。如果工作階段閒置一段時間,會讓工作階段逾時,客戶端會傳送 PING 請求以讓工作階段保持作用中。此 PING 請求不僅讓 ZooKeeper 伺服器知道客戶端仍處於活動狀態,也讓客戶端驗證它與 ZooKeeper 伺服器的連線仍處於活動狀態。PING 的計時保守到足以確保有合理的時間來偵測已中斷的連線,並重新連線到新的伺服器。

一旦成功建立與伺服器的連線(已連線),當執行同步或非同步作業,且符合下列其中一項條件時,基本上有兩種情況會讓客戶端程式庫產生 connectionloss(c 繫結中的結果代碼,Java 中的例外狀況 -- 請參閱 API 文件以取得特定繫結的詳細資料)

  1. 應用程式在已不再作用中/無效的工作階段上呼叫作業
  2. 當有待處理作業傳送到該伺服器時,ZooKeeper 客戶端會與伺服器斷線,亦即,有待處理的非同步呼叫。

3.2.0 中新增 -- SessionMovedException。有一個內部例外,一般來說客戶端不會看到,稱為 SessionMovedException。此例外發生是因為在一個連線中收到一個要求,而該連線的階段已在不同的伺服器上重新建立。此錯誤的正常原因是客戶端傳送一個要求給伺服器,但網路封包延遲,因此客戶端逾時並連線到新的伺服器。當延遲的封包抵達第一個伺服器時,舊伺服器會偵測到階段已移轉,並關閉客戶端連線。客戶端通常不會看到此錯誤,因為他們不會從那些舊連線中讀取資料。(舊連線通常會關閉。)一個可能會看到此狀況的情況是,當兩個客戶端嘗試使用已儲存的階段 ID 和密碼重新建立相同的連線時。其中一個客戶端會重新建立連線,而第二個客戶端會斷線(導致這對客戶端無限期嘗試重新建立其連線/階段)。

更新伺服器清單。我們允許客戶端透過提供一個新的逗號分隔的主機:埠對清單來更新連線字串,每個對應到一個 ZooKeeper 伺服器。此函式會呼叫一個機率負載平衡演算法,可能會導致客戶端與其目前主機斷線,目標是達成新清單中每個伺服器預期的均勻連線數。如果客戶端連線的目前主機不在新清單中,此呼叫會永遠導致連線中斷。否則,決定會根據伺服器數增加或減少的數量而定。

例如,如果先前的連線字串包含 3 個主機,而現在清單包含這 3 個主機和另外 2 個主機,則連線到 3 個主機中每一個主機的 40% 客戶端會移轉到新的主機之一,以平衡負載。此演算法會導致客戶端以 0.4 的機率中斷與其目前連線的主機的連線,並在此情況下讓客戶端連線到 2 個新主機之一,隨機選擇。

另一個範例 -- 假設我們有 5 個主機,現在更新清單移除其中 2 個主機,連線到剩餘 3 個主機的客戶端會保持連線,而連線到 2 個已移除主機的所有客戶端需要移轉到 3 個主機之一,隨機選擇。如果連線中斷,客戶端會移轉到特殊模式,在該模式中,他會使用機率演算法選擇新的伺服器連線,而不仅仅是循環。

在第一個範例中,每個用戶端會決定以 0.4 的機率斷線,但一旦做出決定,它會嘗試連線到一個隨機的新伺服器,而且只有當它無法連線到任何新伺服器時,它才會嘗試連線到舊伺服器。在找到伺服器或嘗試所有伺服器於新清單中並連線失敗後,用戶端會移回正常作業模式,其中它會從 connectString 中挑選一個任意伺服器並嘗試連線到它。如果失敗,它會繼續嘗試輪詢不同的隨機伺服器。(請參閱上面用於最初選擇伺服器的演算法)

本機工作階段。新增於 3.5.0,主要由 ZOOKEEPER-1147 實作。

localSessionsUpgradingEnabled 停用時

localSessionsUpgradingEnabled 啟用時

ZooKeeper 監控

ZooKeeper 中的所有讀取作業 - getData()getChildren()exists() - 都可以設定監控作為副作用。以下是 ZooKeeper 對監控的定義:監控事件是一次性觸發器,傳送給設定監控的用戶端,發生在設定監控的資料變更時。此監控定義中有三個重點

監控會在用戶端連線的 ZooKeeper 伺服器上進行本地維護。這讓監控可以輕量化設定、維護和傳送。當用戶端連線至新伺服器時,監控會針對任何階段事件觸發。在與伺服器斷開連線時,不會收到監控。當用戶端重新連線時,任何先前註冊的監控都會重新註冊,並在需要時觸發。一般來說,這一切都會透明地發生。有一個情況可能會錯過監控:如果在斷線時建立並刪除 znode,則會錯過尚未建立的 znode 存在的監控。

3.6.0 的新增功能:用戶端也可以在 znode 上設定永久性的遞迴監控,這些監控在觸發時不會移除,而且會針對註冊的 znode 以及任何子節點 znode 遞迴觸發變更。

監控語意

我們可以使用三個呼叫來設定監控,這些呼叫會讀取 ZooKeeper 的狀態:exists、getData 和 getChildren。下列清單詳細說明監控可以觸發的事件,以及啟用這些事件的呼叫

持續性、遞迴監控

3.6.0 中的新增功能:現在標準監控中有一個變異,您可以設定在觸發時不會被移除的監控。此外,這些監控會觸發事件類型 NodeCreatedNodeDeletedNodeDataChanged,而且可以遞迴地觸發已註冊監控的 znode 起始的所有 znode。請注意,不會觸發持久遞迴監控的 NodeChildrenChanged 事件,因為這會是多餘的。

使用 addWatch() 方法設定持久監控。觸發語意和保證(除了單次觸發)與標準監控相同。關於事件的唯一例外是,遞迴持久監控器永遠不會觸發子節點已變更事件,因為它們是多餘的。使用 removeWatches() 和監控類型 WatcherType.Any 移除持久監控。

移除監控

我們可以使用呼叫 removeWatches 來移除在 znode 上註冊的監控。此外,即使沒有伺服器連線,ZooKeeper 用戶端也可以透過將 local 標記設定為 true 來移除本機監控。下列清單詳細說明成功移除監控後會觸發哪些事件。

ZooKeeper 對監控的保證

關於監控,ZooKeeper 會維持這些保證

關於監控需要注意的事項

使用 ACL 的 ZooKeeper 存取控制

ZooKeeper 使用 ACL 來控制存取其 znode(ZooKeeper 資料樹的資料節點)。ACL 實作與 UNIX 檔案存取權限非常類似:它使用權限位元來允許/禁止對節點執行各種作業,以及位元套用的範圍。與標準 UNIX 權限不同,ZooKeeper 節點不受使用者(檔案擁有者)、群組和全世界(其他)這三個標準範圍限制。ZooKeeper 沒有 znode 擁有者的概念。相反地,ACL 會指定與這些識別碼相關聯的識別碼和權限組。

另請注意,ACL 僅適用於特定 znode。特別是,它不適用於子節點。例如,如果 ip:172.16.16.1 只能讀取 /app,而 /app/status 可以公開讀取,則任何人都可以讀取 /app/status;ACL 不是遞迴的。

ZooKeeper 支援可插入的驗證機制。識別碼使用 scheme:expression 形式指定,其中 scheme 是識別碼對應的驗證機制。有效的表達式組由機制定義。例如,ip:172.16.16.1 是使用 ip 機制的位址為 172.16.16.1 的主機識別碼,而 digest:bob:password 是使用 digest 機制的使用者識別碼,其名稱為 bob

當用戶端連線到 ZooKeeper 並驗證自身時,ZooKeeper 會將所有對應於用戶端的識別碼與用戶端連線關聯。當用戶端嘗試存取節點時,會根據識別碼對照 znode 的 ACL。ACL 由一組 (scheme:expression, perms) 組成。expression 的格式取決於 scheme。例如,組 (ip:19.22.0.0/16, READ) 會給予 IP 位址以 19.22 開頭的任何用戶端 READ 權限。

ACL 權限

ZooKeeper 支援下列權限

CREATEDELETE 權限已從 WRITE 權限中分出,以進行更細緻的存取控制。CREATEDELETE 的情況如下

您希望 A 能夠對 ZooKeeper 節點執行設定,但無法 CREATEDELETE 子節點。

CREATE 不含 DELETE:用戶端透過在父目錄中建立 ZooKeeper 節點來建立要求。您希望所有用戶端都能夠新增,但只有要求處理器可以刪除。(這有點類似於檔案的 APPEND 權限。)

此外,ADMIN 權限存在於 ZooKeeper 中,因為 ZooKeeper 沒有檔案擁有者的概念。在某種意義上,ADMIN 權限將實體指定為擁有者。ZooKeeper 不支援 LOOKUP 權限(目錄上的執行權限位元,允許您即使無法列出目錄也能 LOOKUP)。每個人都隱含擁有 LOOKUP 權限。這允許您統計節點,但僅此而已。(問題是,如果您想對不存在的節點呼叫 zoo_exists(),則沒有權限可以檢查。)

ADMIN 權限在 ACL 方面也扮演特殊角色:為了擷取 znode 的 ACL,使用者必須擁有 READADMIN 權限,但若沒有 ADMIN 權限,摘要雜湊值將會被遮罩掉。

從版本 3.9.2 / 3.8.4 / 3.7.3 開始,exists() 呼叫現在會驗證節點上的 ACL,而且用戶端必須擁有 READ 權限,否則會引發「權限不足」錯誤。

內建 ACL 架構

ZooKeeeper 具有下列內建 scheme

ZooKeeper C 程式庫 API

ZooKeeper C 函式庫提供下列常數

以下是標準 ACL ID

ZOO_AUTH_IDS 空的身分字串應解讀為「建立者的身分」。

ZooKeeper 客戶端附帶三個標準 ACL

ZOO_OPEN_ACL_UNSAFE 對所有 ACL 完全開放免費:任何應用程式都可以在節點上執行任何操作,並且可以建立、列出和刪除其子節點。ZOO_READ_ACL_UNSAFE 對任何應用程式都是唯讀存取。CREATE_ALL_ACL 會授予節點建立者所有權限。建立者必須先通過伺服器驗證(例如,使用「digest」架構),才能使用此 ACL 建立節點。

以下 ZooKeeper 操作會處理 ACL

應用程式使用 zoo_add_auth 函式向伺服器驗證自身。如果應用程式想要使用不同的架構和/或身分驗證,可以多次呼叫此函式。

zoo_create(...) 操作會建立新的節點。acl 參數是與節點關聯的 ACL 清單。父節點必須設定 CREATE 權限位元。

此操作會傳回節點的 ACL 資訊。節點必須設定 READ 或 ADMIN 權限。如果沒有 ADMIN 權限,摘要雜湊值將會遮罩掉。

此函式會用新的 ACL 清單取代節點的 ACL 清單。節點必須設定 ADMIN 權限。

以下範例程式碼會使用上述 API 透過「foo」架構驗證自身,並建立具有僅建立權限的暫時節點「/xyz」。

注意

這是一個非常簡單的範例,目的是特別說明如何與 ZooKeeper ACL 互動。請參閱 .../trunk/zookeeper-client/zookeeper-client-c/src/cli.c,以取得 C 客戶端實作範例

#include <string.h>
#include <errno.h>

#include "zookeeper.h"

static zhandle_t *zh;

/**
 * In this example this method gets the cert for your
 *   environment -- you must provide
 */
char *foo_get_cert_once(char* id) { return 0; }

/** Watcher function -- empty for this example, not something you should
 * do in real code */
void watcher(zhandle_t *zzh, int type, int state, const char *path,
         void *watcherCtx) {}

int main(int argc, char argv) {
  char buffer[512];
  char p[2048];
  char *cert=0;
  char appId[64];

  strcpy(appId, "example.foo_test");
  cert = foo_get_cert_once(appId);
  if(cert!=0) {
    fprintf(stderr,
        "Certificate for appid [%s] is [%s]\n",appId,cert);
    strncpy(p,cert, sizeof(p)-1);
    free(cert);
  } else {
    fprintf(stderr, "Certificate for appid [%s] not found\n",appId);
    strcpy(p, "dummy");
  }

  zoo_set_debug_level(ZOO_LOG_LEVEL_DEBUG);

  zh = zookeeper_init("localhost:3181", watcher, 10000, 0, 0, 0);
  if (!zh) {
    return errno;
  }
  if(zoo_add_auth(zh,"foo",p,strlen(p),0,0)!=ZOK)
    return 2;

  struct ACL CREATE_ONLY_ACL[] = {{ZOO_PERM_CREATE, ZOO_AUTH_IDS}};
  struct ACL_vector CREATE_ONLY = {1, CREATE_ONLY_ACL};
  int rc = zoo_create(zh,"/xyz","value", 5, &CREATE_ONLY, ZOO_EPHEMERAL,
                  buffer, sizeof(buffer)-1);

  /** this operation will fail with a ZNOAUTH error */
  int buflen= sizeof(buffer);
  struct Stat stat;
  rc = zoo_get(zh, "/xyz", 0, buffer, &buflen, &stat);
  if (rc) {
    fprintf(stderr, "Error %d for %s\n", rc, __LINE__);
  }

  zookeeper_close(zh);
  return 0;
}

可插入的 ZooKeeper 驗證

ZooKeeper 會在各種不同的環境中執行,並使用各種不同的驗證架構,因此它有一個完全可插入的驗證架構。甚至內建的驗證架構也會使用可插入的驗證架構。

要了解驗證架構如何運作,首先必須了解兩個主要的驗證操作。架構首先必須驗證用戶端。這通常在用戶端連線到伺服器時執行,並包含驗證從用戶端傳送或收集的資訊,並將其與連線關聯。架構處理的第二個操作是尋找對應於用戶端的 ACL 中的項目。ACL 項目是 <idspec、權限> 對。idspec 可能與與連線關聯的驗證資訊進行簡單的字串比對,或者可能是針對該資訊評估的表達式。比對是由驗證外掛程式實作來完成的。以下是驗證外掛程式必須實作的介面

public interface AuthenticationProvider {
    String getScheme();
    KeeperException.Code handleAuthentication(ServerCnxn cnxn, byte authData[]);
    boolean isValid(String id);
    boolean matches(String id, String aclExpr);
    boolean isAuthenticated();
}

第一個方法 getScheme 傳回識別外掛程式的字串。由於我們支援多種驗證方法,驗證憑證或 idspec 將永遠加上 scheme: 前綴。ZooKeeper 伺服器使用驗證外掛程式傳回的 scheme 來判斷 scheme 套用到哪些 id。

當用戶端傳送驗證資訊以與連線關聯時,會呼叫 handleAuthentication。用戶端指定資訊對應的 scheme。ZooKeeper 伺服器將資訊傳遞給 getScheme 與用戶端傳遞的 scheme 相符的驗證外掛程式。handleAuthentication 的實作通常會在判斷資訊不正確時傳回錯誤,或者會使用 cnxn.getAuthInfo().add(new Id(getScheme(), data)) 將資訊與連線關聯。

驗證外掛程式參與設定和使用 ACL。當為 znode 設定 ACL 時,ZooKeeper 伺服器會將輸入的 id 部分傳遞給 isValid(String id) 方法。驗證 id 是否有正確的形式是外掛程式的責任。例如,ip:172.16.0.0/16 是有效的 id,但 ip:host.com 則不是。如果新的 ACL 包含「auth」輸入,isAuthenticated 會用於查看是否應將與連線相關聯的此架構的驗證資訊新增至 ACL。有些架構不應包含在 auth 中。例如,如果指定 auth,則不會將用戶端的 IP 位址視為應新增至 ACL 的 id。

在檢查 ACL 時,ZooKeeper 會呼叫 matches(String id, String aclExpr)。它需要將用戶端的驗證資訊與相關的 ACL 輸入配對。為了找出適用於用戶端的輸入,ZooKeeper 伺服器會找出每個輸入的架構,如果該用戶端有來自該架構的驗證資訊,則會呼叫 matches(String id, String aclExpr),其中 id 設定為先前由 handleAuthentication 新增至連線的驗證資訊,而 aclExpr 設定為 ACL 輸入的 id。驗證外掛程式使用自己的邏輯和配對架構來判斷 id 是否包含在 aclExpr 中。

內建兩個驗證外掛程式:ipdigest。可以使用系統屬性新增其他外掛程式。在啟動時,ZooKeeper 伺服器會尋找以「zookeeper.authProvider.」開頭的系統屬性,並將這些屬性的值解釋為驗證外掛程式的類別名稱。可以使用 -Dzookeeeper.authProvider.X=com.f.MyAuth 設定這些屬性,或在伺服器設定檔中新增以下輸入

authProvider.1=com.f.MyAuth
authProvider.2=com.f.MyAuth2

應小心確保屬性上的字尾是唯一的。如果出現重複,例如 -Dzookeeeper.authProvider.X=com.f.MyAuth -Dzookeeper.authProvider.X=com.f.MyAuth2,只會使用其中一個。此外,所有伺服器都必須定義相同的外掛程式,否則使用外掛程式提供的驗證架構的用戶端將無法連線至某些伺服器。

3.6.0 新增:可使用可插入式驗證的替代抽象化。它提供其他引數。

public abstract class ServerAuthenticationProvider implements AuthenticationProvider {
    public abstract KeeperException.Code handleAuthentication(ServerObjs serverObjs, byte authData[]);
    public abstract boolean matches(ServerObjs serverObjs, MatchValues matchValues);
}

您會延伸 ServerAuthenticationProvider,而不是實作 AuthenticationProvider。您的 handleAuthentication() 和 matches() 方法會收到額外的參數(透過 ServerObjs 和 MatchValues)。

一致性保證

ZooKeeper 是一種高性能、可擴充的服務。讀取和寫入操作都設計為快速,但讀取比寫入快。原因在於,在讀取的情況下,ZooKeeper 可以提供較舊的資料,而這又是因為 ZooKeeper 的一致性保證

使用這些一致性保證,很容易在 ZooKeeper 客戶端建立較高層級的功能,例如領導者選舉、屏障、佇列和讀取/寫入可撤銷鎖定(不需要對 ZooKeeper 進行新增)。有關更多詳細資訊,請參閱 配方和解決方案

注意

有時開發人員會錯誤地假設 ZooKeeper 沒有做出的另一個保證。這是:*同時一致的跨客戶端檢視*:ZooKeeper 不保證在任何時間點,兩個不同的客戶端都會有相同的 ZooKeeper 資料檢視。由於網路延遲等因素,一個客戶端可能會在另一個客戶端收到變更通知之前執行更新。考慮客戶端 A 和 B 的場景。如果客戶端 A 將 znode /a 的值從 0 設定為 1,然後告訴客戶端 B 讀取 /a,則客戶端 B 可能會讀取舊值 0,具體取決於連線的伺服器。如果客戶端 A 和客戶端 B 讀取相同的值很重要,則客戶端 B 應在執行讀取之前從 ZooKeeper API 方法呼叫 sync() 方法。因此,ZooKeeper 本身並不能保證變更會在所有伺服器上同步發生,但可以使用 ZooKeeper 原語來建構提供有用的客戶端同步的高層級功能。(有關更多資訊,請參閱 ZooKeeper 配方

繫結

ZooKeeper 客户端程式庫有兩種語言:Java 和 C。以下各節將說明這些語言。

Java 繫結

組成 ZooKeeper Java 繫結的套件有兩個:org.apache.zookeeperorg.apache.zookeeper.data。組成 ZooKeeper 的其他套件在內部使用,或為伺服器實作的一部分。org.apache.zookeeper.data 套件由產生的類別組成,這些類別僅用於容器。

ZooKeeper Java 客户端使用的主要類別是 ZooKeeper 類別。其兩個建構函數僅由一個選用的會話 ID 和密碼有所不同。ZooKeeper 支援跨處理程序實例的會話復原。Java 程式可以將其會話 ID 和密碼儲存在穩定儲存區、重新啟動,並復原程式之前實例使用的會話。

建立 ZooKeeper 物件時,也會建立兩個執行緒:一個 IO 執行緒和一個事件執行緒。所有 IO 都在 IO 執行緒上發生(使用 Java NIO)。所有事件回呼都發生在事件執行緒上。會話維護(例如重新連線到 ZooKeeper 伺服器和維護心跳)在 IO 執行緒上執行。同步方法的回應也會在 IO 執行緒中處理。所有非同步方法和監控事件的回應都會在事件執行緒中處理。這個設計有幾點需要注意:

最後,與關閉相關的規則很簡單:一旦 ZooKeeper 物件關閉或收到致命事件(SESSION_EXPIRED 和 AUTH_FAILED),ZooKeeper 物件就會失效。關閉時,兩個執行緒會關閉,而對 zookeeper 處理的任何進一步存取都是未定義的行為,應避免執行。

用戶端組態參數

以下清單包含 Java 客户端的組態屬性。您可以使用 Java 系統屬性設定這些屬性中的任何一個。對於伺服器屬性,請查看 管理指南的伺服器組態區段。ZooKeeper Wiki 也有關於 ZooKeeper SSL 支援ZooKeeper 的 SASL 驗證 的有用頁面。

C 繫結

C 繫結具有單執行緒和多執行緒程式庫。多執行緒程式庫最容易使用,且與 Java API 最相似。此程式庫將建立一個 IO 執行緒和一個事件傳送執行緒,用於處理連線維護和回呼。單執行緒程式庫允許透過公開多執行緒程式庫中使用的事件迴圈,在事件驅動應用程式中使用 ZooKeeper。

此套件包含兩個共用程式庫:zookeeper_st 和 zookeeper_mt。前者僅提供非同步 API 和回呼,以整合至應用程式的事件迴圈。此程式庫存在的唯一原因是支援不提供 pthread 程式庫或該程式庫不穩定的平台(例如 FreeBSD 4.x)。在所有其他情況下,應用程式開發人員都應連結至 zookeeper_mt,因為它包含對同步和非同步 API 的支援。

安裝

如果您要從 Apache 儲存庫的簽出建立客戶端,請遵循以下步驟。如果您要從 Apache 下載的專案原始碼套件建立,請跳至步驟 3

  1. 在 zookeeper-jute 目錄(.../trunk/zookeeper-jute)中執行 mvn compile。這會在 .../trunk/zookeeper-client/zookeeper-client-c 下建立一個名為「generated」的目錄。
  2. 將目錄變更為*.../trunk/zookeeper-client/zookeeper-client-c*,並執行 autoreconf -if 以啟動 autoconfautomakelibtool。請確定您已安裝 autoconf 2.59 版本或更新版本。跳至步驟 4
  3. 如果您要從專案原始碼套件建立,請解壓縮/解壓縮原始碼 tarball,並將 cd 變更為* zookeeper-x.x.x/zookeeper-client/zookeeper-client-c* 目錄。
  4. 執行 ./configure <your-options> 以產生 makefile。以下是 configure 工具支援的一些選項,可在此步驟中使用
注意

請參閱 INSTALL 以取得關於執行 configure 的一般資訊。1. 執行 makemake install 以建置函式庫並安裝它們。1. 若要產生 ZooKeeper API 的 doxygen 文件,請執行 make doxygen-doc。所有文件都將放置在名為 docs 的新子資料夾中。預設情況下,此命令僅產生 HTML。有關其他文件格式的資訊,請執行 ./configure --help

建置您自己的 C 用戶端

為了能在應用程式中使用 ZooKeeper C API,您必須記得

  1. 包含 ZooKeeper 標頭:#include <zookeeper/zookeeper.h>
  2. 如果您正在建置多執行緒用戶端,請使用 -DTHREADED 編譯器標記編譯以啟用函式庫的多執行緒版本,然後連結到 zookeeper_mt 函式庫。如果您正在建置單一執行緒用戶端,請勿使用 -DTHREADED 編譯,並務必連結到_zookeeper_st_函式庫。
注意

請參閱 .../trunk/zookeeper-client/zookeeper-client-c/src/cli.c 以取得 C 用戶端實作範例

建置區塊:ZooKeeper 操作指南

本節調查開發人員可以在 ZooKeeper 伺服器上執行的所有作業。它比本手冊中較早的概念章節的資訊層級較低,但比 ZooKeeper API 參考層級較高。它涵蓋這些主題

處理錯誤

Java 和 C 用戶端繫結都可能會報告錯誤。Java 用戶端繫結會透過擲出 KeeperException 來執行此動作,在例外狀況中呼叫 code() 將傳回特定錯誤碼。C 用戶端繫結會傳回 ZOO_ERRORS 列舉中定義的錯誤碼。API 回呼會指示兩種語言繫結的結果碼。請參閱 API 文件(Java 的 javadoc,C 的 doxygen)以取得有關可能錯誤及其含義的完整詳細資料。

連線到 ZooKeeper

在我們開始之前,您必須設定一個正在執行的 Zookeeper 伺服器,以便我們可以開始開發用戶端。對於 C 用戶端繫結,我們將使用多執行緒函式庫 (zookeeper_mt) 以及用 C 編寫的簡單範例。若要與 Zookeeper 伺服器建立連線,我們使用 C API - zookeeper_init,其簽章如下

int zookeeper_init(const char *host, watcher_fn fn, int recv_timeout, const clientid_t *clientid, void *context, int flags);

我們將展示在成功連線後會輸出「已連線至 Zookeeper」的用戶端,否則會輸出錯誤訊息。我們將以下程式碼稱為 zkClient.cc

#include <stdio.h>
#include <zookeeper/zookeeper.h>
#include <errno.h>
using namespace std;

// Keeping track of the connection state
static int connected = 0;
static int expired   = 0;

// *zkHandler handles the connection with Zookeeper
static zhandle_t *zkHandler;

// watcher function would process events
void watcher(zhandle_t *zkH, int type, int state, const char *path, void *watcherCtx)
{
    if (type == ZOO_SESSION_EVENT) {

        // state refers to states of zookeeper connection.
        // To keep it simple, we would demonstrate these 3: ZOO_EXPIRED_SESSION_STATE, ZOO_CONNECTED_STATE, ZOO_NOTCONNECTED_STATE
        // If you are using ACL, you should be aware of an authentication failure state - ZOO_AUTH_FAILED_STATE
        if (state == ZOO_CONNECTED_STATE) {
            connected = 1;
        } else if (state == ZOO_NOTCONNECTED_STATE ) {
            connected = 0;
        } else if (state == ZOO_EXPIRED_SESSION_STATE) {
            expired = 1;
            connected = 0;
            zookeeper_close(zkH);
        }
    }
}

int main(){
    zoo_set_debug_level(ZOO_LOG_LEVEL_DEBUG);

    // zookeeper_init returns the handler upon a successful connection, null otherwise
    zkHandler = zookeeper_init("localhost:2181", watcher, 10000, 0, 0, 0);

    if (!zkHandler) {
        return errno;
    }else{
        printf("Connection established with Zookeeper. \n");
    }

    // Close Zookeeper connection
    zookeeper_close(zkHandler);

    return 0;
}

使用前面提到的多執行緒程式庫編譯程式碼。

> g++ -Iinclude/ zkClient.cpp -lzookeeper_mt -o Client

執行用戶端。

> ./Client

從輸出中,如果連線成功,您應該會看到「已連線至 Zookeeper」以及 Zookeeper 的 DEBUG 訊息。

陷阱:常見問題和疑難排解

因此,現在您已經認識 ZooKeeper 了。它快速、簡單,您的應用程式可以正常運作,但等等... 有些地方不對勁。以下是 ZooKeeper 使用者常遇到的陷阱

  1. 如果您正在使用監控,您必須尋找已連線的監控事件。當 ZooKeeper 用戶端與伺服器斷線時,您不會收到變更通知,直到重新連線。如果您正在監控 znode 出現,如果您在斷線期間建立並刪除 znode,您將會錯過該事件。
  2. 您必須測試 ZooKeeper 伺服器故障。只要大多數伺服器都處於活動狀態,ZooKeeper 服務就能在故障中存活下來。要問的問題是:您的應用程式能處理它嗎?在現實世界中,用戶端與 ZooKeeper 的連線可能會中斷。(ZooKeeper 伺服器故障和網路分割是連線中斷的常見原因。)ZooKeeper 用戶端程式庫會負責復原您的連線並讓您知道發生了什麼事,但您必須確保復原您的狀態和任何失敗的未完成要求。找出您是否在測試實驗室而不是在生產環境中做對了 - 使用由多個伺服器組成的 ZooKeeper 服務進行測試,並讓它們重新開機。
  3. 用戶端使用的 ZooKeeper 伺服器清單必須與每個 ZooKeeper 伺服器擁有的 ZooKeeper 伺服器清單相符。如果用戶端清單是 ZooKeeper 伺服器實際清單的子集,事情可以運作,儘管不是最佳狀態,但如果用戶端清單列出 ZooKeeper 群集中不存在的 ZooKeeper 伺服器,則不行。
  4. 小心放置交易記錄。ZooKeeper 中效能最關鍵的部分是交易記錄。在傳回回應之前,ZooKeeper 必須將交易同步到媒體。專用的交易記錄裝置是持續良好效能的關鍵。將記錄放在忙碌的裝置上會對效能產生負面影響。如果您只有一個儲存裝置,請將追蹤檔案放在 NFS 上並增加 snapshotCount;它無法消除問題,但可以減輕問題。
  5. 正確設定您的 Java 最大堆積大小。避免交換非常重要。不必要地轉到磁碟幾乎肯定會讓您的效能下降到無法接受的程度。請記住,在 ZooKeeper 中,所有內容都是按順序排列的,因此如果一個要求觸及磁碟,所有其他排隊的要求都會觸及磁碟。為避免交換,請嘗試將堆積大小設定為您擁有的實體記憶體量,減去作業系統和快取所需的量。找出最佳堆積大小的最佳方法是執行負載測試。如果您因為某些原因無法執行,請保守估計,並選擇遠低於會導致您的機器交換的限制的數字。例如,在 4G 機器上,3G 堆積是一個保守的估計,可以作為起點。

其他資訊的連結

除了正式文件之外,還有其他幾個資訊來源供 ZooKeeper 開發人員使用。