Facebook分佈式隊列系統Scribe:支持百萬機器、PB/h級傳輸

作者:Manolis Karpathiotakis,Dino Wernli,Milos Stojanovic

我們的硬件基礎設施由數百萬臺機器組成,所有這些機器都會生成日誌;我們需要處理和存儲這些日誌,並提供與這些日誌相關的服務。這些日誌的總大小以每小時幾個PB的速度增長。處理日誌的機器通常與生成日誌的機器不是同一臺:日誌會與各種下游處理流程產生關聯,並且它們可能會在不同時間被訪問。完成收集、合併和交付這些日誌(具有低延遲和高吞吐量)的任務需要一種系統化的方法。我們的解決方案是Scribe,這是一個分佈式隊列系統,它對服務日誌從A點移動到B點背後的所有複雜性進行了封裝。

Scribe處理日誌時的輸入速率可以超過2.5TB/s,輸出速率可以超過7TB/s。作爲參考,我們可以看下歐洲核子研究中心的大型強子對撞機,它在最近這次運行期間的輸出速率估計只有每秒25GB(https://home.cern/science/computing/processing-what-record)。Scribe最近進行了一次重大的架構改造和簡化,新架構現在已經投入生產。本文將首次分享Scribe當前的設計、設計出當前架構的一些考慮因素,並介紹我們的規模和演進在過去十年中是如何影響它的。

Scribe:一個通用的緩衝隊列系統

我們的生態系統涉及各種各樣的日誌生成場景。一個典型的例子是web服務器生成的半結構化日誌,用來指示它們的健康狀況。在大多數情況下,開發人員希望將靜態文件的非結構化內容傳輸到下游系統。下游系統通常是我們的衆多分析工具之一。通常的期望是,開發人員應該能夠對一組日誌執行分析和挖掘任務。根據使用用例,他們可能希望觀察實時趨勢或歷史模式。因此,需要將日誌發送到數據倉庫進行歷史分析,或發送到實時流處理系統(如Puma、Stylus和Scuba)進行實時挖掘。

傳送一個日誌

Scribe允許它的用戶從我們的任何機器上向命名的邏輯流(稱爲類別)寫一個有效負載。有效負載可以在大小、結構和格式上有很大的不同,但是Scribe以同樣的方式對待它們。每個日誌都可以被保存一段時期,這個時期的長短是可配置的——通常是幾天。在此期間,使用者可以按順序讀取日誌流,這種操作稱爲跟蹤。

Scribe當前架構的高級視圖。

任何進程都可以使用生產者(Producer)庫來編寫日誌。生產者可以屬於容器中運行的應用程序,比如執行Hack(https://hacklang.org/)代碼的web服務器(Hack是PHP的一種變體)。使用Scribe的工程師可以指引生產者採取下列路徑之一:

  • 寫入一個本地守護進程(Scribed),該守護進程負責最終將日誌發送到存儲後端
  • 直接寫入後端服務器的遠程層(寫入服務)

最後,消費者可以連接一個讀取服務,通過流式方式讀取日誌。

Scribe的第一個角色(https://www.usenix.org/conference/lisa18/presentation/braunschweig)實際上是Scribed的一個早期版本——本質上是NetApp文件歸檔器——它負責將日誌持久化到連接了網絡的存儲驅動器上。雖然Scribe是一個沒有任何宕機時間的長可用系統,但是隨着時間的推移,它的架構已經演變成了一個更復雜的系統,現在包含40多個不同的組件。越來越多的複雜組件使得我們很難維護一個剝離了內部規範的開源版本。這種複雜性是我們歸檔Scribe項目的開放源碼(https://github.com/facebookarchive/scribe)的主要原因。下面詳細介紹Scribe架構的當前版本,重點關注組成數據層的組件。

Scribed

我們的大多數機器都運行一個名爲Scribed的守護進程。如果客戶最關心的是儘快釋放日誌的所有權,那麼他們可以直接將日誌寫入Scribed。Scribed使用本地磁盤(來擴展內存)作爲緩衝區,用於機器失去連接或後端不可用的情況。對於大多數Scribe用戶來說,直接將日誌寫到Scribed就很好用了。但與Scribed通信也有一些缺點:

  • 把日誌存儲到磁盤上可能會有延遲和一致性的問題。
  • 如果Scribed在單臺機器上無法獲取,寫操作可能會丟失,或被阻塞相當長的一段時間。
  • Scribed本是機器上的共享資源,但是試圖寫入大量日誌的用戶會獨佔Scribed資源,從而影響同一機器上其他用戶的體驗。

雖然這些情況比較罕見,但用戶也會遇到;爲了解決這些問題,我們爲用戶構建了一種能力,讓他們能夠繞過Scribed,並且指示他們的生產者直接向Scribe寫服務寫入日誌。這樣做可以減少端到端的延遲,並增加寫可用性,因爲生產者可以將寫操作指向多個後端主機,而不是指向單個本地進程。另一方面,寫彈性可能會降低:例如,如果生產者不能足夠快地將日誌釋放到寫服務,那麼生產者的內存隊列可能會被填滿,從而導致拒絕多餘的寫請求,消息也會丟失。

寫服務

最終,每個日誌都會被髮送到Scribe的某一個後端服務器。Scribe根據地點和可用性將服務器的選擇權委託給內部的負載平衡服務。這種選擇是動態的,後端服務器機羣充當單一的、有彈性的寫服務,沒有任何一處故障點。由於Scribe從數百萬臺機器收集日誌,來自不同機器的日誌可以以任何順序到達寫服務。此外,來自單個機器的日誌可以到達幾個不同的寫服務機器,並最終存儲在幾個不同的存儲後端實例中。這些後端不共享全局時間或先後關係的概念,因此Scribe不會試圖保存它接收到的日誌的相對順序。

相反,寫服務側重於按類別對輸入日誌進行批處理,並將這些日誌按批轉發到存儲後端。存儲是按集羣來組織的:每個存儲集羣是一組託管了Scribe存儲組件的機器。寫服務會選擇把每批日誌放在哪個存儲集羣中。寫服務的目的是將相關的日誌(例如,相同的類別)放在集羣的一個子集中,子集既要足夠大以確保寫可用性,又要足夠小以確保讀可用性,並在這兩者之間取得平衡。此外,集羣放置決策還考慮了其他因素,比如我們希望日誌從哪個地理區域讀取等。

一旦選擇了存儲集羣,寫服務就會轉發日誌。在日誌到達集羣中的持久存儲點之前,日誌通常一直駐留在進程內存中,因此很容易出現一些故障,這些故障會導致日誌丟失;對日誌丟失的最小容忍度是我們的設計考慮之一,以避免產生開銷,影響其高吞吐量和低延遲的指標。有些用戶需要更嚴格的交付保證,他們可以選擇Scribe的一種配置,以性能爲代價換取更少的損失。

LogDevice的緩衝存儲

Scribe的存儲後端是LogDevice(https://engineering.fb.com/core-data/logdevice-a-distributed-data-store-for-logs/),它是一個分佈式日誌存儲系統,在各種工作負載和故障場景下提供了高持久性和可用性。Scribe將每個日誌存儲爲一個LogDevice記錄。通過LogDevice進行持久存儲後,Scribe可以把每個記錄當做像是隻有一個(邏輯)副本那樣操作:LogDevice層隱藏了操作的複雜性,比如記錄複製和恢復等。

LogDevice將記錄組織成序列,稱爲分區。Scribe類別由跨多個LogDevice集羣的多個分區支持。一旦LogDevice在一個分區中持久地存儲了一條記錄,該記錄就可以被讀取了。同樣,請注意,Scribe在LogDevice中留存記錄的時間是有限的——通常是幾天。Scribe的目標是在一段時間內緩衝記錄,以能夠讓客戶使用這些記錄,或者在發生故障時重放記錄。需要保留超過幾天時間的客戶通常會將日誌放入長期存儲區。

在遷移到LogDevice之前,Scribe的存儲後端一直依賴於HDFS(https://hadoop.apache.org/)。Scribe最終達到了HDFS的可伸縮性極限——大約在同一時間,Facebook也棄用了HDFS。轉換到基於LogDevice的後端後,Scribe演變爲當前的形式,並可以有效地服務於更多的用例。

讀取日誌

客戶可以通過讀服務來讀取寫入到Scribe的日誌,該讀服務提供對日誌流的讀訪問。讀服務的實現基於流式Thrift(https://code.fb.com/open-source/under-the-hood-building-and-open-sourcing-fbthrift/),並遵循響應式流規範(https://www.reactive-streams.org/)。除了對日誌流的一般訪問之外,讀服務還充當一個負載平衡層,允許Scribe在共享資源(例如,到存儲後端的連接)的同時爲多用戶提供讀用例服務。

通過讀服務從Scribe讀取日誌涉及到集羣識別(這些集羣包含了請求類別的日誌),並需要讀取相應的LogDevice分區。每個分區的內容合併在一起,並根據存儲時間戳進行部分排序。考慮到使用者正在從多個集羣讀取日誌,並且在單個客戶端主機中生成的日誌可能會存在於多個Scribe集羣中,消費者會避免強制執行嚴格的輸出順序。相反,消費者應用“粗略”排序,確保輸出日誌相互間不超過N分鐘(N通常是30),這個時間是相對於到達持久存儲區的時間而言的:需要按創建日誌的精確順序刷新日誌的客戶,通常使用流處理系統(如Stylus)從Scribe讀取日誌並進一步排序。

設計決策

Scribe將自己公開爲一個運行在主機上的Thrift服務,並從客戶端收集實時流日誌。爲了獲得更好的普適性,Scribe是根據一些一流的需求設計的。具體來說,Scribe服務必須滿足:

  • 簡潔性,爲客戶提供一個簡單的API來讀寫日誌。
  • (寫)可用性,容忍傳輸過程中任何部分的失敗,同時允許在過程中可能丟失少量日誌。
  • 可伸縮性,可以處理數百萬生產者(其聚合後的輸入速率可以超過2.5TB/s)和數十萬消費者(其聚合後的輸出速率可以超過7TB/s)。
  • 多用戶,確保客戶可以通過共享的Scribe介質進行多路複用,而不會影響其他客戶的服務質量。

簡潔性

Scribe爲用戶提供了一個高級API來讀寫日誌,分別是生產者API和消費者API。生產者API可以綁定到多種編程語言,並且僅由一個寫方法組成。消費者API提供一個消費者對象,該對象可用於從Scribe讀取日誌流,也可用於執行其他操作,比如獲取檢查點。

除了高級API之外,Scribe還提供了在我們的主機中可用的方便的二進制文件。想要在Scribe中寫日誌的客戶可以使用如下命令:

# scribe_cat $CATEGORY $PAYLOAD

而如果需要從Scribe中讀取日誌,客戶可以使用下面的命令動態創建一個讀取流來跟蹤給定時間段內的某類別日誌:

# ptail --since $ts1 --till $ts2 $CATEGORY | consumerApp

可用性

Scribe的主要關注點是確保寫入到它的日誌能夠到達具有持久性的LogDevice存儲區,同時不受多種故障類型的影響。從Scribe的“邊緣”開始,生產者實例在內存中緩存日誌,以處理短時間內Scribed不可用的問題(例如,爲了更新的目的Scribed被重新啓動)。通過類似的方式,Scribed在本地磁盤上緩衝日誌,用於應對網絡中斷的情況,因爲網絡中斷就不能順利地把日誌發送到寫服務。LogDevice則通過持久化它接收到的每個日誌記錄的多個副本,來進一步增強寫可用性。這些功能的組合使Scribe能夠成功地服務於絕大多數的寫調用。

Scribe的讀取路徑被設計爲在分散的存儲主機、機架和集羣上下文中有效地提供日誌。具體來說,考慮到Scribe日誌最終所在的存儲集羣的高扇入,讀數據流通常從分佈在世界各地的多個集羣獲取日誌。由於日誌分散在多個集羣中,所以Scribe需要能夠處理集羣無法足夠快或根本無法爲讀取者提供服務的情況。

Scribe可以允許用戶對輸出日誌的交付和順序保證降低要求,從而處理短暫的集羣不可用的情況。實際上,這意味着讀取者可以選擇是等待當前不可用的日誌還是在沒有日誌的情況下繼續。

可伸縮性

Scribe必須能夠容納不斷寫入其中的日誌。而且即使是在一天之內,寫模式也會有很大的不同。Scribe類別的物理存儲分佈在多個LogDevice分區上,每個分區可以維持一個最大的寫吞吐量。Scribe根據分區接收的捲動態地擴展某個類別的分區數量。這個分區供應過程的最終結果是自動處理寫操作時的流量變化,這使得Scribe的吞吐量水平能夠彈性伸縮。

關於日誌讀取,它可能不那麼容易處理,它需要爲單一的消費者進程進行處理,並且爲給定類別生成的大量日誌進行刷新:消費者主機的網卡可能會變得飽和,或接收消費者輸出的進程可能由於CPU的限制,無法以日誌生成的速度來處理日誌。Scribe因此引入了“桶”——它是允許多個獨立的消費者進程共同使用來自單個Scribe類別日誌的功能。桶是一種分片/索引機制:它們允許用戶指定他們希望Scribe對輸入進行分片的流的數量,並允許用戶通過給定消費者來檢索特定的分片。生產者可以顯式地提供桶號,也可以不指定它,讓日誌隨機分佈在可用的桶中。

多用戶

Scribe用戶把它當作一個規模與Facebook同級的服務:在任何給定的時間點,數百萬的生產者向數十萬個Scribe類別寫入日誌;然後數十萬的消費者讀取這些日誌。儘管如此,客戶仍然希望他們的體驗不受Scribe所服務的其他客戶的影響。因此,Scribe不斷地監視使用模式,以檢測可能危及系統整體健康的非預期用戶行爲。

Scribe對寫操作施加速率限制,以保護它的後端。具體地說,Scribe給每個類別劃定一個寫配額:超額會導致Scribe將一個類別列入黑名單(例如,丟棄新進來的數據流),或者僅接受它的一部分日誌,亦或對生產者施加背壓。此外,Scribe會檢測以下情況:客戶多次讀取某個類別的內容,因爲這會增加Scribe和其存儲後端的壓力,並增加跨區域的帶寬消耗——由於Scribe的“write-anywhere”特性,帶寬就成爲了一種寶貴的經常被使用到的資源。

除了控制需求外,Scribe還試圖最小化與Scribe服務的交互,因爲這可能需要人工的干預。例如,故障轉移和停機對終端客戶是透明的,客戶的寫配額是自動擴展的,用於適應有機增長,寫流量的增加會自動導致存儲資源的供應增加,客戶可以使用消費者提供的“檢查點”從停止的地方重啓讀取過程。

Scribe的未來演變

Scribe已經有機地成長了十多年,它的設計也在發展中。Scribe繼續改進的方向之一是進一步簡化整個系統,這將涉及到合併衆多不同的組件,以及以可選插件的形式封裝特定於Facebook的基礎設施。除了這樣的簡化工作,我們還在努力擴展Scribe以支持客戶不斷變化的需求:例如,我們目前正在評估不同交付擔保的顯式公開,比如at-least-once和exactly-once。此外,我們還通過緩存和過濾功能豐富了寫服務,以便爲某些客戶優化用例,這些客戶需要多次讀取相同日誌並/或對根據內容過濾類別的結構化日誌感興趣。最後,我們一直在努力確保Scribe的各個層面——從分片機制到放置策略和災備特性——與我們每天處理的日誌體量保持同步。

在Scribe多次重寫期間出現的一個有趣模式是,它的API和架構決策開始接近其他大型消息傳遞系統。我們打算繼續演進Scribe,並沒有打算將我們的使用場景遷移到其他系統,這主要是因爲Scribe具備自我管理、自我擴展和自我修復的能力,即使在面臨大規模災難性故障時也是如此。考慮到Scribe是提供給多租戶的,這些要求都是很嚴格的。因此,這些需求被構建到其整體設計、每個組件以及組件的協作中。其他系統通常依賴第三方解決方案來實現自動伸縮和災難恢復等功能。

總之,Scribe是一個高度可用的系統和服務,在過去的12年裏,它隨着我們不斷擴展的工具和服務而發展,變得越來越有彈性。我們希望看到Scribe未來的進一步發展,我們非常渴望進一步提高它的可伸縮性和容錯能力,同時繼續簡化它的實現。我們希望這些簡化的努力能夠在將來的某個時候爲再次開源Scribe鋪平道路。

原文鏈接
Scribe: Transporting petabytes per hour via a distributed, buffered queueing system

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