電気ひつじ牧場

技術メモ

isucon11予選に参戦してきました

「何もしてないのに壊れました」というふざけたチーム名で会社の同期(kypくん、shikiさん)とisucon11の予選に参戦しました。 使用言語はGo、結果は42525点、93位/約600チームで予選敗退でした。

前日までにやったこと

チームで1回だけ役割分担・作業方針を確認しました。私のチームでは次のような方針を立てていました。

  • おそらく3台のマシンが与えられるので、最初は各自が1台ずつ自由にデプロイ・ベンチをとって良いスコアを目指す
  • いい感じのスコアができたらPR出してマージ
  • 1台構成で限界が見えてきたら複数台にして、appとDBを分ける

全員が独立してデプロイ・ベンチができるというのがポイントで、自由に色々といじったりデバッグしたりする時間が取れて良かったです。デプロイはLinux向けにクロスコンパイルしたGoのバイナリをscpでデプロイするスクリプトを書いてました。

チームでの方針確認以外には各自で対策してねといった感じで、個人的にはMySQLトランザクションモデルを叩き込んだり、インデックスの張り方を復習したり、過去問対策をしていました。

当日

-20分

全員が最も心配された起床試験を無事合格し、公式のライブ動画を見るところから開始でした。今年はISUCONDITIONというWebアプリがテーマで、全国の椅子の状態が大量に送信されてくるので、それを確認できるダッシュボード付きのアプリケーションを高速化しようといったものでした。

0分

開始と同時にマニュアルの重厚さにやや圧倒されつつチームでざっと読み合わせをしました。ここを疎かにすると失格になったりするので気をつけました。特に、AWSのセキュリティグループを変更してはいけないというルールは重要でしたね。

+30分

最初のベンチで2000点くらいでした。ここまでやったことは以下

  • ツールのインストール・計測・その他ufwやAppArmorを無効にするスクリプトを入れる
  • MariaDBのスロークエリやその他最低限の設定
bind-address            = 0.0.0.0

key_buffer_size = 1G
disable_log_bin

slow_query_log         = 1
slow_query_log_file    = /var/log/mysql/mysql-slow.log
long_query_time = 0
  • Nginxのログ設定

ベンチマークをとった後にkataribe + access.logでAPIごとのレスポンスタイムを、pt-query-digest + mysql-slow.logで全クエリの計測結果を集計して、チームのDiscordに投げていました。

Discordに投げるのは去年isucon10で同じチームだったayumin君が作ったlog2discordを改造して使っていました。

  • SQLログ出力

go-sql-proxyを使ってアプリケーションログにSQLを出力させるような設定をしました。今回はそれほど見ることはありませんでしたが、APIごとに発行されているSQLが分かって便利です。

+2時間

とったベンチマークをヒントにしつつ、全員で怪しそうな関数やその改善案を出し合ったり、マニュアルを読み込んだりしていました。

まず、dstatの結果から明らかに書き込み(POST /api/condition/*)が多いことがわかりました。全国の椅子からコンディションが送られてくる(という設定)なので当然ですね。さらにソースコードを見るとこんな面白いTODOが書いてあり、負荷を下げるため9割のリクエストを落としていました。

// TODO: 一定割合リクエストを落としてしのぐようにしたが、本来は全量さばけるようにすべき
dropProbability := 0.9
if rand.Float64() <= dropProbability {
    // c.Logger().Warnf("drop post isu condition request")
    return c.NoContent(http.StatusAccepted)
}

ここの書き込みをMariaDBではなくRedisに逃して、MariaDBには遅延書き込みするといった案が出ました。

また、スロークエリに以下のようなクエリがあったので、実行計画を見つつisu_condition(jia_isu_uuid, timestamp)にインデックスを貼りました。これだけで一気にスコアが18000くらいになりました。

SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = '<uuid>' ORDER BY `timestamp` DESC LIMIT 1

+4時間

kypくんが/api/isuのN+1の修正、shikiさんがメモリに載せれそうなところをオンメモキャッシュに、私がPOST /api/condition/*の対応を担当してもくもくしていました。

書き込みのRedis移行に関しては知識が貧弱だったため断念し、とりあえずbulk insertすることで対応しました。kypくん、shikiさんともに実装をマージしてもらいこの時点でスコアは20000~30000くらいでした

+6時間

POST /api/condition/*は非同期で書き込めるということだったので、リクエストごとではなく、1秒ごとにbulk insertするように変更しました。またこの頃になるとappのCPU使用率が支配的になっていたので、appサーバ2台 + DBサーバ1台の構成に変更しました。

この間にshikiさんがgenerateIsuGraphResponseの取得件数を全件→1日分だけに絞るようなクエリを書いてくれたり、kyp君が外部APIで使っているhttpクライアントを差し替えたりしてくれました。これらの変更により大体40000点くらいまで上がりました。

一瞬だけYouTube Liveで実況されていたランキングにチームが乗り、チームが沸きましたw

f:id:cha-shu00:20210824221636p:plain
23位!!

+7時間

この辺りでスコアボードが凍結されましたが、ポータルの障害でベンチが取れない時間が続きました。競技時間が若干延長されるということでやり残していた修正を行いました。

  • GET /api/trendのN+1修正
  • POST /api/isuがINSERT→UPDATEとなっているのをINSERT1つにまとめ、トランザクションを消す
    • ただしこれは時間の関係でマージできなかった

+8.5時間

競技終了が近くなってきたので再起動試験を行おうとしたのですが、なぜがAWSコンソールが重くてなかなかインスタンスが終了、起動しませんでした。さらに1度目の再起動ではDBサーバへの接続でミスがありアプリケーションの起動がうまくいってませんでした。やばいやばいと言いながら修正したものの、もう一度再起動試験をする時間はありませんでした(この時点で残り2分くらい)。結局、「統合テストはパス!ぶっつけ本番で行くわよ!!」というエヴァでありがちな方針をとったところでタイムアップとなりました。

競技を終えて

運営による再起動試験は運よくパスしていましたが、普通に危ないのでちゃんと時間をとってやりましょうというのが感想というか教訓です(それはそう)。

予選落ちではあったものの、何もできずに惨敗した去年よりは成長を感じられて良かったです。来年はソロで出て自分一人の力試しするのもアリかもなーと思いました(ただ、チームで出るのもワイワイできて楽しかったので悩ましい)。

運営の皆さん、楽しい大会をありがとうございました。

追記

isuconのMVができたそうです(?) かっこいいですね。

www.youtube.com