翻譯 ZooKeeper: Wait-free coordination for Internet-scale systems

<center>ZooKeeper:因特網規模系統的無等待協調服務</center>

Patrick Hunt and Mahadev Konar Flavio P. Junqueira and Benjamin Reed Yahoo! Grid Yahoo! Research {phunt,mahadev}@yahoo-inc.com {fpj,breed}@yahoo-inc.com

摘要

在本文中,我們描述了用於協調分佈式應用程序進程的服務ZooKeeper。由於ZooKeeper是關鍵基礎結構的一部分,因此ZooKeeper旨在提供一個簡單而高性能的內核,以在客戶端構建更復雜的協調原語。它在複製的集中式服務中合併了來自組消息傳遞,共享寄存器和分佈式鎖定服務的元素。 ZooKeeper公開的接口具有共享寄存器的無等待特性,使用類似於分佈式文件系統緩存失效的事件驅動機制來提供簡單而強大的協調服務。

ZooKeeper接口可以實現高性能服務。除了無等待屬性之外,ZooKeeper改變每個客戶端請求執行FIFO和所有ZooKeeper狀態改變線性化。這些設計決策可以實現滿足本地服務器讀取請求的高性能處理管道。我們表示ZooKeeper每秒可以處理成千上萬的事務,從2:1到100:1讀寫比例的工作負載。這種性能允許客戶端應用程序廣泛地使用ZooKeeper。

1 Introduction

大型分佈式應用程序需要不同形式的協調。 配置是最基本的協調形式之一。 在最簡單的形式中,配置只是系統過程的操作參數列表,而更復雜的系統具有動態配置參數。 組成員身份和領導者選舉在分佈式系統中也很常見:通常,進程需要知道哪些其他進程仍在運行以及這些進程負責什麼。 鎖構成強大的協調原語,實現對關鍵資源的互斥訪問。

協調的一種方法是爲每個不同的協調需求開發服務。 例如,Amazon Simple Queue Service特別關注排隊。 還專門針對leader選舉和配置開發了其他的服務。 實現功能更強大的原語的服務可用於實現功能更弱的原語。 例如,Chubby是具有強同步保證的鎖服務。 然後可以使用鎖來實施leader選舉,集羣管理等。

在設計協調服務時,我們不再在服務器端實現特定的原語,而是選擇公開一個API,使應用程序開發人員可以實現自己的原語。 這種選擇的結果是實現了一個協調內核,該協調內核無需更改服務核心,就可以實現新的原語。 這種方法滿足了應用程序對多種形式的協調的需求,而不是將開發人員約束到一組固定的原語上。

在設計ZooKeeper的API時,我們不再使用阻塞原語,比如鎖。除了其他問題外,阻塞協調服務的原語會導致客戶端變慢或故障,從而對速度更快的客戶端的性能產生負面影響。如果處理請求依賴於其他客戶端的響應和故障檢測,則服務本身的實現將變得更加複雜。因此,我們Zookeeper實現了一個API,可以像文件系統一樣操作按層次結構組織的簡單的免等待數據對象。實際上,Zookeeper API類似於任何其他文件系統,如果只看API簽名,ZooKeeper看起來就像是沒有鎖方法、開啓、關閉的Chubby。但是,實現無等待的數據對象與基於阻塞原語(如鎖)的系統有很大的區別。

雖然無等待特性對性能和容錯很重要,但對做到協調來說還不夠。我們還必須保證操作有序執行。特別是,我們發現,保證所有操作的客戶端FIFO順序和線性化的寫操作,就能有效地實現服務,並且足以實現應用程序感興趣的協調原語。事實上,我們可以用我們的API實現任意數量進程的一致性,並且根據Herlihy(人名)的層次結構,ZooKeeper實現了一個通用對象。

ZooKeeper服務由一組使用複製以實現高可用性和高性能的服務器組成。它的高性能使包含大量進程的應用程序可以使用這種協調內核來管理協調的各個方面。我們能夠使用簡單的管道體系結構實現ZooKeeper,使我們可以處理數百或數千個請求的同時仍保證低延遲。這樣的管道自然支持按FIFO順序從單個客戶端執行操作。保證FIFO客戶端順序使客戶端可以異步提交操作。 使用異步操作,客戶端一次可以執行多個未完成的操作。 例如,當新客戶端成爲leader並且必須操縱元數據並相應地對其進行更新時,就需要使用這個特性。 如果不能進行多個未完成的操作,則初始化時間可以是幾秒左右,而不是亞秒級。

爲了保證更新操作滿足線性化,我們實現了一個基於leader的原子廣播協議,稱爲Zab(Zookeeper Atomic Broadcast) 。但是,ZooKeeper應用程序的典型工作負載是讀操作,因此需要擴展讀吞吐量。在ZooKeeper中,服務器在本地處理讀操作,我們不使用Zab對它們進行完全排序。

在客戶端緩存數據是提高讀取性能的一項重要技術。例如,對於一個進程來說,緩存當前leader的標識符是非常有用的,而不是每次需要了解ZooKeeper時都去獲取。ZooKeeper使用一種監視機制使客戶端可以緩存數據,而無需直接管理客戶端緩存。 使用此機制,客戶端可以監視對給定數據對象的更新,並在更新時接收通知。 Chubby直接管理客戶端緩存。 它會阻止更新,以使所有緩存更改數據的客戶端的緩存無效。 在這種設計下,如果這些客戶端中的任何一個運行緩慢或出現故障,更新都會延遲。 Chubby使用租約來防止有故障的客戶端無限期地阻塞系統。 但是,租約只能限制運行緩慢或故障客戶端的影響,而ZooKeeper watches完全可以避免此問題。

在本文中,我們討論了ZooKeeper的設計和實現。 使用ZooKeeper,即使只有寫入是可線性化的,我們也可以實現應用程序所需的所有協調原語。 爲了驗證我們的態度,我們展示瞭如何使用ZooKeeper實現一些協調原語。

綜上所述,本文的主要貢獻有:

協調內核(Coordination kernel):我們提出了一種無等待的協調服務,該服務具有寬鬆的一致性保證,可用於分佈式系統。 特別是,我們描述了協調內核的設計和實現,並且我們已在許多關鍵應用程序中使用他來實現了各種協調技術。

協調方法(Coordination recipes):我們展示了ZooKeeper如何用於構建更高級別的協調原語,甚至包括阻塞和強一致性原語,這些原語經常在分佈式應用程序中使用。

**協調經驗(Experience with Coordination):**我們分享了一些使用ZooKeeper並評估其性能的方式。

2 The ZooKeeper service

客戶端使用ZooKeeper客戶端庫通過clientAPI向ZooKeeper提交請求。 除了通過clientAPI公開ZooKeeper服務接口外,客戶端庫還管理客戶端與ZooKeeper服務器之間的網絡連接。

