フロント/バックの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したり、ちょっとずつレスポンスを返したりとか)が変えられるようになっています。