編按:本文綜合整理自 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 bytes | 420 bytes | -56% |
| 單筆條目 allocations | 1.1 KB | 461 bytes | -58% |
| 快取寫入吞吐量 | 625,000 entries/s | 893,000 entries/s | +43% |
| 快取查詢延遲 | 828 ns | 670 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)