ZooKeeper八大典型的應用場景

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

1.1 什麼是配置中心,有什麼用

數據發佈/訂閱( Publish/Subscribe)系統,即所謂的配置中心,顧名思義就是發佈者將數據發佈到ZooKeeper的一個或一系列節點上,供訂閱者進行數據訂閱,進而達到動態獲取數據的目的,實現配置信息的集中式管理和數據的動態更新。

1.2 配置中心的設計模式

發佈/訂閱系統一般有兩種設計模式,分別是推(Push) 模式和拉(Pull) 模式。在推模式中,服務端主動將數據更新發送給所有訂閱的客戶端;而拉模式則是由客戶端主動發起請求來獲取最新數據,通常客戶端都採用定時進行輪詢拉取的方式。ZooKeeper採用的是推拉相結合的方式:客戶端向服務端註冊自己需要關注的節點,一旦該節點的數據發生變更,那麼服務端就會向相應的客戶端發送Watcher事件通知,客戶端接收到這個消息通知之後,需要主動到服務端獲取最新的數據。

1.3 配置中心實現原理

如果將配置信息存放到ZooKeeper上進行集中管理,那麼通常情況下,應用在啓動的時候都會主動到ZooKeeper服務端上進行一次配置信息的獲取,同時,在指定節點上註冊一個Watcher監聽,這樣一來,但凡配置信息發生變更,服務端都會實時通知到所有訂閱的客戶端,從而達到實時獲取最新配置信息的目的。

在我們平常的應用系統開發中,經常會碰到這樣的需求:系統中需要使用一些通用的配置信息,例如機器列表信息、運行時的開關配置、數據庫配置信息等。這些全局配置信息通常具備以下3個特性。

1.4 全局配置信息通常具備哪些特性

  • 數據量通常比較小。
  • 數據內容在運行時會發生動態變化。
  • 集羣中各機器共享,配置一致。

對於這類配置信息,一般的做法通常可以選擇將其存儲在本地配置文件或是內存變量中。

1.5 爲什麼要使用配置中心

無論採用哪種方式,其實都可以簡單地實現配置管理。如果採用本地配置文件的方式,那麼通常系統可以在應用啓動的時候讀取到本地磁盤的一個文件來進行初始化,並且在運行過程中定時地進行文件的讀取,以此來檢測文件內容的變更。在系統的實際運行過程中,如果我們需要對這些配置信息進行更新,那麼只要在相應的配置文件中進行修改,等到系統再次讀取這些配置文件的時候,就可以讀取到最新的配置信息,並更新到系統中去,這樣就可以實現系統配置信息的更新。另外一種藉助內存變量來實現配置管理的方式也非常簡單,以Java系統爲例,通常可以採用JMX方式來實現對系統運行時內存變量的更新。從上面的介紹中,我們基本瞭解瞭如何使用本地配置文件和內存變量方式來實現配置管理。通常在集羣機器規模不大、配置變更不是特別頻繁的情況下,無論上面提到的哪種方式,都能夠非常方便地解決配置管理的問題。但是,一旦機器規模變大,且配置信息變更越來越頻繁後,我們發現依靠現有的這兩種方式解決配置管理就變得越來越困難了。我們既希望能夠快速地做到全局配置信息的變更,同時希望變更成本足夠小,因此我們必須尋求–種更爲分佈式化的解決方案。

1.6 配置中心實現的步驟

第一步:配置存儲
在進行配置管理之前,首先我們需要將初始化配置存儲到ZooKeeper.上去。一般情況下,我們可以在ZooKeeper上選取一個數據節點用於配置的存儲,例如/app/database_ config (“配置節點”)

第二步:配置獲取
集羣中每臺機器在啓動初始化階段,首先會從上面提到的ZooKeeper配置節點上讀取數據庫信息,同時,客戶端還需要在該配置節點上註冊一個數據變更的Watcher監聽,一旦發生節點數據變更,所有訂閱的客戶端都能夠獲取到數據變更通知。

