2020-09-17

ISUCON10予選にチームモツ鍋として参加した

2020年9月12日に開催された ISUCON10 予選に、チーム「モツ鍋」として @mintsu123 との2名で参加した。 最終スコアは 1691 で、予選通過はできなかった。

この記事では、予選でやったことについて自分の視点から淡々と記載していきたい。

なお、mintsu氏のブログは以下にあるのでこちらも参考にどうぞ。
https://blog.mintsu-dev.com/posts/2020-09-15-isuccon10-qualify/

事前準備

今回、自分がISUCON前日までハッカソンのイベントに参加しており、そちらに時間を使っていたためISUCONの準備はほぼ何もできなかった...。

一方、チームメンバーのmintsu氏は過去のISUCON参加時に作ったスクリプトの整備や、予選の過去問を解くなどの準備をしてくれていたので、そのあたりの共有会を予選前日の夜に行った。

当日朝

当日、勢い余って8:00くらいには着席していたが、運営側のトラブルがあったようで12:20開始となった。

しかし何も準備していなかった自分としては、過去の戦略や具体的な作業方法についておさらいする時間を確保でき、恵みの雨となった。 この時間で、過去の予選で効果的だった戦略や新しく使えるかもしれない戦略についてGoogle Meetでつないで調査・準備していた。

予選開始

予選開始の時間になり、サーバやリポジトリの初期セットアップ系作業(これを 初動 と呼んでいる)をmintsu氏がやってくれていたので、自分はその間に 予選マニュアル をしっかり読んでおき、あとで要点だけさくっと伝えられるように準備をしていた。

また並行して、アプリケーションのおおまかな構成・仕様理解やDBスキーマの理解を進めていた。

最初の時点のベンチマークでは、スコアは 497 だった(言語はGo)。

Botからのリクエストの遮断

予選のレギュレーションの中に、「Botからのアクセスは503で弾いても問題ない」という主旨の記載があり、これは明らかに早い段階でやってしまったほうがいい案件だと判断し、すぐに対応を行った。

ざっと修正・反映をしてベンチマークを動かしてみたが、この段階ではログからBotアクセスらしきものが確認できなかった。まだ「初期状態だしBotからのリクエストはないのかな〜」と理解して、スコアは落ちていなかったからとmasterに修正を取り込んだ。

server {
    if ($http_user_agent ~ ISUCONbot\(\-Mobile\)?) {
        return 503;
    }

    // ... こんなのをたくさん記載
}

後に、この修正がうまく反映できておらず余計な時間を使うことになるとは、この段階では知らなかった...。 nginxの設定を雰囲気でいじるべきではなかったと反省している 🙇

MySQLサーバを分離

ベンチマークを回している中で、mysqlプロセスのCPU使用率が100%で張り付いているということにmintsu氏が気づき、負荷分散のためにMySQLサーバを別インスタンスで動かすようにした。

これで、1台目ではNginx + app、2台目でMySQLが動く状態になった。

indexの追加

DBのスキーマを見ると、どのカラムにもindexが貼られていないことがわかった。 一方で、indexを活用できると高速化しそうなクエリが色々と使われていることがわかったので、ババっとindexを追加した。

index追加のような、アプリケーションに破壊的な変更を与えないものはざっとやってみて、「漏れがあればあとで追加すればいいし、問題が出れば戻せばいい」というスタンスでやっていた。

-- chairテーブル
CREATE INDEX idx_c_stock ON isuumo.chair(stock);
CREATE INDEX idx_c_price ON isuumo.chair(price);
CREATE INDEX idx_c_popularity ON isuumo.chair(popularity);

-- estateテーブル
CREATE INDEX idx_e_popularity ON isuumo.estate(popularity);
CREATE INDEX idx_e_latitude ON isuumo.estate(latitude);
CREATE INDEX idx_e_longitude ON isuumo.estate(longitude);

