新規投稿のお知らせを受信されたい方は、サブスクリプションをご登録ください:

RustとWasmがCloudflareの1.1.1.1をどのようにパワーアップさせるか

2023/02/28

12分で読了

2018年4月1日、Cloudflareは、1.1.1.1パブリックDNSリゾルバ―を発表しました。長年にわたりトラブルシューティングのデバッグページ、グローバルキャッシュパージ、Cloudflareのゾーンの0 TTL、アップストリームTLS、およびファミリー用の1.1.1.1をプラットフォームに追加してきました。このブログ記事では、舞台裏の詳細と変更点をいくつか共有したいと思います。

プロジェクト開始当初は、DNSリゾルバーとしてKnot Resolverが選ばれていました。私たちは、Cloudflareのユースケースに合うように、その上にシステム全体を構築し始めたのです。DNSプロトコルの実装を気にすることなく、他のことにエネルギーをつぎ込めるため、DNSSECバリデーターだけでなく、実働環境でテスト済みのDNS再帰性リゾルバーを持つことは素晴らしいことでした。

Knot Resolverは、Luaベースのプラグインシステムという点で、非常に柔軟性があります。そのおかげで、DoH/DoT、ロギング、BPFベースの攻撃軽減、キャッシュ共有、反復ロジックのオーバーライドなど、さまざまな製品機能をサポートするためにコア機能を素早く拡張することができました。トラフィックが増大するにつれ、ある種の限界に達しました。

私たちが学んだ教訓

深く掘り下げる前に、Cloudflareのデータセンターのセットアップを簡略化して俯瞰してみましょう。この後話す内容を理解するのに役立ちます。Cloudflareでは、すべてのサーバーが同一です。あるサーバーで動作するソフトウェアスタックは、構成が異なるだけで、他のサーバーとまったく同じように機能します。この設定により、フリートメンテナンスの煩雑さを大幅に軽減することができます。

図1 データセンターのレイアウト

リゾルバはデーモンプロセスであるkresdとして動作しており、単独で動作することはありません。リクエスト、特にDNSリクエストは、Unimogによってデータセンター内のサーバーに負荷分散されます。DoHリクエストは、当社のTLSターミネーターで終端されます。コンフィグやその他の小さなデータは、Quicksilverによって世界中に数秒で配信することができます。これらの支援により、リゾルバーは、トランスポートプロトコルの詳細を気にすることなく、DNSクエリの解決という自身の目標に集中することができます。さて、ここで私たちが改善したいと考えた3つの重要な領域、すなわちプラグインにおけるI/Oブロッキング、キャッシュスペースのより効率的な使用、そしてプラグインの分離について説明しましょう。

イベントループをブロックするコールバック

Knot Resolverは、コア機能を拡張するための非常に柔軟なプラグインシステムを備えています。プラグインはモジュールと呼ばれ、コールバックを基本としています。リクエスト処理中のある時点で、これらのコールバックは現在のクエリコンテキストで呼び出されます。これにより、モジュールは、リクエスト/レスポンスを検査、変更、さらに生成する能力を得ることができます。設計上、これらのコールバックは、基礎となるイベントループのブロックを避けるために、シンプルであるべきです。これは、サービスがシングルスレッドであり、イベントループが同時に多くのリクエストに対応する役割を担っているため、重要です。つまり、1つのリクエストがコールバックで保留されるだけでも、コールバックが終了するまで、他の同時進行のリクエストは進行できないことになります。

このセットアップは、例えば、クライアントに応答する前にQuicksilverからデータを引き出すなど、ブロッキング操作が必要になるまで、十分に機能しました。

キャッシュの効率化

あるドメインに対するリクエストは、データセンター内のどのノードにも到達する可能性があるため、他のノードがすでに答えを持っているのに、クエリを繰り返し解決するのは無駄なことです。直感的に、キャッシュをサーバー間で共有できれば遅延が改善されると考え、新たに追加されたキャッシュエントリをマルチキャストするキャッシュモジュールを作成しました。同じデータセンターにあるノードは、イベントをサブスクライブし、ノードのローカルキャッシュを更新できます。

