Zookeeper實踐篇-Zookeeper經典場景實踐

前言

上篇我們學習了zk的api基礎實踐,並且瞭解了zk原生api與一些開源實現的zk客戶端框架的選擇使用,本篇我們開始學習zk作爲一個分佈式協調工具在分佈式場景中一些實現。

Zookeeper是一個高可用的分佈式數據管理與分佈式協調服務框架,並且因爲自身的Zab協議實現,使得zk可以做到保證分佈式場景中的一致性。因此伴隨着分佈式的發展,zk用來解決分佈式的場景越來越多,例如配置中心、服務治理、分佈式鎖、分佈式隊列等,接下來我們來看看這些常見的分佈式場景下zk的處理和實現原理。

數據發佈/訂閱(配置中心)

在分佈式開發中,我們往往需要實現一個統一的分佈式的配置中心,在我們系統較少的場景下,往往是使用的本地配置文件的方式來處理,但是當分佈式環境中,每次進行配置文件的變更,都需要將所有的機器的配置修改,甚至需要重新啓動服務,這點對於開發和使用來說非常不便,後來也出現了很多配置中心的方案,例如etcd,或者apollo等,而zk作爲通用的分佈式協調系統,自然也可以實現理想的配置中心,即使用zk的數據發佈/訂閱

發佈/訂閱一般來說在很多系統中都有使用,大體的實現方式主要分爲兩種:推--拉,而所謂的推模式,則是服務端將配置的數據變更信息主動發送給所有的訂閱的客戶端,而拉模式則是反過來,是由所有的訂閱的客戶端,主動發起請求來獲取服務端數據,一般都是客戶端採用定時輪詢的機制來實現。但是我們可以思考這兩種模式自身的優缺點,例如推模式,就存在很大可能,在服務端網絡不佳的場景下,推模式可能會導致大量的延遲甚至推送失敗,導致客戶端大量接受不到新的數據變更等問題,而拉模式也可能存在每個客戶端主動拉取數據的延遲性,例如30s輪詢一次服務端,但是在這個時間間隔內可能會導致大量的請求不可達或者服務不可用,也會因爲每個客戶端的網絡和啓動時間等原因導致,部分客戶端已經輪詢到了最新的數據,而部分客戶端依然是上一次的舊數據的情況。可見,無論是哪種模式,都存在一定的弊端,也有一定的優勢,而zk的發佈/訂閱則是基於推拉結合的模式,具體實現原理如下:

客戶端向服務端註冊需要訂閱的節點,而訂閱則會使得客戶端本地存在一個回調通知機制(watch),一旦服務端發現這個節點有數據變更,就去主動的將變更的信息推送給每一個客戶端,觸發客戶端的watch機制。

那麼,我們如果通過zk實現配置中心,該如何操作呢?大致可以分爲以下三步:

1、配置的存儲

在進行配置存儲之前,我們需要在zk上創建一個節點,用來初始化階段將數據存儲進去,例如/app1/database_config節點:

然後將需要管理的配置信息寫入

2、配置獲取

分佈式集羣環境中的每臺機器在工程初始化的時候,都會去zk上初始化一個配置信息,並且向該節點註冊一個watch,一旦該節點的數據發生了變更,所有的客戶端都會獲取到數據變更的通知

3、配置變更

在分佈式系統運行的過程中,可能會出現配置修改的情況,這個時候就需要將zk上該節點的配置進行更新,當我們觸發完修改操作後,zk的服務端就會將此變化發送給所有的客戶端,當客戶端收到通知以後,則可以重新進行最新數據的獲取

Master選舉

Master選舉是分佈式系統中最常見的應用場景之一,我們常用的組件,例如kafka等就是藉助zk實現的Master選舉,除此之外,大數據中的組件也有很多利用zk實現選舉等功能的。而Master往往是一個分佈式系統中具有協調其他系統單元,具有系統中狀態變更的決定權。