第三步:配置變更
在系統運行過程中,可能會出現需要進行數據庫切換的情況,這個時候就需要進行配置變更。藉助ZooKeeper,我們只需要對ZooKeeper上配置節點的內容進行更新,ZooKeeper就能夠幫我們將數據變更的通知發送到各個客戶端,每個客戶端在接收到這個變更通知後,就可以重新進行最新數據的獲取。

二、負載均衡

2.1 什麼是負載均衡

負載均衡(Load Balance)是一種相當常見的計算機網絡技術,用來對多個計算機(計算機集羣)、網絡連接、CPU、磁盤驅動器或其他資源進行分配負載,以達到優化資源使用、最大化吞吐率、最小化響應時間和避免過載的目的。通常負載均衡可以分爲硬件和軟件負載均衡兩類,本節主要探討的是ZooKeeper在“軟”負載均衡中的應用場景。

在分佈式系統中,負載均衡更是一種普遍的技術,基本上每一個分佈式系統都需要使用負載均衡。分佈式系統具有對等
性,爲了保證系統的高可用性,通常採用副本的方式來對數據和服務進行部署。而對於消費者而言,則需要在這些對等的服務提供方中選擇一個來執行相關的業務邏輯,其中比較典型的就是DNS服務。

2.2 ZooKeeper怎麼實現的負載均衡

ZooKeeper採用了一種動態DNS的一種方案實現的負載均衡。一般企業不會使用zk做負載均衡。有nginx和Ribbon不用那不是傻子嗎。

三、命名服務

3.1 什麼是命名服務

命名服務(Name Service)也是分佈式系統中比較常見的類場景,命名服務是分佈式系統最基本的公共服務之一。能夠幫助應用系統通過一個資源引用的方式來實現對資源的定位與使用。
在分佈式系統中,被命名的實體通常可以是集羣中的機器、提供的服務地址或遠程對象等這些我們都可以統稱它們爲名字(Name),其中較爲常見的就是一些分佈式服務框架(如RPC、RMI)中的服務地址列表。

3.2 命名服務有什麼用

通過使用命名服務,客戶端應用能夠根據指定名字來獲取資源的實體、服務地址和提供者的信息等。Java語言中的JNDI便是一種典型的命名服務。JNDI是Java 命名與目錄接口(JavaNaming and Directory Interface)的縮寫,是J2EE體系中重要的規範之一,標準的J2EE容器都提供了對JNDI規範的實現。因此,在實際開發中,開發人員常常使用應用服務器自帶的JNDI實現來完成數據源的配置與管理一使用JNDI方式後,開發人員可以完全不需要關心與數據庫相關的任何信息,包括數據庫類型、JDBC驅動類型以及數據庫賬戶等。

ZooKeeper提供的命名服務功能與JNDI技術有相似的地方,都能夠幫助應用系統通過一個資源引用的方式來實現對資源的定位與使用。另外,廣義上命名服務的資源定位都不是真正意義的實體資源——在分佈式環境中,上層應用僅僅需要一個全局唯一的名字,類似於數據庫中的唯一主鍵。

3.3 ZooKeeper實現分佈式唯一ID

所謂ID,就是一個能夠唯一標識某個對象的標識符。在我們熟悉的關係型數據庫中,各個表都需要一個主鍵來唯–標識每條數據庫記錄,這個主鍵就是這樣的唯一ID。在過去的單庫單表型系統中,通常可以使用數據庫字段自帶的auto_increment屬性來自動爲每條數據庫記錄生成一個唯一的ID,數據庫會保證生成的這個ID在全局唯一。
但是隨着數據庫數據規模的不斷增大,分庫分表隨之出現,而auto_ increment 屬性僅能針對單一表中的記錄自動生成ID,因此在這種情況下,就無法再依靠數據庫的auto_ increment 屬性來唯一標識一條記錄了。於是,我們必須尋求一種能夠在分佈式環境下生成全局唯一ID的方法。
說起全局唯一ID,肯定少不了UUID。沒錯,UUID是通用唯一識別碼(Universally Unique ldentifier) 的簡稱,是一種在分佈式系統中廣泛使用的用於唯-標識元素的標準,最典型的實現是GUID (Globally Unique ldentifier, 全局唯-標識符),主流ORM框架Hibernate有對UUID的直接支持。確實,UUID是一個非常不錯的全局唯一ID生成方式,能夠非常簡便地保證分佈式環境中的唯一性。一個標準的UUID是一個包含32位字符和4個短線的字符串,例如“e70f1357-f260-46ff- a32d- 53a086c57ade"。但是UUID也具有一定的缺陷,比如長度過長,含義不明等。

