天天看點

Google Chubby(中文版)

本文翻譯自Google的[The Chubby lock service for loosely-coupled distributed systems]。翻譯此文旨在傳播更多資訊。翻譯水準有限,時間少且文章太長了,品質一般。完整版本請閱讀原文。如果轉載請完整轉載,并包含本申明。

摘要

1 簡介

2 設計

  2.1 基礎依據(Rationale)

  2.2 系統結構

  2.3 檔案,目錄和句柄

  2.4 鎖和序号

  2.5 事件

  2.7 緩存

  2.8 會話 和 KeepAlives

  2.9 故障切換

  2.12 鏡像 (mirroring)

3 擴充機制

4 實際應用,意外和設計錯誤

  4.1 實際應用和表現

  4.4 故障切換的問題

  4.5 攻擊性的用戶端

  4.6 Lessons learned

5 與相關工作的比較 (Comparison with related work)

6 總結

7 緻謝

參考文獻

=========================================================================

我們描述了我們在Chubby鎖服務方面的經曆。Chubby的目标是為松散耦合的分布式系統,提供粗粒度的鎖定、可靠的存儲(盡管容量不大)。Chubby提供了一個很像帶有意向鎖的分布式檔案系統的接口(interface),不過它的設計重點是可用性和可靠性,而不是高性能。許多Chubby服務執行個體已經使用了超過一年,其中的幾個執行個體每個都同時處理着數萬台用戶端。本文描述了最初的設計和預期使用,将之與實際的使用相比較,并解釋了設計是怎樣修改以适應這些差别的。

本文介紹了一種鎖服務: Chubby。它被計劃用在由适度規模的大量小型計算機通過高速網絡互聯構成的的松散耦合的分布式系統中。例如,一個Chubby執行個體(也叫Chubby單元(Cell))可能服務于通過1Gbit/s網絡互聯的一萬台4核計算機。大部分Chubby單元限于一個資料中心或者機房,不過我們運作着至少一個各個副本(replica)之間相隔數千公裡的Chubby單元。

鎖服務的目的是允許它的客戶應用們(clients)同步它們的活動,并對它們所處環境的基本資訊取得一緻。主要的目标包括在适度大規模的客戶應用群時的可靠性、可用性,以及易于了解的語義;吞吐量和存儲容量被認為是次要的。Chubby的用戶端接口很像這樣一個簡單的檔案系統:能執行整檔案的讀和寫,另外還有意向鎖和和多種諸如檔案改動等事件的通知。

我們預期Chubby幫助開發者處理他們的系統中的粗粒度同步,特别是處理從一組各方面相當的伺服器中選舉上司者。例如Google檔案系統(Google File System)[7]使用一個Chubby鎖來選擇GFS Master 伺服器,Bigtable[3]以數種方式使用Chbbuy:選擇上司者;使得Master可以發現它控制的伺服器;使得客戶應用(client)可以找到Master。此外,GFS和Bigtable都用Chubby作為衆所周知的、可通路的存儲小部分中繼資料(meta-data)的位置;實際上它們用Chubby作為它們的分布式資料結構的根。一些服務使用鎖在多個伺服器間(在粗粒度級别上)拆分工作。

在Chubby釋出之前,Google的大部分分布式系統使用必需的、未提前規劃的(ad hoc)方法做主從選舉(primary election)(在工作可能被重複但無害時),或者需要人工幹預(在正确性至關重要時)。對于前一種情況,Chubby可以節省一些計算能力。對于後一種情況,它使得系統在失敗時不再需要人工幹預,顯著改進了可用性。

熟悉分布式計算的讀者會意識到在多個相同體(peer)間primay選舉是分布式協同(distributed consensus)問題的一個特例,同時意識到我們需要一種異步(asynchronous)通信的解決方案。異步(asynchronous)這個術語描述了絕大多數真實網絡(real networks)如以太網或網際網路的行為:它們容許資料包丢失、延時和重排序。(專家們一般應該了解(真實網絡的)協定集建立在對環境做了很強假設的模型之上。) 異步一緻性由Paxos協定[12, 13]解決了。同樣的協定被Oki和Liskov(見于他們有關Viewstamped replication的論文[19, $4])使用,其他人使用了等價的協定[14, $6]。實際上,迄今為止我們遇到的所有可用的異步協同協定的核心都有Paxos。Paxos不需要計時假設來維持安全性,但必須引入時鐘來確定活躍度(liveness)。這克服了Fisher等人的不可能性結果(impossibility result of Fisher et al.)[5, $1]。

建構Chubby是一種滿足上文提到的各種需求的工程上的工作,不是學術研究。我們聲明沒有提出新的算法和技術。本文的目的在于描述我們做了什麼以及為什麼這麼做,而不是提出這些。在接下來的章節中,我們描述Chubby的設計和實作,以及在實際過程中它是怎麼改變的。我們描述了預料之外的Chubby的使用方式,以及被證明是錯誤的特性。我們忽略了在其他文獻中已經包括的細節,例如一緻性協定或者RPC系統。

有人可能會争論說我們應該建構一個包含Paxos的庫,而不是一個通路中心化鎖服務的庫,即使是一個高可靠的鎖服務。用戶端Paxos庫将不依賴于其他伺服器(除了名字服務(name service)以外),并且假定他們的服務可以實作為狀态機,将為開發者提供标準化的架構(and would provide a standard framework for programmers, assuming their services can be implemented as state machines)。事實上,我們提供了一個這樣的與Chubby無關的用戶端庫。

然而,一個鎖服務具有一些用戶端庫不具有的優點。第一,有時開發者并沒有如人們希望的那樣,為高可用性做好規劃。通常他們的系統從隻有很小負載和寬松的可用性保證的原型開始,代碼總是沒有特别地構造為使用一緻性協定。當服務成熟,得到了使用者,可用性變得更重要了,複制(replaction)和主從選舉(primary election)随後被加入到已有的設計中。 盡管這能通過提供分布式協同的庫來搞定,但鎖服務更易于保持已經存在的程式結構和通信模式。例如,選擇一個master,然後将選舉結果寫入一個已經存在的檔案伺服器中,隻需要加兩條語句和一個RPC參數到已經存在的系統中:一條語句請求一個鎖以成為master,另外傳遞一個整數(鎖請求計數)給寫RPC,再加入一條if語句給檔案伺服器拒絕寫入如果請求計數小于目前的值(用于防止延時的包)。我們發現這種技術比将已有的伺服器加入一緻性協定更容易,尤其是在遷移期間(transition period)必須維持相容性時。

第二,我們的許多服務在選舉parimary,或在它們的各個元件間劃分資料時,需要一種公布結果的機制。這意味着我們應該允許用戶端存儲和取得小量的資料–也就是讀寫小檔案。這能通過名字服務來完成,但是我們的經驗是鎖服務自身相當适合做這件事,既因為這樣減少了用戶端要依賴的伺服器數,也因為協定的一緻性特性是相同(shared)的。Chubby的作為一個名字伺服器的成功,應很大程度上應歸功于一緻的用戶端緩存,而不是基于時間的緩存。特别地,我們發現開發者相當地賞識不需要選擇一個像DNS生存時間值(time-to-live)一樣的緩存逾時值,這個值如果選擇不當會導緻很高的DNS負載或者較長的用戶端故障修複時間。

第三,基于鎖的接口更為程式員所熟悉。Paxos的複制狀态機(replicated state machine)和與排他鎖關聯的臨界區都能為程式員提供順序程式設計的幻覺。可是,許多程式員已經用過鎖了,并且認為他們知道怎麼使用它們。頗為諷刺的是,這樣的程式員經常是錯的,尤其是當他們在分布式系統裡使用鎖時。很少人考慮單個機器的失敗對一個異步通信的系統中的鎖的影響。不管怎樣,對鎖的表面上的熟悉性,戰勝了試圖說服程式員們為分布式決策使用(其他)可靠機制的努力。

最後,分布式協同算法使用quorums做決策,于是它們使用多個副本來達到高可用性。例如,Chubby本身通常在每個單元中有五個副本,Chubby單元要存活(to be up)的話必須保證其中三個副本在正常運作。相反,如果一個用戶端系統使用一個鎖服務,即使隻有一個用戶端能成功取到鎖也能繼續安全地運作。是以,鎖服務能減少可靠的用戶端系統運作所需要伺服器數目。在更寬泛的意義上,人們能夠将鎖服務視作一種通過提供通用的全體選民(electorate)的方式,允許一個客戶系統在少于其多數的成員存活(up)時正确地決策。人們可以設想用不同的方式解決最後的這個問題:通過提供一個“協同服務”(consensus service), 使用一些伺服器提供Paxos協定中的”acceptors”。像鎖服務一樣,“協同服務”也将允許用戶端(clients)在即使隻有一個活躍客戶程序的情況下繼續安全地運作。類似的技術曾經被用于減少拜占庭故障相容問題(Byzantine fault tolerance)所需的狀态機(state machines)數目[24].。然而,假如協同服務不專門地提供鎖(也就是将其删減為一個鎖服務),這種途徑不能解決任意一個上文提到的其它問題。