在前面的文章中,我們學習過zk創建節點的Api特性,其中很重要的一點就是,zk自身具有強一致性,可以保證在分佈式環境下高併發創建節點一定可以保證全局唯一。即無法重複的創建一個已經存在的數據節點。如果有多個客戶端同時請求創建該節點,那麼最終只有一個客戶端能創建成功,利用這個特性,就可以簡單的實現Master選舉等功能。

例如,我們在系統中創建一個日期節點,例如‘2013-09-20’,如圖:

而客戶端每天定時往zk的對應日期節點中創建一個臨時節點,例如圖中的/master_election/2013-09-20/binding節點,只有一個客戶端可以創建這個節點,而其他客戶端都是創建失敗,此時創建成功的客戶端自然而然可以視爲master,具有一定的管理權。而創建失敗的客戶端則可以選擇對該節點註冊一個watch,用來監控master節點,一旦發現master掛了,所有的客戶端收到通知後,再次去創建binding節點,重複上述操作即可保證master節點的選舉操作了。

分佈式鎖

分佈式鎖是控制分佈式系統之間同步訪問資源的一種方式,如果在分佈式系統下,需要共享訪問一個或者一組資源,那麼訪問這些資源的時候,往往需要一些互斥的手段來保證一致性,這種場景下就需要使用分佈式鎖了。而zk可以實現的分佈式鎖一般分爲共享鎖排他鎖兩種。

排他鎖

排他鎖又稱寫鎖或者獨佔鎖,是一種基本的鎖類型。如果對客戶端對數據對象O1添加了排他鎖,那麼在整個持有鎖的過程中,只允許當前客戶端對O1進行讀取和更新操作。在zk中,我們可以通過節點來標識一個鎖,例如/exclusive_lock/lock節點被定義爲一個鎖,如圖所示:

在獲取排他鎖的時候,所有的客戶端都會試圖通過create()方法去在/exclusive_lock下面創建臨時節點/lock,而zk的創建方法最終能保證只有一個客戶端創建成功,此客戶端則認爲成功獲取到了鎖,其他客戶端創建失敗以後,則可以選擇註冊一個watch監聽。我們創建的鎖節點屬於臨時節點,因此在以下兩種情況下,都有可能被釋放:

1.由於臨時節點的特性,不會進行持久化保存,而是和當前連接的session關聯,因此當服務器出現宕機的情況下會被釋放

2.在創建臨時節點成功後,客戶端執行需要處理的業務操作完畢後,主動調用臨時節點的刪除操作

因此無論什麼情況下移除了/lock節點,zk都會將事件通知給所有的watch監聽的客戶端。而這個時候所有的客戶端則可以在收到通知後,再次發起創建節點的請求操作,重複上面的操作。整個排他鎖的流程如圖所示:

共享鎖

我們在日常開發過程中,還會使用到一個共享鎖。所謂共享鎖,又稱之爲讀鎖,即如果當前資源O1被掛載了共享鎖,那麼其他的客戶端只能對當前的資源O1進行讀取的操作,而不是進行更新操作,但是當前資源可以同時掛載多個共享鎖,如果想要更新當前資源,則需要掛載的共享鎖全部釋放。與排他鎖不同的是,排他鎖僅僅對一個客戶端可見,而共享鎖則是可以對多個客戶端同時可見。

我們同樣可以使用zk的臨時節點來實現共享鎖操作,我們指定一個節點/sharedlock爲共享鎖的節點,而每個客戶端則是在當前節點下創建臨時順序節點,例如/shared****lock/192.168.1.1-R-000000001,節點名稱規則爲/shared_lock/ip-讀寫類型-臨時節點序號,如圖:

當所有的客戶端都創建完屬於自己類型的共享鎖後,我們則需要進行讀寫規則判斷,確定當前各個客戶端對資源的操作屬於讀還是寫操作,大概如下幾個步驟:

1.每個客戶端都在當前節點下創建對應規則類型的臨時順序節點,並且對該節點註冊一個子節點變更的watch監聽

2.當創建節點成功以後,每個客戶端需要判斷創建的節點在所有的子節點中的順序