而ZooKeeper實現分佈式唯一ID只要創建順序節點就可以了。
在ZooKeeper中,每一個數據節點都能夠維護一份子節點的順序順列,當客戶端對其創建一個順序子節點的時候ZooKeeper 會自動以後綴的形式在其子節點上添加一個序號,在這個場景中就是利用了ZooKeeper的這個特性。如圖

四、分佈式協調/通知

4.1 什麼是分佈式協調/通知

分佈式協調/通知服務是分佈式系統中不可缺少的一個環節,是將不同的分佈式組件有機結合起來的關鍵所在。

4.2 分佈式協調/通知的作用

對於一個在多臺機器上部署運行的應用而言,通常需要一個 協調者(Coordinator)來控制整個系統的運行流程,例如分佈式事務的處理、機器間的互相.協調等。同時,引入這樣一一個協調者,便於將分佈式協調的職責從應用中分離出來,從而可以大大減少系統之間的耦合性,而且能夠顯著提高系統的可擴展性。

ZooKeeper中特有的Watcher註冊與異步通知機制,能夠很好地實現分佈式環境下不同機器,甚至是不同系統之間的協調與通知,從而實現對數據變更的實時處理。基於ZooKeeper實現分佈式協調與通知功能,通常的做法是不同的客戶端都對ZooKeeper上同一個數據節點進行Watcher註冊,監聽數據節點的變化(包括數據節點本身及其子節點),如果數據節點發生變化,那麼所有訂閱的客戶端都能夠接收到相應的Watcher 通知,並做出相應的處理。

4.3 分佈式系統機器間通信方式

在絕大部分的分佈式系統中,系統機器間的通信無外乎心跳檢測、工作進度彙報和系統調度這三種類型。接下來,我們將圍繞這三種類型的機器通信來講解如何基於ZooKeeper去實現一種分佈式系統間的通信方式。

4.3.1 心跳檢測

機器間的心跳檢測機制是指在分佈式環境中,不同機器之間需要檢測到彼此是否在正常運行,例如A機器需要知道B機器是否正常運行。在傳統的開發中,我們通常是通過主機之間是否可以相互PING通來判斷,更復雜一點的話,則會通過在機器之間建立長連接,通過TCP連接固有的心跳檢測機制來實現上層機器的心跳檢測,這些確實都是一些非常常見的心跳檢測方法。

ZooKeeper怎麼實現分佈式機器間的心跳檢測?

基於ZooKeeper的臨時節點特性,可以讓不同的機器都在ZooKeeper的一個指定節點下創建臨時子節點,不同的機器之間可以根據這個臨時節點來判斷對應的客戶端機器是否存活。通過這種方式,檢測系統和被檢測系統之間並不需要直接相關聯,而是通過ZooKeeper上的某個節點進行關聯,大大減少了系統耦合。

4.3.2 工作進度彙報

在一個常見的任務分發系統中,通常任務被分發到不同的機器上執行後,需要實時地將自己的任務執行進度彙報給分發系統。這個時候就可以通過ZooKeeper 來實現。在ZooKeeper.上選擇一個節點,每個任務客戶端都在這個節點下面創建臨時子節點,這樣便可以實現兩個功能:

  • 通過判斷臨時節點是否存在來確定任務機器是否存活;
  • 各個任務機器會實時地將自己的任務執行進度寫到這個臨時節點上去,以便中心繫統能夠實時地獲取到任務的執行進度。

4.3.3 系統調度

