AWS Advanced JDBC Wrapperを使ってAmazon Auroraのライター/リーダーの接続を動的に切り替える

AWS Advanced JDBC Wrapperを使ってAmazon Auroraのライター/リーダーの接続を動的に切り替える

AWS Advanced JDBC WrapperのRead Write Splitting Pluginを使ってみた
2026.01.23

はじめに

クラウド事業本部サービス開発室の佐藤です。業務ではクラスメソッドメンバーズポータル(以下CMP)の開発を担当しています。CMPではデータベースにAmazon Aurora MySQLを利用しています。Amazon AuroraにはAuroraレプリカという機能があり、参照(SELECT系)のクエリをオフロードすることにより負荷分散することができます。また、Auto Scaling機能により自動的にレプリカの数を増減させることもできます。

WebアプリケーションなどからAuroraに接続する場合は、基本的にはクラスターエンドポイントを利用して接続します。クラスターエンドポイントはライターが利用されるため、参照系も更新系のクエリもすべてライターに接続されます。負荷分散の観点から、アプリケーションからのSELECT系のクエリはAuroraレプリカを利用するように動的に切り替えられないかというのを調べていたところ、AWS Advanced JDBC Wrapper の Read Write Splitting プラグインを利用することで実現可能ということがわかりました。

この記事では、Spring Boot + Kotlin に AWS Advanced JDBC Wrapper を導入し、Read Write Splittingプラグインをを利用する方法を紹介します。

AWS Advanced JDBC Wrapper とは

AWS Advanced JDBC Wrapper は、AWS公式が提供するJDBCドライバの機能を拡張するラッパードライバーです。MySQL Connector/J や PostgreSQL JDBC Driver などの基盤となるJDBCドライバの上で RDS や Aurora 及び AWSの機能をプラグインを介して利用できる仕組みとなっています。

Read Write Splitting Plugin とは

Read/Write Splitting プラグインは、Javaの Connection#setReadOnly メソッドの呼び出しを通じて、ライターとリーダーを切り替える機能を提供してくれます。

setReadOnly(true) が呼び出されると、プラグインが設定されたリーダーの選択戦略に従ってリーダーに接続し、それ以降のクエリをそのインスタンスへとルーティングするような仕組みになっています。

再び setReadOnly が呼び出されるたびに、引数に渡された真偽値(true/false)に応じて、ライター接続とリーダー接続が自動的に切り替わります。

Spring Bootでは @Transactional アノテーションを用いて、Connection#setReadOnly を制御することができます。

利用可能なプラグイン

AWS Advanced JDBC Wrapper はプラグイン形式で機能を提供しており、必要なプラグインを組み合わせて使用することができます。以下のようにプラグインだけでもかなりの種類がありますが、今回はこの中のRead Write Splitting Pluginプラグインを主に利用します。

プラグイン名 説明
Failover Connection Plugin Aurora および RDS Multi-AZ クラスタのフェイルオーバー機能
Host Monitoring Plugin ホスト接続障害の高速検知
IAM Authentication Plugin IAM 認証によるデータベース接続
AWS Secrets Manager Plugin Secrets Manager からの認証情報取得
Federated Authentication Plugin フェデレーション認証を使用した IAM 接続
Okta Authentication Plugin Okta 統合による認証
Aurora Connection Tracker Plugin Aurora クラスタの接続追跡
Read Write Splitting Plugin リーダー/ライターへの自動ルーティング
Limitless Connection Plugin Aurora Limitless Database のロードバランシング
Custom Endpoint Plugin カスタムエンドポイント対応
Blue/Green Deployment Plugin Blue/Green デプロイメントのクライアント側サポート
Data Cache Plugin SQL クエリ結果のキャッシュ

実装方法

Kotlin + Spring Boot 3.x + Spring Data構成で、実装していきます。

依存関係の追加

まずはAWS Advanced JDBC Wrapper の依存関係を追加します。Aurora MySQLを利用しているので、MySQL Connecter Jのライブラリも一緒に追加する必要があります。今回はSpring Data JPAを利用しているのでそちらも利用しています。

build.gradle.kts
dependencies {
    implementation("software.amazon.jdbc:aws-advanced-jdbc-wrapper:2.6.6")
    runtimeOnly("com.mysql:mysql-connector-j:8.2.0")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
}

