エンジニアブログ - Prexy:超高速検索置換ルール
最初のエンジニアリング・ブログへようこそ。他のブログより少し専門的かもしれませんが、誰でも理解できるように最善を尽くしました。この記事では、置換ルールを適用するためのClonable新しいソフトウェアであるPrexyについてお話します。
背景
Clonableユーザーであれば、置換ルール機能についてはすでにご存知かもしれません。この検索・置換ルールを使って、テキストやコードの一部を自分で選択したバリアントと交換することができます。例えば、APIキーやアナリティクスIDを置き換えることで、オリジナルサイトと翻訳されたクローンで異なるアナリティクスを作成することができます。内部的には、これらの同じルールは、元のドメイン名をクローンのドメイン名に置き換えたり、他の多くのことにも使用されます。
Clonable最初のバージョンから、この機能は一度もアップデートされていません。それでも、パフォーマンスと使いやすさの両面で、改善は可能でした。私たち Clonable、長い間使われていない製品の一部を取り上げて改良を始めることがあります。たとえば昨年は、データ・インフラストラクチャーを全面的に見直し、より高速でスケーラブル、そして耐障害性を向上させましたし、その前にはURL翻訳機能をゼロから作り直しました。今期は、置換ルールの番だった。
旧状況と限界
Clonable置換ルールが適用されるのは、レスポンスをクライアントに送り返す直前の最後の段階だけです。そのため、Clonable最初のバージョンは、OpenRestyによって作成されたモジュールreplace-filter-nginx-moduleを使用して、ウェブサーバNGINX内にこれを実装することにしました。このモジュールは、同じくOpenRestyによって作成されたストリーミング正規表現エンジンとしてsregexを使用しています。この場合、ストリーミングという側面が重要だ。なぜなら、すべてのレスポンスを完全にメモリにロードしたくないからだ。なぜなら、すべてのレスポンスを完全にメモリにロードしたくないからです。もしそうすれば、大きなレスポンスの数々によってNGINXがクラッシュしたり、利用可能なメモリが不足してクラッシュしたりするかもしれません。sregexの2つ目の重要な特徴は、複数行を並行して処理できることです。これは、各クローンにはすでにいくつかのデフォルトルールがあり、さらに、クローン専用に追加されたルールがある場合もあるからです。もしこれらが並列に処理できなければ、最初のルールの後で2番目のルールに再度マッチさせなければならないので、レスポンス全体をバッファリングしなければならない。
しかし昨年、私たちは奇妙なパフォーマンスの問題に遭遇することが多くなりました。これらの問題は短時間で終わることが多かったのですが、極端な場合にはページの応答時間が数秒長くなることもありました。このような問題が発生した後は、ページの読み込みが正常に戻ることが多く、問題の再現は困難でした。この問題をさらにデバッグするために、すべてのレスポンスにClonable-Timingsヘッダーを追加しました。その結果、ほとんどすべてのケースでアップストリームは素早く反応し、翻訳もかなりスムーズであることがわかった。しかし、翻訳の完了とリクエストの完全な完了の間にギャップがあり、これが置換ルールに関係している可能性を示唆した。
NGINXの同時実行モデル
しかし、なぜこのような遅延が散発的にしか発生しないのか、なぜサーバーに半分も負荷がかかっていないのにこのような現象が発生するのかについては、まだ答えは得られていません。この現象をよりよく理解するためには、NGINXがどのようにワークロードを処理するかをもう少し深く調べる必要があります。
NGINXは1つのマスタープロセスと複数のワーカープロセスからなるワーカアーキテクチャを採用しています。複数のリクエストを同時に処理するために、コネクションはワーカーに分散されます。しかし、このセットアップには欠点もあります。ワーカーがリクエストの処理に追われているとき、同じワーカーに割り当てられている他のリクエストも待たなければなりません。これは、たとえ他のワーカーが何もすることがなくても、 一つの重いリクエストが複数のリクエストの遅延の原因になる可能性があります。この影響は下の画像によく現れています。この画像の作成中、10 の異なる接続でストレステストが実行されています。すると画像は、3人のワーカーが忙しく、4人目はほとんど何もしていないことを明確に示しています。

