404 Not Found

404 Not Found


nginx

パフォーマンス最適化

パフォーマンス最適化はシステムエンジニアリングの取り組みです。JVM, データベース, コネクションプールの3次元にわたり, ボトルネックを正確に特定してこそ根本原因に対応できます。

1. 学ぶこと


2. パフォーマンスエンジニアの実話

(1) ペインポイント: APIがカタツムリのように遅い

CharlieはOrderFlow注文一覧APIのP99レイテンシが500msで, ユーザー体験が非常に悪いと報告しました。Aliceの分析で以下が判明しました:1) JVMで頻繁にFull GCポーズが発生し200ms持続; 2) JPAで注文一覧を照会する際にN+1問題が発生 (100件の注文 = 101回のSQLクエリ); 3) HikariCPコネクションプールサイズが不足し, コネクション待ちでリクエストがタイムアウト。

(2) システム最適化のソリューション

各層を段階的に最適化し, ボトルネックに的確に対処します:

レベル ボトルネック 最適化ソリューション
JVM Full GCポーズ G1GC + ヒープサイズチューニング
コネクションプール コネクション不足 HikariCPパラメータ最適化
ORM N+1クエリ JOIN FETCH / EntityGraph
データベース フルテーブルスキャン インデックス追加

(3) 成果

Aliceが各コンポーネントを順次最適化した結果, P99レイテンシは500msから50msに低下し, スループットは10倍に向上しました。


3. JVMパラメータチューニング

(1) GCアルゴリズムの比較

GCアルゴリズム 最大ポーズ時間 適用シナリオ JVMパラメータ
G1GC 10-200 ms 一般 (JDK 17デフォルト) -XX:+UseG1GC
ZGC < 1 ms 低レイテンシ (JDK 17+) -XX:+UseZGC
SerialGC 長い 小ヒープ (< 200MB) -XX:+UseSerialGC
ParallelGC スループット優先 -XX:+UseParallelGC
100%
graph LR
    A["JVMチューニング"] --> B["ヒープサイズ -Xms = -Xmx"]
    A --> C["GCアルゴリズム G1 / ZGC"]
    A --> D["GCログ -Xlog:gc*"]
    B --> B1["コンテナ: MaxRAMPercentage"]
    C --> C1["デフォルト: G1GC"]
    C --> C2["低レイテンシ: ZGC"]

(1) ▶ サンプル:Docker環境でのJVM設定

DOCKERFILE
ENTRYPOINT ["java", \
  "-XX:+UseG1GC", \
  "-XX:MaxRAMPercentage=75.0", \
  "-XX:InitialRAMPercentage=50.0", \
  "-XX:+UseStringDeduplication", \
  "-Xlog:gc*:file=/app/logs/gc.log:time,uptime,level,tags", \
  "-jar", "app.jar"]

出力:

TEXT
// 実行成功
パラメータ 意味 推奨値
MaxRAMPercentage コンテナメモリに対する最大ヒープの割合 75.0
InitialRAMPercentage 初期ヒープが占有するコンテナメモリの割合 50.0
+UseG1GC G1ガベージコレクタを使用 デフォルト有効
+UseStringDeduplication 文字列重複排除でメモリ節約 推奨
MaxGCPauseMillis GC目標ポーズ時間 200 (G1)/ なし (ZGC)

(2) ▶ サンプル:ZGC低レイテンシ設定

BASH
java -XX:+UseZGC \
     -XX:MaxRAMPercentage=75.0 \
     -XX:+ZGenerational \
     -Xlog:gc*:file=gc.log \
     -jar app.jar

出力:

TEXT
// コマンド実行成功
📌 要点: ZGCはJDK 17では実験的機能です (JDK 21で正式利用可能)。ポーズ時間1ms未満で, 極めてレイテンシに敏感なシナリオに適しています。


4. HikariCPコネクションプール最適化

(1) コネクションプールパラメータ

パラメータ デフォルト値 説明 推奨値
maximumPoolSize 10 最大コネクション数 CPUコア数 x 2 + ディスク数
minimumIdle = maxPool 最小アイドルコネクション = maxPoolSize
connectionTimeout 30,000 ms コネクションタイムアウト 3,000 ms
idleTimeout 600,000 ms アイドルコネクションタイムアウト 600,000 ms
maxLifetime 1,800,000 ms 最大コネクション寿命 1,800,000 ms
leakDetectionThreshold 0 (無効) コネクションリーク検出 60,000 ms