アプリケーション設定

AWS JDBC Wrapperにパラメーターを渡すための設定ファイルを追加します。

application.yml
spring:
  datasource:
    aws-jdbc-wrapper:
      url: ${JDBC_CONNECTION_STRING}
      username: ${DB_USERNAME}
      password: ${DB_PASSWORD}
      wrapper-plugins: initialConnection,auroraConnectionTracker,readWriteSplitting,failover2,efm2
      wrapper-dialect: aurora-mysql
      reader-host-selector-strategy: random

wrapper-plugins に利用するプラグインの識別子をカンマ区切りで設定することでそのプラグインを有効化できます。Read Write Splittingを利用するには前提となるプラグインがいくつか必要ですので、initialConnection,auroraConnectionTracker,readWriteSplitting,failover2,efm2 を設定し以下のプラグインを有効化しています。

プラグイン名 説明
Aurora Initial Connection Strategy フェイルオーバー時のDNS更新遅延による旧ノードへの誤接続を防ぐために必要
Failover Connection Plugin Aurora および RDS Multi-AZ クラスタのフェイルオーバー機能
Host Monitoring Plugin ホスト接続障害の高速検知
Aurora Connection Tracker Plugin Aurora クラスタの接続追跡
Read Write Splitting Plugin リーダー/ライターノードへの自動ルーティング

reader-host-selector-strategy というパラメータもありますが、これはAuroraレプリカが複数ある場合にどのインスタンスを選択するかの戦略を設定することができます。以下の4つのパラメーターがあります。
今回は random にしています。

設定値 説明
random 利用可能なリーダーホストからランダムに選択(デフォルト)
roundRobin 利用可能なリーダーホストを順番に選択
leastConnections 最も接続数が少ないリーダーホストを選択
fastestResponse 最も応答が速いリーダーホストを選択

次にYAMLの設定値を読み込むための設定クラスを作成します:

package com.example.demo.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Profile

@ConfigurationProperties(prefix = "spring.datasource.aws-jdbc-wrapper")
data class AwsJdbcWrapperConfig(
    val url: String,
    val username: String,
    val password: String,
    val wrapperPlugins: String,
    val wrapperDialect: String,
    val readerHostSelectorStrategy: String,
    val clusterInstanceHostPattern: String,
)

DataSourceクラスの作成

AWS JDBC Wrapper を使用するデータソースを設定します。以下は設定の全容です。

DataSourceConfig.kt
package com.example.demo.config

import com.zaxxer.hikari.HikariConfig
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.jdbc.datasource.SimpleDriverDataSource
import software.amazon.jdbc.Driver
import software.amazon.jdbc.HikariPooledConnectionProvider
import software.amazon.jdbc.ds.AwsWrapperDataSource
import software.amazon.jdbc.hostlistprovider.RdsHostListProvider
import java.util.Properties
import java.util.concurrent.TimeUnit
import javax.sql.DataSource

@Configuration
class DataSourceConfig(
    private val awsJdbcWrapperConfig: AwsJdbcWrapperConfig,
) {
    @Bean
    fun dataSource(): DataSource {
        // SimpleDriverDataSourceを利用することでSpring BootデフォルトのHikariCPを無効化する
        // AWS JDBC Wrapper内部のコネクションプールを使用するため
        val ds = SimpleDriverDataSource()
        val properties = Properties()

        // JDBC Wrapperの設定
        properties["wrapperPlugins"] = awsJdbcWrapperConfig.wrapperPlugins
        properties["wrapperDialect"] = awsJdbcWrapperConfig.wrapperDialect
        properties["readerHostSelectorStrategy"] = awsJdbcWrapperConfig.readerHostSelectorStrategy
        properties["connectTimeout"] = TimeUnit.SECONDS.toMillis(30).toString()

        ds.setDriverClass(Driver::class.java)
        ds.url = awsJdbcWrapperConfig.url
        ds.username = awsJdbcWrapperConfig.username
        ds.password = awsJdbcWrapperConfig.password
        ds.connectionProperties = properties

        // JDBC Wrapper内部のコネクションプール(HikariCP)を有効にする
        Driver.setCustomConnectionProvider(
            HikariPooledConnectionProvider { _, _ ->
                HikariConfig().apply {
                    maximumPoolSize = 10
                    minimumIdle = 2
                    connectionTimeout = TimeUnit.SECONDS.toMillis(30)
                    idleTimeout = TimeUnit.MINUTES.toMillis(10)
                    maxLifetime = TimeUnit.MINUTES.toMillis(30)
                }
            },
        )

        return ds
    }
}

