Meta 每秒如何移動 TB 級數據?
一、引言
Apache Kafka 無處不在。它是分布式消息傳遞的首選,為全球眾多公司提供各種用例服務,包括消息傳遞、日志聚合和流處理。
許多公司都在使用它,包括 PayPal、Uber 和 LinkedIn 等大型科技公司。然而,并非所有人都使用 Kafka;他們會使用其他可用的解決方案,或者……構建自己的系統。在今天的文章中,我們將探討 Scribe,這是 Meta 構建的一個消息隊列服務,用以支撐全球規模的流量。
圖片
二、術語
在繼續之前,我們首先了解一些 Scribe 的概念:
首先是 Category。它向用戶展示了一個邏輯流的概念。
圖片
與 Kafka 類似,向 Category 寫入和讀取數據的客戶端也被稱為生產者和消費者。兩者都以庫的形式實現。一個應用程序可以根據需要擁有多個生產者和消費者實例。
圖片
對于消費消息,一組消費者可以共同處理來自一個 Category 的消息。數據可以根據消息的鍵或值被分割成邏輯分片。一個消費者可以只讀取來自一個邏輯分片的消息。Scribe 允許整個流量在消費者組之間任意分配,而無需依賴分片方案。
圖片
還有一個物理分片的概念,它們是包含與消息負載相關聯的元數據的日志文件。我們將在后面更詳細地探討。
三、高層架構
從寫入路徑來看,生產者實例接收來自用戶的消息,對它們進行批處理,然后將它們發送給 ScribeD。在這個階段,一個批次可能包含來自不同 Category 的消息。然后,ScribeD 將這些批次發送給寫入代理。
消息隨后被拆分,使得來自同一 Category 的消息屬于同一批次。寫入代理然后將這些批次路由到批處理服務。批處理服務持久化這些批次并提交它們的元數據。
圖片
對于讀取路徑,消費者實例聯系讀取流服務,從元數據存儲中檢索元數據。然后,消費者使用元數據中的信息(例如,從哪里讀取元數據)來形成對讀取代理的請求。該代理管理所有相關的數據訪問操作。
接下來,我們將更深入地探討寫入/讀取操作的細節,以及數據是如何存儲的。
四、寫入
生產者庫是入口點。可以在多個主機上啟動多個生產者實例。當應用程序向生產者庫發出寫入請求時,它們將獲得一個包含寫入操作結果的對象。
圖片
為了節省內存占用,生產者將來自多個 Category 的消息在內存中批量聚合。生產者將消息批次刷新到 ScribeD,這是一個本地守護進程,它接收來自主機上所有生產者實例的消息,并最終將它們發送到寫入代理。
圖片
ScribeD 的主要目標是通過將生產者刷新(在內存中)的消息緩沖到磁盤上,來確保這些消息的容錯性。這種方法可以防止在寫入持久化存儲時發生故障導致數據丟失。
圖片
在接收消息批次后,寫入代理首先對這些消息執行準入控制檢查,然后將來自同一 Category 的消息拆分到同一批次中。這些批次被路由到批處理服務。

