パフォーマンスチューニングに入門してきた


# はじめに
先日、サイバーエージェントさんが実施するワークショップに参加して、パフォーマンスチューニングについて勉強させてもらいました。
今回は、学んだ内容を整理してアウトプットしたいと思います。

https://cyberagent.connpass.com/event/270624

こちらの記事は、ワークショップをご担当されたエンジニア社員さんにはレビューいただきましたが、サイバーエージェントさんとは関係のない、非公式で個人的な記事になります。
また、ワークショップで知り得た企業の情報は一切なく、パフォーマンスチューニングという技術的な話題にのみ焦点を当てています。

# パフォーマンスチューニングとは
パフォーマンスチューニングとは、サービスの要件に応じてシステムのパフォーマンスを改善することです。
パフォーマンスは、大きく分けて以下2つの指標を指します。
- レイテンシ：どれだけ速くレスポンスを返せるか
- スループット：どれだけ多くリクエストを捌けるか
ただし、基本的にはトレードオフが付きまとうため、サービスの要件やその時々の状況に合わせた判断が必要になります。

# パフォーマンスチューニングの流れ
パフォーマンスチューニングの流れは、チューニングとモニタリングの2つのフェーズに大きく分けることができます。
まず、チューニングのフェーズでは、サービス要件を満たすための基準値をクリアするまで、以下のステップを繰り返します。
ボトルネックは、パフォーマンスを下げる要因となっている箇所を指します。
1. 負荷
2. 計測
3. ボトルネックの特定
4. 改善

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539031/4e2736a3-d213-baa3-3a79-4f751c0223e4.png">


以上のフェーズで基準値を満たしてデプロイすれば終わりではありません。
次に、モニタリングのフェーズで、本番環境で稼働しているシステムが実際に想定したパフォーマンスを出せているか、レイテンシとスループットを監視します。
ここで想定したパフォーマンスを出せていなかった場合、改めてチューニングし直す必要があります。

# パフォーマンスの改善手段
パフォーマンスを改善するための手段は、以下の3種類に分類できます。
- 解決：処理そのものを高速化する（例：並列処理、クエリチューニング）
- 回避：遅い処理を迂回する（例：CDN、インメモリキャッシュ）
- 緩和：システムの性能を上げる（例：スケールアップ、スケールアウト）

以上の例で挙げているように、様々な手段がありますが、どれもトレードオフを意識して、サービス要件に合った手段を導入することを念頭に置いておくことが大事です。

例えば、インメモリキャッシュを導入すれば、高頻度にアクセスされるデータを一時保存してレスポンスを早められるかもしれませんが、キャッシュされたデータは最新のデータであるとは限りません。そのため、適切なTTLの設定やDB書き込み時のキャッシュ書き込みなどを行って整合性の担保をする必要があります。つまり、速度を上げる代わりに整合性の担保が難しくなる、というトレードオフが発生します。

# ツール
パフォーマンスを改善する上で、擬似的にシステムに負荷をかけて、負荷状況や実行クエリなどを計測、可視化する必要があります。
そのために、負荷やモニタリングをするためのツールが提供されています。
パフォーマンスチューニングを行う上で、今回のワークショップで紹介いただいたツールは以下になります。

**負荷テスト**
- [k6](https://github.com/grafana/k6)
- [locust](https://github.com/locustio/locust)
- [Jmeter](https://github.com/apache/jmeter)

**モニタリング**
- [Cloud Watch](https://aws.amazon.com/jp/cloudwatch/)
- [X-Ray](https://aws.amazon.com/jp/xray/)

負荷テストツールに関しては、サービスのシナリオを考えた上でツールを選定する必要があります。

# チューニング例
今回のワークショップで体験した範囲で、パフォーマンスチューニングの実例を紹介します。

## IN句を使ったSQLクエリにおけるN+1問題の解消
SQLクエリにおけるN+1問題は、1件のデータにN件のデータが紐づいている場合に、合計でN+1回のクエリを実行している問題を指します。（N+1というより、1+Nの方が覚えやすいかも）

N+1問題を解消する方法としては、例えばIN句を用いて1つのクエリとして実行することでクエリの実行回数を大幅に削減できる可能性があります。
IN句に限らず、JOIN句などクエリの実行回数を削減する方法は他にもあると思います。

## インデックスの追加によるスロークエリの解消
スロークエリとは、サービス要件から判断して、データベースへの問い合わせにかかる時間が遅いクエリのことを指します。

スロークエリを解消する方法としては、例えば、クエリの検索条件となるカラムを対象としたインデックスを追加することで、検索にかかる実行時間を大幅に短縮できる可能性があります。

## インメモリキャッシュの導入によるDB負荷の緩和
リクエストを受けてからレスポンスを返すまでにかかる時間の多くは、DBへの問い合わせにかかる時間が占めています。そのため、DBへの問い合わせをなるべく減らすことが、レイテンシを上げるために重要です。

そこで、例えば、高い頻度で必要になるデータはメモリ上に一時保存するインメモリキャッシュを導入することで、DBへの問い合わせ頻度を抑制し、レイテンシを上げられる可能性があります。

## singleflightの導入による重複した関数呼び出しの抑制
[singleflight](https://pkg.go.dev/golang.org/x/sync/singleflight)は、重複した関数呼び出しを抑制するGoのパッケージです。

singleflightを導入することで、同じ関数の呼び出しが複数回実行された場合に、関数の実行回数を1回に留めることが可能になります。例えば、キャッシュの有効期限が切れて同一のデータベースへの問い合わせが並行して走ってしまう場合に、サーバーやデータベースにおける性能の低下を防ぐことができます。

# 推測するな、計測せよ
ワークショップの中でも度々言われていたことが「推測するな、計測せよ」でした。
つまり、パフォーマンスを定量的に評価することが大事で、その上でチューニングの施策を考えるべきということです。
チューニングの手段や例を色々と挙げましたが、計測によるボトルネックの特定や計測によるチューニングの効果の検証を忘れてはいけません。
また、推測そのものがダメという訳ではなく、ある程度の推測をもとにあたりを付けた上で計測を行うこともあります。

# おわりに
キャッシュやインデックスのことは知っていましたが、パフォーマンスを定量的に見てチューニングを試みることは初めてでした。
たった半日でパフォーマンスチューニングとは何か、パフォーマンスチューニングに対する考え方や具体的なアプローチまで教えてくださったサイバーエージェントの社員の皆さんには、とても感謝しています🙏

以下のリンクから、サイバーエージェントさんのconnpassイベントが確認できます。
サイバーエージェントさんのワークショップは面白そうなものが多いので、今後も積極的に参加していきたいです！

https://cyberagent.connpass.com/