使用ZooKeeper,能夠實現另一種系統調度模式:一個分佈式系統由控制檯和一些客戶端系統兩部分組成,控制檯的職責就是需要將一些指令信息發送給所有的客戶端,以控制它們進行相應的業務邏輯。後臺管理人員在控制檯上做的一些操作,實際上就是修改了ZooKeeper上某些節點的數據,而ZooKeeper進一步把這些數據變更以事件通知的形式發送給了對應的訂閱客戶端。

使用ZooKeeper來實現分佈式系統機器間的通信的好處?

不僅能省去大量底層網絡通信和協議設計.上重複的工作,更爲重要的一點是大大降低了系統之間的耦合,能夠非常方便地實現異構系統之間的靈活通信。

五、集羣管理

5.1 什麼是集羣管理

所謂集羣管理,包括集羣監控與集羣控制兩大塊,前者側重對集羣運行時狀態的收集,後者則是對集羣進行操作與控制。在日常開發和運維過程中,我們經常會有類似於如下的需求。

  • 希望知道當前集羣中究竟有多少機器在工作。
  • 對集羣中每臺機器的運行時狀態進行數據收集。
  • 對集羣中機器進行上下線操作。
    在傳統的基於Agent的分佈式集羣管理體系中,都是通過在集羣中的每臺機器上部署一個Agent, 由這個Agent 負責主動向指定的一個監控中心繫統(監控中心繫統負責將所有數據進行集中處理,形成一系列報表,並負責實時報警,以下簡稱“監控中心”) 彙報自己所在機器的狀態。在集羣規模適中的場景下,這確實是一 種在生產實踐中廣泛使用的解決方案,能夠快速有效地實現分佈式環境集羣監控,但是一旦系統的業務場景增多,集羣規模變大之後,該解決方案的弊端也就顯現出來了。

5.2 基於Agent的分佈式集羣管理體系的弊端

5.2.1 大規模升級困難

以客戶端形式存在的Agent, 在大規模使用後,一旦遇上需要大規模升級的情況,就非常麻煩,在升級成本和升級進度的控制上面臨巨大的挑戰。

5.2.2 統一的Agen無法滿足多樣的需求

對於機器的CPU使用率、負載(Load)內存使用率、網絡吞吐以及磁盤容量等機器基本的物理狀態,使用統一的 Agent來進行監控或許都可以滿足。但是,如果需要深入應用內部,對一些業務狀態進行監控,例如,在一個分佈式消息中間件中,希望監控到每個消費者對消息的消費狀態;或者在一個分佈式任務調度系統中,需要對每個機器上任務的執行情況進行監控。很顯然,對於這些業務耦合緊密的監控需求,不適合由一個統一的Agent來提供。

5.2.3 編程語言多樣性

隨着越來越多編程語言的出現,各種異構系統層出不窮。如果使用傳統的Agent方式,那麼需要提供各種語言的Agent客戶端。另- -方面, “監控中心”在對異構系統的數據進行整合上面臨巨大挑戰。

5.3 ZooKeeper具有的兩大特性

  • 客戶端如果對ZooKeeper的一個數據節點註冊Watcher監聽,那麼當該數據節點的內容或是其子節點列表發生變更時,ZooKeeper服務器就會向訂閱的客戶端發送變更通知。
  • 對在ZooKeeper上創建的臨時節點,一旦客戶端與服務器之間的會話失效,那麼該臨時節點也就被自動清除。

利用ZooKeeper 的這兩大特性,就可以實現另一種集羣機器存活性監控的系統。例如, 監控系統在/clusterServers節點上註冊一個 Watcher監聽,那麼但凡進行動態添加機器的操作,就會在/clusterServers節點下創建一個臨時節點: /clusterServers/[Hostname] 這樣一來,監控系統就能夠實時檢測到機器的變動情況,至於後續處理就是監控系統的業務了。

六、Master 選舉

6.1 Master選舉的意義

