世界を動かす技術を、日本語で。

1.1.1.1のDNSキャッシュを最適化して100テラバイトのメモリを節約する

概要

  • Big Pineapple はCloudflareのDNSサービス基盤で、2500億件超のキャッシュエントリを常時管理
  • キャッシュ構造の5段階最適化で 1エントリあたり50%以上のメモリ削減 を実現
  • 全体で 約100TBのメモリを解放 し、サーバ台数換算で130台分に相当
  • メモリ効率改善だけでなく、 スループット向上・レイテンシ低減 も達成
  • 特にECS利用時や多様なレコード型で最適化効果が大きい

Big PineappleのDNSキャッシュ最適化

  • Big Pineapple は1.1.1.1やGateway DNSなどCloudflareの主要DNSサービスを支えるプラットフォーム
  • 常時 2500億件以上のDNSキャッシュエントリ を保持し、1バイトの無駄も大きなコストに直結
  • 5回にわたるキャッシュエントリ構造の見直しで 1エントリあたりのメモリフットプリントを半減
  • この最適化により、全体で 約100TBのメモリを解放、Gen 13サーバ130台分のRAMに相当
  • キャッシュ挿入スループットは43%増加ルックアップレイテンシは19%減少 の高速化も実現

キャッシュエントリの構造

  • 各エントリは クエリ内容を特定するキー (qname, qtype, 認証情報, タグ)と DNS応答本体 (回答・権威・追加レコード、メタデータ)で構成
  • ECS(EDNS Client Subnet)利用時は、同一クエリでもネットワークごとに異なる応答をキャッシュするため、 エントリ数・消費メモリが増大
  • それぞれの型やデータ構造の見直しが最適化の焦点

メモリ使用量のベンチマーク

  • 本番トラフィック比率に近いエントリ生成 (A:56%、AAAA:25%、TXT:19%、1~4レコード)
  • Rustのカスタムアロケータで 割当数・サイズをトラッキング
  • 挿入スループット・ルックアップレイテンシも同時測定 し、パフォーマンス劣化がないことを検証

Vec<T>からBox<[T]>への移行

  • Vec<T>は不要な容量管理情報(8バイト)や過剰割当分のヒープ領域を保持
  • 一度キャッシュしたDNS応答は不変なので、 Box<[T]> に置換し容量情報や未使用領域を排除
  • StringもBox<str>に置換 し、8フィールド合計で1エントリ64バイト削減
  • 全体で 15TB超のメモリ削減 を達成

リスト数とポインタ削減

  • 回答・権威・追加セクションを個別リストで保持→単一リスト+オフセット管理 に変更
  • 各セクション開始位置を u16のオフセット (2バイト)で記録し、2つのリスト分(28バイト)削減
  • ブール値のビットフラグ化 でパディング領域も圧縮

レコードオーナーの最適化

  • 各DNSレコードのオーナー(ドメイン名)は多くがクエリドメインと一致
  • 一致時はオーナー情報を省略し、必要時のみヒープ領域に格納
  • 参照時はキャッシュキーからドメイン名を復元し、 ほとんどのレコードでヒープ割当不要

Enumサイズの最適化

  • Rustのenumは最大バリアントサイズに揃えるため、A/AAAAレコードでも大きな無駄が生じる
  • 例:NAPTR型が136バイト→enum全体が144バイトに
  • A/AAAA型は4~16バイトで十分だが、enumパディングで120バイト以上浪費

バリアントのBox化

  • 大きいバリアントのみBox化しヒープ領域に分離
  • enum本体は8バイトのポインタのみ保持し、 A/AAAAは小型・高速化
  • 割当オーバーヘッドやメモリ局所性低下 のコストはあるが、NAPTR等の大きな型は稀なため全体最適

レコードのワイヤフォーマット格納

  • 全レコードをワイヤフォーマット(生バイト列)でBox<[u8]>に格納
  • enumやBox化によるオーバーヘッドを排除し、データを連続領域にパック
  • CPUキャッシュ効率向上、A/AAAA/TXT/DNSSEC型は直接バイトコピーで高速応答
  • インデックスアクセス不可・シーケンシャル走査が必要 だが、1エントリあたりのレコード数は少なく実用上問題なし

