社内ISUCONの予習の予習としてやったこと

社内で開催されるISUCONに出るために、予習としてやってみたことをまとめます。

いきなりパフォーマンス改善手法について学ぶのではなく、まずは何を学ぶべきか知るための下準備をするところからはじめました。
経験や知識がなく何を予習すれば良いかわからない、ISUCONの進め方のイメージが湧かないというのもありましたが、手法を学ぶ前にまずその手法の使いどころを知るべきだと考えたためです。

自分のような新人プログラマには参考になる内容かもしれません。

## ISUCONの進め方を知る
出てみたブログなどを読み漁ると似たようなことが書いてあるかと思いますが、こんな感じに進めると良いと教わりました。

### 当日の流れ
1. 各チームにインスタンス、sshのログイン方法を渡されて、よーい、どん。
1. ベンチを回す。
1. 渡されたインスタンスの中にレギュレーションのまとめやアプリケーションがあるので中身を確認し、アプリケーションの概要や仕様、禁則事項など必要な情報を素早く把握する。
1. 使用する言語の参照実装に切り替える。
1. Githubリポジトリ(よ)、デプロイスクリプトなどの準備。  
（チームで出る場合はインスタンスを直接触るわけにもいかないので）
1. ボトルネック把握に使う計測ツールを入れる
1. 計測ツールとアプリケーションのコードを並行して見て、ボトルネックを探す
1. ボトルネックを見つけたらブランチを切って修正し、デプロイしてベンチを回す。  
(ブランチを切って手元でやらなくても良い場合もありそうなのでその辺りは柔軟に)
1. スコアが改善していたら、↑の修正ブランチをmasterにマージする。

以降、7~9をいい感じに繰り返してスピードアップさせる。

### 重要なポイント
- とにかく時間がない。環境構築など初動に躓くとあっという間に時間がなくなるので、事前の役割分担・シミュレーションが大事。
- インパクトの大きい修正箇所を見つけることが大事。手当たり次第に修正しても、中々スコアが伸びなかったりする。

## 過去問をやってみる会をやる
AWSにISUCON8予選問題とベンチマーカーのインスタンスを立てて(もらって)、上記に記載した流れを一通りやってみました。

