<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ZooKeeper 原理 | ZooKeeper 狀態(tài)變化應(yīng)對法

          共 6133字,需瀏覽 13分鐘

           ·

          2021-03-14 16:45

          本文基于 ZooKeeper(ZK) 3.6.0 版本介紹應(yīng)對狀態(tài)變化的策略。

          ZK 的常見用途包括同步配置、服務(wù)發(fā)現(xiàn)和協(xié)同分布式過程等,這些用途都要求應(yīng)用程序能夠監(jiān)聽 ZK 節(jié)點集合的狀態(tài)。

          為了達(dá)到這個目的,ZK 客戶端可以輪詢 ZK 集合以獲取狀態(tài)。然而,輪詢并不是最佳的狀態(tài)監(jiān)聽方式。對于頻繁變化的狀態(tài),輪詢可能會錯過某些狀態(tài)變化;對于偶爾變化的狀態(tài),輪詢可能會導(dǎo)致額外的開銷。

          基于這樣的觀察,ZK 提供了 Watcher 機制,能夠避開輪詢來應(yīng)對狀態(tài)變化。這個機制允許應(yīng)用程序在特定的 znode 上注冊 Watcher 以在 znode 的狀態(tài)發(fā)生變化的時候收到通知。

          通過 Watcher 機制應(yīng)對狀態(tài)變化可以采用如下的框架代碼。

          zk.exist("/myZnode", myWatcher, existsCallback, ctx);
          Watcher myWatcher = new Watcher() { public void process(WatchedEvent e) { // process the watch event }}
          StatCallback existsCallback = new StatCallback() {  public void processResult(int rc, String path, Object ctx, Stat stat) { // process the result of the exist call }}

          這里我們設(shè)置了兩個回調(diào)邏輯。

          ?StatCallback 對應(yīng) ZK 客戶端異步請求的回調(diào),具體地說,是 exist 請求。?Watcher 對應(yīng)節(jié)點變化時 ZK 客戶端收到 WatchedEvent 后的回調(diào)。

          這是兩個不同的回調(diào)邏輯,一個在客戶端請求完成是被調(diào)用,一個在監(jiān)視 znode 狀態(tài)發(fā)生變化是被調(diào)用。

          WatchedEvent 的類型

          可以看到,Watcher 的回調(diào)邏輯主要處理的是 WatchedEvent 對象,它是一個包括 KeeperState 和 EventType 兩個字段的數(shù)據(jù)對象。我們從分類的角度來看需要處理的 WatchedEvent 對象都有哪些可能。

          不同 KeeperState 的事件

          從 KeeperState 的角度來說,總共包含這幾種會話狀態(tài)。

          ?SyncConnected?ConnectedReadOnly?Disconnected?Expired?Closed?AuthFailed?SaslAuthFailed

          所有的會話狀態(tài)從名字即可看出其含義。除了 SyncConnected 之外,所有的 KeeperState 類型都會對應(yīng)到 EventType.None 事件類型,這代表沒有節(jié)點狀態(tài)變化,而是會話狀態(tài)發(fā)生了變化。

          ZK 使用了相同的 Watcher 機制來處理應(yīng)用程序相關(guān)事件的通知。這是某種程度的重載,在簡化了類別區(qū)分和工程實現(xiàn)的同時也增加了用戶區(qū)分重載的心智負(fù)擔(dān)。

          不同 EventType 的事件

          SyncConnected 狀態(tài)代表會話正常,除了第一次連接上 ZK 時有一個 EventType.None 事件類型的通知,其后都會與下列事件類型的某一種相關(guān)聯(lián)。

          ?NodeCreated?NodeDeleted?NodeDataChanged?NodeChildrenChanged?DataWatchRemove?ChildWatchRemoved?PersistentWatchRemoved

          其中最后三種事件對應(yīng) 3.5 和 3.6 版本以后支持的 Watcher 移除功能和永久 Watcher 對象的移除功能,我們稍后展開其細(xì)節(jié)。

          前四種事件從名字即可看出其內(nèi)容,分別代表節(jié)點被創(chuàng)建、刪除、改變內(nèi)容或節(jié)點的子節(jié)點發(fā)生改變。對于 ZK 對節(jié)點狀態(tài)變化的抽象,有三點需要注意。

          第一點,節(jié)點狀態(tài)變化事件實現(xiàn)上是一個單純的不帶信息的枚舉。

          換句話說,ZK 產(chǎn)生的節(jié)點狀態(tài)變化事件僅僅表達(dá)某事件已發(fā)生,而無法確定事件的具體內(nèi)容。特別是對于 NodeChildrenChanged 事件,僅代表節(jié)點的子節(jié)點發(fā)生改變,到底是新增子節(jié)點、子節(jié)點刪除還是子節(jié)點數(shù)據(jù)變化,都不清楚。這就要求 ZK 客戶端在收到事件時必須主動再次發(fā)出請求查詢節(jié)點的狀態(tài)或數(shù)據(jù)內(nèi)容。

          這不同于 etcd 中帶有變更內(nèi)容的事件。如果通知中包含變更內(nèi)容,我們就有可能在客戶端僅接受一次通知,即在本地維護(hù)數(shù)據(jù)緩存的狀態(tài),從而無需再次請求查詢節(jié)點。

          第二點,由于上述原因,ZK 原始的 Watcher 機制可能導(dǎo)致應(yīng)用程序錯過中間過程的節(jié)點狀態(tài)變化。

          由于 ZK 實現(xiàn)上采用單次觸發(fā)語義,即設(shè)置的 Watcher 在出現(xiàn)狀態(tài)變化時觸發(fā)一次并被移除,Watcher 被移除后該節(jié)點發(fā)生的事件不再被監(jiān)視。即使 ZK 客戶端在收到事件后重新設(shè)置 Watcher 監(jiān)視,由于網(wǎng)絡(luò)傳輸天然的異步性質(zhì),仍然有可能錯過事件。

          在《ZooKeeper 分布式過程協(xié)同技術(shù)詳解》一書中為這一點開脫時提到,既然每次都需要重新拉取狀態(tài),那么單次觸發(fā)能夠在事件頻發(fā)的情況下減少事件平均產(chǎn)生的通知數(shù)量。但是實踐當(dāng)中這幾乎不成為一個好處,而從用戶角度來說卻要麻煩地處理復(fù)雜的異步情況。

          ZK 3.6.0 版本引入了 Persistent (Recursive) Watcher 類型,能夠支持 Watcher 多次觸發(fā),使 ZK 客戶端不會錯過任何一個事件。

          第三點,Watcher 在服務(wù)器分為 data/child 兩類,在客戶端對其注冊分為 exist/data/child 三類。

          從客戶端角度來看,exist 請求能夠?qū)θ我饴窂接绕涫巧形磩?chuàng)建的路徑設(shè)置 Watcher 監(jiān)視,因此獨立擁有一類 Watcher 來處理。getData 和 getChildren 只能對已經(jīng)存在的節(jié)點路徑設(shè)置 Watcher 監(jiān)視,又根據(jù)其監(jiān)視的是節(jié)點本身或節(jié)點的子節(jié)點,分別擁有一類 Watcher 來處理。

          從服務(wù)器角度來看,DataWatcher 僅監(jiān)視確切路徑對應(yīng)的節(jié)點,在節(jié)點創(chuàng)建、刪除或者數(shù)據(jù)更改時產(chǎn)生通知,而 ChildWatcher 只有在節(jié)點的子節(jié)點發(fā)生變化時產(chǎn)生通知。由于服務(wù)器端的 DataWatcher 無所謂是否路徑對應(yīng)的節(jié)點是否存在,因此就少了一個分類。換個角度看,也可以認(rèn)為在 ZK 客戶端驅(qū)動設(shè)置 Watcher 時已經(jīng)進(jìn)行過檢查,所以服務(wù)器端的 DataWatcher 可以合并情況而不會錯誤的產(chǎn)生節(jié)點狀態(tài)變化事件。

          服務(wù)器的實現(xiàn)里,DataWatcher 和 ChildWatcher 由不同的集合管理,它們會響應(yīng)不同的事件。在移除 Watcher 時,可以指定 Watcher 的類型是 DataWatcher 還是 ChildWatcher 又或者不做區(qū)分來移除。另外,上面提到的 PersistentWatcher 首先屬于 DataWatcher 類型,如果設(shè)置了 Recursive 參數(shù),則同時還是 ChildWatcher 類型。

          Watcher 的生命周期

          接下來,我們針對 Watcher 的設(shè)置到觸發(fā)的整個生命周期及其中可能遇到的異常做一個介紹。

          客戶端上 Watcher 的生命周期

          Watcher 的設(shè)置從 ZK 客戶端開始,具體的分類不再做闡述,主要追蹤代碼的調(diào)用路徑。

          以 exist 為例,用戶傳入的 Watcher 對象或創(chuàng)建此客戶端時設(shè)置的默認(rèn) Watcher 被使用時,有兩個代碼路徑被激活。

          其之一是發(fā)送到服務(wù)器的請求的 watch 字段將被設(shè)置為 true 以代表這次請求包含一個 Watcher 設(shè)置的請求。

          其之二是 Watcher 被包裝在 ExistsWatchRegistration 中。經(jīng)過網(wǎng)絡(luò)層面的等候,在設(shè)置 Watcher 的請求成功時,調(diào)用鏈進(jìn)入ClientCnxn#finishPacket 方法中,調(diào)用 ExistsWatchRegistration 的 register 方法在客戶端緩存 Watcher 的信息。

          緩存的 Watcher 信息主要用于支持 Watcher 在網(wǎng)絡(luò)錯誤的情況下重新設(shè)置以及 Watcher 的移除。

          重新設(shè)置 Watcher 具體來說,是在發(fā)生 ConnectionLoss 異常時,當(dāng) ZK 客戶端成功重新連接到服務(wù)器之后,根據(jù)自己緩存的 Watcher 信息,向服務(wù)器發(fā)送一個 SetWatches 請求以重新設(shè)置此前還未觸發(fā)的 Watcher 集合。

          這個功能避免了用戶必須為了重新設(shè)置 Watcher 而響應(yīng) ConnectionLoss 異常的負(fù)擔(dān),尤其是在 ConnectionLoss 異常發(fā)生時,無法預(yù)知節(jié)點發(fā)生了何種變化。

          另一方面,服務(wù)器端會比對 SetWatches 請求的各個 Watcher 對應(yīng)路徑節(jié)點的信息,根據(jù)是否存在節(jié)點以及節(jié)點最后處理的事務(wù) ID 和子節(jié)點事務(wù) ID 來判斷是否應(yīng)該產(chǎn)生事件。

          這里有兩個需要注意的點。

          一個是,服務(wù)器判斷是否應(yīng)該產(chǎn)生事件存在 False Negative 誤判的情況。具體來說,在通過 exist 設(shè)置 Watcher 時,對應(yīng)節(jié)點如果此前不存在,對應(yīng) Watcher 將被分類為 SetWatches 請求中的 existWatcher 類型。此時,如果該節(jié)點經(jīng)歷了創(chuàng)建后又刪除的事件,則無法被 Watcher 所感知。這也是分布式系統(tǒng)常見問題中所謂的 ABA 問題。

          另一個是 SetWatchers 包含的 Watcher 集合是設(shè)置 Watcher 的成功請求的 Watcher 集合。前文提到,如果使用 ZK 客戶端的異步請求接口,實際上會有兩個回調(diào)。雖然 ZK 能夠自動處理已經(jīng)注冊上的 Watcher 的容錯,但是如果異步請求本身沒有成功,則無法被這個自動重設(shè)機制覆蓋。在這種情況下,應(yīng)用程序應(yīng)該自行重新執(zhí)行請求和設(shè)置 Watcher 的操作。

          關(guān)于 Watcher 的移除,這是在 3.5.0 版本引入的新功能。

          在此前的 ZK 版本中,Watcher 一旦設(shè)置就無法主動移除。Watcher 的移除只有兩種途徑,一是 Watcher 被觸發(fā),二是會話超時或被關(guān)閉。應(yīng)用程序在某些情況下希望根據(jù)外部狀態(tài)的變化主動移除 Watcher,尤其是 Watcher 數(shù)量巨大,而外部狀態(tài)指示這些 Watcher 再也不可能被觸發(fā)的情況下防止內(nèi)存泄漏的場景。移除 Watcher 指令確定移除集合也依賴于前文提到的本地緩存信息。同時需要定義新的客戶端和服務(wù)器的通信文本,在成功執(zhí)行時將產(chǎn)生前文所提的幾種 WatchRemoved 事件。

          服務(wù)器上 Watcher 的生命周期

          前面提到,在客戶端調(diào)用接口是設(shè)置 Watcher 的場景下,網(wǎng)絡(luò)請求包中的 watch 字段將被設(shè)置。這一信息將在服務(wù)器被處理。

          簡單來說,經(jīng)過一系列序列化和請求處理責(zé)任鏈的檢查和裝飾后,設(shè)置 Watcher 的請求最終被發(fā)往 ZKDatabase 并委托給 DataTree 處理。DataTree 是服務(wù)器管理節(jié)點視圖的類。在進(jìn)行請求響應(yīng)的調(diào)用時,會根據(jù)請求中 watch 字段的布爾值來判斷是否要設(shè)置一個服務(wù)器一側(cè)的 Watcher 對象。

          服務(wù)器一側(cè)的 Watcher 對象實際上是一個 ServerCnxn 的實例,它繼承自 Watcher 接口。當(dāng)然,概念上來說這樣的繼承有點勉強,更好的實現(xiàn)方式是采用組合來替代繼承,以明確 ServerCnxn 本身不是 Watcher 接口,而是代行 Watcher 的職責(zé)。

          具體來說,ServerCnxn 是服務(wù)器一側(cè)維護(hù)的網(wǎng)絡(luò)連接對象,DataTree 在處理設(shè)置 Watcher 的請求時會將路徑和 ServerCnxn 這個 Watcher 相關(guān)聯(lián),并且在 DataTree 的內(nèi)部維護(hù)一系列的 Watcher 對象的信息。在處理節(jié)點創(chuàng)建、刪除或數(shù)據(jù)改變等操作時,根據(jù)節(jié)點視圖和 Watcher 信息判斷應(yīng)該觸發(fā)哪些 Watcher,而 Watcher 的觸發(fā)正是執(zhí)行 ServerCnxn 的 process 方法的邏輯,即向客戶端發(fā)送相應(yīng)的事件。

          DataTree 觸發(fā)相應(yīng)事件將委托到服務(wù)器一側(cè)的 WatchManager 處理,WatcherManager 在事件產(chǎn)生時調(diào)用 triggerWatch 方法來調(diào)用 ServerCnxn 的 process 方法。同時對于單次觸發(fā)的 Watcher 對象,將其從 Watcher 集合中移除,而對于 Persistent 的 Watcher 對象,則進(jìn)行保留。

          關(guān)于 Persistent Watcher,最后還有幾個需要討論的點。具體內(nèi)容可以參考 ISSUE [^1][^2] 和 GitHub PR[^3] 上的討論。

          第一個,Persistent Watcher 提供是否是 Recursive 的選項,設(shè)置 Recursive 將同時監(jiān)聽給定路徑對應(yīng)的節(jié)點及其子節(jié)點,否則只監(jiān)聽給定路徑對應(yīng)的節(jié)點。對于監(jiān)聽子節(jié)點的情況,為了減少每次遞歸查找的負(fù)擔(dān),實現(xiàn)上有一個 PathParentIterator 的類來迭代獲取變更節(jié)點的父節(jié)點的優(yōu)化。

          第二個,Persistent Watcher 對于前文和 etcd 做對比的情況,僅解除了單次觸發(fā)的限制,能夠?qū)Χ鄠€狀態(tài)變化對應(yīng)的產(chǎn)生事件。但是,事件的內(nèi)容仍然僅是事件已發(fā)生,而不包括事件的具體變更內(nèi)容。客戶端在收到事件后仍然要重新請求獲取節(jié)點狀態(tài)。為了追溯變更內(nèi)容,通常來說需要引入某種 MVCC 的存儲。

          第三個,Persistent Watcher 和前文所提的 Watcher 自動容忍網(wǎng)絡(luò)錯誤有一定的沖突。在具體的實現(xiàn)中,發(fā)生網(wǎng)絡(luò)錯誤并重設(shè) Watcher 集合的時候,僅僅把 Persistent Watcher 重新設(shè)置,而不會檢測是否需要觸發(fā)期間錯過的事件。這并沒有什么功能上的考量或者優(yōu)點,僅僅是實現(xiàn)上會更復(fù)雜而需要 Persistent Watcher 這個特性的 Curator 庫能夠主動的處理這個問題。

          [^1] https://issues.apache.org/jira/browse/ZOOKEEPER-153

          [^2] https://issues.apache.org/jira/browse/ZOOKEEPER-1416

          [^3] https://github.com/apache/zookeeper/pull/1106


          瀏覽 61
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  爆乳尤物一区二区三区 | 午夜精品一区二区三区国产静华液 | 伊人久久国产精品视频 | 在线免费观看黄色视频 | 亚洲男女操逼 |