MySQL, Spring Boot & HikariCP:深夜2時のシステムダウンを防ぐ!トランザクションとコネクション管理の極意

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

2時、コネクションタイムアウトという名の悪夢

電話が鳴り止まない。目をこすりながらログを確認すると、何千行ものエラーが:HikariPool-1 - Connection is not available。本番環境が完全に停止した瞬間です。これは、データベースが50GBに達し、ユーザーアクセスが一晩で300%急増した際に私が実際に経験した恐怖のシナリオです。

原因はMySQLではありません。真の犯人は、Javaコード内での接続とトランザクションの管理方法にあることが多いのです。Spring Bootのデフォルト設定も十分に優れていますが、数百万件のレコードをさばくには、HikariCPを深くチューニングする必要があります。この記事では、この問題を根本的に解決するために私が行った実践的な調整方法を紹介します。

なぜコネクションプールがそれほど重要なのか?

アプリケーションがクエリを実行するたびに、MySQLとのTCPハンドシェイクと認証を行う必要があり、これには多くのリソースを消費します。リクエストごとに新しい接続を作成して閉じていては、データベースサーバーのCPUはすぐに100%に達してしまいます。

HikariCPは、待機中のタクシー車両のような役割を果たします。新しい車を呼ぶ代わりに、空いている車を利用するだけです。しかし、車両が少なすぎると客(リクエスト)の待ち時間が長くなり, 多すぎると駐車場(RAM)がオーバーフローします。この「ちょうどいい」数値を見つけることが最適化の鍵となります。

標準的な環境構築

まず、pom.xmlを確認しましょう。パフォーマンスとセキュリティのパッチを適用するため、常に最新のMySQL Connectorを使用することをお勧めします。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

HikariCPの設定:デフォルトを盲信しない

デフォルト設定をそのまま本番環境に適用してはいけません。以下は、中〜大規模なトラフィックがあるシステムで私がapplication.ymlによく使用するパラメータです。

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/my_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    hikari:
      # 同時接続数の上限。2コアのDBなら、まずは20から始めるのが良いでしょう。
      maximum-pool-size: 20
      # 常に最低5つの接続を待機させておく。
      minimum-idle: 5
      # 30秒以内に接続を取得できない場合、アプリをハングさせずにエラーを出す。
      connection-timeout: 30000
      # 1つの接続の生存時間。
      max-lifetime: 1800000
      pool-name: MyHikariPool

  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false # 本番環境ではSQLログを無効にしてIOPSを節約する

重要な注意点: 以前、max-lifetimeを30分に設定したことがありますが、MySQLサーバーのwait_timeoutが10分しかありませんでした。アプリがMySQLによって切断された接続を使い続けようとしたため、大量のBroken pipeエラーが発生しました。max-lifetimeは必ずMySQLのタイムアウトより少なくとも2〜3分短く設定してください。

トランザクション管理:小さく保つことが力

@Transactionalの乱用は、データベースのデッドロックやハングアップへの近道です。黄金律は「本当に必要なものだけを囲む」ことです。

Read-Onlyを活用してSELECTを高速化する

読み取り専用のタスクには、@Transactional(readOnly = true)を使用しましょう。Hibernateが変更チェック(dirty checking)をスキップするため、メモリ負荷が軽減され、レスポンス速度が約10〜15%向上します。

@Transactional(readOnly = true)
public List<Product> getAllProducts() {
    return productRepository.findAll();
}

トランザクション内で外部APIを絶対に呼び出さない

これは典型的なミスです。@Transactional内で5秒かかる決済APIを呼び出すと、その接続は5秒間ロックされます。20人の顧客が同時に決済すると、プール全体が枯渇します。まずAPIを呼び出し、その後にトランザクションを開始して結果をDBに保存するようにしましょう。

実環境でのモニタリング

プールがどのように動作しているかを知るには、HikariCPのデバッグログを有効にすべきです。また、MySQL上で直接次のコマンドを使用して、接続状態を定期的に確認してください。

SHOW PROCESSLIST;

もし「Sleep」状態の接続が延々と並んでいる場合、それはトランザクションを長時間保持しすぎているか、コード内で閉じ忘れている兆候です。

この構成を適用した後、私の50GBのシステムは非常に安定して動作しています。ピーク時でもデータベースのCPU使用率は20%に抑えられています。各設定パラメータを理解することは、アプリを高速化するだけでなく、何より夜にぐっすり眠れるようになるために不可欠です。

Share: