Netflix Technology Blog

作者 Parth JainRakesh SukumarYingwu ZhaoRenzo Sanchez-SilvaNathan Fisher
深入探討 Netflix 在大規模環境下建構即時服務依賴地圖的工程挑戰——從串流架構、分散式聚合管線,到時光旅行查詢,以及讓這一切運作的方法論。

前言

在我們的第一篇文章中,我們介紹了這個問題:Netflix 的工程師需要一個統一、即時的服務依賴視圖,以便更快排除故障、了解影響範圍,並導航我們的分散式架構。我們描述了多來源方法——將 eBPF 網路流量、IPC 指標與分散式追蹤結合,組成物理分離的圖層,可獨立查詢或合併成完整視圖。

那篇文章說明了我們「建構了什麼」以及「為什麼」建構。這篇文章則聚焦於「如何」——在 Netflix 規模下建構此系統的工程實務。

事實是:第一版在本地環境運作完美……但正式環境完全不同。Kafka 消費者落後、執行個體記憶體不足、某些節點接收的流量是其他節點的 100 倍,垃圾回收停頓所消耗的 CPU 甚至超過實際業務邏輯。

你在本文學到的不是成功故事,而是學習之旅。我們將帶領你了解啟用規模的架構決策、考驗這些決策的正式環境挑戰、引導我們的優化方法論,以及適用於任何分散式系統的經驗。過程中,我們會分享讓我們得以每秒處理數百萬筆流量記錄、在任何時間點重建拓撲,以及提供次秒級查詢回應的創新——同時維持近即時的新鮮度。

架構深度剖析:為串流與規模而建構

串流優先:為什麼即時性很重要

傳統服務拓撲系統使用批次處理——每小時或每天聚合資料,然後儲存完整快照。這種方法在適度規模下可行,但有根本問題:等到你看到資料時,已經過時了。在凌晨 3 點的正式環境事故中,一小時前的依賴地圖只是考古,而非可觀測性。

我們關鍵的架構決策是建構串流優先。我們不使用批次作業處理歷史資料,而是持續從多區域 Kafka 串流與 IPC 指標(Server-Sent Events)中擷取流量記錄,透過具背壓處理的反應式管線處理,並提供近即時的拓撲更新——通常在數十分鐘內完成,遠優於批次處理的數小時或數天前資料。

這不僅關乎新鮮度——對我們的使用情境至關重要。現場活動無法等待下一個小時批次。事故回應需要最新資料。變更驗證需要立即看到影響。架構必須支援持續更新,同時在不落後的情況下處理大規模流量。

背壓如何啟用即時處理
串流方法帶來新挑戰,也需要解決根本問題:如何在不遺失資料的情況下,即時處理每秒數百萬筆流量記錄,同時在下游系統變慢時不遺失資料?

傳統方法在我們的規模下不足:

  • 無界佇列:簡單但危險。不斷緩衝直到記憶體用盡,執行個體便當機。
  • 基於丟棄的流量控制:緩衝區滿時丟棄資料。快速,但你的拓撲現在不完整——你已遺失連線資訊。
  • 批次處理:處理所有資料,但數小時後。到那時,事故已結束(或更糟,仍以過時資料進行中)。

我們需要不同的東西:在負載下優雅地減速而不遺失資料的能力。這就是反應式串流與背壓變得不可或缺之處。

運作方式如下:當 Stage 3 無法足夠快地寫入圖形資料庫時,它會向 Stage 2 發出減速訊號。Stage 2 再通知 Stage 1。Stage 1 再通知 Kafka 消費者暫停。資料在 Kafka 中等待,直到下游容量恢復。

按 Enter 或點擊以完整大小檢視影像

當下游階段無法跟上時,它會向上游發出減速訊號——背壓與資料流方向相反

背壓自然傳遞至整個系統。當任何階段過載——無論是流量尖峰、GC 停頓或外部減速——管線會自動減速至可持續速率。大多數情況下不會遺失資料,不會有執行個體當機,系統會優雅降級。

