ISUCON8 予選参加してきました。(2018/09/18追記)

久しぶりの更新になってしまい、申し訳ありません。

今回は、 9/15 (土) に ISUCON8 予選に参加してきたので、そのことについてまとめようと思います。 あ、サムネイルは終了1時間前に偶然TOP5に乗ったもので通過はできませんでした。😭

そもそも ISUCON とは

ISUCONをご存知ない方向けに軽く説明すると、

isucon.net

お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトル、それがISUCONです。

公式の引用ですが、Webサービスをとにかく早くしてサービスの高速化をチームごとに競う大会です。

去年に引き続き、僕も Oysters という若手エンジニアの集まりで仲間を募集して参加しました。

当日までにやったこと

メンバーの募集、確定

ISUCONはチーム戦なので、一緒に戦ってくれる信頼できる仲間を募集することが大事です。 僕は、普段一緒にプライベートで開発をしたりしている若手エンジニアの集まり Oysters があったのでそこでメンバーを募集しました。 過去にも何回か ISUCON 練習に参加してくれたメンバーが集まり心強かったです。

使用言語の選定

メンバーが集まったので、どの言語を使って問題をといて行くかを決めました。これについては、去年の参加時に次回はGo言語を使ってみたいということで話がでていたので、特に議論はなくGo言語に決まりました。使用に至った理由としては、言語としての使用が単純なためコードリーディングが用意であること、PHPなどと比較して軽量であること、コンパイル型のためエラーがコンパイル時におおよそわかり時間短縮できるだろうということ、例年の通過者比率がGo言語が多いこと、などがありました。

開発環境構築

前々回くらいから、コンパネからの操作などができなくなり、イメージからの復旧などもできなくなりました。なので、実際の作業は配布されたサーバ上で直接やるのではなく、本番環境のコードからバックアップを取りそれをローカルで動くようにし、ローカルで作業したものを本番にあげるという手順を取りました。

そのために開発環境をあらかじめ Docker + Docker-Compose + Nginx + Golang + MySQL を使って構築しておきました。 参加メンバー全員が開発環境で動かせるように事前に集まって動作確認することも忘れませんでした。 こういう場面でサクッと開発環境の構築ができるのは Docker + Docker-Compose 本当にいいですよね。

プロファイリング方法の調査

次に本番時にどこにWebサービスボトルネックがあるのかを調査するための方法であるプロファイリングについてです。 ISUCONではサービスのチューニングをやって行くわけですが、制限時間が8時間と短くそれなりの規模のWebサービスが渡されるので、チューニングのインパクトがあるものからやっていかないと時間が全然足りません。 そこで使用するのがプロファイリングです。僕は役割分担上プロファイリングをそこまで見ていなかったので詳しいことはかけないですが、自チームでは下記のプロファイリング結果を参考にしてました。

  • kataribe
  • Go の pprof
  • MySQL の slow query log
  • dstat

基本的に kataribe を見てエンドポイントごとのアクセス数とレスポンス時間を見ながら対応優先順位を決めて行く感じです。またpprofを使うとコードのうちのどこに特に時間がかかっているのか分析できるのでおすすめです。slow query log はチューニングする部分がSQLの部分の時に確認しながらインデックスを貼っていったりする用途で使ってました。

当日スケジュール決め

当日の時間は 10時 〜 18時 と短く余裕もそんなにないので事前に役割決めとタイムスケジュールを細かく決めておくと当日スムーズに行動できておすすめです。 僕のチームでは下記の写真のように時間配分と役割を決めました。

タイムスケジュール f:id:zoe302:20180916163255j:plain 役割分担 f:id:zoe302:20180916163407j:plain

スラックチャンネル

当日みんなで集まって作業はするものの、全部口頭でのやり取りは辛いので、スラックチャンネルを用意しました。 これは以前から用意していたので改めて新しく用意したわけではないですが、あったほうが便利なのであえて書いてます。 使用用途としてはわかったことをつらつらと書いていったり、不明なことを書いていったり、ボトルネックになりそうなものを書いたり、などにつかってました。

作業用レポジトリ作成

当日の本番コードのバックアップ兼作業ようのレポジトリです。ここにあらかじめ構築しておいた開発環境のコードと後述するプロビジョニングようスクリプトを入れておくことで当日スムーズにコードのバックアップ→開発、プロビジョニング→開発したものを反映する、という流れができました。

プロビジョニングスクリプト作成

プロビジョニングといってもたいしたことはないのですが、用意しておくに越したことはありません。とりあえず今回僕が用意したものとしては、メンバーがスムーズに作業できるようにメンバーの公開鍵を登録しsshできるようユーザの作成とsudoできるようにするスクリプトを用意しておきました。また必要な時に使えるよう、MySQLのアップグレードの手順とkataribe導入のスクリプトを用意しておきました。他にも事前に用意しておくといいものは用意しておきましょう。

