下述各zookeeper機制的java客戶端實踐參考zookeeper java客戶端之curator詳解。
官方文檔http://zookeeper.apache.org/doc/current/zookeeperOver.html、http://zookeeper.apache.org/doc/current/zookeeperInternals.html描述了部分關於zk的內部工作機制,但是並不夠友好和詳細。
zookeeper簡介
據官網介紹,ZooKeeper是一個用於提供配置信息、命名服務、分布式協調以及分組服務的中心化服務,它們是分布式應用所必需的。從實際應用來看,zookeeper是最廣泛使用的分布式協調服務,包括dubbo、kafka、hadoop、es-job等都依賴於zookeeper提供的分布式協調和注冊服務。其他用於提供注冊服務的中間件還包括consul以及etcd、eureka,但都不及zookeeper廣泛。其他適用場景可見https://xie.infoq.cn/article/1dcd3f8b100645e0da782a279。
官網:https://zookeeper.apache.org/,https://zookeeper.apache.org/doc/r3.4.14/。
zookeeper配置文件詳解:https://zookeeper.apache.org/doc/r3.4.14/zookeeperAdmin.html#sc_configuration,也可以參考https://www.cnblogs.com/smail-bao/p/7009633.html
核心機制
zookeeper節點角色
在zookeeper中,節點分為下列幾種角色:
- 領導者(leader),負責進行投票的發起和決議,更新系統狀態,在Zookeeper集群中,只有一個Leader節點。
- 學習者(learner),包括跟隨者(follower)和觀察者(observer)。
- follower用於接受客戶端請求並想客戶端返回結果,在選主過程中參與投票,在Zookeeper集群中,follower可以為多個。
- Observer可以接受客戶端連接,將寫請求轉發給leader,但observer不參加投票過程,只同步leader的狀態,observer的目的是為了擴展系統,提高讀取速度(不參與投票降低選舉的復雜度)
在一個zookeeper集群,各節點之間的交互如下所示:
注:幾乎所有現代基於分布式架構的中間件都是采用類似做法,例如kafka、es等。
從上可知,所有請求均由客戶端發起,它可能是本地zkCli或java客戶端。 各角色詳細職責如下。
Leader
leader的職責包括:
- 恢復數據;
- 維持與Learner的心跳,接收Learner請求並判斷Learner的請求消息類型;
Leader的工作流程簡圖如下所示,在實際實現中,啟動了三個線程來實現功能。
Follower
follower的主要職責為:
向Leader發送請求;
接收Leader的消息並進行處理;
接收Zookeeper Client的請求,如果為寫清求,轉發給Leader進行處理
Follower的工作流程簡圖如下所示,在實際實現中,Follower是通過5個線程來實現功能的。
各種消息的含義如下:
PING:心跳消息。
PROPOSAL:Leader發起的提案,要求Follower投票。
COMMIT:服務器端最新一次提案的信息。
UPTODATE:表明同步完成。
REVALIDATE:根據Leader的REVALIDATE結果,關閉待revalidate的session還是允許其接受消息。
SYNC:返回SYNC結果到客戶端,這個消息最初由客戶端發起,用來強制得到最新的更新。
zookeeper數據存儲機制
雖然zookeeper采用的是文件系統存儲機制,但是所有數據數據都存儲於內存中。其對外提供的視圖類似於Unix文件系統。樹的根Znode節點相當於Unix文件系統的根路徑。
節點類型
zk中的節點稱之為znode(也叫data register,也就是存儲數據的文件夾),按其生命周期的長短可以分為持久結點(PERSISTENT)和臨時結點(EPHEMERAL);在創建時還可選擇是否由Zookeeper服務端在其路徑后添加一串序號用來區分同一個父結點下多個結點創建的先后順序。
經過組合就有以下4種Znode結點類型:
- persistent:永久性znode。
- ephemeral: 隨着創建的客戶端關閉而自動刪除,不過它們仍然對所有客戶端可見,ephemeral節點不允許有子節點。是實現分布式協調的核心機制。
- sequential:附屬於上述兩類節點,是一種特性。在創建時,zookeeper會在其名字上分配一個序列號。可以作為全局分布式隊列使用。如下:
zookeeper的一致性保證
zookeeper通過下列機制實現一致性保證:
» 所有更新請求順序進行,來自同一個client的更新請求按其發送順序依次執行
» 數據更新原子性,一次數據更新要么成功,要么失敗
» 全局唯一數據視圖,client無論連接到哪個server,數據視圖都是一致的,基於所有寫請求全部由leader完成,然后同步實現
» 實時性,在一定事件范圍內,client能讀到最新數據
讀寫機制
» Zookeeper是一個由多個server組成的集群
» 一個leader,多個follower
» 每個server保存一份數據副本
» 全局數據一致
» 分布式讀寫
» 更新請求全部轉發由leader完成,並在成功后同步給follower
客戶端寫請求的過程如下:
其過程為:
- 1.所有的事務請求都交由集群的Leader服務器來處理,Leader服務器會將一個事務請求轉換成一個Proposal(提議),並為其生成一個全局遞增的唯一ID,這個ID就是事務ID,即ZXID,Leader服務器對Proposal是按其ZXID的先后順序來進行排序和處理的。
- 2.之后Leader服務器會將Proposal放入每個Follower對應的隊列中(Leader會為每個Follower分配一個單獨的隊列),並以FIFO的方式發送給Follower服務器。
- 3.Follower服務器接收到事務Proposal后,首先以事務日志的方式寫入本地磁盤,並且在成功后返回Leader服務器一個ACK響應。
- 4.Leader服務器只要收到過半Follower的ACK響應,就會廣播一個Commit消息給Follower以通知其進行Proposal的提交,同時Leader自身也會完成Proposal的提交。
由於每次的請求都需要轉發給leader並進行投票處理,所以zookeeper並不適合於寫密集型的場景,例如序列產生器、分布式鎖,不同節點數量、不同讀寫比例下zk的tps如下:
來源於官方測試。上述測試基於3.2,2Ghz Xeon, 2塊SATA 15K RPM硬盤。日志(WAL)在單獨的硬盤,快照(zk內存數據快照)寫在OS系統盤,讀寫分別為1K大小,且客戶端不直連leader。且從上可知,節點越多、寫越慢、讀越快,所以一般節點不會很多,但是為了做擴展性和異地,會使用observer節點。
dataDir=/data
dataLogDir=/datalog
dataLogDir如果沒提供的話使用的則是dataDir。zookeeper的持久化都存儲在這兩個目錄里。dataLogDir里是放到的順序日志(WAL)。而dataDir里放的是內存數據結構的snapshot,便於快速恢復(目前基本上所有帶持久化特性的中間件如redis 4.x(kafka采用磁盤append,是個另類)都是借鑒數據庫(oracle/mysql也支持buffer_pool/sga的dump)的做法,定期快照+WAL重放,而不是重啟后清空來盡可能提升性能)。為了達到性能最大化,一般建議把dataDir和dataLogDir分到不同的磁盤上,這樣就可以充分利用磁盤順序寫的特性。如下:
zookeeper快照文件的命名規則為snapshot.**,其中**表示zookeeper觸發快照的那個瞬間,提交的最后一個事務的ID。其默認不會清理,從3.4.0開始,zookeeper提供了自動清理snapshot和事務日志的功能,通過配置 autopurge.snapRetainCount 和 autopurge.purgeInterval 這兩個參數能夠實現定時清理了。這兩個參數都是在zoo.cfg中配置的:autopurge.purgeInterval 這個參數指定了清理頻率,單位是小時,需要填寫一個1或更大的整數,默認是0,表示不開啟自己清理功能。autopurge.snapRetainCount 這個參數和上面的參數搭配使用,這個參數指定了需要保留的文件數目。默認是保留3個。
zkid
znode節點的狀態信息中包含czxid, 那么什么是zxid呢? 在zk中,狀態的每一次改變, 都對應着一個遞增的Transaction id, 該id稱為zxid. 由於zxid的遞增性質, 如果zxid1小於zxid2, 那么zxid1肯定先於zxid2發生. 創建任意節點, 或者更新任意節點的數據, 或者刪除任意節點, 都會導致Zookeeper狀態發生改變, 從而導致zxid的值增加.
znode節點中的信息
Znode結構主要由存儲於其中的數據信息和狀態信息兩部分構成,通過get 命令獲取一個Znode結點的信息如下:
第一行存儲的是ZNode的數據信息,從cZxid開始就是Znode的狀態信息。Znode的狀態信息比較多,幾個主要的為:
-
czxid:
即Created ZXID,表示創建該Znode結點的事務ID -
mzxid:
即Modified ZXID,表示最后一次更新該結點的事務ID -
version
該Znode結點的版本號。每個Znode結點被創建時版本號都為0,每更新一次都會導致版本號加1,即使更新前后Znode存儲的值沒有變化版本號也會加1。version值可以形象的理解為Znode結點被更新的次數。Znode狀態信息中的版本號信息,使得服務端可以對多個客戶端對同一個Znode的更新操作做並發控制。整個過程和java中的CAS有點像,是一種樂觀鎖的並發控制策略,而version值起到了沖突檢測的功能。客戶端拿到Znode的version信息,並在更新時附上這個version信息,服務端在更新Znode時必須必須比較客戶端的version和Znode的實際version,只有這兩個version一致時才會進行修改。
ZooKeeper默認情況下對數據字段的傳輸限制為1MB(所有分布式應用幾乎默認都這個大小,如kafka、dubbo),該限制為任何節點數據字段的最大可存儲字節數,同時也限制了任何父節點可以擁有的子節點數。
zookeeper的其他核心機制
- Zookeeper的核心是原子廣播,這個機制保證了各個Server之間的同步。實現這個機制的協議叫做Zab協議。Zab協議有兩種模式,它們分別是恢復模式(選主)和廣播模式(同步)。當服務啟動或者在領導者崩潰后,Zab就進入了恢復模式,當領導者被選舉出來,且大多數Server完成了和leader的狀態同步以后,恢復模式就結束了。狀態同步保證了leader和Server具有相同的系統狀態。
- 為了保證事務的順序一致性,zookeeper采用了遞增的事務id號(zxid)來標識事務。所有的提議(proposal)都在被提出的時候加上了zxid。實現中zxid是一個64位的數字,它高32位是epoch用來標識leader關系是否改變,每次一個leader被選出來,它都會有一個新的epoch,標識當前屬於那個leader的統治時期。低32位用於遞增計數。
- 每個Server在工作過程中有三種狀態:
LOOKING:當前Server不知道leader是誰,正在搜尋
LEADING:當前Server即為選舉出來的leader
FOLLOWING:leader已經選舉出來,當前Server與之同步
分布式系統的一致性實現算法之paxos
paxos算法基於這樣的原理:
• 在一個分布式數據庫系統中,如果各節點的初始狀態一致,每個節點都執行相同的操作序列,那么他們最后能得到一個一致的狀態。
• Paxos算法解決的什么問題呢,解決的就是保證每個節點執行相同的操作序列。好吧,這還不簡單,master維護一個
全局寫隊列,所有寫操作都必須 放入這個隊列編號,那么無論我們寫多少個節點,只要寫操作是按編號來的,就能保證一
致性。沒錯,就是這樣,可是如果master掛了呢。
• Paxos算法通過投票來對寫操作進行全局編號,同一時刻,只有一個寫操作被批准,同時並發的寫操作要去爭取選票,
只有獲得過半數選票的寫操作才會被 批准(所以永遠只會有一個寫操作得到批准),其他的寫操作競爭失敗只好再發起一
輪投票,就這樣,在日復一日年復一年的投票中,所有寫操作都被嚴格編號排 序。編號嚴格遞增,當一個節點接受了一個
編號為100的寫操作,之后又接受到編號為99的寫操作(因為網絡延遲等很多不可預見原因),它馬上能意識到自己 數據
不一致了,自動停止對外服務並重啟同步過程。任何一個節點掛掉都不會影響整個集群的數據一致性(總2n+1台,除非掛掉大於n台)
因此在生產中,要求zookeeper部署3(單機房)或5(單機房或多機房)或7(跨機房)個節點的集群。
zookeeper java官方客戶端核心package簡介
- org.apache.zookeeper 包含ZooKeeper客戶端主要類,ZooKeeper watch和各種回調接口的定義。
- org.apache.zookeeper.data 定義了和data register相關的特性
- org.apache.zookeeper.server, org.apache.zookeeper.server.quorum, org.apache.zookeeper.server.upgrade是服務器實現的核心接口
- org.apache.zookeeper.client定義了Four Letter Word的主要類
由於zookeeper的java官方客戶端太不友好,因此實際中一般使用三方客戶端Curator。故不對zookeeper客戶端進行詳細分析,參見本文首部對curator的詳解。
watch機制
watch是zookeeper針對節點的一次性觀察者機制(即一次觸發后就失效,需要手工重新創建watch),行為上類似於數據庫的觸發器。
當watch監視的數據發生時,通知設置了該watch的client,客戶端即watcher。watcher的機制是監聽數據發生了某些變化,所以一定會有對應的事件類型和狀態類型,一個客戶端可以監控多個節點,在代碼中體現在new了幾個就產生幾個watcher,只要節點變化都會執行一遍process。其示意圖如下:
在zookeeper中,watch是采用推送機制實現的,而不是客戶端輪訓(有些中間件采用拉的模式,例如kafka消費者,這主要取決於設計者認為的合理性,一般來說流量很大的適合於拉的模式,這樣更好做控制,否則客戶端容易失控;反之推的模式)。watch有兩種類型的事件能夠監聽:znode相關的及客戶端實例相關的。分別為:
- 事件類型:(znode節點相關的)【針對的是你所觀察的一個節點而言的】
- EventType.NodeCreated 【節點創建】
- EventType.NodeDataChanged 【節點數據發生變化】
- EventType.NodeChildrenChanged 【這個節點的子節點發生變化】
- EventType.NodeDeleted 【刪除當前節點】
- 狀態類型:(是跟客戶端實例相關的)【ZooKeeper集群跟應用服務之間的狀態的變更】
- KeeperState.Disconnected 【沒有連接上】
- KeeperState.SyncConnected 【連接上】
- KeeperState.AuthFailed 【認證失敗】
- KeeperState.Expired 【過期】
總結起來,zk watch的特性為:
- 一次性:對於ZooKeeper的watcher,你只需要記住一點,ZooKeeper有watch事件,是一次性觸發的,當watch監視的數據發生變化時,通知設置該watch的client,即watcher,由於ZooKeeper的監控都是一次性的,所以每次必須設置監控。在這里,LZ不得不說一句,一次觸發其實是一個特性、並非設計缺陷,且zk各api已經提供了開關是否繼續開啟,並沒有帶來不方便,所以把它做缺點是說不過去的。
- 客戶端串行執行:客戶端watcher回調的過程是一個串行同步的過程,這為我們保證了順序,同時需要開發人員注意一點,千萬不要因為一個watcher的處理邏輯影響了整個客戶端的watcher回調
- 輕量:WatchedEvent是ZooKeeper整個Watcher通知機制的最小通知單元,整個結構只包含三個部分:通知狀態、事件類型和節點路徑。也就是說Watcher通知非常的簡單,只會告知客戶端發生了事件而不會告知其具體內容,需要客戶端自己去進行獲取,比如NodeDataChanged事件,ZooKeeper只會通知客戶端指定節點的數據發生了變更,而不會直接提供具體的數據內容
-
客戶端設置的每個監視點與會話關聯,如果會話過期,等待中的監視點將會被刪除。不過監視點可以跨越不同服務端的連接而保持,例如,當一個ZooKeeper客戶端與一個ZooKeeper服務端的連接斷開后連接到集合中的另一個服務端,客戶端會發送未觸發的監視點列表,在注冊監視點時,服務端將要檢查已監視的znode節點在之前注冊監視點之后是否已經變化,如果znode節點已經發生變化,一個監視點的事件就會被發送給客戶端,否則在新的服務端上注冊監視點。這一機制使得我們可以關心邏輯層會話,而非底層連接本身。
LEADER服務器的選舉
兩種情況下會發生Leader節點的選舉,集群初始構建的時候;其次,無論如何,leader總是有可能發生宕機可能的。zookeeper中leader的選舉過程為:
集群中的服務器會向其他所有的Follower服務器發送消息,這個消息可以形象化的稱之為選票,選票主要由兩個信息組成,所推舉的Leader服務器的ID(即配置在myid文件中的數字),以及該服務器的事務ID,事務表示對服務器狀態變更的操作,一個服務器的事務ID越大,則其數據越新。整個過程如下所述:
- 1.Follower服務器投出選票(SID,ZXID),第一次每個Follower都會推選自己為Leader服務器,也就是說每個Follower第一次投出的選票是自己的服務器ID和事務ID。
- 2.每個Follower都會接收到來自於其他Follower的選票,它會基於如下規則重新生成一張選票:比較收到的選票和自己的ZXID的大小,選取其中最大的;若ZXID一樣則選取SID即服務器ID最大的。最終每個服務器都會重新生成一張選票,並將該選票投出去。
這樣經過多輪投票后,如果某一台服務器得到了超過半數的選票,則其將當前選為Leader。由以上分析可知,Zookeeper集群對Leader服務器的選擇具有偏向性,偏向於那些ZXID更大,即數據更新的機器。
整個過程如下圖所示:
所以這里實際上簡化了,有一個最后達成一致的細節過程需要進一步闡述(后續補充)。
故障恢復
Zookeeper通過事務日志和數據快照來避免因為服務器故障導致的數據丟失。這一點上所有采用事務機制的存儲實現都一樣,采用WAL+重放機制實現。
- 事務日志是指服務器在更新內存數據前先將事務操作以日志的方式寫入磁盤,Leader和Follower服務器都會記錄事務日志。
- 數據快照是指周期性通過深度遍歷的方式將內存中的樹形結構數據轉入外存快照中。但要注意這種快照是"模糊"的,因為可能在做快照時內存數據發生了變化。但是因為Zookeeper本身對事務操作進行了冪等性保證,故在將快照加載進內存后會通過執行事務日志的方式來講數據恢復到最新狀態。
Zookeeper連接狀態管理
zookeeper的連接狀態機如下:
從上可知,共有5種主要狀態。實際上還有NOT_CONNECTED、CONNECTEDREADONLY、ASSOCIATING、RECONNECTED。
https://curator.apache.org/apidocs/org/apache/curator/framework/state/ConnectionState.html
事務
未完待續。。。
客戶端緩存數據管理
未完待續。。。
權限體系
關於zookeeper的acl認證機制,及相關集成,可參考zookeeper acl認證機制及dubbo、kafka集成、zooviewer/idea zk插件配置。
分析zookeeper的事務日志
可參見http://www.pianshen.com/article/6006190069/。