負荷テストの結果をもとに、パフォーマンス改善に取り組んだ事例をご紹介します。
本記事では、AWSのFargate環境で動作するFastAPIをバックエンドとしたシステムのパフォーマンス課題を解消するために実施した具体的な内容と、そこから得た学びをまとめます。

環境概要

今回のチューニング対象となったシステムは以下の構成です

  • コンテナ基盤: AWS Fargate
  • アプリケーション: FastAPI
  • データベース接続: RDS Proxy
  • データベース: Aurora MySQL
  • 負荷テストツール: Locust

パフォーマンスチューニングの進め方

1. スロークエリログの確認

最初のステップは、データベースのスロークエリログを活用して問題箇所を特定することでした。
スロークエリはデフォルトでファイル出力されますが、出力先をテーブルにすると、SELECT * FROM mysql.slow_log;で簡単に一覧出力できます。
今回はスロークエリログが多量に出力されたため、テーブルに出力することで以下のメリットがありました。

  • クエリを実行時間の降順に並べ替えることで、簡単にボトルネックを特定できる
  • クエリテキストをグルーピングすることで、頻出パターンを効率的に調査可能
  • ログの抽出からチューニングまでをGUIツールで完結できる

2. SQL単体でのチューニングとソース反映

抽出されたスロークエリに対して、以下のアプローチで調査・改善を行いました。

  1. 単体のSQLクエリを実行し、問題点を洗い出し
  2. 改善策を実施した結果を検証し、効果が認められた内容をアプリケーションのコードへ反映

スロークエリの対応と詳細

1. 実行計画を基にした対応

データベースの実行計画を分析し、以下の改善を実施
1. 効果的なインデックスの追加
2. 不要なテーブル結合の排除
条件に応じてJOINを排除し、実行効率を向上
3. その他、一般的なチューニングテクニックの応用

2. インデックスが効かないパターンと回避策

特定の条件下ではインデックスが効かず、調整が必要でした。
※ここでは当案件で遭遇したパターンのみ記載しますが、他にもインデックスが効かないパターンは存在します。

2-1. 関数を使用する場合
まず下記インデックスを追加

ALTER TABLE t_sample ADD INDEX idx_sample_test_date (test_date)

以下のクエリはインデックスが効かず

WHERE CAST(t_sample.test_date AS DATE) >= '2024-10-01'
AND CAST(t_sample.test_date AS DATE) <= '2024-12-31' 

対策として関数を使用せずに条件を指定

WHERE t_sample.test_date >= '2024-10-01 00:00:00'
AND t_sample.test_date <= '2024-12-31 23:59:59'

※このとき関数を外すことにより検索結果が変わらないか、注意が必要
具体的にはtest_dateに2024-12-31 01:00:00のような時分秒が登録されているケースでは、t_sample.test_date <= 2024-12-31'としてしまうと2024-12-31 00:00:00 以前のデータがヒットするためCASTした場合と結果が変わってしまいます。CASTを外す場合はt_sample.test_date <= '2024-12-31 23:59:59'というように時間まで指定する必要があります。

2-2. LIKEでの部分一致
前方に%を付けた検索条件がボトルネックに

WHERE t_sample.mail LIKE '%test@sample.com%' 

対策として以下を実施。技術的に回避できなかったため、要件に合わせて以下のように仕様を調整し、部分一致指定の呼び出し回数を最小化することで対応しました。

  1. APIパラメータを「完全一致」と「部分一致」に分割
  2. もしくは完全一致用APIを別途作成

3. 行ロック時間の短縮

登録系APIで主キーが連番である要件に対応するため、以下の問題と対策を実施
課題: 最大値取得とロック処理で待ち時間が発生、タイムアウトに
対策: シーケンステーブルを導入し、以下のフローに変更。こうすることで最大値取得の負荷軽減とロック時間の短縮が可能になる
公式ドキュメントでも紹介されている方法を導入

1.数字を入れるカラムがあり、1レコードあれば足りる

mysql> CREATE TABLE sequence (id INT NOT NULL);
mysql> INSERT INTO sequence VALUES (0);

2.UPDATE文で数字をインクリメントし、2つ目のクエリでその値を取得する

mysql> UPDATE sequence SET id=LAST_INSERT_ID(id+1);
mysql> SELECT LAST_INSERT_ID();

補足: その後の登録処理で何らかの問題が発生しロールバックした場合、連番に歯抜けが発生するため、要件的に許容範囲かどうかは確認が必要

Locustシナリオでの失敗談

負荷テストツールLocustを使用する際、以下の内容でシナリオを作成したため、無駄に負荷をかけてしまっていました。

  • 更新対象データの固定化
    同じデータを更新し続けたため、履歴データが膨大になり不必要な負荷を増加。
  • 検索負荷の増大
    大量データの登録を行った特定の検索対象に対して検索を行ったため、データ量に比例して検索時間が悪化。

まとめ

本記事では、負荷テストから得た情報を基に具体的なチューニング手法を解説しました。適切なログの活用や実行計画の確認、インデックスの最適化がパフォーマンス向上の鍵となります。特にシステム要件に依存する課題では、仕様変更の相談を含め柔軟な対応が必要です。