反映後にベンチマークを動かしてみたところ、スコアはそこまで上がらなかったが下がってもいなかったのでmasterに反映。なおこの修正も後にきちんと反映されていないことに気が付くという凡ミスをしていた・・・(気づいてよかったね)。

ワイルドにやりすぎて動作確認もワイルドになっていたようだ。

POST /api/estate/nazotte の効率化

なぞって検索 では返却する物件数の最大数が50となっていたが、実装上はSQLで取得した物件を一通り処理したあとに50件以上であれば50で切り落とす...というようになっていて無駄があった。

なので、50件に到達した時点でレスポンスを返すように修正をした。

レプリケーションの設定

MySQLサーバの分離はしたが、まだmysqlプロセスの負荷が高いようだったので、負荷分散のためにMySQLサーバを3台構成で動くようにした。 具体的には1台目をprimaryとし、残り2台をsecondaryとしてレプリケーションを設定した。この辺はmintsu氏のブログに詳しく書かれているのでご参照。

個人的に、この対応がスコアアップに一番有効だったんじゃないかと思っている(根拠はない)。

アプリケーションサーバの複数構成化

同様に、アプリケーションサーバも3台に分散するようにした。 しかし、更新系のクエリが発生するエンドポイントはprimary DBがあるインスタンスに寄せねばならないので、ここだけ気をつけて設定を行った。

// 以下3つのエンドポイントは更新が入る
location ~ ^/api/chair/buy/(.*)$ {
    proxy_pass http://localhost:1323;
}

location = /api/chair {
    proxy_pass http://localhost:1323;
}

location = /api/estate {
    proxy_pass http://localhost:1323;
}

// ほかは更新が無いようだったので、全台へ分散させる
location /api {
    proxy_set_header Host $http_host;
    proxy_pass http://all;
}

バルクインサート

このあたりで、スコアが1000を超え始めるパフォーマンスを出し始めていたのだが、物件と椅子のCSVインポート処理がタイムアウトしてベンチマークに失敗するようになり、スコア0がしばらく続いていた。

2020-09-17-01.png
続くスコア0

CSVインポート処理は1件1件INSERT処理が行われていたので、ここをバルクインサートに修正する以外に残された道はないと判断。

このとき、残り1時間30分くらいだった気がする。これができないままだと0、できたときには予選突破もあるいは?という絶望と希望の狭間にいた。 前回の予選ではたしかスコア0かスコアが初期値から変動しないという屈辱を味わったので、二の舞になりたくない一心でペアプロに取り掛かった。

かなりトライアンドエラーを繰り返しながらの修正となったが、残り時間15分くらいのときに物件と椅子両方のバルクインポートが実装できて、スコアが1500近くになった。

2020-09-17-02.png
人の言葉を失っているコミットメッセージ

2020-09-17-03.png
ついにスコア0から解き放たれた

残り数分

この時点で残り10分くらいしかなく、うかつなことはできない状態になったのでnginxのログを止めたり、初動 のときに設定していたGoのプロファイラを無効化するなど非破壊的な変更をいくつか加え、最終スコアである1691に到達。

他にやりたかったこととして、ベンチマークエラーの原因切り分けのためにいくつかのエンドポイントにSleepを挟んでおり、これを外したかったのだが間に合わなかった。うまく外せていればもう100 ~200くらいはスコア上がったんじゃないかと後悔しているが、予選突破のボーダーラインが2200くらいだったのでどのみち無理だったかな〜と今では思っている。

2020-09-17-04.png
最終スコアさん

全体を通しての感想

今回も残念ながら予選突破とはならなかったが、過去のISUCON予選の失敗を結構活かすことができたと感じており、その点では良かったなぁと思う。

今回自分は準備も特にしていなく凡ミスも多く大変申し訳ない感じとなったが、有能なmintsu氏のおかげで善戦できた。

かなりエキサイティングな1日を過ごすことができた。ISUCON運営の皆様には感謝です。もし次回もあれば、是非参加させていただきたいと思う。