在本節中,我們首先提供theZooKeeper服務的高級視圖。 然後,我們討論客戶端用來與ZooKeeper交互的API。

術語。在本文中,我們使用client來表示ZooKeeper服務的用戶,使用server來表示提供ZooKeeper服務的進程,使用znode來表示ZooKeeper數據中的內存數據節點,它被組織在稱爲數據樹的分層命名空間中。我們還使用術語update和write來指代修改數據樹狀態的任何操作。客戶端在連接到ZooKeeper時建立一個會話,並獲得一個會話句柄,通過它發出請求。

2.1 Service overview

ZooKeeper向其客戶提供了根據分層命名空間組織的一組數據節點(znode)的抽象。這個層次結構中的znode是客戶端通過ZooKeeper API操作的數據對象。 分層命名空間通常在文件系統中使用。 這是組織數據對象的一種理想方式,因爲用戶已經習慣了這種抽象,並且可以更好地組織應用程序元數據。 要引用給定的znode,我們使用標準的UNIX方式表示文件系統路徑。 例如,我們使用*/A/B/C*表示到znode C的路徑,其中C的父節點是B,而B的父節點是A。 所有znode都存儲數據,並且除臨時znode之外的所有znode都可以有子節點。

<center>圖1:ZooKeeper分層名稱空間的圖示。</center>

客戶端可以創建兩種類型的znode: 常規(Regular):客戶端顯式的創建和刪除常規znode; 臨時性(Ephemeral):客戶端創建這樣的znode後,要麼顯式地刪除它們,要麼讓系統在(創建這個節點的)會話終止時自動刪除它們(故意或由於失敗)。

此外,在創建新znode時,客戶端可以設置一個順序標誌。使用順序標誌集創建的節點的名稱後面附加了一個單調遞增的計數器的值。如果n是新的znode, p是父znode,那麼n的序列值永遠不會小於在p下創建的任何其他順序znode的名稱中的值。

ZooKeeper實現了監視器(Watcher),允許客戶端不需要輪詢能夠及時接收更改通知。當客戶端發送帶有監視標誌的讀取操作時,該操作會正常完成,並且服務器保證在返回的信息已更改時通知客戶端。監視器(Watcher)是一次性觸發的;一旦觸發或會話關閉,監視器就會被移除。監視器只會通知更改的發生,不會提供更改的內容。例如,如果客戶端在兩次修改"/foo"之前發出了一個getData("/foo", true),客戶端會受到一個觀察事件,得知數據已更改。會話事件,如連接丟失事件,也會被髮送到監視回調,以便客戶端知道監視事件可能被延遲。

數據模型(Data model)。ZooKeeper的數據模型本質上是一個文件系統,它有一個簡化的API,只能讀寫完整的數據,或者是一個鍵有層次結構的鍵/值表,它有層次鍵。層次命名空間對於爲不同應用程序的命名空間分配子樹和設置這些子樹的訪問權限非常有用。我們還利用客戶端目錄的概念來構建更高級別的原語,我們將在2.4節中看到。

與文件系統中的文件不同,znode不是爲常規數據存儲設計的。 相反,znode映射到客戶端應用程序的抽象,通常對應於用於協調目的的元數據。 爲了說明這一點,在圖1中,我們有兩個子樹,一個用於應用程序1 (/app1) ,另一個用於應用程序2(/app2)。 應用程序1的子樹實現了一個簡單的組成員身份協議:每個客戶端進程p<sub>i</sub>在/ app1下創建一個znode p_i,只要該進程正在運行,它就一直存在。

雖然znodes並不是爲通用數據存儲而設計的,但是ZooKeeper確實允許客戶端存儲一些信息,用於分佈式計算中的元數據或配置。例如,在一個基於leader的應用程序中,對於一個剛剛開始瞭解當前哪個服務器是領導的應用程序服務器來說,這是非常有用的。爲了實現這個目標,我們可以讓當前的領導者在znode空間中一個已知的位置寫入這個信息。znode還具有與時間戳和版本計數器相關聯的元數據,這允許客戶端跟蹤對znode的更改,並根據znode的版本執行條件更新。

會話(Sessions)。客戶端連接到ZooKeeper會啓動一個會話。會話有一個關聯的超時時間。如果在超過該超時時間內,ZooKeeper沒有受到會話的任何消息,則會認爲客戶端故障。當客戶端顯式關閉會話句柄或ZooKeeper檢測到客戶端出錯時,會話結束。在一個會話中,客戶端觀察一系列反映其操作執行情況的狀態變化。會話使客戶端能夠在ZooKeeper集合中透明地從一個服務器移動到另一個服務器,從而在整個ZooKeeper服務器之間持續存活。

2.2 Client API

我們在下面展示ZooKeeper API的相關子集,並討論每個請求的語義。

create(path, data, flags): 用路徑名path創建一個znode,在其中存儲data [],並返回新znode的名稱。flags允許客戶端選擇znode的類型:regular(常規), ephemeral(短暫)和序列類型;

delete(path, version):刪除znode如果它的版本跟傳入version一致;

exists(path, watch):如果路徑名稱爲path的znode存在,則返回true,否則返回false。 watch使客戶端可以在znode上設置監視器;

getData(path, watch):返回與znode關聯的數據和元數據,例如版本信息。watch的工作方式與exists()一樣,不同之處在於,如果znode不存在,ZooKeeper不會設置監視器。

setData(path, data, version):如果version是znode當前版本,則把data寫入路徑爲path的znode。

getChildren(path, watch):返回路徑爲path的znode的子集合。

sync(path):等待操作開始時所有未決的更新傳播到客戶端連接到的服務器。path目前被忽略。

所有方法都有一個同步和一個異步版本,可以通過API使用。當應用程序需要執行單個ZooKeeper操作且沒有要執行的併發任務時,它會使用同步API,因此它會執行必要的ZooKeeper調用並阻塞。然而,異步API使應用程序能夠同時執行多個未完成的ZooKeeper操作和其他任務。ZooKeeper客戶端保證爲每個操作依次調用相應的回調。

請注意,ZooKeeper不使用句柄訪問znodes。相反,每個請求都包含正在操作的znode的完整路徑。這種選擇不僅簡化了API(沒有open()或close()方法),而且還消除了服務器需要維護的額外狀態。

每個更新方法都採用一個預期的版本號,從而支持條件更新的實現。如果znode的實際版本號與預期的版本號不匹配,則更新失敗,並出現意外的版本錯誤。如果􀀀1版本號,它不執行版本檢查。

每個更新方法都採用一個預期的版本號,從而可以實現條件更新。如果znode的實際版本號與預期的版本號不匹配,則更新失敗,並拋出意外的版本錯誤。如果版本號爲-1,則不執行版本檢查。

2.3 ZooKeeper guarantees

ZooKeeper具有兩個基本的順序保證:

Linearizable writes:所有更新ZooKeeper狀態的請求都是可線性化的,並且遵循優先級;

