故障發生的主要點有三個:node
如圖5-1所展現的簡單結構數據庫
三個服務器組成了ZooKeeper的服務。進程會隨機鏈接到其中一個服務器,也可能斷開後再次鏈接到另外一個不一樣的服務器,服務器使用內部協議來保持客戶端之間狀態的同步,對客戶端呈現一致性視圖。
![]()
圖5-2展現了系統的不一樣組件中可能發生的一些故障服務器
一個客戶端從ZooKeeper得到響應時,客戶端能夠很是確定這個響應信息與其餘響應信息或其餘客戶端所接收的響應均保持一致性。有時,ZooKeeper客戶端庫與ZooKeeper服務的鏈接會丟失,並且沒法提供一致性保障的信息,當客戶端庫發現本身處於這種狀況時,就會使用Disconnected事件和ConnectionLossException異常來表示本身沒法瞭解當前的系統狀態。網絡
ZooKeeper客戶端庫會積極地嘗試,使本身離開這種狀況,它會不斷嘗試從新鏈接另外一個ZooKeeper服務器,直到最終從新創建了會話。一旦會話從新創建,ZooKeeper會產生一個SyncConnected事件,並開始處理請求。多線程
ZooKeeper還會註冊以前已經註冊過的監視點,並會對失去鏈接這段時間發生的變動產生監視點事件。併發
Disconnected事件和ConnectionLossException異常的產生的一個典型緣由是由於ZooKeeper服務器故障。
圖5-3展現了這種故障的一個示例異步
客戶端鏈接到服務器s2 ,其中s2 是兩個活動ZooKeeper服務器中的一個,當s2 發生故障,客戶端的Watcher對象就會收到Disconnected事件,而且,全部進行中的請求都會返回ConnectionLossException異常。
若是此時客戶端正在進行某些請求,好比剛剛提交了一個create操做的請求,當鏈接丟失發生時,對於同步請求,客戶端會獲得ConnectionLossException異常,對於異步請求,會獲得CONNECTIONLOSS返回碼。然而,客戶端沒法經過這些異常或返回碼來判斷請求是否已經被處理。一個很是糟糕的方法是簡單處理,當接收到ConnectionLossException異常或CONNECTIONLOSS返回碼時,客戶端中止全部工做,並從新啓動,雖然這樣可使代碼更加簡單,可是,本多是一個小影響,卻變爲重要的系統事件。分佈式
當一個進程失去鏈接後就沒法收到ZooKeeper的更新通知,儘管這聽起來很沒什麼,可是一個進程也許會在會話丟失時錯過了某些重要的狀態變化。spa
圖5-4展現了這種狀況的例子線程
客戶端c1做爲羣首,在t2 時刻失去了鏈接,可是並沒發現這個狀況,直到t4 時刻才聲明爲終止狀態,同時,會話在t2 時刻過時,在t3 時刻另外一個進程成爲羣首,從t 2 到t 4 時刻舊的羣首並不知道它本身被聲明爲終止狀態,而另外一個羣首已經接管控制。
![]()
若是開發者不仔細處理,舊的羣首會繼續擔當羣首,而且其操做可能與新的羣首相沖突。所以,當一個進程接收到Disconnected事件時,在從新鏈接以前,進程須要掛起羣首的操做。正常狀況下,從新鏈接會很快發生,若是客戶端失去鏈接持續了一段時間,進程也許會選擇關閉會話,固然,若是客戶端失去鏈接,關閉會話也不會使ZooKeeper更快地關閉會話,ZooKeeper服務依然會等待會話過時時間過去之後才聲明會話已過時。
注意:很長的延時與過時
爲了使鏈接斷開與重現創建會話之間更加平滑,ZooKeeper客戶端庫會在新的服務器上從新創建全部已經存在的監視點。當客戶端鏈接ZooKeeper的服務器,客戶端會發送監視點列表和最後已知的zxid(最終狀態的時間戳),服務器會接受這些監視點並檢查znode節點的修改時間戳與這些監視點是否對應,若是任何已經監視的znode節點的修改時間戳晚於最後已知的zxid,服務器就會觸發這個監視點。每一個ZooKeeper操做都徹底符合該邏輯,除了exists。exists操做與其餘操做不一樣,由於這個操做能夠在一個不存在的節點上設置監視點,若是仔細看前一段中所說的註冊監視點邏輯,會發現存在一種錯過監視點事件的特殊狀況。
圖5-5說明了這種特殊狀況
致使錯過了一個設置了監視點的znode節點的建立事件,客戶端監視/event節點的建立事件,然而就在/event被另外一個客戶端建立時,設置了監視點的客戶端與ZooKeeper間失去鏈接,在這段時間,其餘客戶端刪除了/event,所以當設置了監視點的客戶端從新與ZooKeeper創建鏈接並註冊監視點,ZooKeeper服務器已經不存在/event節點了,所以,當處理已經註冊的監視點並判斷/event的監視時,發現沒有/event這個節點,因此就只是註冊了這個監視點,最終致使客戶端錯過了/event的建立事件。由於這種特殊狀況,須要
儘可能避免監視一個znode節點的建立事件,若是必定要監視建立事件,應儘可能監視存活期更長的znode節點,不然這種特殊狀況可能會傷害你。
兩種狀況下,ZooKeeper都會丟棄會話的狀態:
【ZooKeeper沒法保護與外部設備的交互操做】
當運行客戶端進程的主機發生過載,就會開始發生交換、系統顛簸或因已經超負荷的主機資源的競爭而致使的進程延遲,這些都會影響與ZooKeeper交互的及時性:
圖5-6經過時間軸展現了這個棘手的問題
在這個例子中,應用程序經過使用ZooKeeper來確保每次只有一個主節點能夠獨佔訪問一個外部資源,這是一個很廣泛的資源中心化管理的方法,用來確保一致性。在時間軸的開始,客戶端c1 爲主節點並獨佔方案外部資源。
事件發生順序以下:
一、在 t1 時刻,由於超載致使與ZooKeeper的通訊中止,c1 沒有響應,c1 已經排隊等候對外部資源的更新,可是還沒收到CPU時鐘週期來發送這些更新。
二、在t2 時刻,ZooKeeper聲明瞭c1 與ZooKeeper的會話已經終止,同時刪除了全部與c1 會話關聯的臨時節點,包括用於成爲主節點而建立的臨時性節點。
三、在t3 時刻,c2 成爲主節點。
四、在t4 時刻,c2 改變了外部資源的狀態。
五、在t5 時刻,c1 的負載降低,併發送已隊列化的更新到外部資源上。
六、在t6 時刻,c1 與ZooKeeper重現創建鏈接,發現其會話已通過期且丟掉了管理權。遺憾的是,破壞已經發生,在t5 時刻,已經在外部資源進行了更新,最後致使系統狀態損壞。解決這個問題有幾個方法:
用一個例子來講明如何經過隔離符號來實現一個簡單的隔離,只有持有最新符號的客戶端,才能夠訪問資源:
在建立表明羣首的節點時,能夠得到Stat結構的信息,其中該結構中的成員之一,czxid,表示建立該節點時的zxid,zxid爲惟一
的單調遞增的序列號,所以可使用czxid做爲一個隔離的符號。當對外部資源進行請求時,或在鏈接外部資源時,還須要提供這個隔離符號,若是外部資源已經接收到更高版本的隔離符號的請求或鏈接時,咱們的請求或鏈接就會被拒絕。
也就是說若是一個主節點鏈接到外部資源開始管理時,若舊的主節點嘗試對外幣資源進行某些處理,其請求將會失敗,這些請求會被隔離開。即便出現系統超載或時鐘偏移,隔離技巧依然能夠可靠地工做。
圖5-7展現瞭如何經過該技巧解決圖5-6的狀況。
一、當c 1 在t 1 時刻成爲羣首,建立/leader節點的zxid爲3(真實環境中,zxid爲一個很大的數字),在鏈接數據庫時使用建立的zxid值做爲隔離符號。 二、以後,c 1 因超載而沒法響應,在t 2 時刻,ZooKeeper聲明c 1 終止,c 2 成爲新的羣首。 三、c 2 使用4所做爲隔離符號,由於其建立/leader節點的建立zxid爲4。 四、在t 3時刻,c 2 開始使用隔離符號對數據庫進行操做請求。 五、在t 4 時刻,c 1 '的請求到達數據庫,請求會因傳入的隔離符號(3)小於已知的隔離符號(4)而被拒絕,所以避免了系統的破壞。 不過,隔離方案須要修改客戶端與資源之間的協議,須要在協議中添加zxid,外部資源也須要持久化保存來跟蹤接收到的最新的zxid。