Apacheでリバースプロキシ、タイムアウトを上手にコントロール


リバースプロキシサーバ

17日目はリバースプロキシサーバについて書きます。

初期のリバースプロキシサーバの主な利用目的は社内イントラネット内に構築したWebサーバを社外からアクセスする事でした。

それがSSLアクセラレータ、ロードバランサ、キャッシュサーバ等の機能を少しずつ拡張し発展してきました。現在ではOpenAM、Keycloak、ADFSといった認証サーバとシングルサインオン連携し、アプリケーション側の認証実装の面倒なところを一気に引き受けるような用途でも使われるようになりました。

このあたりの話は@￰ITのサイトにうまくまとめられていました。こちらをご参考にしてください。
90秒の動画で学ぶITキーワード:リバースプロキシ(Reverse Proxy)

今回はリバースプロキシサーバを構築する際に考慮すべきタイムアウトの話を中心に書きました。

前提となる環境

  • CentOS 7.5
  • httpd-2.4.6-80(CentOS 7.5)

mod_proxy

Apacheでリバースプロキシを実現するにはmod_proxyモジュールを使用します。
https://httpd.apache.org/docs/2.4/en/mod/mod_proxy.html

mod_proxyにはサブモジュールが多数あります。よく使うのはこの3つです。

  • mod_proxy_http 通常バックエンドとはHTTPプロトコルで接続するのでほぼ必須です。
  • mod_proxy_ajp TomcatとAJPプロトコルで接続する場合に必要です。
  • mod_proxy_balancer ロードバランサの用途として使う場合に必要です。

タイムアウトとは

タイムアウトは処理や通信を一定期間待った後、時間切れとなり中止/中断する事です。
リバースプロキシを実装する上で考慮しなければならないタイムアウトは3つあります。

  • TCPコネクションタイムアウト
  • 処理タイムアウト
  • キープアライブタイムアウト

TCPコネクションタイムアウト

リバースプロキシがバックエンドのWebサーバとTCP接続するまで待つタイムアウト時間です。
バックエンドWebサーバの最大接続数を超えて接続しようとしたり、ノード自体が停止している(IPアドレスが存在していないので応答をすぐに返せない為)様なケースで待ちが発生します。基本的にここで長い時間待つ必要はありません。長くても数秒程度でしょう。

処理タイムアウト

TCP接続が完了した後、HTTPリクエストを送信してからレスポンスが戻るまでに待つタイムアウト時間です。処理タイムアウトと言う言葉を使いましたがTCPレベルではソケットタイムアウトを指しています。
例えばバックエンドのWebサーバがデータベースに対してSQL検索し、そのSQL実行が遅い様なケースでタイムアウトします。大きなファイルのダウンロードのようにデータが継続的に流れていれば処理タイムアウト時間を超えてもタイムアウトしません。

キープアライブタイムアウト

HTTPの基本的な動作はリクエスト/レスポンスを送受信する度にTCP接続をオープン/クローズします。いくらHTTPが軽量なプロトコルだと言っても、これでは大量のリクエストをさばく事は出来ません。そこで1つのTCP接続を維持(キープ)したまま、その中で複数(大体100回位)のリクエスト/レスポンスを送受信するのがキープアライブです。
キープアライブタイムアウトはクライアント/サーバの関係で言うと、サーバ側が決定するタイムアウト値です。

TCPコネクションを長時間維持すると、それはそれで問題となります。適切なキープアライブタイムアウト時間を設定し、TCPコネクションをクローズする必要があります。

リバースプロキシの場合はリバースプロキシのフロント側とバックエンド側は適切なキープアライブタイムアウト時間が異なりますので考慮が必要です。

尚、キープアライブはHTTP/1.0時代の言葉でHTTP/1.1では パーシステント コネクションズ(persistent connections)を使うのが正確な様です。

フロント側のキープアライブ