FIFO client order:來自給定客戶端的所有請求均按照客戶端發送的順序執行。

注意,我們對線性化的定義與Herlihy最初提出的定義不同,我們稱之爲A-linearizability (異步線性化)。在Herlihy最初對線性化的定義中,客戶端一次只能有一個未完成的的操作(客戶端是一個線程)。在我們的操作系統中,我們允許一個客戶端有多個未完成的操作,因此我們可以選擇對同一個客戶端未完成的操作保證沒有特定的順序,或者保證FIFO順序。我們選擇後者作爲我們的特性。重要的是要觀察到所有對linearizable 對象成立的結果對A-linearizable對象也成立因爲一個滿足A-linearizable的系統也滿足linearizable 。因爲只有更新請求是可線性化的,所以ZooKeeper在每個副本上本地處理讀請求。這使得服務可以隨着服務器的添加而線性擴展。

若要查看這兩個保證如何相互作用,請考慮以下情形。包含多個進程的系統選舉領導者指揮工作進程。當新領導負責系統時,必須更改大量配置參數,並在完成後通知其他進程。 然後,我們有兩個重要要求:

  • 當新領導開始進行更改時,我們不希望其他進程使用正在更改的配置;
  • 如果新的leader在配置完全更新之前死亡,我們不希望進程使用這個部分配置。

注意,分佈式鎖(例如由Chubby提供的鎖)有助於滿足第一個需求,但不足以滿足第二個需求。使用ZooKeeper,新leader可以指定一條路徑作爲ready znode;其他進程僅在znode存在時才使用配置。新的leader通過刪除ready、更新各種配置znodes和創建ready來更改配置。所有這些更改都可以被流水線化異步發出,以快速更新配置狀態。儘管更改操作的延遲大約爲2毫秒,但是如果請求一個接一個地發出,則更新5000個不同znode的新leader將花費10秒;通過異步發出請求,請求將花費不到一秒的時間。由於順序保證,如果一個進程看到ready的znode,它還必須看到新leader所做的所有配置更改。如果新的leader在就緒的znode創建之前死亡,那麼其他進程就知道配置還沒有完成,所以不會使用它。

上面的方案仍然有一個問題:如果進程在新領導開始進行更改之前看到就緒,然後在更改過程中開始讀取配置,那麼會發生什麼情況?這個問題通過通知的排序保證來解決:如果客戶端正在監視更改,那麼客戶端將在更改後看到系統的新狀態之前看到通知事件。因此,如果讀取就緒的znode請求的進程在該znode發生更改時得到通知,那麼在它可以讀取任何新配置之前,它將看到通知客戶端更改的通知。

當客戶端除了ZooKeeper之外還有自己的通信渠道時,就會出現另一個問題。例如,考慮兩個客戶端A和B,它們在ZooKeeper中具有共享配置,並且通過共享通信通道進行通信。如果A更改了ZooKeeper中的共享配置,並通過共享通信通道將更改告知B,則B將在重新讀取配置時看到更改。如果B的ZooKeeper副本稍微落後於A,則可能看不到新配置。使用上述保證,B可以在重新讀取配置之前發出寫操作,從而確保看到最新的信息。爲了更有效地處理這種情況,ZooKeeper提供了同步請求:當後面跟着一個讀時,就構成了一個慢讀。同步會導致服務器在處理讀之前應用所有掛起的寫請求,而不會產生完全寫的開銷。這個原語在概念上類似於ISIS的flush本原語。

ZooKeeper還具有以下兩個活動性和持久性保證:如果大部分的ZooKeeper服務器都是活動的,並且可以通信,則服務可用;如果ZooKeeper服務成功地響應了一個更改請求,只要最終能夠達到法定服務器數量,這個更改就會在任何數量的故障中持續存在。

2.4 Examples of primitives

在本節中,我們將展示如何使用ZooKeeper API來實現更強大的原語。ZooKeeper服務對這些功能更強大的原語一無所知,因爲它們完全是在客戶端使用ZooKeeper客戶端API實現的。一些常見的原語(如組成員關係和配置管理)也是wait-free(無等待)的。對其他的,比如rendezvous(集合點),客戶端需要等待一個事件。儘管ZooKeeper是無等待的,但我們可以使用ZooKeeper實現高效的阻塞原語。ZooKeeper的順序保證允許對系統狀態進行有效的推理,而watches(監視器)則允許高效的等待。

Configuration Management(配置管理) ZooKeeper可用於在分佈式應用程序中實現動態配置。它以最簡單的形式存儲在znode zc中。進程以zc的完整路徑名啓動。活動進程通過將watch標誌設置爲true來讀取zc並獲取其配置。如果zc中的配置被更新,進程將收到通知並讀取新的配置,(必須)再次將watch標誌設置爲true。

注意,在這個方案中,就像在其他大多數使用watch的方案中一樣,watch用於確保進程擁有最新的信息。例如,如果監視zc的進程收到zc變更的通知,而在它發出對zc的讀取之前,zc還有三次變更,那麼該進程不會再收到這個三個(變更的)通知事件。這不會影響進程的行爲,因爲這三個事件只會通知進程它已經知道的事情:他擁有的zc的信息過時了。

Rendezvous 有時候,在分佈式系統中,並不總是預先清楚最終的系統配置會是什麼樣的。例如,客戶可能想要啓動一個主進程和幾個工作進程,但進程是通過一個調度程序開始,所以客戶沒法提前知道它可以給到連接的主進程的信息,比如地址和端口。我們用ZooKeeper使用一個rendezvous znode zr來處理這個場景,zr是客戶端創建的一個節點。客戶端將zr的完整路徑名作爲啓動參數傳遞給主進程和工作進程的。當主程序啓動時,它把正在使用的地址和端口的信息設置到zr。當工作進程開始啓動時,他們將watch設置爲true來監聽zr。如果zr還沒有被設置,worker將等待zr被修改時的通知。如果zr是臨時節點,主進程和工作進程可以監視zr是否被刪除,並在客戶端結束時清理自己。

Group Membership 我們利用臨時節點來實現組成員關係管理。 確切地說,我們利用了一個事實,即我們可以看到創建臨時節點的會話的狀態。我們首先指定一個znodezg來表示一個組。當該組的進程成員啓動時,它將在zg下創建臨時子節點znode。如果每個進程都有唯一的名稱或標識符,那麼該名稱將用作子進程znode的名稱;否則,進程將創建帶有順序標誌的znode,以獲得唯一的名稱分配。進程可以將進程信息放入子znode的數據中,例如進程使用的地址和端口。

zg下創建子節點znode之後,進程將正常啓動。它不需要做任何其他的事情。如果該進程失效或結束,在zg下表示該進程的znode將自動刪除。

進程可以通過列出zg的子進程來獲得組信息。如果進程希望監視組成員關係的變化,則可以將watch標誌設置爲true,並在收到變更通知時刷新組信息(始終將watch標誌設置爲true)。