Master選舉是一個在分佈式系統中非常常見的應用場景。分佈式最核心的特性就是能夠將具有獨立計算能力的系統單元部署在不同的機器上,構成–個完整的分佈式系統。而與此同時,實際場景中往往也需要在這些分佈在不同機器上的獨立系統單元中選出一個所謂的“老大”, 在計算機科學中,我們稱之爲Master.在分佈式系統中,Master往往用來協調集羣中其他系統單元,具有對分佈式系統狀態變更的決定權。例如,在一些讀寫分離的應用場景中,客戶端的寫請求往往是由Master來處理的;而在另一些場景中,Master則常常負責處理一些複雜的邏輯,並將處理結果同步給集羣中其他系統單元。Master選舉可以說是ZooKeeper最典型的應用場景了。

在分佈式環境中,經常會碰到這樣的應用場景:集羣中的所有系統單元需要對前端業務提供數據,比如一個商品ID,或者是一個網站輪播廣告的廣告ID (通常出現在一些廣告投放系統中)等,而這些商品ID或是廣告ID往往需要從一系列的海量數據處理中計算得到一這通常是 一個非常耗費I/O和CPU資源的過程。鑑於該計算過程的複雜性,如果讓集羣中的所有機器都執行這個計算邏輯的話,那麼將耗費非常多的資源。一種比較好的方法就是隻讓集羣中的部分,甚至只讓其中的一臺機器去處理數據計算,一旦計算出數據結果,就可以共享給整個集羣中的其他所有客戶端機器,這樣可以大大減少重複勞動,提升性能。

6.2 普通實現Master選舉的過程

首先來明確下Master選舉的需求:在集羣的所有機器中選舉出一臺機器作爲Master。
針對這個需求,通常情況下,我們可以選擇常見的關係型數據庫中的主鍵特性來實現:集羣中的所有機器都向數據庫中插入一條相同主鍵ID的記錄,數據庫會幫助我們自動進行主鍵衝突檢查,也就是說,所有進行插入操作的客戶端機器中,只有一臺機器能夠成功一那麼, 我們就認爲向數據庫中成功插人數據的客戶端機器成爲Master。

6.2.1 普通實現Master選舉的弊端

乍一看,這個方案確實可行,依靠關係型數據庫的主鍵特性能夠很好地保證在集羣中選舉出唯一的一個Master。但是我們需要考慮的另一個問題是,如果當前選舉出的Master掛了,那麼該如何處理?誰來告訴我Master掛了呢?顯然,關係型數據庫沒法通知我們這個事件。

6.3 Zookeeper實現Master選舉的過程

利用ZooKeeper的強一致性,能夠很好地保證在分佈式高併發情況下節點的創建一定能夠保證全局唯一性,即ZooKeeper將會保證客戶端無法重複創建一個已經存在的數據節點。也就是說,如果同時有多個客戶端請求創建同一個節點,那麼最終一定只有一個客戶端請求能夠創建成功。利用這個特性,就能很容易地在分佈式環境中進行Master選舉了。

例如:
客戶端集羣每天都會定時往ZooKeeper上創建一個臨時節點,例如/master_ election/2020-1-26/binding,在這個過程中,只有一個客戶端能夠成功創建這個節點,那麼這個客戶端所在的機器就成爲了Master。同時,其他沒有在ZooKeeper上成功創建節點的客戶端,都會在節點/master_ election/2020-1-26上註冊一個子節點變更的Watcher, 用於監控當前的Master機器是否存活,一旦發現當前的Master掛了,那麼其餘的客戶端將會重新進行Master選舉。

七、分佈式鎖

7.1 什麼是分佈式鎖

分佈式鎖是控制分佈式系統之間同步訪問共享資源的一種方式。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那麼訪問這些資源的時候,往往需要通過一些互斥手段來防止彼此之間的干擾,以保證一致性,在這種情況下,就需要使用分佈式鎖了。

在平時的實際項目開發中,我們往往很少會去在意分佈式鎖,而是依賴於關係型數據庫固有的排他性來實現不同進程之間的互斥。這確實是一種非常簡便且被廣泛使用的分佈式鎖實現方式。然而有一個不爭的事實是,目前絕大多數大型分佈式系統的性能瓶頸都集中在數據庫操作上。因此,如果上層業務再給數據庫添加一些額外的鎖,例如行鎖、表鎖甚至是繁重的事務處理,就會讓數據庫更加不堪重負。所以一般不使用數據庫來實現分佈式鎖。