当日やったこと

ここからは当日の流れをslackとgithubのコミットログから遡って思い出しながら書いていきます。 f:id:zoe302:20180916183648j:plain

~ 10:00

事前にメンバーと合流して朝ごはんを食べる約束をしていたので集まって朝ごはんを食べながら戦略の再確認と当日レギュレーションの再確認をしていました。

10:00 ~ 11:30

開始して最初の1時間ですね。正直ここが結構大変でした。 まず最初にとりあえずベンチマークを実行しました。が、この時点でなぜかfailしてました。これについてはすぐ運営からアナウンスがあり、MySQL のコネクションタイムアウトが原因だったようで、MySQLを再起動したら動きました。

karupanerura@運営昨日 午前10時15分 (多くのチームで)ベンチマークがfailしてしまっていますが、MySQLへのコネクションがタイムアウトしているケースが多いので、初期実装のsystemd serviceの再起動をお試しください。

この後は事前に分担していた役割に沿って作業していきます。僕の役割は初動を早くする人でしたが、最初本番のコードのバックアップを念のため開発環境構築の人とペアオペで作業しました。 また平行して、参考実装をGoに切り替えて最初にベンチマークを回しました。この時点でのスコアが1000点前後でした。 この点数をベースにこれから改善して上げていきます。 また、この作業と平行してプロファイリングする人は、最初にアプリケーションの動作をブラウザを通して確認していました。

その後僕は、複数台構成のサーバだったのでそれぞれのサーバの状況を確認していきました。便宜上プライベートIPが若い方からAサーバ、Bサーバ、Cサーバと呼びます。(当日もこの名前で識別してました。) 最初、もらった状態は全てAサーバで返している状態で、WebサーバとしてH2Oが起動しており、SQLサーバとしてMariaDBが起動していました。WebサービスはSPAで映画館の予約システムのようなものでした。 そろそろMySQL以外のSQLがきそうだなとは思っていたので、そんなに驚きはしなかったですが、用意していたMySQLの環境が使えなくなったので少し残念でした。 Bサーバ、Cサーバについては上記ミドルウェアは入っているもののstopしている状態でした。 この時点でDBサーバの処理が詰まってそうだったので早速複数台サーバを活用できるようWebサーバの分離を試みました。 構成としては、AサーバをDB用とし、BサーバをWebサーバ(H2O)として使えるように書き換えました。

11:30 ~ 12:30

Cサーバの方をkataribeを使えるようにH2Oからnginxへ載せ替えをしていました。(H2Oでの使用方法がパッとわからなかったため。) その後少し早いお昼を取りました。モスバーガー美味しかったです。

12:30 ~ 13:30

CサーバのアクセスをnginxがロードバランシングしてBサーバとCサーバのアプリケーションで返せるようにしていましたが、 静的ファイルをうまく返せないようになってしまいハマってしまっていました。 静的ファイルが返せていない原因がテンプレートのほうでoriginをセットしてhttp://{ipアドレス}/ のようにしてアクセスしているのだとわかり、正しくproxy_set_headerをつけることで解決しました。

13:30 ~ 14:30

その後、僕は少し手が空いたので複数台サーバにデプロイするためのスクリプトを用意した。ローカルからリモートのサーバにsshしてソースをpull、goのビルドを実行し、各ミドルウェアをリスタートするというものだ。

#!/bin/bash -eu

# A
ssh 172.16.67.1 <<EOC
sudo systemctl stop mariadb
sudo rm -f /var/log/mariadb/mariadb.log
sudo rm -f /var/lib/mysql/slow.log
sudo systemctl start mariadb
EOC

# B
ssh 172.16.67.2 <<EOC
sudo su - isucon;
cd /home/isucon/oysters-isucon8; git pull;
rm -f /home/isucon/torb/webapp/go/torb;
make -C torb/webapp/go;
sudo systemctl restart torb.go.service;
EOC

# C
ssh 172.16.67.3 <<EOC
sudo su - isucon;
cd /home/isucon/oysters-isucon8; git pull;
rm -f /home/isucon/torb/webapp/go/torb;
make -C torb/webapp/go;
sudo systemctl restart torb.go.service;
sudo rm -f /var/log/nginx/access.log
sudo rm -f /var/log/nginx/error.log
sudo systemctl restart nginx
EOC

その間、ほかの仲間がアプリケーション側のgetEventsのN+1を解消してくれたり、slow query logを見てSQLのチューニングをしてくれたりした。

14:30 ~ 16:00

