プログラマのための技術情報共有サービスを爆速にせよ!― リクルート社内ISUCON 2021 winter レポート
多田 雅斗
概要
まえがき
先日、「R-ISUCON 2021 Winter」が開催されました。ISUCON とは、お題となる Web サービスについてレギュレーションの中でパフォーマンス・チューニングを行い、ベンチマークの得点を競う競技です。(※)
リクルート社内ISUCON はその名の通り、リクルートグループ内のエンジニアを対象とした社内 ISUCON に当たります。平時では合宿所を借りて一泊二日での開催でしたが、今回は2月26日のみの半日・オンラインでの開催となりました。
※「ISUCON」は、LINE株式会社の商標または登録商標です。
お題
今回のお題は、「Niita」という架空の技術情報共有サービスになります。こちらはNTT コミュニケーションズ様が過去に開催した「N-ISUCON 2019」のお題をお借りしたものとなります。
モチーフとなったサービスと同様、下記の機能が実装されています。
- ユーザ
- 登録、ユーザ情報変更、ログイン、ログアウト、アイコン登録
- 記事
- CRUD
- コメント
- CRUD
- いいね
- CR
フロントエンドは、SPA(Single Page Application)として実装されており、バックエンドサーバは Python、Ruby、Node.js、Golang 製のものが提供されています。
各チームには 3 台の AWS EC2 インスタンスが割り当てられ、それぞれのインスタンスには上記アプリケーションと、DB として MySQL がインストールされています。
ベンチマークは、各チームが指定した 1 台のインスタンスに対して HTTP リクエストが大量に送られ、Response の Status code によって得点が加算・減算される仕組みです。
本戦
事前準備
さかのぼって去年の話になりますが、3人それぞれが異なるチームで 社内ISUCON 2019 に参戦した反省を元に、今回こそは入念な事前準備を行って、緒戦の滑り出しをスムーズにしたいという思いがありました。 そこで、今回は開催 1 週間ほど前から少しずつ事前準備を行いました。
まず、競技に必要な全てを格納するリポジトリを作成します。
作成場所として、 github.com(以下 GitHub) と 社内の GitHub Enterprise という2つの選択肢がありましたが、社内の GitHub Enterprise に作成した場合、ネットワークの問題で接続できなくなるリスクがあるため、個人アカウントを利用して GitHub に作成する事になりました。実際、前回参加した時には、本戦中に VPN メンテナンスがありしばらく git が使えないタイミングがありました。事前準備の段階からも、いろいろな設定ファイルやツールをこのリポジトリに入れていくことにします。 本戦ではここに、チューニングするサービス本体のコードを入れていきます。
次に、Ansible を使ってサーバー構成をコード管理できる状態を作っておきます。本戦ではいろいろなインフラの設定ファイルを少しずつ修正してチューニングを繰り返すことになるので、インフラ構成のコード管理を行い、再現性を保つことが必要です。 ISUCON に類するコンペでは、リバースプロキシとして nginx が、データベースとして MySQL がよく利用されます。実際には競技開始まで使用するサーバーの構成は明かされませんが、今回もこの構成が利用されると仮定して、Ansible によって構成のコード管理ができる状態を作っておくことにしました。
具体的には、nginx、MySQLをインストール、これらの設定ファイルの雛形を配布し、デーモンを再起動するまでの一連の流れを Ansible によって自動化しました。
加えてGithub Actions を用いたCIを用意しておき、上記の Ansible のコード変更を GitHub にプッシュすればサーバーに対して自動的に変更が反映される状況を作っておきます。
また、チューニングに使うためのツールとその使い方を調べ、あらかじめPCにインストールしておきます。今回、nginxのアクセスログ解析ツールとして alp、MySQLのslow_logの分析のために pt-query-digest などを用意しました。その他チューニングに利用しそうなコマンド群と合わせて、これらの使い方をカンニングペーパーにまとめておき、競技中にすぐ参照できるようにしておきました。
競技当日
さていよいよ競技当日。
まずガイダンスでサービスの概要が説明され、9時30分の競技開始とともに、3台あるサーバーにログインするためのパスワードが配布されました。まず最初の仕事として ssh の設定を行います。秘密鍵をメンバー全員に配り、公開鍵をサーバーに配布します。
並行して、まず何も考えずにベンチマークを走らせますが、当然のように結果は0点。
どこをチューニングすべきか調べるため、しばらくは情報収集を行い、サーバーのスペックや構成、テーブルスキーマなどを順番に調べていきます。
アプリケーションコードがサーバー上に配置されていたので、これをローカルにダウンロードし、git 管理下に入れます。またメンバーの一人が、このコードのビルド・デプロイを Ansible によって自動化する作業を始めました。
ここで想定外なことに、リバースプロキシが apache であることが判明しました。事前準備で用意した Ansible や解析ツールは、「よくある」構成である nginx が使われていることを想定したものだったので、apache では役に立ちません。 現状に合わせて Ansible のコードを apache 用に書き直すという選択肢もありましたが、我々には apache の利用経験があまりないため、まずはサーバーで動いているリバースプロキシを apache から nginx に載せ替えることにしました。
これらの準備にうかうかしてるうちに2時間ほどが経ってしまいましたが、点数はまだ0点のまま。じわじわ焦りが募ってきますが、真っ先に改善すべきポイントも徐々に見えてきます。
- アプリケーションサーバーが 40MB をこえる巨大な js ファイルを返却していることが分かったので、まずこのファイルを minify してサイズを1/10程度に小さくします。
- ユーザーのアイコンが DB に base64 で記録されており、アイコン画像を返却する際にいちいち DB から画像を読み取っていました。そこでアイコンが登録された際にはファイルとして保存するようにし、 DB の負荷を軽減。
- 3 台あるサーバーの 1 台しか利用されていなかったので、1 台をアプリケーションサーバーとリバースプロキシに、もう 1 台を MySQL 専用機にして負荷分散を図ります。
開始 3 時間、これらの対策をしたあたりで点数が 700 点前後に上がってきました。どうやら 3 位につけているようです。しかしトップ集団があまりにも雲の上に見えます。
さらにチューニングを続けます。
リバースプロキシを nginx に変更したことで、事前に用意していたアクセスログ解析ツール alp を使えるようになったので、 ボトルネックの発見がやりやすくなりました。
- まず静的ファイルの返却に時間がかかっているようでした。そこでjsやアイコン画像などの静的ファイルを、go のアプリケーションサーバーではなくnginxが直接返却するように改修。
- また、MySQL の負荷が高いようだったので、DB 周辺の改善を進めていきます。
- slow_log から、記事につけられたコメントの一覧取得がとても重いことがわかったので、コメントテーブルにインデックスを追加することで負荷を軽減。
- 一部のページで処理時間が異様に長く、記事のリストを取得する部分や、記事についているコメントのリストを取得する部分で案の定 N+1 問題を発見しました。これらをそれぞれクエリ 1 回で取得できるようにし負荷を軽減。
これらの対策を入れることで徐々に点数が上がり、8,000 点近くまで上げてくることができました。
当初からは大きな進歩ですが、他のチームの伸びも大きく、チーム TNT は5位前後をウロウロしています。
それぞれの改善で時間を使ってしまい、速くも残り1時間ほどになっています。
この時点で、残り時間でやりたいと考えていた対策は2つありました。
- MySQL への接続にコネクションプールを使う。
- このときの実装では DB にクエリするたびにコネクションを張り直していたので、オーバーヘッドが重いことが予想されました。
- ユーザーの新規登録の際にパスワードのハッシュの生成が非常に重い。
- ハッシュのストレッチングのために外部プロセスを 1,000 回以上呼び出す作りになっていて、CPU を食いつぶしていました。これを純粋な go 実装に書き換えることでかなりの改善が見込めました。
急ピッチで取り掛かりましたが、なかなかすんなりとは実装が終わらず、細かい不具合でベンチマークも何度も Fail し続ける事になってしまいました。
その後もバグが取り切れずスコアも上がらず、結局競技終了が迫ったことでこれらの対策は諦めざるをえなくなってしまいました。
最終的なスコアは競技終了後にサーバー再起動後のベンチマークで決定されますので、確実に動いていた時点に切り戻し、再起動後にすべてが動作するかどうかのチェックを行います。この再起動後のベンチマークで失敗し、あらゆるチューニングも虚しくスコア 0 点、となってしまうのはこの手のコンペではよくあることですので、それだけは回避しようという事になりました。
結果発表
競技は終了し、最終的には 8,885 点、9 位でのゴールとなりました。 最後の方で野心的な改善を諦め、最終ベンチで確実に動くようにテストしたことで、10 位入賞を果たすことができました。
あとで感想戦で明らかになることなのですが、最後に諦めた2個の改善がもしうまくいっていたらスコアは 40 万点を超えていたので、ぜひともここを完遂したかったところでもあり、反省が残りました。
懇親会
Gather というコミュニケーションツール上で行われました。
Gather は最近のオンラインカンファレンスでも利用されるような、オンライン上のアバターを操作すると近くにいるメンバーのみで会話できるようになるというコミュニケーションツールです。
オンライン会場には複数のテーブルが設置されており、主催者やハイスコアチームを中心に「あの短時間で何やった?」「○○さん一人で参加したのに入選するとかすごいですね」というような会話が繰り広げられました。
また飲食については nonpi foodbox という、事前に登録済みの住所へ指定時刻にお酒とおつまみがデリバリーされるサービスを利用しました。
おそらく運営事務局としても、多数の参加者の個人情報やメニューを一括で管理できるのでとても便利でしょうし、メニュー自体とてもクオリティが高く、参加者の評判が高かったのが印象的でした。
オンライン懇親会とは思えないような豪華なメニューで、本音を言うと本戦以上に興奮してしまいました。
感想戦
本戦は金曜開催でしたが、ベンチマークサーバなどを停止するのは月曜日ということで、土日は有志で感想戦となりました。
制限時間が無いので冷静に問題箇所を分析することができ、ISUCON 特有の「焦って変更したけどよくわからずスコアが上がった」という状態から「原因を特定して改善できた」という状態に昇華することができました。知見を溜めるという意味でも、感想戦はISUCON の醍醐味かなと思っています。
多くのメンバーが参加したというわけではありませんでしたが、Slack 上で非同期に最高記録を更新していくペースを見ると、「なぜこれが昨日できなかったのか」と思わずにはいられませんでした。
感想戦を受けて、実際に行ったのは以下のような変更です。
- コネクションプール修正、sha256 のシステムコール呼び出しを go で行う変更 -> 40万点に改善
- select count を go 内でキャッシュ -> 53万点に改善
- nginx 設定変更 & SQL join の改善 -> 59万点に改善
- 画像取得部分を nginx proxy cache をかませる -> 68万点に改善
- mysql cnf の innodb_* チューン -> 79万点に改善
- アプリケーションログの無効化 -> 82万点に改善
- 有効にしていた mysql slow log 無効化 -> 94万点に改善
- nginx keep alive 有効化、nginx log 無効化、今まで入れてたプロファイラ無効化、contentType 判定を文字列先頭一致 -> 117万点に改善(どれが効いたかは不明)
結局本戦では 1万点もいかなかったのですが、感想戦では 100倍以上改善することができました。
感想
本戦が半日しかなかったのもあり、思いついたアイデアをすべて時間内に実装することは叶わず、順位としては大変悔しいものとなりました。ただ、その後の感想戦をサボらず自主的に実施したことで、思いついたアイデアを検証し、自分たちの知見とすることができました。特に、go における MySQL のコネクションプールの設定や、defer の仕様を把握できたことは、実業務にもすぐに活用できる知見となりました。
また、初のオンラインかつ半日での開催ということで、参加者としても多少の不安があったのですが、Teams や Gather、nonpi foodbox などのサービスをフルに活用した運営事務局の努力もあり、従来の 社内ISUCON に勝るとも劣らない活気あふれる会になったと思います。
我々は普段、データエンジニアや機械学習エンジニアとしてデータを用いた施策のための基盤開発や機械学習モデル開発を行っていますが、今回得た知見はより良いプロダクトを開発のため存分に活かしていきたいと思っています。そして次回も必ず参加し、優勝目指して頑張ります!