**Simple Locks ** 雖然ZooKeeper不是鎖服務,但它可以用來實現鎖。使用ZooKeeper的應用程序通常使用根據需要定製的同步原語,就像上面說的那樣。這裏,我們將展示如何用ZooKeeper實現鎖,以表明它可以實現各種各樣的通用同步原語。

最簡單的鎖實現使用“鎖文件”。該鎖由znode表示。爲了獲取鎖,客戶端嘗試創建帶有臨時標誌的指定znode。如果創建成功,客戶端將持有鎖。否則,客戶端可以讀取znode,並設置watch標誌,以便在當前leader死亡時收到通知。客戶端在鎖失效或顯式刪除znode時釋放鎖。其他等待鎖的客戶端在觀察到znode被刪除後再次嘗試獲取鎖。

雖然這個簡單的鎖定協議可以使用,但它確實存在一些問題。首先,它受到羊羣效應的影響。如果有很多客戶端在等待獲取鎖,那麼當鎖被釋放時,即使只有一個客戶端可以獲取鎖,它們也會一起爭奪鎖。其次,它只實現了獨佔鎖。下面兩個原語顯示瞭如何同時解決這兩個問題。

**Simple Locks without Herd Effect **我們定義了一個鎖znode l來實現這個鎖。直觀地,我們將請求鎖的所有客戶端排列起來,每個客戶端按照請求到達的順序獲得鎖。因此,希望獲得鎖的客戶可以執行以下操作:

Lock 1 n = create(l + “/lock-”, EPHEMERAL|SEQUENTIAL) 2 C = getChildren(l, false) 3 if n is lowest z node in C, exit 4 p = znode in C ordered just before n 5 if exists(p, true) wait for watch event 6 goto 2 Unlock 1 delete(n)

在鎖的第1行中使用SEQUENTIAL標誌,讓所有客戶端按照順序嘗試獲取鎖。在第3行如果發現客戶端的znode的序列號是最低的,則客戶端持有鎖。否則,客戶端將等待擁有鎖的znode刪除,或者在客戶端znode之前獲取鎖。通過只監視客戶端znode之前的znode,我們避免了羊羣效應,只在鎖被釋放或鎖請求被放棄時喚醒一個進程。一旦客戶端監視的znode消失,客戶端必須檢查它現在是否持有鎖。(之前的鎖請求可能已經被放棄,有一個序列號較低的znode仍在等待或持有鎖。)

釋放鎖就像刪除表示鎖請求的znoden一樣簡單。通過在創建時使用EPHEMERAL標誌,崩潰的進程將自動清除所有鎖請求或釋放它們可能擁有的任何鎖。

綜上所述,該鎖方案有以下優點:

  1. 刪除一個znode只會導致一個客戶端喚醒,因爲每個znode都被另一個客戶端監視,所以我們沒有羊羣效應;
  2. 沒有輪詢或超時;
  3. 由於我們實現鎖定的方式,通過瀏覽ZooKeeper數據,我們可以看到鎖爭用的數量、中斷鎖和調試鎖問題。

Read/Write Locks 爲了實現讀/寫鎖,我們稍微改變了鎖過程,將讀鎖和寫鎖過程分開。解鎖過程與全局鎖的情況相同。

Write Lock 1 n = create(l + “/write-”, EPHEMERAL|SEQUENTIAL) 2 C = getChildren(l, false) 3 if n is lowest znode in C, exit 4 p = znode in C ordered just before n 5 if exists(p, true) wait for event 6 goto 2 Read Lock 1 n = create(l + “/read-”, EPHEMERAL|SEQUENTIAL) 2 C = getChildren(l, false) 3 if no write znodes lower than n in C, exit 4 p = write znode in C ordered just before n 5 if exists(p, true) wait for event 6 goto 3

這個鎖過程與之前的鎖略有不同。寫鎖只在命名上有所不同。因爲讀鎖可以共享,所以第3行和第4行略有不同,因爲只有以前的寫鎖znodes纔會阻止客戶端獲得讀鎖。當多個客戶端在等待一個讀鎖,並在刪除序列號較低的“write-”znode時得到通知時,似乎出現了“羊羣效應”;事實上,這是我們所期望的行爲,所有那些讀客戶端都應該被釋放,因爲它們現在可以擁有鎖。

Double Barrier(雙重屏障) 雙重屏障使客戶端能夠同步計算的開始和結束。當barrier閾值定義的足夠多的進程加入barrier時,進程開始它們的計算並在完成之後離開barrier。我們用znode在ZooKeeper中表示一個barrier,這個障礙被稱爲b。每個進程p在進入時都通過創建一個znode作爲b的子進程來註冊b -,當它準備離開時取消註冊刪除子進程。當b的子節點znodes的數量超過barrier閾值時,進程可以進入barrier。當所有進程都刪除了它們的子進程時,進程可以離開barrier。我們使用watch有效地等待進入和退出條件得到滿足。如果要離開,進程會觀察是否存在b的就緒子進程,該子節點將由導致子節點數超過障礙閾值的進程創建。 如果要離開,進程會觀察某個特定的子節點是否消失,並且僅在刪除znode後檢查退出狀態。

3 ZooKeeper Applications

我們現在描述一些使用ZooKeeper的應用程序,並簡要解釋它們是如何使用它的。我們用粗體顯示每個示例的原語。

The Fetching Service(獲取服務) 爬蟲是搜索引擎的一個重要部分,Yahoo!抓取的Web文檔有數十億個。抓取服務(FS)是Yahoo!爬蟲的一部分,且仍在運行中。本質上,它有由頁面獲取進程控制的的主進程。主服務器向獲取進程提供配置,獲取進程回寫它們的狀態和運行狀況。在FS中使用ZooKeeper的主要優點是:可以從主服務器的故障中恢復,保證在出現故障時仍然可用,並將客戶端與服務器解耦,允許它們僅通過從ZooKeeper讀取狀態就可以將請求指向健康的服務器。因此,FS主要使用ZooKeeper來管理配置元數據,儘管它也使用ZooKeeper來選擇master (leader election)。

<center>圖2:帶有抓取服務的ZK服務器的工作負載。每個點代表一個一秒的樣本。</center>

圖2顯示了FS在三天內使用的ZooKeeper服務器的讀寫流量。爲了生成這個圖,我們計算週期內每秒的操作數,每個點對應於這一秒內的操作數。我們觀察到讀流量比寫流量高得多。在速率高於每秒1000次操作的時間裏,讀寫操作比例在10:1和100:1之間變化。這個工作負載中的讀操作是getData()、getChildren()和exists(),按流行度遞增。

