Zookeeper原理及使用

ZooKeeper是Hadoop Ecosystem中非常重要的組件,它的主要功能是爲分佈式系統提供一致性協調(Coordination)服務,與之對應的Google的類似服務叫Chubby。今天這篇文章分爲三個部分來介紹ZooKeeper,第一部分介紹ZooKeeper的基本原理,第二部分介紹ZooKeeper提供的Client API的使用,第三部分介紹一些ZooKeeper典型的應用場景。

ZooKeeper基本原理

1. 數據模型
zookeeper-tree
如上圖所示,ZooKeeper數據模型的結構與Unix文件系統很類似,整體上可以看作是一棵樹,每個節點稱做一個ZNode。每個ZNode都可以通過其路徑唯一標識,比如上圖中第三層的第一個ZNode, 它的路徑是/app1/c1。在每個ZNode上可存儲少量數據(默認是1M, 可以通過配置修改, 通常不建議在ZNode上存儲大量的數據),這個特性非常有用,在後面的典型應用場景中會介紹到。另外,每個ZNode上還存儲了其Acl信息,這裏需要注意,雖說ZNode的樹形結構跟Unix文件系統很類似,但是其Acl與Unix文件系統是完全不同的,每個ZNode的Acl的獨立的,子結點不會繼承父結點的,關於ZooKeeper中的Acl可以參考之前寫過的一篇文章《說說Zookeeper中的ACL》。

2.重要概念 
2.1 ZNode
前文已介紹了ZNode, ZNode根據其本身的特性,可以分爲下面兩類:

  • Regular ZNode: 常規型ZNode, 用戶需要顯式的創建、刪除
  • Ephemeral ZNode: 臨時型ZNode, 用戶創建它之後,可以顯式的刪除,也可以在創建它的Session結束後,由ZooKeeper Server自動刪除

ZNode還有一個Sequential的特性,如果創建的時候指定的話,該ZNode的名字後面會自動Append一個不斷增加的SequenceNo。

2.2 Session
Client與ZooKeeper之間的通信,需要創建一個Session,這個Session會有一個超時時間。因爲ZooKeeper集羣會把Client的Session信息持久化,所以在Session沒超時之前,Client與ZooKeeper Server的連接可以在各個ZooKeeper Server之間透明地移動。

在實際的應用中,如果Client與Server之間的通信足夠頻繁,Session的維護就不需要其它額外的消息了。否則,ZooKeeper Client會每t/3 ms發一次心跳給Server,如果Client 2t/3 ms沒收到來自Server的心跳回應,就會換到一個新的ZooKeeper Server上。這裏t是用戶配置的Session的超時時間。

2.3 Watcher
ZooKeeper支持一種Watch操作,Client可以在某個ZNode上設置一個Watcher,來Watch該ZNode上的變化。如果該ZNode上有相應的變化,就會觸發這個Watcher,把相應的事件通知給設置Watcher的Client。需要注意的是,ZooKeeper中的Watcher是一次性的,即觸發一次就會被取消,如果想繼續Watch的話,需要客戶端重新設置Watcher。這個跟epoll裏的oneshot模式有點類似。

3. ZooKeeper特性 
3.1 讀、寫(更新)模式
在ZooKeeper集羣中,讀可以從任意一個ZooKeeper Server讀,這一點是保證ZooKeeper比較好的讀性能的關鍵;寫的請求會先Forwarder到Leader,然後由Leader來通過ZooKeeper中的原子廣播協議,將請求廣播給所有的Follower,Leader收到一半以上的寫成功的Ack後,就認爲該寫成功了,就會將該寫進行持久化,並告訴客戶端寫成功了。

3.2 WAL和Snapshot
和大多數分佈式系統一樣,ZooKeeper也有WAL(Write-Ahead-Log),對於每一個更新操作,ZooKeeper都會先寫WAL, 然後再對內存中的數據做更新,然後向Client通知更新結果。另外,ZooKeeper還會定期將內存中的目錄樹進行Snapshot,落地到磁盤上,這個跟HDFS中的FSImage是比較類似的。這麼做的主要目的,一當然是數據的持久化,二是加快重啓之後的恢復速度,如果全部通過Replay WAL的形式恢復的話,會比較慢。

3.3 FIFO
對於每一個ZooKeeper客戶端而言,所有的操作都是遵循FIFO順序的,這一特性是由下面兩個基本特性來保證的:一是ZooKeeper Client與Server之間的網絡通信是基於TCP,TCP保證了Client/Server之間傳輸包的順序;二是ZooKeeper Server執行客戶端請求也是嚴格按照FIFO順序的。

3.4 Linearizability
在ZooKeeper中,所有的更新操作都有嚴格的偏序關係,更新操作都是串行執行的,這一點是保證ZooKeeper功能正確性的關鍵。

ZooKeeper Client API

