追逐微秒:Lighter 的延遲工程實踐

@Lighter_xyz
英語1 天前 · 2026年7月08日
190K
552
79
39
266

TL;DR

深入解析 Lighter 邁向次毫秒級延遲的技術歷程,涵蓋 GC 調優、CPU 親和性以及記憶體管理,旨在為高效能的 Ethereum L2 進行擴展。

簡介

經過數個月的反覆迭代,Lighter 的端到端 p99 已從 280 毫秒降至穩定的 55 毫秒。交易處理的 p99 也從 20–30 毫秒的峰值降至 1 毫秒以下。熱路徑的 apply 時間穩定在 100–250 微秒。所有數據均來自主網,且在大規模環境下實現。

規模有多大?2026 年 6 月 5 日,Lighter 創下單日 8.11 億筆交易的歷史新高,平均每秒處理 9,388 筆交易(TPS),峰值達 20,740 TPS。

Lighter 的排序器(sequencer)使用 Go 語言編寫。交易在 Lighter L2 上執行,並附帶完整的加密證明結算至 L1。關鍵的架構洞察在於:證明過程與執行是解耦的——證明生成為非同步運行,絕不阻塞交易。這意味著交易者體驗到的延遲完全由 Lighter 的執行層和 API 層決定。

Lighter 的 API 層是交易者直接互動的服務。它在記憶體中維護一個完整且最新的交易所狀態快照,並從該本地狀態提供 REST 和 WebSocket 連接。一個關鍵的 goroutine 會接收來自執行引擎的狀態更新,並即時將其套用到記憶體快取中。所有下游服務(WebSocket 推送、API 讀取、交易模擬)都從這些快取中提供服務。

涵蓋的關鍵概念

以下概念提供了本文其餘技術內容中關鍵主題的高層次概述:

在主網上線前發現問題:一個與主網完全相同的負載測試環境,使用合成帳戶和真實負載,並進行遠比生產環境更全面的監控,可在交易者察覺之前捕捉瓶頸。

雙環境可觀測性:負載測試攜帶了生產環境中成本過高的指標;主網則即時監控更新新鮮度、模擬延遲和端到端交易生命週期。兩者結合,提供完整覆蓋。

千次分配致死:系統性地將堆積重的型別和深層複製,替換為堆疊分配的替代方案和不可變快照。在具備垃圾回收(GC)的語言中,更少的分配 = 更少的暫停 = 更低的尾部延遲。

作業系統層級控制:將關鍵執行緒固定到專用 CPU 核心,並設定接近即時的優先級。

硬體感知部署:NUMA 感知的放置確保熱路徑擁有快速的本機記憶體存取。這是延遲堆疊中的最後一層。

二進制勝過便利:在熱路徑上手動實作二進制序列化,消除了反射和分配開銷。每節省一微秒,都會在每秒數千次的更新中疊加放大。

在主網上線前發現瓶頸

看不見的東西就無法修復。在進行任何最佳化之前,我們投入了大量資源建立工具,以精確定位時間花費在何處。

負載測試環境

當我們需要測試某項功能時,會啟動一個專用的負載測試環境:它與主網基礎設施完全相同,使用相同的配置和部署拓撲運行服務。這個環境並非持續運行,而是按需啟動,完成特定測試後即銷毀。

一旦環境啟動,我們會建立合成帳戶並產生真實的交易負載,以模擬市場在壓力下的真實狀況。這允許我們:

  • 超精細計時:熱路徑中每一步的計時、每個操作下快取建置的持續時間,以及從提交到確認的每個階段的交易生命週期時間戳記。
  • 按需分析與飛行記錄器:我們可以在負載測試期間按需擷取 CPU、記憶體和執行追蹤分析。Go 的飛行記錄器讓我們能夠持續收集追蹤資料。當偵測到慢速事件時,會自動擷取最後幾秒的執行記錄,讓我們能夠在事後診斷瞬態延遲峰值。
  • 分散式追蹤:我們為熱路徑中的每個主要函數都添加了追蹤跨度(trace spans),從而能夠精細地查看在單一更新週期內、跨服務邊界時,時間具體花費在何處。

雙環境監控

我們密切監控兩個環境(主網和負載測試),但監控粒度不同。