Katta Katta是使用ZooKeeper進行協調的分佈式索引器,是一個非yahoo !應用程序。Katta使用分片劃分索引工作。主服務器將分片分配給從服務器並跟蹤進度。從服務器可能會失敗,所以主服務器必須隨着從服務器的來來去去重新分配負載。主服務器也可能出現故障,因此其他服務器必須準備好在出現故障時接管。Katta使用ZooKeeper跟蹤從服務器和主服務器(組成員關係)的狀態,並處理主服務器故障轉移(leader選舉)。Katta還使用ZooKeeper來跟蹤和傳播分配給從服務器的分片(配置管理)。

Yahoo! Message Broker Yahoo! Message Broker (YMB)是一個分佈式發佈-訂閱系統。該系統管理數以千計的topic,客戶端可以向這些topic發佈消息,也可以從這些topic接收消息。topic分佈在一組服務器中,以提供可伸縮性。使用主備份模式複製每個topic,該模式確保將消息複製到兩臺機器,以確保消息的可靠傳遞。組成YMB的服務器使用無共享的分佈式架構,這使得協調對於正確的操作至關重要。YMB使用ZooKeeper管理topic的分配(配置元數據),處理系統中機器的故障(故障檢測和組成員關係),以及系統運行。

<center>圖3:Yahoo! Message Broker(YMB)在ZooKeeper的結構佈局</center>

圖3顯示了YMB的znode數據佈局的一部分。每個代理域都有一個名爲nodes的znode,它對組成YMB服務的每個活動服務器都有一個臨時的znode。每個YMB服務器在nodes下創建一個帶有負載和狀態信息的臨時znode,並通過ZooKeeper提供組成員和狀態信息。像shutdown和migration prohibition這樣的節點,由構成該服務的所有服務器監控,並允許對YMB進行集中控制。對於YMB管理的每個topic,topic目錄都有一個子znode。這些topic znodes具有子znodes,它們記錄着每個topic的主服務器和備份服務器以及該topic的訂閱者。主服務器和備份服務器znodes不僅允許服務器發現負責某個topic的服務器,還可以管理leader election和服務器崩潰。

4 ZooKeeper Implementation

ZooKeeper通過在組成服務的每個服務器上覆制ZooKeeper數據來提供高可用性。我們假定服務器是因爲崩潰而發生故障的,這種服務器故障可能稍後就會恢復。圖4顯示了ZooKeeper服務的高層組件。在接收到請求後,服務器爲執行請求做準備(請求處理器)。如果這樣的請求需要在服務器(寫請求)之間進行協調,那就使用一致性協議(原子廣播的一種實現),最後,服務器將更改提交給ZooKeeper數據庫,並將其完全複製到全體服務器中。對於讀取請求,服務器只需讀取本地數據庫的狀態並生成對請求的響應。

<center>圖4:ZooKeeper服務組件</center>

複製的數據庫是包含整個數據樹的內存數據庫。默認情況下,樹中的每個znode最多存儲1MB的數據,但是這個最大值是可以在特定情況下更改的可配置參數。爲了提高可恢復性,我們有效地將記錄更新到磁盤,並在將他們應用到內存中的數據庫之前強制寫入磁盤介質中。實際上,就像Chubby,我們保留已提交操作的重放日誌(在本例中爲預寫日誌),並定期生成內存數據庫的快照。

每個ZooKeeper服務器都向客戶端提供服務。客戶端只需要連接到一個服務器來提交它的請求。就像前面說的,讀取請求來自每個服務器數據庫的本地副本。更改服務狀態的請求(寫請求)由一致性協議處理。

作爲一致性協議的一部分,寫請求被轉發到稱爲leader的單個服務器。其他的ZooKeeper服務器稱爲followers,接收來自leader的包含狀態改變的proposals(提案)信息,並就狀態改變達成一致。

4.1 Request Processor

由於消息傳遞層是原子的,所以我們保證本地副本永遠不會偏離,儘管在任何時間點,某些服務器可能應用了比其他服務器更多的事務。與客戶端發送的請求不同,事務是冪等的。當leader接收到一個寫請求時,它會計算應用寫請求時系統的狀態,並將其轉換爲捕獲這個新狀態的事務。因爲可能有未完成的事務尚未應用到數據庫,所以必須計算將來的狀態。例如,如果客戶端執行附帶條件的setData,並且請求中的版本號與正在更新的znode的未來版本號相匹配,則服務將生成一個包含新數據、新版本號和更新的時間戳的setDataTXN。如果出現錯誤,如版本號不匹配或要更新的znode不存在,則生成errorTXN。其他的動物管理員服務器稱爲追隨者,接收來自領導者的包含狀態改變的信息建議,並就狀態改變達成一致。

4.2 Atomic Broadcast

所有更新ZooKeeper狀態的請求都會轉發給leader。leader執行請求,並通過原子廣播協議Zab將狀態更改廣播到ZooKeeper。接收客戶端請求的服務器在提交相應狀態更改時會響應給客戶端。Zab默認使用簡單的quorums原則來決定一個提案,所以Zab和ZooKeeper只有在大多數服務器都正確的情況下才能工作(即當使用2f + 1服務器時,我們可以容忍f個服務器故障)。

爲了實現高吞吐量,ZooKeeper嘗試保持請求處理管道滿載。在處理管道的不同部分可能有數千個請求。由於狀態更改依賴於先前狀態更改的應用程序,因此Zab提供了比常規原子廣播更強的順序保證。更具體地說,Zab保證一個leader廣播的變更按照發送的順序傳遞,並且在之前leader廣播自己的變更之前,所有來自前任領導的變更都會傳遞給一個已經建立的領導(leader變更後,之前的消息順序不變)。

有一些實現細節可以簡化我們的實現併爲我們提供出色的性能。我們使用TCP進行傳輸,因此消息順序由網絡維護,這允許我們簡化實現。我們使用Zab選擇的leader作爲ZooKeeper leader,由這個進程創建和提出事務。我們使用日誌來跟蹤提案,將其作爲內存數據庫的預寫日誌,這樣就不必將消息兩次寫入磁盤。

在正常操作期間,Zab會按順序準確地一次交付所有消息,但由於Zab不會持久地記錄所交付的每條消息的id,所以Zab可能會在恢復期間重新交付一條消息。因爲我們使用冪等事務,所以只要按順序交付,多次交付是可以接受的。事實上,ZooKeeper要求Zab重新傳遞至少所有在最後一個快照開始後傳遞的消息

4.3 Replicated Database

每個副本在內存中都有一個ZooKeeper狀態的拷貝。當ZooKeeper服務器從崩潰中恢復時,它需要恢復這個內部狀態。在服務器運行一段時間後,重放所有已發送的消息以恢復狀態將花費非常長的時間,因此ZooKeeper使用定期快照,並且只需要在快照開始後重新發送消息。我們稱ZooKeeper快照爲模糊快照,因爲我們獲取快照時不鎖定ZooKeeper狀態;相反,我們對樹進行深度優先掃描,原子地讀取每個znode的數據和元數據,並將它們寫入磁盤。由於產生的模糊快照可能應用了在生成快照期間交付的狀態更改的某個子集,因此結果可能在任何時間點上都不對應於ZooKeeper的狀態。然而,由於狀態更改是冪等的,因此只要按順序應用狀態更改,我們就可以應用它們兩次。