這就是讓「即時」在我們規模下運作的原因。在正常運作時,我們以最小延遲處理。在負載尖峰或暫時減速期間,我們減速而非崩潰。資料仍會被處理——只是晚幾秒或幾分鐘,而非立即處理。對於拓撲更新,這個權衡是可接受的:略微延遲的即時更新,遠優於一小時前的批次資料或因丟棄記錄而造成的不完整拓撲。

這種方法的代價是複雜度。相較於傳統同步阻塞模型,反應式串流更難理解(我們會在挑戰部分進一步討論)。但在 Netflix 規模下,背壓不是可選——它是讓系統在正式環境負載下可靠運作的機制。

多層架構:物理分離以進行獨立最佳化

如同我們在第一篇文章所述,我們的多來源方法使用三個物理分離的拓撲層,每層使用不同的儲存最佳化:

  • 網路層:eBPF 流量記錄於圖形資料庫分割區——涵蓋全面但缺乏應用程式脈絡
  • IPC 層:應用程式指標於與網路層不同的圖形資料庫中——端點細節豐富但僅限已檢測服務
  • 追蹤層:分散式追蹤於欄式儲存(Parquet)——實際請求路徑但已取樣。(我們在下一篇文章涵蓋追蹤層及其整合

按 Enter 或點擊以完整大小檢視影像

流量記錄與 IPC 指標透過兩個獨立最佳化的管線進入分離的圖形儲存,透過單一 API 統一

物理儲存隔離啟用獨立最佳化——每層有不同的吞吐量、查詢模式與演進時程。在查詢時,我們跨相關儲存系統執行平行查詢並合併結果,提供次秒延遲的統一視圖,同時維持各層獨立演進的彈性。

三階段分散式聚合管線

網路層擷取的核心是一個三階段分散式管線。此架構解決網路流量記錄的根本挑戰:它們只顯示個別網路躍點,而非建構有用拓撲所需的真實應用程式層級連線

核心問題:網路中介

在雲端環境中,應用程式之間的流量很少直接流動——它會穿越負載平衡器、NAT 閘道、API 閘道與代理等中介網路元件。網路流量記錄顯示個別躍點:App A → Load Balancer 與 Load Balancer → App B 顯示為獨立流量。但工程師需要的是邏輯依賴:App A → App B。若不解析這些中介,我們的拓撲將充斥基礎設施元件,而非顯示故障排除所需的服務對服務關係。

三階段管線解決此問題:

按 Enter 或點擊以完整大小檢視影像

流量記錄管線細節——三個透過 SSE 連接的階段,豐富化在最終寫入圖形前應用

Stage 1:初始聚合(FlowLog Ingestion Service)

多區域 Kafka(4 個區域)
→ 過濾無效流量記錄
→ 5 分鐘時間視窗批次
→ 為每個視窗建立初始聚合器
→ 透過一致性雜湊分佈
→ 透過 SSE 串流至 Stage 2

Stage 1 從多區域 Kafka 取用流量記錄,過濾無效記錄,將其批次為 5 分鐘時間視窗,並建立初始聚合器物件。在此階段,我們仍在處理原始網路躍點——識別涉及中介的流量,但尚未解析它們。聚合器串流至 Stage 2 進行解析。

Stage 2:網路中介解析層(Intermediate GraphEntity Ingestion Service)

Stage 1 聚合器(透過 SSE 串流)
→ 依中介分組流量(負載平衡器、NAT 閘道、代理等)
→ 識別配對:(來源 → 中介)+(中介 → 目的地)
→ 解析為直接邊緣:來源 → 目的地
→ 追蹤經過的中介
→ 跨兩個躍點聚合指標
→ 透過一致性雜湊重新分佈
→ 透過 SSE 串流至 Stage 3

這是關鍵步驟Stage 2 執行圖形解析:

  1. 依中介收集流量:將中介為來源或目的地的聚合器分組——建立流向中介的流量地圖(來源 → 中介)與離開中介的流量地圖(中介 → 目的地)
  2. 解析直接邊緣:對於每個中介,聯結其傳入與傳出流量,以建立直接應用程式邊緣(App A → App B),合併兩個躍點的指標
  3. 結果:乾淨的應用程式層級拓撲,顯示 App A → App B 而非 App A → Load Balancer → App B

此解析在聚合時發生,而非查詢時,解析後的邊緣流向 Stage 3。

為什麼不能在單一階段完成?根本問題是資料局部性。要將 App A → Load Balancer → App B 解析為 App A → App B,我們需要在同一個執行個體上同時擁有兩個流量以執行聯結。但在 Stage 1,流量依據 Kafka 的分割區散佈在各執行個體。Stage 2 的關鍵功能是依中介識別碼重新分佈聚合器——所有涉及「Load Balancer X」的流量路由至同一個執行個體進行解析。這是經典的 map-reduce 模式:Stage 1 映射,Stage 2 依中介洗牌並化簡,Stage 3 執行最終聚合。

按 Enter 或點擊以完整大小檢視影像

為什麼單一階段不夠的具體範例——Stage 1 依分割區散佈流量,Stage 2 依中介重新洗牌以解析直接邊緣,Stage 3 持久化最終結果。

Stage 3:最終聚合與豐富化(GraphEntity Ingestion Service)

Stage 2 聚合器(透過 SSE 串流)流量
→ 跨時間視窗的最終聚合
→ 以外部資料豐富化(查詢鍵值儲存)
→ 轉換為圖形實體
→ 持久化至圖形資料庫(節流寫入)

Stage 3 對已解析邊緣執行最終聚合,從外部資料來源(應用程式健康狀態、擁有權、中繼資料)豐富化圖形節點,將聚合器轉換為具體圖形實體(填入所有屬性的節點與邊緣),並以受控節流將其持久化至分散式圖形資料庫,以尊重儲存系統限制。

為什麼是三階段而非兩階段?

我們最初使用兩階段:在 Stage 1 聚合,在 Stage 2 解析並持久化。這在測試中運作,但正式環境規模失敗——Stage 2 因資料集中而過載。

問題:中介解析需要將涉及某個中介的所有流量收集在同一個執行個體上。因此,處理熱門應用程式及其中介的流量記錄的執行個體因顯著資料集中而成為「熱節點」。加上資料豐富化(查詢外部儲存以取得健康狀態與中繼資料),最忙碌的執行個體也執行最多 I/O。

解決方案:將職責分割為三個階段。Stage 2 專注於解析並重新分佈。Stage 3 處理豐富化與持久化。這種分級重新分佈——分佈、解析、再分佈、持久化——將負載分散至多個執行個體,並將計算密集的解析與 I/O 密集的豐富化隔離。即使中介看到 100 倍典型流量,也不會有單一執行個體成為瓶頸。

為什麼使用 Server-Sent Events 而非 gRPC 或訊息佇列?

我們最初使用 gRPC,但它成為效能瓶頸——序列化開銷、連線池管理與串流回應的記憶體壓力消耗的 CPU 超過業務邏輯。訊息佇列增加了基礎設施複雜度,對我們的使用情境沒有好處。

SSE 被證明是理想選擇:輕量 HTTP 型通訊協定,序列化最小,與反應式串流的自然背壓整合,以及更簡單的連線模型。經驗:產業最佳實務如「使用 gRPC 進行服務通訊」並非普遍適用。對於串流大量預聚合資料,較輕量的替代方案可能更適合。測量,而非假設。

為什麼 IPC 不需要三階段

按 Enter 或點擊以完整大小檢視影像

IPC 管線與流量記錄管線模式相同,但只需要單一階段。

IPC 層使用單階段聚合,因為:(1) IPC 指標已處於應用程式層級——沒有需要解析的中介,(2) 資料從一開始就已正確分割——每個節點透過一致性雜湊接收其分配應用程式的所有 IPC 指標,無需重新分佈。這突顯了一個關鍵原則:資料分割策略決定處理架構。當資料以正確分割方式到達時,你可以直接聚合;當它沒有(如需要中介解析的網路流量),你需要洗牌/重新分佈階段。

動態負載分佈:雜湊與自動擴展的運作方式

當我們的 Auto Scaling Groups 動態新增或移除執行個體時,我們如何決定哪個執行個體接收哪個聚合器?傳統方法假設靜態叢集——當叢集大小變更時,需要明確重新平衡、協調服務或手動資料移動。

我們的做法:動態一致性雜湊

我們使用一致性雜湊,並從服務登錄進行動態執行個體探索。每個執行個體查詢登錄以取得目前健康 ASG 執行個體清單,以排序順序維護(確保所有執行個體有相同視圖),並將此清單用於雜湊函式 findOwnerInstance(aggregator.primaryKey)。當 ASG 擴展或縮減時,雜湊函式會根據更新後的執行個體清單自然重新分佈聚合器——無需明確協調。

關鍵洞察:利用現有基礎設施。我們的服務登錄已追蹤 ASG 成員資格以進行健康檢查。將其作為真實來源,可免費取得動態叢集成員資格。一致性雜湊提供穩定分割(在成員資格變更期間,大多數聚合器留在同一執行個體),而排序清單確保一致性。

結果

負載自動跟隨基礎設施。在流量尖峰或現場活動期間,新執行個體立即接收其份額。在部署期間,聚合器無縫移至健康執行個體。此模式對正式環境穩定性至關重要——無需手動介入,無需協調協定,只需自動重新平衡。

V1 之旅:正式環境規模的主要挑戰

讓初始版本(V1)上線正式環境讓我們學到規模改變一切。在開發中運作的東西在正式環境中失效。每個假設都受到考驗。而修復一個瓶頸會揭示下一個瓶頸。

挑戰 1:Kafka 消費者延遲

問題:我們的多區域 Kafka 消費者開始落後——消費者延遲從數秒增長至數分鐘,然後是數小時。流量記錄到達的速度快於我們處理的速度。如果持續下去,我們永遠追不上,我們的「即時」拓撲將變得越來越過時。

調查:我們大量檢測 Kafka 消費者指標。主要發現:

  • Kafka 的分割區數少於消費者群組大小的最佳值
  • 每次提取操作擷取的記錄相對較少
  • 網路通訊端緩衝區未針對我們的吞吐量調整大小
  • 跨區域讀取延遲增加開銷

套用的解決方案

  1. 增加 Kafka 分割區:更多分割區讓消費者群組中有更多平行消費者,將負載分散至更多執行個體。
  2. 調整提取參數:增加每次提取操作的記錄數,減少網路往返次數。這以每則訊息延遲(我們提取較大批次)換取吞吐量(每秒處理更多記錄)。
  3. 增加通訊端接收緩衝區大小:確保網路緩衝區永遠不限制提取操作。在我們的規模下,預設緩衝區大小太小。

結果:吞吐量顯著改善,延遲降低至可接受水準——即使在尖峰流量期間,通常也低於一分鐘。

經驗:在規模下,你無法孤立最佳化。修復 Kafka 延遲揭示下一個瓶頸:執行個體本身無法跟上更高的擷取速率。管線移動更快,這暴露了下游容量問題。

挑戰 2:熱節點與資料放大

問題:這是我們面臨最嚴重的正式環境問題。Auto Scaling Group 中的某些執行個體接收的流量是其他執行個體的 100 倍。記憶體使用量飆升。垃圾回收停頓變得頻繁且長時間。更多 CPU 時間花在 GC 上而非業務邏輯。最終,熱執行個體會 DOWN,觸發級聯故障,因為其負載重新分佈至其他執行個體。

根本原因調查
熱門服務的流量記錄主導流量量。像我們的驗證層或推薦 API 這樣的服務被數百個其他服務呼叫,產生的流量記錄比典型服務多出數個數量級。

我們最初的架構使用一致性雜湊來決定哪個執行個體擁有每個目的服務的聚合。給定目的地的所有流量記錄都路由至同一執行個體——該目的地的「擁有者」。此設計看似合理:將相關資料分組以進行有效聚合。

但熱門目的地建立了熱節點。一個執行個體可能擁有驗證服務,另一個可能擁有很少使用的後端服務。負載分佈極度不均——某些執行個體處理的流量記錄是其他執行個體的 100 倍。

更糟的是,在重新分佈期間發生資料放大。考慮一個被 100 個上游服務跨 10 個執行個體呼叫的服務。所有 10 個執行個體都接收該目的地的流量記錄(因為它們都有呼叫它的本地用戶端)。當它們將聚合器路由至擁有者執行個體時,該執行個體接收 10 個獨立的聚合器必須合併。資料量在洗牌期間倍增。

當許多執行個體將相同金鑰的資料路由至一個擁有者時,資料量就在落點處倍增——熱節點的根本原因。

我們使用 async-profiler 與堆積傾印分析進行大量分析。結果很清楚:熱執行個體將大部分 CPU 用於垃圾回收,試圖管理聚合器物件的快速配置與釋放,因為流量記錄湧入的速度快於處理速度。記憶體壓力導致 GC 顛簸,消耗 CPU,減慢處理速度,增加記憶體壓力——一個惡性循環。

解決方案:三階段管線的雙重效益
我們先前描述的三階段管線——主要設計用於代理解析——結果正是我們解決熱節點問題所需的。原因如下:

Stage 1 在任何分佈之前先在本地執行初始聚合。而非立即將每筆流量記錄傳送至遠端執行個體。每個執行個體直接在記憶體中將原始流量記錄線上聚合為時間視窗聚合器(超過 5 分鐘期間);這允許原始流量快速被丟棄並進行垃圾回收,顯著降低記憶體壓力,並確保只有聚合結果透過網路傳輸至下游階段。

在收件匣中取得 Netflix Technology Blog 的故事

免費加入 Medium 以取得此作者的更新。

記住我以加快登入

Stage 2 專注於代理解析,但也提供中間重新分佈。來自 Stage 1 的聚合器透過一致性雜湊分佈至 Stage 2 執行個體。現在我們移動的是壓縮的聚合器,而非個別流量記錄。解析後,Stage 2 再次將已解析邊緣重新分佈至 Stage 3,提供第二次雜湊操作,進一步分散負載。

Stage 3 接收已壓縮兩次且分佈兩次的已解析聚合器。即使對於極熱門服務,負載也已分散至足夠的分佈點,不會有單一執行個體過載。

關鍵洞察:由一個需求驅動的架構決策(代理解析)通常會作為有益副作用解決其他問題(負載分佈)。具分級重新分佈的三階段管線達成兩個目標——它解析代理以顯示乾淨的應用程式層級拓撲,並透過跨多個分佈點分散負載來防止熱節點。

從 gRPC 切換至 SSE
如先前所述,此挑戰也揭示 gRPC 並非我們規模下階段間通訊的正確通訊協定。我們將 gRPC 替換為 Server-Sent Events,大幅降低發送端與接收端的資源消耗。

結果

  • CPU 使用率在執行個體間均勻分佈——不再有負載是其他執行個體 10 倍的熱節點
  • 由於更好的聚合與更輕量的通訊協定,網路頻寬使用量顯著下降
  • 由於降低物件配置率,記憶體壓力減輕
  • 系統隨 Auto Scaling Group 變更而優雅擴展

經驗:技術選擇必須符合你的特定使用情境。gRPC 非常適合請求-回應 RPC 模式。對於管線中串流大量聚合資料,較輕量的替代方案可能更適合。讓測量引導決策,而非產業炒作或現有團隊專業知識。

挑戰 3:記憶體與垃圾回收

問題:即使修復熱節點後,我們仍看到高堆積使用率、頻繁的垃圾回收停頓,以及執行個體偶爾 DOWN。GC 日誌顯示停頓消耗大量 CPU 時間——在某些情況下,超過我們的業務邏輯。

根本原因:多個因素造成:物件在等待 5 分鐘聚合視窗完成時累積在堆積中、資料流經各階段時不同物件類型之間的不必要轉換,以及不可變性開銷——遵循 Scala 最佳實務,我們對聚合器使用不可變資料結構,但每次更新都建立新物件,在每秒數百萬筆記錄時壓垮垃圾回收器。

調查:堆積傾印與 GC 日誌揭示超過有用壽命而保留的流量記錄物件、不必要的中間轉換物件,以及不可變聚合器版本的持續建立/處置。Minor GC 每隔幾秒發生一次,Major GC 花費數百毫秒——JVM 花在垃圾回收上的時間多於業務邏輯。

套用的解決方案

  1. 更快處理:立即處理流量記錄,快速聚合,釋放參照。最佳化 Pekko 串流階段以最小化物件壽命。
  2. 消除不必要的轉換:直接在階段間路由聚合器,而非轉換為中間類型。
  3. 熱路徑上的可變結構:這有爭議——Scala 最佳實務強調不可變性。但在我們的規模下,不可變性建立了太多物件。我們務實地在熱路徑選擇可變聚合器(其他地方保持不可變性),將效能優先於慣例。切換至可變聚合器將堆積配置降低超過 50%,並顯著減少 GC 停頓時間,儘管需要更仔細的程式碼審查。
  4. 調整時間視窗:平衡資料新鮮度與記憶體壓力。

結果

  • 堆積使用量大幅下降
  • GC 停頓降低至可接受水準(數十毫秒而非數百毫秒)
  • CPU 從垃圾回收釋放以用於業務邏輯
  • 執行個體穩定性改善——不再因記憶體問題而有執行個體 DOWN

經驗:「最佳實務」是起點,而非絕對規則。在獨特規模下,你可能需要偏離慣例。但要基於測量而非推測,並了解權衡。不要在所有地方放棄不可變性——只是在效能資料證明有必要的地方。

挑戰 4:反應式串流複雜度

問題:我們的 Pekko Streams 管線會意外停滯。背壓傳遞不如預期運作。我們難以偵錯為什麼某些串流會在沒有明顯錯誤的情況下停止處理。反應式程式設計心智模型——強調非同步邊界、背壓與需求驅動處理——被證明比預期更難掌握。

我們學到的
具背壓的反應式串流是建構能優雅處理負載尖峰的系統的強大工具。當下游消費者減速(由於暫時負載、GC 停頓或外部系統減速)時,背壓允許上游生產者減速,而非溢位緩衝區或丟棄資料。

但這種力量伴隨複雜度:

  • 非直觀行為:傳統命令式程式碼從上到下流動。反應式串流是需求驅動——下游消費者從上游生產者拉取。這控制反轉並不直觀。
  • 非同步邊界:Pekko Streams 中的 .async 運算子建立處理移至不同執行緒的邊界。這可以改善平行性,但也引進緩衝區大小、需求訊號與錯誤傳遞的複雜度。我們最初誤解何時使用 .async,導致過度平行化的串流,建立比效益更多的開銷。
  • 偵錯困難:當串流停滯時,沒有指向問題的堆疊追蹤。你必須了解內部機制——需求訊號、緩衝區狀態、物化器狀態——才能診斷問題。

我們的做法

  1. 深度學習投入:我們投入大量時間深入了解反應式串流概念。閱讀文件、用小範例實驗,以及建立團隊專業知識。
  2. 簡化模式:在可能的情況下,我們簡化串流圖形。複雜的分支與合併模式強大但難以偵錯。我們偏好具清晰階段邊界的線性流程。
  3. 更好的監控:我們在串流邊界新增指標——追蹤緩衝區大小、元素吞吐量、背壓事件。對串流內部的可見性有助於診斷問題。
  4. 團隊教育:我們記錄學習成果,分享有效模式,並建立關於反應式串流的機構知識。

經驗:強大的抽象需要投入。不要在沒有驗證的情況下假設你了解框架。刻意建立你的心智模型,用實驗測試它,並對你的理解保持謙遜。反應式串流值得為需要優雅處理負載的系統而掌握,但預期有學習曲線。

V2 演進:持續精煉

V1 讓我們上線正式環境。主要架構挑戰——Kafka 延遲、熱節點、記憶體壓力——已解決。但完整規模的正式環境揭示新的最佳化機會。V2 代表將運作系統轉變為正式環境就緒系統的持續精煉。

挑戰 5:持續堆積壓力

問題:儘管有 V1 最佳化,我們仍觀察到高於預期的堆積使用率。GC 指標改善但未達最佳。記憶體分析顯示有改善空間。

根本原因:更深入分析揭示我們仍在階段間進行不必要的物件轉換。我們會在路由至下一階段前將聚合器轉換為完整圖形實體(填入所有屬性),即使下一階段只需要壓縮的聚合器狀態。

解決方案:架構變更為直接在所有階段路由聚合器,只在 Stage 3 持久化前立即轉換為最終圖形實體。這消除了兩個中間轉換步驟與相關物件配置。

結果:堆積使用量進一步下降,GC 停頓變得更不頻繁,記憶體餘裕改善。

挑戰 6:序列化複雜度

問題:SSE 訊息的自訂序列化邏輯偶爾造成難以重現與偵錯的錯誤。程式碼庫不同部分使用不一致的序列化方法。

解決方案:在整個管線中標準化為 JSON 編碼。雖然比二進位序列化略低效,但 JSON 的人類可讀性讓偵錯容易得多,而開銷與其他操作相比可忽略不計。一致性消除了整類錯誤。

結果:序列化相關錯誤消失。因為我們可以直接讀取 SSE 訊息內容,偵錯變得更容易。

挑戰 7:串流處理低效率

問題:即使更好地了解反應式串流,我們的 Pekko 設定仍不理想。我們對某些階段過度平行化,對其他階段則不足。 .async 邊界放置不佳。

解決方案:透過持續分析與實驗,我們調整平行性參數、緩衝區大小,並精煉非同步邊界放置。我們在串流邊界新增監控以識別瓶頸。

結果:吞吐量改善,處理延遲更一致。

挑戰 8:不均勻的圖形資料庫吞吐量

問題:寫入我們圖形資料庫的分佈不均。某些分割區接收大量寫入流量,而其他分割區閒置。這導致節流不均勻啟動,並限制整體寫入吞吐量。

解決方案:在寫入圖形資料庫前實作聚合器批次,並改善跨分割區的分佈邏輯。而非立即寫入每個聚合器,我們將其批次並以協調操作寫入多個實體。

結果:更一致的寫入吞吐量,更好地利用資料庫容量。

挑戰 9:聚合時的資料豐富化

除了核心拓撲圖形,我們需要以額外脈絡豐富化節點。在 Stage 3,持久化圖形實體前,我們從外部來源整合豐富化資料——應用程式健康狀態、擁有權資訊與其他中繼資料。在聚合時而非查詢時執行此豐富化,避免查詢後聯結的效能開銷,並確保每個拓撲節點在查詢時都有完整脈絡。

模式辨識

每個 V2 挑戰遵循相同模式:正式環境揭示假設,分析識別根本原因,針對性修復改善特定指標。測量、假設、驗證、迭代。這就是你在規模下建構的方式——不是一次把所有事情做對,而是持續學習與改進。

時光旅行:持續拓撲重建

我們建構的最強大功能之一是查詢歷史拓撲:「當此事故發生時,呼叫圖看起來如何?」此時光旅行功能需要解決一個有趣的架構挑戰——如何有效儲存與重建跨時間的拓撲。

問題

工程師需要回答時間性問題:事故期間拓撲看起來如何?依賴關係如何演變?傳統方法——完整快照或事件來源——要麼有指數儲存成本,要麼需要緩慢的日誌重播。

我們的做法:具變更追蹤的時間視窗聚合器

我們結合兩種機制:

1. 時間視窗聚合器快照:每個聚合器儲存其 5 分鐘視窗的 startTs 與 endTs 時間戳。這些不可變聚合器以 (entity_id, timestamp) 為鍵持久化在圖形資料庫中,每 5 分鐘提供檢查點狀態。

2. 屬性層級變更追蹤:圖形資料庫在屬性層級維護變更歷史——僅儲存帶有時間戳的變更屬性。這比完整實體複本有效得多,並提供超越 5 分鐘聚合邊界的視窗內精度。

3. 查詢時重建:查詢歷史拓撲時,我們查詢時間範圍的變更歷史 API,擷取所有變更,並依序套用變更以重建拓撲狀態。

此方法提供有效儲存(壓縮的聚合器狀態 + 稀疏屬性變更)、快速擷取(索引的變更歷史,無需日誌重播)與彈性分析(任意時間範圍,無需預先計算所有可能性)。

查詢時重新聚合:我們可以使用與擷取相同的聚合器類別在查詢時進一步聚合歷史資料。這啟用任意 groupby 維度(可用性層級、業務領域、部署叢集),這些維度未預先計算,允許探索性分析而無爆炸性儲存成本。

分散式系統的經驗

雖然這些挑戰特定於服務拓撲,但經驗廣泛適用於規模下的分散式系統。

規模改變一切

每秒 100 個請求可行的東西,在每秒 100,000 個請求時失效。變化不是線性的——是質變。在適度規模下沒問題的方法,在極端規模下遇到根本障礙。

我們旅程中的例子:不可變資料結構在每秒數百萬次配置時建立 GC 壓力;單階段聚合在冪律流量分佈下災難性失敗;標準 gRPC 在大量串流聚合時變得沉重。

經驗:當規模證明合理時,願意打破傳統智慧。但要基於測量,而非推測。

一次最佳化一個瓶頸

分散式系統有級聯瓶頸。修復 Kafka 延遲,你會發現熱節點問題。修復熱節點,你會發現 GC 問題。修復 GC,你會發現序列化低效率。

這不是失敗——是複雜系統的本質。每次最佳化提高吞吐量,壓力下一最弱點。方法:根據影響優先,徹底修復目前瓶頸並以測量確認解決,然後移至下一個。規模下的最佳化是持續的,而非一次性的。

分佈是規模的關鍵

單一聚合點是不可避免的瓶頸。一致性雜湊分散負載,但當資料本身分佈不均(像我們的冪律分佈)時,無法防止集中。

我們具分級重新分佈的三階段管線解決此問題。負載在每個階段的多個分佈點間分散。即使資料高度偏斜,也不會有單一執行個體過載。一般原則:處理規模下的偏斜資料時,在每個階段使用具重新分佈的多階段處理。

目前狀態與影響

Service Topology 目前在正式環境運作,處理來自多個區域的流量記錄、IPC 指標與追蹤,並以次秒延遲提供查詢服務。Netflix 各團隊每天使用它進行事故調查、影響範圍分析、依賴關係理解與正式環境變更管理。此系統已成為維持規模下可靠性的必要基礎設施。

結論

Netflix 的 Service Topology 代表在規模下建構分散式系統的旅程。我們從工程師難以透過分散工具了解依賴關係開始。我們建構使用串流聚合、網路中介解析與時光旅行功能的多層架構。我們學到規模下的最佳化是持續的——測量、迭代、驗證、重複。

我們面臨的挑戰——Kafka 延遲、熱節點、記憶體壓力——需要當資料證明合理時打破傳統智慧。每個修復揭示下一個瓶頸。但這種迭代過程,由持續測量引導,正是讓系統在極端規模下運作的原因。

在下一篇文章中,我們將探討追蹤層整合、跨異質儲存的統一查詢,以及所有三層如何結合以提供全面的拓撲可見性。

致謝

Service Topology 由 Parth JainRakesh SukumarYingwu ZhaoRenzo Sanchez-SilvaNathan Fisher 建構。

特別感謝許多讓這成為可能的 Netflix 工程師——建構更廣泛系統的可觀測性團隊、提供儲存基礎的圖形資料庫平台團隊,以及在整個開發過程中提供寶貴回饋與使用情境的 Platform Modernization Engineering 與 Live 團隊。