Typetalk 開発チームの堀江(is2ei)です。Typetalk 開発チームの比較的若い方のメンバーでISUCON8予選に出場してきました。ブログを書くまでがISUCONとのことなので、他の参加者にならって僕もWriteupを書いてみます。ちなみに、3人ともISUCON初出場でした。
- チーム名:モテスパイラル
- メンバー:is2ei, futaha, m-nagae
- 順位:運営チームによる再起動試験でFailしたので順位なし。(最終スコアだけでいえば、おおよそ528チーム中75位くらい)
- 最終スコア:20,524
- 結果:予選敗退(予選突破ボーダーラインは 36,471)
事前に準備したこと
ansibleのplaybookを書いた
公開鍵やツールのインストールを行い、処理が終わったらTypetalkに通知させてました。僕は普段アプリ側の開発を担当しているので、playbookを書く機会はほとんどありませんが、 やってみると使い方がわかりやすく、特にハマることもなくサーバをさくさく設定できました。
fabfile書いた
アプリの再起動やデプロイを行い、処理が終わったらTypetalkに通知させてました。インフラのオペレーション(再起動とか)もansibleに集約させるか迷ったのですが、競技開始後に立てつけで小さいタスクを作る方がやりやすいと思い、 fabric2を使いました。
PrivateBinを立てた
ログやコマンド実行結果の共有を行いました。練習中、ログの共有をTypetalkに貼っていたのですが、長めのログが普通の会話と混ざると見づらいと思ったのでプライベートのサーバにPrivateBinを立てました。
使用したVPSはConoHaです。用途はたった3人で使うPrivateBinなので特にサービス内容にこだわらなかったのですが、運営チームの提供しているサービスをなるべく使いたいという思いがありました。
また、複数台構成の練習にインスタンスの同時起動数増加をConoHaのサポートへお願いしたことがあり、その際も迅速に対応していただけました。ありがとうございます!
BacklogでWikiを書いた
過去問を解いた時の結果や事務的な共有事項(日程など)を書きました。
BacklogのGitサーバー機能でリポジトリを準備した
GitリポジトリにはBacklogを使いました。
HackMDで作業手順書書いた
競技開始後は色々とやることがあるので定型的な作業はHackMDにあらかじめメモを残しときました。
設定ファイルを用意した
設定ファイルや便利スクリプトをansibleで初期セットアップを流す時に一緒に配置するようにしました。vimrcとgitconfigはm-nagae謹製のものをもらいました。いわゆる秘伝のタレですね。
競技中に使ったツール
- Emacs
- ansible
- fabric
- Visual Studio Code
- IntelliJ
- Atom
- alp
- mysqldumpslow
- pprof
- Backlog
- Typetalk
- その他、make など
競技中の流れ
僕らのチームが競技中にしたことを時系列で書いていきます。記憶とリポジトリのコミット、Typetalkのメッセージ履歴を元に書き起こすため、やや大雑把な記述となります。出題アプリケーションについては後日運営チームより講評が公開されると思いますので詳細は省きます。
8:00 – 10:00 競技開始前準備
- レギュレーションをよく読む
- レギュレーションで気になったところを話しました。昨年のレギュレーションとは違い”メンテナンスコマンド”なるものが運営から実行されるという記述があり、何だろうね〜と言っていました。(※メンテナンスコマンドがなんだったのかは最後までわかりませんでした)
- サブディスプレイを用意
- コマンドの結果などをみんなで見れるように、ラップトップをディスプレイにつなぎました(つないだのはとりあえず堀江のラップトップ一台のみ)。ガチ勢はメンバー全員がタブレットやUSBディスプレイを持参とのことでしたが、そこまでやる気力はありませんでした。
- ホワイトボードを用意する
- サーバの構成を話したりするのに便利だと思い、ホワイトボードを用意しました。
10:00 – 10:30 競技開始〜初期セットアップ
- マニュアルを読む(futaha, m-nagae)
- ISUCONではレギュレーションとは別に、マニュアルが競技開始後に提供されます。僕がansibleやfab2を実行している間、ベンチマークの実行方法などの調査をお願いしました。
- 参考実装をGolangに切り替え(futaha, m-nagae)
- ISUCONではGolangを使うと決めていたので、マニュアルに記載してある手順を元に参考実装を切り替えてもらいました。
- 初回ベンチマーク(futaha, m-nagae)
- 参考実装をGolangに切り替えたあとに、ベンチマークを実行しました。Golang実装での初期スコアは大体1,400程度でした。
- サーバのログイン設定を行う(堀江)
- サーバに公開鍵を登録したりしました。 前準備でansibleのplaybookを作成しておいたので、ホストを設定して流すだけでした。
- サーバの構成情報を取得(堀江)
- fabfileで構成情報(CPUのコア数)などを取得するコマンドを作っておいたので、それを流しました。
- アプリのソースコードと /etc 以下をリポジトリにPush(堀江)
- 出題されたアプリケーションのソースコードと /etc 以下の設定ファイルをリポジトリにPushしました。時間に余裕がある内はプルリクベースで着実に修正を入れていこうと話したりしました。
10:30 – 11:30 第一次停滞期
- リポジトリにPushできない問題を調査(堀江、m-nagae)
- ソースコードを修正し、リポジトリにPushしようとするとエラーが発生してPushできない現象が起きており、僕とm-nagaeで小一時間くらい悩んでました。
リポジトリを作り直したり、色々試した後にリポジトリ容量の上限に達していることに気づき、練習用のリポジトリを消したりして対応しました。
- ソースコードを修正し、リポジトリにPushしようとするとエラーが発生してPushできない現象が起きており、僕とm-nagaeで小一時間くらい悩んでました。
- H2OをNginxに載せ替えチャレンジ(futaha)
- 出題でH2Oが出てきたので、かなりびっくりしました。実は、H2O自体はデフォルト設定でもかなりの性能が出ると聞いていたのでNginxからH2Oに載せ替える練習もしようか検討をしていました。
しかし過去の参加者が同様の試みをして失敗していたと知り、無難にNginxで対策を行っていたのでまさかという感じです。 事前練習をしていないH2Oでのチューニングはリスクが高いと判断し、Nginxへの載せ替える方針を立てました。
- 出題でH2Oが出てきたので、かなりびっくりしました。実は、H2O自体はデフォルト設定でもかなりの性能が出ると聞いていたのでNginxからH2Oに載せ替える練習もしようか検討をしていました。
11:30 – 12:30 ログ設定など
- H2OのアクセスログフォーマットをLTSVに変更(futaha)
- Nginxに載せ替えを行ったところ、CSSなどの静的ファイルが404になるなどのエラーが発生したため、H2Oに切り戻し、alpで処理ができるようにアクセスログをLTSV形式に変更してもらいました。
- COUNT(*)をやっているクエリのカラムを絞るように修正(m-nagae)
- SELECT COUNT(*)~ をやっているクエリをカラムを指定するように修正してくれました。これは着実にパフォーマンス改善につながる修正ではありますが、この時点ではボトルネック(GetEvent関数のループクエリ)を叩けていないため、スコアは初期と変わらず1400台。
- MariaDBのスロークエリをログに出力するように変更(堀江)
- MySQLで練習をしていたので、パスを少し変更するだけでサッと入れることができました。ログをmysqldumpslowで集計したところ、reservationテーブルのSELECT関連が重そうだな、くらいのことを把握しました。
- pprofを入れる(堀江)
- アプリのプロファイリングをするのにpprofの設定を行いました。pprofでプロファイリングを行ったところ、GetEvent関数で実行されているクエリがボトルネックになっていることがわかりました。
第一次停滞期を乗り越えた後で多少焦りはありましたが、ボトルネックもぼんやり見えてきたこともあり、この時点では「まだあわてるような時間じゃない」というムードでした。
- アプリのプロファイリングをするのにpprofの設定を行いました。pprofでプロファイリングを行ったところ、GetEvent関数で実行されているクエリがボトルネックになっていることがわかりました。
- Makefileでデプロイできるように修正(堀江)
- Makefileを修正して、最新の差分を取得〜ビルド〜再起動を行うようにしました。makeコマンドの実行はfabfileで定義しておくことで、fab2コマンド一発でmakeの実行〜Typetalkへ通知を行うようにしました。
- 予約処理を名前付きの関数に切り出す(堀江)
- 予約処理がエンドポイントの定義のところに無名関数で書かれており、pprofで分析するのが面倒になるためreserveという名前の関数として切り出しました。
12:30 – 15:00 諸々実装開始
- 複数台構成チャレンジ(futaha)
- futahaが調べてくれたところ、H2Oにはロードバランスを行う機能がまだ実装されていないということがわかりました。
そのため、別サーバでNginx構成を試しつつ、最後にベンチマーク先を切り替えて、スケールアウトさせようという方針を話し合いました。
※なお、競技終了後の感想部屋で聞いたのですが、host.confでmultiをonにするなどしてロードバランスを実現させるハックがあるとのことです。ただそのやり方だと/initializeとかだけ特定のホストに投げるなどの細かい設定が難しそうなので、仮にその方法を知っていたとしてもNginxに切り替えていたと思います。
- futahaが調べてくれたところ、H2Oにはロードバランスを行う機能がまだ実装されていないということがわかりました。
- MariaDBを別ホストからアクセスできるように修正(堀江)
- 複数台構成チャレンジ中のfutahaから依頼を受けて、MariaDBに別ホストからアクセスできるように設定しました。
- ループクエリを解消チャレンジ(堀江)
- ISUCON7予選の問題で練習した経験から、ループクエリはボトルネックになりがちと思っていました。ちょうどGetEvent関数がループクエリを使っていたので解消しようとしましたが、なかなかベンチマークが通らず……。
- 排他制御を入れる(堀江)
- アプリに排他制御を入れるようにしました。ただ、競合が問題になるほどアプリを高速化できていないため、スコアは初期1400台と変わらず。
- インデックスを作成(堀江)
- reservationテーブルとusersテーブルにインデックスを追加しました。スコアは1400台と変化なし。
- EXPLAINでみると効いているので効果はあるとは思いますが、この時点でのボトルネックとなっているのはあくまでGetEvent関数のループクエリだったのでスコアには影響がなかったのでしょう。
15:00 – 17:00 第二次停滞期
- masterに直接コミットする方針に切り替え(m-nagae、堀江)
- 時間が足りなくなって来たため、直接masterブランチにコミット〜デプロイする方針に切り替えました。この時間帯になると「さすがに仙道でもあわてるだろ」というくらいに焦りが出て来ます。
- Nginxで静的ファイルが404になる問題を解決(futaha、堀江)
- rootの指定とlocationの指定に重複があったので、重複を解消したら直りました。
- GetEvent関数にEvent構造体の配列を直接渡すように変更(堀江)
- GetEvent関数のループクエリの修正がなかなかベンチマークを通らないため、気分転換にEvent構造体の配列をあらかじめ取得している部分に関してはそれを直接関数に渡すように変更しました。
- Event構造体の配列を持っていない呼び出し箇所では既存のGetEvent関数をリネームしただけのGetEventById関数を使用しました。ただ、ベンチマークを叩けてはいないためスコアは初期の1400台とほぼ変わらず。
- GROUPとMINを使っているクエリをORDER BYを使った書き方に変更(堀江)
- ボトルネックとなっているクエリでGROUP~MINが入ってたので、ORDER BYを使うように変えてみました。スコアは変わらず1400台。EXPLAINでみても改善できてなさそうだったので、あまり意味はなかったでしょう。
17:00 – 17:45 ブレークスルー
- GetEvent関数のループクエリを解消(堀江)
- ループクエリを解消する修正が成功したので、スコアが3,600台に上がりました。
- GetEventById関数を削除して、GetEvent関数に統合(堀江)
- ループクエリが解消できたので、同じような修正をGetEventById関数に入れ込もうとしたのですが、うまくベンチマークが通らなかったので、GetEventById関数を削除して、GetEvent関数に処理を統合しました。
この変更でスコアは6,000台くらいに上がったと思いますが、この時間帯はかなりテンパってて正確なスコアは覚えていません……。
- ループクエリが解消できたので、同じような修正をGetEventById関数に入れ込もうとしたのですが、うまくベンチマークが通らなかったので、GetEventById関数を削除して、GetEvent関数に処理を統合しました。
- GetEvent関数内のログ出力を消す(堀江)
- pprofで再度見た所、GetEvent関数内で複数使用しているLog.Printlnが結構時間かかっていたので、消しました。この時のスコアは8,000台ぐらいだったと思います。
- SheetsテーブルのSELECTをキャッシュさせる(堀江)
- pprofで確認すると、SheetsテーブルのSELECTがボトルネックになっていました。GetEvents関数からループ内でGetEvent関数を呼び出していましたが、GetEvent関数内ではSheetsテーブルは変更されないので、GetEvents関数の最初の方であらかじめSELECTしておいて、GetEvent関数に渡すようにしました。この修正でスコアは16,000台まで上昇しました。
17:45 – 18:00 スコア提出前最終確認〜そして敗北へ
- 再起動テスト(futaha)
- スコア提出後の追試に備えてサーバの再起動テストを行いました。何回かベンチを回して最終スコアは20,000台でフィニッシュ。
この時間帯では特に改善を入れてないので、4000ほどスコアが上昇したのは何回かベンチを回したことにより、諸々がメモリにのったせいだと思います。ちなみに、最終的なサーバ構成は初期構成と同じH2O+MariaDB+アプリの一台構成でした。
- スコア提出後の追試に備えてサーバの再起動テストを行いました。何回かベンチを回して最終スコアは20,000台でフィニッシュ。
- sheetsテーブルがSELECTのみということに気づく(堀江)
- sheetsテーブルはアプリ全体を通してデータが変更、追加されなかったので、初回アクセス時にメモリへ載せることも可能でした。しかし、時間がもうないので修正は見送りました。オンメモリ戦略はISUCON7本戦の過去問で練習していたのに、この時間まで気づけなかったことが非常に悔しいです……。
敗因について
予選突破できなかったのはとても悔しいです。敗因は主に実装スピードが足りなかったことと、サーバのリソースを使いきれなかったところにあると思います。初期セットアップ〜ボトルネックの洗い出しはとてもスムーズに行きましたが、中盤以降、ガリガリ実装していくフェーズが完全に練習不足でした。
所感
ISUCONで必要な知識(Golangやインフラ)は僕が普段業務であまり触らないところだったので、ISUCON出場は良い勉強の機会になりました。
とても貴重な機会を与えてくれた運営チームには感謝の気持ちでいっぱいです。また、半ば強引に誘ったにも関わらず一緒に出てくれた二人(futaha, m-nagae)にも大変感謝しております;-) 次は競プロやCTFの大会に出てみたいなぁ、と思った僕でした。