上面這些讨論建議了我們的兩個關鍵的設計決策:

我們選擇一個鎖服務,而不是一個庫或者一緻性服務,以及

我們選擇提供小檔案,以使得被選出來的primaries可以公布它們自身以及它們的參數,而不是建立和維護另外一個服務。

某些設計決策則來自于我們預期的用途和我們的環境:

一個通過Chubby檔案來公布其Primary的服務,可能擁有數千的用戶端。是以,我們必須允許數千的用戶端監視這個檔案,并且最好不需要太多伺服器。

用戶端和有多個副本(replica)的服務的各個副本要知道什麼時候服務的primary發生了變化。這意味着一種事件通知機制将很有用,以避免輪詢。

即使用戶端不需要間歇性地輪詢檔案,很多用戶端還是會這樣做;這是支援許多開發者的結果。是以,緩存這些檔案是很可取的。

我們的開發者對不直覺的緩存語義感到困擾,是以我們傾向于一緻的緩存(consistent caching)。

為了避免金錢上的損失與牢獄之災(jail time),我們提供了包括通路控制在内的安全機制。

一個可能讓一些讀者吃驚的選擇是,我們不希望鎖被細粒度地使用,這種情況下這些鎖可能隻被持有一段很短的時間(數秒或更短);實際上(instead),我們希望粗粒度地使用。例如,一個應用程式可能使用鎖來選舉一個primary,然後這個primary在一段相當長的時間可能是數小時或數天裡,處理所有對資料的通路。這兩種使用方式意味着對鎖伺服器的不同的需求。

粗粒度的鎖在鎖伺服器引入的負載要小得多。特别是鎖的擷取頻率通常隻會與用戶端應用系統的事務頻率隻有很微弱的關聯。粗粒度的鎖不常被請求,是以臨時性的鎖伺服器不可用給用戶端的造成的延時會更少。在另一方面,一個鎖從用戶端到另一個用戶端可能需要高昂的恢複處理,所有人們不希望鎖伺服器的故障恢複造成鎖的丢失。是以,粗粒度的鎖能夠經曆鎖伺服器的失敗而繼續有效是很有用的,這裡不太在意這樣做的開銷,并且這樣的鎖使得許多用戶端可由少數可用性稍低的鎖伺服器服務得足夠好(and such locks allow many clients to be adequately served by a modest number of lock servers with somewhat lower availability)。

細粒度的鎖會有不同的結論。即使鎖服務的短暫的不可用也可能導緻許多用戶端挂起。因為鎖服務上的事務頻率将随着所有用戶端的事務頻率之和一起增長,性能和随意增加新伺服器的能力十分重要。不需要在鎖伺服器失敗之間維持鎖可以減少鎖定的開銷,這是優勢;頻繁地丢棄鎖也不是一個很嚴重的問題,因為鎖隻被持有一段很短的時間。(用戶端必須準備好在網絡分離期間丢失鎖,是以鎖伺服器故障恢複造成的鎖的丢失不會引入新的恢複路徑。(Clients must be prepared to lose locks during network partitions, so the loss of locks on lock server fail-over introduces no new recovery paths.))

Chubby被計劃為隻提供粗粒度的鎖定。幸運的是,對于用戶端而言,實作根據其自身的應用系統定制的細粒度鎖是很簡單的。一個應用程式可能将它的鎖劃分成組,并使用Chubby的粗粒度鎖将這些鎖分組配置設定給應用程式特定的鎖伺服器。維護這些細粒度的鎖隻需要很少的狀态,這些伺服器隻需要持有一個不常變的、單調遞增的請求計數器,這個請求計數器不會經常被更新。用戶端能夠在解鎖時發現丢失了的鎖,并且如果使用了一個簡單定長的租期(lease),其協定将會簡單高效。這種模式的最重要的收益是我們的用戶端開發者對他們的負載所需的伺服器的供應負責,也将他們從自己實作協同的複雜性中解放出來。

Chubby有兩個主要的通過RPC通信的元件:一個伺服器和一個用戶端應用連結的庫,如圖一所示。所有Chubby用戶端和伺服器之間的通信由用戶端庫居間達成。還有一個可選的第三個元件,代理伺服器,将在第3.1節讨論。

一個Chubby單元由一組稱作副本集(replicas)的伺服器(典型的是五個)組成,按降低關聯失敗的可能性來放置(例如分别放在不同的機架)。這些副本使用分布式一緻的協定來選舉一個Master,Master必須從副本集得到多數投票,并得到副本集在一個持續數秒的被稱為master租期(lease)的時間段内不再選舉另一個不同的Master的承諾。隻要Master繼續赢得大多數投票,這個master租期就會周期性地被副本集重新整理。

副本集維護着一個簡單資料庫的備份集,但是隻有master發起對資料庫的讀寫。其他的所有副本簡單地從master複制使用一緻性協定傳送的更新。

用戶端們通過發送master位置請求給列在DNS中的副本集來查找master。非master的副本傳回master的辨別來回應這些請求。一旦一個用戶端定位到master,它将所有請求指引到該master,直到該master停止回應或者指明自己不再是master。寫請求被通過一緻性協定傳播給所有副本,這些請求在寫入達到Chubby單元中的多數副本時被答謝确認。杜請求有master獨自滿足,這樣是安全的–隻要master租期沒有過期,因為沒有别的master可能存在。如果一個master失敗了,其他的副本在他們的master租期到期時運作選舉協定,典型地,一個新的master将在幾秒之内選舉出來。例如,最近兩次的選舉花費了6秒和4秒,但是我們也見過高達30秒的。

如果一個副本失敗了并且在幾個小時内沒有恢複,一個簡單的替換系統從一個空閑池選擇一台新的機器,并在其上啟動鎖伺服器的二進制檔案(binary)。然後它将更形DNS表,将失敗了的副本的IP替換為新啟動的啟動的IP。目前的master周期性地輪詢DNS并最終注意到這個變化,然後它在該Chubby單元的資料庫中更新單元成員清單,這個清單在所有成員之間通過正常的複制協定保持一緻。與此同時,新的副本取得存儲在檔案伺服器上的資料庫備份和活躍副本的更新的組合。一旦新副本處理了一個目前master在等待送出的請求,新的副本就被許可在新master的選舉中投票。

Chubby開放出一個類似于UNIX的檔案系統[22]的接口,但比Unix的檔案系統更簡單。它由一個嚴格的檔案和目錄樹組成,名字的各部分使用反斜杠劃分。一個典型的名字如下:

其中的ls字首與所有的Chubby名字相同,代表着鎖服務(lock service)。第二個部分(foo)是Chubby單元的名字,通過DNS查詢被解析到一個或多個Chubby伺服器。一個特殊的單元名字local,表明應該使用用戶端的本地Chubby單元,這個Chubby單元通常在同一棟樓裡,是以這個單元最有可能能通路到。名字的剩餘部分,/wombat/pouch,由Chubby單元内部解析。同樣跟UNIX一樣,每個目錄包含一個所有子檔案和子目錄的清單,而每個檔案包含一串不解析的位元組。

因為Chubby的命名結構組成了一個檔案系統,我們既可以通過它的專門API将它開放給應用系統,也可以通過我們的其他檔案系統例如GFS使用的接口。這樣顯著地減少了編寫基本浏覽和名字空間操作的工具的工作(effort),也減少了教育訓練那些偶然用Chubby的使用者的需求。

這種設計使得chubby接口不同于UNIX檔案系統,它使得分布更容易(The design differs from UNIX in a ways that easy distribution)。為允許不同目錄下的檔案由不同的Chubby Master來服務,我們沒有放出(expose)那些将檔案從一個目錄移動到另一個目錄的操作,我們不維護目錄修改時間,也避開路徑相關的權限語義(也就是檔案的通路由其本身的權限控制,而不由它上層路徑上的目錄控制)。 為使緩存檔案中繼資料更容易,系統不公開最後通路時間。

其名字空間包含檔案和目錄,統一叫做節點(nodes)。每個這樣的節點在其所在的Chubby單元内隻有一個名字,沒有符号連結和硬連結。

節點可能是永久的或者瞬時(ephemeral)的。任意節點都可以被顯示地(explicitly)删除,但是瞬時節點也會在沒有用戶端打開它時被删除(另外,對目錄而言,在它們為空時被删除)。瞬時檔案被用作臨時檔案,也被用作一個用戶端是否存活的訓示器(給其他用戶端)。任意節點都能作為一個意向性(advisory)的讀寫鎖;這些鎖将在2.4節更詳細地描述。