Spring BootでRead Write Splittingプラグインを利用する場合は、DataSourceの設定をする際にいくつか注意があります。

AWS Advanced JDBC Wrapperのドキュメントにも記載されていますが、

The use of read/write splitting with the annotation @Transactional(readOnly = True) is only recommended for configurations using an internal connection pool. Using the annotation with any other configurations will cause a significant performance degradation.

引用: https://github.com/aws/aws-advanced-jdbc-wrapper/blob/main/docs/using-the-jdbc-driver/using-plugins/UsingTheReadWriteSplittingPlugin.md#limitations-when-using-spring-bootframework

Spring Frameworkの @Transactional(readOnly = True) と一緒に利用するとパフォーマンス問題が起きると記載されています。実際にRead Write Splittingを利用して負荷試験を行ったところ、パフォーマンスが10分の1程度に落ちるほどには悪くなることが確認できました。

この問題の対処法としては、ドキュメントにも記載がある通り、

If you want to use the driver's internal connection pooling, we recommend that you explicitly disable external connection pools (provided by Spring). You need to check the spring.datasource.type property to ensure that any external connection pooling is disabled.

引用: https://github.com/aws/aws-advanced-jdbc-wrapper/blob/main/docs/using-the-jdbc-driver/using-plugins/UsingTheReadWriteSplittingPlugin.md#internal-connection-pools

Spring 自体のコネクションプール機能を無効化し、AWS Advanced JDBC Wrapper側の内部コネクションプール機能を有効化することで対処することができます。

そのため、DataSourceの設定では SimpleDriverDataSource を利用することでSpring Bootのデフォルトのコネクションプールの HikariCP を無効化しています。その後、Driver.setCustomConnectionProviderHikariPooledConnectionProvider を利用して、AWS JDBC Wrapper内部のコネクションプールとしてHikariCPを有効化しています。これにより、Writer/Reader それぞれに対して効率的なコネクション管理が行われるようになります。実際に負荷試験を行ったところパフォーマンスの問題は解消していました。

Service クラスで利用する

Read Write Splittingプラグインが有効化されていると、@Transactional(readonly = true) の場合は、リーダーインスタンスに、 @Transactional の場合はライターに自動的に振り分けてくれるようになります。

package com.example.demo.service

import com.example.demo.repository.UserRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional(readOnly = true) // デフォルトはreadonly
class UserService(
    private val userRepository: UserRepository,
) {
    // リーダーに振り分けられる
    fun findAll(): List<User> {
        return userRepository.findAll()
    }
    // ライターに振り分けられる
    @Transactional
    fun create(name: String, email: String): User {
        val user = User(name = name, email = email)
        return userRepository.save(user)
    }
}

動作確認

ECSにアプリケーションをデプロイして確認してみました。Read Write Splitting が正しく動作しているかをログから確認します。ログレベルを TRACE にすることで ReadWriteSplittingPlugin のログを確認することができます。

application.yml
logging:
  level:
    software.amazon.jdbc: TRACE

CloudWatch Logsを確認すると、ReadWriteSplittingからのログが確認できます。Switched from a writer to a reader host. とある通り、ライターからリーダーにスイッチされていることが確認できました。

CloudWatch Logs
Reader connection set to 'xxx.xxxx.rds.amazonaws.com:3306/
Switched from a writer to a reader host. New reader host: 'xxx.xxxx.ap-northeast-1.rds.amazonaws.com:3306/
Successfully connected to a new reader host: 'xxx.xxxx.ap-northeast-1.rds.amazonaws.com:3306

まとめ

AWS Advanced JDBC Wrapperを利用することで、アプリケーションからAuroraに接続する際にリーダインスタンスとライターインスタンスを動的に切り替えることができました。他にも便利なプラグインがいろいろあるので、適宜必要なプラグインを利用してみましょう。

参考リンク

この記事をシェアする

FacebookHatena blogX

関連記事