パフォーマンス最適化
パフォーマンス最適化はシステムエンジニアリングの取り組みです。JVM, データベース, コネクションプールの3次元にわたり, ボトルネックを正確に特定してこそ根本原因に対応できます。
1. 学ぶこと
- JVMパラメータチューニング:ヒープサイズ / GCアルゴリズム選択 (G1GC / ZGC)
- HikariCPコネクションプールパラメータ最適化とコネクションリーク検出
- JPAクエリ最適化:N+1問題と解決策
@EntityGraph/fetch join - データベースインデックス戦略とスロークエリログ分析
- AliceはOrderFlow注文一覧APIのP99レイテンシを500msから50msに最適化しました
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 |
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設定
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"]
出力:
// 実行成功
| パラメータ | 意味 | 推奨値 |
|---|---|---|
MaxRAMPercentage |
コンテナメモリに対する最大ヒープの割合 | 75.0 |
InitialRAMPercentage |
初期ヒープが占有するコンテナメモリの割合 | 50.0 |
+UseG1GC |
G1ガベージコレクタを使用 | デフォルト有効 |
+UseStringDeduplication |
文字列重複排除でメモリ節約 | 推奨 |
MaxGCPauseMillis |
GC目標ポーズ時間 | 200 (G1)/ なし (ZGC) |
(2) ▶ サンプル:ZGC低レイテンシ設定
java -XX:+UseZGC \
-XX:MaxRAMPercentage=75.0 \
-XX:+ZGenerational \
-Xlog:gc*:file=gc.log \
-jar app.jar
出力:
// コマンド実行成功
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本番設定
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
出力:
設定が反映されました。
(2) ▶ サンプル:コネクションリーク検出
# リーク検出を有効化 (コネクション保持が60秒を超えると警告ログ)
spring:
datasource:
hikari:
leak-detection-threshold: 60000
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問題のデモ
// 悪い例: N+1問題
// 100件の注文を取得する1SQL + 各注文のitemsを取得する100SQL = 合計101SQL!
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
order.getItems().size(); // 各注文の遅延ロードをトリガー
}
出力:
// 実行成功
(2) ▶ サンプル:JOIN FETCHでN+1問題を解決
// 良い例: 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);
出力:
// 実行成功
(3) ▶ サンプル:@EntityGraphによる宣言的ロード
@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);
出力:
// 実行成功
| ソリューション | SQL文数 | 適用シナリオ | 保守性 |
|---|---|---|---|
| デフォルトの遅延ロード | N+1 | 単一オブジェクト照会 | 高 |
| JOIN FETCH | 1 | 特定クエリ | 中 |
| @EntityGraph | 1 | 再利用可能なロード計画 | 高 |
| @BatchSize | N/batchSize | バッチロード | 高 |
6. データベースインデックス最適化
(1) インデックス戦略
(1) ▶ サンプル:データベースインデックスの追加
-- 注文ステータス照会用インデックス
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);
出力:
CREATE TABLE
| インデックスタイプ | ユースケース | 例 |
|---|---|---|
| 単一カラムインデックス | 単一条件クエリ | WHERE status = 'PENDING' |
| 複合インデックス | 複数条件クエリ | WHERE status = ? AND created_at > ? |
| ユニークインデックス | 一意制約 | UNIQUE(sku) |
| カバリングインデックス | インデックスにすべての照会列を含む | SELECT status FROM orders WHERE id = ? |
(2) ▶ サンプル:スロークエリ分析
-- 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;
出力:
CREATE TABLE
| EXPLAINフィールド | 意味 | ポイント |
|---|---|---|
type |
アクセスタイプ | ALL (フルテーブルスキャン)は最適化が必要 |
key |
使用されたインデックス | NULLはインデックス未使用を示す |
rows |
スキャン行数 | 低いほど良い |
Extra |
追加情報 | Using filesortは最適化が必要 |
7. 総合サンプル:OrderFlowのP99を500msから50msに削減
# 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
# 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
// 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倍 |
❓ よくある質問
batch_sizeは何をしますか?hibernate.jdbc.batch_size=50を設定すると, INSERT/UPDATE文がバッチ実行され (50レコードずつ), データベースへの往復回数が削減されます。order_inserts=trueと組み合わせるとさらに効果的です。/metricsでHTTPリクエスト時間を確認; 2) GCログを分析してGCポーズを特定; 3) HikariCPメトリクスでコネクションプール状況を確認; 4) MySQLスロークエリログでスロークエリを特定; 5) APMツール (SkyWalking/Zipkin)でエンドツーエンドのトレーシング。📖 まとめ
- JVMチューニング:デフォルトでG1GCで十分;ZGCは超低レイテンシ向け;MaxRAMPercentageでコンテナメモリを制御
- HikariCP:コネクションプールサイズ = CPUコア数 x 2 + ディスク数;leakDetectionを有効にしてメモリリークを排查
- N+1問題:JOIN FETCHで単一SQL, @EntityGraphで宣言的ロード, @BatchSizeでバッチロード
- データベースインデックス:頻繁に使用するクエリ条件にインデックスを追加;EXPLAINで実行計画を分析
- バッチ処理:hibernate.jdbc.batch_size + order_insertsでデータベース往復を削減
- 体系的最適化:ボトルネック特定 → ベースライン定量 → 段階的最適化 → 結果検証
📝 練習問題
-
基本問題 (難易度:⭐): OrderFlowのJVM G1GCパラメータとHikariCPコネクションプールパラメータを設定し, GCログとコネクションリーク検出を有効にしてください。
-
応用問題 (難易度:⭐⭐):
@EntityGraphとJOIN FETCHを使って注文一覧照会のN+1問題を解決し, 最適化前後のSQL文数と応答時間を比較してください。 -
チャレンジ (難易度:⭐⭐⭐): JMeterでOrderFlow注文一覧APIの負荷テストを行い, パフォーマンスベースラインを確立し, 段階的に最適化 (JVM + コネクションプール + N+1 + インデックス)してください。各最適化ステップのP50とP99の変化を記録し, 最終的にP99を50ms以下に最適化してください。