每個節點有多種中繼資料,包括通路控制清單(ACLs)的三個名字,分别用于控制讀、寫和修改其ACL。除非被覆寫,節點在建立時繼承父目錄的ACL名字。ACLs本身是位于一個ACL目錄中的檔案, 這個ACL目錄是Chubby單元的一個為人熟知的本地名字空間。這些ACL檔案的内容由簡單的通路名字(principals)清單組成;這裡讀者可能會想起Plan 9的 groups[21]。這樣,如果檔案F的寫ACL名字是foo,并且ACL目錄中包含了一個名foo的檔案,foo檔案中包含bar這個條目,那麼使用者bar就可以寫檔案F。使用者由内嵌在RPC系統裡的機制鑒權。因為Chubby的ACLs是平常的檔案,它們自動地就可以由其他想使用類似的通路控制機制的服務通路。

每個節點的中繼資料包括四個單調遞增的64位編号。這些編号允許用戶端很容易地檢測變化:

執行個體編号:大于任意先前的同名節點的執行個體編号。

内容的世代編号(隻針對檔案):這個編号在檔案内容被寫入時增加。

鎖的世代編号:這個編号在節點的鎖由自由(free)轉換到持有(held)時增加。

ACL的世代編号:這個編号在節點的ACL名字被寫入時增加。

Chubby也放出了一個64位的檔案内容的校驗碼,是以用戶端可以分辨檔案是否有變化。

用戶端通過打開節點得到類似于UNIX檔案描述符的句柄(handles)。句柄包括:

校驗位:阻止用戶端自行建立或猜測句柄,是以完整的通路控制檢查隻需要在句柄建立時執行(對比UNIX,UNIX在打開時檢查權限位但在每次讀寫時不檢查,因為檔案描述符不能僞造)。

一個序列号:這個序列号允許master分辨一個句柄是由它或前面的master生成。

模式資訊:在句柄打開時設定的是否允許新master在遇見一個由前面的master建立的舊句柄時重建該句柄的狀态。

每個Chubby檔案和目錄都能作為一個讀寫鎖:要麼一個用戶端句柄以排他(寫者)模式持有這個鎖,要麼任意數目的用戶端句柄以共享(讀者)模式持有這個鎖。像大部分程式員熟知的互斥器(mutexes),鎖是意向性的(advisory)。就是隻與對同一個鎖的加鎖請求沖突:持有鎖F既不是通路檔案F的必要條件,也不會阻止其他用戶端通路檔案F。我們舍棄強制鎖—強制鎖使得其他沒有持有鎖的用戶端不能通路被鎖定的對象:

Chubby鎖經常保護由其他服務實作的資源,而不僅僅是與鎖關聯的檔案。以一種有意義的方式執行強制鎖定可能需要我們對這些服務做更廣泛的修改。

我們不想強迫使用者在他們為了調試和管理而通路這些鎖定的檔案時關閉應用程式。在一個複雜的系統中,這種在個人電腦上常用的方法是很難用的。個人電腦上的管理者見可以通過訓示使用者關閉應用或者重新開機來中止強制鎖定。

我們的開發者用正常的方式來執行錯誤檢測,例如“lock X is held”,是以他們從強制檢查中受益很少。有Bug或者惡意的程序有很多機會在沒有持有鎖時破壞資料,是以我們我們發現強制鎖定提供的額外保護沒有實際價值。

在Chubby中,請求任意模式的鎖都需要寫權限,因而一個無權限的讀者不能阻止一個寫者的操作。

在分布式系統中,鎖定是很複雜的,因為通信經常發生變化(communication is typically uncertain),并且程序可能各自獨立地失敗(fail independently)。像這樣,一個持有鎖 L 的程序可能發起請求 R ,但接着就失敗了。另一個程序可能請求 L 并在 R 到達目的地之前執行了某些操作。如果 R 來晚了,它可能在沒有鎖 L 的保護下操作,也有可能在不一緻的資料上操作。接收消息順序紊亂的問題已經被研究得很徹底:解決方案包括虛拟時間(virtual time)[11]和虛拟同步(virtual synchrony)[1],它通過確定與每個參與者的觀察一緻的順序處理消息來避免這個問題。

在一個已有的複雜系統中給所有的互動(interaction)引入順序編号(sequence number)的成本很高。作為替代,Chubby通過隻在使用鎖的互動(interaction)中引入序号提供了一種方法。任何時候,鎖持有者可能請求一個序号,一個不透明(opaque)的描述了鎖剛獲得時的狀态的位元組串(byte-string)。它包括鎖的名字,被請求的鎖定模式(獨占還是共享),以及鎖的世代編号。用戶端在其希望操作将被鎖保護時傳遞這個序号給伺服器(例如檔案伺服器)。接受伺服器被預期将測試這個序号是否仍然有效并且具有适當的鎖定模式,如果它将拒絕該請求。序号的有效性可與伺服器的Chubby緩存核對,或者如果伺服器不想維護一個到Chubby的Session的話,可與伺服器最近觀察到的序号比對。這種序号機制隻需要給受影響的消息增加一條字元串,并且很容易地解釋給我們的開發者。

雖然我們發現序号使用簡單,但重要的協定也在緩慢演化。是以Chubby提供了一種不完美但是更容易的機制來降低延遲的或者重排序的到不支援序号的伺服器的風險。如果一個用戶端以通常的方式釋放了一個鎖,像人們期待的那樣,這個鎖立即可供其他的用戶端索取。然而,如果一個鎖是因為持有者停止工作或者不可通路,鎖伺服器将阻止其他的用戶端在一個被稱作鎖延時(lock-delay)的小于某個上界的時間段内索取該鎖。用戶端可能指定任意的低于某個上界(目前是一分鐘)的鎖延時,這個限制防止有毛病的用戶端造成一個鎖(和像這樣的某些資源)在一個随意長的時間裡不可用。 盡管不完美,鎖延時保護未修改過的伺服器和用戶端免受消息延時和重新開機導緻的日常問題的影響。

在建立句柄時,Chubby用戶端可能會訂閱一系列事件。這些事件通過Chubby庫的向上調用(up-call)異步地傳遞給用戶端。這些事件包括:

檔案内容被修改–常用于監視通過檔案公布的某個服務的位置資訊(location)。

子節點的增加、删除和修改 — 用于實作鏡像(mirroring)(第2.12節)。(除了允許新檔案可被發現外,傳回子節點上的事件使得可以監控臨時檔案(ephemeral files)而不影響這些臨時檔案的引用計算(reference counts))

Chubby master故障恢複 — 警告用戶端其他的事件可能已經丢失了,是以必須重新檢查資料。

一個句柄(和它的鎖)失效了 — 這經常意味着某個通訊問題。

鎖被請求了(lock required) — 可用于判斷什麼時候primary選出來了。

來自另一個用戶端的相沖突的鎖請求 — 允許鎖的緩存。

事件在相應的動作發生之後被遞送。這樣,如果一個用戶端被告知檔案内容發生了變化,一定能保證(it’s guaranteed) 接下來它讀這個檔案會看到新的資料(或者比該事件還要更近的資料)。

上面提到的最後兩種事件極少使用,事後思考它們可以略去(and with hindsight could have been omitted)。例如在primary選舉(primary election)後,用戶端通常需要與新的primary聯系,而不是簡單地知道一個新的primary存在了;因而,它們會等待primary将位址寫入檔案的檔案修改事件。鎖沖突事件在理論上允許用戶端緩存其他伺服器持有的資料,使用Chubby鎖來維護緩存的一緻性。一個沖突的鎖請求将會告訴用戶端結束使用與鎖相關的資料;它将結束等待進行的操作,将修改重新整理到原來的位置(home location),丢棄緩存資料,然後釋放鎖。到目前為止,沒有人采納這種用法。

用戶端将Chubby句柄看作一個指向一個支援多種操作的不透明結構的指針。句柄之通過open()建立,以及通過close()銷毀。

Open() 打開一個帶名字的檔案或目錄産生一個句柄,類似于一個UNIX檔案描述符。隻有這個調用需要一個節點名,所有其他的調用都在句柄上操作。

這個節點名是相對于某個已知的目錄句柄的,用戶端庫提供了一個一直有效的“/”目錄句柄。Directory handles avoid the difficulties of using a program-wide current directory in a multi-threaded program that contains many layers of abstraction. (目錄句柄避開了在包含很多層抽象的多線程程式内使用程式範圍内的目前目錄的困難[18]。)

在調用Open()時,用戶端指定了多個選項:

句柄将被如何使用(讀;寫和鎖定;改變ACL);句柄隻在用戶端擁有合适的權限時建立。

需要傳遞的事件(檢視第2.5節)。

鎖延時(第2.4節)。

是否應該(或者必須)建立一個新的檔案或目錄。如果要建立一個檔案,調用者可能要提供初始内容和初始的ACL名字。其傳回值表明這個檔案實際上是否已經建立。