例如,假設在一個ZooKeeper數據樹中,兩個節點/foo和/goo的值分別爲f1和g1,當模糊快照開始時,它們的版本都是1,接下來的狀態更改流到達時,其形式爲(transactionType, path, value, new-version): (SetDataTXN, /foo, f2, 2) (SetDataTXN, /goo, g2, 2) (SetDataTXN, /foo, f3, 3)

在處理了這些狀態更改之後,/foo和/goo的版本3和版本2的值分別爲f3和g2。然而,模糊快照可能已經記錄了/foo和/goo在版本3和1中分別擁有f3和g1的值,這不是ZooKeeper數據樹的有效狀態。如果服務器崩潰並使用此快照恢復,並且Zab重新提交狀態更改,則結果狀態對應於崩潰前的服務狀態。

4.4 Client-Server Interactions

當服務器處理寫請求時,它還會發送和清除與該更新相對應的任何watch的通知。服務器按順序處理寫操作,而不同時處理其他寫操作或讀操作。這確保了通知的嚴格連續性。注意,服務器在本地處理通知。只有客戶端連接到的服務器才能跟蹤並觸發該客戶端的通知。

讀取請求在每個服務器的本地處理。每個讀取請求都被處理並使用zxid標記,該zxid對應於服務器看到的最後一個事務。這個zxid定義了讀請求相對於寫請求的部分順序。通過本地處理讀取,我們獲得了優異的讀取性能,因爲它只是本地服務器上的內存操作,不需要運行磁盤活動或一致協議。這種設計選擇是在以讀爲主的工作負載下實現卓越性能目標的關鍵。

使用快速讀取的一個缺點是不能保證讀取操作的優先順序。也就是說,讀操作可能返回一個過時的值,即使已經提交了對同一znode的最新更新。並不是所有的應用程序都需要優先順序,但是對於需要優先順序的應用程序,我們實現了同步。該原語異步執行,並在對其本地副本執行所有掛起的寫操作之後由leader排序。爲了保證給定的讀操作返回最新的更新值,客戶端在讀操作之後調用sync。客戶端操作的FIFO順序保證和全局同步保證使讀操作的結果能夠反映發出同步之前發生的任何更改。在我們的實現中,我們不需要原子廣播同步,因爲我們使用基於leader的算法,我們只是將同步操作放在leader和執行同步調用的服務器之間的請求隊列的末尾。爲了做到這一點,follower必須確保領導者仍然是領導者。如果存在提交的掛起事務,則服務器不會懷疑leader。如果掛起隊列爲空,leader需要發出一個空事務來提交,並在該事務之後命令同步。這樣做的好處是,當leader處於負載時,不會產生額外的廣播流量。在我們的實現中,設置了超時,以便領導者在follower拋棄它們之前意識到它們不是領導者,因此我們不會發出null事務。

ZooKeeper服務器按照FIFO順序處理來自客戶端的請求。響應包括響應相對的zxid。即使在沒有活動的間隔期間,心跳消息也包括客戶端所連接的服務器所看到的最後一個zxid。如果客戶端連接到一個新服務器,新服務器通過檢查客戶端的最後一個zxid和它的最後一個zxid來確保它的ZooKeeper數據視圖至少與客戶端的視圖一樣新。如果客戶端的視圖比服務器的更近,服務器在服務器跟上之前不會重新建立與客戶端的會話。客戶端保證能夠找到另一個具有系統最新視圖的服務器,因爲客戶端只看到已經複製到大多數ZooKeeper服務器上的更改。這種行爲對於保證持久性非常重要。

爲了檢測客戶端會話失敗,ZooKeeper使用了超時(機制)。如果在會話超時內沒有其他服務器從客戶端會話接收到任何信息,leader將確定出現了故障。如果客戶端發送請求的頻率足夠高,那麼就不需要發送任何其他消息。否則,客戶端在低活動期間發送心跳消息。如果客戶端無法與服務器通信來發送請求或心跳,它將連接到另一個ZooKeeper服務器來重新建立會話。爲了防止會話超時,ZooKeeper客戶端庫會在會話空閒時間s/3 ms後發送一個心跳,如果在2s/3ms沒有收到服務器信號,則切換到新的服務器,其中s是會話超時,單位爲毫秒。

5 Evaluation

我們在一個50臺服務器的集羣上執行了所有評估。每臺服務器都有一個Xeon雙核2.1GHz處理器,4GB內存,千兆以太網和兩個SATA硬盤。我們將下面的討論分爲兩個部分:吞吐量和請求延遲。

5.1 Throughput

爲了評估我們的系統,我們對系統飽和時的吞吐量以及各種注入故障時吞吐量的變化進行了基準測試。我們改變了組成ZooKeeper服務的服務器數量,但始終保持客戶端數量不變。爲了模擬大量客戶端,我們使用35臺機器模擬250個併發客戶端。

我們有ZooKeeper服務器的Java實現,以及Java和C客戶端。在這些實驗中,我們使用配置爲記錄到一個專用磁盤並在另一個專用磁盤上拍攝快照的Java服務器。我們的基準測試客戶端使用異步Java客戶端API,每個客戶端至少有100個未完成的請求。每個請求都包含對1K數據的讀取或寫入。我們沒有展示其他操作的基準測試,因爲所有修改狀態的操作的性能大致相同,而非狀態修改操作(不包括同步)的性能大致相同。(同步的性能近似於輕量級寫操作,因爲請求必鬚髮送給leader,但不會廣播)。客戶端每300毫秒發送一次已完成操作的計數,我們每6s發送樣品。爲了防止內存溢出,服務器會限制系統中併發請求的數量。ZooKeeper使用請求節流來防止服務器過載。在這些實驗中,我們將ZooKeeper服務器配置爲最多處理2000個請求。

<center>圖5:飽和系統的吞吐量性能,隨讀寫比的變化而變化</center>

Servers 100% Reads 0% Reads
13 460k 8k
9 296k 12k
7 257k 14k
5 165k 18k
3 87k 21k

<center>表1:飽和系統極端情況下的吞吐量性能</center>

在圖5中,顯示了當我們改變讀與寫請求的比率後的吞吐量,每條曲線對應於提供ZooKeeper服務的不同數量的服務器。表1顯示了讀取負載的極端值。讀吞吐量比寫吞吐量高,因爲讀不使用原子廣播。該圖還顯示了服務器的數量對廣播協議的性能也有負面影響。從這些圖中,我們可以看到,系統中的服務器數量不僅會影響服務可以處理的故障數量,還會影響服務可以處理的工作負載。請注意,三個服務器的曲線與其他服務器的曲線交叉在60%左右。這種情況並不只存在於三服務器配置中,由於本地讀取啓用了並行性,所有配置都會出現這種情況。但是,在圖中的其他配置中是看不到的,因爲我們爲可讀性限制了最大y軸吞吐量。

