天天看點

深入了解zookeeper

zookeeper概況

背景&問題

在生産環境中,為了提高服務可用性、支撐更多的使用者量等,分布式應用服務都會在不同IDC多個節點上部署,我們很可能會遇到以下問題:

  • 分布在各個機器、IDC的應用程式如何能高效讀取、修改配置?
  • 當配置變更時,各節點應用如何快速發現變化、及時響應處理?
  • 如何在應用程式部署的各個節點中,選舉一個節點,作為leader,執行協調相關操作? leader挂掉時,其他節點能重新發起leader選舉? 如何避免腦裂? 如何處理網絡分區?
  • 當某個節點異常挂掉時,如何及時發現?

第1、2點在節點數較少、對性能要求不高的情況下,我們可以通過将配置存儲在mysql+定時輪詢解決。若對性能要求較高我們就需要結合cache、agent、配置變更notify、mysql等元件實作一套配置系統來解決,如淘寶的diamond。

第3、第4點,在複雜的分布式環境中,我們會遇到高網絡延時、網絡波動、磁盤故障、機器當機、機器半死不活、機房斷電、網絡分區等一系列問題,同時要避免資料不一緻、腦裂,還要追求高吞吐、低延時,最大程度減少因選舉leader導緻服務不可用時間等,最糟糕的是分布式理論FLP(consensus is impossible with asynchronous systems and even one failure)、CAP(consistency,high availability,partition-tolerance)告訴我們在設計上需要權衡取舍,這些如果讓業務應用程式來處理,就具有一定的複雜性。

是以,這類複雜問題不适合應用程式自己解決,應用程式需要一個God,一個值得信賴的Oracle,同時God提供的Service應該盡量簡單、易了解、高性能、易擴充。

Yahoo的工程師們為了解決應用這些問題,設計實作了zookeeper,為什麼叫zookeeper? 因為yahoo内部不少分布式系統命名是動物名字,同時分布式環境中的複雜、混亂跟動物園(zoo)是不是有點類似?而zookeeper就是維持、管理整個動物園的秩序,這就是zookeeper的名稱的來曆。

ZooKeeper是一個分布式管理服務,可為應用提供配置管理、名稱服務、狀态同步、叢集管理等功能,我們的應用場景主要是配置管理、分布式鎖。

  • 為什麼apache給它定義是個分布式協調式服務,而不是存儲服務?它的設計目标定位是什麼?
  • 它可以當存儲服務來使用嗎? 它的資料模型是怎樣的? 如何持久化存儲的?
  • zookeeper服務端的讀寫流程是怎樣實作的?
  • zookeeper c api是如何實作的?
  • 如何通過zookeeper實作分布式鎖、Leader Election?
  • 在生産環境實踐中我們遇到了哪些問題?如何優化zookeeper性能? 對zookeeper進行監控?

本文将結合zookeeper源碼(3.4.6)、在生産環境實踐經驗,通過分析以上問題來深入了解zookeeper,以及分享我們在實踐中遇到的問題及經驗。

首先,我們一窺zookeeper全貌,了解下其總體架構及設計目标。

zookeeper架構

深入了解zookeeper

zookeeper叢集節點數量一般由奇數個節點組成,節點角色由follower和leader組成,所有寫請求需轉發到leader,每次寫請求需叢集一半以上節點應答成功才寫入成功,讀請求在任意一台follower節點上都可以處理,隻要叢集中有一半以上的節點存活、并能互相通信,zookeeper叢集就可以持續提供服務,是以具有較高的可用性。節點數量越多,可用性會進一步提高,但是會影響寫性能,是以在生産環境中一般部署5台。 zookeeper提供的接口類似nosql系統,常用的接口有get/set/create/getchildren等,接口簡單易用。

zookeeper設計目标

簡單

zookeeper資料模型簡單,易懂,類似檔案系統的層次樹形資料結構,存儲資料未做shard分散到多機,而是各單機完整存儲整個樹形層次空間上所有路徑的節點資料,資料全部儲存在記憶體,是以可提供高吞吐量、低延遲的服務,也意味着zookeeper不适合儲存大節點資料。

高可用、高性能讀

因單機上儲存了所有資料,若沒有多機之間資料同步複制機制,zookeeper系統可用性将極低,是以zookeeper在設計上一個重要目标是可複制的,各節點通過zookeeper atomic broadcast算法選舉leader,同步資料。所有寫請求follower節點都需轉發給leader,讀請求在任意一台follower節點上都可以處理。

有序

zookeeper通過基于tcp連接配接、寫請求由leader處理等機制提供有序保證,基于有序機制,zookeeper可以提供同步原語,實作分布式鎖等機制。

zookeeper資料模型

存儲系統常見的資料模型有關系型表格型(Relational Model)、層次樹型(Hierarchical model)、扁平型(Flat model)、網絡型(Network Model)、對象型(Object-oriented Model).

深入了解zookeeper

zookeeper的資料模型是層次型,類似檔案系統,但是zookeeper的設計目标定位是簡單、高可靠、高吞吐、低延遲的記憶體型存儲系統,是以它的value不像檔案系統那樣會适合儲存大的值,官方建議儲存的value大小要小于1M,提供的接口類似nosql存儲系統(key是路徑)。

深入了解zookeeper

