1. 程式人生 > 其它 >zookeeper 客戶端_ZooKeeper原理ZooKeeper網路故障應對法

zookeeper 客戶端_ZooKeeper原理ZooKeeper網路故障應對法

技術標籤:zookeeper 客戶端zookeeper 遠端主機強迫關閉一個連線zookeeper 遠端主機強迫關閉了一個現有的連線。zookeeper客戶端zookeeper遠端主機強迫關閉了一個現有的連線

網路故障可以說是分散式系統一生之敵。如果永遠不發生網路故障,我們實際上可以設計出高可用強一致的分散式系統。可惜的是不發生網路故障的分散式環境還不存在,ZooKeeper 使用過程中也需要小心的應付網路故障。

在介紹應對網路故障之前,我們首先看到沒有故障的時候 ZooKeeper 對網路連線的處理。ZooKeeper 客戶端啟動時從配置中讀取所有可用的伺服器的資訊,它會隨機嘗試和其中一臺伺服器連線,如果成功地建立起連線,ZooKeeper 客戶端和服務端會建立起一個會話(session)。在會話超時之前,伺服器會響應客戶端的請求,每次新的請求都會重新整理會話超時的時間,在沒有業務請求的情況下,客戶端也會通過定期的心跳來維持會話。當 ZooKeeper 客戶端和當前連線的伺服器失聯時,客戶端會試著從可用的伺服器列表中重新連線到一臺伺服器上。

總體地看過 ZooKeeper 正常的網路連線處理之後,我們來看看網路故障在 ZooKeeper 中的抽象。網路故障在 ZooKeeper 的層面被轉換為兩種異常,一種是 ConnectionLossException ,一種是 SessionExpireException。前者發生在 ZooKeeper 客戶端與某臺伺服器斷開之後,後者發生在 ZooKeeper 伺服器通知客戶端會話超時的時候。

ConnectionLossException

這個異常肯定是 ZooKeeper 中最讓人頭痛的異常之一了。ZooKeeper 客戶端通過 socket 和 ZooKeeper 叢集的某臺伺服器連線,這個連線在客戶端由 ClientCnxn 管理,在服務端由 ServerCnxn 管理。ConnectionLossException 在 ZooKeeper 客戶端與伺服器的連線異常關閉的時候丟擲,它僅僅表明 ZooKeeper 客戶端發現自己與當前伺服器的連線斷開,除此之外什麼也不知道。因為這個不知道,我們要對不同的斷開原因進行探測和處理。

從可恢復的故障中恢復

ConnectionLossException 是一個可恢復的異常,它僅代表 ZooKeeper 客戶端與當前伺服器的連線斷開。ZooKeeper 客戶端完全有可能稍後連線上另一個伺服器並重新開始傳送請求。在 ZooKeeper 叢集網路不穩定的情況下,我們要特別小心地處理這類異常,而不是直接層層外拋。否則,因為網路抖動導致上層應用崩潰是不可接受的。此外,在這種異常情況下,重新建立一個 ZooKeeper 客戶端開啟一個新的會話,只會加劇網路的不穩定性。這是因為 ZooKeeper 客戶端不重連的情況下,伺服器只能通過會話超時來釋放與客戶端的連線。如果由於連線過多導致響應不穩定,開啟新的會話只會惡化這個情況。這種情況有點類似於 DDOS 攻擊。

一種常見的容忍 ConnectionLossException 的方式是重做動作,也就是形如下面程式碼的處理邏輯