批處理服務壓縮批次數據并將其刷新到臨時數據存儲和持久化負載存儲。之后,批處理服務將這些批次的元數據(包括指向批次數據的指針)寫入基于日志的元數據存儲。這些元數據將幫助 Scribe 為消費者提供順序的、流式抽象的讀取模式。
五、存儲
如前所述,Meta 將元數據和數據分離開。消息元數據存儲在 LogDevice 中,這是一個為順序數據讀寫操作優化的基于日志的存儲系統。
1、元數據存儲
對于每個 Category,LogDevice 將每個數據批次的元數據追加到一個稱為 log 的文件中。每個日志都是該 Category 的一個物理分片。日志文件中的每個記錄都有一個單調遞增的序列號。
圖片
Meta 在 LogDevice 集群中存儲了數百萬個日志。每個 LogDevice 集群包含存儲節點。每個記錄可以在這些節點的任意子集上進行復制,確保數據冗余。
2、持久化數據存儲(DDS)
所有消息負載都存儲在 Tectonic 中,這是 Meta 構建的用于替代 HDFS 的分布式文件系統。這個文件系統也是其他系統的支柱,例如 Meta 的數據倉庫。
Tectonic 不是基于一個因子完全復制負載,而是支持糾刪碼來確保數據可靠性。與簡單的復制相比,存儲占用空間會更小;然而,在發生故障需要數據重建時,這種方法將需要更多資源。
圖片
為了控制數據塊的數量,Scribe 可以將來自多個 Category 的數據存儲在同一個塊中。寫入代理累積大小達到幾十兆字節級別的塊,然后執行刷新操作到 Tectonic 存儲節點的磁盤。在一個塊中,來自同一 Category 的數據也會被累積起來,以更有效地服務讀取操作,累積大小最高可達 2 兆字節。
3、臨時數據存儲(EDS)
除了持久化存儲,消息負載也存在于區域性的臨時數據存儲中,這本質上是一個具有兩層結構的緩存:本地緩存和遠程緩存。
這一點很重要,因為 DDS 可能位于與讀取用戶不同的區域。一個 Category 通常從全球運行的生產者那里接收輸入數據記錄。這導致當消費者發出讀取請求時,它從不同區域讀取大部分數據。
EDS 在這里起到了救援作用,因為它允許用戶從不同區域訪問 DDS。其目標是盡量減少對包含實際負載數據的遠程存儲的訪問。
除了降低延遲和避免對 Tectonic 的壓力之外,EDS 的其他有趣職責將在本文后面討論。
圖片
對于遠程層,Meta 利用 Memcached 來存儲消息負載,存儲時長為 1-2 小時。Meta 認為這段時間對于想要消費被認為是“溫”數據的用戶來說已經足夠。對于更“熱”的數據,Meta 使用 Cachelib 來管理緩存,它利用讀取代理主機的空閑內存資源。我們將在討論讀取路徑時更深入地探討 EDS。
六、讀取
讀取路徑的入口點也是一個名為 Consumers 的庫。應用程序通常啟動一組消費者實例來從 Category 讀取數據。當用戶啟動一個消費者實例時,會生成一個到讀取流服務的有狀態連接。
Meta 嘗試以確保消費者實例和讀取流服務位于同一區域的方式建立連接。該讀取流服務負責(在元數據的幫助下)為用戶提供流抽象。
該服務的一個實例擁有一個到 LogDevice 集群的連接池。當它收到消費者請求時,它會根據消費者的輸入,識別所有必需的 LogDevice 集群、物理分片以及要讀取的分片范圍。
圖片
一個不同的實例(稱為讀取器)將處理從 LogDevice 集群實際讀取數據的操作。這個讀取流實例合并來自讀取器的結果,為消費者提供單一的元數據流。
消費者使用這個元數據(包括數據指針)向讀取代理發出 RPC 請求以獲取負載。讀取代理的主要工作是處理與訪問負載存儲相關的所有事情。
圖片
當收到來自消費者的請求時,讀取代理嘗試從區域臨時數據存儲中獲取數據。數據可能存在于 Memcached 實例的內存緩存中。
如果讀取代理必須訪問持久化數據存儲,則該請求被路由到與 DDS 中存儲的負載位于同一區域的讀取代理實例。
- 如果區域 A 的讀取代理實例需要存儲在區域 B 的負載,它會將請求路由到位于區域 B 的實例。然后數據被填充到區域 B 的 EDS 中。
圖片
- 之后,來自區域 C 的讀取代理實例對同一數據的請求將由區域 B 的 EDS 提供服務。雖然仍然需要跨區域通信,但與直接從區域 B 的 DDS 讀取相比,延遲較低。
如果消費者指定了任何過濾或序列化選項,讀取代理將對其訪問的消息應用這些選項。
圖片
- Meta 將讀取代理設計為數據暫存區。在將數據填充到 EDS 時,讀取代理將 Category 的負載從其原始格式轉換為列式格式,以幫助消費應用程序避免這個昂貴的過程,特別是當它們需要數據進行分析工作負載時。
- 讀取代理還可以應用消費應用程序過濾器的下推;它可以評估 SQL 查詢的一個子集。
七、元數據如何管理
元數據是 Scribe 的核心。許多組件需要訪問元數據存儲來支持寫入和讀取操作。盡管 LogDevice 是一個事務性的、高可用的數據庫,但數百萬客戶端嘗試查詢元數據則是另一回事。
為了解決這個問題,Meta 在元數據存儲之上構建了一個緩存和分發層。有一個后臺作業會掃描整個元數據數據庫,并將內容填充到一個配置分發系統中??蛻舳丝梢詮拇颂幾x取元數據。
圖片
對于更詳細的元數據,例如包含指向所需數據的指針的物理分片,一個周期性作業會輪詢 LogDevice 集群以檢索集群、Category 和物理分片之間的最新映射關系,并將該映射關系暴露在一個高度復制的數據庫中。
圖片
對于來自消費者的請求,讀取流服務實例訂閱相關的映射關系。當在運行時添加或刪除物理分片時,映射關系可能會發生變化?;谶@些信息,服務可以相應地啟動或停止讀取器實例。
八、流量如何管理
為了應對全球規模的流量,Scribe 在不同層級利用了多層流量管理系統。
集群內流量整形: Scribe 動態調整其內部資源以處理單個 LogDevice 集群內的流量。當特定 Category 出現流量激增時,一項服務會自動將其現有的物理分片拆分成更小的分片,以確保該 Category 的水平可擴展性。當流量下降時,系統會在多余的分片超過保留期后逐漸移除它們。
圖片
集群間流量整形: 控制平面持續監控每個 LogDevice 集群的寫入和讀取工作負載,以調整傳入的寫入工作負載限制,確保集群不會被壓垮。
圖片
當達到限制時,寫入代理可以將流量路由到同一區域內的另一個 LogDevice 集群。如果過載是全球性的,則使用優雅降級方法來保護系統。
主機級流量整形: 在單個服務器級別,Scribe 使用 Meta 的服務網格來路由流量。服務網格使用主機暴露的負載指標來將請求路由到負載最輕的服務器。
圖片
當服務器接受一個將在長時間內消耗大量資源的請求時,它會立即通告一個“虛假的”高負載指標。這個高值告訴系統的其余部分,該服務器已被“預留”,即使實際的資源使用尚未開始。
圖片
然后服務器指數級衰減這個負載值。負載指標隨著時間的推移逐漸降低。等到請求的實際資源消耗變得明顯時,人工設置的值已經衰減到能夠反映服務器實際負載的程度。
這個機制至關重要,因為它可以防止服務網格將過多請求路由到已經在處理繁重工作負載的服務器。如果沒有它,服務網格會在一個長時間請求的最初幾秒內認為主機空閑,并假定該服務器能夠處理新的請求。
對于特定任務,Scribe 使用一致性哈希將請求分配給特定主機,這有助于最大化效率。這在諸如寫入代理將特定數據 Category 的消息組發送到同一個批處理服務主機,或者當請求需要路由到特定的讀取代理主機以利用該主機上的內存緩存等用例中很有幫助。
九、臨時數據如何管理
如上所述,Scribe 在臨時數據存儲中管理數據副本,目的是:
- 防止頻繁訪問持久化存儲,從而減少跨區域網絡流量的延遲
- 為分析應用程序以更具讀取效率的格式表示數據
Scribe 圍繞資源使用一種約束滿足模型,例如持久化存儲 IO 或 Memcached 網絡利用率,來確定創建哪些副本以及創建在何處。系統的“控制平面”基于幾個因素做出這些決策,旨在在資源限制內最大化收益。考慮的關鍵因素包括:
- 讀取扇出: 有多少讀取器正在訪問特定的數據 Category?
- 消息新鮮度: 消費者主要是讀取最新的消息還是舊消息?
- 數據過濾: 臨時數據存儲中的新副本能否顯著減少需要傳輸的數據量?
- 反序列化成本: 讀取代理是否需要執行成本高昂的數據格式轉換?臨時數據存儲中格式合適的副本可以節省反序列化過程的成本。
根據數據中心特定的資源限制,Scribe 將采用不同的緩存策略以充分利用可用資源。例如,一個 Memcached 出口流量有限的區域可能會使用讀取代理內存中的副本作為“盾牌”,以防止過多請求沖擊緩存。
Scribe 的控制平面不斷重新計算這些策略,以適應波動的工作負載。
十、支持的保證
Scribe 支持幾種消息傳遞保證:盡力而為、至少一次和可重復讀取的至少一次。
1、盡力而為
盡力而為保證的總體思路是優先考慮吞吐量和可用性。這是為高容量、“發射后不管”的數據流設計的權衡,在這種情況下,快速傳遞大部分數據比確保每條消息都恰好傳遞一次且保持嚴格的順序更重要。