那麼zookeeper的層次模型是通過什麼資料結構實作的呢? get、set、getchildren的時間複雜度又分别是多少呢? 通過閱讀zookeeper server源碼,zookeeper是基于ConcurrentHashMap實作的,path是key,value是DataNode,DataNode儲存了value、children、 stat等資訊。

  1. zookeeper database模型的調用鍊路

  2. ZKDatabase

  3. DataTree

  4. ConcurrentHashMap<String,DataNode> nodes =newConcurrentHashMap<String,DataNode>();

  5. DataNode

  6. data,acl,stat,children

  7. classStat{

  8. long czxid;// created zxid

  9. long mzxid;// last modified zxid

  10. long ctime;// created

  11. long mtime;// last modified

  12. int version;// version

  13. int cversion;// child version

  14. int aversion;// acl version

  15. long ephemeralOwner;// owner id if ephemeral, 0 otw

  16. int dataLength;//length of the data in the node

  17. int numChildren;//number of children of this node

  18. long pzxid;// last modified children

  19. }

ConcurrentHashMap是線程安全的hash table,采用了鎖分段技術來減少鎖競争,以提高性能。其結構如下圖所示,由兩部分組成,Segment和HashEntry,鎖的粒度是Segment,每個Segment 對象包含整個散列映射表的若幹個桶,散列沖突時通過連結清單來解決.

深入了解zookeeper

是以zookeeper在使用ConcurrentHashMap時其各接口期望時間複雜度如下:

  • get:O(1)
  • create/set:O(1)
  • getchildren:O(1)

zookeeper持久化存儲

從資料模型我們知道zookeeper所有資料都是加載都記憶體,基于ConcurrentHashMap建構一顆DataTree,那麼zookeeper要保證機器重新開機資料不丢失就需要實作持久化存儲,而zookeeper的持久化實作是通過snapshot、txnlog實作的,snapshot是zookeeper記憶體資料的完整鏡像,zookeeper在運作中會定時生成,txnlog是快照時間點之後的事物日志,zookeeper在重新開機時,通過snapshot和txnlog重建DataTree. 下圖是運作中的zookeeper叢集的生成的資料檔案。

深入了解zookeeper

snapshot和log檔案分布儲存在哪?保留多少個snapshot和log檔案? 什麼時候清理廢棄的snapshot和log 檔案? 這些都可以通過在zookeeper的zoo.cfg配置檔案中指定,dataDir指定snapshot路徑,dataLogDir指定事物日志路徑,事物日志對zk吞吐量、延時有着非常大的延時,建議datadir與dataLogDir使用不同的裝置,避免磁盤IO資源的争奪,影響整個系統性能和穩定性。autopurge.snapRetainCount項表示保留多少個snapshot,每個snapshot快照清理間隔小時可以通過autopurge.purgeInterval來指定。

snapshot的生成和log檔案的寫入是在SyncRequestProcessor類中實作的,事物日志類TxnLog,快照類FileSnap,事物日志會追加到TxnLog,當記錄數大于1000會刷到磁盤,當寫入log數大于snapCount/2+randRoll(nextInt(snapCount/2)時,會開啟線程将DataTree dump到磁盤,具體實作邏輯如下:

  1. if(zks.getZKDatabase().append(si)){

  2. logCount++;

  3. if(logCount >(snapCount /2+ randRoll)){

  4. randRoll = r.nextInt(snapCount/2);

  5. // roll the log

  6. zks.getZKDatabase().rollLog();

  7. // take a snapshot

  8. if(snapInProcess !=null&& snapInProcess.isAlive()){

  9. LOG.warn("Too busy to snap, skipping");

  10. }else{

  11. snapInProcess =newThread("Snapshot Thread"){

  12. publicvoid run(){

  13. try{

  14. zks.takeSnapshot();

  15. }catch(Exception e){

  16. LOG.warn("Unexpected exception", e);

  17. }

  18. }

  19. };

  20. snapInProcess.start();

  21. }

  22. logCount =0;

  23. }

  24. }

  25. toFlush.add(si);

  26. if(toFlush.size()>1000){

  27. flush(toFlush);

  28. }

從zookeeper持久化的基本實作可知若寫請求較大會頻繁生成快照,同時因為toFlush是同步重新整理資料到磁盤的,是以會影響吞吐率、延時,這也是為什麼txnlog建議使用性能較好的存儲硬體的原因(如SSD)。

zookeeper核心角色及概念

leader

follower

observer

session

watcher

access control

zookeeper server讀寫流程分析

在zookeeper的服務端實作中,通過抽象出leader、follower、observer共性特點,讀寫請求的處理流程可以按照功能拆分成各階段(pipeline),每個processor負責處理其中一個階段,采用設計模式的職責鍊形式,一個processor處理完,通過隊列分發到下一個processor中。processor相當于工廠各元部件,而leader、follower、observer隻是使用、組裝的各元部件不一緻,但他們可以高度複用相同的元部件,精簡實作,減少代碼備援。

職責鍊處理類介紹

PrepRequestProcessor

此處理類根據請求的指令(create,set等)負責生成事物請求資訊資料結構request,統計正在進行的事物等。

FollowerRequestProcessor

此處理類負責将寫請求分發給leader.

CommitProcessor

SyncRequestProcessor

如前面持久化存儲所述,此處理類負責持久化存儲,将批量事物日志重新整理到磁盤和定時生成快照。

SendAckRequestProcessor

此處理類在收到寫請求提議後,回複ACK給leader.

ProposalRequestProcessor

此處理類負責将所有寫請求轉發給follower節點。

ToBeAppliedRequestProcessor

FinalRequestProcessor

此處理類如名字所言,是請求流行線式處理最後一環,負責處理查詢請求(從zkdatabase的DataTree讀取資料)和寫事務請求。

zookeeper讀流程

zookeeper寫流程

zookeeper c api

總結

參考資料

  • Apache Zookeeper
  • Apache ZooKeeper: the making of
  • ZooKeeper學習之server端實作的基本骨架