Zookeeper內幕

這篇博文是關於Zookeeper官網上zookeeperInternals的翻譯(http://zookeeper.apache.org/doc/trunk/zookeeperInternals.html),講述了Zookeeper的內部機制。由於博主的水平有限,如有錯誤和疏漏之處,懇請讀者不吝指正。

名詞解釋

Quorum: 在同一應用中服務器的一組複製(A replicated group of servers in the same application)(這個是在官網上找到的解釋,可能這個解釋太泛了,我更傾向於加上約束條件,也就是在同一應用中服務器的滿足一定條件的一組複製。如Majority Quorum表示超過半數的同一應用中服務器的一組複製)

Epoch: ZooKeeper事務ID(zxid)的高32-bit, Leadership激活將確保每個Leader使用不同的Epoch。(這個單詞很貼切,當新的Leader被激活,代表開啓了新的紀元,所以epoch加1)

Counter: Zookeeper事務ID(zxid)的低32-bit。

介紹

這個文檔包含了Zookeeper內部機制的信息。 在此,將討論下列主題。

原子廣播

Zookeeper的核心是一個保持所有的服務器同步的原子消息傳遞系統

保證(Guarantees),特性(Properties)和定義(Definitions)

ZooKeeper使用的消息傳遞系統提供如下具體的保證:

可靠交付

如果消息m被一個服務器交付,那麼它最終會被所有的服務器交付

全序

如果消息a在消息b之前被一個Server交付,那麼消息a將在消息b之前被所有的Server交付。如果a和b是被交付的消息,要麼a在b之前交付,要麼b在a之前交付。

因果順序 (Causal order)

如果消息b的發送者在交付了消息a 之後才發送消息b, 那麼消息b必然是排在消息a之後。如果一個發送者在發送了消息b之後,才發送消息c, 那麼消息c是排到消息b之後。

ZooKeeper消息傳遞系統需要是高效,穩定以及簡單實現和維護。我們大量地使用了消息傳遞,所以我們需要這個系統能夠處理上千級別的rps. 雖然我們能夠要求至少k+1個正確服務器來發送新消息,但我們必須能夠從如電力中斷等相關故障中恢復過來。當我們實現這個系統時,我們只有很少的時間和工程師資源,所以我們需要一個對於工程師易於理解和實現的協議。我們發現我們的協議滿足所有的這些目標。我們的協議假設我們能夠在服務器之間構建點對點的FIFO通道。當然類似的服務通常假設消息交付會丟失或者消息順序是會打亂的,但是我們通過TCP通信協議來實現FIFO通道是非常實用的。特別地,我們依賴TCP的下列特性:

有序交付(Ordered delivery)

數據按照它被髮送的順序交付,並且消息m只有在之前發送的消息交付之後才能交付。(對此的一個推論是如果消息m丟失,那麼所有在它之後的消息也將會丟失)

關閉後沒有消息傳遞(No message after close)

一旦FIFO通道被關閉,沒有消息能夠通過它來接收。

FLP已經證實了在可能出現故障的情況下,一致性是不能在異步分佈式系統中達成的。爲了確保在故障可能出現的情況下達到一致性,我們使用了超時機制。然而,我們依賴時間來解決存活問題,而不是正確性問題。所以,如果超時機制停止工作(例如時鐘故障),那麼這個消息傳遞系統可能就會中止,但它不會違反它的保證。

當描述ZooKeeper消息傳遞協議,我們將討論數據包,提案和消息:

數據包(Packet)

通過FIFO通道發送的字節序列

提案(Proposal)

一個協定的單元。在ZooKeeper服務器的Quorum之間交換數據包來商定提案。大部分提案包含消息,然而NEW_LEADER提案是不關聯消息的例子。

消息(Message)

字節序列被原子廣播到所有的ZooKeeper服務器。消息被放入提案中,並在它交付前被商定。

如上所述,ZooKeeper保證了消息的全序性,並且它也保證提案的全序性。ZooKeeper通過ZooKeeper事務ID(zxid)來公佈全序。所有的提案在它被提議時將會被打上zxid,並且準確地反映全序。提案將會發送到所有的ZooKeeper服務器。當有一個Quorum確認這個提案,它將會被提交。如果提案中包含一個消息,當這個提案提交時,這個消息也將會被交付。確認意味着這個服務器已經將這個提案記錄到持久存儲中。我們的Quorum有這樣的要求:任何兩個Quorum必須具有至少一個共同的server。我們通過要求所有的Quorum具有(n/2+1)的大小來保證這一點。這裏的n是組成ZooKeeper服務的服務器數量。

zxid由兩部分組成:Epoch和Counter。 在我們實現中,zxid是一個64-bit數字。我們使用高32-bits表示epoch, 和低32-bits表示counter。 因爲zxid由兩部分組成,所以可以將zxid表示成一個數字,也可以表示成一對整形數(epoch, count)。 Epoch數字表示leadership的改變。每次一個新的leader上臺執政,它將有自己的epoch數字。我們使用一個簡單的算法來分配一個唯一的zxid給一個proposal:Leader簡單地增加zxid來爲每一個proposal獲得一個唯一的zxid。Leadership激活將確保只有一個leader使用一個給定的Epoch, 所以我們的這個簡單算法能夠保證了每一個提案將有一個唯一的id。

ZooKeeper消息傳遞包含兩個階段

Leader激活(Leader activation)

在這個階段一個Leader建立系統的正確的狀態,並準備開始做出提案。


活動消息傳遞(Active messaging)

在這個階段,一個Leader接收消息的提議並協調消息的交付。

ZooKeeper是一個整體的協議。我們不會聚焦於單個提案,而是將提案流看成一個整體。o我們嚴格的排序允許我們可以高效地實現這個整體協議,並極大地簡化了我們的協議。Leadership activation體現了這個整體的概念。一個leader成爲active,只有當一個跟隨者Quorum已經和這個leader同步(leader也可以算爲跟隨者,你總是可以投票給自己),他們有相同的狀態。這個狀態由Leader相信的所有已經提交的提案,跟Leader的提案,和NEW_LEADER提案組成。(希望你思考這個問題,leader相信已經提交的提案是否包含了所有真的已經提交的提案?答案是肯定的。下面,我們會搞清楚爲什麼。)

Leader激活(Leader Activation)

Leader activation包含了Leader選舉。目前在ZooKeeper中有兩個Leader選舉算法:LeaderElection和FastLeaderElection(AuthFastLeaderElection是FastLeaderElection的一個變種,使用UDP協議,並且允許服務器執行一個簡單認證來避免IP欺騙)。ZooKeeper消息傳遞並不關心具體的Leader選舉方法,只要滿足下列要求就可以:

  • Leader已經看到所有跟隨者的最高zxid。

  • 已經有一個服務器Quorum提交來跟隨這個leader。

在這兩個要求中只有第一個,在跟隨者中的最高zxid需要總是被保持(必須的)來保證系統正確運行。第二個要求,跟隨者的Quorum僅需要保持高概率就可以。我們將重複檢查第二個要求,所以如果事故在leader選舉的時候或者之後發生,並且Quorum丟失,我們將通過放棄當前的Leader激活,並執行另一個選舉。

在Leader選舉之後,單一的server將被指定爲Leader,並且開始等待跟隨者們來連接。剩餘的服務器將嘗試連接到Leader。Leader將發送跟隨者錯過的所有提案來進行同步。或者如果一個跟隨者錯過了太多的提案,leader將會發送整個狀態快照給這個跟隨者。

這裏有一個極端情況。一個跟隨者有一個還沒有被leader看到提案集合U到達。提案被按序被看到,所以提案集合U會有一個zxid比leader看到的zxid都高。這個跟隨者必須在Leader選舉之後纔到達,否則這個跟隨者將被選爲leader, 因爲它已經看到一個更高的zxid。因爲已經提交的提案必須被服務器Quorum看到,而且選擇Leader的服務器Quorum沒有看到U。這些提案沒有被提交,所以他們會被拋棄。當這個跟隨者連接上Leader, Leader會告訴這個跟隨者丟棄U。

新leader在新協議中開始使用的第一個zxid設爲(e+1,0),其中e是從leader看到的最高zxid中獲得epoch值。在Leader和跟隨者同步之後,它會提出一個NEW_LEADER協議。一旦這個NEW_LEADER協議被提交,這個leader將會激活並且開始接收和發起提案。

這個聽起來比較複雜,但是leader激活過程可以下列基本的操作規則完成:

  • 跟隨者會在和leader同步之後應答NEW_LEADER協議

  • 跟隨者只會應答從單一server來的帶有給定zxid的NEW_LEADER提案

  • 當跟隨者Quorum已經應答了這個NEW_LEADER提案,新leader將會提交這個提案

  • 當NEW_LEADER提案已經提交,跟隨者將會提交它從Leader那接收的任何狀態

  • 新Leader將不會接收新的提案直到NEW_LEADER提案被提交

如果Leader選舉錯誤地終止,我們不會有問題,因爲NEW_LEADER提案將不會提交。這是因爲leader不會有法定人數。當這個發生,leader和任何剩餘的跟隨者將會超時,並回退到Leader選舉。

活動消息傳遞(Active Messaging)

Leader激活完成了所有重擔。一旦Leader被加冕,他會開始廣發提案。只要他是Leader, 沒有其它的Leader能夠出現因爲其他Leader不能獲得跟隨者的Quorum。如果一個新Lead確實出現了,這就意味着之前的Leader丟失了Quorum,並且這個新Leader將會在它的leadership激活過程中清理任何遺留下的混亂。

ZooKeeper消息傳遞操作類似一個經典的二階段提交。

2pc

所有的通信通道都是 FIFO,所以每件事都是按序完成。特別地,下列操作約束會被遵守。

  • Leader使用相同的順序向所有的跟隨者發送提案。而且,這個順序和請求被接收的順序一致。因爲我們使用FIFO通道,這意味着跟隨者也有序地接收提案。

  • 跟隨者們按照消息接收的順序來處理消息。這意味着消息將被按序應答,並且因爲FIFO通道,leader從跟隨者們那按序接收應答信息。這也意味着如果消息m被寫入持久性存儲器,所有在消息m之前提議的消息已經寫入到持久性存儲器。

  • 只要跟隨者的Quorum已經應答了一個消息,Leader將會發起提交消息(COMMIT)給所有的跟隨者。因爲消息被按序應答,所以提交消息將會被Leader將會和跟隨者接收順序一樣的發送COMMIT消息。

  • 提交信息(COMMIT)被按序處理。當一個提案被提交,跟隨者交付提案消息。

總結

到此已經講完了,爲什麼它可以工作?特別地,爲什麼新Leader相信的提案集合中包含了所有已經提交的提案?首先,所有的提案有唯一的zxid,所以不像其他協議,我們不必擔心兩個具有相同的zxid的值被提出;跟隨者們(一個Leader也是一個跟隨者)按序看到和記錄提案;提案是按序提交;一次只會有一個活動的Leader,因爲跟隨者一次只跟隨一個單一的Leader; 一個新Leader已經從先前的epoch看到所有被提交的提案, 因爲它已經從服務器Quorum中看到最高的zxid;在新Leader成爲active之前,將會提交它所看到的任何未提交的之前epoch提案。

比較

難道這個不只是Multi-Paxos嗎?不是,Multi-Paxos需要一些方法來確定僅有單一的協作者。我們不能依靠這些保證。然而我們使用Leader激活來從Leadership改變或者仍然相信自己是active的舊Leader中恢復過來。

難道這個不僅僅是Paxos嗎?你的活動消息傳遞(Active Messaging)階段看起來就像是Paxos的階段2? 事實上,對於我們活動消息傳遞,看起來像不需要處理終止(abort)的2階段提交。活動消息傳遞和這兩個都不同了,就某種意義上它是實現跨提案順序需求。如果我們沒有保持所有數據包的嚴格FIFO順序,它就崩潰了。並且,我們的Leader激活(Leader activation)階段和它們兩個都是不同。特別地,我們epoch的使用允許我們跳過因未提交提案而導致的阻塞,並且不需要擔心一個給定zxid的重複提案。

Quorums

原子廣播和Leader選舉使用Quorum概念來保證系統的一致性視圖。ZooKeeper使用多數派(Majority) Quorum,這意味着每一個投票在這些協議中發生需要一個多數派表決。例如確認一個Leader的提案;Leader只能在從服務器Quorum接收到確認才能提交。

如果我們從多數派使用中提取我們真正需要的特性,我們僅需要保證,當進程組投票驗證一個操作時,它們兩兩至少相交於一個服務器。使用多數派可以保證這一個特性。然而,有其他不同於多數派的構建Quorum的方法。例如,我們可以賦予權重給服務器投票,以及說明這些服務器的投票更加重要。爲了獲得一個Quorum,我們需要獲得足夠的投票,這樣所有投票的權重和就超過所有權重總體和的一半。

層次結構是不同於上述的一種結構。它使用權重並在廣域部署很有用的。使用這種結構,我們把服務器分割成不相交的組,並給進程賦予權重。爲了形成一個Quorum,我們不得不從組的多數派G中獲得足夠的服務器支持,這樣對於在G中每個組g,從g中投票權重和超過在g中權重總和的半數。有趣的是這個結構可以出現更小的Quorum。例如我們有9個服務器,我們將它們分成3組,並且給每個服務器賦予權重1,然後我們就可以形成大小爲4的Quorum。注意在每一個組的多數派中,如果兩個進程子集分別構成服務器多數派,那麼它們必定會有一個非空交集。期待協同定位(co-location)的多數派會有一個高可用的服務器多數派是合理的。

使用ZooKeeper, 可以通過配置服務器來使用多數派Quorum,權重或者組的層次結構。

日誌(Logging)

ZooKeeper使用slf4j作爲日誌的抽象層。log4j版本1.2目前被選爲日誌最終實現。爲了更好的嵌入支持,計劃在將來將選擇最終日誌實現的決定留給最終用戶。因此,我們總是在代碼中使用slf4j api來寫日誌語句,但是目前在運行時配置的是Log4j來記錄日誌。注意slf4j沒有FATAL級別,之前在FATAL級別的消息已經被移到ERROR級別。關於爲ZooKeeper配置log4j信息,請看ZooKeeper Administrator's Guide的日誌章節。

開發者指南

請在代碼中寫日誌語句時遵循slf4j手冊。在創建日誌語句時,也需要閱讀FAQ on performance 。補丁審覈人將按照下列要求評判。

使用正確的級別記錄日誌

在slf4j中有多種日誌級別。選擇正確的一個是很重要的。按照嚴重性從高到低排序:

  1. ERROR級別用來定位可能仍然能夠允許應用程序繼續運行的錯誤事件。

  2. WARN級別用來定位潛在的有危害的情況。

  3. INFO級別用來定位粗粒度地突出應用程序的進程的信息消息。

  4. DEBUG級別用來定位對於調試一個應用程序有幫助的細粒度的信息事件

  5. TRACE級別用來定位比DEBUG級別更加細粒度的信息事件。

ZooKeeper通常在生產模式下將INFO級別或者更高的嚴重性的日誌消息寫入日誌。

標準的slf4j風格使用

靜態消息日誌記錄

LOG.debug("process completed successfully!");

而在需要創建參數化消息時,使用格式化錨

LOG.debug("got {} messages in {} minutes",new Object[]{count,time});    

命名(Naming)

Loggers should be named after the class in which they are used.

日誌記錄器的命名應該按照使用它們的類進行命名。

public class Foo {
    private static final Logger LOG = LoggerFactory.getLogger(Foo.class);
    ....
    public Foo() {
       LOG.info("constructing Foo");

異常處理

try {
  // code
} catch (XYZException e) {
  // do this
  LOG.error("Something bad happened", e);
  // don't do this (generally)
  // LOG.error(e);
  // why? because "don't do" case hides the stack trace
 
  // continue process here as you need... recover or (re)throw
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章