(1) ▶ サンプル:HikariCP本番設定

YAML
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 20
      connection-timeout: 3000
      idle-timeout: 600000
      max-lifetime: 1800000
      leak-detection-threshold: 60000
      pool-name: OrderFlowHikariCP

出力:

TEXT
設定が反映されました。

(2) ▶ サンプル:コネクションリーク検出

YAML
# リーク検出を有効化 (コネクション保持が60秒を超えると警告ログ)
spring:
  datasource:
    hikari:
      leak-detection-threshold: 60000
💻 出力 (リーク発生時):

TEXT
WARN  - Connection leak detection triggered for {conn-12345}
  Stack trace of the call site that acquired the connection:
    at com.orderflow.service.OrderService.getOrder(OrderService.java:45)
    ...

5. JPAクエリ最適化

(1) N+1問題

(1) ▶ サンプル:N+1問題のデモ

JAVA
// 悪い例: N+1問題
// 100件の注文を取得する1SQL + 各注文のitemsを取得する100SQL = 合計101SQL!
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
    order.getItems().size();  // 各注文の遅延ロードをトリガー
}

出力:

TEXT
// 実行成功

(2) ▶ サンプル:JOIN FETCHでN+1問題を解決

JAVA
// 良い例: JOIN FETCHで単一クエリ
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items i JOIN FETCH i.product WHERE o.status = :status")
List<Order> findByStatusWithItems(@Param("status") String status);

出力:

TEXT
// 実行成功

(3) ▶ サンプル:@EntityGraphによる宣言的ロード

JAVA
@Entity
@NamedEntityGraph(
    name = "Order.withItemsAndProduct",
    attributeNodes = {
        @NamedAttributeNode("items"),
        @NamedAttributeNode(value = "items", subgraph = "item-product")
    },
    subgraphs = {
        @NamedSubgraph(name = "item-product", attributeNodes = @NamedAttributeNode("product"))
    }
)
public class Order { /* ... */ }

// リポジトリでの使用
@EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
List<Order> findByStatus(String status);

出力:

TEXT
// 実行成功
ソリューション SQL文数 適用シナリオ 保守性
デフォルトの遅延ロード N+1 単一オブジェクト照会
JOIN FETCH 1 特定クエリ
@EntityGraph 1 再利用可能なロード計画
@BatchSize N/batchSize バッチロード

6. データベースインデックス最適化

(1) インデックス戦略

(1) ▶ サンプル:データベースインデックスの追加

SQL
-- 注文ステータス照会用インデックス
CREATE INDEX idx_order_status ON orders(status);

-- 一般的なクエリパターン用複合インデックス
CREATE INDEX idx_order_status_created ON orders(status, created_at DESC);

-- 商品検索用インデックス
CREATE INDEX idx_product_name ON products(name);

-- 注文アイテムの商品検索用インデックス
CREATE INDEX idx_order_item_product ON order_items(product_id);

出力:

TEXT
CREATE TABLE
インデックスタイプ ユースケース
単一カラムインデックス 単一条件クエリ WHERE status = 'PENDING'
複合インデックス 複数条件クエリ WHERE status = ? AND created_at > ?
ユニークインデックス 一意制約 UNIQUE(sku)
カバリングインデックス インデックスにすべての照会列を含む SELECT status FROM orders WHERE id = ?

(2) ▶ サンプル:スロークエリ分析

SQL
-- MySQL スロークエリログ設定
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;  -- 1秒を超えるクエリをログに記録
SET GLOBAL log_queries_not_using_indexes = ON;

-- クエリ実行計画を分析
EXPLAIN SELECT o.*, i.*
FROM orders o
JOIN order_items i ON o.id = i.order_id
WHERE o.status = 'PENDING'
  AND o.created_at > '2024-01-01'
ORDER BY o.created_at DESC
LIMIT 20;

出力:

TEXT
CREATE TABLE
EXPLAINフィールド 意味 ポイント
type アクセスタイプ ALL (フルテーブルスキャン)は最適化が必要
key 使用されたインデックス NULLはインデックス未使用を示す
rows スキャン行数 低いほど良い
Extra 追加情報 Using filesortは最適化が必要

7. 総合サンプル:OrderFlowのP99を500msから50msに削減

