ISUCON14に参加しました

こんにちは、株式会社HRBrainでバックエンドエンジニアをしている[ts__0625](https://x.com/ts__0625)です。

4年ぶりに[ISUCON](https://isucon.net/archives/58818382.html)へ参加してきました。本記事はそれをネタにした、HRBrain Advent Calendar 2024 の20日目の記事です。ISUCON14の結果、当日にやったこと、事前にやったこと、振り返り、などを書きます！


https://qiita.com/advent-calendar/2024/hrbrain


ISUCONとは、8時間の競技時間中でWebアプリケーションの性能向上を競う大会です。今年は12/8に開催されました。チームで参加することもできますが、私は1人でISUCONへ参加しました。4年前は複数人でISUCONへ参加しましたが、あまりまじめに参加しておらず、当日の盛り上げ役、兼、打ち上げの財布のような存在でした。参加してみるとISUCONは案外面白く、今回は自分主導で改善イテレーションを設計してみたくなったので、あえて1人で参加することにしました。決して友達がいないわけではありません。

# ISUCON14の結果

最高スコアは5502、順位は322位でした。

![スクリーンショット 2024-12-08 21.58.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26980/84907606-c3bd-9477-452b-049058e199c9.png)

1位の50000点台から見ると悲しくなりますが、初回スコア800点台から見るとけっこう改善した感はあります。

[残骸(当日使用したGitHubのレポジトリ)](https://github.com/suzuito/isucon14)

# 当日やったこと

ざっくりとこんな流れでした

- 09:15-10:00 開始前
  - 準備する。ローカルのセットアップ。MySQL Workbench(<- ER図の作成用)、Goのインストール。ネット環境の確認。手書きノートの準備。机の整理整頓。心の準備とか
  - ライブ視聴
- 10:00-11:00 仕様の把握
  - CloudFormationを実行し、環境を構築
  - 当日マニュアルとアプリケーションマニュアルを読む
- 11:00-12:00 改善イテレーションの仕組み作り
  - アプリケーションの構成を把握する
  - ソースコードと設定ファイルをローカルのgitで管理できるようにし、コマンド一発でデプロイできるようにする
  - メトリクスをコマンド一発で取得できるようにする
- 12:00-12:30 お昼休み
- 12:30-17:45 性能を改善し続ける
- 17:45-18:00 お掃除
  - MySQLスロークエリログ、Nginxのログ出力を除去する(再起動の確認をやっている暇はなかった)

# 事前にやったこと

ISUCON対策として休日を3日ぐらいを費やしました

- (0.5日)座学（情報収集とチーム名決定）
  - ISUCONの競技形式を把握。4年ぶりで競技形式をかなり忘れていた...。[こちら](https://isucon.net/archives/56735884.html)と[ISUCON14に関連する資料](https://isucon.net/archives/58818382.html)を読んだ。私は辛い食べ物が好きなのでチーム名を麻婆豆腐とした
- (0.5日)事前に何するか？を決める
  - 事前にやることとして、「時間の節約」「正確なボトルネックの把握」の2つをテーマとした。前回の参加経験から「とにかく時間がない！」と「ここ変えたら性能良くなるんじゃね？直感はダメ」ということを学んでいたため
  - ISUCONの過去問をやりながら、上記のテーマに沿って、下記を作成した
    - ISUCON当日のタイムライン(当日の何時になにをやるか決めた)
    - チートシート(コマンドライン、便利リンク集)
    - GitHubのレポジトリ(Ansibleの設定ファイルの雛形)
- (2日)過去問をやる
  - [https://github.com/matsuu/aws-isucon](https://github.com/matsuu/aws-isucon)を利用し、AWS上に環境を作ることで過去問に取り組んだ。ISUCON12予選をトライ


# 感想(振り返り)

楽しかった！

## 良かったこと

次回のISUCONでも続けたいこと

### 改善イテレーション

  - コマンド一発でデプロイ、アプリケーションを再起動できたので快適だった。デプロイ周りの混乱はなかった(デプロイ周りの混乱がなかった大きな要因の1つは、1人参加であった、というのもある)
    - [inventory.deploy.ini](https://github.com/suzuito/isucon14/blob/bdb02998027813b7bb1f6e43af7a501d3d5d7dd8/inventory.deploy.ini)、[playbook.deploy.yaml](https://github.com/suzuito/isucon14/blob/bdb02998027813b7bb1f6e43af7a501d3d5d7dd8/playbook.deploy.yaml)、[playbook.restart.yaml](https://github.com/suzuito/isucon14/blob/bdb02998027813b7bb1f6e43af7a501d3d5d7dd8/playbook.restart.yaml)を用いて、コマンド一発！
    - ```
      ansible-playbook -i inventory.deploy.ini playbook.deploy.yaml && \
      ansible-playbook -i inventory.deploy.ini playbook.restart.yaml
      ```
  - コマンド一発でメトリクスを収集できたので、気軽にメトリクスを収集できた
    - [playbook.metrics.yaml](https://github.com/suzuito/isucon14/blob/main/playbook.metrics.yaml)を用いてコマンド一発！
    - ```
      ansible-playbook -i inventory.metrics.ini \
      --extra-vars "dt=$(date +%s)" playbook.metrics.yaml
      ```

これらをAnsibleを用いて実現しました。

<details><summary>Ansibleを用いた方法の解説</summary>

Ansibleでは、inventory(リモートサーバー)とplaybook(実行される手順)を定義することで、ローカル上、リモートサーバー上で実行される手順を自動化します。

inventoryの

playbookの中で利用することができる手順は、[こちら](https://docs.ansible.com/ansible/latest/collections/index.html#list-of-collections)に掲載されています。ISUCONで利用しそうな手順は[ansible.builtin](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/index.html#plugins-in-ansible-builtin)にあるもので十分でしょう。

inventory.deploy.iniによってデプロイ先のリモートサーバーを定義します。isu_a、isu_b、isu_cはホスト名です。isu_g1、isu_g2はホストのグループです。Ansibleではこれらのホストのグループに対して、playbookと呼ばれる手順を実行します。

```ini:inventory.deploy.ini
# isu_g1とisu_g2を分割している理由は
# isu_a、isu_cをアプリケーションサーバー、isu_bをデータベースサーバーとしたためです。
# この分割はplaybook.restart.yamlファイルの中で活かされます。
[isu_g1]
isu_a
isu_c
[isu_g2]
isu_b

[all:vars]
# ssh user
# isuconユーザーでsshできるように、~/.ssh/id_rsa の公開鍵をauthorized_keysへ追加しておいてます。
ansible_user=isucon
# ssh keyのファイルパスを指定します。
ansible_ssh_private_key_file=~/.ssh/id_rsa
```

```yaml:playbook.deploy.yaml
# ソースコードと設定ファイルを配布します
- name: deploy
  hosts: isu_g1,isu_g2 # ここに列挙されたホストのグループに対して手順を実行します
  tasks: # 下記の手順が、各ホスト上で順番に実行されます
  - name: copy env
    # ansible.builtin.copyは遅いのでsynchronizeを使います
    ansible.builtin.synchronize:
      src: ./home/isucon/env.sh
      dest: /home/isucon/env.sh
  - name: copy app
    ansible.builtin.synchronize:
      src: ./home/isucon/webapp/
      dest: /home/isucon/webapp/
  - name: build
    # ansible.builtin.shellはサーバー上でコマンドを実行します
    ansible.builtin.shell:
      cmd: /home/isucon/local/golang/bin/go build -o isuride *.go
      chdir: /home/isucon/webapp/go
- name: deploy as sudo
  hosts: isu_g1,isu_g2
  become: yes # sudoで実行する場合、ここをyesとします
  tasks:
  - ansible.builtin.synchronize:
      src: ./etc/systemd/system/isuride-go.service
      dest: /etc/systemd/system/isuride-go.service
  - ansible.builtin.synchronize:
      src: ./etc/systemd/system/isuride-matcher.service
      dest: /etc/systemd/system/isuride-matcher.service
  - ansible.builtin.synchronize:
      src: ./etc/mysql/mysql.conf.d/mysqld.cnf
      dest: /etc/mysql/mysql.conf.d/mysqld.cnf
  - ansible.builtin.synchronize:
      src: ./etc/nginx/nginx.conf
      dest: /etc/nginx/nginx.conf
  - ansible.builtin.synchronize:
      src: ./etc/nginx/sites-available/isuride.conf
      dest: /etc/nginx/sites-available/isuride.conf
```

```yaml:playbook.restart.yaml
# isu_g1上ではミドルウェアとアプリケーションを再起動します
- name: restart as sudo
  hosts: isu_g1
  become: yes # sudo で実行する
  tasks:
  - ansible.builtin.shell:
      cmd: /opt/isucon-env-checker/envcheck
  - ansible.builtin.systemd_service:
      state: stopped
      name: isuride-go
  - ansible.builtin.systemd_service:
      state: stopped
      name: isuride-matcher
  # nginx
  - ansible.builtin.systemd_service:
      state: stopped
      name: nginx
  - ansible.builtin.shell:
      cmd: rm -f /var/log/nginx/access.log
  - ansible.builtin.systemd_service:
      state: started
      name: nginx
  - ansible.builtin.systemd_service:
      state: started
      name: isuride-go
  - ansible.builtin.systemd_service:
      state: started
      name: isuride-matcher
# isu_g2上ではデータベースを再起動します
- name: restart as sudo
  hosts: isu_g2
  become: yes # sudo で実行する
  tasks:
  - ansible.builtin.shell:
      cmd: /opt/isucon-env-checker/envcheck
  # mysql
  - ansible.builtin.systemd_service:
      state: stopped
      name: mysql
  - ansible.builtin.shell:
      cmd: rm -f /var/log/mysql/mysql-slow.log
  - ansible.builtin.systemd_service:
      state: started
      name: mysql
```

```yaml:playbook.metrics.yaml
# サーバー上でalpとpt-query-digestを実行し
# 実行結果をローカルへ転送します
- name: deploy
  hosts: isu_g1,isu_g2
  become: yes # sudo で実行する
  tasks:
  - ansible.builtin.shell:
      cmd: pt-query-digest /var/log/mysql/mysql-slow.log > {{dt}}.slowquery.txt
  - ansible.builtin.shell:
      cmd: cat /var/log/nginx/access.log | alp ltsv --output='all' -m '/assets/.+,/images/.+,/api/chair/rides/[0-9A-Z]+/status,/api/app/rides/[0-9A-Z]+/evaluation,/api/player/competition/[0-9a-z]+/ranking,/api/organizer/competition/[0-9a-z]+/score,/api/organizer/competition/[0-9a-z]+/finish,/api/organizer/player/[0-9a-z]+/disqualified' --sort='avg' > {{dt}}.api.txt
  - ansible.builtin.fetch: # サーバーからローカルへ転送します
      src: "{{dt}}.slowquery.txt" # {{dt}}はコマンドライン引数として渡される変数です
      dest: metrics/
  - ansible.builtin.fetch:
      src: "{{dt}}.api.txt"
      dest: metrics/
```

</details>

### メトリクス収集

  - alpを用いたAPIのエンドポイント毎のメトリクス、pt-query-digestを用いてMySQLのスロークエリログのメトリクス、この2つは基本中の基本だと学んだ。大いに参考にした
- 改善点の見つけ方
  - 基本は遅いエンドポイント、スロークエリを順番に改善してゆく、というのが良さそう
  - 当日のマニュアルに書いてある「スコアの計算方法」とか「アプリケーションの仕様」を把握し、改善点をなんとなく把握できた。仕様をきちんと読んだことが良かった。今回でいうと
    - スコアの計算方法によると、ライドを効率良く捌くことでスコアを大きく向上できると直感した。その直感自体は間違ってなかった。そして仕様によると、椅子の移動速度に関係する部分、Rideのアルゴリズム、状態の通知、この辺りが改善点かもな、と当りをつけることができた
    - あと、わざわざ[こんなこと](https://gist.github.com/wtks/8eadf471daf7cb59942de02273ce7884#:~:text=%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E3%81%AE%E3%81%9F%E3%82%81%E3%80%81-,%E8%87%AA%E7%94%B1%E3%81%AB%E5%88%A9%E7%94%A8%E6%96%B9%E6%B3%95%E5%8F%8A%E3%81%B3%E4%BB%95%E6%A7%98%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%97%E3%81%A6%E6%A7%8B%E3%81%84%E3%81%BE%E3%81%9B%E3%82%93,-%E3%80%82%20isuride%2Dmatcher)が書いてあるってことは、たぶん改善点なんだろうな、っていうのもあったり

### お決まりのパターンがわかってきた

  - 過去問を真面目に解いたので、なんとなくわかってきた、お決まりのパターン
    - Nginx,MySQL,systemdなどのミドルウェアが毎回デフォルトのまま使われている
    - N+1の改善
    - MySQLのテーブルにインデックスを貼る
    - サーバー複数台での負荷分散
    - 実装の変更を伴う系(毎回計算を回避するために計算結果をテーブルへ格納しておく、みたいな)をできるとスコアが大きく向上する

## 改善したいこと

これができてたら高得点だったかも。悔まれり。

### コードやSQLを短時間で修正する腕力がない

  - 実装の変更を伴う系(N+1を除去したり、事前に計算して結果をテーブルへ挿入したり)の改善を競技時間中にやり切れなかった。過去問もそうだけど、ここを改善できると高得点を取れるという印象があり、今回もそうだった
    - 椅子の位置情報の全履歴を格納してるテーブルを、最新の位置情報だけにすることで改善できそうなことは分かっていたけど、時間がなく手付かずに
    - 椅子とユーザーのマッチングアルゴリズムの改善をしたけどうまく動かなかったりして2時間ぐらい費やした

### 1人参加は無理かなぁ

  - 腕力が足りないなら人数を増やすか。今回で改善イテレーションを確立できたので、次回は複数人でチームを結成して取り組んでみるのもアリ。~~やっぱ友達は大事だ~~

### ボトルネックの把握がまだまだ甘い

  - 今回、2時間かけて対応したマッチングアルゴリズムの改良が不発に終わり、めちゃんこ悲しかった。ちゃんと調べてないので詳細は不明だが、椅子の移動速度を改善しない限り、マッチングアルゴリズムを改善しても意味がなかったのではないか？と推測している。ボトルネックを改善していかない限り、他をいくら改善してもスコアは向上しない(ただしマッチングアルゴリズムについてなぜスコアが向上しなかったのか？をもう少し詳しく調べたいと思っている...)

## まとめ

今回は「改善箇所がなんとなくわかるようになったけど、競技時間中にやりきれん」という感じ。前回は「飲み会の財布」「改善箇所わからん」だったのでだいぶ進歩した！(たぶん)

社内の他メンバーもISUCON14へ参加しており、競技終了後に打ち上げ飲み会へ。参加した人たちとあーでもないこーでもないと話すことがやっぱり楽しいですね。

そして毎回思うけど、これ作問してる人たちと運営がマジ凄いです。今回もありがとうございました！


# 最後に

株式会社HRBrainでは新しいメンバーを募集中です！
興味がある方は下記のリンクから宜しくお願い致します！

https://www.hrbrain.co.jp/recruit