ISUCON#12予選で3114点で予選敗退(終始ナニモワカラン)

こう見えて本業はインフラエンジニア(最近はエンジニアしていない)ウスター商事でございます。
今回は、「たまにはエンジニアしたいなぁ」という雑な思いからふと出場したISUCONについて記します。

目次 本ページは10分くらいで読めます

ISUCONとは

ISUCON(Iikanjini Speed Up Contest)とはLINE株式会社が運営するWEBページを
イイカンジに高速化することを目的にプログラミングやWEB関連のインフラ構築力を総合的に競うコンテストです。

毎年開催されており、今年で12回目の開催となります。
1~3人のチームで、お題として与えられた激重なWEBページを、限られた計算機資源(CPUやメモリやディスクやネットワークの帯域などなど)を使って、限られた時間(8時間)の間で、いかに早く処理させるかがポイントです。

事前にやったこと

  • 1.メンバ集め
    同じ会社のWEB+NW屋さん(neko-attack先輩)とWEB+Pythonプログラム屋さん(issi030さん)とLinuxインフラ+DB屋さん(ウスター商事)の3人チームでチームを組みました。
    neko-attack先輩は会社の同僚でお家が近く、issi030さんは元同僚で現在は別会社・別都道府県なので3人で集まるときは遠隔で連携を取りました。
  • 2.事前学習
    初めての出場なので体系的に勉強しようと思い、過去のISUCONのノウハウが体系的にまとまっている本を買いました。
    達人が教えるWebパフォーマンスチューニング 〜ISUCONから学ぶ高速化の実践
    また、過去問を解くためにさくらインターネットさんのレンタルサーバを建てたり、仮想スイッチを建てて環境構築をしたりしていました。
  • 3.事前準備
    予選当日に初めての技術や初めてのコマンドは実行できないと考えていたので、
    事前学習で打ったコマンドをコマンド集としてメモにまとめたり、
    何度も打つコマンドはシェルスクリプト(事前に書いておいた命令の塊と思ってください)にまとめたり、
    当日の1時間刻みのおおよそのタイムスケジュールなどを作成しておきました(結局うまくいかなかったが…)

実力的に割り切っていたこと

  • 1.ミドルウェアの載せ替えはしない
     Nginx↔Apacheとかの載せ替えできるだろうけどめんどくさい(neko-attack先輩)
     Python↔GoのAPL載せ替えは、コード内容しだいで可能だが効果が薄ければやらない(issi030さん)
    Mysql↔PostgreSQL↔MariaDB↔MongoDBはできるだろうけど時間が厳しい(ウスター)
  • 2.コミットは自由にベンチ回すときだけ声がけ
     誰かの作業を待って手が止まることは避けたいので、ベンチマーク実行の前後と結果だけ声をかけて、あとは個々人で喋りながら手を動かす。
  • 3.環境を壊さないこと最優先
     トップを狙えるほどの実力でないのはわかりきっている、再起動試験や環境試験、レギュレーション違反防止をしっかりと行う。