YAML
# application-prod.yml (最適化版)
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 20
      connection-timeout: 3000
      leak-detection-threshold: 60000
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
    properties:
      hibernate:
        default_batch_fetch_size: 100
        jdbc:
          batch_size: 50
        order_inserts: true
        order_updates: true
BASH
# JVMパラメータ (Docker ENTRYPOINT)
java -XX:+UseG1GC \
     -XX:MaxRAMPercentage=75.0 \
     -XX:InitialRAMPercentage=50.0 \
     -XX:+UseStringDeduplication \
     -XX:MaxGCPauseMillis=100 \
     -Xlog:gc*:file=/app/logs/gc.log:time,uptime,level,tags \
     -jar app.jar
JAVA
// EntityGraphとバッチフェッチで最適化されたOrderRepository
public interface OrderRepository extends JpaRepository<Order, Long> {

    @EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
    @Query("SELECT o FROM Order o WHERE o.status = :status ORDER BY o.createdAt DESC")
    Page<Order> findByStatusPaged(@Param("status") String status, Pageable pageable);

    @EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
    Optional<Order> findById(Long id);
}

// Hibernateバッチサイズ設定でコレクションのN+1を解消
// application.yml: hibernate.default_batch_fetch_size=100
最適化前 最適化後 改善率
P99: 500 ms P99: 50 ms 10倍
SQL/リクエスト: 101 SQL/リクエスト: 1 100倍
GCポーズ: 200 ms GCポーズ: 50 ms 4倍
コネクション待ち: 100 ms コネクション待ち: 2 ms 50倍

❓ よくある質問

Q G1GCとZGCはどちらを選ぶべきですか?
A G1GCはJDK 17のデフォルトで, ほとんどのシナリオに適しており, ポーズ時間は10-200msです。ZGCはポーズ時間1ms未満で, 金融取引などの低レイテンシシナリオに適していますが, スループットはやや低いです。まずG1GCから始め, レイテンシ問題に直面した場合のみZGCに切り替えてください。
Q コネクションプールは大きいほど良いですか?
A いいえ。大きすぎるコネクションプールは:1) データベース負荷の増大; 2) コンテキストスイッチのオーバーヘッド; 3) メモリ使用量の増加をもたらします。推奨式:コネクション数 = (CPUコア数 x 2) + 有効スピンドル数。
Q @EntityGraphとJOIN FETCHの違いは?
A @EntityGraphは宣言的で, EntityやRepository上に定義して再利用可能; JOIN FETCHは命令的で, 各@Query内に記述します。複雑なシナリオではEntityGraphの方が保守しやすいです。
Q Hibernateのbatch_sizeは何をしますか?
A hibernate.jdbc.batch_size=50を設定すると, INSERT/UPDATE文がバッチ実行され (50レコードずつ), データベースへの往復回数が削減されます。order_inserts=trueと組み合わせるとさらに効果的です。
Q パフォーマンスボトルネックを特定する方法は?
A 1) Actuatorの/metricsでHTTPリクエスト時間を確認; 2) GCログを分析してGCポーズを特定; 3) HikariCPメトリクスでコネクションプール状況を確認; 4) MySQLスロークエリログでスロークエリを特定; 5) APMツール (SkyWalking/Zipkin)でエンドツーエンドのトレーシング。
Q パフォーマンステストの方法は?
A JMeter, Gatling, またはk6を使用します。段階的に負荷を増加 (ramp-up)し, 応答時間とエラー率の変曲点を観察します。テストシナリオは単一APIエンドポイント, 混合シナリオ, ピークシナリオをカバーしてください。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度:⭐): OrderFlowのJVM G1GCパラメータとHikariCPコネクションプールパラメータを設定し, GCログとコネクションリーク検出を有効にしてください。

  2. 応用問題 (難易度:⭐⭐): @EntityGraphJOIN FETCHを使って注文一覧照会のN+1問題を解決し, 最適化前後のSQL文数と応答時間を比較してください。

  3. チャレンジ (難易度:⭐⭐⭐): JMeterでOrderFlow注文一覧APIの負荷テストを行い, パフォーマンスベースラインを確立し, 段階的に最適化 (JVM + コネクションプール + N+1 + インデックス)してください。各最適化ステップのP50とP99の変化を記録し, 最終的にP99を50ms以下に最適化してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%