[Dd]enzow(ill)? with DB and Python

DBとか資格とかPythonとかの話をつらつらと

ISHOCON2に参加してきました

転職決めたあたりでアウトプット増やそうと思ってはじめたブログも1年過ぎてました。今日はISHOCON2に参加してきました。

scouty.connpass.com

ISHOCON1のときはまだscoutyの社員じゃなかったので普通の参加者でしたが、今回はスタッフ兼参加者として言ってきました。途中でベンチマーカが瀕死になるなどトラブルもありましたが、充実した一日でした。@showwinさんありがとうございました。

結果

予習もしてたんであれですが、3位でした。ただ上二人が20万超だったのに9万弱止まりだったのでまだまだです。どんくらいだったかは@showwinさんのツイートがわかりやすい。

Pythonで解いたものの、思いついたことは全部やりきった上でこのスコア差だし上位陣と比べて大きな見落としもなさそうなので、Flaskで素直に解くのはここが限界なのかも・・?

やったこと

リポジトリで公開済です。

github.com

とりあえずプロファイラとかセットアップ

wsgi_lineprofとかいれてとりあえず初期実装でのプロファイルをしました。

https://github.com/denzow/ishocon2-practice/blob/master/python/logs/0.1_line_data.log

複数件投票をするところのINSERTがめっちゃ遅いことがわかりました。ここは同じ内容をいれているだけなので、投票数という列を追加して、ループせずに1SQLで同じデータを保持するようにしました。

github.com

コネクションプールとか

先に細かいところを潰そうと思い、都度接続になっていたDBとのコネクションを簡易プールに置き換えました。また、投票完了後の画面はテンプレートを使用するまでもなく静的なHTMLだったので、そのまま変数にいれておいてそれを使うようにしました。

github.com

URLデコードを回避

github.com

ここは予習で気がついていたのですが、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)をなんとなくつけてできるだけ負荷を削りました。

github.com

変化のないデータをすべて定数化

投票情報以外はベンチマーク中に変更されないので、すべて定数化して起動時に読み込むようにしました。

github.com

このタイミングでは投票リクエストをバッファして、ある程度まとめてINSERTする予定だったので、投票リクエストの格納バッファの準備もしていました。(結局やめた)

投票をDBに永続化せず、Redisに載せた

投票結果をすべてRedisに格納し、DBに永続化しないようにしました。

github.com

投票時のコメントや、候補者ごとの投票数等必要になる切り口ごとにキャッシュをつくっています。RedisはDBより早いとはいえ、uwsgiのキャッシュフレームワークを使ったほうが早いのですが、キーに対するあいまい検索が必要になったのでRedisを泣く泣く追加しました。

この時点で400万件あるusersへのselect以外はSQLを使わなくなっています。

なんかいろいろやった

この頃は、ベンチマーカが不安定になっており負荷をかけると異常終了していた時間帯でした。そのため、ちゃんと1修正ずつ試すのが困難だったので雑なコミットになっています。

github.com

定数化していたHTMLの空白、改行を削ってバイト数を稼いでみたりしてます。後はindexページの結果のHTMLをキャッシュしてみたりしましたがあまり効果はなかったです。

my.cnfとかuwsgiのワーカ数とかいじってた

さすがに400万件はキャッシュ乗らないし打つ手がなくなってきたので、細かい調整をしていました。

github.com

多少効果はあり8万点ほど出ていましたが、この時点で上二人は20万弱でしたのでなにか見落としがないか目を皿にしていました。

MySQLとお別れした

最終コミットでした。

github.com

400万件を4GBメモリーのc4.largeでキャッシュするのは無理かと思ってたのですが、どうも全列をキャッシュする必要はなかったので、ぎりぎり乗せられたのでSQL部分をすべてRedis問い合わせにしました。usersのキャッシュは永続化したかったので、RedisのDB2に分けています。

この変更もあって9万弱まで伸びましたが時すでにお寿司というか焼け石に水でした。

振り返って

まとめてみましたが、強いて言えばRedisのZRANKを使えばもうちょい早くなっただろう部分がありましたが、大きくやり残したところも見当たらずこのあたりがPython(Flask)の限界なのかもと思いました。Goとかに書き換えればいいのかもしれないですが、悔しいのでSanicとかAPIStarあたりのAsyncフレームワークをつかってもうちょっとどうにかならないかを今度追試してみたいと思います。