当日やったこと

  • 先に結果だけ報告。初回ベンチマーク(2,629点)→最高値(3,251点)→最終(3,114点)

  • 9:20 Discordで集合
    0.5次予選の全員起床AND全員集合に成功。これはもう勝ちました。約束された勝利です。
  • 9:40 ISUCONライブ配信視聴
    お題となるWEBページの説明や環境の説明などなど。今回はeSportsならぬisuportsというお題らしい。
  • 10:00 環境構築スタート
    AWS上に3つのサーバを構築し、環境を壊したときのバックアップのため
    スナップショットを取得、サーバ内のファイルをローカルに転送
    練習の成果もあり15分程度でバックアップ完了して初回ベンチマークを実行完了。
    →初回ベンチマーク(2,629点)
  • 11:00 サーバの全体像を把握してDBがボトルネックと想定
    htopコマンド・alpによるnginxのアクセスログ解析・slow-query-logの解析・mysqltunerの解析から
    "SELECT player_id, MIN(created_at) AS min_created_at FROM visit_history WHERE tenant_id = ? AND competition_id = ? GROUP BY player_id"
    というDBクエリがめちゃくちゃ多くて処理が重たい一因と判断。
    慣れている言語のためAPLはPythonコードをissi030さんと読みながらGoコードの中を確認しつつ、DBに上記のクエリを流している部分の改善を検討。
    GROUP BY player_idのあとにMinで計算しているあたりがややこしくて、改善に悩む。
    合わせてDBの設定を変えようとしたが、root権限がなさそうなので運営に質問。
    マニュアルにroot権限の記載を忘れてました(テヘッ!)って言われたので、質問をしてよかったなと思う(この日一番のお仕事)
  • 12:00 DBのroot権限をゲットしたのでボトルネックを改善へ
    Mysqlの上記のクエリに対してIndexを貼りました。すでにtenant_idにIndexがあったので追加でcompetitionIDにも
    ALTER TABLE visit_history ADD INDEX tenant_id_competition_id_index (tenant_id, competition_id);
    →ベンチマーク(2,434点)→Indexが効いていないか?と思って焦ったが、サーバ分割していないしこんなもんかなと思いました
  • 12:30 WEB/APLとDBを分割
    ・ベンチマークをそれぞれのサーバで回したりするできるため、早々にDBをサーバ2へ分割。
    ・サーバ1は
     /home/isucon/webappのdocker.ymlにDB接続先などの環境設定ファイルがあったので、
     アドレスをDBのサーバ2に変更。
    ・サーバ2側は
     /etc/mysql/mysql.conf.d/mysqld.confあたりの設定で、
     bind-addressを0.0.0.0にしてどのサーバからでもアクセスできるように変更。
    ・またDBの設定としてisuconユーザがアクセスできるように特権権限の範囲を192.168.XXX.XXXに広げる設定をmysqlのrootユーザで追加。
    CREATE USER isucon@192.168.% IDENTIFIED BY 'isucon';
    GRANT ALL PRIVILEGES ON isucondition.* TO isucon@192.168.%;
    →ベンチマーク(2,957点)→まだまだ圧倒的にDBが遅いのでIndexの貼り方かクエリの投げ方かでissi030さんと共同で切り分け。
    →neko-attack先輩はnginxとmysqlのメモリやキャッシュの設定をチューニングしながら繰り返しベンチマーク実施(設定をミスして0点になることもしばしば笑)
  • 14:00 無力感を感じながらDBをいじくり回す
    ・昼ごはんをおにぎりやゼリーで済ませながら、引き続きDBと格闘中。
     Indexの貼り方が問題ではなくもっと根本的な問題だと方向性を疑い始める。
    ・issi030さんがAPL実装とalpの解析結果から「MysqlではなくSQliteのほうが処理の支配項ではないか?」と疑う(本当にナイスプレイ)
    →ベンチマーク(2,942点)→横ばい
  • 14:30 SQliteをMysqlに載せ替えられるか確認する
    ・技術的に不可能ではないがコード変更とDBの整合性を取るのは時間内には困難と判断。
  • 15:00 SQliteのままで手を打てるところに手を出す
    ・初めて触るSQliteに戸惑いつつも、Indexをいくつか貼ってみる
    CREATE INDEX player_score_index ON player_score (tenant_id, competition_id, player_id);
    CREATE INDEX competition_index ON competition (tenant_id); ←今思うとcreated_atもIndexしておけばよかった。
    CREATE INDEX player_idx1 ON player (tenant_id);←今思うとcreated_atもIndexしておけばよかった。
    このとき何度もベンチマークが0点になり、Indexの張り方が悪いと勘違いして戻したりしていたが、同時にnginxの修正を入れまくっていたのでそっちの原因もありそうだと後で判明。
    ・Indexをそのまましっかり効かせていればもう少しスコアが伸びていたかも?
    →ベンチマーク(3,251点)→我々のチームの瞬間最高スコア。
  • 16:00 ココからは環境を壊さないようにチューニングの世界へ
    ・mysqld.cnf にinnodb_buffer_poolを追加してバッファの値を調整したり
    ・nginxとmysqlとaplに書いてあったdebugやログ出力系の設定をすべて切ったり
    ・issi030がコードに書いてある「// player_scoreを読んでいるときに更新が走ると不整合が起こるのでロックを取得する」の解消とLock解除で高速化を狙ってコードとファイルをいじってくれていた。
    →ベンチマーク(3,067点)→変更はあまり効かず
  • 17:00 チームの当初方針通りにコード凍結して再起動試験へ
    ・再起動試験で0点を連発してめっちゃ焦る。
    ・Githubで管理してた各種設定ファイルから怪しそうな設定を戻してサービス再起動→0点を何度か繰り返す。
  • 17:40 一旦落ち着こう、サービスを順番に立ち上げて数分待ってからベンチマークを回す
    ・当日マニュアルを読みながらDBスキーマを初期化、各種サービスをDB→APL→WEBの順で立ち上げ直す。
    ・入れていたログツールのアンインストールと不要プロセスのkillして最軽量で立ち上げ。
    →ベンチマーク(3,203~3,114点)→チームの最高値に近い値でFinish。
  • 17:55 反省会
  • 18:00~ 運営の事後のライブ放送視聴
  • 19:00 解散

反省

  • 圧倒的な勉強不足(特にウスターが担当したDB周り)
    MysqlとPostgreSQLばかり勉強していたが、もっと広くDBとSQLクエリを本質的に学ぶべきであった。自身を持ってAPLからのクエリを変更できること、Indexの効果を正しく測定できること、DBの載せ替えにビビりすぎないことを目標にしたい。
  • WEB・APL・DBを同時に別々にいじりすぎるとわけが解らなくなる
    15:00時台にSQliteの対処に移ったときに連携不足で同時並行で変更を実施しすぎたために、スコア変動の原因がつかめず、むやみに有効な対処を戻してしまった(SQliteのIndexとか)1変更1ベンチマークを徹底したい。
  • 最大のボトルネックは私という未熟者。
    今回の環境ではDB周りの処理が支配項だったと思っています。
    ウスターがDB改善やMysqlへの載せ替えをスムーズに実施していればnginxの改善やAPLの改善が付いてきてスコアが伸びただろうと思うと、ウスター自身がボトルネックであったと反省するばかりです。引き続き精進いたします。