Knot Resolverのデフォルトのキャッシュ実装は、LMDBです。これは、小規模から中規模のデプロイメントでは、高速で信頼性の高いものです。しかし、私たちの場合、キャッシュの立ち消えが間もなく問題になりました。キャッシュ自体は、TTLや人気度などを追跡しません。キャッシュが一杯になったら、すべてのエントリを消去してやり直すだけです。ゾーン列挙のようなシナリオでは、後に検索される可能性が低いデータでキャッシュが一杯になってしまう可能性があります。

さらに、マルチキャストキャッシュモジュールでは、有用でないデータを全ノードに増幅し、同時にキャッシュハイウォーターマークに誘導することで、事態を悪化させました。その後、すべてのノードが同じ時刻にキャッシュを削除してやり直したため、遅延が急増しました。

モジュールの分離

Luaモジュールの数が増えるにつれて、デバッグがますます困難になってきました。これは、1つのLuaの状態をすべてのモジュールで共有するため、あるモジュールが誤動作すると他のモジュールに影響を与える可能性があるためです。例えば、コルーチンが多すぎたり、メモリ不足になったりと、Luaのステート内部で何か問題が発生したとき、プログラムがクラッシュすればラッキーですが、その結果のスタックトレースは読みづらいものでした。また、実行中のモジュールは、Luaランタイムだけでなく、FFIにも状態を持つため、メモリ安全性が保証されず、強制的に破棄したり、アップグレードすることは困難でした。

BigPineappleの導入

私たちのややニッチな要求を満たすような既存のソフトウェアは見つからなかったため、結局、Cloudflareは自社内での構築を始めました。最初の試みは、Knot ResolverのコアをRustで書かれたThin Serviceでラップすることでした(修正型edgedns)。

これは、ストレージとC/FFIタイプの間で常に変換する必要があることや、その他のいくつかの奇妙な点(たとえば、キャッシュからレコードを検索するABIは、次の呼び出しまたは読み取りトランザクションの終了まで、返されたレコードが不変であることを期待します)のために困難であることが判明しました。しかし、このようにホスト(サービス)がゲスト(リゾルバーコアライブラリ)に何らかのリソースを提供するような分割機能を実装しようとしたとき、そのインターフェイスをどうすればより良いものにできるのか、多くのことを学びました。

その後、再帰的なライブラリ全体を、非同期ランタイムを中心とした新しいライブラリに置き換えました。そして、設計し直したモジュールシステムがそれに追加され、より多くのコンポーネントを交換しながら、時間をかけてこっそりサービスをRustに書き換えていきました。この非同期ランタイムはtokioで、ノンブロッキングとブロッキングの両方のタスクを実行するためのきちんとしたスレッドプールインターフェイスと、他のクレート(Rustライブラリ)と連携するための優れたエコシステムを提供しています。

その後、futuresコンビネーターが面倒になったので、すべてをasync/awaitに変換するようになりました。これはRust 1.39async/await機能が搭載される前の話です。そのため、しばらくはnightly(Rust beta)を使っていましたが、いくつかの不都合がありました。async/awaitが安定すると、Goに似た人間工学的なリクエスト処理ルーチンを書くことができるようになりました。

すべてのタスクは同時に実行することができ、特定のI/Oの重いタスクは、より細かいスケジューリングの恩恵を受けるために、より小さな断片に分割することができます。ランタイムは単一スレッドではなくスレッドプール上でタスクを実行するため、タスクの横取り(Work Stealing)の恩恵も受けられます。これにより、以前は一つのリクエストの処理に多くの時間がかかり、イベントループ上の他のリクエストがすべてブロックされてしまうという問題がありましたが、これを回避することができるようになります。

図2 コンポーネントの概要

最後に、私たちが満足できるプラットフォームを作り上げ、BigPineappleと呼ぶことにしました。上図は、BigPineappleの主要なコンポーネントと、コンポーネント間のデータの流れの概要を示しています。BigPineappleの内部では、サーバーモジュールがクライアントからのインバウンドリクエストを受け取り、それを検証して統一されたフレームストリームに変換し、ワーカーモジュールで処理することができます。ワーカーモジュールはワーカーのセットを持ち、そのタスクはリクエストに含まれる質問に対する答えを見つけ出すことです。各ワーカーはキャッシュモジュールとやりとりして、答えがそこにあってまだ有効かどうかをチェックし、そうでない場合は再帰的にクエリを反復するためにリカーソルモジュールを駆動します。リカーサーはI/Oを行わず、何か必要なときにサブタスクをコンダクターモジュールに委譲します。その後、コンダクターはアウトバウンドクエリーを使用して、上流のネームサーバーから情報を取得します。このプロセス全体を通して、一部のモジュールはSandboxモジュールと対話し、内部でプラグインを実行することによってその機能を拡張することができます。