7.2 Zookeeper實現分佈式鎖

7.2.1 排他鎖

7.2.1.1 什麼是排他鎖

排他鎖(Exclusive Locks, 簡稱X鎖),又稱爲寫鎖或獨佔鎖,是一種基本的鎖類型。
如果事務T對數據對象O 加上了排他鎖,那麼在整個加鎖期間,只允許事務T對O進行讀取和更新操作,其他任何事務都不能再對這個數據對象進行任何類型的操作一直到T釋放了排他鎖。排他鎖的核心是如何保證當前有且僅有一個事務獲得鎖,並且鎖被釋放後,所有正在等待獲取鎖的事務都能夠被通知到。

7.2.1.2 ZooKeeper實現排他鎖

定義鎖
在通常的Java開發編程中,有兩種常見的方式可以用來定義鎖,分別是synchronized機制和JDK5提供的ReentrantLock然而,在ZooKeeper中,沒有類似於這樣的API可以直接使用,而是通過ZooKeeper上的數據節點來表示一個鎖,例如/exclusive_ lock/lock節點就可以被定義爲一個鎖。

獲取鎖
在需要獲取排他鎖時,所有的客戶端都會試圖通過調用create()接口, 在/exclusive_ lock 節點下創建臨時子節點/exclusive_ lock/lock。 在前面幾節中我們也介紹了,ZooKeeper會保證在所有的客戶端中,最終只有一個客戶端能夠創建成功,那麼就可以認爲該客戶端獲取了鎖。同時,所有沒有獲取到鎖的客戶端就需要到/exclusive_lock節點上註冊一個子節點變更的Watcher監聽,以便實時監聽到lock節點的變更情況。

釋放鎖
在“定義鎖”部分,我們已經提到,/exclusive_ lock/lock 是一個臨時節點,因此在以下兩種情況下,都有可能釋放鎖。

  • 當前獲取鎖的客戶端機器發生宕機,那麼ZooKeeper上的這個臨時節點就會被移除。
  • 正常執行完業務邏輯後,客戶端就會主動將自己創建的臨時節點刪除。
    無論在什麼情況下移除了lock 節點,ZooKeeper 都會通知所有在/exclusive_ lock 節點上註冊了子節點變更Watcher監聽的客戶端。這些客戶端在接收到通知後,再次重新發起分佈式鎖獲取,即重複“獲取鎖”過程。整個排他鎖的獲取和釋放流程,見下圖。

7.2.2 共享鎖

7.2.2.1 什麼是共享鎖

共享鎖(Shared Locks,簡稱S鎖),又稱爲讀鎖,同樣是一種基本的鎖類型。如果事務T對數據對象O1加上了共享鎖,那麼當前事務只能對O1進行讀取操作,其他事務也只能對這個數據對象加共享鎖一直到該數據對象上的所有共享鎖都被釋放。共享鎖和排他鎖最根本的區別在於,加上排他鎖後,數據對象只對一個事務可見,而加上共享鎖後,數據對所有事務都可見。

7.2.2.2 ZooKeeper來實現共享鎖

定義鎖

和排他鎖一樣,同樣是通過ZooKeeper.上的數據節點來表示一一個鎖, 是一個類似於“/shared_lock/[Hostname]-請求類型-序號”的臨時順序節點,例如/shared_lock/192.168.0.1-R-0000000001,那麼,這個節點就代表了一個共享鎖。

獲取鎖
在需要獲取共享鎖時,所有客戶端都會到/shared_lock這個節點下面創建一個臨時順序節點,如果當前是讀請求,那麼就創建例如/shared_ lock/192.168.0.1-R-0000000001的節點;如果是寫請求,那麼就創建例如/shared_ lock/192.168.0.1- W000000001的節點。