主網 會進行嚴格的即時監控。我們追蹤所有影響交易體驗的指標:

  • 新鮮度追蹤:我們透過最活躍市場的 order_book WebSocket 通道測量延遲。本質上,就是訂單簿更新產生到客戶端收到之間的時間差。這是直接反映交易者體驗的指標:你看到的訂單簿有多舊?我們在兩個點進行追蹤。最重要的是執行引擎 → 客戶端:從執行引擎處理狀態變更到產生的 order_book 更新到達客戶端的完整管道。我們也測量 API 層 → 客戶端:API 層為每個傳出的更新加上時間戳記,客戶端則與自己的時鐘比較,從而單獨得到最後一英里的延遲。
  • 模擬執行延遲:API 層在將每個交易傳遞給執行引擎之前,會先進行模擬執行(驗證簽名、nonce 和餘額)。每個模擬執行都會被計時並報告。
  • 端到端交易生命週期:從提交到確認的完整週期延遲直方圖。
  • 快取效率:我們從記憶體提供服務的頻率,以及何時回退到較慢的儲存。

負載測試 的監控粒度更細。由於它不服務真實交易者,我們可以不受限地提高監控強度:熱路徑中每一步的計時、分配分析、每次快取變更的額外直方圖桶。這種更細的粒度能捕捉到主網生產安全指標無法揭示的微觀層級回歸。

主網監控告訴我們系統目前為真實交易者提供的表現。負載測試監控則在回歸問題進入生產環境之前就將其捕捉。

深層複製消除與堆積分配戰爭

Go 的垃圾回收器是一種延遲稅。每個堆積分配最終都會變成一次 GC 暫停,而在交易引擎上,GC 暫停對交易者來說就是延遲峰值。因此,我們徹底檢查了熱路徑,盡可能減少分配。結果是更少的延遲峰值和更可預測的效能。

深層複製消除

深層複製是分配工廠。我們透過幾種方式來處理它們:

  • 不可變快照:將訂單簿快取設為不可變。讀取操作會返回一個指向當前快照的指標,無需複製。更新操作則建立一個新版本,並通過 atomic.Pointer 原子性地交換。
  • 移除不必要的複製:找到那些深層複製了資料但後續從未修改的程式碼路徑,並將其徹底移除。

分配意識設計

  • 堆疊分配的數值:在熱路徑中,將堆疊沉重的 big.Intbig.Rat 替換為堆疊分配的替代方案(int128float64int64 除法),應用於價格轉換、訂單簿深度映射、數量計算等。在關鍵格式化函數上速度提升高達 8.3 倍,且每次操作零堆積分配。
  • 條件式更新:當沒有變化時跳過分配。
  • 預先設定大小的集合:預先設定資料結構的大小,消除增長與複製的循環。
  • 緩衝區重複使用:訂閱者路徑每秒需要反序列化數千次更新。我們透過基於池的重複使用機制,減少了中間緩衝區的分配。

這使得尾部延遲變得平穩。在此之前,Lighter 的端到端 p99(執行引擎到客戶端,通過 order_book WebSocket 通道測量)在分配密集的時段會飆升至 200–280 毫秒。在完成深層複製消除、不可變快取和堆積分配工作後,p99 穩定在約 50–60 毫秒的平穩區間,幾乎沒有峰值:

Lighter - inline image

端到端延遲:執行引擎 → 客戶端(p99)

API 層的 GC 壓力也顯著下降,但這不僅僅是一項改變。我們從根本上重新設計了 API 伺服器快取的運作方式。此前,快取依賴 TTL 過期機制,每次更新都會分配新物件。這些短暫存活的分配都成了 GC 的工作。重新設計後,完整的交易所狀態從快照熱啟動,並以長生命週期、不可變的結構保存在記憶體中,通過原子操作進行交換。這消除了 TTL 的波動和每次更新的分配,GC 壓力也隨之下降。我們密切監控記憶體使用量,工作集是可控的。

我們將舊版 API 伺服器與重新設計的 API 伺服器在主網流量下並行運行。重新設計的伺服器 GC 暫停持續時間(p75)約為 3 毫秒,而舊版伺服器約為 5–6 毫秒。GC 暫停時間大約減少了一半:

Lighter - inline image

GC 暫停持續時間:標準 vs. 快照 API 伺服器

