← 返回 Siami 首頁

Cloudflare 優化 1.1.1.1 DNS 快取 5 個 Rust 改動省下 100TB 記憶體

▲ 360 💬 103
Cloudflare 優化 1.1.1.1 DNS 快取 5 個 Rust 改動省下 100TB 記憶體

編按:本文綜合整理自 Cloudflare Blog〈How we saved 100 terabytes of memory by optimizing 1.1.1.1’s DNS cache〉與 Hacker News 討論,並加入 Siami 編輯部觀點與分析。

事件概要

Cloudflare 旗下負責 1.1.1.1 公開 DNS 解析、Gateway DNS、DNS Firewall 與 AS112 等服務的內部平台 Big Pineapple,儲存超過 250 億筆 DNS 快取條目。工程團隊透過五個層級的 Rust 記憶體優化,把每筆快取條目的記憶體用量從 953 bytes 降到 420 bytes,降幅 56%。整體釋出約 100 TB 記憶體,而且快取寫入吞吐量提升 43%、查詢延遲降低 19%。

這不是一次性的大改寫,而是五個獨立、可以疊加的小改動,每個改動都有明確的基準測試數字支持。


大快取究竟有多大

Big Pineapple 在冷啟動時是空的,隨著 DNS 查詢湧入而逐漸填滿。每筆條目都是 key-value 結構:key 是查詢的網域名稱、記錄類型與認證狀態;value 是時間戳、TTL、命中次數與一連串 DNS 記錄。

為了容納熱門查詢的反覆造訪,Big Pineapple 把每個資料中心的快取灌到接近上限,單一節點的常駐記憶體使用量在 p99 高達 9.3 GB。當一筆條目因為 TTL 過期或主動剔除而被釋出,後面等著進場的查詢結果立刻補位,整個過程幾乎沒有空窗。

Cloudflare 的 DNS 服務不只是網路愛好者的 1.1.1.1,還包含企業級的 Gateway DNS 過濾、DNS Firewall 與 AS112 黑洞路由,所有這些服務共用同一個快取引擎。


五個改動的技術細節

改動 1:用 Box 取代 Vec,消除 capacity 欄位

Rust 標準函式庫的 Vec<T> 內部儲存三個欄位:heap 指標、長度、容量。每次 push 元素時,Vec 都會檢查容量是否足夠。但在快取場景裡,DNS 回應寫入後就不會再修改,capacity 欄位完全沒用,卻白白佔用 8 bytes。

把 8 個 Vec 和 String 欄位改成 Box<[T]> 和 Box<str>,單一條目立刻省下 64 bytes,同時也免去了 reserve 空間的開銷。

改動 2:合併三個 list,用 offset 取代指標

原本 answer、authority、additional 三段 DNS 回應各自用一個 Vec<Record> 儲存,每個 list 各有 8-byte 指標與 8-byte 長度。改成單一 list 加上兩個 2-byte offset 之後,單筆條目再省 28 bytes。

改動 3:刪掉 owner 欄位

DNS 記錄的 owner(網域名稱)在多數情況下跟查詢的網域名稱相同。原本每筆記錄都儲存完整的 owner 字串,但既然大多數情況下可以從 query key 反推,這個欄位在快取裡只是冗餘。

改成 Option<Box<Name>>,當 owner 等於查詢網域時填入 None,執行階段再推回去。光這個改動就讓大多數 A 與 AAAA 記錄免去一次 heap 配置。

改動 4:enum 改裝箱

Rust 的 enum 大小等於最大 variant 的大小。如果把 RecordData 設計成 enum,每個變體直接存資料,那麼 A 記錄(4 bytes)和 AAAA 記錄(16 bytes)都會被 padding 到 enum 最大變體的尺寸。問題是 A 和 AAAA 加起來佔 Cloudflare 流量 80% 以上,大部分條目都在浪費空間。

把較大的 variant 裝箱(Box)到獨立 heap 配置,enum 本身只存 8-byte 指標。

改動 5:直接用 wire format

裝箱有兩個副作用:allocator 會把每個變體對齊到某個邊界造成浪費,且記憶體局部性變差。最後乾脆把 record data 寫成 raw bytes 的 buffer,跟快取條目的其他結構化欄位分開儲存。

這個改動消除了 enum overhead 與裝箱成本,record data 在 buffer 裡緊密排列,回應時多數記錄類型可以直接 memcpy 進外送的封包。


實測數字

指標優化前優化後變化
單筆條目淨用量953 bytes420 bytes-56%
單筆條目 allocations1.1 KB461 bytes-58%
快取寫入吞吐量625,000 entries/s893,000 entries/s+43%
快取查詢延遲828 ns670 ns-19%