3.並且判斷在當前節點的類型的操作

  • 如果是讀請求的節點,在此之前的所有的子節點類型都是讀類型的,那麼當前客戶端也可以執行讀操作;如果之前的節點有寫請求類型,那麼在寫請求之後的所有客戶端都需要進行等待寫操作完成。

  • 如果當前的節點是寫請求操作,那麼除非當前節點在第一個,否則進入等待

4.當接收到watch通知操作後,每個客戶端都會重複創建watch監聽操作

整個共享鎖的邏輯基於排他鎖的思想,但是邏輯較複雜,整體流程如圖所示:

羊羣效應

上面我們創建的兩種分佈式鎖,一般在規模不大的情況下,性能還算不錯,但是我們不禁考慮一個問題,每次有子節點變化的時候,都會將所有的watch進行通知,但是每次也只有一個客戶端在收到通知後擁有鎖的操作權,其他的客戶端其實是沒有作用的,依然是重新註冊watch監聽,等待下一次通知。如果在頻繁的事件監聽觸發情況下,我們不免發現有着大量的重複通知,有多少個客戶端監聽,每次觸發就會通知到多少客戶端,這種重複通知操作稱之爲羊羣效應(驚羣效應)

那麼我們在使用過程中能不能優化分佈式鎖,避免觸發羊羣效應呢?

其實是可以的,我們不妨思考觸發羊羣效應的原因,即每個客戶端註冊了watch監聽都是對父節點的子節點變化的監聽,但是我們知道每次觸發監聽,其實就是排在第一位的臨時節點被該客戶端釋放,那麼我們可以在每次註冊watch監聽的時候,找到當前客戶端創建的臨時節點的前一個節點,進行監聽,這樣,無論前面的鎖如何變化,每個客戶端只關心上一個客戶端的節點是否被釋放,一旦釋放,則會觸發通知給當前客戶端,避免了所有客戶端都收到通知的情況

分佈式隊列

提到隊列,我們最先想到的會是FIFO的隊列,即先進先出的順序隊列,在分佈式環境下想要實現FIFO的隊列很簡單,我們可以利用類似共享鎖的的實現。例如,所有的客戶端都會在/queuefifo這個節點下創建一個臨時順序節點,如/queue****fifo/192.168.1.1-0000000001/queue_fifo/192.168.1.1-0000000002等,如圖所示:

創建完節點後,我們將根據以下4個步驟來確定執行的順序:

1.通過調用getChildren()接口來獲取/queue_fifo節點下的所有子節點,即獲取了當前隊列中的所有元素

2.確定自己的節點序號在所有子節點中的順序

3.如果自己不是序號最小的子節點,即不是最前面的子節點,那麼則需要進入繼續等待,同時這裏我們需要向比當前序號小的最後一個節點註冊watch監聽

4.接受到watch監聽後,重新重複步驟1即可

整個的過程大概如圖所示:

而在分佈式開發環境中,除了FIFO隊列以外,我們往往還存在一個分佈式隊列,即等待一個隊列的元素都聚集以後才進行下一步統一的安排,否則一直進行等待操作。而這種操作往往存在於分佈式環境的並行計算或者並行操作中,而該隊列則是基於FIFO隊列的一個增強實現,我們可以通過註冊一個已經存在的節點,如/queuebarrier,我們將需要等待的數量n作爲值寫入當前節點中,例如n=10,則代表當前節點下存在10個子節點的時候纔會開始下一步操作,而這個過程中,每個客戶端都會在該節點下創建一個臨時節點,如/queue****barrier/192.168.1.1,如圖所示:

創建完節點以後,我們可以通過以下五個步驟來確定執行順序:

1.每個客戶端通過調用getData()來獲取/queue_barrier節點中規定的子節點數量

2.通過調用getChildren()接口獲取/queuebarrier節點下的所有子節點,然後對/queuebarrier節點添加一個子節點列表變化的watch

3.每次觸發watch的時候計算子節點數量

4.如果當前數量不足規定的數量,則繼續添加監聽,進入等待

5.如果數量剛好滿足,則可以執行下一步的業務邏輯操作

整個分佈式等待隊列的操作過程,如圖所示:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章