フロント/バックのreverse proxy構成で、指定秒数以内に必ずレスポンスを返す方法


目的

フロントがHTTPリクエストを受けて、バックエンドのアプリケーションサーバにreverse proxyするような構成において、指定秒数以内に何かしらのレスポンスを返したい。

200が返せない場合は、処理を打ち切って500を返したい。

背景

フロントでApacheやNginxをreverse proxyとして使っている場合、バックエンドが無応答になってしまうと、クライアントにレスポンスが返るのはデフォルトで数十〜数百秒後(ApacheのTimeoutのデフォルトは300秒、Nginxのproxy_read_timeoutのデフォルトは60秒)になってしまいます。

通常のWebサービスではこのオーダーのタイムアウトでもいいのかもしれませんが、数秒以内に(エラーでもいいので)レスポンスを返すことが求められる環境も存在します。(最近、特に多いのではないでしょうか:P)

もちろんバックエンドが無応答にならないようにするのが最善なのですが、そうはいってもダメになるときはダメになります。そういった局面でも、指定秒数以内にエラーのレスポンスを確実に返して、最悪の状況(ペナルティをくらったりとか)に陥らないようにしたい、というのが今回のお話の背景です。


バックエンドが無応答になる理由はいろいろありますが、典型的なのはDBへのクエリに時間がかかったのが原因で、というのが多いのではないかと思います。

話を単純化した例をあげます。


今、ピークで秒間100リクエスト(100req/s)を捌いていて、平均の応答時間が1秒だとします。図示するとこんな感じですね。


そしてとある新機能を投入したのですが、クエリの性能試験とチューニングが甘くて、応答時間が5秒に悪化してしまったとします。図示するとこんな感じです。


図を見ると、積算された500req/sを捌けるだけのリソースがあればいいように見えますが、現実はさにあらず。

DBの負荷はますます増大し、5秒で返せていたクエリは過負荷により雪だるま式に6秒、8秒、10秒とその応答時間が劣化していき、バックエンドのアプリケーションサーバーではレスポンス待ちプロセスがどんどん増えていきます。もし慌ててフロントやバックエンドのアプリケーションサーバの数(台数やプロセス数)を増やそうものなら、ますますDBに負荷をかけることになり傷口に塩を塗ることになってしまいます。

こうなるともうシステムダウンです。

ダウンなのですが、同じダウンでもクライアントに応答待ちで待たせるのではなく、指定秒数(5秒とか)でエラーのレスポンスを返したい、というわけです。


あともう一つ気に留めておいて欲しいのは、例でみたように、req/sが大きければ大きいほど、ちょっとしたことであっという間にカタストロフィを迎えてしまう、という点です。月間PVやデイリーPVも、特にマーケティング方面の指標としては有益なのですが、インフラやアプリケーションエンジニアの人は、ピーク時の秒あたりのリクエスト数も注視するべきだと僕は思います。

検証

さて、どうやったらタイムアウトを制御できるか、という本題に入ります。

今回は、この2つをフロントのreverse proxyとして取り上げます。

  • Apache 2.2.15 (prefork)
  • Nginx 0.7.67
Apache (prefork)

まず、リクエストしているクライアント数<MaxClientsの状況下では、ProxyTimeout Directiveで制御できます。

AddHandler    send-as-is asis

ProxyTimeout  5
ErrorDocument 502 /static/502.asis

ProxyPass         /static !
ProxyPass         / http://localhost:5000/ disablereuse=On keepalive=Off retry=5
ProxyPassReverse  / http://localhost:5000/
goa[~]$ time curl -i http://delhi/?t=8
HTTP/1.1 200 bad gateway
Date: Tue, 22 Jun 2010 04:58:04 GMT
Content-Length: 14
Connection: close
Content-Type: text/plain; charset=utf-8

mata kite ne


real    0m5.011s
user    0m0.000s
sys     0m0.000s

goa[~]$ time curl -i http://delhi/?b=8
HTTP/1.1 200 OK
Date: Tue, 22 Jun 2010 05:01:27 GMT
Content-Type: text/plain; charset=utf-8
Connection: close
Transfer-Encoding: chunked

curl: (18) transfer closed with outstanding read data remaining

real    0m5.027s
user    0m0.000s
sys     0m0.000s

ちゃんと期待した通り、5秒でレスポンスが返ってきました。

ただ、バックエンドからbodyがちょろちょろ返ってくる(2秒おきにbodyのデータを返す)場合は、ProxyTimeoutやTimeoutは効きませんでした。

goa[~]$ time curl -i http://delhi/?d=1
HTTP/1.1 200 OK
Date: Tue, 22 Jun 2010 05:07:06 GMT
Content-Type: text/plain; charset=utf-8
Connection: close
Transfer-Encoding: chunked

delayed 1
delayed 2
delayed 3
delayed 4
delayed 5
delayed 6

real    0m12.024s
user    0m0.000s
sys     0m0.000s


問題は次で、リクエストしているクライアント数>MaxClientsの場合です。

実験がしやすいように低めの値にしています。

ListenBackLog 1

StartServers           3
MinSpareServers        3
MaxSpareServers        3
MaxClients             3

この状態で、16リクエストをほぼ同時に行った結果がこれです(結果は抜粋です)。

