主要是想整理和回顧一下當先Zookeeper的問題,因為昨天在與同事聊天的過程中,有很多人比較極端的認為當下的ZK已經要退出歷史的舞台了,現在已經沒有用武之地了。但是筆者覺得事情不是那么的絕對,或者說就算是ZK要退出歷史舞台,我們也應該是知道為什么,而不是單純的現在很多項目不使用了,我們就一棒子打死,這不是一個技術人應該有的態度和做事的風格。
一、為什么會出現ZK
因為本文是包括一部分回顧的意思在的,所以第一章來回顧為什么會出現ZK就太合適不過了。很多人對ZK的認識就是一個分布式協調中間件,是一個在分布式時代下的產物。提到事務可能我們最熟悉的理論就是ACID,這個理論在關系型數據庫中被使用的比較多。但是在分布式場景中可能CAP和BASE理論是應用的比較廣泛,因為在分布式場景中又多出了很多變量,比如網絡,未知狀態等。
BASE可以說是在CAP之后,結合實際應用場景作出的一種分布式妥協理論,但是基本上是與CAP的思路和范圍約束是保持一致的,下面我們通過一個表格進行對比和回顧一下:
C (Consistency) | A (Avaliablity) | P (Partition tolerance) |
一致性:在分布式環境下多個副本的狀態要保持一個一致的狀態 | 可用性:達到一個可用的狀態,在有限時間內返回需要的結果 | 分區容錯:在出現網絡分區故障的時候,仍然能保證一執行和可用性,除非是整個網絡都出現了故障 |
E (Eventually consistent) | BA (Basically Avaliable) | S (Soft state) |
最終一致性:系統中所有的副本在經過一段時間后會達到一個一執行,不要求實時的 | 基本可用:服務是可用的,但是可能在時間或者返回結果上做一些妥協 | 軟狀態:允許在事務過程中出現中間狀態,但是中間狀態不會影響整體可用性,允許在狀態轉換中出現一定的延遲 |
那么在理論之上就出現了一些分布式事務協議,下面首先來看一下2PC和3PC,其實他們的區別不大,我這里用更加通俗的語言進行說明。其實2PC我們都能理解,但是兩者一出現,其實我們只要通過分析兩者的不同,就能更好的記住和理解了:
2PC :
1.協調者給參與者發送事務內容,等待參與者的預執行結果
2.如果參與者收到了所有參與者反饋的Yes,然后向參與着下發提交的命令,參與者提交各自的任務,並且釋放事務資源
如果參與者收到了一個No ,或者是等待一個超時,那么下發回滾的命令,參與者回滾各自的任務,釋放資源事務資源
存在問題:
1.單點問題:如果在第一階段參與者都進行了事務操作,並且將結果上報給了協調者,但是這個時候協調者掛了,那么所有的參與者會一直阻塞
2.阻塞時間較長:如果出現單點問題,或者是在等待協調者命令的時候出現了網絡問題,那么當前的參與者就會一致阻塞
3.如果在發送提交或者是回滾指令的過程中,其中一個參與者無法接受到指令,那么整個分布式系統中就出現了數據不一致的問題
3PC :只是在2PC的基礎上進行了一些優化,但是並沒有完全解決2PC的問題
1.協調者向各參與者發送事務內容,詢問是否可以執行
2.如果所有參與者都反饋可以執行,向各參與者發送執行的命令
如果有參與者反饋NO,或者是等待超時,向各參與者發送放棄事務執行的指令
如果此時協調者掛了,那么沒有收到明確消息的參與者會自動終止當前事務
3.各參與者執行事務之后,向協調者反饋是否提交
如果所有參與者都反饋Yes,那么協調者向所有的參與者發送提交的指令,參與者收到執行進行提交操作,釋放事務的資源
如果收到一個No或者是超時,那么會向所有參與者下發回滾的指令,釋放資源。
存在問題:
1.在階段3也可能出現協調者掛了的場景,或者是給各參與者下發提交或回滾指令的時候,參與者沒有收到
那么針對這種情況,參與者不會一直阻塞等待,而是會在超時之后,直接提交本地事物
2PC與3PC的對比:其實3PC的優化點主要是為了減少阻塞的時間,特別是在出現單點故障的時候,但是因為在3階段的參與者最終是可以直接提交本地事物的,所以還是會產生數據不一致的問題。
了解過Zookeeper的人應該知道,ZK並沒有直接采用Paxos協議,而是自己開發了ZAB的協議,這里也簡單的聊一下ZAB協議:
ZAB對於每一個寫請求,會在每個Zk節點上保持一個事務日志,同時再加上定期的將內存的鏡像刷新到磁盤來保證數據的一致性和持久性,以及宕機可恢復
二、Zookeeper目前為什么不那么受歡迎了?
ZK是最早被大眾所接受的分布式協調中間件,應該說功能范圍比較的寬泛,所以使用的場景應該也是非常的多。但是慢慢的隨着集群規模的擴大和使用場景的逐漸復雜,發現在很多場景中好像ZK的表現不是那么的出色,這也許就是某些人覺得ZK的時代已經過去了的原因吧,下面從多個角度來聊一下ZK的使用。
當下的微服務是非常的流行,隨之而來的就是服務治理和服務發現的需求,在很多實際場景中ZK都是被用來做為注冊中心使用,進而提供服務發現的能力。首先我們知道ZK是基於CP的,也就是說在出現網絡分區或者是數據不一致的情況下,就會犧牲A。但是如果在服務發現的場景中,比如數據獲取到的數據不是最新的,或者出現了網絡分區,但是可以與同機房的服務實例進行通信,其實這種情況都是可以容忍的啊,不一定要直接犧牲其可用性啊。所以說如果是在服務發現的場景中,可能基於AP的設計會比較好接受,因為畢竟服務的通信不能因為注冊中心的狀態而受到影響,注冊中心不可用,並不代表着服務不可用。而Zookeeper就是基於CP實現的,所以這應該算做它不受歡迎的一個原因吧。
第二個原因應該就是ZK的處理能力了,特別是近些年,流量的增長十分的迅速而且是呈指數級的增長,作為服務發現中心的ZK要面臨的寫請求的壓力非常大,而帶來的表現也不是特別的理想。這個就是與ZK的設計實現有點關系了,下面我們從ZK對消息的處理原理上來說明一下,在什么情況下ZK會表現的不理想。
首先我們知道ZK的一致性協議是在Paxos的基礎上實現的,是一種主備的模式。在任意時刻只能存在一個Leader,所有的寫請求都需要有Leader下發,然后根據Quorum機制完成寫請求。而且ZK還是一種嚴格順序的強一致性,也就是說先提交的Proposal如果沒有被處理完,那么后面提交的就一定要等待。所以說在微服務規模逐漸擴大的場景中,如果只有單一進程來處理整個集群的寫請求,那么就一定會在一定程度上影響寫的性能。
還有一個原因可能就是在服務狀態變化的過程中,ZK會把變化的過程也都進行持久化的記錄,這無形中就增加了很多的操作,但是對於一個服務發現的組件來說,其實並不關心中間的狀態變化,而只需要知道實時的服務列表就可以了,而不需要知道變化的過程和歷史的記錄。所以ZK的這部分能力也是影響寫效率的一個重要因素。
最后就是ZK的健康檢查機制,是利用Zk的Session活性Track機制以及結合Ephemeral ZNode的機制,簡單的說就是將服務的健康檢測綁定在ZK對於session的健康檢測,或者說是綁定在TCP長連接的探活上了,但是探活OK對應的服務就一定是OK的嗎?所以這個算是ZK作為服務健康檢測來說,比較大的一個軟肋,在這點上同為基於CP實現的Consul就顯得比較靈活一些。
三、ZK中leader掛掉之后的疑問?
這個小節是我在看在《從Paxos到Zookeeper 分布式一致性原理與實踐》的時候有的疑惑,后來也去查了一些源碼的資料。
具體問題是這樣,在一個proposal被廣播后,leader會等待接受follower返回的ack消息,如果超過半數的follower返回ack,那么leader就會提交這個proposal,同時本地也會提交。我的疑問就在這里,如果在leader提交當前proposal的時候掛了,那怎么保證呢?
而基於ZAB協議我們知道,ZAB會保證以下兩條原則:
1.已經被處理的proposal不能丟
2.沒有被commit的proposal需要被丟棄
這里我自己回答一下我的疑問:
首先如果是在本地提交之后就立刻掛了,那么follower會收到提交的消息嗎?這個我查了一下zk的源碼,具體的代碼位置是在Leader#tryToCmooit方法中
} else { p.request.logLatency(ServerMetrics.getMetrics().QUORUM_ACK_LATENCY); commit(zxid); inform(p); } zk.commitProcessor.commit(p.request); if (pendingSyncs.containsKey(zxid)) { for (LearnerSyncRequest r : pendingSyncs.remove(zxid)) { sendSync(r); } }
commit方法就是提交的操作,在這里會將提交的消息先發送給所有的follower
/** * Create a commit packet and send it to all the members of the quorum * * @param zxid */ public void commit(long zxid) { synchronized (this) { lastCommitted = zxid; } QuorumPacket qp = new QuorumPacket(Leader.COMMIT, zxid, null, null); sendPacket(qp); ServerMetrics.getMetrics().COMMIT_COUNT.add(1); }
而zk.commitProcessor.commit就是leader自己的提交操作,這樣在一定程度上就能夠保證消息在leader節點上被正確的發送出去。
但是這樣還是不足以保證follower一定能夠執行到這個提交的消息。
到這里還是有一些疑惑了,記得在書中有這樣一段話,也許有很多人和我一樣當時不是很理解吧“
由於所有提案被 COMMIT 之前必須有合法數量的 follower ACK,即必須有合法數量的服務器的事務日志上有該提案的 proposal,因此,zxid最大也就是數據最新的節點保存了所有被 COMMIT 消息的 proposal 狀態。
這段話怎么理解呢?比如一個proposal在leader提交之后,leader掛掉了,但是集群內的其他follower並沒有收到並且執行對應的commit。這之后就要進行leader選舉了,此時在所有的follower上面這個proposal都是沒有被提交的狀態。
按照zk的選舉原則,一定會選出一個zxid最大的follower作為新的leader,那么此時這個follower上面一定有當前的這個proposal,並且在日志中一定會記錄這個proposal對應的ack,此時當前的leader就會在同步階段與其他follower詢問,當前的proposal是否有ack的日志記錄
那么我們知道,之前的舊leader一定是收到了超過半數follower的ack,才會本地提交的。所以這次新的leader發送詢問請求一定會得到超過半數follower的回答,包含了ack的記錄,那么此時就可以確定當前的proposal是可以被提交的。
同樣,如果一個proposal在舊的leader中沒有被commoit,那在新的leader 進行詢問的時候,一定是得到少於半數的ack回答,那么新的leader就會丟棄掉這個proposal。
四、那現在Zookeeper真的不能用了嗎?
首先來說結論:肯定不是的。
通過上一節的說明其實可以知道,ZK只是在服務發現的場景中表現的不是那么的出色,但是這與其設計實現原理有關系,因為它根本就不是為高並發這種場景所准備的,它的可用性要求以及持久化等特性只是在服務發現的場景中被過度的放大了。但是在粒度比較粗的分布式鎖,分布式選舉,TPS不高的分布式數據同步等場景中還是非常適合的。而這些需求在大數據,離線任務的相關業務領域中比較突出,因為在大數據領域比較講究分割數據集,並且大部分時間分任務多進程/線程去並行的處理這些數據集,但是最終還是需要一個組件來協調這些計算結果的,那這就比較適合ZK來實現。所以說在選取技術組件的時候應該更多的考慮到業務的實現場景,應該更加合理的進行SLA的評估之后再做決定。
五、ZK的可應用場景
- 數據發布/訂閱:將配置信息統一配置,各服務通過客戶端watch的方式來獲取配置內容,並跟蹤配置變化
- 命名服務:zk提供的可以在某一個節點下順序的創建子節點,返回帶有順序標識的節點名稱,可以用作全局唯一ID,但是缺點也是不具備業務屬性
- Master的選舉:所有客戶端在某一個節點下面創建臨時節點,只有一個客戶端可以創建成功
- 分布式鎖:X鎖是在某一節點下創建一個臨時節點,S鎖是在某一節點下順序的創建節點,只是S鎖為了避免羊群效應,根據讀和寫的不同邏輯,所以需要watch比自己需要小的那個節點即可
- 分布式隊列:與S鎖的實現機制相同,都是在一個節點下順序的創建臨時節點