從寫入路徑來看:
- 優先考慮寫入可用性: 在此保證下,Scribe 的主要目標是以最少的資源消耗持久化海量數據。
- 機會主義路由: 消息被路由到最近或最可用的寫入代理主機,甚至可以在需要時路由到另一個數據中心。這確保了即使路徑擁塞,數據也能繼續流動。
- 重試和重復: 如果寫入請求失敗,客戶端將重試調用。這可能導致同一條消息被成功傳遞多次,從而導致消息重復。這是為了最大化可用性而已知且可接受的權衡。
- 可能存在數據丟失: 如果寫入調用失敗且無法恢復,客戶必須準備容忍小程度的數據丟失。
從讀取路徑來看:
- 收集: Scribe 的寫入路徑將單個 Category 的消息分散到許多物理分片上。讀取此數據的消費者必須從所有這些分片收集消息。
- 容忍無序: 在所有分片上強制執行嚴格的順序會引入顯著的延遲并降低吞吐量。為了避免這種情況,Meta 引入了一種配置,允許用戶在延遲和數據排序之間進行權衡。
2、至少一次
其主要目標是確保每條消息都成功存儲。Scribe 通過以下方式實現這一點:
存儲確認: 客戶端只有在收到存儲層確認消息已成功保存后,才認為消息已發送。
圖片
積極重試: 如果消息沒有收到確認,它會被重新發送。這個過程在寫入路徑的每一步重復,直到確認消息已保存。
圖片
然而,積極的重試可能導致數據重復。為了最小化這種情況,Scribe 實施了多項優化:
- 主動分片初始化: Scribe 預測并提前初始化物理分片。這可以防止重試因建立新分片的過程而延遲。
- 分級超時: 寫入路徑中的每一步都有一個比前一步更長的超時時間。這確保了一步不會在下一步仍在處理請求時過早放棄并重試,從而減少不必要的重試和重復。
- 保守批處理: 對于需要“至少一次”傳遞的數據,Scribe 以較小的批次發送消息。這意味著如果發生重試,潛在的重復事件涉及的消息數量較少。
3、可重復讀取
不可重復讀: 在一個事務過程中,如果事務在不同時間點看到同一數據具有不同的值,則此行為稱為不可重復讀。
通過可重復讀取,Scribe 保證消費者讀取數據流時,如果重新讀取,將看到相同順序的相同數據。這是比上述兩種保證更強的保證,對于需要嚴格數據排序和可靠處理的用例(如變更數據捕獲)至關重要。
Scribe 提供了兩種不同的方法來實現這一點,每種方法都有其自身的權衡:寫入路徑變體和讀取路徑變體。
寫入路徑變體:
圖片
- 它確保邏輯分片的流量被發送到單個、專用的物理分片。
- 然而,這將最大吞吐量限制在單個物理分片能夠處理的范圍內。
讀取路徑變體:
圖片
- 一個內部服務,稱為順序分片生成器,讀取一個 Category 的所有分散的元數據分片。
- 然后,生成器根據下游應用程序所需的批次大小,將消息重新排序到一個新的元數據分片中。
- 這為 Category 中的所有消息提供了單一的、確定性的順序。它還允許下游應用程序定義自己的批次大小。
- 然而,它向數據流添加了一個額外的步驟,包括額外的操作,從而給整個系統引入了延遲。
十一、結語
在本文中,我們首先探討了 Scribe 的術語,然后深入研究了其架構,該架構包含許多在讀寫路徑上職責清晰的組件。Scribe 還將元數據與數據分離,并引入了緩存層來改進讀取路徑。
然后我們更仔細地了解了 Scribe 如何管理流量、元數據以及緩存系統中的數據。最后,我們將通過研究 Scribe 提供的幾種消息傳遞保證來結束本文。
參考資料
- Meta, Scribe: How Meta transports terabytes per second in real time, (2025)https://www.vldb.org/pvldb/vol18/p4817-karpathiotakis.pdf
作者丨Vu Trinh 編譯丨Rio
來源丨網址:https://vutr.substack.com/p/how-did-meta-move-terabytes-of-data



