goa[~]$ n=1; while [ $n -le 16 ]; do ( time wget -O - --save-headers --connect-timeout=999 --timeout=999 http://delhi/?t=16 & ); n=$(($n+1)); done
real    0m5.016s
real    0m5.018s
real    0m5.014s
real    0m10.019s
real    0m10.006s
real    0m14.011s
real    0m15.001s
real    0m14.999s
real    0m22.896s
real    0m22.899s
real    0m27.011s
real    0m27.896s
real    0m27.893s
real    0m32.931s
real    0m32.931s
real    0m35.477s

最初の3つは期待通り5秒で返ってきていますが、それ以降はどんどん時間がかかっているのがわかります。ちなみに、curl --connect-timeout 999 --max-time 999 --retry 0で試してみたところ、だいたい9つめ以降は21秒でconnection failで終了しました。追いかけてないですが、21秒はcurl的なタイムアウトなのではないかと思います。(だれか知ってる人がいたら教えてください><)

いずれにせよ、こちらでタイムアウトの時間が制御できなくては、目的が達せられません。

まとめると、MaxClientsを増やせば目的は達せられますが、preforkの場合はそんなに多くは増やせないので、Apacheだとちょっと厳しい、という結論になりました。

ちょっと事情があってApacheならpreforkなんですが、だれかworklerやeventで試してみた人がいたら教えてください><

追記

Nginx

次にNginx。

worker_processes  2;
events {
  worker_connections  8;
  use epoll;
}

http {
  ...
  server {
  ...
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
      root  /path/to/static/contents;
    }

    location / {
      proxy_pass        http://127.0.0.1:5000;
      proxy_pass_header Server;
      proxy_set_header  Host $host;
      proxy_set_header  X-Forwarded-Host   $host;
      proxy_set_header  X-Forwarded-Server $host;
      proxy_set_header  X-Forwarded-For    $proxy_add_x_forwarded_for;
      proxy_set_header  X-Geo              $geo;

      proxy_read_timeout 5;
    }
  ...

Nginxのドキュメントに依れば、reverse proxyの場合は、

  • max_clients = worker_processes * worker_connections/4

だそうなので、前掲のパラメータではMaxClientsは4になりますね。


まず、リクエストしているクライアント数<MaxClientsの場合。

goa[~]$ time curl -i http://delhi:1081/?t=8
HTTP/1.1 504 Gateway Time-out
Server: nginx/0.7.67
Date: Tue, 22 Jun 2010 08:39:46 GMT
Content-Type: text/html
Content-Length: 383
Connection: close

mata kite ne!

real    0m5.010s
user    0m0.000s
sys     0m0.000s

goa[~]$ time curl -i http://delhi:1081/?b=8
HTTP/1.1 200 OK
Date: Tue, 22 Jun 2010 08:40:49 GMT
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: close
Server: Plack::Handler::Starlet


real    0m5.010s
user    0m0.000s
sys     0m0.000s

期待通り5秒でレスポンスが返ってきています。

ただ、proxy_read_timeoutはreadとreadの間のタイムアウトなので、バックエンドからbodyがちょろちょろ返ってくる(2秒おきにbodyのデータを返す)場合は、proxy_read_timeoutが効きません。Apacheと同じですね。(総時間でタイムアウトを起こす方法知ってる人がいたら教えてください><)

goa[~]$ time curl -i http://delhi:1081/?d=1
HTTP/1.1 200 OK
Date: Tue, 22 Jun 2010 08:41:51 GMT
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: close
Server: Plack::Handler::Starlet

delayed 1
delayed 2
delayed 3
delayed 4
delayed 5
delayed 6

real    0m12.011s
user    0m0.000s
sys     0m0.000s


続いて、リクエストしているクライアント数>MaxClientsの場合です。

goa[~]$ n=1; while [ $n -le 16 ]; do ( time wget -O - --save-headers --connect-timeout=999 --timeout=999 http://delhi:1081/?t=16 & ); n=$(($n+1)); done
real    0m5.017s
real    0m5.018s
real    0m5.013s
real    0m6.018s
real    0m6.017s
real    0m6.010s
real    0m11.021s
real    0m11.022s
real    0m11.017s
real    0m15.013s
real    0m15.014s
real    0m15.020s
real    0m20.022s
real    0m20.018s
real    0m20.014s
real    0m26.009s

Apacheと同じような結果になってしまいましたが、このケースでは意図的にMaxClientsが小さくなるようにしていたのでした。マルチスレッド+イベント駆動モデルなNginxは、Apache (prefork)と比べて、メモリフットプリントを小さく抑えつつMaxClientsをかなり大きくできます。つまり、通常運用ではこんなパラメータにはしません。例えば、このように、

worker_processes  3;
events {
  worker_connections  4096;
  use epoll;
}

MaxClientsが3,072になるようにしてもう一度試してみると、

goa[~]$ n=1; while [ $n -le 16 ]; do ( time wget -O - --save-headers --connect-timeout=999 --timeout=999 http://delhi:1081/?t=16 & ); n=$(($n+1)); done
real    0m5.018s
real    0m5.015s
real    0m5.015s
real    0m5.011s
real    0m5.014s
real    0m5.017s
real    0m5.020s
real    0m5.018s
real    0m5.026s
real    0m5.025s
real    0m5.017s
real    0m5.019s
real    0m5.015s
real    0m5.014s
real    0m5.014s
real    0m5.015s

きれいに5秒でレスポンスを返しています。いい感じです。

結論

  • フロントのreverse proxyは、マルチスレッドやイベント駆動といった、MaxClientsを大きくできるモデルのものにする。
  • 僕の場合はNginxで。メモリ消費量も少ないし、少しですけど個人的に稼働実績もあるので。
  • Apacheでもworkerやevent MPMならいいのかもしれない。

おまけ

バックエンドのアプリケーションサーバーは、Plackで実装して検証しました。リクエストパラメータで挙動(指定秒数sleepしたり、ちょっとずつレスポンスを返したりとか)が変えられるようになっています。