メモリ・パフォーマンス最適化の効果

  • メモリ消費の大幅削減(合計100TB、最大50%以上)
  • ヒープ断片化・パディングの副次的削減
  • スループット向上・レイテンシ低減による高速化
  • ECSや可変長レコード型が多い拠点での効果が特に大きい

まとめ

  • Big Pineappleのキャッシュ最適化は、メモリ効率とパフォーマンスの両立 を実現
  • データ構造の見直し・Rustの型最適化・ワイヤフォーマット利用 が鍵
  • 大規模分散DNSサービス運用における実践的な最適化事例

Hackerたちの意見

だからこそ、システムプログラミングは今でも重要なんだよね。だけど、CacheEntryのメンバーのすぐ後にレコードデータを配置するっていう明らかな最適化を見逃してるみたい。これはCプログラマーの私が言ってるだけかもしれないけど、Rustではそんなに簡単じゃないかもね。

興味がある人のために言うと、これはRustで動的サイズの型を使えば技術的には可能なんだけど、実際には難しくて言語の他の部分とうまく連携しないんだよね。nomiconのエントリーは「はい、カスタムDSTは今のところほとんど未完成な機能です。」で締めくくられてるよ。

使い勝手は悪いけど、全然できないわけじゃない。

システムプログラミングは常に重要だよ。物事は安いけど、いつかはそうじゃなくなるからね。

CacheEntryの保存方法によるね。おそらく&[CacheEntry]のスライスに保存されてるから、各エントリーのサイズが固定されてる必要があって、レコードデータを一緒に保存することはできないんだ。

残念ながら、Rustはこういうトリックにはあまり向いてないね。ここがZigの強みなんだ。Rustでは、アロケーションに大いに役立つ適切なアリーナすら使えないから。Cloudflareは最近、メモリ制約のあるプロジェクトのためにZigを選び始めたよ。

自分のMaraDNSでは、ブラックリストのエントリーのメモリ使用量を攻撃的に最適化したんだ。一つの大きなmalloc()でエントリーのメモリを確保して、そのメモリブロックを使って潜在的なブラックリストエントリーを走査するって感じ。エントリーごとにmalloc()を使ってた時は、大きなブラックリストが237メガバイトのメモリを占めてたけど、最適化して一回のmalloc()で読み込むようにしたら、たったの9.5メガバイトになったよ。

記事が答えてない疑問が一つあるんだけど、なんでキャッシュを使ってるの?もしキャッシュがそんなに大きいなら、それはキャッシュじゃないよね。問題のデータセットはどれくらい大きいの?2500億エントリーがあるって。80/20の法則を考えると、1.25兆レコードってこと?データソースに対するサービスの速度や応答時間はどうなの?その時点で、複数のキャッシュを少ないRAM内データベースに置き換えるのが十分かもしれないね。面白い問題だよ。

もしかしたら誤解してるかもしれないけど、これは1.1.1.1を支えてるだけで、内部データセットを前面に出してるわけじゃないよ。キャッシュミスはネームサーバーに当たるから、つまりデータセットは「世界中のすべてのDNSレコード」ってことだね。

彼らはすべてのサーバーで消費されるキャッシュを加算してるんだ。一つの巨大な深いキャッシュじゃないよ。

キャッシュを使わないとダメだよ。Cloudflareは全てのレコードを事前に把握してるわけじゃないから、権威サーバーに再帰的に問い合わせをしないといけないんだ。それもTTLの期間だけ有効だし、グローバルなDNSレコードデータベースなんてものは存在しないよ。

これは再帰的リゾルバーだね。グローバルDNSデータセットは、直接提供するために集められるものじゃなくて、観察からキャッシュするものなんだ。データソースは、第三者が運営する権威あるネームサーバーで、遅いものもあれば、遅延や損失のあるネットワークの背後にあるものもある。オリジンの応答時間は、たぶん1ミリ秒から2秒の間で、応答しないオリジンもある。

Hacker Newsで議論の続きを見る