WEBアプリケーションサーバを構築する際、ELBやApacheやnginxなど何らかのリバースプロキシを前段に置くケースは非常に多いと思います。
今回は、筆者が各種リバースプロキシで1回ずつ位ハマった気がする、KeepAliveタイムアウト問題について解説します。
HTTP KeepAlive
まずはHTTPにおけるKeepAliveについて簡単に説明します。
HTTP1.1の持続的接続(Persistent Connections)の根幹をなす機能で、1つのTCPコネクションで複数のリクエスト、レスポンスをやり取りする仕組みです。
このような機能が求められた背景には、TCP接続時の3Wayハンドシェイクのような負荷の高い処理を減らしたい、という目的があります。
単純化したシーケンス図で表すと、以下のようなフローになります。
KeepAliveタイムアウト
サーバー側は次のリクエストを待つ際、いつ送られるかわからないリクエストを永遠に待ち続けるわけにもいかないため、通常、一定のタイムアウトが設けられます。
いつまでもコネクションを確立したままにできないのはクライアント側も同じで、次回のリクエストで再利用するまでのタイムアウトが設定されるのが一般的です。
クライアントがリクエストを送信し、サーバーがリクエストを受信するまでの間にサーバー側のタイムアウトが発生してしまった場合、リクエストに失敗してしまいます。
KeepAliveタイムアウトはクライアントがリクエストを受信してから次のリクエストを送信するまでの間に発生するため、クライアント側のKeepAliveタイムアウトでは同様の問題は発生しません。
リバースプロキシでのKeepAlive
リバースプロキシは、SSLの終端や負荷分散などの目的でクライアントとプロキシ先のHTTP通信の橋渡しを行います。
KeepAliveを利用してバックエンドとの接続をプーリングすることで、フロントエンドの大量のコネクションから送られるHTTPリクエストを少数のバックエンドコネクションで処理することが可能です。
アイドル状態でプールされていたコネクションを利用しようとしたタイミングでタイムアウトが発生する可能性があるため、(十分に発生し得る程度の)低確率でリクエストに失敗する事になります。
問題を回避するためにはリバースプロキシ側でプール内の接続のタイムアウトを管理できるよう、バックエンドサーバー側のKeepAliveTimeoutより短い値を設定する必要があります。
以下は代表的なリバースプロキシのバックエンドコネクションのタイムアウトに関するリンクです。
- ELB (idle-timeout)
- nginx (ngx_http_upstream_module keepalive_timeout)
- Apache (ProxyPass ttl)
※筆者が以前Apacheで検証した際は、ProxyPassのttl設定がPrefork MPMでは動作せず、Event MPM/Worker MPMでないと動作しない問題に遭遇しました
まとめ
リバースプロキシのHTTP KeepAlive設定クライアント側から切断されるよう、クライアントにサーバより短いタイムアウトを設定するのが原則です。
今回解説した問題は、ELB、nginx、Apacheなどの一般的なL7リバースプロキシ全てに可能性があり、レースコンディション(並行処理の競合)で発生します。
こういった並行処理起因の不具合は再現が難しく、調査が長引いてしまうことも多いです。
思い当たる方は今一度設定を見直してみてはいかがでしょうか。
余談 TCP KeepAliveについて
TCPにもKeepAliveの仕組みがあり、コネクションの生死を確認しハーフクローズを避けるための機能で、全くの別物です。
混同しないように注意しましょう。