そのいくつかをより詳しく見て、私たちが以前抱えていた問題をどのように克服したかを見ていきましょう。

I/Oアーキテクチャの更新

DNSリゾルバーは、クライアントと複数の権威あるネームサーバとの間のエージェントとみなすことができます。これは、クライアントからリクエストを受け取り、上流のネームサーバーから再帰的にデータを取得し、応答を構成してクライアントに送り返します。つまり、インバウンドとアウトバウンドの両方のトラフィックを持ち、それぞれサーバーとコンダクターのコンポーネントで処理されるわけです。

サーバーは、異なるトランスポートプロトコルを使用するインターフェイスのリストでリッスンします。これらは後に「フレーム」というストリームに抽象化されます。各フレームは、DNSメッセージの高レベルの表現であり、いくつかの追加のメタデータが含まれています。その下には、UDPパケット、TCPストリームのセグメント、HTTPリクエストのペイロードがありますが、これらはすべて同じように処理されます。フレームは非同期タスクに変換され、そのタスクの解決を担当するワーカーに拾われます。完成したタスクは、再びレスポンスに変換され、クライアントに返されます。

このようにプロトコルとそのエンコーディングを「フレーム」で抽象化することで、枯渇を防ぐための公平性の確保や、サーバーの過負荷を防ぐためのペーシング制御など、フレームソースを制御するためのロジックを簡素化することができました。これまでの実装で一つわかったことは、一般に公開されているサービスでは、I/Oのピーク性能は、クライアントのペースを公平に保つ能力よりも重要であるということです。これは主に、各再帰リクエストの時間や計算量が大きく異なり(例えばキャッシュヒットとキャッシュミス)、それを事前に推測することが困難なためです。再帰的サービスにおけるキャッシュミスは、Cloudflareのリソースだけでなく、問い合わせを受ける権威あるネームサーバーのリソースも消費するため、その点には注意が必要です。

サーバーの反対側には、すべてのアウトバウンド接続を管理するコンダクターがあります。上流に手を伸ばす前に、いくつかの質問に答える手助けとなります。例えば、遅延の点で、最も速く接続できるネームサーバーはどれか?すべてのネームサーバーに到達できない場合はどうすればよいか?接続に使用するプロトコルは何か?また、より良い選択肢はあるか?などです。コンダクターは、RTT、QoSなどのアップストリームサーバーのメトリクスを追跡することで、これらの決定を下すことができます。この知識により、上流の容量やUDPパケットロスなどを推測し、例えば、前のUDPパケットが上流に届かなかったと思ったときに再試行するなど、必要なアクションを取ることができます。

図3 I/Oコンダクター

図3は、コンダクターに関する簡略化したデータフローです。これは、上流のリクエストを入力として、前述のエクスチェンジャーから呼び出されます。リクエストは最初に重複が排除されます。つまり、小さなウィンドウで、多くのリクエストがコンダクターにやってきて同じ質問をした場合、そのうちの1つだけが通過し、他のものはキューに入れられます。これはキャッシュエントリが期限切れになったときによくあることで、不必要なネットワークトラフィックを減らすことができます。次に、リクエストと上流のメトリクスに基づいて、接続インストラクターは、利用可能な場合はオープン接続を選ぶか、パラメータのセットを生成します。これらのパラメータにより、I/O実行者は上流に直接接続したり、当社のArgo Smart Routingテクノロジーを使用して別のCloudflareデータセンターを経由するルートを取ることもできます。

キャッシュ

再帰的なサービスにおけるキャッシュは、サーバーがキャッシュされたレスポンスを1ミリ秒未満で返すことができる一方で、キャッシュミスで応答するのに数百ミリ秒かかるため、重要です。メモリは有限のリソース(Cloudflareのアーキテクチャでは共有リソース)であるため、私たちはキャッシュのためのスペースをより効率的に使用することを改善目標と掲げました。新しいキャッシュは、KV Storeの代わりにキャッシュ置換データ構造(ARC)で実装されています。これにより、人気のないエントリは徐々に除去され、データ構造はスキャンに強いため、1つのノード上のスペースを有効に活用することができます。