リバースプロキシがWebブラウザなどのユーザエージェントと直接接続する場合はキープアライブタイムアウトは短め(1秒程度)とするのが適当です(Apacheのデフォルトは5秒)。Webブラウザは短時間でコンテンツをダウンロードする為、同時に5つ程度のTCPコネクションをオープンします。そしてキープアライブを使って連続的(間を空けずに)にダウンロードします。従って1秒程度でも充分に機能します。逆にキープアライブタイムアウトが長いと弊害があります。通信が終わってもWebブラウザとの間でコネクションを開放しません。リバースプロキシサーバ側の最大接続数に達してしまう可能性があります(MPMがprefork,workerの場合、eventでは発生しません)。

リバースプロキシとWebブラウザの間にロードバランサが存在している場合は、逆にキープアライブタイムアウトを長くしたほうがオープン/クローズが最小限に抑えられ一般的には効率が良いです。ただしロードバランサの仕様に依存します。

バックエンド側のキープアライブ

バックエンドはフロント側のキープアライブより少し長めにします。
リバースプロキシサーバで指定できる値ではない為、バックエンドのサーバ側で指定します。

proxy:error reading status line from remote server ...問題

Apacheを運用しているとエラーログに下記メッセージが出力される事はありませんか?
アクセス数の多いサイトだと結構な量が出力され、嫌ですよね。

(104)Connection reset by peer: proxy: error reading status line from remote server 192.168.12.7
proxy: Error reading from remote server returned by /top

これはリバースプロキシ(Apache)とバックエンドの間でコネクションを再利用した際にリバースプロキシからリクエストを送信するタイミングとバックエンド側がコネクションをクローズしたタイミングが一致した時に発生します。リバースプロキシ側からすると、さっきまで道が存在していたのに、まばたきを1回したら、目の前の道が無くなった様な状況です。微妙なタイミングで発生するエラーなので意図的に再現するのが非常に難しい事象です。
クライアント(ブラウザ)にはHTTPステータス502が返るか、TCPコネクションが切断されます。
この問題にも効果的なパラメタがありますので説明します。

各タイムアウトとApacheのパラメタ

Apacheにはタイムアウト系パラメタが多数あります。そのネーミングがとても分かりづらいのが難点です。
日本語ドキュメントが追い付いていなかったので。英語のドキュメントをベースに整理しました。
Apacheのパラメタにはディレクティブと呼ばれるものとディレクティブのサブパラメタがあります。
タイムアウト系パラメタはProxyPassディレクティブのサブパラメタにも存在しています。
mod_proxy - Apache HTTP Server Version 2.4 - ProxyPass

パラメタ 説明 デフォルト値
ProxyTimeout
ディレクティブ
処理タイムアウト(ソケットタイムアウト)の値です。 TimeOutディレクティブの値
timeout
サブパラメタ
処理タイムアウト(ソケットタイムアウト)の値です。バックエンド毎に個別に指定したい場合はこちらを使用します。 ProxyTimeoutディレクティブの値
connectiontimeout
サブパラメタ
TCPコネクションタイムアウト値のです。 timeoutサブパラメタの値
TimeOut
ディレクティブ
全てのタイムアウトのデフォルト値のようなものです。
またApache上でCGIプログラムなどが動作する際の処理タイムアウト相当となります。
60秒
KeepAliveTimeout
ディレクティブ
フロント側に対するキープアライブタイムアウト値です。 5秒
proxy-initial-not-pooled
環境変数
有効にするとクライアントの最初の接続にはプールしたコネクションを使わず、新しくコネクションを作成します。mod_proxy_httpの機能です。mod_proxy_ajpでは使用できません。 無効

TCPコネクションタイムアウトにはconnectiontimeoutサブパラメタ

バックエンドのTCPコネクションタイムアウトにはProxyPassディレクティブのconnectiontimeoutサブパラメタを使います。デフォルト値は60秒(元をたどるとTimeOutディレクティブ)です。数秒程度が適当であろうと思います。
本記事では詳しく述べていませんがmod_proxy_balancerでロードバランシングする場合は0秒(待たない)が適当です。

処理タイムアウトにはProxyTimeoutディレクティブ

バックエンドの処理タイムアウトにはProxyTimeoutディレクティブを使います。デフォルト値は60秒(元をたどるとTimeOutディレクティブ)です。

バックエンドのキープアライブにはproxy-initial-not-pooled

リバースプロキシで考慮するのは次の2点です。

  • proxy:error reading status line from remote server ...問題への対策。
  • リバースプロキシサーバ/バックエンドサーバ間の使われないコネクションを速やかにクローズ。

