ISUCON6予選にチーム「試運転」で参加しました
私と@menphim, @yurahunaで予選2日目に参加しました。
事前にやったこと
使用する言語の選択では,メンバー全員が研究で多少は使っていることから,Pythonを選択することに決めました。過去のISUCONでの本選出場者数から考えるとPythonはやや分が悪いっぽいのですが,普段Web系のプログラミングをしていない勢のチームだったので使い慣れた言語の使う方が大切だと思ったからです。
練習はISUCON5の予選でやりました。「MySQLってどうやってログインするの?」とか「バックアップってどうやるの?」というところから始めないといけなくて時間がかかりましたが,数日かかって21000点くらいに到達して「私なんだかいける気がしてきた…!」(ここに青葉ちゃんの画像を貼る)となりました。
ISUCON5はMySQLのクエリ改善が重要だったのですが,それ以前のISUCONではキャッシュやパラメータ設定が重要だったりしたこともあったようなので,Redisのチュートリアルをやったり,NginxやMySQLやGunicornの秘伝のタレを作ったりしておきました。
予選本番
問題のアプリケーションの内容
はてなキーワードのようなアプリでした。
キーワードに関する記事を登録,編集,削除(実はベンチマークには削除リクエストは含まれていないらしい?)ができて,登録されているキーワードが記事の中に出現したときには <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化できたのでベンチマークを実行したのですが,スコアほとんど上がらず…
結果
やったこと (Python)
— しょラー (@shora_kujira16) September 18, 2016
1. マイクロサービスをぶち壊してisutarをisudaに取り込んだ
2. htmlifyでAho Corasickを使った
3. url_forを消した
4. テキトーにインデックスをはった#ISUCON
11,000点くらいでした。キャッシュ戦術重要ですね。
学んだこと / 反省したこと
MySQLのmy.cnfのシンボリックリンク問題
my.cnfの実体をGitのリポジトリ下において/etc/mysql/my.cnfにシンボリックリンクを置くことにしていたのですが,なぜか設定ファイルが読み込まれない現象に遭遇しました。これはAppArmorが関係しているようです。
実は「ISUCON AppArmor」でググると過去の犠牲者がたくさん見つけられます。 ISUCONなら apt-get purge apparmor
も考慮するべきですね。
Meinheld
Web Framework Benchmarksや過去のISUCON参加者でGunicornのworker classにMeinheldを使ってスコアを改善したという事例があったので,練習の時に試してみると実際1000点くらい上がったので今回も使ってみました。
理由はよくわからないのですが,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
なんか指定したほうが良いらしいです。
utf8mb4
MySQLの文字コードにはハハパパ問題と寿司ビール問題という2つの罠があるようです。
www.slideshare.net
ハハパパ問題
www.slideshare.net
MySQL 5.5.11 unicode_ci で同一視される文字
寿司ビール問題
まとめ
多くのことを学べたので事実上の勝利