寫請求比讀請求花費的時間長有兩個原因。首先,寫請求必須通過原子廣播,這需要一些額外的處理並增加請求的延遲。處理寫請求時間較長的另一個原因是,服務器必須確保在向leader發送確認之前,將事務記錄到非易失性存儲中。原則上,這個要求是過分的,但是對於我們的生產系統,我們用性能來換取可靠性,因爲ZooKeeper構成了應用的基礎。我們使用更多的服務器來容忍更多的錯誤。通過將ZooKeeper數據分割成多個ZooKeeper集合來增加寫吞吐量。這種複製和分區之間的性能權衡已經被Gray等人觀察到。

<center>圖6:飽和系統的吞吐量,當所有客戶端都連接到leader時,讀寫比率的變化</center>

ZooKeeper能夠通過在組成服務的服務器之間分配負載來實現如此高的吞吐量。我們可以分配負載,因爲我們寬鬆的一致性保證。Chubby的客戶端把所有的要求都交給leader。圖6顯示瞭如果我們不利用這種寬鬆,並強制客戶端只連接到leader會發生什麼。正如預期的那樣,讀占主導地位的工作負載的吞吐量要低得多,但即使是寫占主導地位的工作負載,吞吐量也要低得多。服務客戶端導致的額外CPU和網絡負載會影響領導者協調提案廣播的能力,這反過來會對總體寫性能產生負面影響。

<center>圖7:隔離的原子廣播組件的平均吞吐量。 誤差線表示最小值和最大值。</center>

原子廣播協議完成了系統的大部分工作,因此對ZooKeeper的性能的限制比其他任何組件都要大。圖7顯示了原子廣播組件的吞吐量。爲了對其性能進行基準測試,我們通過直接在leader上生成事務來模擬客戶端,因此不存在客戶端連接或客戶端請求和響應。在最大吞吐量時,原子廣播組件會受到CPU的限制。理論上,圖7的性能可以與100%寫操作的ZooKeeper的性能相匹配。但是,ZooKeeper客戶端通信、ACL檢查和請求事務轉換都需要CPU。對CPU的爭用將ZooKeeper吞吐量降低到實質上低於隔離狀態下的原子廣播組件。因爲ZooKeeper是關鍵的生產組件,所以到目前爲止,我們對ZooKeeper的開發重點一直是正確性和健壯性。通過消除額外副本、同一對象的多次序列化、更有效的內部數據結構等,可以有很多機會顯著提高性能。

<center>圖8:故障時吞吐量。</center>

爲了顯示隨着時間的推移系統在注入故障時的行爲,我們運行了一個由5臺機器組成的ZooKeeper服務。我們運行了與以前相同的飽和度基準,但這一次我們將寫入百分比保持在恆定的30%,這是預期工作負載的保守比率。我們定期終止一些服務器進程。圖8顯示了隨時間變化的系統吞吐量。圖中標註的事件如下:

  1. follower的故障和恢復;
  2. 不同follower的故障和恢復;
  3. leader故障;
  4. 前兩個標記中有兩個followers (a,b)失敗,第三個標記(c)中恢復;
  5. leader故障。
  6. 恢復leader。

從這個圖表中可以看到一些重要現象。首先,如果followers故障並快速恢復,那麼ZooKeeper能夠在失敗的情況下維持高吞吐量。單個followers的故障不會阻止服務器產生仲裁(quorum),只會降低吞吐量,大致相當於服務器在失敗前處理的讀請求的份額。其次,我們的leader選舉算法能夠足夠快速的恢復,以防止吞吐量大幅下降。在我們的觀察中,ZooKeeper只需要不到200毫秒就可以選出一個新的leader。因此,儘管服務器會在一秒內停止服務請求,但由於採樣週期(以秒爲單位),我們不會觀察到吞吐量爲零。第三,即使followers需要更多的時間恢復,但是一旦他們開始處理請求,ZooKeeper也可以在開始處理請求後再次提高吞吐量。在事件1、2和4之後,我們不能恢復到完全吞吐量水平的一個原因是,客戶端僅在其與跟隨者的連接斷開時才切換跟隨者。因此,在事件4之後,直到leader在事件3和事件5故障,客戶端才重新分配自己。實際上,隨着客戶的來來去去,這種不平衡會隨着時間的推移自行解決。

Table 3: Barrier experiment with time in seconds. Each point is the average of the time for each client to finish over five runs.

5.2 Latency of requests

爲了評估請求的延遲,我們創建了一個以Chubby基準爲模型的基準測試。我們創建一個工作進程,它只發送一個create,等待它完成,發送一個對新節點的異步刪除,然後開始下一個create。我們相應地改變工作程序的數量,每次運行,我們讓每個工作程序創建50,000個節點。我們通過將創建請求的完成數量除以所有工作程序完成所花費的總時間來計算吞吐量。

Number of servers
Workers 3 5 7 9
1 776 748 758 711
10 2074 1832 1572 1540
20 2740 2336 1934 1890

表2顯示了基準測試的結果。create請求包含1K的數據,而不是Chubby基準測試中的5字節,以便更好地符合我們的預期使用。即使有這些更大的請求,ZooKeeper的吞吐量也比Chubby公佈的吞吐量高出3倍以上。單個ZooKeeper worker基準測試的吞吐量表明,3臺服務器的平均請求延遲爲1.2ms, 9臺服務器的平均請求延遲爲1.4ms。

# of clients
# of barriers 50 100 200
200 9.4 19.8 41.0
400 16.4 34.1 62.0
800 28.9 55.9 112.1
1600 54.0 102.7 234.4

<center>表3:以秒爲單位的屏障實驗。 每個點是每個客戶完成五次運行的平均時間</center>

5.3 Performance of barriers

在這個實驗中,我們依次執行了一些barrier(屏障)來評估用ZooKeeper實現的原語的性能。對於給定數量的b個barrier,每個客戶首先進入所有b個barrier,然後依次離開所有b個barrier。當我們使用2.4節中的雙屏障算法時,客戶端首先等待所有其他客戶端執行enter()過程,然後再轉移到下一個調用(類似leave())。

表3展示了我們的實驗結果。在本次實驗中,我們有50個、100個、200個客戶端陸續進入b個barrier,b ∈{200,400,800,1600}。儘管一個應用程序可以有數千個ZooKeeper客戶端,但通常只有更小的子集參與到每個協調操作中,因爲客戶端通常根據應用程序的具體情況進行分組。