Close() 關閉一個打開的句柄。以後不再允許使用這個句柄。這個調用永遠不會失敗。一個相近的調用Poison()引起該句柄上未完成的和接下來的操作失敗,但不關閉它,這就允許一個用戶端取消由其他線程建立的Chubby調用而不需要擔心被它們通路的記憶體的釋放。

在句柄上進行的主要調用包括:

GetContentsAndStat() 傳回檔案的内容和中繼資料。一個檔案的内容被原子地、完整地讀取。我們避開部分讀取和寫入以阻礙大檔案。一個相關的調用GetStat()隻傳回中繼資料,而ReadDir()傳回一個目錄下的名字和中繼資料(the names and meta-data for the children of a directory)。

SetContents()将内容寫到檔案。可選擇地,用戶端可能提供一個内容世代編号以允許用戶端模拟在檔案上的比較并交換:隻有在世代編号是目前值時内容才被改變。檔案的内容總是完整地、原子地寫入。一個相關的調用SetACL()在節點關聯的ACL名字上執行一個類似的操作。

Delete() 删除一節節點,如果它沒有孩子的話。

Accquire(), TryAccquire(), Release() 獲得和釋放鎖。

GetSequencer() 傳回一個描述這個句柄持有的鎖的序号(sequencer)(見第2.4節)。

SetSequencer() 将一個序号與句柄關聯。在這個句柄上的随後的操作将失敗如果這個序号不再有效的話。

CheckSequencer() 檢查一個序号是否有效。(見第2.4節)

如果節點在該句柄建立之後被删除,其上的調用将會失敗,即使這個檔案随後又重新建立了也是如此。也就是一個句柄關聯到一個檔案的執行個體,而不是檔案名。Chubby可能在任意的調用上使用通路控制,但是總是檢查Open() 調用。

除了調用自身需要的參數之外, 上面的所有調用都帶有一個操作(operation)參數。這個操作參數持有可能與任何調用相關的資料和控制資訊。特别地,通過操作的參數用戶端可能:

用戶端可以利用這個API執行primary選舉:所有潛在的primaries打開鎖檔案,并嘗試獲得鎖。其中一個成功獲得鎖,并成為primary,而其他的則作為副本。這個Primary将它的辨別用SetContents()寫入到鎖檔案,在對檔案修改事件的響應中,它被用戶端和副本們發現,它們用GetContentsAndStat()讀取鎖檔案。理想情況下,Primary通過GetSequencer()取得一個序号,然後将序号傳遞給與它通信的其他伺服器:這些伺服器應該用CheckSequencer()确認它仍然是primary。一個鎖延時可能與不能檢查序号(第2.4節)的服務一起使用。

為了減少讀傳輸量,Chubby用戶端将檔案資料和節點中繼資料(包括檔案缺失資訊(file absence)) 緩存在記憶體中的一個一緻的(consistent)、write-through的緩存中。這個緩存由一個如後文描述的租期機制(lease mechanism)維護,并通過由master發送的過期信号(invalidations)來維護一緻性。Master保留着每個用戶端可能正在緩存的資料的清單。這個協定保證用戶端要麼看到一緻的Chubby狀态,要麼看到錯誤。

當檔案資料或者中繼資料将被修改時,修改操作被阻塞,同時master發送過期信号給所有可能緩存了這些資料的用戶端。這種機制建立在下一節要詳細讨論的KeepAlive RPC之上。在接收到過期信号後,用戶端清除過期的狀态資訊,并通過發起它的下一個KeepAlive調用應答伺服器。修改操作隻在伺服器知道每個用戶端都将這些緩存失效以後再繼續,要麼因為用戶端答謝了過期信号,要麼因為用戶端讓它的緩存租期(cache lease)過期。

隻有一次過期來回(round)是必需的,因為master在緩存過期信号沒有答謝期間将這個節點視為不可緩存的(uncachable)。這種方式讓讀操作總是被無延時地得到處理;這是很有用的,因為讀操作的數量要大大超過了寫操作。另一種選擇可能是在過期操作期間阻塞對該節點的通路;這将使得過度急切的用戶端在過期操作期間連續不斷地以無緩存的通路轟炸master的可能性大大降低,其代價是偶然的延遲。 如果這成為問題,人們可能會想采用一種混合方案,在檢測到過載時切換處理政策(tactics)。

緩存協定很簡單:它在修改時将緩存的資料失效,而永不去更新這些資料。有可能去更新緩存而不是讓緩存失效也會一樣簡單,但是隻更新的協定可能會無理由地低效;某個通路檔案的用戶端可能無限期地收到更新,進而導緻次數無限制的、完全不必要的更新。

盡管提供嚴格一緻性的開銷不小,我們拒絕了更弱的模型,因為我們覺得程式員們将發現它們很難用。類似地,像虛拟同步(virtual synchrony)這種要求用戶端在所有的消息中交換序号的機制,在一個有多種已經存在的通信協定的環境中也被認為是不合适的。

除了緩存資料和中繼資料,Chubby用戶端還緩存打開的句柄。因而,如果一個用戶端打開一個前面已經打開的檔案,隻有第一次Open()調用時會引起一個給Master的RPC。這種緩存被限制在較低層次(in a minor ways),是以它不會影響用戶端觀察到的語義:臨時檔案上的句柄在被應用程式關閉後,不能再保留在打開狀态;而容許鎖定的句柄則可被重用,但是不能由多個應用程式句柄并發使用。最後的這個限制是因為用戶端可能利用Close()或者Poison()的邊際效應:取消正在進行的向Master請求的Accquire()調用。

Chubby的協定允許用戶端緩存鎖 — 也就是,懷着這個鎖會被同一個用戶端重新使用的希望,持有鎖比實際需要更長的時間。如果另一個用戶端請求了一個沖突的鎖,可以用一個事件告知鎖持有者,這允許鎖持有者隻在别的地方需要這個鎖時才釋放鎖(參考§2.5)。

一個Chubby會話是Chubby單元和Chubby用戶端之間的一種關系,它存在于麼某個時間間隔内,并由稱做KeepAlive的間歇性地握手來維持。除非一個Chubby用戶端通知master,否則,隻要它的會話依然有效,該用戶端的句柄、鎖和緩存的資料都仍然有效。(然而,會話維持的協定可能要求用戶端答謝一個緩存過期信号以維持它的會話,請看下文)

一個用戶端在第一次聯系一個Chubby單元的master時請求一個新的會話。它要麼在它結束時明确地結束會話,或者在會話陷入空轉時(在一段時間内沒有任何打開的句柄和調用)結束會話。

每個會話都有關聯了一個租期(lease) — 一個延伸向未來的時間間隔,在這個時間間隔内master保證不單方面中止會話。間隔的結束被稱作租期到期時間(lease timeout)。Master可以自由地向未來延長租期到期時間,但是不能将它往回移動。

在三種情形下,Master延長租期到期時間:會話建立時、master故障恢複(見下文)發生時和它應答來自用戶端的KeepAlive RPC時。在接收KeepAlive時,master通常阻塞這個RPC請求(不允許其傳回),直到用戶端的上前一個租期接近過期。然後,Master允許RPC傳回給用戶端,并告知用戶端新的租期過期時間。Master可能任意長度地延伸逾時時間。預設的延伸是12秒,但一個過載的master可能使用更高的值,以降低它必須處理的KeepAlive調用數目。用戶端在收到上一個回複以後立即發起一個新的KeepAlive,這樣用戶端保證幾乎總是有一個KeepAlive調用阻塞在master上。

除了延伸用戶端的租期,KeepAlive的回複還被用于将事件和緩存過期傳回給用戶端。Master允許一個KeepAlive在有事件或者緩存過期需要遞送時提前傳回。在KeepAlive的回複上搭載事件,保證了用戶端不應答緩存過期則不能維持會話,并使得所有的RPC都是從用戶端流向master。這樣既簡化了用戶端,也使得協定可以通過隻允許單向發起連接配接的防火牆。

用戶端維持着一個本地租期逾時,這個本地逾時值是Master的租期的較小的近似值。它跟master的租期過期不一樣,是因為用戶端必須在兩方面做保守的假設。一是KeepAlive花在傳輸上的時間,一是master的時鐘超前的度。為了維護一緻性,我們要求伺服器的時鐘頻率相對于用戶端的時鐘頻率,不會快于某個常數量(constant factor)。

如果一個用戶端的本地租期過期了,它就變得不能确定master是否終結了它的會話。于是這個用戶端清空并禁用自己的緩存,我們稱它的會話處于危險(jeopardy)中。用戶端在等待一個之後的被稱作寬限期的間隔,預設是45秒。如果這個用戶端和master在寬限期結束之前設法完成了一個成功的KeepAlive,用戶端就再開啟它的緩存。否則,用戶端假定會話已經過期。這樣做了,是以Chubby的API調用不會在Chubby單元變成不可通路時無限期阻塞,如果在通訊重建立立之前寬限期結束了,調用傳回一個錯誤。