さらに、以前のようにマルチキャストでデータセンター全体にキャッシュを複製するのではなく、BigPineappleは同じデータセンター内のピアノードを認識し、自分のキャッシュにエントリーが見つからない場合は、あるノードから別のノードにクエリーを中継します。これは、各データセンターの健全なノードにクエリーを一貫してハッシュ化することで実現されています。そのため、例えば同じ登録ドメインに対するクエリーは、同じサブセットのノードを経由することになり、キャッシュのヒット率が高まるだけでなく、ネームサーバーの性能や機能に関する情報を保存するインフラストラクチャキャッシュにも役立ちます。

図4 更新されたデータセンターのレイアウト

非同期型再帰ライブラリ

再帰ライブラリは、クエリーで答えを見つける方法を知っているので、BigPineappleのDNSの頭脳と言えます。ルートから始まり、クライアントのクエリーをサブクエリーに分解し、それを使ってインターネット上の様々な権威あるネームサーバーから再帰的に知識を収集します。その成果物が「答え」です。async/awaitのおかげで、以下のような関数として抽象化できます:

async fn resolve(Request, Exchanger) → Result<Response>;

この関数には、与えられたリクエストに対するレスポンスを生成するために必要なロジックがすべて含まれていますが、それ自体ではI/Oを行いません。その代わりに、上流の権威あるネームサーバと非同期にDNSメッセージを交換する方法を知っているExchangerトレイト(Rustインターフェイス)を渡します。Exchangerは通常、さまざまなawaitポイントで呼び出されます。たとえば、再帰が始まるとき、最初に行うことの1つは、そのドメインに最も近いキャッシュされたデリゲートを検索することです。最終的なデリゲートがキャッシュにない場合は、このドメインに責任を持つネームサーバーを尋ね、それ以上の処理を行う前に応答を待つ必要があります。

この設計のおかげで、「いくつかの応答を待つ」部分を再帰的なDNSロジックから切り離すことができ、Exchangerのモック実装を提供することでテストがより簡単になります。さらに、再帰的反復コード(特にDNSSECの検証ロジック)は、多くのコールバックに散らばっているのではなく、順次書かれているため、より読みやすくなっています。

楽しい事実:DNS再帰性リゾルバーをゼロから書くのは、まったく楽しくありません!

これは、DNSSEC検証の複雑さだけでなく、様々なRFC非互換のサーバー、フォワーダー、ファイアウォールなどに必要な「回避策」のためです。そこで、私たちはdeckardをRustに移植し、テストに役立てました。さらに、この新しい非同期再帰ライブラリへの移行を開始したとき、まず「シャドウ」モードで実行し、本番サービスから実際のクエリーのサンプルを処理し、違いを比較しました。これはCloudflareの権威あるDNSサービスでも過去に実施したことがあります。再帰的なサービスでは、インターネット上のすべてのデータを検索する必要があるため、再帰的なサービスの方が若干難しく、権威あるネームサーバーでは、ローカライズや負荷分散などの理由で同じクエリーに対して異なる答えを出すことが多く、多くの偽陽性をしばしば引き起こします。

2019年12月、Cloudflareは本番エンドポイントをゆっくりと新サービスに移行する前に、残りの問題を解決するため、ついに公開テストエンドポイントで新サービスを発表しました(お知らせを参照)。その後も、DNS再帰(特にDNSSEC検証)でエッジケースを発見し続けましたが、ライブラリの新しいアーキテクチャのおかげで、これらの問題の修正と再現が非常に容易になりました。

サンドボックス化されたプラグイン

BigPineappleでは、DNSのコア機能をその場で拡張できることが重要であり、そのためにプラグインシステムを一新しました。以前は、Luaプラグインはサービス本体と同じメモリ空間で動作しており、一般的に自由にやりたいことができました。C/FFIを使ってサービスとモジュールの間で自由にメモリ参照を渡せるので便利です。例えば、最初にバッファにコピーすることなく、キャッシュから直接応答を読み取ることができます。しかし、モジュールが初期化されていないメモリを読み取ったり、間違った関数シグネチャを使ってホストのABIを呼び出したり、ローカルソケットでブロックしたり、その他の好ましくないことを実行したりする可能性があり、さらにサービスにはこれらの動作を制限する方法がないため、危険でもあります。