自分もサーバ側の設定で特にすることが一旦なくなったのでアプリケーションのソースを見て改善できそうなところを直していた。 この間にアプリケーションのgetEventsの改善が終わり少しスコアが上がったがこの時点ではまだ大きくスコアに変動はなく2600点くらいだった。 その後アプリケーション担当の人はgetEventの方でのN+1問題に着手した。

f:id:zoe302:20180916185228p:plain

16:00 ~ 18:00

nginxの静的ファイルのキャッシュに着手、いつもの設定ファイルを持ってきて終わり、今回のアプリでは特に静的ファイルの容量、枚数もなかったのであまりスコアは伸びなかった。 この間にアプリケーションのgetEventのN+1が解消し、ベンチを回した結果、20000点を超え一瞬TOP5に乗りメンバーで歓声を上げましたw

f:id:zoe302:20180916183054p:plain

他のチームも最高スコアは上のチームもいたので、全然TOP5にはなれなさそうでしたが、ちょっと嬉しかったです。 その後も細かいアプリケーションのチューニングをしたのですが、どうやら予約の排他制御がうまくできていなかったようで、たまにfailしてしまう現象に遭遇してしまいました。 そこからは排他制御の再現と解消を考えている間に時間が近づいてきたので、再起動のテストを残り1時間くらいで確認して、終了となりました。

f:id:zoe302:20180916184728p:plain 最終ベンチマーク

施策まとめ

  • サーバ構成を1台構成から3台構成に A: H2O + Go + MariaDB, B: null, C: null -> A: MariaDB, B: Go, C: Nginx(兼Load Balancer) + Go
  • getEvents, getEventのN+1問題の解消
  • 予約テーブルにインデックスはる
  • MariaDBのチューニング

当日振り返り

よかったことも悪かったことも感想も雑多に

  • 勝ちにいくなら役割を固定して練習しておいたほうがよかった。
    • 役割分担はしたものの割と直前で決めたので、あまりなれていない部分もあったため、初動が遅れた。
  • プロファイリング結果を見ながら根拠を持って改修に入れた。
    • これは前回までは割りと行き当たりばったりで改修していたものを今回からちゃんと解析ツールを入れたことで得点に繋がりそうなところを優先的に手を入れられた。
  • あらかじめサーバ構成やミドルウェアの選定をある程度考えていたので、想定外のミドルウェアがきても対して揺さぶられなかった。
  • それぞれが役割を全うして戦えた。変に周りの作業に手を出したり、作業内容がバッティングしないように動けた。
  • 1時間に一回くらいの進捗報告でお互いの作業内容を把握しながら動けた。
  • (個人的に)インフラ周りのボトルネックがあまりなかったので、アプリ側をもう少し見てもよかったかも、、でも全体的にバランスはよかったと思う。
  • 複数サーバ構成を見越してデプロイスクリプトをあらかじめ用意しておけばよかった。当日30分くらいで用意できたからそんなに反省ではないが、、
  • (予想)予約のエンドポイントのレスポンスが202 Accepted になっていることを深夜に気づいてよくよく考えたらその時点で予約完了していなくてもよかったのでは、と思った。
    • 予約受付のレスポンスだけ返して、整合性を確認しながら遅延処理で予約していけばスループット向上できたのでは?
  • ポータルが使いやすい、見やすい。途中詰まって少し重かったけど許容範囲内
  • 最近静的コンテンツが多めの問題設定から少し外れて、またロジックよりだったのが意外
  • 土曜なのに開始時間遅延してなくてすごい。運営のみなさま準備お疲れ様でした
  • ちょうど最近「Web API: The Good Parts」を読んで結構役立った気がする

次回までにやること

  • 役割を決めての練習をする
  • Nginxの設定項目を復習する
  • 自分でもプロファイル結果もっと見れるようにしておく
  • デプロイスクリプト用意しておく
  • 参加に興味持った人が増えてきそうなので余裕あったら2チームで練習、参加したい

最後に

ISUCON運営メンバー様、サーバ用意してくださったGMO様、本当に毎年ありがとうございます。 これからも宜しくお願いします!!

追記1

30位 32,957 50位 24,658 100位 15,354 150位 8,938 200位 3,739 250位 1,391

最終スコアから見るに50位のラインは超えてそう。続報に期待

追記2

予選の結果が出ました。

isucon.net

確認したところうちのチームは追試の結果、failで失格でした。。

fail 23,034 Oysters1 (3)

原因はやはり排他制御周りのようです。

また続報で、1週間、バックアップ目的での予選のインスタンスへのアクセスと、復習のためにベンチマークの実行ができるらしいです。

昨年もバックアップ期間はありましたが、ベンチマーカーまで動かしてくれるのはとてもありがたいですね!

本当にありがとうございます。

追記3

予選問題の公開がきました!これでいつでも復習できますね。僕も復習して来年こそは本戦出場目指します!!

github.com