はじめに
こんにちは、エンジニアの若月と柴田、江藤です。 今回は2022年7月8日に開催された社内イベント RECRUIT ISUCON 2022 に「フルーツくん」というチームで出場したのでその参加記を書こうと思います。
RECRUIT ISUCONはISUCON※というコンテストをリクルートの社内イベントとして開催したものです。 参加者はお題として与えられたWebサービスの高速化に取り組み、競技時間内にどれだけ高速化ができたかを競います。 順位はWebサービスが一定時間内に処理できたリクエスト数に応じて算出されるスコアによって決まります。 今回はR-Calendarというスケジュール管理サービスの改善に8時間かけて取り組みました。 私たちのチームが高速化のために行った作業をインフラとアプリに分けて紹介します。
※ ISUCON は、LINE株式会社の商標または登録商標です
インフラ編(若月)
自分がISUCONに参加する場合、毎回同じような型に従って進めていくようにしています。 作業手順をREADMEに書いておき、測定用のツールなどをまとめて入れるシェルスクリプトや、デプロイ用のシェルスクリプトなどを用意しています。
競技の中ではアプリのコード、テーブルのスキーマ、ミドルウェアの設定ファイルといった変更対象はgitで管理するようにしていて、サーバー内でブランチ名を指定するとその内容をデプロイするようなスクリプトを用意しています。 チームではgoを使っているので、アプリをビルドして再起動し、ミドルウェアの設定ファイルを更新して再起動します。 手元からはサーバーにsshしてそのスクリプトを実行するスクリプトを用意しているので、コードを変更したら、GitHubにpushして手元でコマンドを実行するだけでデプロイできるようにしています。デプロイが成功したらチームのSlackチャンネルに通知が行き、今はどのブランチの状態なのかがわかるようになっています。
序盤
開始直後に参考実装でベンチマークを走らせてどれくらいのスコアが出るかをチェックしました。
その後、参考実装をgitで管理するようにしてアプリ担当者がローカルで作業できるようにしました。
3台のサーバーにあるnginx, mysqlなどのミドルウェアの設定ファイルもサーバーごとにgitで管理するようにしました。
以前のコンテストで、サーバーによってスペックが異なっていたり無駄なサービスが動いていたりしていたこともあったので確認しました。 今回の場合は1台目のサーバーだけメモリが2倍程度多かったようですが、メモリを使い切るようなこともなかったので特に使い分けは気にしませんでした。
中盤
ボトルネックの部分を順番に改善していきたいので測定を行いました。
チームではgoを使っているので、標準ライブラリのpprofを使ってCPU負荷のプロファイリングをしました。 今回の場合はセッションID生成部分のCPU負荷が高いことがわかりました。
kataribe はnginxのアクセスログを特定の形式で出しておくと、それを集計してプロファイリング結果を出してくれます。この集計結果がベンチマークのスコア付けと直接関係があるはずなので重要視します。今回はベンチマーカーはアプリのポートをそのまま叩いていたため、測定のためにnginxをそのポートに立てるようにしました。
pt-query-digest はスロークエリのプロファイリング結果を出してくれるものですが、今回は時間的余裕があまりなかったため、有効活用はできませんでした。
サーバーをなるべく余らせたくないので1台目のサーバーをアプリ、2台目のサーバーをDBとしました。
途中でnginxがtoo many open files
というログを吐き出したので、ファイルディスクリプタの上限を上げて解消しました。
終盤
測定のために使っていたログやサービスを無効化し、サーバーの再起動後も起動しないようにしました。
競技終了後には再起動試験があり、サーバーを再起動したときに正常に動作しない場合は失格となってしまうため、余裕を持って再起動のチェックをしました。
アプリ編(柴田、江藤)
参考実装としてJava、Node.js、Goの3種類が用意されていましたが、今回は経験値の点からGoを選択しました。 実装の変更は以下のような順序で行いました。
- ブランチを切って修正(ローカルでの実行環境も用意されていたためこれを活用)
- サーバーにデプロイして軽く動作確認
- ベンチ実行して効果確認 アプリ本体に対して行った改善を以下に列挙します。
セッションID生成部分の高速化
ユーザログイン時のセッションID生成処理の中に明らかに無駄な処理(10000通りのランダム文字列を生成した後1つを選ぶ)があったため、無駄な部分を除去しました。
また、乱数生成のライブラリを crypto/rand
から math/rand
に変更してみました(ISUCONだから許されるやつ)。
会議室名のマスタをメモリに載せる
会議室名が有効な文字列であるかを判定するメソッド内で、会議室名を列挙したテキストファイルを毎回読み込んでいたため、これをメモリに載せるようにしました。
ユーザ情報をメモリに載せる
DBで管理しているユーザの情報をメモリで扱うようにしました。
ユーザを取得するクエリはユーザIDを条件とするものしかなかったため、keyをユーザID・valueをユーザ構造体とするmapを作成し、DBの代わりにこのmapを操作するよう変更しました。
ただし、mapに同時に書き込みを行うとpanicが発生するため、 sync.RWMutex
で排他制御を行いました。
index追加
スロークエリを見ると、会議室予約の際に既存の予約と重複していないかをチェックするクエリに時間がかかっていることが分かりました。
クエリの条件としては room_id = ? AND NOT(start_at >= ? OR end_at <= ?)
という形だったため、 (room_id, start_at)
にindexを追加することで高速化を図りました。
しかしながら、実行計画を確認する余裕がなかったため、本当に効果があったのかは謎のままでした。
サービスを1つに統一
R-Calendarはorecoco-reserveという会議室予約を管理するマイクロサービスと連携する構成になっていました。 通信にかかるオーバーヘッドにより速度が落ちている様子だったため、orecoco-reserveの実装をR-Calendar内に移植しサービスを1つにまとめました。
振り返ってみると、この変更によりスコアを上げることができましたが、変更点が多くて実装に時間がかかってしまったのは問題点でした。 特にorecoco-reserveでバリデーションエラーとなるケースの処理を移植するのが困難で、ベンチマークを実行してfailedとなることが多々ありました。 競技終了後の解説では別の方法として、複数の会議室予約情報を一度に送受信して通信回数を減らすという方針が紹介されていました。 実装にかかる時間も考えて、いかに短時間で効果が出る方針を考えるのもISUCONの難しさであり面白さだと思います。
アプリ編まとめ
- 場当たり的に改善内容を決めて実装していった
- 各改善にどの程度効果があったのか今ひとつ分からず
- サービス統一は想定以上に複雑で時間が溶けた
- より単純な改善で同等の効果を出せるような方法を考えるべきだった
- 計測 -> 改善部分・内容の決定 -> 実装 のサイクルをより正確に行えるようにしたい
まとめ
8時間の競技を通して、フルーツくんチームのスコアは下の画像のように推移していきました。 マイクロサービスの統一やファイルディスクリプタが枯渇する問題の解決に時間がかかったため競技終盤までスコアが伸び悩みましたが、最大で1200点近いスコアを出すことに成功しています。 入賞はできませんでしたが、チームで議論しながらの開発はとても勉強になりますし面白かったです。 競技終了後も解説や他チームとの交流を通じて気づけなかった改善点や高速化方法を知ることができました。 次回は今回の反省を踏まえてより高得点を狙っていきたいです。

終わりに
ここまで読んでいただいてありがとうございます。 弊社では、様々な職種のエンジニアを募集しています。興味のある方は、以下の採用ページをご覧ください。