そこで、組み込みのLuaランタイムをJavaScriptやネイティブモジュールに置き換えることを検討しましたが、同じ頃、WebAssembly(略してWasm)用の組み込みランタイムが登場し始めました。WebAssemblyプログラムの2つの優れた特性は、サービスの残りの部分と同じ言語で記述できることと、隔離されたメモリ空間で実行されることです。そこで、WebAssemblyモジュールの制限に合わせてゲスト/ホストのインターフェースのモデリングを開始し、それがどのように機能するかを確認しました。

BigPineappleのWasmランタイムは現在、Wasmerを使用しています。WasmtimeWAVMなど、時間をかけていくつかのランタイムを試しましたが、私たちの場合はWasmerのほうがシンプルに使えることがわかりました。このランタイムは、各モジュールが独自のインスタンスで動作し、隔離されたメモリとシグナルトラップを持つことができ、前に説明したモジュールの隔離の問題は自然に解決しました。これに加えて、同じモジュールのインスタンスを複数同時に実行できます。注意深く制御されており、1つのインスタンスから別のインスタンスへ、1つのリクエストも逃さずにアプリをホットスワップすることができます!これは、サーバーを再起動することなく、アプリをその場でアップグレードすることができるため、非常に便利です。WasmのプログラムがQuicksilver経由で配布されていることを考えると、BigPineappleの機能は数秒以内に世界中で安全に変更することができるのです!

WebAssemblyのサンドボックスをよりよく理解するために、いくつかの用語をご紹介します。

  • ホスト:Wasmランタイムを実行するプログラム。カーネルと同様に、ゲストアプリケーションをランタイムを通して完全に制御することができます。
  • ゲストアプリケーション:サンドボックス内にあるWasmのプログラム。制限された環境の中では、ランタイムが提供する自身のメモリ空間にアクセスし、インポートされたホストコールを呼び出すことしかできません。略して「アプリ」と呼んでいます。
  • ホストコール:ホストで定義された関数のうち、ゲストがインポート可能なもの。syscallと同様、ゲストアプリがサンドボックスの外のリソースにアクセスする唯一の方法です。
  • ゲストランタイム:ゲストアプリケーションがホストと簡単にやりとりするためのライブラリ。一般的なインターフェースを実装しているため、アプリは基本的な詳細を知らなくても、async、socket、log、tracingを使用することができます。

それではサンドボックスに飛び込みましょう。まずはゲスト側から、一般的なアプリの寿命がどのようなものかを見てみましょう。ゲストランタイムの助けを借りて、ゲストアプリを通常のプログラムと同様に記述することができます。そのため、他の実行可能ファイルと同様に、アプリはロード時にホストから呼び出されるエントリーポイントとしてのスタート関数から始まります。また、コマンドラインと同様に引数で提供されます。この時点で、インスタンスは通常いくつかの初期化を行い、さらに重要なことに、異なるクエリフェーズ用のコールバック関数を登録します。これは、再帰リゾルバーでは、クエリが応答を生成するのに十分な情報を生成する前に、キャッシュ検索やドメインの委任チェーンを解決するためのサブリクエストを行うなど、いくつかのフェーズを通過する必要があるため、これらのフェーズに関連付けることができることは、さまざまなユースケースでアプリを活用するために必要なのです。start関数は、フェーズコールバックを補完するためにいくつかのバックグラウンドタスクを実行し、グローバル状態を格納することもできます。例えば、指標を報告したり、外部ソースから共有データを事前フェッチしたりすることができます。ここでも、通常のプログラムの書き方と同じ方法を取ることができます。

しかし、プログラムの引数はどこから来るのでしょうか?ゲストアプリがログや指標を送信するにはどうすればいいでしょうか?答えは、外部関数です。

図5 Wasmベースのサンドボックス

図5では、真ん中にサンドボックスの境界であるバリアがあり、ゲストとホストを隔てているのがわかりますね。一方が他方に連絡を取る唯一の方法は、あらかじめピアによってエクスポートされた関数のセットを介して行うことです。図のように、「hostcalls」はホストがエクスポートし、ゲストがインポートして呼び出すもので、一方、「trampoline」はホストが知っているゲストの関数です。

