Django アプリサーバ、gunicorn と fastcgi の比較

概要
最近、会社では、FastCGIより、Gunicornを使うのがどう?といわれました。Gunicornを触ったことない僕はFastCGIのロードテストも実際やったことなくて、メソッドについて、(preforkがいいか、threadedがいいか)の読んでいたものを元にした推測しかできない状態で、知識足りないと思った。
Gunicornは何かというと、Pythonで作られたWSGIに対応するウェブサーバーです。同期、非同期ウェブアプリ両方対応できますし、作りがよくてかなりスピーディーそうですし、Djangoアプリを簡単に組み込めますし、pythonで運用が楽というのがポイントですね。もちろん、エンドユーザーが直接Gunicornに接続するのではなく、Nginxのローダーバランサーでプロクシーのが一般的だと思っています。
テストアプリケーション
ということで、ちゃんとテストしようと思いまして、GunicornとFastCGI preforkとFastCGI threadedを比較できるテスアプリを作りました。Bitbucketにアップしましたので、ご参考ください。
まず、Buildoutを使いましたので、以下のコマンドでテストサーバーの環境を作ります。MySQLとNginxも必要なので、インストールしておいてください。
python bootstrap.py
./bin/buildout init -d
./bin/buildout
プロジェクトディレクトリのなか、fastcgi_nginx.confとgunicorn_nginx.confができるので、それぞれのテストをするときに、リンクを/etc/nginx/sites-enabled/においてください。アプリは同じポートを使っているので、両方の設定ファイルを同時に有効にすることができない。Gunicornを有効にするときに、まず、FastCGIのリンクを/etc/nginx/sites-enabled/から削除してください。
MySQLのDBは以下のSQLで作れます。
CREATE DATABASE gunicorn_test CHARACTER SET utf8;
DBを作った後に、syncdbコマンドを実行してください。管理者は特にいらないので、noを入力して大丈夫です。
./bin/syncdb
同じポートを使いますし、同時に立ち上げることができないんですが、それぞれのアプリサーバーは以下のように立ち上げます。
./bin/runfcgi (fcgi prefork)
./bin/runfcgi_threaded (fastcgi threaded)
./bin/run_gunicorn -w <# of workers> (gunicorn)
テストアプリはトップページしかなくて、一つのフォームを持って、一ページくらい(20個)のコンテンツを表示するような処理をします。
クライアントは以下のように環境をつくれます。
cd testing/httpclient
python bootstrap.py
./bin/buildout init -d
./bin/buildout
クライアントは以下のように実行できます。クライアントのプロセス数と遅延時間を指定できます。遅延時間でテストの期間を指定できます。デフォールトはプロセス数500で、遅延時間10秒。この設定で、500プロセスの処理を10秒の間に伸びます。1秒目は50プロセス、2秒目は50プロセス。。。と言うふうにテストを行います。つまり、1秒間のテスト数はプロセス数わり遅延時間 (500 / 10 = 50)
./bin/python run_test.py <host> <# processes> <wait time>
クライアントが行う処理は、トップページを表示し、フォームにテキストデータを入れて、POSTする処理です。なので、1プロセスは3回リクエストをします。トップページを表示、POSTリダイレクト、トップページを表示と言う風なテストを行います。なぜなら、ページの閲覧とデータの登録が激しいテストをしたかったわけです。
テストの環境
私は今まで知っていたかぎり、theadedはメモリを節約してくれるけど、コアを使いこなせなくて、メモリが足りるなら、preforkの方がいいという認識でした。Gunicornもマルチプロセスモデルを使っているので、同じくthreadedより早いはずだが、HTTPの解析はFastCGIより若干遅いかなと思いました。Gunicornの作りが全く別なので、なんとも言えないけど、作りが一緒んであれば、HTTPよりFastCGIが若干早いはず。
このテストはEC2上で行い、ハイCPUミディアムインスタンス5台(サーバー1台、クライアント4台)。なぜかというと、複数のコアを使いこなすかどうかをテストしたかったわけです。サーバーインスタンスは2ギガくらいメモリを持っているので、かなりのリクエストを処理するには充分足りるかと思っていました。
Gunicornは5ワーカー(リクエストを処理するプロセス)を使ってテストしました。Gunicornはコア数 * 2 + 1 のワーカーを使うのをおすすめしています。
毎回テストを行う前にDBをクリアしました。
echo "delete from perftest_mymodel;" | mysql -u root gunicorn_test
テストを行った時に、 1クライアント500プロセス、10秒間にしました。ようするに、2000ユーザーを同時にアプリをアクセスして、サーバーの処理できるリミットをテストしました。
./bin/python runtest xxx.compute.internal 500 10
テストの成果
fastcgi (threaded)
---------------------------
Min time: 0.0689101219177s
Max time: 23.1616601944s
Average Time: 8.089163658s
Errors: 1002 / 2000 (50.1%)
fastcgi (prefork)
---------------------------
Min time: 0.0668342113495s
Max time: 21.9225800037s
Average Time: 4.586471732s
Errors: 561 / 2000 (28.1%)
gunicorn
----------------------------
Min time: 0.0718009471893s
Max time: 21.6963949203s
Average Time: 4.199061027s
Errors: 293 / 2000 (14.7%)
Min timeは最低処理時間「1プロセス」、Max Timeは最高処理時間(1プロセス)、Average Timeは平均処理時間(1プロセス)、Errorsは途中でエラーが出て失敗して、処理が出来なかった率とプロセス数。エラーが出たプロセスは時間の計測に入らない。
まとめ
FastCGIは思ったより頑張ってましたが、メモリが充分あれば、プロセスモデルを使ったpreforkメソッドを使うべきでしょうね。平均処理する時間が半分になり、途中でエラーが出る率も半分になりますよね。
Gunicornの場合は処理時間がFastCGIのpreforkと少し早く見えますけど、最低処理時間が少し高くなって、あんまりかわらないんですが、エラー数がpreforkよりさらに半分くらいになりました。要するに、GunicornはFastCGI preforkより多くのユーザーを扱うことができました。ということは、本当の運用しているアプリケーションにGunicornがリソースをより効率的に使う可能性が高いですね。BeProudではもうちょっと検討するのですが、非同期アプリケーションの仕事も増えていますし、将来にGunicornを使うのが良さそうに見えます。
もし、誰かがこのテストを使ったら、他のハードウエア、環境などでは、どういう結果がでるかを聞きたいと思っています。もしくは、テストについてのコメントがあれば、ぜひ宜しくお願いします。