判斷讀寫順序
根據共享鎖的定義,不同的事務都可以同時對同一個數據對象進行讀取操作,而更新操作必須在當前沒有任何事務進行讀寫操作的情況下進行。基於這個原則,我們來看看如何通過ZooKeeper的節點來確定分佈式讀寫順序,大致可以分爲如下4個步驟。

  1. 創建完節點後,獲取/shared_ lock節點下的所有子節點,並對該節點註冊子節點變更的Watcher監聽。
  2. 確定自己的節點序號在所有子節點中的順序。
  3. 對於讀請求:
    如果沒有比自己序號小的子節點,或是所有比自己序號小的子節點都是讀請求,那麼表明自己已經成功獲取到了共享鎖,同時開始執行讀取邏輯。如果比自己序號小的子節點中有寫請求,那麼就需要進入等待。
    對於寫請求:
    如果自己不是序號最小的子節點,那麼就需要進入等待。
  4. 接收到Watcher通知後,重複步驟1。

釋放鎖

和排他鎖一樣。

完整的共享鎖流程。如圖。

7.2.2.2 共享鎖帶來的問題(羊羣效應)

上面的這個共享鎖實現,大體上能夠滿足一般的分佈式集羣競爭鎖的需求,開且性能都還可以,這裏說的一般場景是指集羣規模不是特別大,一般是在10臺機器以內。

7.2.2.2.1共享鎖在實際運行中最主要的步驟
  1. 192.168.0.1 這臺機器首先進行讀操作,完成讀操作後將節點/192.168.0.1-R-000000001刪除。
  2. 餘下的4臺機器均收到了這個節點被移除的通知,然後重新從/shared_lock節點上獲取一份新的子節點列表。
  3. 每個機器判斷自己的讀寫順序。其中192.168.0.2 這臺機器檢測到自己已經是序號最小的機器”了,於是開始進行寫操作,而餘下的其他機器發現沒有輪到自己進行讀取或更新操作,於是繼續等待。
  4. 繼續…
    上面這個過程就是共享鎖在實際運行中最主要的步驟了,我們着重看下上面步驟3中提到的:“而餘下的其他機器發現沒有輪到自己進行讀取或更新操作,於是繼續等待。”很明顯,我們看到,192.168.0.1 這個客戶端在移除自己的共享鎖後,ZooKeeper發送了子節點變更Watcher通知給所有機器,然而這個通知除了給192.168.0.2這臺機器產生實際影響外,對於餘下的其他所有機器都沒有任何作用。
7.2.2.2.2 羊羣效應

在這整個分佈式鎖的競爭過程中,大量的“ Watcher通知”和“子節點列表獲取”兩個操作重複運行,並且絕大多數的運行結果都是判斷出自己並非是序號最小的節點,從而繼續等待下一次通知,這個看起來顯然不怎麼科學。客戶端無端地接收到過多和自己並不相關的事件通知,如果在集羣規模比較大的情況下,不僅會對ZooKeeper服務器造成巨大的性能影響和網絡衝擊,更爲嚴重的是,如果同一時間有多個節點對應的客戶端完成事務或是事務中斷引起節點消失,ZooKeeper服務器就會在短時間內向其餘客戶端發送大量的事件通知——這就是所謂的羊羣效應
上面這個ZooKeeper分佈式共享鎖實現中出現羊羣效應的根源在於,沒有找準客戶端真正的關注點。我們再來回顧一下上面的分佈式鎖競爭過程,它的核心邏輯在於:判斷自己是否是所有子節點中序號最小的。於是,很容易可以聯想到,每個節點對應的客戶端只需要關注比自己序號小的那個相關節點的變更情況就可以了一而不需要關注全局的子列表變更情況。

7.2.3 改進後的分佈式鎖實現