頻率更能說明問題。舊版 API 伺服器觸發 GC 週期的頻率約為重新設計版本的 2.2 倍,這直接反映了短壽命分配的減少和整體 GC 壓力的降低:

Lighter - inline image

GC 週期頻率:標準 API 伺服器 vs. 快照 API 伺服器

執行引擎的 GOGC 調優

我們將類似的 GC 優化思路應用到執行引擎本身。Go 的 GOGC 參數控制垃圾回收器的運行積極程度。預設值以 CPU 時間換取記憶體效率,但對於延遲敏感的熱路徑而言,這種取捨是錯誤的。

調整 GOGC 後,執行引擎的 GC 持續時間從平均約 30 微秒(峰值可達 100 微秒)降至穩定的約 10 微秒範圍。實現了約 3 倍的減少,且幾乎沒有峰值:

Lighter - inline image

調整 GOGC 後的執行引擎 GC 持續時間

三個月的主網數據證實了這一改進:調整後的時期表現平穩且可預測。

交易處理也隨之改善。在 90 天的主網數據中,涵蓋所有交易類型(建立訂單、取消、清算、去槓桿、轉帳等),p99 從調整 GOGC 前頻繁的 20–30 毫秒峰值,降至調整後大部分低於 1 毫秒,偶爾出現 3–4 毫秒的離群值。該改進已持續超過兩個月:

Lighter - inline image

交易處理時間 - p99(最近 90 天)

快照服務 — 更少的數據、熱部署

消除分配開銷後,我們解決了下一個瓶頸:內部消息匯流排中流動的數據量。我們建立了一個快照服務,用於維護完整的記憶體狀態快照。這帶來兩個主要影響:

  • 傳輸數據更少。 由於快照服務持有完整狀態,執行引擎不再需要在每次更新時通過消息匯流排推送完整狀態。它寫入的數據更少,這意味著消耗的網路頻寬更少,接收端需要反序列化的工作量也更少。
  • 零預熱部署。 部署時,API 層從快照啟動(帳戶、訂單簿、市場資訊、API 公鑰),並立即準備好提供服務。無需等待快取填充的預熱期。載入快照後,API 層會訂閱更新串流,並即時套用增量變更。

在快照基礎之上,記憶體快取會持續更新:

  • 帳戶資訊:通過 sync.Map 實現無鎖讀取,更新時使用原子指標交換。
  • 訂單簿:儲存為不可變快照。讀取獲取一個指標,更新則交換一個新版本。讀取路徑上無鎖。
  • API 金鑰快取:所有金鑰都保存在記憶體中。完全消除了外部查詢。

結果:整個交易所狀態都位於本地記憶體中,即時更新,每次部署都是熱啟動。

對執行引擎的連鎖效應非常顯著。以前,執行引擎需要將快取金鑰寫入網絡,供其他服務偶爾讀取。現在所有數據都保存在記憶體中,且快照服務負責狀態分發,這些網絡寫入就不再必要了。我們將其刪除。結果:區塊時間的 p99 從約 2.6 毫秒降至約 1.2–1.8 毫秒,僅僅是因為執行引擎現在寫入的數據少了很多:

Lighter - inline image

區塊時間 - p99

CPU 規劃

完成用戶空間的所有優化後,我們開始接觸核心層面。

我們有許多 goroutine,它們需要被調度到 CPU 上執行。對於低延遲系統來說,最小化調度開銷至關重要。預設情況下,Go 運行時會將 goroutine 多路複用到 OS 執行緒上,而 OS 可以自由地將執行緒在各個 CPU 核心之間遷移。這兩者都會引入不可預測的延遲。

我們透過疊加四種機制消除了這些問題:

  1. `runtime.LockOSThread()`:將 goroutine 鎖定到單個 OS 執行緒,防止 Go 調度器遷移它。
  2. 通過 `sched_setaffinity` 設定 CPU 親和性:將該 OS 執行緒固定到特定的 CPU 核心(Linux)。這能防止核心在不同核心之間遷移它,避免 L1/L2 快取失效。
  3. 通過 SCHED_FIFO 設定高優先級調度:提升執行緒的調度優先級,確保核心在調度時優先考慮它。
  4. 忙等待自旋循環:熱路徑運行一個帶有空 default 分支的 select 語句,這樣 goroutine 永遠不會進入暫停狀態。如果沒有這個循環,當沒有數據可用時,Go 會將 goroutine 移至「可運行」狀態,而重新調度會增加喚醒延遲。有了自旋循環,goroutine 會在其固定的核心上持續運行,並以零調度延遲獲取新的更新。

