#isucon 2014予選の延長戦をやってみた
予選の時間内では足りてないことばかりだったので、もう少し試行錯誤することにしました。
#isucon 2014の予選をほぼ一人で戦うハメになった話 - orangain flavor
目標は50000点、できれば60000点出したい。
予選終了時
最終提出スコア: 32710
細々とした改良
- nginxの設定を追加。
- redis-pyのパーサーをhiredisに置き換え。
- テンプレートエンジンを使わないよう変更。
- アプリを見なおして、RedisのRead/Write数を削減。
スコアはあまり上がらず 32912。
Gunicornのワーカーをmeinheldに置き換え
前回のエントリのコメントで id:methane 氏に教えていただいたmeinheldに置き換え、nginx―Gunicorn間でKeepAliveを有効にしました。PyPI最新の0.5.6です。
スコアは5000ほど上がって、 37223。
副作用として、nginxでCookieがないときだけ静的なindex.htmlを返すことができなくなりました。
nginxでの良い設定方法がわからず、こんな感じで無理矢理やっていたのですが、meinheldを使うと400 Bad Requestが返ってくるようになりました。
location = / { if ($http_cookie !~* "session") { root /home/isucon/webapp/public/; rewrite ^.* index.html break; } proxy_pass http://app; proxy_set_header Host $http_host; proxy_http_version 1.1; proxy_set_header Connection ""; } location / { proxy_pass http://app; proxy_set_header Host $host; proxy_http_version 1.1; proxy_set_header Connection ""; }
原因不明ですが、Varnishを使うともっと簡単に設定できるようになると聞いていたので、この時点では深追いしませんでした。
Varnish導入
Varnishを導入しました。yumで4.0.xをインストールしたつもりが、Amazon Linuxだと3.0.xしかインストールできずしばらくハマりました。
また、daemonの起動に失敗してもどこにもログが吐かれないのでだいぶ悩みました。 varnishd -C -f FILE
でvclを検証すれば良いことを知りました。
バックエンドとしてUnix Domain Socketに対応していないようなので、元の通りGunicornをTCP 8080で動かすようにしました。
静的ファイルはnginx、アプリはgunicornと振り分けるよう設定しました。
スコアはさほど変わらず 37632。
エラーページを静的に返すように
Varnishで簡単にキャッシュできるようになったので、ログイン失敗時に /?err=***
のページにリダイレクトするようにしました。
これは結構効いて、スコアは 43715 に。Varnish便利。
データをインメモリに持つように
ユーザー情報とBAN/Lockの情報をメモリに保持するようにしました。マルチプロセスなので、結局Redisにもアクセスする必要があり、苦労した割に効果は薄かったです。
スコアは上がらず、というかむしろ下がり 42533。
FlaskからBottleに書き換え
プロファイリング結果を見るとFlaskで時間を食っていたので、さらにmicroなフレームワーク Bottle に置き換えました。
これは結構効果があり、スコアは 50386。
Sessionを暗号化されていないCookieに置き換え
正直どうなのかという話ですが、Session Cookieの暗号化と検証を捨て、ユーザー名と最終ログイン情報をCookieに直接格納しました。
このとき、Cookieの値に空白が含まれるとベンチマーカーが意図したとおりに返してくれない問題に若干苦しみました。
スコアは若干上がり 51076。
RedisからのReadをパイプライン化
attempt_loginの条件分岐の中で、細切れでRedisからReadしていたのを、パイプラインで一括取得するようにしました。
これによりRedisへのアクセスは、1リクエストあたり最大Read1回、Write1回の計2回で済むようになりました。
スコア: 52705
1回休み
Redisの設定を見ると、いつの間にかRedisのappendonlyが無効になっており、永続化されてない状態になっていました。
有効にしてスコアを測り直すと 51246 でした。
PyPyにしてみる
PyPyを使ったら速くなったりしないかなーと思って試してみますが、スコアは 46607 に下がったので却下。
PyPyに単純に置き換えただけで、標準以外のライブラリも普通に動くのだなと感動しました。
2014-10-05 23:34追記: 後ほどちゃんとPyPyに合わせてチューニングしたらCPythonよりも高いスコアがでました。詳しくは文末の追記を参照。
Varnishのチューニング
VarnishのCPU利用率が高いのでチューニングしてみました。
- VARNISH_MIN_THREADS=200 → 50
- -p thread_pools=4
- -p session_linger=100
いまいちよくわからないもののスコアは若干上がり 53016。
最新のmeinheldに置き換え
GitHubの最新版を使うと良いと教わったので、置き換えてみました。
@orangain gunicorn の worker は何を使われてます? meinheld の github 最新版を使ってもらえると性能上がるかも。
— INADA Naoki (@methane) 2014年10月4日
スコアは若干上がり、 53872。
まとめ
最終的なスコアは 53872 となりました。これ以上はどうすればスコアが上がるか思いつきません。
とりあえずの目標としてた50000は超えましたが、60000には届きませんでした。Pythonのマルチプロセスモデルだとこの辺が限界なんでしょうか。。
ちなみにワークロードやGunicornのワーカー数はいじってもスコアが上がらなかったので、変えてません。
クーポンのおかげで、あまりお金をかけずに勉強できました。AWS様ありがとうございます。
参考サイト
- Gunicorn
- Varnish
- Redis
- Bottle
- PyPy
おまけ
GOGC=offでベンチマーク実行
$ GOGC=off ~/benchmarker bench --workload 10
スコア: 79324
静的ファイルを返さないように
<link>
タグと<img>
タグをdocument.write
で書きだすようにしました。
$ GOGC=off ~/benchmarker bench --workload 10
スコア: 180084
orangain/isucon2014-qualifier-python at no-static
2014-10-05 23:34追記:PyPyをちゃんと導入してみた
例によって id:methane 氏に教えて頂きました。ほんとうに感謝です。
@orangain あと、PyPyを使う場合は JIT のウォームアップのオーバーヘッドが大きいので、プロセス数を絞った上で、 init で wrk か何かで温めてやる必要があります。
— INADA Naoki (@methane) 2014年10月5日
PyPyへの最適化
Tornado 3.2をインストールし、GunicornのワーカーをTornadoWorkerに置き換えました。またC拡張であるhiredisもアンインストールしました。
さらにinitスクリプトの中で、JITのウォームアップとして25秒間ベンチマークを走らせました。
ベンチマーク実行中は、GunicornのCPU消費がCPythonに比べて格段に減り、ワーカー数を減らすほどスコアが上がるようになりました。ワーカー数1、ワークロード14で 54736 まで上がりました。
1プロセスへの最適化
1プロセスを前提とすれば、メモリにRedisと同じ内容を保持し、RedisへのReadアクセスを完全に無くすことができます。Readしないように書き換えたところ、同じくワーカー数1、ワークロード14で 55955 まで上がりました。
orangain/isucon2014-qualifier-python at pypy
PyPyを使ってみて
PyPyは今回初めてまともに使いましたが、サードパーティのライブラリを含めてエラーなどは全く無く、単純に置き換えただけ*1で正常に動作し、正直驚きました。
Amazon Linuxへのインストールこそ非公式配布のバイナリを使いましたが、非常に安定している印象を受けました*2。
PyPyを使えば、Pythonでも1プロセスで高速に処理するGo言語のような戦い方ができることを確認できて良かったです。それでも来年はGoで出られる程度にはGo言語力を高めたいですが。