現在我們來看看如何改進上面的分佈式鎖實現。首先,我們需要肯定的一點是,上面提.到的共享鎖實現,從整體思路上來說完全正確。這裏主要的改動在於:**每個鎖競爭者,只需要關注/shared_lock節點下序號比自己小的那個節點是否存在即可,**具體實現如下。

  1. 客戶端調用create()方法創建-一個類似於“/shared_ lock/[Hostname]-請求類型序號”的臨時順序節點。
  2. 客戶端調用getChildren() 接口來獲取所有已經創建的子節點列表,注意,這裏不註冊任何Watcher。
  3. 如果無法獲取共享鎖,那麼就調用exist()來對比自己小的那個節點註冊Watcher。
    注意,這裏“比自己小的節點”只是一個籠統的說法,具體對於讀請求和寫請求不一樣。
    讀請求:向比自己序號小的最後一個寫請求節點註冊Watcher監聽。
    寫請求:向比自己序號小的最後一個節點註冊Watcher監聽。
  4. 等待Watcher通知,繼續進入步驟2。
    改進後的分佈式鎖流程如下圖所示。
    在這裏插入圖片描述

7.2.4 建議

在多線程併發編程實踐中,我們會去儘量縮小鎖的範圍一對於分佈式鎖實現的改進其實也是同樣的思路。那麼對於開發人員來說,是否必須按照改進後的思路來設計實現自己的分佈式鎖呢?答案是否定的。在具體的實際開發過程中,建議根據具體的業務場景和集羣規模來選擇適合自己的分佈式鎖實現。
在集羣規模不大、網絡資源豐富的情況下,第一種分佈式鎖實現方式是簡單實用的選擇
而如果集羣規模達到一定程度,並且希望能夠精細化地控制分佈式鎖機制,那麼不妨試試改進版的分佈式鎖實現

八、分佈式隊列

分佈式隊列,簡單地講分爲兩大類,一種是常規的先入先出隊列,另一種則是要等到隊列元素集聚之後才統一安排執行的Barrier模型。

8.1 FIFO:先入先出隊列

FIFO (First Input First Output,先入先出)的算法思想,以其簡單明瞭的特點,廣泛應用於計算機科學的各個方面。而FIFO隊列也是一種非常典型且應用廣泛的按序執行的。

隊列模型:先進入隊列的請求操作先完成後,纔會開始處理後面的請求。
使用ZooKeeper實現FIFO隊列,和共享鎖的實現非常類似。FIFO 隊列就類似於一個全寫的共享鎖模型,大體的設計思路其實非常簡單:所有客戶端都會到/queue_ fifo這個節點下面創建一個臨時順序節點,例如/queue_ fifo/192.168.0.1-0000000001

創建完節點之後,根據如下4個步驟來確定執行順序。

  1. 通過調用getChildren()接口來獲取/queue_ fifo 節點下的所有子節點,即獲取隊列中所有的元素。
  2. 確定自己的節點序號在所有子節點中的順序。
  3. 如果自己不是序號最小的子節點,那麼就需要進入等待,同時向比自己序號小的最後一個節點註冊Watcher監聽。
  4. 接收到Watcher通知後,重複步驟1。

8.2 Barrier:分佈式屏障

Barrier原意是指障礙物、屏障,而在分佈式系統中,特指系統之間的一個協調條件,規定了一個隊列的元素必須都集聚後才能統一進行安排, 否則一直等待。這往往出現在那些大規模分佈式並行計算的應用場景上:最終的合併計算需要基於很多並行計算的子結果來進行。

這些隊列其實是在FIFO隊列的基礎上進行了增強,大致的設計思想如下:
開始時,/queue_ barrier 節點是一個已經存在的默認節點,並且將其節點的數據內容賦值爲一個數字n來代表Barrier 值,例如n=10表示只有當/queue_ barrier 節點下的子節點個數達到10後,纔會打開Barrier。 之後,所有的客戶端都會到/queue_ barrier 節點下創建一個臨時節點,例如/queue_ barrier/192.168.0.1

創建完節點之後,根據如下5個步驟來確定執行順序。

  1. 通過調用getData()接口獲取/queue_ barrier 節點的數據內容: 10。
  2. 通過調用getChildren( )接口獲取/queue_ barrier 節點下的所有子節點,即獲取隊列中的所有元素,同時註冊對子節點列表變更的Watcher監聽。
  3. 統計子節點的個數。
  4. 如果子節點個數還不足10個,那麼就需要進入等待。
  5. 接收到Watcher通知後,重複步驟2。
發佈了68 篇原創文章 · 獲贊 12 · 訪問量 40萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章