ZooKeeper 動態重新設定
概觀
在 3.5.0 版本之前,Zookeeper 的成員資格和其他所有設定參數都是靜態的 - 在開機時載入,在執行時不可變更。操作人員訴諸於「滾動重新啟動」- 一種手動密集且容易出錯的設定變更方法,這會導致生產環境中的資料遺失和不一致。
從 3.5.0 開始,不再需要「滾動重新啟動」!ZooKeeper 完全支援自動設定變更:Zookeeper 伺服器組、其角色(參與者/觀察者)、所有埠,甚至法定人數系統都可以在不中斷服務且維持資料一致性的情況下動態變更。重新設定會立即執行,就像 ZooKeeper 中的其他作業一樣。可以使用單一重新設定命令執行多項變更。動態重新設定功能不會限制作業並行性,不需要在重新設定期間停止用戶端作業,對管理員來說具有非常簡單的介面,而且不會增加其他用戶端作業的複雜性。
新的用戶端功能允許用戶端找出組態變更,並更新儲存在其 ZooKeeper 處理常式的連線字串(伺服器清單及其用戶端埠)。使用機率演算法在新的組態伺服器間重新平衡用戶端,同時保持用戶端遷移的程度與合奏成員身份變更成正比。
此文件提供重新組態的管理員手冊。如需重新組態演算法、效能測量等詳細說明,請參閱我們的論文
- Shraer, A., Reed, B., Malkhi, D., Junqueira, F. Primary/Backup 群集的動態重新組態。在 USENIX 年度技術大會 (ATC)(2012),425-437:連結:論文 (pdf)、投影片 (pdf)、影片、hadoop 高峰會投影片
注意:從 3.5.3 開始,動態重新組態功能預設為停用,且必須透過 reconfigEnabled 組態選項明確開啟。
設定格式變更
指定用戶端埠
伺服器的用戶端埠是伺服器接受用戶端連線要求的埠。從 3.5.0 開始,clientPort 和 clientPortAddress 組態參數不應再使用。反之,此資訊現在是伺服器關鍵字規格的一部分,如下所示
server.<positive id> = <address1>:<port1>:<port2>[:role];[<client port address>:]<client port>**
用戶端埠規格在分號的右側。用戶端埠位址為選用,如果未指定,則預設為「0.0.0.0」。與往常一樣,角色也為選用,可以是 participant 或 observer(預設為 participant)。
合法伺服器陳述範例
server.5 = 125.23.63.23:1234:1235;1236
server.5 = 125.23.63.23:1234:1235:participant;1236
server.5 = 125.23.63.23:1234:1235:observer;1236
server.5 = 125.23.63.23:1234:1235;125.23.63.24:1236
server.5 = 125.23.63.23:1234:1235:participant;125.23.63.23:1236
指定多個伺服器位址
自 ZooKeeper 3.6.0 起,可以為每個 ZooKeeper 伺服器指定多個位址(請參閱 ZOOKEEPER-3188)。這有助於提高可用性,並為 ZooKeeper 增加網路層復原能力。當伺服器使用多個實體網路介面時,ZooKeeper 能夠繫結到所有介面,並在發生網路錯誤時執行時間切換到正常運作的介面。可以使用直線 (|) 字元在組態中指定不同的位址。
使用多個位址的有效組態範例
server.2=zoo2-net1:2888:3888|zoo2-net2:2889:3889;2188
server.2=zoo2-net1:2888:3888|zoo2-net2:2889:3889|zoo2-net3:2890:3890;2188
server.2=zoo2-net1:2888:3888|zoo2-net2:2889:3889;zoo2-net1:2188
server.2=zoo2-net1:2888:3888:observer|zoo2-net2:2889:3889:observer;2188
standaloneEnabled 旗標
在 3.5.0 之前,可以在獨立模式或分散式模式下執行 ZooKeeper。這些是獨立的實作堆疊,且無法在執行時間切換它們。預設(為了向後相容性)將 standaloneEnabled 設為 true。使用此預設的結果是,如果使用單一伺服器啟動,則不允許合奏成長;如果使用多個伺服器啟動,則不允許縮小至包含少於兩個參與者。
將旗標設為 false 會指示系統執行分散式軟體堆疊,即使合奏中只有一個參與者。為達成此目的,(靜態)組態檔應包含
standaloneEnabled=false**
使用此設定,可以啟動包含單一參與者的 ZooKeeper 合奏,並透過新增更多伺服器來動態擴充。同樣地,可以透過移除伺服器來縮小合奏,使其僅剩單一參與者。
由於執行分散式模式允許更大的彈性,我們建議將旗標設為 false。我們預期舊版的獨立模式將在未來被棄用。
reconfigEnabled 旗標
從 3.5.0 開始,在 3.5.3 之前,沒有辦法停用動態重新組態功能。我們希望提供停用重新組態功能的選項,因為在啟用重新組態的情況下,我們有安全方面的疑慮,惡意行為者可以對 ZooKeeper 群集的組態進行任意變更,包括將受危害的伺服器新增到群集中。我們希望由使用者自行決定是否啟用,並確保適當的安全措施已就位。因此,在 3.5.3 中引入了 reconfigEnabled 組態選項,以便可以完全停用重新組態功能,而且任何嘗試透過重新組態 API 重新組態群集(無論是否經過驗證)在預設情況下都會失敗,除非將 reconfigEnabled 設為 true。
若要將選項設為 true,組態檔案 (zoo.cfg) 應包含
reconfigEnabled=true
動態設定檔
從 3.5.0 開始,我們區分可以在執行期間變更的動態組態參數和在伺服器開機時從組態檔案讀取且在執行期間不會變更的靜態組態參數。目前,下列組態關鍵字被視為動態組態的一部分:server、group 和 weight。
動態組態參數儲存在伺服器上的個別檔案中(我們稱之為動態組態檔案)。此檔案使用新的 dynamicConfigFile 關鍵字從靜態組態檔案連結。
範例
zoo_replicated1.cfg
tickTime=2000
dataDir=/zookeeper/data/zookeeper1
initLimit=5
syncLimit=2
dynamicConfigFile=/zookeeper/conf/zoo_replicated1.cfg.dynamic
zoo_replicated1.cfg.dynamic
server.1=125.23.63.23:2780:2783:participant;2791
server.2=125.23.63.24:2781:2784:participant;2792
server.3=125.23.63.25:2782:2785:participant;2793
當群集組態變更時,靜態組態參數會保持不變。動態參數會由 ZooKeeper 推播,並覆寫所有伺服器上的動態組態檔案。因此,不同伺服器上的動態組態檔案通常是相同的(它們只會在重新組態進行中或新組態尚未傳播到某些伺服器時暫時不同)。一旦建立,不應手動變更動態組態檔案。變更只能透過以下概述的新重新組態指令進行。請注意,變更離線群集的組態可能會導致與儲存在 ZooKeeper 記錄中的組態資訊(以及從記錄填入的特殊組態 znode)不一致,因此強烈建議不要這樣做。
範例 2
使用者可能偏好最初指定單一組態檔案。因此,下列內容也是合法的
zoo_replicated1.cfg
tickTime=2000
dataDir=/zookeeper/data/zookeeper1
initLimit=5
syncLimit=2
clientPort=
如果每個伺服器上的組態檔案尚未採用此格式,它們會自動分割為動態和靜態檔案。因此,上述組態檔案會自動轉換為範例 1 中的兩個檔案。請注意,如果 clientPort 和 clientPortAddress 行(如果已指定)是多餘的(如上述範例),它們會在此過程中自動移除。原始的靜態組態檔案會備份(在 .bak 檔案中)。
向下相容性
我們仍支援舊的組態格式。例如,下列組態檔案是可以接受的(但不建議使用)
zoo_replicated1.cfg
tickTime=2000
dataDir=/zookeeper/data/zookeeper1
initLimit=5
syncLimit=2
clientPort=2791
server.1=125.23.63.23:2780:2783:participant
server.2=125.23.63.24:2781:2784:participant
server.3=125.23.63.25:2782:2785:participant
開機期間,會建立一個動態設定檔,包含前面說明的設定之動態部分。然而,在此情況下,「clientPort=2791」行會保留在伺服器 1 的靜態設定檔中,因為它並非冗餘,而且並未指定為「server.1=...」的一部分,使用 變更設定格式 章節中說明的格式。如果呼叫設定伺服器 1 的用戶端埠的重新設定,我們會從靜態設定檔中移除「clientPort=2791」(動態檔案現在包含此資訊,作為伺服器 1 規格的一部分)。
升級到 3.5.0
只有在將您的組合升級到 3.4.6 版本後,才應將正在執行的 ZooKeeper 組合升級到 3.5.0。請注意,這僅適用於滾動升級(如果您願意完全關閉系統,則不必執行 3.4.6)。如果您嘗試在未執行 3.4.6 的情況下進行滾動升級(例如從 3.4.5),您可能會收到下列錯誤
2013-01-30 11:32:10,663 [myid:2] - INFO [localhost/127.0.0.1:2784:QuorumCnxManager$Listener@498] - Received connection request /127.0.0.1:60876
2013-01-30 11:32:10,663 [myid:2] - WARN [localhost/127.0.0.1:2784:QuorumCnxManager@349] - Invalid server id: -65536
在滾動升級期間,每個伺服器會依序關閉,並使用新的 3.5.0 二進位檔重新開機。在使用 3.5.0 二進位檔啟動伺服器之前,我們強烈建議更新設定檔,讓所有伺服器陳述式「server.x=...」都包含用戶端埠(請參閱 指定用戶端埠 章節)。如前所述,您可以在單一檔案中保留設定,並保留 clientPort/clientPortAddress 陳述式(不過如果您以新格式指定用戶端埠,這些陳述式現在是冗餘的)。
ZooKeeper 群集的動態重新設定
ZooKeeper Java 和 C API 已擴充,並新增 getConfig 和 reconfig 指令,以利重新設定。這兩個指令都有一個同步(封鎖)變異和一個非同步變異。我們在此使用 Java CLI 示範這些指令,但請注意,您也可以類似地使用 C CLI,或直接從程式呼叫這些指令,就像任何其他 ZooKeeper 指令一樣。
API
Java 和 C 用戶端都有兩組 API。
-
重新設定 API:重新設定 API 用於重新設定 ZooKeeper 集群。從 3.5.3 開始,重新設定 Java API 已從 ZooKeeper 類別移至 ZooKeeperAdmin 類別,而使用此 API 需要設定 ACL 和使用者驗證(請參閱 安全性 以取得更多資訊)。
-
取得設定 API:取得設定 API 用於擷取儲存在 /zookeeper/config znode 中的 ZooKeeper 集群設定資訊。使用此 API 不需要特定設定或驗證,因為任何使用者都可以讀取 /zookeeper/config。
安全性
在 3.5.3 之前,重新設定上沒有強制執行安全機制,因此任何可以連線到 ZooKeeper 伺服器組合的 ZooKeeper 用戶端都可以透過重新設定變更 ZooKeeper 集群的狀態。因此,惡意用戶端有可能將受損伺服器新增到組合中,例如,新增受損伺服器或移除合法的伺服器。這些情況在個別情況下可能是安全漏洞。
為了解決此安全性問題,我們從 3.5.3 開始,針對重新配置引入了存取控制,因此只有特定使用者集可以使用重新配置命令或 API,而且這些使用者需要明確設定。此外,ZooKeeper 集群的設定必須啟用驗證,才能驗證 ZooKeeper 用戶端。
我們也為在安全環境(例如公司防火牆後方)操作和與 ZooKeeper 組合互動的使用者提供一個逃生艙口。對於想要使用重新配置功能,但不想負擔設定明確的重新配置存取檢查授權使用者清單的負擔的使用者,他們可以將 "skipACL" 設定為 "yes",這會略過 ACL 檢查,並允許任何使用者重新配置集群。
整體而言,ZooKeeper 為重新配置功能提供彈性的設定選項,讓使用者可以根據使用者的安全性需求進行選擇。我們讓使用者自行決定是否已採取適當的安全措施。
-
存取控制:動態設定儲存在特殊 znode ZooDefs.CONFIG_NODE = /zookeeper/config 中。預設上,此節點對所有使用者都是唯讀的,除了超級使用者和明確設定為寫入存取的使用者。需要使用重新配置命令或重新配置 API 的用戶端應設定為具有對 CONFIG_NODE 的寫入存取權的使用者。預設上,只有超級使用者具有完全控制權,包括對 CONFIG_NODE 的寫入存取權。超級使用者可以透過設定具有與指定使用者相關聯的寫入權限的 ACL,授予其他使用者寫入存取權。可以在 ReconfigExceptionTest.java 和 TestReconfigServer.cc 中找到一些如何設定 ACL 和使用驗證重新配置 API 的範例。
-
驗證:使用者的驗證與存取控制是正交的,並委派給 ZooKeeper 的可插入驗證機制所支援的現有驗證機制。請參閱 ZooKeeper and SASL 以取得關於此主題的更多詳細資料。
-
停用 ACL 檢查:ZooKeeper 支援 "skipACL" 選項,如果將 skipACL 設定為 "yes",則會完全略過 ACL 檢查。在這種情況下,任何未驗證的使用者都可以使用重新配置 API。
擷取目前的動態設定
動態設定儲存在一個特殊的 znode ZooDefs.CONFIG_NODE = /zookeeper/config。新的 config
CLI 指令會讀取這個 znode(目前它僅是 get /zookeeper/config
的包裝器)。與一般的讀取相同,若要擷取最新的提交值,您應先執行 sync
。
[zk: 127.0.0.1:2791(CONNECTED) 3] config
server.1=localhost:2780:2783:participant;localhost:2791
server.2=localhost:2781:2784:participant;localhost:2792
server.3=localhost:2782:2785:participant;localhost:2793
請注意輸出的最後一行。這是設定版本。版本等於建立此設定的重新設定指令的 zxid。第一個建立的設定版本等於第一個成功建立的 leader 傳送的 NEWLEADER 訊息的 zxid。當設定寫入動態設定檔時,版本會自動成為檔名的一部分,而靜態設定檔會更新為新動態設定檔的路徑。對應較早版本設定檔會保留以供備份用途。
在開機期間,版本(如果存在)會從檔名中擷取。版本絕不應由使用者或系統管理員手動變更。系統會使用它來得知哪個設定是最新的。手動操作它可能會導致資料遺失和不一致。
就像 get
指令一樣,config
CLI 指令接受 -w 旗標,用於設定 znode 的監控,以及 -s 旗標,用於顯示 znode 的統計資料。它另外接受一個新的旗標 -c,它只會輸出版本和對應於目前設定的用戶端連線字串。例如,對於上述設定,我們會取得
[zk: 127.0.0.1:2791(CONNECTED) 17] config -c
400000003 localhost:2791,localhost:2793,localhost:2792
請注意,當直接使用 API 時,此指令稱為 getConfig
。
與任何讀取指令一樣,它會傳回您的用戶端所連線的追隨者已知的設定,它可能會稍微過時。您可以使用 sync
指令取得更強的保證。例如,使用 Java API
zk.sync(ZooDefs.CONFIG_NODE, void_callback, context);
zk.getConfig(watcher, callback, context);
注意:在 3.5.0 中,傳遞給 sync()
指令的路徑並不重要,因為伺服器的所有狀態都會更新為與 leader 相同(因此您可以使用 ZooDefs.CONFIG_NODE 以外的不同路徑)。不過,這可能會在未來變更。
修改目前的動態設定
修改設定是透過 reconfig
指令完成。有兩種重新設定模式:增量式和非增量式(大量)。非增量式只會指定系統的新動態設定。增量式會指定對目前設定的變更。reconfig
指令會傳回新的設定。
幾個範例在:ReconfigTest.java、ReconfigRecoveryTest.java 和 TestReconfigServer.cc。
一般
移除伺服器:任何伺服器都可以移除,包括領導者(雖然移除領導者會導致短暫的不可用,請參閱論文中的圖 6 和 8 文件)。伺服器不會自動關閉。相反地,它會變成「沒有投票權的追隨者」。這有點類似於觀察者,因為其投票不會計入提交操作所需的投票法定人數。但是,與沒有投票權的追隨者不同,觀察者實際上看不到任何操作建議,也不會確認它們。因此,與觀察者相比,沒有投票權的追隨者對系統處理量有更顯著的負面影響。沒有投票權的追隨者模式只應作為暫時模式使用,在關閉伺服器或將其新增為追隨者或觀察者加入到整體之前。我們不出於兩個主要原因自動關閉伺服器。第一個原因是我們不希望連接到此伺服器的所有用戶端立即斷開連線,導致大量連線要求湧入其他伺服器。相反,如果每個用戶端獨立決定何時遷移會更好。第二個原因是,有時(很少)可能需要移除伺服器才能將其從「觀察者」變更為「參與者」(這在 其他意見 一節中有說明)。
請注意,新組態應具備一定數量的參與者才算合法。如果建議的變更會使叢集參與者少於 2 個,且已啟用獨立模式(standaloneEnabled=true,請參閱 standaloneEnabled 旗標 一節),則不會處理重新組態(BadArgumentsException)。如果已停用獨立模式(standaloneEnabled=false),則保持 1 個或更多參與者是合法的。
新增伺服器:在呼叫重新組態之前,管理員必須確保新組態中法定人數(多數)的參與者已連線並與目前的領導者同步。為達成此目的,我們需要在加入伺服器正式成為整體的一部分之前,將其連線到領導者。這是透過使用伺服器初始清單啟動加入伺服器來完成的,這在技術上不是系統的合法組態,但 (a) 包含加入者,且 (b) 提供加入者足夠的資訊,以便其尋找並連線到目前的領導者。我們列出了一些安全執行此操作的不同選項。
- 加入者的初始組態包含最後提交組態中的伺服器和一個或多個加入者,其中 加入者列為觀察者。例如,如果伺服器 D 和 E 同時新增到 (A, B, C) 中,且伺服器 C 正在移除,則 D 的初始組態可以是 (A, B, C, D) 或 (A, B, C, D, E),其中 D 和 E 列為觀察者。類似地,E 的組態可以是 (A, B, C, E) 或 (A, B, C, D, E),其中 D 和 E 列為觀察者。請注意,將加入者列為觀察者並不會實際使他們成為觀察者 - 它只會防止他們意外地與其他加入者組成法定人數。相反地,他們會連絡目前組態中的伺服器並採用最後提交的組態 (A, B, C),其中沒有加入者。加入者的組態檔案會在發生此情況時自動備份和替換。在連線到目前的領導者後,加入者會變成沒有投票權的追隨者,直到系統重新組態且他們被新增到整體(視情況而定,作為參與者或觀察者)。
- 每個加入者的初始組態包含最後提交組態中的伺服器 + 加入者本身,列為參與者。例如,若要將新伺服器 D 加入包含伺服器 (A、B、C) 的組態,管理員可以使用包含伺服器 (A、B、C、D) 的初始組態檔案啟動 D。如果 D 和 E 同時加入 (A、B、C),則 D 的初始組態可以是 (A、B、C、D),而 E 的組態可以是 (A、B、C、E)。類似地,如果同時加入 D 並移除 C,則 D 的初始組態可以是 (A、B、C、D)。切勿在初始組態中將多個加入者列為參與者(請參閱以下警告)。
- 無論將加入者列為觀察者或參與者,只要清單中包含目前的領導者,也可以不列出所有目前的組態伺服器。例如,在加入 D 時,如果 A 是目前的領導者,我們可以使用僅包含 (A、D) 的組態檔案啟動 D。然而,這比較脆弱,因為如果 A 在 D 正式加入整體之前發生故障,D 就不知道其他任何人,因此管理員必須介入並使用其他伺服器清單重新啟動 D。
註解
警告
切勿在相同的初始組態中指定多個加入伺服器作為參與者。目前,加入伺服器不知道它們正在加入現有的整體;如果將多個加入者列為參與者,它們可能會形成獨立的法定人數,造成腦裂情況,例如獨立於您的主要整體處理作業。在初始組態中將多個加入者列為觀察者是可以的。
如果現有伺服器的組態發生變更,或者在加入者成功連線並得知組態變更之前它們變得不可用,則可能需要使用更新的組態檔案重新啟動加入者,才能連線。
最後,請注意,加入者一旦連線到領導者,就會採用它缺席的最後提交組態(加入者的初始組態會在重新寫入之前備份)。如果加入者在此狀態下重新啟動,它將無法啟動,因為它不在其組態檔案中。若要啟動它,您必須再次指定初始組態。
修改伺服器參數:您可以透過使用不同的參數將伺服器加入整體,來修改伺服器任一埠口或其角色(參與者/觀察者)。這適用於增量和大量重新組態模式。無需移除伺服器再將其加回去;只要指定新參數,就像伺服器尚未在系統中一樣。伺服器會偵測組態變更並執行必要的調整。請參閱 增量模式 區段中的範例,以及 其他說明 區段中此規則的例外。
也可以變更整體使用的法定人數系統(例如,動態地將多數法定人數系統變更為階層式法定人數系統)。不過,這僅允許使用大量(非增量)重新組態模式。一般來說,增量重新組態僅適用於多數法定人數系統。大量重新組態適用於階層式和多數法定人數系統。
效能影響:移除追隨者時幾乎沒有效能影響,因為它並未自動關閉(移除的影響是伺服器的投票不再被計算)。新增伺服器時,沒有領導者變更,也沒有明顯的效能中斷。有關詳細資訊和圖表,請參閱 論文 中的圖 6、7 和 8。
最顯著的中斷將在以下情況之一發生領導者變更時發生
- 領導者從集合中移除。
- 領導者的角色從參與者變更為觀察者。
- 領導者用來將交易傳送給其他人的埠(法定人數埠)已修改。
在這些情況下,我們會執行領導者交接,舊領導者會提名新領導者。產生的不可用性通常比領導者崩潰時短,因為不需要偵測領導者故障,而且在交接期間通常可以避免選出新領導者(請參閱 論文 中的圖 6 和 8)。
當伺服器的用戶端埠已修改時,它不會中斷現有的用戶端連線。與伺服器的新連線必須使用新的用戶端埠。
進度保證:在重新配置操作的呼叫中,舊組態的過半數必須可用且已連線,ZooKeeper 才能進行進度。一旦呼叫重新配置,舊和新組態的過半數都必須可用。最後的轉換發生在 (a) 新組態已啟用,以及 (b) 負責人在新組態啟用前排定的所有操作都已提交。一旦 (a) 和 (b) 發生,只需要新組態的過半數。但是,請注意,客戶端看不到 (a) 或 (b)。特別是,當重新配置操作提交時,只表示負責人已傳送啟用訊息。這並不一定表示新組態的過半數收到此訊息(啟用時需要收到此訊息),或 (b) 已發生。如果要確定 (a) 和 (b) 都已發生(例如,為了知道關閉已移除的舊伺服器是安全的),可以簡單地呼叫更新(set-data
或其他過半數操作,但不是 sync
)並等待它提交。達成此目的的另一種方法是向重新配置通訊協定中加入另一輪(為了簡單和與 Zab 相容,我們決定避免此方法)。
遞增模式
增量模式允許新增和移除伺服器至目前的組態。允許多個變更。例如
> reconfig -remove 3 -add
server.5=125.23.63.23:1234:1235;1236
新增和移除選項都會取得逗號分隔的引數清單(沒有空格)
> reconfig -remove 3,4 -add
server.5=localhost:2111:2112;2113,6=localhost:2114:2115:observer;2116
伺服器陳述的格式與 指定用戶端埠 區段中所述的格式完全相同,且包含用戶端埠。請注意,這裡可以使用「5=」取代「server.5=」。在上述範例中,如果伺服器 5 已在系統中,但具有不同的埠或不是觀察者,則會更新該伺服器,並在配置提交後,該伺服器會成為觀察者,並開始使用這些新埠。這是一種將參與者變更為觀察者,反之亦然,或變更其任一埠的簡單方式,而且無需重新開機伺服器。
ZooKeeper 支援兩種法定人數系統:簡單多數系統(其中,領導者在收到多數投票者的確認後提交作業)和更複雜的階層系統,其中不同伺服器的投票權重不同,且伺服器會分組進行投票。目前,僅當領導者已知的最後一個建議配置使用多數法定人數系統時,才允許增量重新配置(否則會擲出 BadArgumentsException)。
增量模式 - 使用 Java API 的範例
List<String> leavingServers = new ArrayList<String>();
leavingServers.add("1");
leavingServers.add("2");
byte[] config = zk.reconfig(null, leavingServers, null, -1, new Stat());
List<String> leavingServers = new ArrayList<String>();
List<String> joiningServers = new ArrayList<String>();
leavingServers.add("1");
joiningServers.add("server.4=localhost:1234:1235;1236");
byte[] config = zk.reconfig(joiningServers, leavingServers, null, -1, new Stat());
String configStr = new String(config);
System.out.println(configStr);
還有一個非同步 API,以及一個接受以逗號分隔字串(而非 List
非遞增模式
重新配置的第二種模式是非增量的,其中用戶端會提供新的動態系統配置的完整規格。新的配置可以就地提供,或從檔案中讀取
> reconfig -file newconfig.cfg
//newconfig.cfg 是動態設定檔,請參閱 動態設定檔
> reconfig -members
server.1=125.23.63.23:2780:2783:participant;2791,server.2=125.23.63.24:2781:2784:participant;2792,server.3=125.23.63.25:2782:2785:participant;2793}}
新的配置可以使用不同的法定人數系統。例如,即使目前的成員組使用多數法定人數系統,您也可以指定階層法定人數系統。
批次模式 - 使用 Java API 的範例
List<String> newMembers = new ArrayList<String>();
newMembers.add("server.1=1111:1234:1235;1236");
newMembers.add("server.2=1112:1237:1238;1239");
newMembers.add("server.3=1114:1240:1241:observer;1242");
byte[] config = zk.reconfig(null, null, newMembers, -1, new Stat());
String configStr = new String(config);
System.out.println(configStr);
還有一個非同步 API,以及一個接受包含新成員(而非 List
條件式重新設定
有時(特別是在非增量模式中),新的建議配置會取決於用戶端「認為」目前的配置為何,且應僅套用於該配置。具體來說,只有當領導者最後一個配置具有指定的版本時,reconfig
才會成功。
> reconfig -file <filename> -v <version>
在先前列出的 Java 範例中,可以指定配置版本來設定重新配置條件,而不是 -1。
錯誤狀況
除了正常的 ZooKeeper 錯誤條件之外,重新配置可能會因下列原因而失敗
- 目前正在進行另一個重新配置(ReconfigInProgress)
- 建議的變更會讓叢集剩餘的參與者少於 2 個,如果已啟用獨立模式,或如果已停用獨立模式,則可以保持 1 個或更多參與者(BadArgumentsException)
- 重新配置處理開始時,新的配置沒有法定人數連線,且與領導者不同步(NewConfigNoQuorum)
- 指定了
-v x
,但最新組態的版本y
不是x
(BadVersionException) - 要求增量式重新組態,但 leader 中的最後組態使用與多數系統不同的法定人數系統 (BadArgumentsException)
- 語法錯誤 (BadArgumentsException)
- 從檔案讀取組態時發生 I/O 例外 (BadArgumentsException)
大多數這些情況都由 ReconfigFailureCases.java 中的測試案例說明。
其他說明
活性:為了更了解增量式和非增量式重新組態之間的差異,假設客戶端 C1 將伺服器 D 新增到系統,而不同的客戶端 C2 將伺服器 E 新增到系統。對於非增量模式,每個客戶端會先呼叫 config
以找出目前的組態,然後透過新增它自己建議的伺服器在本地端建立新的伺服器清單。新的組態接著可以使用非增量 reconfig
指令提交。在兩個重新組態都完成後,只會新增 E 或 D 中的一個 (不會兩個都新增),這取決於哪個客戶端的請求第二個到達 leader,覆寫先前的組態。其他客戶端可以重複這個程序,直到其變更生效。此方法保證系統範圍的進度 (也就是對於其中一個客戶端),但不保證每個客戶端都會成功。為了有更多控制權,C2 可以要求僅在目前組態的版本未變更的情況下執行重新組態,如 條件重新組態 區段中所說明。這樣一來,如果 C1 的組態先到達 leader,它就可以避免盲目覆寫 C1 的組態。
對於增量式重新組態,兩個變更都會生效,因為它們只是由 leader 依序套用至目前的組態,無論目前的組態為何 (假設第二個重新組態請求在 leader 傳送第一個重新組態請求的提交訊息後到達 leader -- 目前 leader 會拒絕提出重新組態,如果已經有另一個重新組態正在處理中)。由於保證兩個客戶端都會進展,因此此方法保證更強的活性。實際上,多個同時進行的重新組態可能很少見。目前,非增量式重新組態是動態變更法定人數系統的唯一方式。目前僅允許在多數法定人數系統中使用增量式組態。
將觀察者變更為追隨者:顯然地,如果發生錯誤 (2),也就是如果參與投票的伺服器數量少於允許的最小數量,則將參與投票的伺服器變更為觀察者可能會失敗。然而,將觀察者轉換為參與者有時可能會因更微妙的原因而失敗:例如,假設目前的組態為 (A、B、C、D),其中 A 為領導者,B 和 C 為追隨者,而 D 為觀察者。此外,假設 B 已發生故障。如果提交一個重新組態,其中 D 被指定為追隨者,則會因錯誤 (3) 而失敗,因為在此組態中,新組態中的多數投票者(任何 3 個投票者)都必須連線並與領導者保持最新狀態。觀察者無法確認在重新組態期間傳送的歷程記錄前綴,因此不會計算在這些 3 個必要的伺服器中,而重新組態將會中止。如果發生這種情況,客戶端可以透過兩個重新組態命令來達成相同的任務:首先呼叫一個重新組態來從組態中移除 D,然後呼叫第二個命令來將其新增回組態中作為參與者(追隨者)。在中間狀態期間,D 是非投票追隨者,並且可以在第二個重新組態命令期間確認狀態轉移。
重新平衡用戶端連線
當啟動 ZooKeeper 群集時,如果每個客戶端都獲得相同的連線字串(伺服器清單),則客戶端將會從清單中隨機選擇一個伺服器來連線,這使得每個伺服器的預期客戶端連線數相同。我們實作了一個方法,當伺服器組透過重新組態變更時,此方法會保留此屬性。請參閱 論文 中的第 4 節和第 5.1 節。
為了讓此方法運作,所有客戶端都必須訂閱組態變更(透過直接或透過 getConfig
API 命令對 /zookeeper/config 設定監控)。當觸發監控時,客戶端應透過呼叫 sync
和 getConfig
來讀取新組態,如果組態確實是新的,則呼叫 updateServerList
API 命令。為了避免同時發生大量客戶端移轉,最好讓每個客戶端在呼叫 updateServerList
之前隨機休眠一小段時間。
可以在下列找到幾個範例:StaticHostProviderTest.java 和 TestReconfig.cc
範例(這不是食譜,而是一個簡化的範例,僅用於說明一般概念)
public void process(WatchedEvent event) {
synchronized (this) {
if (event.getType() == EventType.None) {
connected = (event.getState() == KeeperState.SyncConnected);
notifyAll();
} else if (event.getPath()!=null && event.getPath().equals(ZooDefs.CONFIG_NODE)) {
// in prod code never block the event thread!
zk.sync(ZooDefs.CONFIG_NODE, this, null);
zk.getConfig(this, this, null);
}
}
}
public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
if (path!=null && path.equals(ZooDefs.CONFIG_NODE)) {
String config[] = ConfigUtils.getClientConfigStr(new String(data)).split(" "); // similar to config -c
long version = Long.parseLong(config[0], 16);
if (this.configVersion == null){
this.configVersion = version;
} else if (version > this.configVersion) {
hostList = config[1];
try {
// the following command is not blocking but may cause the client to close the socket and
// migrate to a different server. In practice it's better to wait a short period of time, chosen
// randomly, so that different clients migrate at different times
zk.updateServerList(hostList);
} catch (IOException e) {
System.err.println("Error updating server list");
e.printStackTrace();
}
this.configVersion = version;
}
}
}