Chubby庫能夠通過一個”危險”(jeopardy)事件在寬限期開始時通知應用程式。當知道會話在通訊問題後幸存下來,一個”安全”(safe)事件告知應用程式繼續工作;而如果會話時間過去了,一個”過期”(expire)事件被發送給應用程式。這些通知允許應用程式在對它的會話狀态不确定時停下來,并且在問題被證明是瞬時的時不需要重新開機進行恢複。在啟動開銷很高的服務中,這在避免服務不可用方面可能是很重要的。

如果一個用戶端持有某個節點上的句柄 H,并且任意一個H上的操作都因為關聯的會話過期了而失敗,那麼所有接下來的H上的操作(除了Close()和Poison()) 将以同樣的方式失敗。用戶端可以用這去保證網絡和伺服器不可用時會導緻随後的一串操作丢失了,而不是一個随機的操作子序列丢失,這樣就允許複雜的修改可以以其最後一次寫标記為已送出。

當一個master失效了或者失去了master身份時,它丢棄記憶體中有關會話、句柄和鎖的資訊。有權力的(authoritative)會話租期計時器開始在master上運作。這樣,直到一個新的master被選舉出來,租期計時器才停止;這是合法的因為它等價于延長用戶端的租期。如果一個master選舉很快發生,用戶端可以在它們本地(近似的)租期計時器過期之前與新的master聯系。如果選舉用了很長時間,用戶端刷空它們的緩存并等待一個寬限期(grace period),同時嘗試尋找新的master。以這種方式,寬限期(grace period)使得會話在跨越超出正常租期的故障恢複期間被維持。

圖2展示了一個漫長的故障恢複事件中的事件序列,在這個恢複事件中用戶端必須使用它的寬限期來保留它的會話。時間從左到右延伸,但是時間是不可以往回縮的。用戶端會話租期顯示為粗箭頭,它既被新的、老的Master看到(M1-3,上方),也被用戶端都能看到(C1-3,下方)。傾斜向上的箭頭标示KeepAlive請求,傾斜向上的箭頭标示這些KeepAlive請求的應答。原來的Master有一個給用戶端的租期M1,而用戶端有一個(對租期的)保守的估計C1。在通過KeepAlive應答2通知用戶端之前,原Master允諾了一個新的租期M2;用戶端能夠延長它的視線為租期C2。原Master在應答下一個KeepAlive之前死掉了,過了一段時間後另一個master被選出。最終用戶端的租期估計值(C2)過期了。然後用戶端清空它的緩存,并為寬限期啟動一個計時器。

在這段時間裡,用戶端不能确定它的租期在master上是否已經過期了。它沒有銷毀它的會話,但是它阻塞所有應用程式對它的API的調用,以防止應用程式觀測到不一緻的資料。在寬限期開始時,Chubby的庫發送一個危險事件給應用程式,允許它停下來直到會話狀态能夠确定。最終一個新的master選舉完成了。Master最初使用對它的前任可能持有的給用戶端的租期的保守估算值M3。新Master的來自用戶端的第一個KeepAlive請求(4)被拒絕了,因為它的master代數不正确(下文有較長的描述)。重試請求(6)成功了,但是通常不去延長master租期,因為M3是保守的。然後它的回複(7)允許用戶端延再一次長它的租期(C3),還可以選擇通知應用程式其會話不再處于危險狀态。因為寬限期足夠長,可以覆寫從租期C2的結束到租期C3的開始之間的間隔,用戶端除了觀測到延遲不會看到别的。但假如寬限期比這個間隔短,用戶端将丢棄會話,并将失敗報告給應用程式。

一旦用戶端與新的master聯系上,用戶端庫和master協作提供給應用程式一種沒有故障發生過的假象。為達到此目的,新的master必須重新構造一個前面的master擁有的記憶體狀态的保守估算(conservative approximation)。它通過讀取存儲在硬碟上的資料(通過正常的資料庫複制協定複制)來完成一部分,通過從用戶端擷取狀态完成一部分,通過保守的假設(conservative assumptions)完成一部分。資料庫記錄了每個會話、被持有的鎖和臨時檔案。

一個新選出來的master繼續處理:

1. 它先選擇一個新的代編号(epoch number),這個編号在用戶端每次請求時都要求出示。Master拒絕來自使用較早的代編号的用戶端的請求,并提供新的代編号給它們。這保證了新的master将不會響應一個很早的發給前一個master的包,即使此前的這個Master與現在的master在同一台機器上。

2. 新的Master可能會響應對master的位置的請求(master-location requests),但是不會立即開始處理傳入的會話相關的操作。

3. 它為記錄在資料庫中的會話和鎖建構記憶體資料結構。會話租期被延長至上一個master可能當時正使用的最大期限。

4. Master現在允許用戶端進行KeepAlive,但不能執行其他會話相關的操作。

5. It emits a fail-over event to each session; this causes clients to flush their caches (because they may have missed invalidations), and to warn applications that other events may have been lost.

5. 它發出一個故障切換事件給每個會話;這引起用戶端重新整理它們的緩存(因為這些緩存可能錯過了過期信号(invalidations)),并警告應用程式别的事件可能已經丢失。

6. Master一直等待,直到每個會話應答了故障切換事件或者使其會話過期。

7. Master允許所有的操作繼續處理。

8. 如果一個用戶端使用一個在故障切換之前建立的句柄(可依據句柄中的序号來判斷),master重新建立了這個句柄的記憶體印象(in-memory representation),并執行調用。如果一個這樣的重建後的句柄被關閉後,master在記憶體中記錄它,這樣它就不能在這個master代(epoch)中再次建立;這保證了一個延遲的或者重複的網絡包不能偶然地重建一個已經關閉的句柄。一個有問題的用戶端能在未來的master代中重建一個已關閉的句柄,但倘若該用戶端已經有問題的話,則這樣不會有什麼危害。

9. 在經過某個間隔(如,一分鐘)後,master删除沒有已打開的檔案句柄的臨時檔案。在故障切換後,用戶端應該在這個間隔期間重新整理它們在臨時檔案上的句柄。這種機制有一個不幸的效果是,如果該檔案的最後一個用戶端在故障切換期間丢失了會話的話,臨時檔案可能不會很快消失。

讀者将不意外地得知,遠遠不像系統的其他部分那樣被頻繁使用的故障切換代碼,是一些有趣的bug的豐富來源。

第一版的Chubby使用帶複制的Berkeley DB版本[20]作為它的資料庫。Berkeley DB提供了映射位元組串的鍵到任意的位元組串值上B-樹。我們設定了一個按照路徑名稱中的節數排序的鍵比較函數,這樣就允許節點用它們的路徑名作為鍵,同時保證兄弟節點在排序順序中相鄰。由于Chubby不使用基于路徑的權限,資料庫中的一次查找就可以滿足每次檔案通路。

Berkeley DB使用一種分布式一緻協定将它的資料庫日志複制到一組伺服器上。隻要加上master租期,就吻合Chubby的設計,這使得實作很簡單。相比于在Berkeley DB的B-樹代碼被廣泛使用并且很成熟,其複制相關的代碼是最近增加的,并且使用者較少。維護者必須優先維護和改進他們的最受歡迎的産品特性。

在Berkeley DB的維護者解決我們遇到的問題期間,我們覺得使用這些複制相關的代碼将我們暴露在比我們願負擔的更多的風險中。結果,我們寫了一個簡單的,類似于Birrell et al.[2]的設計的,利用了日志預寫和快照的資料庫。資料庫日志還是想以前一樣利用一緻性協定在多個副本之間分布。Chubby至少了Berkeley DB的很少的特性,這樣重新可使整個系統大幅簡化;例如,我們需要原子操作,但是我們确實不需要通用的事務。

每隔幾個小時,Chubby單元的mster将它的資料庫快照寫到另一棟樓裡的GFS檔案伺服器上 [7]。使用另一棟樓(的檔案伺服器)既確定了備份會在樓宇損毀中儲存下來,也確定備份不會在系統中引入循環依賴,同一棟樓中的GFS單元潛在地可能依賴于Chubby單元來選舉它的master。 備份既提供了災難恢複,也提供了一種初始化一個新建立的副本的資料庫的途徑,而不會将初始化的壓力放在其他正在提供服務的副本上。

Chubby允許一組檔案(a collection of files)從一個單元鏡像到另外一個單元。鏡像是很快的,因為檔案很小,并且事件機制(參見第2.5節)會在檔案增加、删除或修改時立即通知鏡像處理相關的代碼。在沒有網絡問題的前提下,變化會一秒之内便在世界範圍内的很多個鏡像中反映出來。如果一個鏡像不可到達,它保持不變直到連接配接恢複。然後通過比較校驗碼識别出更新了的檔案。