バックエンドのキープアライブにはproxy-initial-not-pooled環境変数を使います。
proxy-initial-not-pooledはApache2.2の日本語サイトに下記に説明があります。

この環境変数をセットすると、クライアントの最初の接続にはプールした 接続を使わなくなります。これは競合状態を原因とする "proxy: error reading status line from remote server" エラーメッセージを 回避します。競合状態は、プロキシがプールした接続をチェックした後、 プロキシの送ったデータがバックエンドに到達する前にバックエンドが接続を閉じると発生します。 この変数をセットすることでパフォーマンスが劣化することを知っておくべきです。 特に HTTP/1.0 のクライアントに影響します。

クライアントがリバースプロキシとの間で最初に接続をした時、リバースプロキシとバックエンドの間に他のクライアントが使っていたコネクションが残っていも、それは使わず新しくコネクションをオープンして使います。そうすれば意図せずクローズされてしまう事はないという意味です。
ここで注意があります。他のクライアントが使っていたコネクションを再利用しないという事は、残ったコネクションはリバースプロキシサーバ/バックエンドサーバ双方に無駄なリソースとなります。特にバックエンドサーバにとっては最大接続数に達してしまう危険があります。
バックエンドとの間に残ったコネクションは速やかにクローズすべきです。先に「バックエンドはフロント側のキープアライブより少し長めにします。」と書いたのはそうした意図があった為です。
尚、バックエンドサーバのキープアライブタイムアウトはリバースプロキシサーバ側では指定できないので、バックエンドサーバ側で指定します。

フロントとのキープアライブにはKeepAliveTimeout

フロントとのキープアライブにはKeepAliveTimeout ディレクティブを使います。デフォルト値は5秒です。

記載サンプル

ここまで説明したパラメタの記載サンプルです。

KeepAliveTimeout 1
ProxyTimeout 300
setEnv proxy-initial-not-pooled 1
<Location /backend/>
  ProxyPass http://backendsvr/ connectiontimeout=5
</Location> 

その他の考慮ポイント

タイムアウト以外のリバースプロキシサーバを構築する際の考慮ポイントを説明します。

MPM

Apache2.4からデフォルトMPMはeventとなりました。しかしCentOS7/RHEL7のデフォルトMPMはいまだにpreforkです。リバースプロキシでpreforkは非効率なのでworkerかeventに変更します。

具体的な変更方法は下記を参考にしてください。
Apacheチューニング方法(Apache2.4, prefork) MPMの切り替え方法(Apache2.4)

ファイルオープン数

ApacheがオープンしたコネクションもOSレベルでは1つのファイルとして扱われます。1つのプロセスがオープンできるファイル数はデフォルトで1024です。Apacheがバックエンドのサーバと大量のコネクションをオープンするとその上限に引っかかる事があります。Apacheは複数の子プロセスを生成します。1つの子プロセスの上限なのでそうそう超えることはないですが、必要に応じて変更します。

具体的な変更方法は下記を参考にしてください。
Linux_ファイルディスクリプタ数の上限

バックエンドのコネクションポート枯渇

リバースプロキシとバックエンドの間のコネクションは出来る限り再利用(キープアライブ)し、コネクションのオープン/クローズは最小限にすべきです。しかしながらリバースプロキシの特性上、仕方がありません。
proxy-initial-not-pooledを有効にすればポートが枯渇する可能性は低いですが、もし不足するような場合は利用できるポート番号の範囲を広げること検討します。

sysctl で net.ipv4.ip_local_port_range パラメタを変更します。
デフォルトはCentOS7なら32768~60999です。それを10000~60999の様に変更します。当然OS内で起動するサービスがリッスンするポートと被らない様、範囲を検討します。
具体的な変更方法は下記を参考にしてください。
b.l0g.jp TCP/IPの送信用ポート範囲を変更する

最後に

本記事を記載にするにあたって過去に担当したシステムの資料を見直しました。
実は勘違いしていた部分もあったりして、今回は知識の再整理ができました。

記事レビューをしてくれた 育休中のSさん/同僚のエンジニアの皆さんに感謝します。