ZooKeeper Client Library提供了豐富直觀的API供用戶程序使用,下面是一些常用的API:

  • create(path, data, flags): 創建一個ZNode, path是其路徑,data是要存儲在該ZNode上的數據,flags常用的有: PERSISTEN, PERSISTENT_SEQUENTAIL, EPHEMERAL, EPHEMERAL_SEQUENTAIL
  • delete(path, version): 刪除一個ZNode,可以通過version刪除指定的版本, 如果version是-1的話,表示刪除所有的版本
  • exists(path, watch): 判斷指定ZNode是否存在,並設置是否Watch這個ZNode。這裏如果要設置Watcher的話,Watcher是在創建ZooKeeper實例時指定的,如果要設置特定的Watcher的話,可以調用另一個重載版本的exists(path, watcher)。以下幾個帶watch參數的API也都類似
  • getData(path, watch): 讀取指定ZNode上的數據,並設置是否watch這個ZNode
  • setData(path, watch): 更新指定ZNode的數據,並設置是否Watch這個ZNode
  • getChildren(path, watch): 獲取指定ZNode的所有子ZNode的名字,並設置是否Watch這個ZNode
  • sync(path): 把所有在sync之前的更新操作都進行同步,達到每個請求都在半數以上的ZooKeeper Server上生效。path參數目前沒有用
  • setAcl(path, acl): 設置指定ZNode的Acl信息
  • getAcl(path): 獲取指定ZNode的Acl信息

ZooKeeper典型應用場景

