転職決めたあたりでアウトプット増やそうと思ってはじめたブログも1年過ぎてました。今日はISHOCON2に参加してきました。
ISHOCON1のときはまだscoutyの社員じゃなかったので普通の参加者でしたが、今回はスタッフ兼参加者として言ってきました。途中でベンチマーカが瀕死になるなどトラブルもありましたが、充実した一日でした。@showwinさんありがとうございました。
結果
予習もしてたんであれですが、3位でした。ただ上二人が20万超だったのに9万弱止まりだったのでまだまだです。どんくらいだったかは@showwinさんのツイートがわかりやすい。
ISHOCON2 お疲れ様でした。高校二年生が優勝するとは思ってもいませんでしたが、若者には負けてられない…という気持ちになりました。 #scouty_ishocon pic.twitter.com/09Pl5L9n8f
— Shogo ITO (@showwin) 2018年8月25日
Pythonで解いたものの、思いついたことは全部やりきった上でこのスコア差だし上位陣と比べて大きな見落としもなさそうなので、Flaskで素直に解くのはここが限界なのかも・・?
やったこと
リポジトリで公開済です。
とりあえずプロファイラとかセットアップ
wsgi_lineprofとかいれてとりあえず初期実装でのプロファイルをしました。
https://github.com/denzow/ishocon2-practice/blob/master/python/logs/0.1_line_data.log
複数件投票をするところのINSERTがめっちゃ遅いことがわかりました。ここは同じ内容をいれているだけなので、投票数という列を追加して、ループせずに1SQLで同じデータを保持するようにしました。
コネクションプールとか
先に細かいところを潰そうと思い、都度接続になっていたDBとのコネクションを簡易プールに置き換えました。また、投票完了後の画面はテンプレートを使用するまでもなく静的なHTMLだったので、そのまま変数にいれておいてそれを使うようにしました。
URLデコードを回避
ここは予習で気がついていたのですが、Flaskのrequest.form
からの値の取り出しはめちゃめちゃ遅いのです。恐らくunquote_plus
がかなり遅い。なので、DB側にURLエンコード済のデータをいれておきデコードせずにSQLに使えるようにしています。
といっても、対象のデータが400万件ある上にSQLで実行することも難しいので処理用のスクリプトを書いてやりました。 https://github.com/denzow/ishocon2-practice/blob/master/python/work.py
なお、Flaskのソースを確認したところrequest._get_stream_for_parsing()
でデコード前の情報が取れることがわかったので、ここから直接デコード前の結果を取得していました。
あと、一部CSSをnginxから配信するようにした上でhttp2も有効化しておきました。
URLデコードを撲滅
投票結果画面等で表示する部分はURLエンコードされたままだとまずいので、unquote_plus
を泣く泣く呼んでいましたが、表示に使う分だけデコードすればいいことに気がついたのでさらに削りました。また、@lru_cache(maxsize=100)
をなんとなくつけてできるだけ負荷を削りました。
変化のないデータをすべて定数化
投票情報以外はベンチマーク中に変更されないので、すべて定数化して起動時に読み込むようにしました。
このタイミングでは投票リクエストをバッファして、ある程度まとめてINSERTする予定だったので、投票リクエストの格納バッファの準備もしていました。(結局やめた)
投票をDBに永続化せず、Redisに載せた
投票結果をすべてRedisに格納し、DBに永続化しないようにしました。
投票時のコメントや、候補者ごとの投票数等必要になる切り口ごとにキャッシュをつくっています。RedisはDBより早いとはいえ、uwsgiのキャッシュフレームワークを使ったほうが早いのですが、キーに対するあいまい検索が必要になったのでRedisを泣く泣く追加しました。
この時点で400万件あるusersへのselect以外はSQLを使わなくなっています。
なんかいろいろやった
この頃は、ベンチマーカが不安定になっており負荷をかけると異常終了していた時間帯でした。そのため、ちゃんと1修正ずつ試すのが困難だったので雑なコミットになっています。
定数化していたHTMLの空白、改行を削ってバイト数を稼いでみたりしてます。後はindexページの結果のHTMLをキャッシュしてみたりしましたがあまり効果はなかったです。
my.cnfとかuwsgiのワーカ数とかいじってた
さすがに400万件はキャッシュ乗らないし打つ手がなくなってきたので、細かい調整をしていました。
多少効果はあり8万点ほど出ていましたが、この時点で上二人は20万弱でしたのでなにか見落としがないか目を皿にしていました。
MySQLとお別れした
最終コミットでした。
400万件を4GBメモリーのc4.largeでキャッシュするのは無理かと思ってたのですが、どうも全列をキャッシュする必要はなかったので、ぎりぎり乗せられたのでSQL部分をすべてRedis問い合わせにしました。usersのキャッシュは永続化したかったので、RedisのDB2に分けています。
この変更もあって9万弱まで伸びましたが時すでにお寿司というか焼け石に水でした。
振り返って
まとめてみましたが、強いて言えばRedisのZRANKを使えばもうちょい早くなっただろう部分がありましたが、大きくやり残したところも見当たらずこのあたりがPython(Flask)の限界なのかもと思いました。Goとかに書き換えればいいのかもしれないですが、悔しいのでSanicとかAPIStarあたりのAsyncフレームワークをつかってもうちょっとどうにかならないかを今度追試してみたいと思います。