過負荷をかわす Apache の設定
KLab Advent Calendar 2011 「DSAS for Social を支える技術」の9日目です。
前回は php を動かしている Apache の手前にリバースプロキシを 置く必要性を解説しました。 今日は、 その前の php のプロセス数を絞る設定と合わせて、実際に Apache で 設定する方法を紹介します。
以降、 php を動かしている Apache の事をアプリサーバー、リバースプロキシ+ 静的ファイル配信を行っている Apache の事をプロキシサーバーと呼びます。
基本設定
まずは基本的な設定のおさらいです。アプリサーバー
並列数を絞るには MaxClients を設定します。アプリがどれくらいの時間を CPUの処理で使って、どのくらいの時間を外部リソース待ちに使っているかにも よりますが、だいたいCPU数の1.5倍〜2倍くらいが適当だと思います。 HypertThreading や TurboBoost などがある場合は、論理CPUを使いきらなくても 他のCPUの性能がその分上がるので、CPU数とイコールでも良いかもしれません。
以下の設定例では並列数を2にしています。 (追記: この設定は後述の試験をした2コアサーバー用のものです。DSAS for Social の本番環境用Webサーバーはもっとコア数多いし、DBやMemcachedの待ち時間もあるので、もっと大きい値を利用します)
MinSpareServers 2 MaxSpareServers 2 StartServers 2 ServerLimit 2 MaxClients 2
プロキシサーバー
ローカルホストのアプリサーバーのポート(1234としておきます)にリバースプロキシします。 静的ファイルは /static/ に置くことにして、それ以下のパスはプロキシせずに プロキシサーバーが直接返します。
ProxyTimeout を 4 秒に設定することで、5秒ルールを避けます。
# VirtualHost 内 ProxyRequests Off ProxyPass /static/ ! ProxyPass / http://127.0.0.1:1234/ ProxyPreserveHost On ProxyTimeout 4
プロキシサーバーはほとんどメモリを食わないので、並列数は128とか 256とか大き目に設定しても大丈夫です。
スロットル制御
本題の、過負荷になったら、アプリサーバーにプロキシせずにレスポンスを返す 方法に入りましょう。 アプリサーバーの並列数を使いきっている状況では、リクエストをキューイング しておき、処理中だったリクエストのどれかが完了したら次のリクエストを キューから取り出すようにします。 キューの長さが一定値を超えたら、それ以上はキューイングしないでエラーに すれば良さそうです。
もう判りましたよね?キューとは backlog のことです。
アプリサーバーの設定
キューの長さは、安定してレスポンスを返していたらこれ以上は溜まらない、 という数に設定します。 たとえば秒間50リクエストを安定して捌ける場合、 バックログを 50 にしたら、バックログの最後にキューイングされた リクエストは1秒前後で返されることになります。これで5秒ルールに十分 間に合います。
この記事を書きながら触っている環境は古い2コアサーバーなので、 20 程度にしておきます。
Listen 1234 ListenBackLog 20
プロキシサーバーの設定
少なくとも Linux では、 Backlog に空きがある状況で接続の syn パケットが来ると、 すぐに syn/ack を返します。 プロキシサーバーとアプリサーバーの間はループバックアダプタ経由で接続されているので、 パケットロスの心配はありませんし、並列数も絞っていてロードアベレージが 極端に増えることもないので、 1ms もかからずに接続が成功するはずです。
一方、 backlog がいっぱいになった場合、 syn に対して syn/ack が返らないので、 通常は接続元であるプロキシサーバーが 3秒ほど待ってから syn を再送するはずです。 connectiontimeout を短い時間に設定することで、接続できなかったときにすぐに エラーを返すことができます。
connectiontimeout を設定するには、上で行った ProxyPass ディレクティブに パラメータを追加します。とりあえず 1 秒に設定しておきましょう。また、接続がエラーになっても リクエストの転送が止まらないように、 retry=0 もセットで設定しておきます。
ProxyPass / http://127.0.0.1:1234/ connectiontimeout=1 retry=0
効果確認
スロットル制御の動作確認をしましょう。こんな php ファイルを用意しておきます。<?php $prime = array(); $i = 2; while (count($prime) < 1000) { $ok = true; foreach ($prime as $p) { if ($i % $p == 0) { $ok = false; break; } } if ($ok) { $prime[] = $i; } $i++; }まずは、1並列で実行してみます。
$ ab -n100 -c1 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php ... Concurrency Level: 1 Time taken for tests: 10.129 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 ... Percentage of the requests served within a certain time (ms) 50% 101 66% 101 75% 101 80% 102 90% 102 95% 102 98% 103 99% 117 100% 117 (longest request)
だいたい100msちょっとでレスポンスが返ってきますね。 テストに使っているのは2コアのサーバーで、アプリサーバーは2並列なので、 2並列で負荷をかけてもレスポンスタイムは少ししか増えないはずです。
$ ab -n100 -c2 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php ... Concurrency Level: 2 Time taken for tests: 5.771 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 ... Percentage of the requests served within a certain time (ms) 50% 102 66% 102 75% 103 80% 104 90% 198 95% 202 98% 207 99% 208 100% 208 (longest request)
ベンチマーク用に用意した専用環境ではなくて共有のテスト環境を使っているので、 一部のリクエストが遅くなっているのはご了承ください。
次は、4並列で負荷をかけてみましょう。2リクエストを並列処理して2リクエストが backlog に溜まるので、平均レスポンスタイムは2倍くらいに増えるものの、 ちゃんとエラーにならずに全部のリクエストを処理できるはずです。
$ ab -n100 -c4 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php ... Concurrency Level: 4 Time taken for tests: 6.282 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 ... Percentage of the requests served within a certain time (ms) 50% 205 66% 207 75% 210 80% 304 90% 440 95% 450 98% 476 99% 477 100% 477 (longest request)
うまく行きましたね。次は、 backlog と同じ 20 並列で負荷をかけてみましょう。 最悪のレスポンスタイムが 100ms X 20 で 2秒くらいになりますが、ぎりぎりでエラーには ならないはずです。 (リクエスト数も100から200に増やします)
$ ab -n200 -c20 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php ... Concurrency Level: 20 Time taken for tests: 12.485 seconds Complete requests: 200 Failed requests: 0 ... Percentage of the requests served within a certain time (ms) 50% 1042 66% 1083 75% 1324 80% 1559 90% 2002 95% 2011 98% 2017 99% 2034 100% 2052 (longest request)
さて、次は倍の40並列です。2リクエストが処理中になり20リクエストが backlog にたまりますが、以降はアプリサーバーがレスポンスを返して新しいリクエストを 受け取るまでは接続できないので、エラーになるリクエストが出はじめるはずです。
$ ab -n200 -c40 -H 'Host: virtualhost.example.org' http://testhost:1234/nadeko.php ... Concurrency Level: 40 Time taken for tests: 6.223 seconds Complete requests: 200 Failed requests: 83 ... Percentage of the requests served within a certain time (ms) 50% 1026 66% 1119 75% 1130 80% 1133 90% 1138 95% 1224 98% 2117 99% 3187 100% 3191 (longest request)
20並列の時と比べると、200リクエスト中83リクエストがエラーになり、 テストにかかった時間も 12.5秒から 6.2秒と半分程度になりました。 backlog を超えたリクエストを「処理せずにエラーを返す」ことで、 過負荷状況下でも「秒間正常レスポンス数」を維持していることが解ります。
終わりに
以前は、 MySQL の set コマンドで設定できるパラメータを変更したことはあっても、 Apache の設定は完全に「インフラの人」にお願いしていました。 自分で Apache の設定を触るきっかけになったのは チューニンガソンに出場して、 自分でもサーバーのチューニングができるんだという自信がついたからです。 チューニンガソンを開催してくださった運営・スポンサーの方、ありがとうございました。