operation(...) {  zk.create(path, data, ids, mode, callback, data);}callback = (rc, path, ctx, name) -> {  switch (Code.get(rc)) {    case CONNECTIONLOSS:      operation(...);      break;    // ...  }}

服務端上操作可能已經成功

在上一節中,我們介紹了通過重做動作來從 ConnectionLossException 中恢復的方法。然而,重做動作是有風險的,這是因為先前的動作可能在客戶端上已經成功。如果當前動作是寫動作且不冪等,就可能在應用層面觀察到意圖與實際執行的操作不一致。

ConnectionLossException 僅代表 ZooKeeper 客戶端與當前伺服器的連線斷開。但是,在斷開之前,對應的請求完全可能已經發送出去,已經到達服務端並被處理。只是由於客戶端與伺服器的連線斷開,導致 ZooKeeper 客戶端沒收到迴應而丟擲 ConnectionLossException 罷了。

如前所述,對於讀操作,重試通常沒有什麼問題,因為我們總能得到重試成功的時候讀操作應有的返回值或異常。對於寫操作,我們具體展開討論。

對於 setData 操作,在重試成功的情況下,不考慮具體的業務邏輯,我們可以認為問題不大。因為兩次把節點設定為同一個值是冪等操作,對於前一次操作更新了 version 從而導致重試操作 version 不匹配的情況,我們也可以對應處理這種可解釋的異常。這類似於 HTTP 中 PUT 方法的語義。

對於 delete 操作,重試可能導致意外的 NoNodeException,我們可以吞掉這個異常或者觸發業務相關的異常邏輯。

對於 create 操作,情況稍微複雜一點。在不帶 sequential 要求的情況下,create 可能成功或者觸發一個 NodeExistException,可以採取跟 delete 對應的處理方式;在 sequential 的情況下,有可能先前的操作已經成功,而重試的操作也成功,也就是建立了兩個 sequential 節點。由於我們丟失了先前操作的返回值,因此先前操作的 sequential 節點就成了孤兒,這有可能導致資源洩露或者更嚴重的一致性問題。例如,基於 ZooKeeper 的一種 leader 選舉演算法依賴於 sequential 節點的排序,一個序號最小的孤兒節點將導致整個演算法失敗。在這種情況下,孤兒節點獲取了 leader 許可權,但其 callback 卻在早前被 ConnectionLossException 觸發了,因此當選 leader 及後續響應無法觸發。同時,由於該節點成為孤兒,它也不會被刪除,從而演算法不再往下執行。

客戶端可能錯過狀態變化

ZooKeeper 的 Watcher 是單次觸發的,在前一次 Watcher 被觸發到重新設定 Watcher 並被觸發的間隔之間的事件可能會丟失。這本身是 ZooKeeper 上層應用需要考慮的一個重要的問題。ConnectionLossException 觸發 Watcher 接收到一個 WatchedEvent(EventType.None, KeeperState.Disconnected) 的事件。一旦收到這個事件,ZooKeeper 客戶端必須假定 ZooKeeper 上的狀態可能發生任意變化。對於依賴於某些狀態例如自己是應用程式中的 leader 的動作,需要掛起動作,在恢復連結後確認狀態之後再重新執行動作。

這裡有一個設計上的細節需要注意,不同於一般的 WatchedEvent 會在觸發 Watcher 後將其移除,EventType.None 的 WatchedEvent 在不設定系統屬性 zookeeper.disableAutoWatchReset=true 的情況下只會觸發 Watcher 而不將其移除。同時,在成功重新連線伺服器之後會將當前的所有 Watcher 通過 setWatches 請求重新註冊到伺服器上。伺服器通過對比 zxid 的數值來判斷是否觸發 Watcher。從而避免了由於網路抖動而強迫使用者程式碼在 Watcher 的處理邏輯中處理 ConnectionLossException 並重新執行操作設定 Watcher 的負擔。特別的,當前客戶端上註冊的所有的 Watcher 都將受到網路抖動的影響。但是要注意重新註冊的 Watcher 中監聽 NodeCreated 事件的 Watcher 可能會錯過該事件,這是因為在重新建立連線的過程中該節點由於其他客戶端的動作可能先被建立後被刪除,由於僅就有無節點判斷而沒有 zxid 來幫助判斷,這被稱為 ABA 問題。

SessionExpiredException

這個異常比 ConnectionLossException 稍好處理一些,因為它是不可恢復的故障。ZooKeeper 客戶端會話超時之後無法重新和伺服器成功連線。因此,我們通常只需要重新建立一個 ZooKeeper 客戶端例項並重新開始開始工作。但是會話超時會導致 ephemeral 節點被刪除,如果上層應用邏輯與此相關的話,就需要仔細的處理 SessionExpiredException。

會話超時的檢測

ZooKeeper 客戶端與伺服器成功建立連線後,ClientCnxn.SendThread 會週期性的向伺服器傳送 ping 資訊,伺服器在處理 ping 資訊的時候重置會話超時的時間。如果伺服器在超時時間內沒有收到客戶端發來的任何新的資訊,那麼它就會宣佈這個會話超時,並顯式的關掉對應的連結。ZooKeeper 會話超時相關的邏輯在 SessionTracker 中。所有會話檢查和超時的判斷都是由 ZooKeeper 叢集的 leader 作出的,也就是所謂的仲裁動作(quorum operation),因此客戶端超時是所有伺服器一致的共識。

如果 ZooKeeper 客戶端嘗試重新連線伺服器,當它重新連線上某個伺服器時,該伺服器查詢會話列表,發現這個重連請求屬於超時會話,通過返回非正整數的超時剩餘時間通知客戶端會話已超時。隨後,ZooKeeper 客戶端得知自己已經超時並執行相應的退出邏輯。

這裡有一個非常 tricky 的事情,就是 ZooKeeper 客戶端的會話超時永遠是由服務端通知的。考慮這樣一種超時情況,在伺服器掛了或者客戶端與伺服器網路分割槽的情況下,ZooKeeper 客戶端是無法得知自己的會話已經超時的。ZooKeeper 目前沒有辦法處理這一情況,只能依賴上層應用自己去處理。例如,通過其他邏輯確定會話已超時之後,主動地關閉 ZooKeeper 客戶端並重啟。在 Curator 中,這種檢測是通過 ConnectionStateManager#processEvents 週期性的檢測在收到最後一個 disconnect 事件後過去的時間,從而在必然超時的時候通過反射向 ZooKeeper 客戶端注入會話超時事件。

ephemeral 節點的刪除

ephemeral 節點的刪除相關的最大的問題是關於基於 ZooKeeper 的 leader 選舉的。ZooKeeper 提供了 leader 選舉的 recipe 參考[1],總的來說是基於一系列 ephemral sequential 節點的排序來做的。當上層應用採用這種演算法做 leader 選舉時,如果 ZooKeeper 客戶端與伺服器超時,由於 ZooKeeper 相關的操作和相應往往和上層應用的主執行緒是分開的,這樣在上層應用得知自己不是 leader 之前就有可能作出很多越權的操作。

例如,在 FLINK 的設計中,只有成為 leader 的 JobManager 才有許可權寫 checkpoint 資料。但是,由於丟失 leadership 的訊息從 ZooKeeper 叢集傳遞到 ZooKeeper 客戶端再到通知上層應用,這幾個步驟之間都是非同步的,所以此前的 leader 並不能第一時間得知自己丟失 leadership 了。同時,其他的 JobManager 可能併發地被通知當選 leader。此時,叢集中就會有兩個 JobManager 認為自己是 leader,也就是所謂的雙主情形。如果對它們寫 checkpoint 資料的動作不做其他限制,就可能導致兩個 leader 併發地寫 checkpoint 資料而導致狀態不一致。這個由於響應時間帶來的問題在 Curator 的技術注意事項中已有提及[2],由於發生概率較小,而且僅在未能及時響應 ZooKeeper 伺服器資訊時才會發生,因此雖然不少系統都有這個理論上的 BUG,但是很多時候只是作為注意事項幫助開發者和使用者在極端情況下理解發生了什麼。

FLINK-10333[3] 和 ZooKeeper 郵件列表上我發起的這個討論[4]詳細討論了這種情況下面臨的挑戰和解決方法。

[1] https://zookeeper.apache.org/doc/r3.5.5/recipes.html#sc_leaderElection
[2]https://cwiki.apache.org/confluence/display/CURATOR/TN10
[3] https://issues.apache.org/jira/browse/FLINK-10333
[4] https://lists.apache.org/x/thread.html/[email protected]%3Cuser.zookeeper.apache.org%3E

HBase官方社群推薦必讀好文

HBase 測試|HBase 2.2.1隨機讀寫效能測試

HBase 原理|HBase 記憶體管理之 MemStore 進化論

HBase 實踐|說好不哭,但 HBase 2.0 真的好用到哭

HBase 抗戰總結|阿里巴巴 HBase 高可用8年抗戰回憶錄

關注 HBase技術社群,獲取更多技術乾貨

33010623137dbad826dff31ada2766ad.png

你也「在看」嗎??