計測ツールは [Datadog APM](https://docs.datadoghq.com/ja/tracing/)を使用しました。
エンドポイントごとのリクエスト数やレイテンシー、合計かかった時間などを簡単にみることが出来て便利でした。

### インスタンスにDatadog agentを入れる
Datadogのコンソールから Integrations > agent を開くとセットアップ手順のガイドが出てきます。
インストールに必要なコマンドをコピーし、ssh接続をして叩きます。

インストール成功したら、[DOCS](https://docs.datadoghq.com/ja/agent/)を参照しながらコマンドでagentを立ち上げます。

これで完了ですが、一応ダッシュボードから system metricsなどを開いてデータが取得できていることを確認しておきます。

### Datadog APMを入れる
これもコンソールから　APM > services > new service を開き、ほぼ[セットアップ手順](https://docs.datadoghq.com/ja/tracing/setup_overview/setup/nodejs/)に従うだけです。

私はNodeJSを使うことにしていたので、[dd-trace](https://www.npmjs.com/package/dd-trace)というライブラリをインストールし、アプリケーションコードに以下を書き加えるだけです。

```typescript
import tracer from "dd-trace";

tracer.init({ analytics: true });
```

これで完了ですので、今度もダッシュボードからデータが取得できていることを確認します。

### 計測結果をみて怪しいところを探す
なんとなくアプリケーションの概要を把握した上で、早速計測結果をみていきます。

APMタブを選ぶと、以下のようににService別の計測ダッシュボードの一覧が表示されています。
![スクリーンショット 2020-11-30 22.58.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91546/0289c474-5c86-c463-8276-e91dc1387098.png)
![スクリーンショット 2020-11-30 22.59.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91546/57239cf9-5c8a-9bee-951c-62da799ae47e.png)

色々出てきて迷いますが、まずは本体の webapp1 を選んでみます。

![スクリーンショット 2020-11-30 23.00.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91546/45044eb2-9d06-8d4d-855d-b198f44be837.png)

エンドポイントごとの合計所要時間をみるとどうやら `GET /api/users/:id` や `GET /`あたりが特に遅いようです。
`GET /admin` もレイテンシーは長いですが前者と比べるとリクエスト数が少ないですね。

`GET /api/users/:id` を選択して、より詳細をみていきます。

![スクリーンショット 2020-11-30 23.01.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91546/51a318c7-1ce6-cc32-097c-118a7e9ca6b8.png)

どうやらこのSQL文が怪しいです。AVG SPAN/TRACEを見るとめちゃくちゃ呼ばれていることがわかります。

>Avg Span/trace
>スパンが少なくとも 1 回存在する、現在のリソースを含むトレースのスパンの平均発生回数。
cf. https://docs.datadoghq.com/ja/tracing/visualization/resource/#%E3%82%B9%E3%83%91%E3%83%B3%E3%82%B5%E3%83%9E%E3%83%AA%E3%83%BC

### 怪しいところのコードを見てみる
怪しいところを見つけたので、このコードを改善できるか具体的に調べていきます。

[該当のSQL](https://github.com/isucon/isucon8-qualify/blob/fb07e6dc4790d8203d87027d5625a1fc055c2ed6/webapp/nodejs/index.ts#L190)を探し周辺コードを読むと、 [`getEvent`](https://github.com/isucon/isucon8-qualify/blob/fb07e6dc4790d8203d87027d5625a1fc055c2ed6/webapp/nodejs/index.ts#L159-L216)という関数によってループのさらにループの中で都度このSQLが実行されてしまっていたことがわかります。いわゆる N+1問題ですね。

しかもこの `getEvent` を呼び出す `getEvents` は `GET /` などでも呼ばれており、他のエンドポイントの遅い原因も同じく `getEvent` にありそうです。

### コードを修正する
ボトルネックがわかったので修正をします。

`getEvents` が色々なエンドポイントから呼ばれていたのと、取得結果は破壊的メソッドで取り扱われていて少し読みにくかったので（型もないし）、壊さないように直せるか不安でした。
なのであまり手を加えず、ループ自体は残しつつ外側でまとめてSELECTする程度の修正にしました。

https://github.com/isucon/isucon8-qualify/blob/fb07e6dc4790d8203d87027d5625a1fc055c2ed6/webapp/nodejs/index.ts#L179

```typescript
const [sheetRows] = await fastify.mysql.query("SELECT s.*, r.id as reservation_id, r.reserved_at FROM sheets s LEFT JOIN(SELECT*FROM reservations WHERE event_id=? AND canceled_at IS NULL GROUP BY event_id,sheet_id HAVING reserved_at=MIN(reserved_at))r ON s.id=r.sheet_id ORDER BY s.`rank`,s.num", [event.id]);
```

https://github.com/isucon/isucon8-qualify/blob/fb07e6dc4790d8203d87027d5625a1fc055c2ed6/webapp/nodejs/index.ts#L190-L194

```typescript
    if (sheet.reservation_id) {
      if (loginUserId && sheet.reservation_id === loginUserId) {
        sheet.mine = true;
      }
```

https://github.com/isucon/isucon8-qualify/blob/fb07e6dc4790d8203d87027d5625a1fc055c2ed6/webapp/nodejs/index.ts#L197

```typescript
      sheet.reserved_at = parseTimestampToEpoch(sheet.reserved_at);
```

`event`ごとに `sheets`を取得しており、1 `sheets`に有効な `reservation`は1つしかないはずなので、このSELECT分で事足りるはずです。

修正後、ベンチを回すと  `580` → `4514 ~ 7842 ` くらいにスコアが伸びていました :clap: 
（本家ISUCONより大きいインスタンスのサイズを使うなどしているので、本家のスコアとは比較できない気がします）

過去問をやってみる会は初動の動きを確認するところまでがゴールでしたので、他にもまだやれることはあるはずですが一旦ここで終了します。

## 講評を読んで振り返る
初動の動きは躓くことなくスムーズに修正が出来た点は良かったですが、もっと点を伸ばすには何ができたかを知るために[講評](http://isucon.net/archives/52520045.html)を読みます。

大体こんな感じの視点で読みます。

- 他の修正ポイントを知り、どのような思考・調査でその課題に気づくことが出来そうか考える  
→これが本番での戦略に繋がる
- 単純に足りない知識をインプットする  
ロック地獄とか聞いたことはあるけど、実際にどう解消するかとか知識がなかったので。
- 自分の行った修正をもっといい感じに修正出来そうか知る。  
1段階分のループをなくすだけの弱腰の修正をしてしまったが、 [この修正](https://qiita.com/najeira/items/527e1f54fa417fd20b65#getevent)は2段階分のループを外してとても点数が伸びたよう

## 終わりに
「とにかく時間がないので限られた時間内で環境を用意して、効果の大きい修正を効率的に行うこと」が大事なんだなぁという感想が得られ、また苦手なインフラ周りや計測ツール周りについて勉強していくための足掛かりを作ることが出来ました。

社内ISUCON本番までの予習としてやるべきことがわかったのはもちろん、開発者としてスキルアップするための知識に触れることが少し出来た気がするので実りの多い予習が出来た気がします。