エクスポートされていないゲストインスタンス内の関数またはクロージャを呼び出すために使用されるため、トランポリンと呼ばれます。フェーズコールバックは、トランポリン関数が必要な理由の1つの例です。各コールバックはクロージャを返すため、インスタンス化時にエクスポートすることはできません。つまり、ゲストアプリがコールバックを登録したい場合、コールバックアドレス「hostcall_register_callback(pre_cache, #30987)」でホストコールを呼び出します。コールバックを呼び出す必要がある場合、ゲストのメモリ空間を指しているため、ホストはそのポインターを呼び出すことはできません。その代わりにできることは、前述のトランポリンのひとつを活用し、コールバッククロージャのアドレス「trampoline_call(#30987)」を与えることです。

アイソレーションオーバーヘッド
裏表のあるコインのように、新しいサンドボックスには、いくつかのオーバーヘッドが追加されています。WebAssemblyが提供するポータビリティとアイソレーションは、余分なコストをもたらします。ここでは、2つの例を挙げます。

まず、ゲストアプリはホストメモリを読み取ることができません。その仕組みは、ゲストがホストコールでメモリ領域を提供し、ホストがデータをゲストのメモリ空間に書き込むというものです。これは、サンドボックスの外にいれば必要ないメモリコピーを導入されます。悪いことに、今回のユースケースでは、ゲストアプリはクエリや応答に対して何かをすることになっているので、ほとんどの場合、リクエストごとにホストからデータを読み取る必要があります。一方、良いニュースは、リクエストのライフサイクルの間、データが変化しないことです。そこで、ゲストアプリがインスタンス化された直後に、ゲストのメモリ空間に大量のメモリをあらかじめ割り当てます。割り当てられたメモリは使用されることなく、アドレス空間の穴を占有する役割を果たします。ホストはアドレスの詳細を取得すると、ゲストが必要とする共通データを持つ共有メモリ領域をゲストのスペースにマッピングします。ゲストコードが実行を開始する際には、共有メモリオーバーレイのデータにアクセスすればよく、コピーは必要ありません。

もう一つの問題は、最新のプロトコルであるoDoHのサポートをBigPineappleに追加しようとしたときでした。このプロトコルの主な仕事は、クライアントクエリーを解読して解決し、答えを暗号化して送り返すことです。設計上、これはコアDNSに属さないので、代わりにWasmアプリで拡張する必要があります。しかし、WebAssemblyの命令セットは、AESやSHA-2などの一部の暗号プリミティブを提供しないため、ホストハードウェアの利点を得ることができません。この機能をWasmに導入するために、WASI-cryptoで取り組みが進行しています。それまでは、HPKEをホストコールを介してホストに委ねるだけで、Wasm内で実行した場合と比較して、すでに4倍のパフォーマンス向上が確認されています。

WasmのAsync
以前、コールバックがイベントループをブロックしてしまうという問題について覚えていますか?本来は、サンドボックス化されたコードをいかに非同期で実行するかが問題です。なぜなら、リクエスト処理のコールバックがどんなに複雑であっても、それが中断できれば、ブロックが許可される時間に上限を設けることができるからです。幸いなことに、Rustの非同期フレームワークはエレガントかつ軽量です。これにより、一連のゲストコールを使用して、「Future」を実装する機会が得られます。

Rustでは、Futureは非同期計算のための構成要素です。ユーザーの立場からすると、非同期プログラムを作るためには、状態遷移を促すポーリング可能な関数を実装することと、何らかの外部イベント(例えば、時間が経過するとソケットが読み取り可能になるなど)によってポーリング可能な関数を再度呼び出す必要がある場合に、自身を起動するコールバックとしてwakerを配置することの2点に注意する必要があります。前者は、I/Oからバッファされたデータを読み取り、タスクのステータスを示す新しい状態(完了または中断)を返すなど、プログラムを徐々に進行できるようにするものです。後者は、タスクが完了するまでビジーループするのではなく、タスクが待機していた条件が満たされたときにFutureがポーリングされるようにするため、タスクが中断する場合に便利です。

サンドボックスでどのように実装されているか見てみましょう。ゲストが何らかのI/Oを行う必要があるシナリオでは、制限された環境下にあるため、ホストコールを介して行う必要があります。ホストが基本的なソケット操作(open、read、write、close)を反映した簡易ホストコール群を提供すると仮定すると、ゲストは以下のように疑似ポーラーを定義することができます。

fn poll(&mut self, wake: fn()) -> Poll {
	match hostcall_socket_read(self.sock, self.buffer) {
    	    HostOk  => Poll::Ready,
    	    HostEof => Poll::Pending,
	}
}

ここでは、ホストコールがソケットからバッファにデータを読み込み、その戻り値によって、関数は先に述べた状態(finished(Ready)またはyielded(Pending))のいずれかに移行することができます。魔法はホストコールの内部で起こります。図5で、それがリソースにアクセスする唯一の方法であることを思い出してください。ゲストアプリはソケットを所有しませんが、「hostcall_socket_open」を介して「ハンドル」を取得することができ、これによりホスト側にソケットが作成され、ハンドルが返されます。ハンドルは理論的には何でも良いのですが、実際には整数のソケットハンドルを使うと、ホスト側のファイル記述子や、ベクトルやスラブ内のインデックスにうまくマッピングされます。返されたハンドルを参照することで、ゲストアプリは実際のソケットをリモートで制御できます。ホスト側は完全に非同期なので、ソケットの状態をゲストに中継するだけでよいのです。上記でwaker関数が使われていないことに気づかれた方は、お見事です!これは、ホストコールが呼び出されたときに、ソケットのオープンを開始するだけでなく、ソケットがオープンされたときに呼び出される(または失敗した)現在のwakerを登録するためです。そのため、ソケットの準備が整うと、ホストタスクがウェイクアップされ、対応するゲストタスクをコンテキストから検索し、図5のようにトランポリン関数を用いてウェイクアップします。ゲストタスクが他のゲストタスク、例えばasync mutexなどを待つ必要があるケースは他にもあります。ここでのメカニズムは同様で、ホストコールを使用してwakerを登録します。

これらの複雑なものはすべて、使いやすいAPIを備えたゲスト非同期ランタイムにカプセル化されているため、ゲストアプリは根本的な詳細を気にすることなく、通常のasync関数にアクセスすることができます。

終わりに(でも進化は続きます)

このブログ記事では、いまだ進化を続ける1.1.1.1を支える革新的なプラットフォームの概要をご紹介しました。今日の時点で、ファミリー向け1.1.1.1AS112Gateway DNSなどのいくつかの製品はBigPineappleで実行されているゲストアプリによってサポートされています。Cloudflareは、新しい技術を取り入れることを楽しみにしています。コミュニティまたは電子メールで皆様のご意見をお寄せください。

Cloudflareは企業ネットワーク全体を保護し、お客様がインターネット規模のアプリケーションを効率的に構築し、あらゆるWebサイトやインターネットアプリケーションを高速化し、DDoS攻撃を退けハッカーの侵入を防ぎゼロトラスト導入を推進できるようお手伝いしています。

ご使用のデバイスから1.1.1.1 にアクセスし、インターネットを高速化し安全性を高めるCloudflareの無料アプリをご利用ください。

より良いインターネットの構築支援という当社の使命について、詳しくはこちらをご覧ください。新たなキャリアの方向性を模索中の方は、当社の求人情報をご覧ください。
DNS (JP)Resolver (JP)1.1.1.1 (JP)日本語

Xでフォロー

Cloudflare|@cloudflare

関連ブログ投稿

2024年4月12日 13:00

「Foundation DNS」の公式リリースで権威DNSを改善

Cloudflareは、新しいエンタープライズグレードの権威DNSサービスであるFoundation DNSを立ち上げました。Foundation DNSは、新たなエンタープライズ向け権威DNSサービスとして、権威DNSサービスの信頼性、セキュリティ、柔軟性、分析を強化するよう設計されています...

2024年1月09日 14:00

2023年第4四半期DDoS脅威レポート

CloudflareのDDoS脅威レポート第16版へようこそ。本版では、2023年第4四半期および最終四半期のDDoS動向と主要な調査結果について、年間を通じた主要動向のレビューとともにお届けします...