在生產環境的量測裡,p99 常駐記憶體從 9.3 GB 降到 5.3 GB,降幅 43%。Cloudflare 表示釋出的記憶體會用來提升快取容量,進而提高命中率。


為什麼這件事重要

DNS 解析是網際網路最基礎的服務之一,每個 HTTP 請求、每封 email、每個 API call 背後幾乎都會觸發 DNS 查詢。Cloudflare 1.1.1.1 是全球最大的免費公開 DNS 之一,承載的查詢量以兆計。在這樣的規模下,每個 byte 的快取條目都會被放大成實際的伺服器成本與能源消耗。

當你的程式語言幫你處理記憶體配置時,很容易忘記它「順便」幫你多配置了多少。

這篇技術文章最有意思的地方,是它把「沒必要的記憶體浪費」拆解成五個獨立的小問題。每個改動都是幾十行 Rust 程式碼的改寫,沒有演算法突破、沒有新架構、沒有重新設計資料流。只是把不必要的欄位刪掉、把動態結構換成靜態結構、把格式化資料換成 wire format。疊加起來卻是 100 TB 等級的釋出。

這也再次驗證一個系統程式設計的老問題:高階抽象(Vec、enum、String)會內建一些「以防萬一」的彈性,像是 Vec 的 capacity、enum 的 padding、String 的 UTF-8 邊界檢查。在小規模時這些成本微不足道,但當單一系統要服務全球流量時,每個抽象的成本都會被放大到不可忽視的程度。


數據解讀與質疑

Cloudflare 在文章裡附了一張 p99 記憶體使用量隨版本演進的折線圖,每個改動上線後可以看到穩定的下降平台。但有幾個數字值得停下來看:

  • 吞吐量 +43% 與延遲 -19% 是副作用,不是目標。 記憶體優化通常會犧牲 CPU,因為要做更多序列化、反序列化、指標計算。這篇文章的數字反過來,意味著 wire format 的設計反而讓 cache hit 路徑變短(直接 memcpy)。如果未來繼續優化,會不會碰到記憶體已經接近極限、但每次省下 1 byte 都要花費 10 倍工程時間的遞減報酬?
  • 100 TB 是「fleet-wide」總和。 Cloudflare 在全球有數百個資料中心,這個數字分散到每個節點後,平均每節點省下的記憶體大約是數 GB。對一個企業級 DNS 服務來說,這代表可以少買幾台機器、少繳一些雲端帳單;但對一般開發者來說,這個規模的優化並不容易複製。
  • 「記憶體變貴」這個假設還能撐多久? Hacker News 留言裡 mhitza 提到「記憶體現在貴了 10 倍」,這對應到 2025-2026 年 DRAM 價格因為 AI 訓練需求暴漲的現象。但這個假設是週期性的,DRAM 價格過去每 3-5 年就會經歷一次波動。如果記憶體成本回落,這種優化的優先順序會下降。

eviks 在留言裡質疑「為什麼一開始設計系統時沒人抓到這種小事」,jgrahamc 回應這正是 premature optimization 的反面教材:當年 Big Pineapple 剛上線時,記憶體成本相對低,工程師把時間花在正確性與快速出貨上才是合理選擇。等到記憶體成本變高,再回頭看這些低垂的果實,工程 ROI 完全不一樣。


Siami 編按

這篇文章的另一個亮點,是 Cloudflare 把基準測試方法寫得非常透明:每個改動都用「隨機產生接近真實流量分佈的條目」來量測,且用自訂 allocator 記錄每次配置的 byte 數。這種基準測試方法論本身就是值得學習的對象。對任何在維運大型系統的人來說,能清楚回答「這個改動到底省了多少記憶體」比「我覺得省了」重要得多。

網友熱門留言 (6)

#1 jgrahamc 0
One of the evils of premature optimization is how much time you spend on the optimization vs. the benefit you get from it. If your goal is correctness and shipping fast and you're not memory constrained then spending time using the least amount of memory is a waste of time specifically because memory costs the same.
#2 stickfigure 0
LLMs have changed the equation somewhat. With a rather short prompt, claude/codex will take your code, write a harness, profile it, build experiments, profile those, and give some pretty solid advice which one to pick. Then integrate the changes.
#3 eviks 0
Were there no design discussions/reviews when the system was setup to catch trivial things like this [storing capacity field for read-only data]?
#4 micromacrofoot 0
It was working so no one thought to check.
#5 mhitza 0
Premature optimization argument fits right in. Now that memory is up to 10x more expensive it is worth considering optimizing programs with large memory footprint.
#6 gbear605 0
Engineers are expensive, especially good system engineers who are trained in your code base. Very possible that this just hadn't gotten to the top of the priority list.