鏡像被最常見地用于将配置檔案複制到各種各樣的遍布全世界的計算叢集。一個叫global的特殊的Chubby單元,含有一個子樹 /ls/global/master,這個子樹被鏡像到每個其他的Chubby單元的子樹 /ls/cell/slave。這個Global單元很特别,因為它的五個副本分散在全球相隔很遠的多個地區,是以幾乎總是能夠從大部分國家/地區通路到它。

在這些從global單元鏡像的檔案中,有Chubby自己的通路控制清單,多種含有Chubby單元和其他系統向我們的監控服務廣告它們的存在的檔案,允許用戶端定位巨大的資料集如BigTable單元的指針資訊(的檔案),以及許多其他系統的配置檔案。

因為Chubby的用戶端是獨立的程序,是以Chubby必須處理比人們預想的更多的用戶端;我們曾經看見過90,000個用戶端直接與一個Chubby伺服器通訊 — 這遠大于涉及到的機器總數。因為在一個單元之中有一個master,而且它的機器跟那些用戶端是一樣的,于是用戶端們能以巨大的優勢(margin)将master打敗。是以,最有效的擴充技術減少與master的通信通過一個重大的因數(Thus, the most effective scaling techniques reduce communication with the master by a significant factor)。假設master沒有嚴重的性能缺陷,master上的請求進行中的較小改進隻有很小的(have little)效果。我們使用了下面幾種方法:

這裡我們描繪兩種熟悉的機制,代理(proxies)和分區(partitioning),這兩種機制将允許Chubby擴充更多。我們現在還沒有在産品環境中使用它們,但是它們已經被設計出來了,并且可能很快被使用。我們還沒有顯示出考慮擴充到五倍以外的需要:第一,在一個資料中心想要放的或者依賴于單個執行個體服務的機器的數目是有限制的;第二,因為我們為Chubby的用戶端和伺服器使用類似配置的機器,硬體上的提升增加了每台機器上的用戶端的數量時,同時也增加了每台伺服器的容量。

Chubby的協定可以由可信的程序代理(在兩邊都使用相同的協定),這個程序将來自其他用戶端的請求傳遞給Chubby單元。一個代理可以通過處理KeepAlive和讀請求減少伺服器負載,它不能減少從代理的緩存中穿過的寫入流量。但即使有積極的用戶端緩存,寫入流量仍遠小于Chubby正常負載的百分之一(參見第4.1節),這樣代理允許大幅提升用戶端的數量。如果一個代理處理Nproxy個用戶端,KeepAlive流量将減少Nproxy倍,而Nproxy可能是一萬甚至更大。A proxy cache can reduce read traffic by at most the mean amount of read-sharing–a factor of around 10 (參見第4.1節)。但因為讀隻占目前的Chubby負載的10%以下,KeepAlive流量上的節省仍然是到目前為止更重要的成效。

代理增加了一個額外的RPC用于寫和第一次讀。人們可能會預期代理會使得Chubby單元的臨時不可用次數至少是以前的兩倍,因為每個代理後的用戶端依賴于兩台可能失效的機器:它的代理和Chubby的Master。

機敏的讀者會注意到2.9節描述的故障切換,對于代理機制不太理想。我們将的第4.4節讨論這個問題。

像第2.3節提到的,Chubby的接口被選擇為Chubby單元的名字空間能在伺服器之間劃分。雖然我們還不需要它,但是現在的代碼(code)能夠通過目錄劃分名字空間。 如果開啟劃分,一個Chubby單元将由 N 個分區 組成,每個分區都有一組副本(replicas)和一個master。每個在目錄 D 中的節點 P(D/C) 将被儲存在分區 P(D/C)= hash(D) mod N 上。注意 D 上的中繼資料可能存儲在另一個不同的分區 P(D) = hash(D’) mod N 上, D’ 是 D 的父目錄。

分區被計劃為以很小的分區之間的通信來啟用很大的Chubby單元集(Chubby cells)。盡管Chubby沒有硬連結(hard links),目錄修改時間,和跨目錄的重命名操作,一小部分操作仍然需要跨分區通信:

因為每個分區獨立地處理大部分調用,我們預期這種通信隻會對性能和可用性造成不太大的影響。

除非分區數目 N 很大,我們預期每個用戶端将與大部分分區聯系。 是以,分區将任意一個分區上的讀寫通訊量(traffic)降低為原來的N分之一,但是不會降低 KeepAlive 的通信量。如果Chubby需要處理更多用戶端的話,我們的應對方案将會聯合使用代理和分區。

下面的表給出了某個Chubby單元的快照的統計資訊;其中的RPC頻率是從一個10分鐘的時間段裡計算出來的。這些數字在Google的Chubby單元中是很常見的。

從表中可以看到下面幾點:

現在我們簡要描述寫我們的Chubby單元不可用的原因。如果我們假定(樂觀地)如果一個單元有一個master願意服務則是”線上的(up)”,在我們的Chubby的采樣中,在數周内我們記錄下合計61次不可用,合計共有700單元-天(??)。我們排除了維護引起的關閉資料中心時的不可用。所有其他的不可用的原因包括:網絡擁塞,維護,超負荷和營運人員引起的錯誤,軟體,硬體。大部分不可用在15s以内或者更短,另外52次在30秒以内;我們的大部分應用程式不會被Chubby的30秒内的不可用顯著地影響到。剩下的就此不可用由這些原因引起:網絡維護(4),可疑的網絡連接配接問題(2),軟體錯誤(2),超負荷(1)。

在好幾打Chubby單元年(cell-years)的運作中,我們有六次丢失了資料,由資料庫軟體錯誤(4)和營運人員錯誤(2)引起;與硬體錯誤沒有關系。具有諷刺意味的是,操作錯誤與為避免軟體錯誤的更新有關。我們有兩次糾正了由非master的副本的軟體引起的損壞。

Chubby的資料都在記憶體中,是以大部分操作的代價都很低。我們生産伺服器的中位請求延時始終保持在一毫秒以内(a small fraction of a millisecond),不管Chubby單元的負載如何,直到Chubby單元超出負荷,此時延遲大幅增加,會話掉線。超負荷通常發生在許多(> 90,000)的會話激活時,但是也能由異常條件引起:當用戶端們同時地發起幾百萬讀請求時(在4.3節有描述),或者當用戶端庫的一個錯誤禁用了某些讀的緩存,導緻每秒成千上萬的請求。因為大部分RPC是KeepAlive,伺服器可以通過延長會話租期(參考第三章)跟許多用戶端維持一個急哦較低的中位請求延時。Group commit reduces the effective work done per request when bursts of writes arrive,但是這很少見。

用戶端觀測到的RPC讀延遲受限于RPC系統和網絡;對一個本地Chubby單元而言在1毫秒以下,但跨洲則需要250毫秒。RPC寫(包括鎖操作)被資料庫的日志更新進一步地延遲了5-10毫秒,但是如果一個最近剛失敗過的用戶端要緩存該檔案的話,最高可以被延遲幾十秒。即使這種寫延遲的變化程度對伺服器上的中位請求延遲也隻有很小的影響,因為寫相當地罕見。

如果會話沒有丢棄的話,Chubby是相當不受延遲變化的影響的。在一個時間點上(At one point),我們在Open()中增加了人為的延時以抑制攻擊性的用戶端(參考第4.5節);開發人員隻會在延時超過十秒并且反複地被延時時會注意到。我們發現擴充Chubby的關鍵不是伺服器的性能;降低到伺服器端的通訊可以有遠遠大得多的作用。沒有重要的努力用在調優讀/寫相關的伺服器代碼路徑上;我們檢查了沒有極壞的缺陷(bug)存在,然後集中在更有效的擴充機制上。在另一方面,如果一個性能缺陷影響到用戶端會每秒讀幾千次的本地Chubby緩存,開發者肯定會注意到。

Google的基礎架構設施大部分是用C++寫的,但是一些正在不斷增加中的系統正在用Java編寫 [8]。這種趨勢呈現出Chubby的一個預料之外的問題,而Chubby有一個複雜的用戶端協定和不那麼簡單的(non-trival)用戶端庫。

Java通過讓它有些不厭其煩地與其他語言連接配接,以漸進采用的損耗,來鼓勵整個程式的移植性。通常的用于通路非原生(non-native)的庫的機制是JNI [15],但JNI被認為很慢并且很笨重。我們的Java程式員們是如此不喜歡JNI以至于為了避免使用JNI而傾向于将很大的庫轉換為Java,并維護它們。

Chubby的用戶端庫有7000行代碼(跟伺服器差不多),并且用戶端協定很精細。去維持這樣一個用Java寫的庫需小心和代價,同時一個沒有緩存的實作将埋葬Chubby伺服器。是以,我們的Java使用者跑着多份協定轉換伺服器,這些伺服器暴露一個很類似于Chubby用戶端API的簡單的RPC協定。即使事後回想,怎麼樣去避免編寫、運作并維護這些額外的伺服器仍然是不那麼明顯的。