Prexyの時間
ボトルネックが明らかになったので、それを改善する計画を立てました。このプロジェクトはRegExとProxyを合体させてPrexyと名付けられた。最終的な結果には3つの主な要件があった:
Prexyは現在のセットアップをそのまま置き換えることができる。既存のセットアップを壊さないように、置換ルールは同じように適用されるべきである。
置換ルールは、ソリューションがメモリを使いすぎるのを防ぐためにストリーム化する必要があります。
新しいソリューションは、現在のソリューションよりも高速になるはずです。
まず、リクエストを効率的に処理できる強力なマルチスレッドランタイムを探す必要があった。いくつかの選択肢を検討した結果、プログラミング言語Rust用の Tokyoランタイムにたどり着きました。Rustは、C++のような低レベル言語の安全性リスクなしに、高速なプログラムを書けることで知られています。Tokyoランタイムは、ネットワーク・アプリケーションのために作られた柔軟な非同期ランタイムです。私たちにとって最も重要な特徴のひとつは、ワークステーリングが可能なことです。つまり、NGINXとは異なり、リクエストは1つのワーカーに束縛されるのではなく、何もすることがないワーカーが他のワーカーから仕事を「盗む」ことができる。こうすることで、サーバーのリソースをより有効に活用することができます。
最初のプロトタイプ
基礎となる技術を選択した後、このプロジェクトによってどれくらいの速度向上が得られるか、またその価値があるかどうかを大まかに見積もるために、初期のプロトタイプをセットアップすることにした。正規表現エンジン(実際にルールを処理して適用する部分)の最初の実装として、標準的な正規表現ライブラリ(Rust用語ではクレートと呼ばれる)を使用した。このエンジンは、sregexと同様、バックトラックしないので、ReDoS問題のリスクが少ない。ReDoSでは、正規表現が入力に直面し、その入力を評価するのにかかる時間が指数関数的に増加する。これはパフォーマンスにとって致命的であり、cloudflareが 大規模な停止に陥った原因となった。そのため、私たちにとって、使用するエンジンがバックトラッキングしないものであることが重要です。
この正規表現エンジンを使って、初期のプロトタイプを作った。このプロトタイプはハードコードされたルールを使用し、正規表現エンジンはストリーミングの代わりにレスポンス全体をバッファリングしたが、初期のパフォーマンステストにはこれで十分だった。
この正規表現エンジンを使って、初期のプロトタイプを作った。このプロトタイプはハードコードされたルールを使用し、正規表現エンジンはストリーミングの代わりにレスポンス全体をバッファリングしたが、初期のパフォーマンステストには十分だった。私たちのテスト・セットアップは2つのVMで構成され、1つのVMは古いソリューションと新しいソリューションの両方を実行できるセットアップを実行した。そうすることで、2つの方法の比較を簡単に行うことができた。もう1台のサーバーはNGINXを実行し、オリジン・サーバーとしてテスト・ファイルを提供した。同じサーバーで、負荷の生成に使用したwrkツールも実行した。純粋なデータの場合、ロード・ジェネレーターを別のVMで実行するのが望ましいため、これは完全に最適とは言えないが、このVMには十分なリソースがあり、NGINXとwrkが互いに干渉することはなかった。
Prexyは1つのリクエストを処理するのに約22倍速くなりました(下の画像を参照)。これほど大きな差があれば、機能を完全なものにすることで発生するかもしれないパフォーマンスの低下を吸収する十分な余地があったからです。

最初のテストの後、私たちはコンパイルされた正規表現をキャッシュするなど、いくつかの明らかな最適化を実装した。また、上流への接続をより効率的に再利用する方法も実装した。これにより、約20%の速度向上を達成しました。さらに、両方のソリューションをピーク負荷下でテストし、可能な限り多くのリクエストをサーバーに送りました。ここでも、その差ははっきりと現れ、Prexyは旧ソリューションの約25倍のスループットを達成した。

ストリーミング正規表現エンジン
このプロジェクトの要件の一つは、使用する正規表現エンジンがストリーミングであることだった。プロトタイプのregex createはそうではないので、この分野で追加作業が必要だった。しかし、regex crateは高度に最適化されていることが判明したので、この実装をストリーミング・エンジンの基礎とすることにした。正規表現エンジンを使ってデータをストリーミングする場合、いくつか注意すべき点がある。第一に、まだどんなデータが来るかわからない。そのため、部分一致から作業を開始する必要があります。部分一致とは、まだ完全ではありませんが、すでに1文字以上マッチしているものを指します。完全一致を見つけるときは、重複する部分一致がないことをチェックする必要があります。そのため、部分一致が完全一致でないことが判明した場合のみ、マッチの処理を開始することができます。
さらなる最適化
予想通り、正規表現エンジンの再構築はPrexyのパフォーマンスにマイナスの影響を与えた。しかし、古いソリューションと比較するとまだ余裕があり、さらに最適化を加えることで、正規表現エンジンを再構築する前よりもさらに高いパフォーマンスを得ることができました。私たちが適用した最適化のひとつは、特定のファイルにルールを適用するかどうかを記憶することでした。CSSやJSファイルのような静的アセットが変更されることはほとんどなく、特定のファイルにマッチすることがないのに毎回ルールを実行するのは無駄です。この代替案として、置換結果をキャッシュする方法があるが、この戦略の欠点は、すべてのファイルを保存するために多くのメモリを必要とすることだ。どのルールを適用するかを記憶しておくことで、情報をビットマップとして保存することにより、1ファイルあたり数バイトで済む。
さらに、正規表現エンジン内の最適化を最大限に活用し、例えば、置換で使用しない場合はキャプチャグループを保持しないようにしています。その結果、正規表現エンジンはマッチ全体の開始と終了を記憶するだけでよくなり、作業時間が短縮されました。また、名前付きキャプチャ・グループを無効にしました。これはストリーミング・バリアントでは非常に複雑であったためで、古いソリューションではこれもサポートしていなかったため、必ずしも必要ではありませんでした。これらの最適化により、最終的に以下のようなパフォーマンスが得られた:

最終結果
Prexyの動作が旧来のソリューションと本当に同じであることを確認するために広範囲にわたってテストした後、私たちはPrexyの段階的な展開を開始した。約1週間かけて、各クローンに対してPrexyを有効化した。私たちのモニタリングでは、有効化のタイミングはしばしば簡単に見ることができました。レスポンスタイムが約50%減少するのは珍しいことではありませんでした。10%程度のわずかな差は、置換ルールがほとんどない他の顧客でも見られました。私たちのウェブサイトは約30%速くなりました(~50ms)。
Prexyを導入した結果、インフラ全体に以下のような違いが見られました。
RAM消費量を33%削減
CPU消費を20%削減
10-50%速いレスポンス
全体として、Prexyは成功したプロジェクトだったと言えます。時代遅れのNGINXモジュールを置き換えることで、より新しく効率的なテクニックを使用できるようになり、すべてのお客様にとって測定可能な明確な違いが生まれました。将来的には、新機能と既存機能の強化の両面で、Clonable改善を続けていきます。
このエンジニアリングブログをお読みいただきありがとうございます。私たちの製品について、このような技術的な洞察をもっと頻繁に得たい場合はお知らせください。このプロジェクトに興奮しましたか?それなら、私たちの求人ページをご覧ください :)