1. 名字服務(NameService) 
分佈式應用中,通常需要一套完備的命令機制,既能產生唯一的標識,又方便人識別和記憶。 我們知道,每個ZNode都可以由其路徑唯一標識,路徑本身也比較簡潔直觀,另外ZNode上還可以存儲少量數據,這些都是實現統一的NameService的基礎。下面以在HDFS中實現NameService爲例,來說明實現NameService的基本布驟:

  • 目標:通過簡單的名字來訪問指定的HDFS機羣
  • 定義命名規則:這裏要做到簡潔易記憶。下面是一種可選的方案: [serviceScheme://][zkCluster]-[clusterName],比如hdfs://lgprc-example/表示基於lgprc ZooKeeper集羣的用來做example的HDFS集羣
  • 配置DNS映射: 將zkCluster的標識lgprc通過DNS解析到對應的ZooKeeper集羣的地址
  • 創建ZNode: 在對應的ZooKeeper上創建/NameService/hdfs/lgprc-example結點,將HDFS的配置文件存儲於該結點下
  • 用戶程序要訪問hdfs://lgprc-example/的HDFS集羣,首先通過DNS找到lgprc的ZooKeeper機羣的地址,然後在ZooKeeper的/NameService/hdfs/lgprc-example結點中讀取到HDFS的配置,進而根據得到的配置,得到HDFS的實際訪問入口

2. 配置管理(Configuration Management) 
在分佈式系統中,常會遇到這樣的場景: 某個Job的很多個實例在運行,它們在運行時大多數配置項是相同的,如果想要統一改某個配置,一個個實例去改,是比較低效,也是比較容易出錯的方式。通過ZooKeeper可以很好的解決這樣的問題,下面的基本的步驟:

  • 將公共的配置內容放到ZooKeeper中某個ZNode上,比如/service/common-conf
  • 所有的實例在啓動時都會傳入ZooKeeper集羣的入口地址,並且在運行過程中Watch /service/common-conf這個ZNode
  • 如果集羣管理員修改了了common-conf,所有的實例都會被通知到,根據收到的通知更新自己的配置,並繼續Watch /service/common-conf

3. 組員管理(Group Membership) 
在典型的Master-Slave結構的分佈式系統中,Master需要作爲“總管”來管理所有的Slave, 當有Slave加入,或者有Slave宕機,Master都需要感知到這個事情,然後作出對應的調整,以便不影響整個集羣對外提供服務。以HBase爲例,HMaster管理了所有的RegionServer,當有新的RegionServer加入的時候,HMaster需要分配一些Region到該RegionServer上去,讓其提供服務;當有RegionServer宕機時,HMaster需要將該RegionServer之前服務的Region都重新分配到當前正在提供服務的其它RegionServer上,以便不影響客戶端的正常訪問。下面是這種場景下使用ZooKeeper的基本步驟:

  • Master在ZooKeeper上創建/service/slaves結點,並設置對該結點的Watcher
  • 每個Slave在啓動成功後,創建唯一標識自己的臨時性(Ephemeral)結點/service/slaves/${slave_id},並將自己地址(ip/port)等相關信息寫入該結點
  • Master收到有新子結點加入的通知後,做相應的處理
  • 如果有Slave宕機,由於它所對應的結點是臨時性結點,在它的Session超時後,ZooKeeper會自動刪除該結點
  • Master收到有子結點消失的通知,做相應的處理

4. 簡單互斥鎖(Simple Lock) 
我們知識,在傳統的應用程序中,線程、進程的同步,都可以通過操作系統提供的機制來完成。但是在分佈式系統中,多個進程之間的同步,操作系統層面就無能爲力了。這時候就需要像ZooKeeper這樣的分佈式的協調(Coordination)服務來協助完成同步,下面是用ZooKeeper實現簡單的互斥鎖的步驟,這個可以和線程間同步的mutex做類比來理解:

  • 多個進程嘗試去在指定的目錄下去創建一個臨時性(Ephemeral)結點 /locks/my_lock
  • ZooKeeper能保證,只會有一個進程成功創建該結點,創建結點成功的進程就是搶到鎖的進程,假設該進程爲A
  • 其它進程都對/locks/my_lock進行Watch
  • 當A進程不再需要鎖,可以顯式刪除/locks/my_lock釋放鎖;或者是A進程宕機後Session超時,ZooKeeper系統自動刪除/locks/my_lock結點釋放鎖。此時,其它進程就會收到ZooKeeper的通知,並嘗試去創建/locks/my_lock搶鎖,如此循環反覆

5. 互斥鎖(Simple Lock without Herd Effect) 
上一節的例子中有一個問題,每次搶鎖都會有大量的進程去競爭,會造成羊羣效應(Herd Effect),爲了解決這個問題,我們可以通過下面的步驟來改進上述過程:

  • 每個進程都在ZooKeeper上創建一個臨時的順序結點(Ephemeral Sequential) /locks/lock_${seq}
  • ${seq}最小的爲當前的持鎖者(${seq}是ZooKeeper生成的Sequenctial Number)
  • 其它進程都對只watch比它次小的進程對應的結點,比如2 watch 1, 3 watch 2, 以此類推
  • 當前持鎖者釋放鎖後,比它次大的進程就會收到ZooKeeper的通知,它成爲新的持鎖者,如此循環反覆

這裏需要補充一點,通常在分佈式系統中用ZooKeeper來做Leader Election(選主)就是通過上面的機制來實現的,這裏的持鎖者就是當前的“主”。

6. 讀寫鎖(Read/Write Lock) 
我們知道,讀寫鎖跟互斥鎖相比不同的地方是,它分成了讀和寫兩種模式,多個讀可以併發執行,但寫和讀、寫都互斥,不能同時執行行。利用ZooKeeper,在上面的基礎上,稍做修改也可以實現傳統的讀寫鎖的語義,下面是基本的步驟:

  • 每個進程都在ZooKeeper上創建一個臨時的順序結點(Ephemeral Sequential) /locks/lock_${seq}
  • ${seq}最小的一個或多個結點爲當前的持鎖者,多個是因爲多個讀可以併發
  • 需要寫鎖的進程,Watch比它次小的進程對應的結點
  • 需要讀鎖的進程,Watch比它小的最後一個寫進程對應的結點
  • 當前結點釋放鎖後,所有Watch該結點的進程都會被通知到,他們成爲新的持鎖者,如此循環反覆

7. 屏障(Barrier) 
在分佈式系統中,屏障是這樣一種語義: 客戶端需要等待多個進程完成各自的任務,然後才能繼續往前進行下一步。下用是用ZooKeeper來實現屏障的基本步驟:

  • Client在ZooKeeper上創建屏障結點/barrier/my_barrier,並啓動執行各個任務的進程
  • Client通過exist()來Watch /barrier/my_barrier結點
  • 每個任務進程在完成任務後,去檢查是否達到指定的條件,如果沒達到就啥也不做,如果達到了就把/barrier/my_barrier結點刪除
  • Client收到/barrier/my_barrier被刪除的通知,屏障消失,繼續下一步任務

8. 雙屏障(Double Barrier)
雙屏障是這樣一種語義: 它可以用來同步一個任務的開始和結束,當有足夠多的進程進入屏障後,纔開始執行任務;當所有的進程都執行完各自的任務後,屏障才撤銷。下面是用ZooKeeper來實現雙屏障的基本步驟:

  • 進入屏障:

Client Watch /barrier/ready結點, 通過判斷該結點是否存在來決定是否啓動任務每個任務進程進入屏障時創建一個臨時結點/barrier/process/${process_id},然後檢查進入屏障的結點數是否達到指定的值,如果達到了指定的值,就創建一個/barrier/ready結點,否則繼續等待Client收到/barrier/ready創建的通知,就啓動任務執行過程
  • 離開屏障:

Client Watch /barrier/process,如果其沒有子結點,就可以認爲任務執行結束,可以離開屏障每個任務進程執行任務結束後,都需要刪除自己對應的結點/barrier/process/${process_id}

轉自:test_soy的博客  https://blog.csdn.net/test_soy/article/details/78200957

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