エンジニア

遅いクライアントがサーバーを殺す ― リバースプロキシの役割を理解する

投稿日:

はじめに

こんにちは。エンジニアリング&デザインマネジメント室(EDMO)でSREを担当しております堀江です。最近、社内のサーバーエンジニアの会話の中で、全く別の文脈でリバースプロキシの必要性について話題になりました。なんだか面白かったのと、せっかくなのでブログの記事としてまとめてみようと思いました。

弊社のサーバー開発ではPHP言語での開発の採用が多いため、PHPアプリケーションサーバーの前段にあるWebサーバーの役割について、改めて確認してみようと思います。

スロークライアント問題とは

「スロークライアント」とは、通信速度が遅いクライアントのことです。具体的には以下のようなケースが該当します。

  • 電波状況の悪い場所でスマートフォンを使っているユーザー
  • 3G回線や低速なモバイル回線で接続しているユーザー
  • 海外から高レイテンシな経路でアクセスしているユーザー
  • 帯域が逼迫しているネットワーク環境のユーザー

こうしたクライアントは、サーバーからのレスポンスを受け取るのに時間がかかります。問題は、この「受け取るのに時間がかかる」という状況が、サーバー側のリソースを長時間占有してしまうことです。

なぜ問題になるのか

PHP-FPMを例に考えてみましょう。PHP-FPMはワーカープロセスのプールを持っており、リクエストが来るとプールからワーカーを1つ割り当てて処理します。

ここでは、nginxなしで直接PHP-FPMにアクセスする構成を想定してみます。通常のリクエスト処理の流れは以下のようになります。

  1. クライアントからリクエストが届く
  2. PHP-FPMワーカーがリクエストを受け取り、処理を実行する
  3. レスポンスを生成する
  4. クライアントにレスポンスを送信する
  5. 送信が完了したらワーカーがプールに戻る

ここで重要なのは、ワーカーは「クライアントへの送信が完了するまで」解放されないという点です。

高速なクライアントであれば、レスポンス送信は数十msから数百msで終わります。しかしスロークライアントの場合、レスポンスの送信に数秒〜数十秒かかることもあります。その間、ワーカープロセスは何もせずにただ待っているだけなのに、占有され続けます。

これが「スロークライアント問題」です。

小規模では顕在化しない

この問題の厄介な点は、小〜中規模の環境では顕在化しにくいことです。

同時接続数が少なければ、たとえ数人のスロークライアントがワーカーを占有していても、他のリクエストを処理するワーカーは十分に残っています。問題が表面化するのは、同時接続数が増えてワーカープールが枯渇し始めたときです。

実際、負荷試験で初めてこの問題に気づくケースも多いです。「CPUもメモリも余裕があるのに、なぜかレスポンスが詰まる」という現象が起きたら、スロークライアント問題を疑ってみてください。

Webサーバーによるバッファリング

では、この問題をどう解決するのでしょうか。答えは「バッファリングを行うリバースプロキシを前段に置く」ことです。

nginxのバッファリングの仕組み

nginxをリバースプロキシとして前段に配置すると、以下のような流れになります。

  1. クライアントからリクエストが届く
  2. nginxがリクエストを完全に受信してから、PHP-FPMに転送する
  3. PHP-FPMがリクエストを処理し、レスポンスを生成する
  4. PHP-FPMはnginxにレスポンスを高速に送信し、すぐにプールに戻る
  5. nginxがレスポンスをバッファに保持する
  6. nginxがクライアントの速度に合わせてレスポンスを送信する

ポイントは、PHP-FPMワーカーが「クライアントへの送信完了」を待たなくてよくなることです。PHP-FPMとnginxは同一サーバーまたは高速なネットワークで接続されているため、その間の通信は一瞬で終わります。

スロークライアントへの送信に時間がかかっても、それを担当するのはnginxです。

nginxがこの役割に適している理由は、そのアーキテクチャにあります。nginxはイベント駆動型アーキテクチャを採用しており、ワーカープロセスあたりのメモリフットプリントが非常に小さく、また非同期I/O(epoll、kqueueなど)を活用することで、1つのプロセスで数万のコネクションを同時に維持できます。つまり、大量のスロークライアントを抱えても、リソース消費を最小限に抑えながら接続を維持できるのです。

I/OバウンドとCPUバウンド

ここで「I/Oバウンド」と「CPUバウンド」という概念を整理しておきましょう。

  • CPUバウンド: 処理速度がCPUの計算能力に依存する状態。複雑な計算処理など。
  • I/Oバウンド: 処理速度がI/O(ネットワーク、ディスクなど)の待ち時間に依存する状態。

スロークライアントへのレスポンス送信は、典型的なI/Oバウンドの処理です。CPUはほとんど使わず、ただネットワークの送信完了を待っているだけです。

