ISUCON6予選にチーム「試運転」で参加しました

isucon.net

私と@menphim, @yurahunaで予選2日目に参加しました。

事前にやったこと

使用する言語の選択では,メンバー全員が研究で多少は使っていることから,Pythonを選択することに決めました。過去のISUCONでの本選出場者数から考えるとPythonはやや分が悪いっぽいのですが,普段Web系のプログラミングをしていない勢のチームだったので使い慣れた言語の使う方が大切だと思ったからです。

練習はISUCON5の予選でやりました。「MySQLってどうやってログインするの?」とか「バックアップってどうやるの?」というところから始めないといけなくて時間がかかりましたが,数日かかって21000点くらいに到達して「私なんだかいける気がしてきた…!」(ここに青葉ちゃんの画像を貼る)となりました。

ISUCON5はMySQLのクエリ改善が重要だったのですが,それ以前のISUCONではキャッシュやパラメータ設定が重要だったりしたこともあったようなので,Redisのチュートリアルをやったり,NginxやMySQLやGunicornの秘伝のタレを作ったりしておきました。

予選本番

問題のアプリケーションの内容

はてなキーワードのようなアプリでした。

d.hatena.ne.jp

キーワードに関する記事を登録,編集,削除(実はベンチマークには削除リクエストは含まれていないらしい?)ができて,登録されているキーワードが記事の中に出現したときには <a href="...">...</a> で囲む必要があります。

同一箇所に複数のキーワードがマッチした場合には(出現位置,キーワードの長さ DESC)という順番で優先されます。例を以下に挙げます。

また,キーワード記事には「はてなスター」のような機能があり,お星様をつけることができます。

序盤

初手でNginx, MySQL, Gunicornの設定を秘伝のタレに書き換えました。

ただ,この時点で動作確認をすると500が帰ってきて大変でした。

まずNginxとGunicornの間をUnix domain socketで接続するように変更したら,エラーが出てしまいました。ログを確認するとなぜか urllib の文字が。今回のアプリケーションはキーワード記事を管理するisudaというアプリケーションと,はてなスターを管理するisutarというアプリケーションに分かれており,相互にHTTPで情報をやり取りするマイクロサービスのような構成になっていました。isudaとisutarの接続ができなくなってしまったことでエラーになっていたようなので,とりあえずUnix domain socketでの接続を無効にすることにしました。

他にもGunicornのworker classをMeinheldに変更したら謎のエラーが起こったり,/etc/mysql/my.cnfに貼ったシンボリックリンク先の設定ファイルが読み込まれなかったりして,まともに動くようになった頃には13:00頃になっていました…

設定が終わったら何かアプリに加える前にプロファイルを取ると心に決めていたので,kataribeで実行時間の多いパスを探すことにしました。合計実行時間は /, /login, /keyword が上位でした。

他には,mysqldumpslowでスロークエリを探してみると,スロークエリらしいクエリは何も見つかりませんでした。ベンチマーク中にtopコマンドを見てもわかるように,今回はSQL勝負ではないようです。

中盤

load_starsとhtmlifyの2つのメソッドが遅いことが分かったので,この2つを中心に改善することにしました。

load_stars の中身はisudaからisutarにHTTPリクエストを投げて「はてなスター」の情報を取ってくる処理でした。HTTPリクエストがボトルネックっぽいのでisudaとisutarのマイクロサービスをぶち壊してモノリシックな構成にすることにしました。isutarの実装がとても軽かったので,isudaにisutarを取り込みました。

続いてhtmlifyの中身は記事中に出現したキーワードにリンクを付ける処理でした。正規表現よりはAho-Corasickのほうマシな気がしたので,ライブラリを探してきて検証をしていました。

終盤

ブラウザ上での動作確認では正しく動いているっぽいのに,ベンチマークにかけるとFAILする現象が発生していてデバッグが大変だったのですが,その原因が分かりました。いつの間にか,ブラウザで閲覧しているIPアドレスが,以前ISUCON5の練習で使っていたインスタンスIPアドレスになっていました。しかもそのIPアドレスでは,別のどこかのISUCONチームのインスタンスが立ち上がっていたので間違いに全く気付きませんでしたw

なんとかAho-Corasick化できたのでベンチマークを実行したのですが,スコアほとんど上がらず…

結果

11,000点くらいでした。キャッシュ戦術重要ですね。

学んだこと / 反省したこと

MySQLのmy.cnfのシンボリックリンク問題

my.cnfの実体をGitのリポジトリ下において/etc/mysql/my.cnfにシンボリックリンクを置くことにしていたのですが,なぜか設定ファイルが読み込まれない現象に遭遇しました。これはAppArmorが関係しているようです。

serverfault.com

実は「ISUCON AppArmor」でググると過去の犠牲者がたくさん見つけられます。 ISUCONなら apt-get purge apparmor も考慮するべきですね。

Meinheld

Web Framework Benchmarksや過去のISUCON参加者でGunicornのworker classにMeinheldを使ってスコアを改善したという事例があったので,練習の時に試してみると実際1000点くらい上がったので今回も使ってみました。

FrameworkBenchmarks/gunicorn_conf.py at 497d7bb95d18f00350686c08bed9e117730e1a97 · TechEmpower/FrameworkBenchmarks · GitHub

orangain.hatenablog.com

理由はよくわからないのですが,URLに日本語が入っている場合に落ちてしまいました。以下のコードで落ちてるっぽいので後で調べます。

werkzeug/routing.py at 9ab649fdc225037162a9d29be08648249c4588ab · pallets/werkzeug · GitHub

werkzeug/_compat.py at 9ab649fdc225037162a9d29be08648249c4588ab · pallets/werkzeug · GitHub

開発用の環境の整備

手元でソースコードを編集して git push したあとにサーバー側で git pull することでデプロイしていたのですが,問題が発生した時には git reset --hard HEAD して git push -f するダメなワークフローだったので,今度はもう少しまともなワークロフローを考えたいです。

ログの取り方

Gunicornでログを出力する方法がわからず with open('/tmp/foo.log) as f: printf(message, file=f)` でデバッグしていたのですが,さすがに効率が悪すぎるのであとでやり方を調べます。

MySQLのオプション

MySQLへの接続で以下のようなコードが書いてありました。

cur.execute("SET SESSION sql_mode='TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY'")
cur.execute('SET NAMES utf8mb4')
sql_mode

なんか指定したほうが良いらしいです。

www.songmu.jp

utf8mb4

MySQL文字コードにはハハパパ問題と寿司ビール問題という2つの罠があるようです。

www.slideshare.net

ハハパパ問題

www.slideshare.net

MySQL 5.5.11 unicode_ci で同一視される文字

寿司ビール問題

blog.kamipo.net

akataworks.hatenadiary.jp

まとめ

多くのことを学べたので事実上の勝利