以前、私がセカンダリDNSについてのブログを書いて以来、CloudflareのDNSトラフィックは、月間15.8兆DNSクエリーから38.7兆へと倍以上に増加しています。当社のネットワークは現在、100カ国以上270都市以上に広がり、世界中で10,000万以上のネットワークと相互接続しています。w3 statsによると、「Cloudflareは全Webサイトの15.3%にDNSサーバープロバイダーとして利用されている」とのことです。これは、可能な限り最速かつ最も信頼性の高い方法でDNSを提供するために、当社が大きな責任を担っていることを意味しています。
DNSクエリーの応答時間は最も重要なパフォーマンス指標ですが、時として注目されない別の指標があります。DNSレコードの伝達時間は、当社のAPIに送信された変更が当社のDNSクエリーの応答に反映されるまでの時間です。DNSレコードの伝達時間は、お客様が素早く設定を変更し、システムをより俊敏にするために、1ミリ秒単位で重要視されます。当社のDNS伝達パイプラインはすでに非常に高速であることが知られていましたが、実装すればパフォーマンスを大幅に改善できるいくつかの改善点を特定しました。このブログでは、DNSレコードの伝達速度を劇的に改善した方法と、それがお客様に与える影響について説明します。
DNSレコードの伝達方法
Cloudflareは、お客様のDNSレコードの変更を多段階のパイプラインで受け取り、当社のグローバルネットワークにプッシュするため、世界中で利用することができます。
上図に示す手順:
お客様がDNS Records API(またはUI)を使ってレコードを変更します。
変更内容はデータベースに永続化されます。
データベースイベントは、Zone Builderによって消費されるKafkaメッセージをトリガーします。
Zone Builderはメッセージを受け取り、データベースからゾーンの内容を収集し、分散KVストアであるQuicksilverにプッシュします。
そして、Quicksilverはこの情報をネットワークに伝達させます。
もちろん、これは起きていることを簡略化したものです。実際には、当社のAPIは1秒間に何千ものリクエストを受け取っています。すべてのPOST/PUT/PATCH/DELETEリクエストは、最終的にDNSレコードの変更につながります。APIやCloudflareダッシュボードに表示される情報が、DNSクエリーに応答するために使用する情報と最終的に一致するように、これらの各変更を処理する必要があります。
これまで、DNSの伝達パイプラインにおける最大のボトルネックの1つは、上記の手順4で示したZone Builderでした。グローバルネットワークに書き込まれるレコードの収集と整理を担うZone Builderは、特に大きなゾーンの伝達時間の大半を占めていました。スケールを拡大していく上で、システムに存在する可能性のあるボトルネックを取り除くことは重要であり、これはそのようなボトルネックの1つとして明確に認識されていました。
産みの苦しみ
上記のパイプラインが最初に発表されたとき、Zone Builderは1秒間におよそ5から10件のDNSレコードの変更を受け取っていました。当時のZone Builderは以前のシステムより大幅に改善されましたが、Cloudflareが経験していた成長および今なお経験している成長を考えると、長く続くことはありませんでした。現在では、1秒間に平均250件のDNSレコードが変更されており、これはZone Builderが最初に発表されたときの25倍という驚異的な伸びを示しています。
Zone Builderが最初に設計された方法は、非常にシンプルなものでした。ゾーンが変更されると、Zone Builderはそのゾーンのデータベースからすべてのレコードを取得し、Quicksilverに保存されているレコードと比較します。違いがあれば修正し、データベースとQuicksilverの間の一貫性を維持します。
これは、フルビルドとして知られています。フルビルドは、各DNSレコードの変更が1つのゾーン変更イベントに対応するため、非常に効果的です。これは、複数のイベントをバッチ処理し、その後必要に応じてドロップできることを意味します。例えば、ユーザーが自分のゾーンに10回変更を加えた場合、これは10個のイベントになります。Zone Builderはとにかくゾーンの全レコードを取得するので、ゾーンを10回ビルドする必要はありません。最終的な変更が送信された後に、1回ビルドすれば良いだけなのです。
ゾーンに100万レコードや1,000万レコードが含まれる場合はどうなるのでしょうか?これは非常に現実的な問題です。Cloudflareがスケーリングしているだけでなく、我々の顧客も一緒にスケーリングしているからです。今日、当社最大のゾーンは現在数百万レコードを有しています。当社のデータベースはパフォーマンスのために最適化されていますが、100万レコードを含むフルビルドでさえ、最大で35秒を要し、これは主にデータベースクエリーの遅延によるものです。さらに、Zone Builderがゾーンの内容をQuicksilverに保存されているレコードと比較する際に、Quicksilverからゾーンのすべてのレコードを取得する必要があり、時間がかかってしまいます。しかし、その影響は一人の顧客だけにとどまりません。データベースから読み込む他のサービスのリソースも消費し、Zone Builderが他のゾーンをビルドする速度も遅くなってしまうのです。
レコード単位のビルド:新しいビルドタイプ
この問題のソリューションは、すでに頭の中にある方も多いのではないでしょうか。
なぜ、Zone Builderは、変更されたレコードをデータベースにクエリ―し、単一のレコードだけを伝達させないのでしょう
もちろん、これが正しいソリューションであり、最終的に行き着いた先でもあります。しかし、そこに至るまでの道のりは、見かけほど単純なものではありませんでした。
まず、当社のデータベースは、ゾーンタッチ時にPostgreSQL Queue(PGQ)イベントを作成し、最終的にKafkaイベントに変換する一連の関数を使用しています。当初、当社は個々のレコードイベントを区別していなかったため、Zone Builderはデータベースにクエリ―するまで、実際に何が変更されたのかが分かりませんでした。
次に、Zone Builderはレコードに加えて、依然としてDNSゾーンの設定を担います。DNSゾーン設定の例としては、カスタムネームサーバーコントロールやDNSSECコントロールなどがあります。そのため、当社のZone Builderは、特定のビルドタイプを意識して、互いに踏み込まないようにする必要がありました。さらに、レコード単位のビルドは、各イベントを個別に処理する必要があるため、ゾーンのビルドと同じようにバッチ処理することができません。
その結果、全く新しいスケジューリングシステムを書く必要がありました。最後に、異なるスケジューラの種類に対応するため、Quicksilverのインタラクションを書き直す必要がありました。これらの問題は、以下に内訳できます:
変更されたレコードの情報を含む、レコード変更用の新しいKafkaイベントパイプラインを作成する。
Zone Builderを、ある定義されたスケジューラインタフェースを実装する新しいタイプのスケジューラに分離する。
イベントを正しい順序で1つずつ読み込むために、レコード単位のスケジューラを実装する。
レコード単位のスケジューラのための新しいQuicksilverインターフェイスを実装する。
以下は、新しいスケジューラタイプを持つ新しいZone Builderの内部を表したハイレベルな図です。
この2つのスケジューラ間でロックをかけることは非常に重要です。そうしないと、フルビルドスケジューラがレコード単位のスケジューラの変更を古いデータで上書きしてしまう可能性があるからです。
このレコード単位のアーキテクチャは、DNSSECによる否定的回答に対するCloudflareのブラック・ライ・アプローチを使用しなければ、何も実現できないことに注意することが重要です。通常、DNSSECによる否定的回答を適切に提供するためには、ゾーン内のすべてのレコードが正規にソートされていなければなりません。これはApexレコードからゾーン内の全レコードへの参照リストを維持するために必要です。否定的回答に対するこの通常のアプローチでは、ゾーンに追加された単一のレコードは、このソートされた名前のリスト内での挿入ポイントを決定するために全てのレコードを収集する必要があります。
バグ
すべてが順調に進んだCloudflareブログを書ければいいのですが、決してそうはいきません。バグは起こるものですから、それに対応し、次回はこの特定のバグが起こらないように自分自身をセットアップする準備が必要です。
今回発見された大きなバグは、Quicksilverの古いレコードのクリーンアップに関連するものでした。Zone Builderをフル活用すれば、データベースとQuicksilverの両方に存在するレコードを正確に把握できる贅沢があります。このため、書き込みとクリーンアップがかなり簡単な作業になります。
レコード単位のビルドが導入されたとき、作成、更新、削除などのレコードイベントはすべて異なる方法で処理される必要がありました。作成と削除は、Quicksilverからレコードを追加するか削除するかのどちらかであるため、非常に単純です。更新は、PGQがKafkaイベントを生成する方法によって、予期せぬ問題を引き起こしました。レコードの更新には新しいレコードの情報しか含まれていないため、レコード名が変更されたときに、古いレコードをクリーンアップするためにQuicksilverで何をクエリーすればよいかを知る術がありませんでした。つまり、お客様がDNS Records APIでレコードの名前を変更しても、古いレコードは削除されないということでした。最終的には、これらの特定の更新イベントを作成と削除の両方のイベントに置き換えることで、この問題は解決され、Zone Builderは古いレコードをクリーンアップするために必要な情報を得ることができました。
どれもロケット手術のようなものではありませんが、当社はCloudflareのスケーリングに合わせて成長するよう、エンジニアリングの労力を費やしてソフトウェアを継続的に改良しています。そして、何百万ものドメインが当社に頼っているときに、Cloudflareのこのような基本的な低レベルの部分を変更することは困難なことなのです。
結果
今日、すべてのDNS Records APIのレコード変更は、Zone Builderによってレコード単位のビルドとして扱われます。以前述べたように、フルビルドを完全に取り除くことはできていませんが、現在ではDNSビルド全体の約13%を占めています。この13%は、ゾーン全体の内容を知る必要があるDNS設定に加えられた変更に相当します。
以下のように2つのビルドタイプを比較すると、レコード単位のビルドはフルビルドよりも平均して150倍速いことがわかります。以下のビルド時間には、データベースクエリー時間とQuicksilverの書き込み時間の両方が含まれています。
そこからQuicksilverを通じて、当社のレコードがグローバルネットワークに伝達されるのです。
上記の150倍は平均値に対する改善ですが、冒頭で述べた4000倍はどうでしょうか。ご想像の通り、ゾーンのサイズが大きくなればなるほど、フルビルド時間とレコード単位のビルド時間の差も大きくなります。100万レコードのテストゾーンを使用してレコード単位のビルドを何度か行い、その後フルビルドを何度か行いました。その結果を以下の表に示します。
ビルドタイプ
ビルド時間 (ms)
レコード単位#1
6
レコード単位#2
7
レコード単位#3
6
レコード単位#4
8
レコード単位#5
6
フル#1
34032
フル#2
33953
フル#3
34271
フル#4
34121
フル#5
34093
レコード単位のビルドを5回行った場合、ビルド時間は8ミリ秒以下であることが分かります。しかし、フルビルドを実行した場合、ビルド時間は平均34秒でした。これは、4250倍のビルド時間の短縮になります!
平均的なサイズのゾーンと大規模なゾーンの両方のビルド時間を考えると、Cloudflareのすべてのお客様がこのパフォーマンス向上の恩恵を受けていることは明らかであり、恩恵はゾーンのサイズが大きくなる場合にのみ向上します。さらに、Zone BuilderはデータベースとQuicksilverのリソースをあまり使用しないので、他のCloudflareシステムは、向上した処理能力での稼働が可能となるのです。
次のステップ
この結果は非常にインパクトのあるものでしたが、当社はさらに良い結果を出すことができると考えています。将来的には、フルビルドを完全に排除し、ゾーン設定のビルドに置き換えることを計画しています。すべてのレコードに加えてゾーン設定を取得する代わりに、ゾーン設定ビルダーはゾーンの設定を取得し、それをQuicksilver経由でグローバルネットワークに伝達させるだけでよいのです。レコード単位のビルドと同様に、ゾーン設定の複雑さとそれに触れるアクターの数のために、これは困難な課題です。最終的にこれが実現できれば、フルビルドを正式に引き上げて、当社が長年にわたって成長させてきたスケールのgitの履歴にリマインダーとしてそれを残すことができます。
また、レコードの変更をグループにまとめて、データベースやQuicksilverに問い合わせる回数を最小限にするバッチシステムを導入する予定です。
このような技術的・運用的な課題を解決することにワクワクするでしょうか?Cloudflareはエンジニアリングや他のチームにおいて常に才能あるスペシャリストやゼネラリストを採用しています。