PHP-FPMのようなプロセスベースのアーキテクチャは、CPUバウンドな処理には向いていますが、I/Oバウンドな待ち時間でプロセスを占有されると非効率になります。一方、nginxのようなイベント駆動型のアーキテクチャは、I/Oバウンドな処理を大量に捌くのが得意です。

つまり、それぞれの得意分野で役割分担をしているわけです。

nginx + PHP-FPM構成を改めて見る

ここまでを踏まえて、定番の「nginx + PHP-FPM」構成を改めて見てみましょう。

クライアント → nginx → PHP-FPM

この構成には以下のような意味があります。

  • バッファリングによるスロークライアント対策: 本記事で説明した通り
  • プロトコル変換: HTTPとFastCGIの変換
  • 静的ファイル配信: 画像やCSS、JavaScriptなどをnginxが直接配信
  • SSL終端: TLS/SSLの処理をnginxが担当
  • その他: gzip圧縮、アクセスログ、レートリミットなど

「静的ファイルがないならnginx不要では?」

APIサーバーのように静的コンテンツを配信しない場合、「nginxは余計なレイヤーでは?」という疑問が出てくることがあります。

しかし、ここまで説明してきた通り、nginxの役割は静的ファイル配信だけではありません。バッファリングによるスロークライアント対策という重要な役割があります。

「ALBから直接PHP-FPMに繋げばいいのでは?」

クラウド環境では「ALB(Application Load Balancer)から直接アプリケーションサーバーに繋げばいいのでは?」という議論もあります。

これについては、ALBがどこまでバッファリングを行うかを確認する必要があります。ALBの実装によってはバッファリングが不十分な場合もあり、その場合はスロークライアント問題が発生する可能性があります。

また、PHP-FPMの場合はFastCGIプロトコルを使用するため、HTTPしか話せないALBから直接接続することはできません。PHP-FPMをHTTPサーバーとして動作させる構成(PHP Built-in serverやRoadRunnerなど)を使う場合は検討の余地がありますが、スロークライアント対策の観点は忘れないようにしましょう。

余談: AWS ELBのバッファリング事情

AWSのロードバランサーについて補足しておきます。Classic Load Balancer(CLB)はバッファリングを行う挙動が知られていましたが、Application Load Balancer(ALB)については公式ドキュメントに明記されていないものの、ノンバッファリング(ストリーミング)の挙動を示すことが報告されています。

つまり、ALBを使用している環境でも、スロークライアント対策としてnginxなどのバッファリングプロキシを前段に置く意義があるということです。「ALBがあるからnginxは不要」とは言い切れません。

nginxの代替

バッファリング機能を持つリバースプロキシはnginx以外にも存在します。HAProxyやEnvoyでも代替できます。

ただし、導入のしやすさ、ドキュメントの豊富さ、運用実績の多さという点で、nginxは依然として手軽な選択肢です。特にPHP-FPMとの組み合わせは長年の実績があり、トラブルシューティングの情報も豊富に蓄積されています。サービスメッシュなどで既にEnvoyを利用している場合には設定の見直しで適切なバッファリングができる可能性があります。

CloudFrontをキャッシュなしで使う

少し変わったテクニックとして、Amazon CloudFrontをキャッシュなしで使用する方法もあります。

「キャッシュしないのにCDNを入れる意味があるの?」と思われるかもしれませんが、CloudFrontはオリジンからのレスポンスを受け取ってからクライアントに配信します。つまり、キャッシュヒットがなくても、スロークライアント対策としての効果が得られるのです。

オリジンサーバーはCloudFrontに対して高速にレスポンスを返し、スロークライアントへの配信はCloudFrontが担当する。ここまで読んできた方なら、これがまさにバッファリングプロキシの役割だとお分かりいただけるでしょう。

キャッシュがないのにパフォーマンスが向上するという、一見不思議な現象の裏にはこうした仕組みがあります。

まとめ

本記事では、スロークライアント問題とリバースプロキシの役割について説明しました。

  • スロークライアント問題: 通信速度の遅いクライアントがサーバーのワーカープロセスを長時間占有してしまう問題
  • バッファリングの効果: nginxなどのリバースプロキシがレスポンスをバッファリングすることで、アプリケーションサーバーを早期に解放できる
  • 役割分担: I/Oバウンドな処理はWebサーバーに、CPUバウンドな処理はアプリケーションサーバーに、という適材適所の設計

「なんとなくnginxを前段に置いている」という状態から、「この構成には意味がある」と理解した上で設計できるようになると、より良いアーキテクチャ選定ができるようになると思います。

ぜひWebサーバー周りの設定を再確認してみてください。

採用情報

ワンダープラネットでは、一緒に働く仲間を幅広い職種で募集しております。

-エンジニア
-, ,

© WonderPlanet Inc.