這個實驗有兩個有趣觀察結果,處理所有barrier的時間隨barrier數量的增加而線性增長,這表明併發訪問同一數據樹的一部分沒有產生任何意外的延遲,並且延遲隨客戶端數量的增加而成正比。這是不使ZooKeeper服務飽和的結果。實際上,我們觀察到,即使客戶端以lock-step( 鎖定步進)進行,barrier操作(進入和離開)的吞吐量在所有情況下都在每秒1,950到3,100個操作之間。在ZooKeeper操作中,這相當於每秒10,700到17,000個操作之間的吞吐量值。在我們的實現中,讀寫比率爲4:1(讀操作的80%),與ZooKeeper可以達到的原始吞吐量(根據圖5超過40,000)相比,我們的基準代碼使用的吞吐量要低得多。 這是由於客戶端在等待其他客戶端。

6 Related work

ZooKeeper的目標是提供減輕分佈式應用程序中協調流程問題的服務。 爲了實現此目標,其設計使用了以前的協調服務,容錯系統,分佈式算法和文件系統的思想。

我們並不是第一個提出分佈式應用協調系統的人。一些早期的系統提出了一種分佈式鎖服務,用於事務性應用程序,以及用於在計算機集羣忠共享信息。最近,Chubby提出了一個系統來管理分佈式應用程序的諮詢鎖。Chubby和ZooKeeper有一些共同的目標。它還有一個類似文件系統的界面,並使用一致性協議來保證副本的一致性。然而,ZooKeeper並不是一個鎖服務。客戶端可以使用它來實現鎖,但是在它的API中沒有鎖操作。與Chubby不同,ZooKeeper允許客戶連接到任何ZooKeeper服務器,而不僅僅是leader。ZooKeeper客戶端可以使用本地副本來提供數據和管理watch,因爲它的一致性模型要比Chubby寬鬆得多。這使得ZooKeeper能夠提供比Chubby更高的性能,允許應用程序更廣泛地使用ZooKeeper。

爲了減輕構建容錯分佈式應用程序的問題,文獻中已經提出了一些容錯系統。早期的一個系統是ISIS[5]。ISIS系統將抽象的類型規範轉換爲容錯的分佈式對象,從而使容錯機制對用戶透明。荷魯斯[30]和合奏[31]是由ISIS演化而來的系統。動物園管理員擁抱ISIS虛擬同步的概念。最後,圖騰保證了在一個利用局域網[22]硬件廣播的體系結構中消息傳遞的總順序。動物園管理員工作與各種各樣的網絡拓撲,這激勵我們依賴TCP連接之間的服務器進程,不假定任何特殊的拓撲或硬件功能。我們也不會暴露在ZooKeeper內部使用的任何集成通信。

爲了減輕構建容錯分佈式應用程序的問題,文獻中已經提出了一些容錯系統。早期有一個系統ISIS。ISIS系統將抽象的類型規範轉換爲容錯的分佈式對象,從而使容錯機制對用戶透明。Horus和Ensemble是由ISIS演化而來的系統。ZooKeeper包含ISIS虛擬同步的概念。最後,Totem保證了在一個利用局域網硬件廣播的體系結構中消息傳遞的總順序。ZooKeeper可使用多種網絡拓撲,這促使我們依靠服務器進程之間的TCP連接,而不需要假設任何特殊的拓撲或硬件功能。我們也不會暴露在ZooKeeper內部使用的任何集成通信。

構建容錯服務的一項重要技術是狀態機複製,而Paxos是一種能夠有效實現異步系統的複製狀態機的算法。我們使用的算法具有Paxos的一些特徵,但是它將事務日誌和數據樹恢復所需的預寫日誌結合在一起,從而完成一種高效的實現。已經有了一些協議提案,可以實際實現Byzantine-tolerant的複製狀態機[7、10、18、1、28]。 ZooKeeper並不假設服務器是Byzantine,但我們確實使用了檢查和和健康檢查等機制來捕獲非惡意的Byzantine故障。Clement等人討論了一種不修改當前服務器代碼就能使ZooKeeper完全具有Byzantine容錯能力的方法。目前爲止,我們還沒有在產品中觀察到使用完全Byzantine容錯協議可以防止的錯誤。

Boxwood是一個使用分佈式鎖服務器的系統。Boxwood爲應用程序提供了更高層次的抽象,它依賴基於Paxos的分佈式鎖服務。和Boxwood一樣,ZooKeeper也是一個用於構建分佈式系統的組件。然而,ZooKeeper具有高性能要求,並且在客戶端應用程序中使用更廣泛。ZooKeeper公開應用程序用於實現高級原語的低級原語。

ZooKeeper類似於一個小型文件系統,但它只提供了文件系統操作的一個小子集,並添加了大多數文件系統中不存在的功能,比如順序保證和條件寫。然而,ZooKeeper 在watch精神上類似於AFS的緩存回調。

Sinfonia引入了迷你事務,這是一種用於構建可伸縮分佈式系統的新模型。Sinfonia被設計用來存儲應用程序數據,而ZooKeeper存儲應用程序元數據。 ZooKeeper保持狀態完全複製並存儲在內存中,以實現高性能和一致的延遲。我們對文件系統的使用,如操作和排序,讓功能類似於迷你事務。znode是一個方便的我們在上面添加watch的抽象,一個功能是Sinfonia。Dynamo允許客戶端在分佈式鍵值存儲中獲取和放置相對較小(小於1M)的數據量。不像ZooKeeper,Dynamo中的關鍵空間不是分層的。Dynamo也不提供強大的持久性和一致性保證或寫入,而是解決了讀取衝突。

DepSpace使用一個元組空間來提供Byzantine容錯服務。像ZooKeeper一樣,DepSpace使用一個簡單的服務器接口在客戶機上實現強同步原語。雖然DepSpace的性能遠低於ZooKeeper,但它提供了更強的容錯性和機密性保證。

7 Conclusions

ZooKeeper通過將無等待對象公開給客戶端,來解決分佈式系統中協調進程的問題。我們發現ZooKeeper在Yahoo!內部和外部的多個應用中都很有用。ZooKeeper通過使用watch的快速讀取,爲以讀取爲主的工作負載實現了每秒數十萬次操作的吞吐量值,這兩種操作都由本地副本提供。雖然我們的一致性保證讀取和watch似乎很弱,但是我們已經展示的用例表明,即使讀取沒有優先級排序並且數據對象的實現也是如此。事實證明,免等待屬性對於高性能至關重要。

雖然我們只描述了少數幾個應用,還有許多其他使用ZooKeeper的應用。我們相信這樣的成功是由於它簡單的接口以及可以通過這個接口實現強大的抽象。此外,由於ZooKeeper的高吞吐量,應用程序可以廣泛使用它,而不僅僅是粗粒度鎖。

Acknowledgements

我們要感謝Andrew Kornev和Runping Qi爲ZooKeeper做出的貢獻;Zeke Huang和Mark Marchukov提供的有價值的反饋;布萊恩·庫珀和勞倫斯·拉蒙蒂亞努感謝他們對ZooKeeper的早期貢獻;Brian Bershad和Geoff Voelker對演講做了重要的評論。

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