このコンテンツは自動機械翻訳サービスによる翻訳版であり、皆さまの便宜のために提供しています。原本の英語版と異なる誤り、省略、解釈の微妙な違いが含まれる場合があります。ご不明な点がある場合は、英語版原本をご確認ください。
Cloudflareが注力している分野の1つは、エンドユーザーのためにインターネットを高速化することです。その方法の一部として、特にクリティカルパス上のプロセスに注目し、スピードを落とす「大きな壁」やボトルネックを調べることが挙げられます。最近、当社のプライバシープロキシ製品に目を向けた時、改善の大きな機会を見出しました。
Cloudflareのプライバシープロキシ製品とは?これらのプロキシを使用すると、ユーザーはアクセスしているWebサイトに個人情報を公開することなく、Webを閲覧することができます。Cloudflareは、AppleのPrivate RelayやMicrosoftのEdge Secure Networkなど、プライバシープロキシのインフラストラクチャを運用しています。
あらゆる安全なインフラストラクチャと同様に、ユーザーが訪問しているWebサイトへの接続を開く前に、これらのプライバシープロキシで認証を行っていることを確認します。プライバシーを保護する方法でこれを行うために(Cloudflareがエンドユーザーに関する情報を最小限に抑えるために)、オープンインターネット標準であるプライバシーパスを使用して、プロキシサービスに対して認証するトークンを発行しています。
ユーザーがプライバシープロキシ経由でWebサイトを訪問するたびに、リクエストのProxy-Authorizationヘッダーに含まれるプライバシーパストークンの有効性を確認します。ユーザーのトークンを暗号的に検証する前に、このトークンがすでに使われているかどうかを確認します。トークンが使われなければ、ユーザーのリクエストを通過させます。それができなければ、「二重支出」になります。アクセス制御の観点からは、二重支出は問題があることを示唆しています。プライバシーの観点からは、支出の二重元化は匿名性セットやプライバシーの特性を低下させる可能性があります。パフォーマンスの観点からは、当社のプライバシープロキシが毎秒数百万のリクエストを処理し、サイトへのアクセスを認証するのに時間がかかるため、チェックは迅速に行われる必要があります。このダブル投資のチェックの遅延を約40ミリ秒から1ミリ秒未満に低減した方法を見てみましょう。
Jagerというトレースプラットフォームを使っています。これにより、コードがどのパスを経て、関数の実行にどれくらいの時間がかかったかを確認することができます。これらのトレースを調べたところ、約40ミリ秒の遅延が見られました。良いリードを得ることができましたが、それだけでは問題であると結論づけることはありませんでした。その理由は、トレースのごく一部しかサンプリングしていないため、ここで見たことが全体像ではないからです。より多くのデータを調べる必要がありました。サンプリングするトレースの数を増やすことはできますが、当社のシステムがトレースを処理するのにトレースが大きく重くなるのです。メトリクスは軽量なソリューションです。すべての二重支出チェックについてデータを取得するための指標を追加しました。
このグラフの線は、世界中の最も遅いプライバシープロキシの遅延の中央値です。メトリクスデータから、WAFが予想より45ミリ秒長いと仮定して、リクエストの大部分に影響を与える問題であるという確信が得られました。しかし、このような事態は想定されていたのでしょうか?どのような数字を予想していましたか?
どのような時間が予想されるのかを理解するために、「二重投資チェック」の構成要素について詳しく説明します。ダブルスペンドチェックの際、プライバシーパス トークンが存在するかどうか、バックアップするデータストアに問い合わせます。私たちが使用しているデータストアは memcached
です。世界中のサーバー上で多くのmemcached
インスタンスが動作しているので、どのサーバーに問い合わせるか?これには、mcrouter
を使用します。どのmemcached
サーバーに問い合わせるかを考える代わりに、mcrouter
にリクエストを与えると、mcrouterは使用するmemcached
サーバーの選択を処理します。私たちは、mcrouter
がリクエストを処理するのに要した時間の中央値を調べました。このグラフは、サーバーごとの平均遅延を経時的に示しています。スパイクもありますが、ほとんどの場合、遅延は1ミリ秒未満です。
この時点で、私たちは、二重投資チェックの遅延が、どこでも予想以上に長いものであることを確信し、根本原因を探し始めました。
私たちは、この科学的手法から着想を得ました。私たちはコードを分析し、コードのセクションが遅延の原因となっている理由に関する理論を作り出し、データを使用してその理論を否定しています。残っている理論については、修正を実装し、それらが機能するかどうかをテストしました。
コードを見てみましょう。大まかに説明すると、二重支出のチェックのロジックは次のとおりです。
接続を取得。これは次のように分類できます:
memcachedバージョン
コマンドを送信します。これは、接続がまだデータを送信できているかどうかのヘルスチェックとして機能します。
接続がまだ良い場合は、それを取得します。それ以外の場合は、新しい接続を確立します。
接続でmemcached
getコマンドを送信します。
上記の各ステップごとの理論を確認してみましょう。
ヘルスチェックは主に健全性チェックとして測定されました。バージョンコマンドはシンプルで高速に処理できるため、時間がかかることはありません。そして、私たちは自由に行動することができました。遅延の中央値は1ミリ秒未満でした。
なぜ接続を得るために待つ必要があるのかを理解するために、接続の取得方法についてさらに詳しく説明します。このコードでは、接続プールを使用しています。プールは、mcrouter
へのすぐに使える接続のセットです。プールを持つメリットは、リクエストを行うたびに接続を確立するためのオーバーヘッドを支払う必要がないことです。プールにはサイズの限界があります。私たちの制限は、1サーバーあたり20回でしたが、ここに潜在的な問題があります。毎秒5,000件のリクエストを処理し、リクエストを45ミリ秒維持するサーバーがあるとします。小さなの法則と呼ばれるものを使って、システム内のリクエストの平均数を推定することができます。5000 x 0.045 = 225
です。プールサイズの制限上、一度に20接続しかできないため、一度に20のリクエストしか処理できません。つまり、205つのリクエストが待機中ということになります!二重費のチェックを実行すると、接続が得られるまで約40ミリ秒かかるかもしれません。
さまざまなサーバーのメトリクスを調査しました。1秒あたりのリクエスト数が何であれ、レイテンシーは一貫して40ミリ秒以内であり、これは理論ではありませんでした。たとえば、このグラフは、1秒あたり最大20件のリクエストを処理したサーバーのデータを示しています。経時的なヒストグラムが表示され、リクエストの大部分は40~50ミリ秒バケットに分類されます。
理論3:Nagleのアルゴリズムの遅れとACKの遅れ
私たちはこれまでの観察について、Geminiとチャットすることにしました。さまざまな提案がありましたが、最も興味深いのはTCP_NODELY
が設定されているかどうかを確認することでした。もしこのオプションをコードに設定していたら、Nagleのアルゴリズムと呼ばれるものを無効にすることができました。Nagleのアルゴリズム自体は問題ではありませんでしたが、別の機能や遅延 ACKと同時に有効にすると、遅延が生じかねませんでした。その理由を説明するために、例を見てみましょう。
グループチャットアプリを実行しているとします。通常、人々は完全な思考を入力し、1つのメッセージにまとめて送信します。ところが、「こんにちは」という言葉をひとつずつ送ってくる友人がいるのです。送信、「方法」。送信、「are」です。送信、「あなた」です。送信、実にたくさんの通知があります。Nagleのアルゴリズムはこれを防ぐことを目的としています。Nagle氏は、友人が1つの短いメッセージを送りたいなら、それは大丈夫ですが、順番に1回しかできないと言い、直後にさらに単一の単語を送信しようとすると、Nagleはその単語を下書きメッセージに保存します。メッセージの下書きが一定の長さに達すると、Nagleが送信します。しかし、メッセージの下書きがこれほど長くならない場合はどうでしょうか。これを管理するために、遅延したACKは友人がメッセージを送信するたびに40ミリ秒のタイマーを起動します。タイマーが終了する前にアプリへの入力がなくなると、メッセージがグループに送信されます。
Cloudflareが作成したコードと、私たちが依存している依存関係からのコードの両方を詳しく見てみました。memcacheコマンドを送信できるコードを実装するために、memcache-async
クレートに依存していました。memcached Version
コマンドを送信するためのコードを次に示します。
self.io.write_all(b"version\r\n").await?;
self.io.flush().await?;
珍しいことはありません。次に、get関数の内部を調べました。
let writer = self.io.get_mut();
writer.write_all(b"get ").await?;
writer.write_all(key.as_ref()).await?;
writer.write_all(b"\r\n").await?;
writer.flush().await?;
コードでは、io
をTcpStream
として設定します。つまり、各write_all
呼び出しがメッセージを送信することになります。Nagleのアルゴリズムを有効にすると、データの流れは次のようになります:
なるほど。3つの小さなメッセージをすべて送信しようとしましたが、「get」を送信した後、カーネルはトークンと\r\n
をバッファに入れて待機し始めました。mcrouter
が「get」を取得したとき、完全なコマンドを持っていなかったため、何もすることができませんでした。つまり、40ミリ秒待ちでした。次に、応答としてACKを送信します。ACKを取得し、残りのコマンドをバッファに送信しました。mcrouter
はコマンドの残りの部分を取得し、処理し、トークンが存在するかどうかを示すレスポンスを返しました。Nagleのアルゴリズムを無効にした場合、データの流れはどのようになりますか?
3つの小さなメッセージすべてを送ることになります。mcrouter
は完全なコマンドを持ち、すぐにレスポンスを返します。待つことは一切不要です。
CloudflareのLinuxサーバーには、遅延の許容範囲が最小限に抑えられています。以下は、これらの境界を定義するLinuxソースコードのスニペットです。
#if HZ >= 100
#define TCP_DELACK_MIN ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */
#define TCP_ATO_MIN ((unsigned)(HZ/25))
#else
#define TCP_DELACK_MIN 4U
#define TCP_ATO_MIN 4U
#endif
コメントでは、TCP_DELACK_MIN
は、ACKがACKを送信する前に待機する最小遅延時間であることを示しています。Cloudflareのカスタムカーネル設定を調べたところ、以下のことがわかりました。
CONFIG_HZ=1000
CONIG_HZ
は最終的にHZ
に伝播し、40ミリ秒の遅延をもたらします。数字の背景はここにあるのです!
1つのコマンドに対して1つのメッセージを送信するだけなのに、1つのコマンドに対して3つのメッセージを送信していました。Wiresharkのget
コマンドの様子をキャプチャし、3つの個別のメッセージを送信していることを検証しました。(これはMacOS上でローカルにキャプチャしました。興味深いことに、すべてのメッセージに対してACKが送られました。)
修正は、BufWriter<TcpStream>
を使用することで行われました。これにより、write_all
が小さなメッセージをユーザー空間のメモリバッファにバッファリングし、flush
はmemcached
コマンド全体を1つのメッセージで送信します。Wiresharkのキャプチャは、ずっとクリーンに見えました。
この修正を本番環境にデプロイした後、ダブルスペンチェック遅延の中央値があらゆる場所で予想値に低下しました。
私たちの調査は、組織的でデータ駆動型のアプローチに従いました。まず、観測可能性のツールを使って問題の規模を確認することから始めました。そのことから、テスト可能な仮説を形成し、データを使用して、それらを組織的に覆すことができました。このプロセスは最終的に、サードパーティの依存関係を利用した方法によって、Nagleのアルゴリズムと遅延したACKとの間の微妙な相互作用が実現しました。
結局は、より良いインターネットの構築を支援することが、当社の使命なのです。節約されたミリ秒ごとに、エンドユーザーがより高速で、よりシームレスに、プライベートなブラウジング体験を提供することに貢献します。私たちはこのサービスを展開できることを嬉しく思います。そして、さらなるパフォーマンス向上を追求し続けることを楽しみにしています。