盡管Chubby被設計為一種鎖服務,我們發現它的最流行的應用是做為名字伺服器。

在正常的網際網路命名系統 — DNS中,緩存是基于時間的。DNS條目有一個生存時間(TTL),DNS資料在這個時間内沒有得到重新整理的話,就将被丢棄。通常很容易選擇一個合适的TTL值,但是當希望對失敗的服務做快速替換時,這個TTL會很小,以至于使DNS伺服器超載。

例如,很常見地,我們的開發者運作有數千個程序的任務,且其中的每個程序之間都要通信,這引起了二次方級的DNS查詢。我們可能希望使用一個60秒的TTL,這将允許行為不正常的用戶端沒有過長的延遲就被換掉,同時也在我們的環境中不被認為是一個過長的替換時間。在這種情況下,為維持某一個小到3000個用戶端的任務的DNS緩存,可能需要每秒15萬次查找。(作為比照,一個2-CPU 2.6GHz Xeon DNS伺服器麼秒可能能處理5萬請求)。更大的任務産生更惡劣的問題,并且多個任務可能同時執行。在引入Chubby以前,DNS負載的波動對Google來時曾經是的一個嚴重問題。

相反,Chubby的緩存使用顯示地失效(invalidations),是以在沒有修改時,一個恒定頻率的會話KeepAlive請求能無限期地維護一個用戶端上的任意數目的緩存條目。 一個2-CPU 2.6GHz Xeon的Chubby Master 曾被見到過處理9萬個直接與它通訊(沒有Proxy)的用戶端;這些用戶端包括了具有上文描述過的那種通訊模式的龐大任務。不需要輪詢每個名字的提供快速名字更新的能力是如此吸引人,以至于現在Google的大部分系統都由Chubby提供名字服務。

盡管Chubby的緩存允許一個單獨的單元能承受大量的用戶端,負載峰值仍可能是一個問題。當我們第一次釋出基于Chubby的名字服務時,啟動了一個3千程序的任務(其結果是産生了9百萬個請求)可能将Chubby的Master壓垮。為了解決這個問題,我們選擇将名字條目組成批次,因而一次查詢将傳回同一個任務内的大量的(通常是100個)相關程序的名字映射,并緩存起來。

Chubby提供的緩存語義要比名字服務需要的緩存語義更加精确;名字解析隻要求定期的通知,而非完全的一緻性。因而,這裡有一個時機,可以通過引入特别地為名字查找設計的簡單協定轉換伺服器,來降低Chubby的負載。假如我們預見到将Chubby用作名字服務的用法,我們可能選擇實作完整的代理,而不是提供這種簡單但沒有必要的額外的伺服器。

有一種更進一步的協定轉換伺服器存在:Chubby DNS伺服器。這中伺服器将存儲在Chubby裡的命名資料提供給DNS用戶端。這種伺服器很重要,它既能減少了DNS名字到Chubby名字之間的轉換,也能适應已經存在的不能輕易轉換的應用程式,例如浏覽器。

原來的master故障切換(§2.9)的設計要求master在新的會話建立時将新會話寫入資料庫。在Berkeley DB版本的鎖伺服器中,在許多程序同時啟動時建立會話的開銷成為問題。為了避免超載,伺服器被修改為不在會話建立時儲存會話資訊到資料庫,而是在會話的第一次修改、鎖請求或者打開臨時檔案時寫入資料庫中。此外,活動會話在每次KeepAlive時以某種機率被記錄到資料庫中。這樣,隻讀的會話資訊的寫入在時間上分散開來。

盡管為了避免超載,這種優化是必要的,但它有一種負面效應:年輕的隻讀的會話可能沒有被記錄在資料庫中,因而在故障切換發生時可能被丢棄。雖然這種會話不持有鎖,這也是不安全的;如果所有已記錄的會話在被丢棄的會話的租期到期之前簽入到新的master中,被丢棄的會話可能在一段時間内讀到髒資料。這在實際中很少見,但是在大型系統中,幾乎可以肯定某些會話将簽入失敗,因而迫使新的master必須等待最大租期時間。盡管這樣,我們修改了故障切換設計,既避免這個效應,也避開目前這種模式帶給代理的複雜性。

在新的設計下,我們完全避免在資料庫中記錄會話,而是以目前master重建句柄的方式重建它們(參見§2.9,8)。一個新的master現在必須等待一個完整的最壞情況下的租期過期,才能允許操作繼續處理。因為它不能得知是否所有的會話都簽入(check in)了(參見§2.9,6)。再者,這在實際中隻有很小的影響,因為很可能不是所有的會話都會簽入。

一旦會話能在沒有在磁盤上的狀态資訊重建,代理伺服器就能管理master不知情的會話。一個額外的隻提供給代理的操作允許它們修改與鎖有關聯的會話。這允許在在代理失敗時,一個代理可從另一個代理取得一個用戶端。Master上增加的唯一必要的修改是保證不放棄與代理會話關聯的鎖或者臨時檔案句柄,直到一個新的代理有機會獲得它們。

Google的項目團隊可自由地設定它們自己的Chubby單元,但這樣做加強了他們的維護負擔,而且消耗了額外的硬體資源。許多服務是以使用共享的Chubby單元,這使得隔離行為不正常的用戶端變得很重要。Chubby是本是計劃用于在單個公司内部運作的,是以針對它的惡意的服務拒絕襲擊是很罕見的。然而,錯誤、誤解和開發者不一樣的預期都導緻了類似于襲擊的效應。

我們的某些解決方式是很粗暴的。例如,我們檢查項目團隊計劃使用Chubby的方式,并在檢查使人滿意之前拒絕其通路共享的Chubby名字空間。這種方式的一個問題是開發者經常不能預計他們的服務将來會被如何使用,以及使用量會怎麼樣增長。

我們的檢查的最重要的方面是判斷任何的Chubby的資源(RPC頻率,硬碟空間,檔案數目)是否會随着該項目的使用者數量或處理的資料量線性(或者更壞)地增長。任何線性增長必須被由一個修正參數來調低,這個修正參數可以調整為降低Chubby上負載到一個合理邊界的值。盡管如此,我們的早期檢查仍然不徹底足夠。

一個相關的問題是大部分軟體文檔中缺少性能建議。一個由某個團隊編寫的子產品,可能在一年後被另一個團隊複用,并帶來極糟糕的後果。有時很難向接口設計者解釋他們必須修改他們的接口,不是因為它們不好,而是因為其他開發者可能較少知道RPC的代價。

下面我們列出了我們遇到過的一些問題。

缺少可應付沖擊的緩存(aggressive caching) 最初,我們沒有意識到緩存不存在的檔案和重用打開的檔案句柄的關鍵性的需要。 盡管在教育訓練時進行了嘗試,我們的開發人員通常在一個檔案不存在時,仍會編寫無限重試的循環,或者通過重複地關閉/打開一個檔案來輪詢檔案(實際上隻需打開一次就可以)。

最初,我們對付這些重試循環的方法是,在某個應用程式短時間内做了許多Open()同一個檔案的嘗試時,引入指數級遞增的延時。在某些情形下,這暴露了開發者接受了的漏洞,但是通常這需要我們花更多的時間做教育訓練。最後,讓重複性的Open()調用廉價會更容易一些。

缺少限額 Chubby從沒有被計劃用做存放大量資料的存儲系統,是以沒有存儲限額。事後反思,這是很弱智的。Google的某個項目寫了一個跟蹤資料上傳的子產品,存儲了某些中繼資料到Chubby中。這種上傳很少發生,并且隻限于一小群人,是以使用的空間是有上限的。然而,兩個其他服務開始使用該子產品作為追蹤來自于多得多的使用者群的上傳的手段。不可避免地,他們的服務一直增長,直到對Chubby的使用到達極限:一個1.5M位元組的檔案在每次使用者動作時被整個重寫,被這些服務使用的空間超出了其他所有Chubby用戶端加起來的空間需求。

我們在檔案大小上引入了一個限制(256KB),并推動這些服務遷移到更合适的存儲系統上去。但要在這些由很繁忙的人們維護着的生産系統上要取得重大變化是很困難的,—-花了大約一年将這些資料遷移到其他地方。

釋出/訂閱 曾有數次将Chubby的事件機制作為Zephyr[6]式的釋出/訂閱系統的嘗試。Chubby的超重量級的保證和它在維護緩存一緻性時使用過期(invalidation)而不是更新,使得它除了無意義的訂閱/釋出示例之外,都很慢、效率很低。幸運地是,所有這樣的應用都在重新設計應用程式的成本變得太大以前被叫停(caught)。

