文章目錄
摘要
我們描述了我們使用Chubby鎖服務的經驗,該服務旨在爲鬆散耦合的分佈式系統提供粗粒度鎖以及可靠(儘管低容量)存儲。Chubby提供的接口很像帶有諮詢鎖的分佈式文件系統,但設計重點在於可用性和可靠性,而不是高性能。該服務的許多實例已使用了一年多,其中幾個實例每個同時處理數萬個客戶端。本文描述了初始設計和預期用途,將其與實際使用進行了比較,並解釋瞭如何修改設計以適應差異。
1. 簡介
本文描述了一個名爲Chubby的鎖服務。它適用於鬆散耦合的分佈式系統,該系統由大量通過高速網絡連接的小型機器組成。例如,一個Chubby實例(也稱爲Chubby單元)可能服務於通過1Gbit/s以太網連接的一萬臺4核處理器機器。大多數Chubby單元被限制在一個數據中心或機房,但我們確實運行了至少一個Chubby單元,其副本相隔數千公里。
鎖服務的目的是允許其客戶端同步他們的活動並就其環境的基本信息達成一致。主要目標包括可靠性,適用於大量客戶端的可用性以及易於理解的語義;吞吐量和存儲容量被認爲是次要的。Chubby的客戶端接口類似於執行整個文件讀取和寫入的簡單文件系統,通過諮詢鎖和文件修改等各種事件的通知進行擴展。
我們期望Chubby幫助開發者處理他們系統中的粗粒度同步,特別是處理從一組對等服務器中選舉領導者的問題。例如,Google File System
[7]使用Chubby鎖來指定GFS主服務器,Bigtable[3]以多種方式使用Chubby:選擇主服務器、使主服務器發現它控制的服務器、以及允許客戶端找到主服務器。此外,GFS和Bigtable都使用Chubby作爲存儲衆所周知且可用的少量元數據的位置;實際上,他們使用Chubby作爲其分佈式數據結構的根。某些服務使用鎖來對多個服務器之間的工作進行分區(粗粒度的)。
在部署Chubby之前,Google的大多數分佈式系統都使用臨時方法進行初步選舉(當工作可以重複而不會造成損害時),或者需要操作員干預(當正確性十分必要時)。在前一種情況下,Chubby可以節省大量的計算工作量。在後一種情況下,它在不需要人爲干預系統失敗中實現了可用性的顯著改善。
熟悉分佈式計算的讀者會認識到在對等體中選擇一個主作爲分佈式共識問題的實例,並且意識到我們需要使用異步通信的解決方案;該術語描述了絕大多數真實網絡的行爲,例如以太網或因特網,它們允許數據包丟失、延遲和重新排序。(實踐者通常應該注意基於對環境做出更強假設的模型的協議。)Paxos協議[12,13]解決了異步共識。Oki和Liskov使用了相同的協議(參見他們關於viewstamped replication
[19,§4]的論文),其他人關注到的等價協議[14,§6]。實際上,到目前爲止我們遇到的異步共識的所有工作協議都以Paxos爲核心。Paxos在沒有時間假設的情況下保持安全,但必須引入時鐘以確保活躍;這克服了Fischer等人的不可能性結果[5,§1]。
構建Chubby是滿足上述需求要求的工程努力;這不是研究。我們聲稱沒有新的算法或技術。本文的目的是描述我們所做的及爲什麼,而不是爲它辯護。在接下來的部分中,我們將介紹Chubby的設計和實現,以及如何根據經驗對其進行更改。我們描述了使用Chubby的意想不到的方式,以及被證明是錯誤的功能。我們省略了其他文獻中已涵蓋的細節,例如共識協議或RPC系統的詳細信息。
2. 設計
2.1 理由
有人可能會說我們應該建立一個體現Paxos的庫,而不是一個訪問中心鎖服務的庫,甚至是一個高可靠的庫。客戶端Paxos庫將不依賴於其他服務器(除了名稱服務),並且將爲程序員提供標準框架,假設他們的服務可以作爲狀態機實現。實際上,我們提供了一個獨立於Chubby的客戶端庫。
然而,鎖服務比客戶端庫具有一些優勢。首先,我們的開發人員有時不會按照人們的意願設計高可用性。通常,他們的系統從原型開始,負載很小,寬鬆的可用性保障;代碼總是沒有爲共識協議特別構造。隨着服務的成熟和客戶端的增加,可用性變得更加重要;然後將副本和主選舉添加到現有設計中。雖然這可以通過提供分佈式共識的庫來完成,但是鎖服務可以更容易地維護現有的程序結構和通信模式。例如,要選擇一個主服務器然後寫入現有文件服務器,只需要向現有系統添加兩條語句和一個RPC參數:一個將獲得鎖成爲主服務器,傳遞一個額外的整數(鎖獲取計數)寫入RPC,並向文件服務器添加if語句,以便在獲取計數低於當前值時拒絕寫入(以防止延遲數據包)。我們發現這種技術比使現有服務器參與共識協議更容易,尤其是如果在過渡期間必須保持兼容性的話。
其次,我們的許多服務在其組件之間選擇主或分區數據需要一種機制來廣播結果。這表明我們應該允許客戶端存儲和獲取少量數據 - 即讀取和寫入小文件。這可以通過名稱服務來完成,但我們的經驗是鎖服務本身非常適合這項任務,因爲這減少了客戶端所依賴的服務數量,並且因爲協議的一致性特徵是共享。Chubby作爲名稱服務器的成功很大程度上歸功於它使用一致的客戶端緩存,而不是基於時間的緩存。特別是,我們發現開發人員非常認同不必選擇緩存超時,例如DNS生存時間值,如果選擇不當,可能導致高DNS負載或長客戶端故障恢復時間。
第三,我們的程序員更熟悉基於鎖的接口。Paxos的複製狀態機和與獨佔鎖相關的臨界區都可以爲程序員提供順序編程的假象。然而,許多程序員以前遇到過鎖,並認爲他們知道使用它們。具有諷刺意味的是,這些程序員通常是錯誤的,特別是當他們在分佈式系統中使用鎖時; 很少人考慮具有異步通信的系統中獨立機器故障對鎖的影響。然而,對鎖的明顯地熟悉克服了說服程序員使用可靠機制進行分佈式決策的障礙。
最後,分佈式一致性算法使用仲裁來做出決策,因此他們使用多個副本來實現高可用性。例如,Chubby本身通常在每個單元中有五個副本,其中三個必須運行才能使單元運行起來。相反,如果客戶端系統使用鎖服務,即使是單個客戶端也可以獲得鎖並安全地進行。因此,鎖服務減少了可靠客戶端系統運行所需的服務器數量。從寬鬆的意義上講,人們可以將鎖服務視爲提供通用選舉的一種方式,允許客戶端系統在少於其大多數成員的情況下正確地做出決策。可以想象以不同的方式解決這個最後的問題:通過提供“共識服務”,使用多個服務器來提供Paxos協議中的“受主”。與鎖服務一樣,即使只有一個活躍的客戶端進程,共識服務也可以讓客戶端安全地運行;類似的技術已用於減少拜占庭容錯[24]所需的狀態機數量。但是,假設共識服務不是專門用於提供鎖(將其減少爲鎖服務),則該方法不會解決上述任何其他問題。
這些論點提出了兩個關鍵設計決策:
- 我們選擇了鎖服務,而不是庫或者共識服務,以及
- 我們選擇提供小文件,以允許當選的主服務廣播自己及其參數,而不是建立和維護另一個服務。
來自我們的預期用途和環境的一些決策:
- 一個通過Chubby文件廣播其主的服務可能有數千個客戶端。因此,我們必須允許數千個客戶端查看此文件,最好不需要很多服務器。
- 複製的服務的副本和客戶端可能希望知道該服務的主的更改時間。這表明事件通知機制對於避免輪詢很有用。
- 即使客戶端不需要定期輪詢文件,許多人也會這樣做; 這是支持許多開發人員後得出的結論。因此,需要緩存文件。
- 我們的開發人員對非直觀的緩存語義感到困惑,所以我們更喜歡一致的緩存。
- 爲了避免經濟損失和監禁時間,我們提供安全機制,包括訪問控制。
一個可能讓一些讀者感到驚訝的選擇是我們不希望鎖使用是細粒度的,它們可能只持續很短的持續時間(秒或更短);相反,我們期望粗粒度使用。例如,應用可能會使用鎖來選擇主,然後該主將在相當長的時間內(可能是數小時或數天)處理對該數據的所有訪問。這兩種使用方式表明了鎖服務器的不同要求。
粗粒度鎖對鎖服務器的負載要小得多。特別是,鎖獲取率通常僅與客戶端應用程序的事務率弱相關。粗粒度鎖很少被獲取,因此鎖服務器短時不可用很少會延遲客戶端。另一方面,從客戶端之間的鎖轉移可能需要高昂的恢復過程,因此不希望鎖服務器的故障恢復導致鎖丟失。因此,粗粒度鎖可以在鎖服務器故障時很好地存活,對這樣做的開銷幾乎不用關心,並且這種鎖允許許多客戶端由適度數量的可用性稍低的鎖服務器充分服務。
細粒度鎖導致不同的結論。即使鎖服務器短暫不可用也可能導致許多客戶端停止運行。性能和隨意添加新服務器的能力非常值得關注,因爲鎖服務的事務率隨着客戶端的合計事務率而增長。通過在鎖定服務器故障期間不維持鎖來減少鎖的開銷是有利的,並且每隔一段時間丟失鎖的時間損失並不嚴重,因爲鎖被短時間保持。(客戶端必須準備好在網絡分區期間丟失鎖,因此鎖服務器故障恢復時的鎖丟失不會引入新的恢復路徑。)
Chubby旨在僅提供粗粒度鎖。幸運的是,客戶端可以直接實現針對其應用量身定製的細粒度鎖。應用可能會將其鎖分組,並使用Chubby的粗粒度鎖將這些分組鎖分配給應用特定的鎖服務器。維持這些細粒度鎖需要很少的狀態;服務器只需保留一個很少更新的非易失性、單調遞增的獲取計數器。客戶端可以在釋放鎖時獲悉丟失的鎖,如果使用簡單的固定長度租約,則協議可以簡單有效。該方案最重要的好處是我們的客戶端開發人員負責支持其負載所需的服務器,但卻免除了實現共識本身的複雜性。
2.2 系統架構
Chubby有兩個主要組件通過RPC進行通信:服務器和客戶端應用鏈接的庫;請參見圖1。Chubby客戶端與服務器之間的所有通信都由客戶端庫作爲中介。一個可選的第三個組件,即代理服務器,將在3.1節中討論。
一個Chubby單元由一小組稱爲副本的服務器(通常爲五個)組成,副本放置以減少關聯故障(例如,在不同的機架中)的可能性。副本使用分佈式共識協議來選舉一個主;主必須獲得大多數副本的投票,以及承諾幾秒鐘間隔內(稱爲主租約)不會選舉不同的主。如果主繼續贏得大部分投票,主租約將由副本定期更新。
副本維護一個簡單數據庫的副本,但只有主啓動對此數據庫的讀寫操作。所有其他副本只需從主服務器複製使用共識協議發送的更新。
客戶端通過將主位置請求發送到DNS中列出的副本來查找主服務器。非主服務器副本通過返回主服務器的標識來響應此類請求。一旦客戶端找到了主服務器,客戶端就會將所有請求定向到它,直到它停止響應,或直到它表示它不再是主服務器。寫請求通過共識協議傳播到所有副本; 當寫入到達單元中的大多數副本時,將確認此類請求。只有主服務器才能滿足讀取請求;如果主租約尚未到期,這是安全的,因爲沒有其他主服務器可能存在。如果主服務器發生故障,則其他副本在主租約到期時運行選舉協議;通常會在幾秒鐘內選出一個新的主。例如,最近的兩次選舉分別爲6s和4s,但我們看到的值高達30s(§4.1)。
如果一個副本失敗並且幾小時內沒有恢復,則簡單的替換系統會從空閒池中選擇一臺新計算機並在其上啓動鎖服務器二進制文件。然後,它會更新DNS表,將故障副本的IP地址替換爲新副本的IP地址。當前主服務器定期輪詢DNS並最終注意到更改。然後它更新單元數據庫中單元成員的列表;此列表通過常規復制協議在所有成員之間保持一致。在此期間,新副本從存儲在文件服務器上的備份和來自活躍副本的更新的組合中獲取數據庫的最新副本。一旦新副本處理了當前主節點等待提交的請求,則允許副本在新主節點的選舉中投票。
2.3 文件,目錄和句柄
Chubby導出一個類似於UNIX[22]但比UNIX更簡單的文件系統接口。它以通常方式由嚴格的文件和目錄樹組成,名稱組件由斜槓分隔。典型的名稱是:
/ls/foo/wombat/pouch
ls
前綴對所有Chubby名稱都是通用的,代表鎖服務。第二個組件(foo)是Chubby單元的名稱;它通過DNS查找解析爲一個或多個Chubby服務器。一個特殊的單元名稱local表示應該使用客戶端的本地Chubby單元;這通常是同一建築物中的一個,因此也是最容易訪問到的一個。名稱/wombat/pouch
的其餘部分在對應名稱的Chubby單元中進行解釋。和UNIX一樣,每個目錄都包含子文件和目錄的列表,而每個文件包含一系列未解釋的字節。
由於Chubby的命名結構類似於文件系統,因此我們能夠通過其自己的專用API以及我們的其他文件系統(如Google File System)使用的接口將其提供給應用程序。這大大減少了編寫基本瀏覽和名稱空間操作工具所需的工作量,並減少了教育非正式Chubby用戶的需要。
該設計與UNIX的不同之處在於易於分發。爲了允許不同目錄中的文件從不同的Chubby主服務器提供服務,我們不暴露可以將文件從一個目錄移動到另一個目錄的操作、我們不維護目錄修改時間、並且我們避免了路徑相關的權限語義(即,對文件的訪問權限由文件本身的權限控制,而不是由通向文件的路徑上的目錄控制)。爲了更容易緩存文件元數據,系統不會顯示上次訪問時間。
名稱空間僅包含文件和目錄,統稱爲節點。每個這樣的節點在其單元中只有一個名稱;沒有符號或硬鏈接。
節點可以是永久性的,也可以是暫時性的。可以顯式刪除任何節點,但如果暫時性節點沒有客戶端打開它們,則也會被刪除(對於目錄,它們是空的)。暫時性文件用作臨時文件,並作爲客戶端對其他方活着的標示符。任何節點都可以充當諮詢讀/寫鎖;這些鎖在2.4節中有更詳細的描述。
每個節點都有各種元數據,包括用於控制讀取、寫入和更改節點的ACL名稱的三個訪問控制列表(ACLs)名稱。除非被覆蓋,否則節點在創建時會繼承其父目錄的ACL名稱。ACL本身就是位於ACL目錄中的文件,ACL目錄是單元本地名稱空間的一個衆所周知的部分。這些ACL文件由主體名稱的簡單列表組成;讀者可能會想起計劃9的團隊[21]。因此,如果文件F的寫入ACL名稱爲foo,並且ACL目錄包括含有條目bar的文件foo,則允許用戶bar寫入F。用戶通過RPC系統內置的機制進行身份驗證。由於Chubby的ACL是簡單文件,因此它們自動對希望使用類似訪問控制機制的其他服務可用。
每個節點的元數據包括四個單調遞增的64位數字,允許客戶端輕鬆檢測更改:
- 實例編號;大於具有相同名稱的任何先前節點的實例編號。
- 內容世代編號(僅限文件);寫入文件內容時會增加。
- 鎖世代編號;當節點的鎖從空閒轉換爲持有時,這會增加。
- ACL世代編號;當寫入節點的ACL名稱時,這會增加。
Chubby還暴露了64位文件內容校驗和,因此客戶端可以判斷文件是否不同。
客戶端打開節點以獲取類似於UNIX文件描述符的句柄。句柄包括:
- 檢查編號數字以阻止客戶端創建或猜測句柄,因此只有在創建句柄時才需要執行完全訪問控制檢查(與UNIX比較,UNIX會在打開時檢查其權限位,但不會在每次讀/寫時檢查,因爲文件描述符不能僞造)。
- 一個序列編號,允許主服務器判斷句柄是由它還是由前一個主服務器生成的。
- 在打開時提供的模式信息,以允許主服務器在向新重新啓動的主服務器提供舊句柄時重新創建其狀態。
2.4 鎖和序列生成器
每個Chubby文件和目錄都可以充當讀寫鎖:一個客戶端句柄可以以獨佔(寫入)模式保持鎖,或者任何數量的客戶端句柄都可以將鎖保持在共享(讀取器)模式。像大多數程序員所知的互斥鎖一樣,鎖是建議性的。也就是說,它們只與獲取相同鎖的其他嘗試衝突:持有一個名爲F的鎖既不需要訪問文件F,也不能阻止其他客戶端這樣做。我們拒絕強制鎖,這使得沒有鎖的客戶端無法訪問鎖定的對象:
- Chubby鎖通常保護由其他服務實現的資源,而不僅僅是與鎖相關聯的文件。要以有意義的方式強制執行強制鎖,這要求我們對這些服務進行更廣泛的修改。
- 我們不希望強制用戶在需要訪問鎖定文件以進行調試或管理時關閉應用。在複雜的系統中,使用大多數個人計算機上採用的方法更加困難,因爲個人計算機上管理軟件只需指示用戶關閉其應用或重新啓動即可破壞強制鎖。
- 我們的開發人員通過編寫諸如“鎖X被持有”之類的斷言的傳統方式執行錯誤檢查,因此它們從強制檢查中獲益很少。當沒有鎖時,Buggy或惡意進程有很多機會破壞數據,因此我們發現強制鎖提供的額外防護沒有重要價值。
在Chubby中,在任一模式下獲取鎖都需要寫入權限,因此無權限的讀無法阻止寫入者運行。
鎖在分佈式系統中很複雜,因爲通信通常是不確定的,並且進程可能獨立地失敗。因此,持有鎖L的進程可以發出請求R,但隨後失敗。另一個進程可以獲取L並在R到達其目的地之前執行一些操作。如果R稍後到達,則可以在沒有L保護的情況下對其進行操作,並且可能在不一致的數據上進行操作。無序接收消息的問題已得到很好的研究;解決方案包括虛擬時間[11]和虛擬同步[1],它通過確保按照與每個參與者的觀察一致的順序處理消息來避免問題。
將序列編號引入現有複雜系統中的所有交互中是很昂貴的。相反,Chubby提供了一種方法,通過該方法可以將序列號引入僅使用鎖的那些交互中。在任何時候,鎖持有者都可以請求序列發生器,這是一個不透明的字節串,用於在獲取後立即描述鎖的狀態。它包含鎖的名稱,獲取它的模式(獨佔或共享)以及鎖生成編號。如果客戶端期望通過鎖保護操作,則客戶端將序列生成器傳遞給服務器(例如文件服務器)。接收服務器應測試序列生成器是否仍然有效並具有適當的模式;如果沒有,它應該拒絕該請求。可以針對服務器的Chubby緩存檢查序列生成器的有效性,或者,如果服務器不希望與Chubby保持會話,則針對服務器觀察到的最新序列生成器。序列生成器機制只需要在受影響的消息中添加一個字符串,並且很容易向我們的開發人員解釋。
雖然我們發現序列發生器易於使用,但重要的協議發展緩慢。因此,Chubby提供了一種不完善但更容易的機制,可以降低對不支持序列生成器的服務器的延遲或重新排序請求的風險。如果客戶端以正常方式釋放鎖,則可以立即對其他客戶端聲明可用,正如人們所期望的那樣。但是,如果鎖因爲持有者失敗或無法訪問而變爲空閒,則鎖服務器將阻止其他客戶端在稱爲鎖延遲的時段內要求鎖。客戶端可以指定任意鎖延遲,直到某個邊界,目前爲一分鐘;此限制可防止有故障的客戶端在任意長時間內使鎖(因而導致某些資源)不可用。雖然不完美,但鎖延遲可保護未經修改的服務器和客戶端免受因消息延遲和重新啓動而導致的日常問題。
2.5 事件
Chubby客戶端在創建句柄時可以訂閱一系列事件。這些事件通過Chubby庫的一個向上調用來異步傳遞給客戶端。事件包括:
- 修改的文件內容 - 通常用於監視通過文件通知的服務的位置。
- 添加,刪除或修改子節點 - 用於實現鏡像(§2.12)。(除了允許發現新文件之外,爲子節點返回事件還可以監視臨時文件,而不會影響其引用計數。)
- Chubby主服務器故障 - 警告客戶端其他事件可能已丟失,因此必須重新掃描數據。
- 句柄(及其鎖)已變爲無效 - 這通常表明存在通信問題。
- 獲得鎖 - 可用於確定選舉主的時間。
- 來自另一個客戶端的衝突鎖請求 - 允許緩存鎖。
事件在相應的操作發生後傳遞。因此,如果通知客戶端文件內容已經改變,則保證在隨後讀取文件時看到新數據(或者更新的數據)。
提到的最後兩個事件很少使用,且事後可能會被省略。例如,在主選舉之後,客戶端通常需要與新的主通信,而不是簡單地知道存在主;因此,它們等待文件修改事件,指示新主節點將其地址寫入文件中。理論上,衝突鎖事件允許客戶端緩存其他服務器上保存的數據,使用Chubby鎖來維護緩存一致性。衝突鎖請求的通知將告訴客戶端完成使用與鎖相關聯的數據:它將完成掛起操作,刷新對起始位置的修改,丟棄緩存數據和釋放鎖。到目前爲止,還沒有人使用這種方式。
2.6 API
客戶端將Chubby句柄視爲指向支持各種操作的不透明結構的指針。句柄僅由Open()創建,並使用Close()銷燬。
Open()打開命名文件或目錄以生成句柄,類似於UNIX文件描述符。只有這個調用需要一個節點名稱;所有其他調用都在句柄上操作。
相對於現有目錄句柄評估名稱;該庫提供始終有效的“/”句柄。目錄句柄避免了在包含多層抽象[18]的多線程程序中使用程序範圍內當前目錄的困難。
客戶端顯示各種選項:
- 如何使用句柄(讀取;寫入和鎖;更改ACL); 僅當客戶端具有適當的權限時才能創建句柄。
- 應該傳遞的事件(見§2.5)。
- 鎖延遲(§2.4)。
- 是否應該(或必須)創建新文件或目錄。如果創建了文件,則調用者可以提供初始內容和初始ACL名稱。返回值表示文件是否實際創建。
Close()關閉一個打開的句柄。不允許進一步使用該句柄。這個調用永遠不會失敗。相關的調用Poison()會導致句柄上的未完成和後續操作失敗而沒有關閉它;這允許客戶端取消其他線程發出的Chubby調用,而不必擔心釋放他們正在訪問的內存。
作用於句柄的主要調用有:
- GetContentsAndStat()返回文件的內容和元數據。文件的內容以原子方式和整體讀取。我們避免了部分讀取和寫入以阻止大文件。相關調用GetStat()僅返回元數據,而ReadDir()返回目錄子項的名稱和元數據。
- SetContents()寫入文件的內容。可選地,客戶端可以提供內容生成編號以允許客戶端模擬文件的compare-and-swap操作;僅當生成編號爲當前時才更改內容。文件的內容始終以原子方式整體寫入。相關調用SetACL()對與節點關聯的ACL名稱執行類似的操作。
- 如果節點沒有子節點,Delete()將刪除該節點。
- Acquire(),TryAcquire(),Release()獲取及釋放鎖。
- GetSequencer()返回一個序列生成器(§2.4),它描述了該句柄持有的任何鎖。
- SetSequencer()將序列生成器與句柄相關聯。 如果該序列生成器不再有效,則對句柄的後續操作將失敗。
- CheckSequencer()檢查序列生成器是否有效(參見§2.4)。
如果創建句柄後節點已被刪除,則調用失敗,即使文件隨後已重新創建。也就是說,句柄與文件的實例相關聯,而不是與文件名相關聯。Chubby可以對任何調用應用訪問控制檢查,但會始終檢查Open()調用(參見§2.3)。
除了調用本身所需的任何其他參數外,上述所有調用都需要一個操作參數。操作參數保存可能與任何調用相關聯的數據和控制信息。特別是,通過操作參數,客戶端可以:
- 提供回調以使調用異步,
- 等待該調用完成,和/或
- 獲取擴展錯誤和診斷信息。
客戶端可以使用此API執行主選舉,如下所示:所有潛在的主打開鎖文件並嘗試獲取鎖。其中一個成功併成爲主,而其他成爲副本。主使用SetContents()將其標識寫入鎖文件,以便客戶端和副本可以使用GetContentsAndStat()讀取文件,可能是爲了響應文件修改事件(§2.5)。理想情況下,主使用GetSequencer()獲取一個序列生成器,然後將其傳遞給與之通信的服務器;他們應該使用CheckSequencer()確認它仍然是主。鎖延遲可以與無法檢查序列生成器的服務一起使用(§2.4)。
2.7 緩存
爲了減少讀取流量,Chubby客戶端將保持一致地緩存文件數據和節點元數據(包括文件缺失),直接寫入的緩存緩存在內存中。緩存由下面描述的租約機制維護,並通過主服務器發送的失效保持一致,其保存每個客戶端可能緩存的內容的列表。該協議可確保客戶端看到Chubby狀態的一致視圖或錯誤。
當要更改文件數據或元數據時,修改將被阻塞,而主服務器會將數據的失效發送給可能已緩存它的每個客戶端;此機制基於KeepAlive RPC之上,將在下一節中進行更全面的討論。收到失效後,客戶端會刷新無效狀態,並通過進行下一次KeepAlive調用進行確認。只有在服務器知道每個客戶端已使其緩存失效之後,纔會進行修改,因爲客戶端確認了失效,或者因爲客戶端允許其緩存租約過期。
只需要進行一輪失效,因爲主節點將節點視爲不可訪問,而緩存失效仍未被確認。這種方法允許始終無延遲地處理讀取;這很有用,因爲讀取次數大大多於寫入次數。另一種方法是阻止在失效期間訪問節點的調用;這將使得過熱的客戶端在失效期間使用未緩存的訪問來轟炸主服務器的可能性降低,這是以偶爾的延遲爲代價的。如果這是一個問題,可以考慮採用混合方案,如果檢測到過載,則切換策略。
緩存協議很簡單:它使更改中的緩存數據失效,並且永遠不更新它。更新而不是使失效更簡單,但僅更新協議可能是任意低效的;訪問文件的客戶端可能無限期地接收更新,從而導致無限數量的不必要更新。
儘管提供嚴格一致性的開銷很大,我們拒絕較弱的模型,因爲我們覺得程序員會發現它們更難使用。類似地,在具有各種預先存在的通信協議的環境中,諸如虛擬同步之類的機制被要求客戶端在所有消息中交換序列號被認爲是不合適的。
除了緩存數據和元數據,Chubby客戶端還緩存打開的句柄。因此,如果客戶端打開之前已打開的文件,則只有第一個Open()調用有必要導致到主服務器的RPC。這種緩存限制在很小的一些方面,因此它永遠不會影響客戶端觀察到的語義:如果應用關閉了臨時文件,則臨時文件的句柄不能保持打開;允許鎖的句柄可以重用,但不能由多個應用句柄同時使用。最後一個限制是因爲客戶端可能使用Close()或Poison()來消除對主服務器的未完成Acquire()調用的副作用。
Chubby的協議允許客戶端緩存鎖 - 也就是說,保持鎖的時間超過嚴格必要的時間,希望它們可以被同一個客戶端再次使用。如果另一個客戶端請求了衝突鎖,則一個事件通知鎖持有者,讓持有者在其他地方需要時釋放鎖(參見§2.5)。
2.8 會話和KeepAlives
Chubby會話是Chubby單元和Chubby客戶端之間的關聯關係;它存在一段時間,並由稱爲KeepAlives的週期性握手維持。除非Chubby客戶端以其他方式通知主服務器,否則客戶端的句柄、鎖和緩存數據都保持有效,前提是其會話保持有效。(但是,會話保持協議可能要求客戶端確認緩存失效以獲取其會話;請參閱下文。)
客戶端在首次聯繫Chubby單元的主節點時請求新會話。它會在它終止時或者會話已經空閒時(沒有打開句柄且一分鐘內沒有調用)顯式結束會話。
每個會話都有一個相關的租約 - 一個延伸到未來的時間間隔,在此期間主服務器保證不會單方面終止會話。此間隔的結束稱爲會話租約超時。此間隔的結束稱爲會話租約超時。主服務器可以在將來進一步延長此超時,但可能不會及時向後移動此超時。
主服務器在三種情況下延長租約超時:在創建會話時、發生主服務器故障恢復時(見下文)、以及響應從客戶端發送的KeepAlive RPC時。收到KeepAlive後,主服務器通常會阻止RPC(不允許它返回),直到客戶端的上一個租約間隔接近到期爲止。主服務器稍後允許RPC返回到客戶端,從而通知客戶端新的租約超時。主服務器可以將超時延長到任意值。默認延長爲12秒,但過載的主服務器可能會使用更高的值來減少必須處理的KeepAlive調用的數量。客戶端在收到上一個回覆後立即啓動新的KeepAlive。因此,客戶端確保幾乎總是有一個KeepAlive調用阻塞在主服務器上。
除了延長客戶端的租約外,KeepAlive回覆還用於將事件和緩存失效傳回客戶端。主服務器允許KeepAlive在要發送事件或失效時提前返回。KeepAlive回覆時的捎帶事件可確保客戶端無法在不確認緩存失效的情況下維持會話,並導致所有Chubby RPC從客戶端流向主服務器。這簡化了客戶端,並允許協議經由允許僅在一個方向上啓動連接的防火牆進行操作。
客戶端維護本地租約超時,這是主服務器租約超時的保守近似值。它與主服務器的租約超時不同,因爲客戶端必須同時考慮其KeepAlive回覆傳輸中花費的時間和主服務器時鐘前進的速率的時間從而做出保守的假設;爲了保持一致性,我們要求服務器的時鐘前進速度不超過客戶端的已知常數因子。
如果客戶端的本地租約超時到期,則無法確定主服務器是否已終止其會話。客戶端清空並禁用其緩存,我們說它的會話處於危險之中。客戶端等待另一個稱爲寬限期的間隔,默認爲45秒。如果客戶端和主服務器在客戶端寬限期結束之前設法交換成功的KeepAlive,則客戶端將再次啓用其緩存。否則,客戶端假定會話已過期。這樣做是爲了當Chubby單元變得不可訪問時,Chubby API調用不會無限期地阻塞;如果在重新建立通信之前寬限期結束,則調用返回錯誤。
當寬限期從危險事件開始時,Chubby庫可以通知應用。當已知會話倖免於通信問題時,安全事件會告知客戶端繼續;反之如果會話超時,則發送過期事件。此信息允許應用在不確定其會話狀態時自行停頓,並且如果問題證明是暫時的,則無需重新啓動即可恢復。這對於避免具有很大啓動開銷的服務中斷非常重要。
如果客戶端在節點上持有句柄H並且H上的任何操作因相關會話已過期而失敗,則H上的所有後續操作(除了Close()和Poison())將以相同的方式失敗。客戶端可以使用它來保證網絡和服務中斷僅導致丟失後續一系列操作,而不是任意子序列,從而允許將複雜的更改標記爲通過最終寫入的提交。
2.9 故障恢復
當主服務器失敗或以其他方式失去主控權時,它會丟棄其關於會話、句柄和鎖的內存中狀態。會話租約的權威計時器在主服務器上運行,因此在選擇新的主服務器之前,會話租約計時器被停止;這是合法的,因爲它相當於延長客戶的租約。如果主服務器選舉很快發生,客戶端可以在其本地(近似)租約計時器到期之前聯繫新主服務器。如果選舉需要很長時間,客戶端會在嘗試查找新主服務器時刷新緩存並等待寬限期。因此,寬限期允許超過正常租約超時的跨故障恢復維護會話。
圖2顯示了冗長的主故障恢復事件中的事件序列,其中客戶端必須使用其寬限期來保留其會話。時間從左到右增加,但倍數不增加。客戶端會話租約顯示爲粗箭頭,由新舊主服務器(上面的M1-3)和客戶端(下面的C1-3)查看。向上傾斜的箭頭表示KeepAlive請求,向下傾斜的箭頭表示其回覆。原始主服務器具有給客戶端的會話租約M1,而客戶端具有保守的近似值C1。在通過KeepAlive應答2通知客戶端之前,主服務器具有租約M2;客戶端能夠擴展其對租約C2的視圖。主服務器在回覆下一個KeepAlive之前就已經死了,並且在選出另一個主服務器之前已經過了一段時間。最終,客戶端對其租約(C2)的近似值到期。然後,客戶端刷新其緩存並啓動寬限期的計時器。
在此期間,客戶端無法確定其租約是否已在主服務器上過期。它不會銷燬其會話,但會阻止其API上的所有應用調用,以防止應用觀察到不一致的數據。在寬限期開始時,Chubby庫嚮應用發送危險事件,以允許其自行停止,直到可以確定其會話的狀態。
最終一個新的主服務器選舉成功。主服務器最初使用其前任可能爲客戶端提供的會話租約的保守近似M3。從客戶端到新主服務器的第一個KeepAlive請求(4)被拒絕,因爲它具有錯誤的主服務器時世代號(在下面詳細描述)。重試的請求(6)成功了但通常不會進一步延長主服務器租約因爲M3是保守的。但是,回覆(7)允許客戶端再次延長其租約(C3),並且可選地通知應用其會話不再處於危險之中。由於寬限期足以覆蓋租約C2結束與租約C3開始之間的間隔,客戶端只看到延遲。如果寬限期小於該間隔,則客戶端將放棄會話並嚮應用報告失敗。
一旦客戶端聯繫到新主服務器,客戶端庫和主服務器就會合作,爲應用提供沒有發生故障的錯覺。爲了實現這一點,新的主服務器必須重建前一個主服務器所具有的內存狀態的保守近似。它部分通過讀取在磁盤上穩定存儲的數據(通過正常的數據庫複製協議進行復制),部分是通過從客戶端獲取狀態,部分通過保守的假設來實現這一點。數據庫記錄每個會話、持有鎖和臨時文件。
一個新當選的主服務器運行:
- 它首先選擇一個新的客戶端世代號,客戶端需要在每次調用時展示。主服務器拒絕來自使用較舊世代號的客戶端的調用,並提供新的世代號。這確保新的主服務器不會響應發送給先前的主服務器的非常舊的數據包,即使是在同一臺計算機上運行的數據包。
- 新的主服務器可以響應主服務器位置請求,但不會首先處理傳入的會話相關的操作。
- 它爲記錄在數據庫中的會話和鎖構建內存數據結構。會話租約延長到前一個主服務器可能使用的最大值。
- 主服務器現在允許客戶端執行KeepAlives,但沒有其他與會話相關的操作。
- 它向每個會話發出故障恢復事件;這會導致客戶端刷新緩存(因爲它們可能錯過失效),並警告應用可能已丟失其他事件。
- 主服務器等待直到每個會話確認故障恢復事件或讓其會話到期。
- 主服務器允許所有操作繼續運行。
- 如果客戶端使用在故障恢復之前創建的句柄(根據句柄中的序列號的值確定),則主服務器將重新創建句柄的內存中表示並兌現該調用。如果關閉了這樣的重新創建的句柄,則主服務器將其記錄在內存中,以便在主服務世代中無法重新創建它;這可確保延遲或重複的網絡數據包不會意外地重新創建關閉的句柄。有缺陷的客戶端可以在未來的世代重新創建一個關閉的句柄,但鑑於客戶端已經出現故障,這是無害的。
- 經過一段時間間隔(比如說一分鐘),主服務器會刪除沒有打開文件句柄的臨時文件。在故障恢復後,客戶端應在此間隔期間刷新臨時文件上的句柄。如果此類文件上的最後一個客戶端在故障恢復期間丟失其會話,則此機制具有令人遺憾的影響,即臨時文件可能不會立即消失。
讀取者不會驚訝地發現,故障恢復代碼的運行頻率遠遠低於系統的其他部分,但它是有意思的漏洞的豐富來源。
2.10 數據庫實現
Chubby的第一個版本使用Berkeley DB[20]的複製版本作爲其數據庫。Berkeley DB提供了將字節串鍵映射到任意字節的B樹。我們安裝了一個鍵值比較函數,它首先按路徑名中的組件數進行排序;這允許節點將其路徑名稱作爲鍵,同時保持兄弟節點在排序順序中相鄰。由於Chubby不使用基於路徑的權限,因此數據庫中的單個查找就足以進行每個文件訪問。
Berkeley DB使用分佈式共識協議在一組服務器間複製其數據庫日誌。一旦添加了主服務器租約,這與Chubby的設計相匹配,這使得實現變得簡單。
雖然Berkeley DB的B-tree代碼被廣泛使用且成熟,但複製代碼是最近添加的,並且使用者較少。軟件維護人員必須優先考慮維護和改進其最受歡迎的產品功能。雖然Berkeley DB的維護人員解決了我們遇到的問題,但我們認爲使用複製代碼會使我們面臨比我們希望冒的風險更大的風險。因此,我們編寫了一個簡單的數據庫,使用類似於Birrell等人[2]的設計的預寫日誌和快照。如前所述,數據庫日誌使用分佈式共識協議在副本之間分發。Chubby使用了Berkeley DB的一些功能,因此這種重寫允許整個系統的顯著簡化;例如,雖然我們需要原子操作,但我們不需要一般性事務。
2.11 備份
每隔幾個小時,每個Chubby單元的主服務器都會將其數據庫的快照寫入另一個建築中的GFS文件服務器[7]。使用獨立的建築可確保備份在建築損壞後仍然存在,並且備份不會在系統中引入循環依賴性;同一建築中的GFS單元可能依賴於該Chubby單元來選擇其主。
備份提供災難恢復和初始化新替換副本的數據庫的方法,而不會對正在使用的副本施加負載。
2.12 鏡像
Chubby允許將文件集合從一個單元鏡像到另一個單元。鏡像很快,因爲文件很小,如果添加、刪除或修改文件,事件機制(第2.5節)會立即通知鏡像代碼。如果沒有網絡問題,在一秒鐘之內,全世界數十個鏡像都會發生變化。如果鏡像無法訪問,則在恢復連接之前它將保持不變。然後通過比較校驗和來識別更新的文件。
鏡像最常用於將配置文件複製到分佈在世界各地的各個計算集羣。一個名爲global的特殊單元包含一個子樹/ls/global/master
,它映射到每個其他Chubby單元中的子樹/ls/cell/slave
。global單元是特殊的,因爲它的五個副本位於世界上廣泛分離的地區,因此幾乎總是可以從大多數組織訪問。
從global單元中鏡像的文件包括Chubby自己的訪問控制列表,Chubby單元和其他系統向監控服務公佈其存在的各種文件,允許客戶端定位大型數據集(如Bigtable單元)和對於其他系統的許多配置文件的指針。
3. 擴展機制
Chubby的客戶端是獨立進程,因此Chubby必須處理的客戶端數量超出預期;我們已經看到90000個客戶端直接與一個Chubby主服務器通信 - 遠遠超過所涉及的機器數量。因爲每個單元只有一個主服務器,並且它的機器與客戶端的機器相同,所以客戶端可以大大超過主服務器。因此,最有效的擴展技術通過重要因素減少了與主服務器的通信。假設主服務器沒有嚴重的性能錯誤,那麼主服務器上的請求處理的微小改進幾乎沒有效果。我們使用幾種方法:
- 我們可以創建任意數量的Chubby單元;客戶端幾乎總是使用附近的單元(與DNS一起使用)以避免依賴遠程機器。我們的典型部署使用一個Chubby單元用於數千臺機器的數據中心。
- 當主服務器處於高負載時,主服務器可以將租約時間從默認的12s增加到大約60s,因此需要處理更少的KeepAlive RPC。(KeepAlives是迄今爲止主要的請求類型(見4.1),未能及時處理它們是過載服務器的典型故障模式;客戶端對其他調用中的延遲變化很不敏感。)
- Chubby客戶端緩存文件數據、元數據、缺少文件和打開句柄,以減少他們在服務器上進行的調用次數。
- 我們使用協議轉換服務器將Chubby協議轉換爲不太複雜的協議,如DNS等。我們將在下面討論其中一些。
在這裏,我們描述了兩種熟悉的機制,代理和分區,我們期望它將允許Chubby進一步擴展。我們尚未在生產中使用它們,但它們是已經設計,可能很快就會使用。我們目前沒有必要考慮超過五倍的擴展:首先,人們希望放入數據中心或依賴單個服務實例的機器數量有限制。其次,因爲我們爲Chubby客戶端和服務器使用類似的機器,所以增加每臺機器的客戶端數量的硬件改進也增加了每臺服務器的容量。
3.1 代理
Chubby的協議可以通過將來自其他客戶端的請求傳遞給Chubby單元的可信進程代理(在兩端使用相同的協議)。代理可以通過處理KeepAlive和讀取請求來減少服務器負載;它無法減少經過代理緩存的寫入流量。但即使使用激進的客戶端緩存,寫入流量也只佔Chubby正常工作負載的1%(見§4.1),因此代理可以顯著增加客戶端數量。如果代理處理Nproxy個客戶端,則KeepAlive流量將減少Nproxy倍,可能是1萬或更多。代理緩存最多可以將讀取流量減少到平均共享讀取量 - 大約10倍(§4.1)。但由於讀取目前構成了Chubby負載的10%以下,因此節省KeepAlive流量是迄今爲止更重要的功效。
代理爲寫入和首次讀取增加了額外的RPC。人們可能期望代理使該單元暫時不可用頻率至少變成以前的兩倍,因爲每個代理客戶端依賴於可能失敗的兩臺機器:其代理和Chubby主服務器。
機警的讀者會注意到2.9節中描述的故障恢復策略對於代理來說並不理想。我們將在4.4節討論這個問題。
3.2 分區
如2.3節所述,選擇了Chubby的接口,以便可以在服務器之間對單元的名稱空間進行分區。雖然我們還不需要它,但代碼可以按目錄對名稱空間進行分區。如果啓用,Chubby單元將由N個分區組成,每個分區都有一組副本和一個主服務器。目錄D中的每個節點D/C
將存儲在分區P(D/C) = hash(D) mod N
上。注意,D的元數據可以存儲在不同的分區P(D) = hash(D0) mod N
,其中D0是D的父級。
分區旨在實現分區之間幾乎沒有通信的大型Chubby單元。雖然Chubby缺少硬鏈接、目錄修改時間和跨目錄重命名操作,但仍有一些操作需要跨分區通信:
- ACL本身就是文件,因此一個分區可能會使用另一個分區進行權限檢查。但是,ACL文件很容易被緩存;只有Open()和Delete()調用需要ACL檢查;並且大多數客戶端不需要讀取ACL的公共可訪問文件。
- 刪除目錄時,可能需要進行跨分區調用以確保目錄爲空。
由於每個分區獨立於其他分區處理大多數調用,因此我們希望該通信僅對性能或可用性產生適度影響。
除非分區數N很大,否則可以預期每個客戶端將聯繫大多數分區。因此,分區會將任何給定分區上的讀寫流量減少N倍,但不一定會減少KeepAlive流量。如果Chubby需要處理更多客戶端,我們的策略涉及代理和分區的組合。
4. 使用,驚喜和設計錯誤
4.1 使用和行爲
下表給出了作爲Chubby單元快照的統計數據;RPC速率是在十分鐘期間看到的。這些數字是Google中單元的典型。
可以看出以下幾點:
- 許多文件用於命名;見§4.3。
- 配置、訪問控制和元數據文件(類似於文件系統的超級塊)很常見。
- 消極緩存很重要。
- 平均有
230k/24k = 10
個客戶端使用每個緩存文件。 - 很少有客戶端持有鎖,共享鎖很少見;這與用於主選舉和在副本之間分區數據的鎖一致。
- RPC流量由KeepAlives會話主導;有少量讀取(未命中緩存);寫入或鎖獲取很少。
現在我們簡要介紹一下我們單元中斷的典型原因。如果我們(樂觀地)假設一個單元是“在線”,只要它有一個願意服務的主服務器,那麼在我們的單元樣本上,我們在幾周的時間內記錄了61次中斷,總共相當於700個單元日的數據。我們排除因關閉數據中心的維護而導致的中斷。所有其他原因包括:網絡擁塞、維護、過載以及運營人員、軟件和硬件引起的錯誤。大多數中斷時間爲15秒或更少,52次未滿30秒;我們的大多數應用不會受到少於30s的Chubby中斷的嚴重影響。其餘9次中斷是由網絡維護(4)、懷疑的網絡連接問題(2)、軟件錯誤(2)和過載(1)引起的。
由於數據庫軟件錯誤(4)和操作員錯誤(2),在幾十單元年的運行時間內,我們丟失了六次數據;沒有涉及硬件錯誤。具有諷刺意味的是,操作錯誤涉及以避免軟件錯誤的升級。我們已經糾正了非主副本中由軟件引起的兩次損壞。
Chubby的數據適合存儲在RAM中,因此大多數操作都很便捷。我們的生產服務器的平均請求延遲始終遠遠低於一毫秒,無論單元負載如何,直到單元接近過載,此時延遲大幅增加且會話被丟棄。當許多會話(> 90000)處於活躍狀態時,通常會發生過載,但可能是由異常情況引起的:當客戶端同時發出數百萬個讀取請求時(在第4.3節中描述),以及當客戶端庫中的錯誤禁用某些讀取的緩存時 ,每秒產生數萬個請求。由於大多數RPC都是KeepAlive,因此服務器可以通過增加會話租約期(請參閱§3)來維持與許多活躍客戶端的低平均請求延遲。當突發寫入到達時,分組提交減少了每個請求完成的有效工作,但這種情況很少見。
在客戶端測量的RPC讀取延遲受RPC系統和網絡的限制;對於本地單元,它們不到1毫秒,而對相對極之間則爲250毫秒。寫入(包括鎖操作)由於數據庫日誌更新延遲進一步增大了5-10ms,但如果最近操作失敗的客戶端緩存該文件,則最多延遲數十秒。即使寫入延遲的這種可變性對服務器上的平均請求延遲幾乎沒有影響,因爲寫入很少發生。
如果不刪除會話,客戶端對延遲變化相當不敏感。我們一度在Open()中添加了人爲延遲來遏制濫用客戶端(見§4.5);開發者只有當延遲超過十秒並且反覆出現時才注意到。我們發現擴展Chubby的關鍵不是服務器性能;減少與服務器的通信可能會有更大的影響。我們檢查了沒有出現任何令人震驚的錯誤,然後專注於可能更有效的擴展機制。另一方面,開發者會注意到如果性能錯誤可能會影響到本地Chubby緩存(客戶端可能每秒讀取數千次)。
4.2 Java客戶端
谷歌的基礎組件主要是用C ++編寫的,但越來越多的系統都是用Java編寫的[8]。這種趨勢給Chubby帶來了意想不到的問題,Chubby有一個複雜的客戶端協議和一個不平凡的客戶端庫。
Java鼓勵整個應用的可移植性,代價是通過使其有點令人厭煩地與其他語言鏈接而犧牲增量使用。訪問非原生庫的常用Java機制是JNI[15],但它被認爲是緩慢而繁瑣的。我們的Java程序員不喜歡JNI,爲了避免使用它們,他們更喜歡將大型庫轉換爲Java,並承諾支持它們。
Chubby的C ++客戶端庫是7000行(與服務器相當),客戶端協議很精巧。維護Java維護庫需要小心和代價,而且沒有高速緩存的實現會給Chubby服務器帶來負擔。因此,我們的Java用戶運行協議轉換服務器的副本,其導出與Chubby的客戶端API緊密對應的簡單RPC協議。事後看來,我們如何避免編寫、運行和維護這個額外服務器的成本並不明顯。
4.3 用作名稱服務
儘管Chubby被設計爲鎖服務,但我們發現其最受歡迎的用途是作爲名稱服務器。
在正常的Internet命名系統(DNS)中的緩存是基於時間。DNS條目具有生存時間(TTL),並且DNS數據在該時間段內未刷新時將被丟棄。通常可以直接選擇合適的TTL值,但如果需要及時替換失敗的服務,TTL可能會變得足夠小從而使DNS服務器過載。
例如,我們的開發人員通常會運行涉及數千個進程的作業,並且每個進程都可以相互通信,從而導致二次查詢次數增加。我們可能希望使用60s的TTL;這將允許在沒有過度延遲的情況下更換出現問題的客戶端,並且在我們的環境中不被認爲是不合理的短暫替換時間。在這種情況下,要維護只有3千個客戶端的單個作業的DNS緩存,每秒需要15萬次查找。(相比之下,2-CPU 2.6GHz Xeon DNS服務器每秒可處理5萬個請求。)較大的作業會產生更嚴重的問題,並且有多個作業會同時運行。在引入Chubby之前,我們的DNS負載的可變性對Google來說是一個嚴重的問題。
相比之下,Chubby的緩存使用顯式失效,因此在沒有更改的情況下,固定速率的KeepAlive會話請求可以無限期地在客戶端維護任意數量的緩存條目。已經看到一個2核CPU 2.6GHz Xeon的Chubby主服務器可以處理直接與之通信的9萬個客戶端(沒有代理);客戶端包括具有上述類型通信模式的大型作業。提供快速名稱更新而無需單獨輪詢每個名稱的能力非常吸引人,以至於Chubby現在爲公司的大多數系統提供名稱服務。
儘管Chubby的緩存允許單個單元支持大量客戶端,但負載峯值仍然是一個問題。當我們第一次部署基於Chubby的名稱服務時,啓動一個3千個進程作業(從而產生900萬個請求)可能會使Chubby主服務器屈服。爲解決此問題,我們選擇將名稱條目分組到批中,以便單個查找將返回並緩存作業中大量(通常爲100個)相關進程的名稱映射。
Chubby提供的緩存語義比名稱服務所需的更精確;名稱解析只需要及時通知而不是完全一致。因此,通過引入專爲名稱查找而設計的簡單協議轉換服務器,有機會減少Chubby的負載。如果我們預見到使用Chubby作爲名稱服務,我們可能會選擇更快地實現完整代理,以避免需要這個簡單但仍然需要的額外的服務器。
存在另一個協議轉換服務器:Chubby DNS服務器。這使得存儲在Chubby中的命名數據可供DNS客戶端使用。此服務器對於簡化從DNS名稱到Chubby名稱的轉換以及適應無法輕鬆轉換的現有應用(如瀏覽器)非常重要。
4.4 故障恢復問題
主服務器故障恢復的原始設計(第2.9節)要求主服務器在創建數據庫時將新會話寫入數據庫。在鎖服務器的Berkeley DB版本中,當一次啓動許多進程時,創建會話的開銷成爲問題。爲了避免過載,服務器被修改爲在數據庫中存儲會話不是在第一次創建會話時,而是在嘗試第一次修改、鎖獲取或打開臨時文件時。此外,活躍會話在每個KeepAlive上以一定概率記錄在數據庫中。因此,只讀會話的寫入在時間上分散了。
儘管有必要避免過載,但是這種優化具有不期望的效果,即年輕的只讀會話可能不會記錄在數據庫中,因此如果發生故障恢復則可能被丟棄。雖然這些會話沒有持有鎖,但這是不安全的;如果所有記錄的會話都在丟棄的會話租約到期之前接入新的主服務器,則丟棄的會話會讀取陳舊的數據一段時間。這在實踐中很少見;在大型系統中,幾乎可以肯定某些會話將無法登記,從而迫使新主服務器等待最長的租約時間。儘管如此,我們已經修改了故障恢復設計,以避免這種影響,並避免當前方案引入代理的複雜性。
在新設計下,我們完全避免在數據庫中記錄會話,而是以與主服務器當前重新創建句柄相同的方式重新創建它們(§2.9,¶8)。在允許操作繼續之前,新的主服務器現在必須等待完整的最壞情況租約超時,因爲它無法知道是否所有會話都已登記(§2.9,¶6)。同樣,這在實踐中幾乎沒有影響,因爲很可能並非所有會話都會登記。
一旦可以在沒有固化磁盤狀態的情況下重新創建會話,代理服務器就可以管理主服務器不知道的會話。僅對代理可用的額外操作允許它們更改與鎖相關聯的會話。這允許一個代理在代理失敗時從另一個代理接管客戶端。主服務器所需的唯一進一步更改是保證不會放棄與代理會話關聯的鎖或臨時文件句柄,直到新代理有機會聲明它們爲止。
4.5 濫用客戶端
Google的項目團隊可以自由建立他們自己的Chubby單元,但這樣做會增加他們的維護負擔,並消耗額外的硬件資源。因此,許多服務使用共享的Chubby單元,這使得將客戶端與其他的不當行爲隔離開來很重要。Chubby旨在在一家公司內運營,因此針對它的惡意拒絕服務攻擊很少見。但是,錯誤、誤解以及我們開發人員的不同期望會產生類似於攻擊的效果。
我們的一些補救措施是嚴厲的。例如,我們審查項目團隊計劃使用Chubby的方式,並拒絕訪問共享的Chubby名稱空間,直到審覈滿意爲止。這種方法的一個問題是,開發人員往往無法預測將來如何使用他們的服務,以及使用如何增長。讀者會注意到我們自己未能預測如何使用Chubby本身的諷刺意味。
我們審查的最重要方面是確定是否使用任何Chubby資源(RPC速率,磁盤空間,文件數)與用戶數量或項目處理的數據量是否呈線性增長(或更差)。必須通過補償參數來緩解任何線性增長,該補償參數可以被調整以將Chubby上的負載減少到合理的界限。然而,我們的早期審覈還不夠徹底。
一個相關的問題是大多數軟件文檔中缺乏性能建議。一個團隊編寫的模塊可能會在一年之後由另一個團隊複用,並帶來災難性的後果。有時很難向接口設計者解釋他們必須改變他們的接口不是因爲它們不好,而是因爲其他開發人員可能不太瞭解RPC的成本。
下面列出我們遇到的一些問題。
4.5.1 缺乏積極的緩存
最初,我們並不意識到緩存文件缺失的迫切需要,也不重用打開文件句柄。儘管嘗試了教育,我們的開發人員經常編寫循環,當文件不存在時無限期地重試,或者在人們可能期望他們只打開文件一次時重複打開關閉文件來輪詢文件。
起初,當應用程序在短時間內多次嘗試Open()相同文件時,我們通過引入指數增加的延遲來對抗這些重試循環。在某些情況下,開發人員承認這些暴露的錯誤,但通常需要我們花更多的時間在教育上。最後,使重複的Open()調用更廉價更加容易。
4.5.2 缺少配額
Chubby從未打算用作大量數據的存儲系統,因此它沒有存儲配額。 事後來看,這是天真的。
Google的一個項目編寫了一個模塊來跟蹤數據上傳,在Chubby中存儲一些元數據。這種上傳很少發生,僅限於一小部分人,因此空間有限。但是,另外兩項服務開始使用相同的模塊作爲跟蹤來自更廣泛用戶羣的上傳的手段。不可避免地,這些服務一直在增長,直到Chubby的使用極限:在每個用戶操作上整個重寫了一個1.5MByte的文件,並且該服務使用的整體空間超出了所有其他Chubby客戶端的空間需求。
我們引入了文件大小限制(256kBytes),並鼓勵服務遷移到更合適的存儲系統。但是很難對繁忙的人們維護的生產系統進行重大改變 - 數據遷移到其他地方大約花費了一年的時間。
4.5.3 發佈/訂閱
已經有幾次嘗試使用Chubby的事件機制作爲Zephyr[6]風格的發佈/訂閱系統。Chubby的重要的保證及其使用失效而不是更新來維護緩存一致性使得它對於除了最簡單的發佈/訂閱示例之外的所有操作都是緩慢且低效的。幸運的是,在重新設計應用的成本太大之前,已經獲得了所有這些用途。
4.6 經驗教訓
在這裏,我們列出的經驗教訓,以及各方面的設計變更,如果我們有機會,我們會做:
4.6.1 開發人員很少考慮可用性
我們發現我們的開發人員很少考慮失敗可能性,並且傾向於將像Chubby這樣的服務視爲始終可用。例如,我們的開發人員曾經構建了一個系統,該系統使用了數百臺機器,當Chubby選出新的主服務器時,這些機器啓動恢復程序需要幾十分鐘。這使得單個故障的後果在時間和受影響的機器數量上放大了一百倍。我們希望開發人員規劃短暫的Chubby中斷,以便這樣的事件對他們的應用幾乎沒有影響。這是第2.1節中討論的粗粒度鎖的參數之一。
開發人員也無法理解正在運行的服務與其應用可用的服務之間的區別。例如,全局Chubby單元(參見§2.12)幾乎總是運行的,因爲兩個以上地理位置較遠的數據中心很少同時發生故障。但是,給定客戶端觀察到的可用性通常低於客戶端本地Chubby單元的觀察到的可用性。首先,本地單元不太可能與客戶端分區,其次,雖然本地單元可能由於維護而經常停機,但是相同的維護會直接影響客戶端,因此客戶端不會觀察到Chubby的不可用性。
我們的API選擇也會影響開發人員選擇處理Chubby中斷的方式。例如,Chubby提供了一個事件,允許客戶端檢測何時發生主服務器故障恢復。目的是讓客戶檢查可能的更改,因爲其他事件可能已丟失。不幸的是,許多開發人員選擇在接收此事件時讓應用崩潰,從而大大降低了系統的可用性。相較來說,我們發送冗餘的“文件更改”事件可能更好,甚至保證在故障恢復期間不丟失任何事件。
目前,我們使用三種機制來防止開發人員對Chubby可用性過度樂觀,尤其是全局單元的可用性。首先,正如前面提到的(§4.5),我們回顧了項目團隊計劃如何使用Chubby,並建議他們拋棄將其可用性與Chubby的關係過於緊密的技術。其次,我們現在提供執行某些高級任務的庫,以便開發人員自動與Chubby中斷隔離。第三,我們使用每個Chubby中斷的事後反思作爲一種手段,不僅可以消除Chubby和我們的操作程序中的錯誤,還可以降低應用對Chubby可用性的敏感性 - 兩者都可以提高我們系統的整體可用性。
4.6.2 可以忽略細粒度鎖
在2.1節結束時,我們爲客戶端運行以提供細粒度鎖的服務器勾畫了一個設計。到目前爲止,我們還不需要編寫這樣的服務器,這可能是一個驚喜;我們的開發人員通常發現要優化他們的應用,他們必須刪除不必要的通信,這通常意味着找到一種使用粗粒度鎖的方法。
4.6.3 API選擇不佳會產生意外影響
在大多數情況下,我們的API發展良好,但有一個錯誤突出。我們取消長時間運行的調用的方法是Close()和Poison() RPC,它們也會丟棄句柄的服務器狀態。這防止了可以獲取鎖的句柄被共享,例如,由多個線程共享。我們可以添加一個Cancel() RPC來允許更多的開放句柄共享。
4.6.4 RPC使用會影響傳輸協議
KeepAlives既用於刷新客戶端的會話租約,也用於將事件和緩存失效從主服務器傳遞到客戶端。此設計具有自動且理想的效果,即客戶端無法在不確認緩存失效的情況下刷新其會話租約。
這似乎是理想的,除了它在我們選擇的協議中引入了一個擔憂。TCP的退避策略不關注較高層次的超時,例如Chubby租約,因此基於TCP的KeepAlives在高網絡擁塞時導致許多會話丟失。我們被迫通過UDP而不是TCP發送KeepAlive RPC;UDP沒有擁塞避免機制,所以我們更願意只在必須滿足高級時間邊界時才使用UDP。
我們可以使用額外的基於TCP的GetEvent() RPC來擴充協議,該RPC將用於在正常情況下傳遞事件和失效,以與KeepAlives相同的方式使用。KeepAlive回覆仍將包含未確認事件的列表,以便事件最終被確認。
5. 與相關工作的比較
Chubby基於由來已久的想法。Chubby的緩存設計源自分佈式文件系統[10]。它的會話和緩存令牌在行爲上類似於Echo[17];會話減少了V系統中租約[9]的開銷。暴露通用鎖服務的想法可以在VMS[23]中找到,儘管該系統最初使用了允許低延遲交互的專用高速互連網絡。與其緩存模型一樣,Chubby的API基於文件系統模型,包括類似文件系統的名稱空間不僅僅對文件非常方便[18,21,22]。
Chubby與分佈式文件系統(例如Echo或AFS[10])的性能和存儲期望不同:客戶端不會讀取、寫入或存儲大量數據,並且他們不期望高吞吐量甚至低延遲,除非數據被緩存。他們確實都期望一致性、可用性和可靠性,但是當性能不那麼重要時,這些特性更容易實現。由於Chubby的數據庫很小,我們可以在線存儲它的許多副本(通常是五個副本和一些備份)。我們每天多次進行完整備份,並通過數據庫狀態的校驗和,我們每隔幾個小時進行一次副本互相校驗。普通文件系統性能和存儲要求的弱化使我們能夠從單個Chubby主服務器中爲數萬個客戶端提供服務。通過提供許多客戶端可以共享信息和協調活動的中心點,我們解決了系統開發人員面臨的一類問題。
文獻中描述的大量文件系統和鎖服務阻礙了詳盡的比較,因此我們提供了其中一個的詳細信息:我們選擇與Boxwood的鎖服務[16]進行比較,因爲它是最近設計的,它也設計爲在鬆散耦合的環境中運行,但其設計在各方面與Chubby有所不同,有些是令人關注的,有些是偶然的。
Chubby在單個服務中實現了鎖、可靠的小文件存儲系統和會話/租約機制。相比之下,Boxwood將這些分爲三個:鎖服務、Paxos服務(狀態的可靠存儲庫)和故障檢測服務。Boxwood系統本身將這三個組件一起使用,但另一個系統可以獨立使用這些組件。我們懷疑這種設計差異源於目標受衆的差異。Chubby旨在爲不同的受衆和應用組合;其用戶範圍從創建新分佈式系統的專家到編寫管理腳本的新手。對於我們的環境,使用熟悉的API的大規模共享服務似乎很有吸引力。相比之下,Boxwood提供了一個工具包(至少對我們來說)適合於少數更復雜的開發人員,這些開發人員可以共享代碼但不需要一起使用的項目。
在許多情況下,Chubby提供了比Boxwood更高層次的接口。例如,Chubby結合了鎖和文件名稱空間,而Boxwood的鎖名稱是簡單的字節序列。Chubby客戶端默認緩存文件狀態;Boxwood的Paxos服務的客戶端可以通過鎖服務實現緩存,但可能會使用Boxwood本身提供的緩存。
這兩個系統具有明顯不同的默認參數,根據不同的期望而選擇:每個客戶端每隔200ms聯繫每個Boxwood故障檢測器,超時爲1秒; Chubby的默認租約時間爲12秒,KeepAlives每7秒交換一次。Boxwood的子組件使用兩個或三個副本來實現可用性,而我們通常每個單元使用五個副本。但是,這些選擇本身並不表示深層設計差異,而是表明必須如何調整此類系統中的參數以適應更多客戶端機器,或與其他項目共享機架的不確定性。
一個更有趣的區別是Boxwood缺乏Chubby寬期限的引入。(回想一下,寬期限允許客戶端在不丟失會話或鎖的情況下渡過長長的Chubby主服務器中斷。Boxwood的“寬限期”相當於Chubby的“會話租約”,這是一個不同的概念。)同樣,這種差異是對兩個系統中的規模和失敗概率的期望不同的結果。雖然主服務器的故障恢復很少見,但丟失的Chubby鎖對於客戶端來說是成本高昂的。
最後,兩個系統中的鎖用於不同目的。Chubby鎖是重量級的,需要序列生成器才能安全地保護外部資源,而Boxwood鎖是輕量的,主要用於Boxwood內部。
6. 總結
Chubby是一種分佈式鎖服務,旨在實現Google分佈式系統中活動的粗粒度同步;它已被廣泛用作配置信息的名稱服務和存儲庫。
它的設計基於衆所周知的優點:在容錯的幾個副本之間的分佈式共識、一致的客戶端緩存以減少服務器負載、同時保留簡單的語義、及時通知更新、以及熟悉的文件系統接口。我們使用緩存、協議轉換服務器和簡單的負載適配,以允許每個Chubby實例擴展到數萬個客戶端進程。我們希望通過代理和分區進一步擴展它。
Chubby已成爲Google的主要內部名稱服務;它是MapReduce[4]等系統的常見交流機制;存儲系統GFS和Bigtable使用Chubby從冗餘副本中選擇主;它是需要高可用性的文件的標準存儲庫,例如訪問控制列表。
7. 致謝
許多人爲Chubby系統做出了貢獻:Sharon Perl在Berkeley DB上編寫了複製層;Tushar Chandra和Robert Griesemer編寫了複製的數據庫,取代了Berkeley DB;Ramsey Haddad將API和Google的文件系統接口結合起來;Dave Presotto,Sean Owen,Doug Zongker和Praveen Tamara編寫了Chubby DNS、Java和命名協議轉換器以及完整的Chubby代理;Vadim Furman添加了打開句柄和文件缺失的緩存;Rob Pike,Sean Quinlan和Sanjay Ghemawat提供了寶貴的設計建議;許多谷歌開發者發現了早期的缺陷。
參考文獻
- BIRMAN, K. P., AND JOSEPH, T. A. Exploiting virtual synchrony in distributed systems. In 11th SOSP (1987), pp. 123–138.
- BIRRELL, A., JONES, M. B., AND WOBBER, E. A simple and efficient implementation for small databases. In 11th SOSP (1987), pp. 149–154.
- CHANG, F., DEAN, J., GHEMAWAT, S., HSIEH, W. C., WALLACH, D. A., BURROWS, M., CHANDRA, T., FIKES, A., AND GRUBER, R. Bigtable: A distributed structured data storage system. In 7th OSDI (2006).
- DEAN, J., AND GHEMAWAT, S. MapReduce: Simplified data processing on large clusters. In 6th OSDI (2004), pp. 137–150.
- FISCHER, M. J., LYNCH, N. A., AND PATERSON, M. S. Impossibility of distributed consensus with one faulty process. J. ACM 32, 2 (April 1985), 374–382.
- FRENCH, R. S., AND KOHL, J. T. The Zephyr Programmer’s Manual. MIT Project Athena, Apr. 1989.
- GHEMAWAT, S., GOBIOFF, H., AND LEUNG, S.-T. The Google file system. In 19th SOSP (Dec. 2003), pp. 29–43.
- GOSLING, J., JOY, B., STEELE, G., AND BRACHA, G. Java Language Spec. (2nd Ed.). Addison-Wesley, 2000.
- GRAY, C. G., AND CHERITON, D. R. Leases: An efficient fault-tolerant mechanism for distributed file cache consistency. In 12th SOSP (1989), pp. 202–210.
- HOWARD, J., KAZAR, M., MENEES, S., NICHOLS, D., SATYANARAYANAN, M., SIDEBOTHAM, R., AND WEST, M. Scale and performance in a distributed file system. ACM TOCS 6, 1 (Feb. 1988), 51–81.
- JEFFERSON, D. Virtual time. ACM TOPLAS, 3 (1985), 404–425.
- LAMPORT, L. The part-time parliament. ACM TOCS 16, 2 (1998), 133–169.
- LAMPORT, L. Paxos made simple. ACM SIGACT News 32, 4 (2001), 18–25.
- LAMPSON, B. W. How to build a highly available system using consensus. In Distributed Algorithms, vol. 1151 of LNCS. Springer–Verlag, 1996, pp. 1–17.
- LIANG, S. Java Native Interface: Programmer’s Guide and Reference. Addison-Wesley, 1999.
- MACCORMICK, J., MURPHY, N., NAJORK, M., THEKKATH, C. A., AND ZHOU, L. Boxwood: Abstractions as the foundation for storage infrastructure. In 6th OSDI (2004), pp. 105–120.
- MANN, T., BIRRELL, A., HISGEN, A., JERIAN, C., AND SWART, G. A coherent distributed file cache with directory write-behind. TOCS 12, 2 (1994), 123–164.
- MCJONES, P., AND SWART, G. Evolving the UNIX system interface to support multithreaded programs. Tech. Rep. 21, DEC SRC, 1987.
- OKI, B., AND LISKOV, B. Viewstamped replication: A general primary copy method to support highly-available distributed systems. In ACM PODC (1988).
- OLSON, M. A., BOSTIC, K., AND SELTZER, M. Berkeley DB. In USENIX (June 1999), pp. 183–192.
- PIKE, R., PRESOTTO, D. L., DORWARD, S., FLANDRENA, B., THOMPSON, K., TRICKEY, H., AND WINTERBOTTOM, P. Plan 9 from Bell Labs. Computing Systems 8, 2 (1995), 221–254.
- RITCHIE, D. M., AND THOMPSON, K. The UNIX timesharing system. CACM 17, 7 (1974), 365–375.
- SNAMAN, JR., W. E., AND THIEL, D. W. The VAX/VMS distributed lock manager. Digital Technical Journal 1, 5 (Sept. 1987), 29–44.
- YIN, J., MARTIN, J.-P., VENKATARAMANI, A., ALVISI, L., AND DAHLIN, M. Separating agreement from execution for byzantine fault tolerant services. In 19th SOSP (2003), pp. 253–267.