正確衡量影響至關重要。Apply 時間會隨流量條件變化,因此絕對數值會隨負載波動。為了隔離 CPU 固定(pinning)的效果,我們在相同流量下並行運行兩組 API 伺服器。一組保持未固定狀態作為對照組,另一組則啟用 CPU 固定。兩者在同一時間、相同負載下的百分比差異,揭示了真實情況。

在固定之前,兩組的表現一致。相同的基準延遲,相同的峰值行為:

Lighter - inline image

熱路徑 Apply 時間 - CPU 規劃前

對其中一組啟用固定後,它持續低於未固定的基準線。相同的流量,更低的延遲。固定組的峰值也被限制在更低水平,因為執行緒遷移抖動和 L1/L2 快取失效已被消除:

Lighter - inline image

熱路徑 Apply 時間 - CPU 規劃後

NUMA 感知部署

如果固定核心的記憶體存取跨越了 NUMA 邊界,那麼僅靠 CPU 固定是不夠的。NUMA 節點是一組擁有自己本地記憶體的 CPU。從遠端 NUMA 節點存取記憶體會帶來10 倍的懲罰,相比之下本地存取則快得多。

Lighter 的 API 伺服器最初運行在擁有 2 個 NUMA 節點的大型機器上:

text
1$ lscpu | grep NUMA
2NUMA 節點數: 2
3NUMA 節點0 CPU: 0-95
4NUMA 節點1 CPU: 96-191
5
6$ cat /sys/devices/system/node/node0/distance
710 100

距離矩陣說明了問題:本地存取成本為 10,跨節點存取成本為 100。10 倍的懲罰。當多個 API 伺服器共享同一台機器時,不可避免地,有些伺服器的固定 CPU 位於一個 NUMA 節點,而它們的工作記憶體(記憶體快取、更新緩衝區)卻在另一個節點上。每次熱路徑迭代都必須支付跨節點存取的成本。

解決方案反直覺:我們將 API 伺服器遷移到更小、只有單個 NUMA 節點的機器。規格減半,但所有記憶體存取現在都保證是本地存取:

text
1$ lscpu | grep NUMA
2NUMA 節點數: 1
3NUMA 節點0 CPU: 0-95

這同時降低了成本並改善了延遲。熱路徑的 apply 時間進一步降至約 100–250 微秒,離峰時段可降至約 100 微秒。相比之下,在 2 個 NUMA 節點的機器上僅使用 CPU 固定時,該範圍約為 200–520 微秒:

Lighter - inline image

熱路徑 Apply 時間 - NUMA 優化

自訂二進制序列化

Lighter 的執行引擎通過內部消息匯流排將狀態更新發布到 API 層。每個系統更新都流經此路徑。最初的序列化使用了一個通用的編碼庫,但反射、型別開關和每個欄位的分配在熱路徑上造成了不必要的開銷。

我們將其替換為手動實作的二進制序列化:為系統中的每個實體類型提供固定佈局、無反射的編碼/解碼。每個類型都有一個專用的編碼器/解碼器,能夠在已知的字節偏移量處讀寫欄位。沒有反射或型別開關,且分配極少。每個編解碼器都經過往返測試和模糊測試,以捕捉回歸問題。

結果是,在提供所有下游快取的關鍵路徑上,序列化開銷顯著減少。

獨立的基準測試可通過社群成員 @UngusTrade 建置和維護的儀表板進行查閱,該儀表板比較了各永續合約交易場所的即時交易延遲:latency.perps.trading

一鍵儲存

使用 YouMind AI 深度閱讀爆款文章

保存原文、追問細節、總結觀點,並在一個 AI 工作空間裡把爆款文章沉澱成可複用筆記。

了解 YouMind
寫給創作者

把你的 Markdown 變成乾淨的 𝕏 文章

圖片上傳、表格、程式碼區塊,往 𝕏 上手動重排太痛苦。YouMind 把整篇 Markdown 一鍵轉成乾淨、可直接發佈的 𝕏 文章草稿。

試試 Markdown 轉 𝕏

更多可拆解樣本

近期爆款文章

探索更多爆款文章