這裡我們列出教訓和多種如果有機會的話我們可能會做出的設計變更:

開發者極少考慮可用性。 我們發現開發者極少考慮失敗的可能性,并傾向于認為像Chubby這樣的服務好像會用于可用。例如,開發者曾經建構了一個用了幾百台機器的系統,這個系統在Chubby選舉出一個新的Master時就發起一個持續幾十分鐘的恢複處理過程。這将一個單一故障的後果放大到受影響的機器數與時間的乘積的倍數。我們偏好開發者為短時間的Chubby不可用做好計劃,以便這樣的事件對他們的應用隻有很少影響,甚至沒有影響。這是粗粒度鎖定的一個有争議的地方,在第2.1節讨論了這個問題。

開發者同樣未能成功地意識到服務上線和服務對他們的應用可用的之間的差別。例如,全局Chubby單元(參見 $2.12)基本上總是線上的,因為很少會有兩個實體距離遙遠的資料中心同時下線。然後,對于某個用戶端而言,它被觀測到的可用性通常低于這個用戶端的觀測到的本地Chubby單元的可用性。首先,本地單元較少可能與用戶端之間發生網絡斷開,另外,盡管本地Chubby單元可能會因為維護操作而下線,但同樣的維護操作也會直接影響到用戶端,是以Chubby的不可用就不會被用戶端看到。

我們的API選擇同樣能影響到開發者處理Chubby不可用的方式。例如,Chubby提供了一個時間允許用戶端檢測什麼時候發生了master的故障切換。本來這是用于用戶端檢查可能的變化的,因為别的時間可能丢失了。不幸的是,許多開發選擇在收到這個事件是關閉他們的程式,是以大幅降低了他們系統的可用性。我們本來有可能通過給“檔案改變(file-change)”事件以做得更好,甚或可以保證在故障切換期間不丢失事件。

目前我們用了三種機制防止開發者對Chubby的可用性過分樂觀,特别是對全局單元(Global cell) 的可用性過分樂觀。首先,如前文提到的那樣,我們複查各個項目團隊打算如如何使用Chubby,并建議它們不要使用可能将它們的可用性與Chubby的可用性綁定的太緊密。第二,現在我們提供了執行某些高層次任務的庫,是以開發者被自動地與Chubby中斷隔離。第三,我們利用每次Chubby中斷的事後分析作為一種手段,不僅清除Chubby和運維過程中的缺陷(bugs),還降低應用程式對Chubby的可用性的敏感性 — 兩方面都帶來系統的整體上更好的可用性。

差勁的API選擇導緻了不可預料的後果 對于大部分而言,我們的API演化得很好,但是有一個錯誤很突出。我們的取消長期運作(long-running)的調用是Close()和Poison() RPC,它們也會丢棄伺服器的配置設定給對應句柄的狀态 。這阻止了能請求鎖的句柄被 共享,例如,被多個線程共享。我們可能會添加一個 Cancel() RPC以允許更多打開的句柄的共享。

RPC的使用影響了傳輸協定 KeepAlive被用于重新整理用戶端的會話租期,也被用于從master端傳遞事件和緩存過期到用戶端。這個設計有自動的符合預期的效應:一個用戶端不能在沒有應答緩存過期的情況下重新整理會話狀态。

這似乎很理想,除了這樣會在傳輸協定的選擇中引入緊張情形以外。TCP的擁塞時回退(back off)政策不關心更高層的逾時,例如Chubby的租期,是以基于TCP的KeepAlive在發生高網絡擁塞時導緻許多丢失的會話。我們被迫通過UDP而不是TCP發送KeepAlive RPC調用;UDP沒有擁塞回避機制,是以我們隻有當高層的時間界限必須滿足時才使用UDP協定。

我們可能增加一個基于TCP的GetEvent() RPC來增強傳輸協定,這個RPC将用于在正常情形下傳遞事件和過期,它與KeepAlives的方式相同。KeepAlive回複仍将包含一個未應答事件清單,因而事件最終都将被應答。

Chubby是基于長期穩定的思想(well-established ideas)之上的。Chubby的緩存設計源自于分布式檔案系統相關的成果[10]。它的會話和緩存标記(tokens)在行為上與Echo的相應部分類似[17];會話降低租期壓力與V 系統(V System)類似。放出一個一般性的(general-purpose)鎖服務的理念最早出現在VMS[23]中,盡管該系統最初使用了一個特殊目的的許可低延遲互動的高速聯網系統。 像它的緩存模型,Chubby的API是建立在檔案系統模型上的,其中包括類似于檔案系統的名字空間要比單純的檔案要友善很多的思想。 [18, 21, 22] (原文:Like its caching model, Chubby’s API is based on a file-system model, including the idea that a filesystem-like name space is convenient for more than just files [18, 21, 22].)

Chubby差別于一個像Echo或者AFS[10]這樣的檔案系統,主要表現在在它的性能和存儲意圖上:用戶端不讀、寫或存儲大量資料,并且他們不期待很高的吞吐量,甚至如果資料不緩存的話也不預期低延時。它們卻期待一緻性,可用性和可靠性,但是這些屬性在性能不那麼重要是比較容易達成。因為Chubby的資料庫很小,我們能線上存儲它的多個拷貝(通常是五個副本和少數備份)。我們每天做很多次完整備份,并通過資料庫狀态的校驗碼(checksums),我們每隔幾個小時互相比較副本。對正常檔案系統的性能和存儲需求的弱化(weakening)允許我們用一個Chubby master服務數千個用戶端。我們通過提供一個中心點,許多用戶端在這個中心點共享資訊和協同活動,解決了一類我們的系統開發人員面對的問題。

各種文獻(literature)描述了大量的檔案系統和鎖伺服器,是以不可能做一個徹底的比較。于是我們選擇了其中一個做詳細比較:我們選擇與Boxwood的鎖服務16比較,因它是最近設計的,并且設計為運作在松耦合環境下,另外它的設計在多個方面與Chubby不同,某些很有趣,某些是本質上的。(原文:some interesting and some incidental)

Chubby 實作了鎖,一個可靠的小檔案存儲系統,以及在單個服務中的會話/租期機制。相反,Boxwood将這些劃分為三部分:鎖服務,Paxos服務(一個可靠的狀态存儲庫),以及對應的失敗檢測服務。 Boxwood系統它本身使用了這三個元件,但是另一個系統可能獨立地使用這些建構塊。我們懷疑設計上的這個差別源自不同的目标客戶群體(target audience)。Chubby被計劃用于多種多樣的目标客戶(audience) 和應用的混合體, 它的使用者有建立新分布式系統的專家,也有編寫管理腳本的新手。對于我們的環境,一個提供熟知的API的大規模(large-scale)的共享服務似乎更有吸引力。相反,Boxwood提供了一個工具包(至少我們看到的是這樣),這個工具包适合于一小部分經驗豐富、老練的開發者用在那些可能共享代碼但不需要一起使用的項目中。

許多檔案被用于命名(naming),參見第$4.3節。

參數配置,通路控制和中繼資料檔案(類似于檔案系統的super-blocks)很普遍。

Negative Caching很突出。

平均來看,每個緩存檔案由230k/24k≈10個用戶端使用。

較少的用戶端持有鎖,共享鎖很少;這與鎖定被用于primary選舉和在多個副本之間劃分資料的預期是相符合的。

RPC流量主要是會話 KeepAlive,有少量的讀(緩存未命中引起),隻有很少的寫或鎖請求。

ACL 本身是檔案,是以一個分區可能會使用另一個分區做權限校驗。 然後, ACL 檔案可以很快捷地緩存;隻有Open() 和 Delete() 操作需要做 ACL 核查;并且大部分用戶端讀公開可通路的、不需要ACL的檔案。

當一個目錄被删除時,一個跨分區的調用可能被需要以確定這個目錄是空的。

我們可以建立任意數量的Chubby單元;用戶端幾乎總是使用一個鄰近的單元(通過DNS找出)以避免對遠端機器的依賴。我們的有代表性的釋出讓一個有幾千台機器的資料中心使用一個Chubby單元。

Master可能會在負載很重時将租期(lease times)從預設的12秒到延長到最大60秒左右,這樣它就隻需要處理更少的KeepAlive RPC調用。(KeepAlive是到目前為止的占統治地位的請求類型(參考第4.1節),并且未能及時處理它們是一個超負荷的伺服器的代表性的失敗模式;用戶端對其他調用中的延遲變化相當地不敏感。)

Chubby用戶端緩存檔案資料,中繼資料,檔案缺失(the absence of files)和打開的句柄,以減少它們在伺服器上做的調用。

我們使用轉換Chubby協定為不那麼複雜協定如DNS等的協定轉換伺服器。我們後文描述其中的一些。

提供一個回調(callback)以使調用以異步方式執行。

等待調用的結束,和/或

取得展開的錯誤和診斷資訊。