1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

はじめに

KINTO Technologies Advent Calendar 2021 - Qiita の12日目の記事です。
Kintoのグローバル展開により、さまざまなクラウドとサービスを触れるチャンスがありました。
その中、Amazon AuroraとAliyun PolarDBの2つのRDBサービスを検証したことがあります。

Amazon Aurora

Amazon Auroraは、MySQL および PostgreSQL と互換性のあるクラウド向けのリレーショナルデータベースであり、従来のエンタープライズデータベースのパフォーマンスと可用性に加え、オープンソースデータベースのシンプルさとコスト効率性も兼ね備えています。Amazon Aurora のストレージシステムは、分散型で耐障害性と自己修復機能を備えており、データベースインスタンスごとに最大 128 TB まで自動的にスケールされます。Amazon Aurora は、最大 15 個の低レイテンシーリードレプリカ、ポイントインタイムリカバリ、Amazon S3 への継続的なバックアップ、3 つのアベイラビリティーゾーン間でのレプリケーションにより、優れたパフォーマンスと可用性を発揮します。

Aliyun PolarDB

Aliyun PolarDBはAmazon Auroraの競合製品として存在しています。

両製品ともフルマネージドで従来の運用時の手間を軽減できるすばらしいサービスだと認識しております。
今回は、インフラではなく、アプリケーション開発側の観点から両製品の利便性と使用する際の注意ポイントと対応事項を考察します。
(弊社サーバ側の実装は大半Spring Bootを使用しているので、Spring Bootを例として対応実装を示します。)

アプリケーション側の観点

AuroraとPolarDBは、両方ともRead-Writeレプリカを自動的にやってくれますが、
アプリケーション側はシームレスで利用できるかどうかを重要視しています。
要は、アプリ側にとって、以下の2点でアプリ側は対応する必要があるのか、どう対応するのかはポイントとなります。

  • ReadとWriteのインスタンスノードの振り分け
  • 複数Readインスタンスノードへの負荷ロードバランシング

その2点について、簡単にAuroraとPolarDBを以下の表で比較しました。

AuroraとPolarDBの比較

Amazon Aurora Aliyun PolarDB
Read-Writeの振り分け アプリ側は対応が必要 シームレス、アプリ側は対応不要
複数Readノードのロードバランシング アプリ側は対応が必要 シームレス、アプリ側は対応不要

ReadノードとWriteノードをまとめて、
Auroraはカスタムエンドポイントを、PolarDBはクラスタエンドポイントを作成できます。

Amazon Auroraの場合

Auroraのカスタムエンドポイントは一定の時間間隔(デフォルト5秒間隔)でメンバーノード向けにDNSを切り替えるだけで、
アプリケーション側はカスタムエンドポイント向けコネクションを作成するだけだと、以下の問題が発生します。
1.Readノード向け、DB書き込み、エラー発生する可能性がある。
2.ラウンドロビンの負荷分散ができず、一定時間間隔内にすべての負荷は1つのノードに集中する。
要は、Auroraは以上の2点でアプリ側にとってシームレスではありません。
以上の機能をアプリ側は実装する必要があります。

Aliyun PolarDB

PolarDBのクラスタエンドポイントはプロキシのような存在で、
アプリケーション側はクラスタポイント向けコネクションを作成したら、
1.検索処理とアップデート処理を自動的に認識され、ReadノードとWriteノードに自動的に振り分けます。
2.ラウンドロビンの負荷分散ができる。
要は、PolarDBは以上の2点でアプリ側にとってシームレスです。

Spring Boot上でAuroraの対応案

それでは、Spring Bootは以上の2点に対してAuroraへの実装対応案を提示します。

案1:個別実装

Spring Boot + Mybatisで一般的に以下のステップでReadとWriteの自動振り分けを実装します。

Datasourceコンフィギュレーション

①application.xml

application.yml
spring:
  datasource:
    master:
      jdbc-url: jdbc:mysql://xxxxmaster.ap-northeast-2.rds.amazonaws.com:3306/test
      username: master
      password: password
      driver-class-name: com.mysql.cj.jdbc.Driver
    slave1:
      jdbc-url: jdbc:mysql://xxxxslave1.ap-northeast-2.rds.amazonaws.com:3306/test
      username: slave1
      password: password
      driver-class-name: com.mysql.cj.jdbc.Driver
    slave2:
      jdbc-url: jdbc:mysql://xxxxslave2.ap-northeast-2.rds.amazonaws.com:3306/test
      username: slave2
      password: password
      driver-class-name: com.mysql.cj.jdbc.Driver

②マルチデータソース設定

DataSourceConfig.java
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.slave1")
    public DataSource slave1DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.slave2")
    public DataSource slave2DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                          @Qualifier("slave1DataSource") DataSource slave1DataSource,
                                          @Qualifier("slave2DataSource") DataSource slave2DataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
        targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource);
        targetDataSources.put(DBTypeEnum.SLAVE2, slave2DataSource);
        MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
        myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
        myRoutingDataSource.setTargetDataSources(targetDataSources);
        return myRoutingDataSource;
    }
}

③Mybatis設定

MyBatisConfig.java
@EnableTransactionManagement
@Configuration
public class MyBatisConfig {

    @Resource(name = "myRoutingDataSource")
    private DataSource myRoutingDataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public PlatformTransactionManager platformTransactionManager() {
        return new DataSourceTransactionManager(myRoutingDataSource);
    }
}

DataSourceルーティング設定

①DataSourceタイプ定義

DBTypeEnum.java
public enum DBTypeEnum {

    MASTER, SLAVE1, SLAVE2;

}

②ThreadLocalにDataSource源を保存し、アクセスするたびにラウンドロビンを行う

DBContextHolder.java
public class DBContextHolder {

    private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();

    private static final AtomicInteger counter = new AtomicInteger(-1);

    public static void set(DBTypeEnum dbType) {
        contextHolder.set(dbType);
    }

    public static DBTypeEnum get() {
        return contextHolder.get();
    }

    public static void master() {
        set(DBTypeEnum.MASTER);
    }

    public static void slave() {
        // ロードバランシングラウンドロビンポリシー
        int index = counter.getAndIncrement() % 2;
        if (counter.get() > 9999) {
            counter.set(-1);
        }
        if (index == 0) {
            set(DBTypeEnum.SLAVE1);
        }else {
            set(DBTypeEnum.SLAVE2);
        }
    }
}

③DataSourceルーティングキーを設定する

MyRoutingDataSource.java
public class MyRoutingDataSource extends AbstractRoutingDataSource {
    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return DBContextHolder.get();
    }
}

④AOPでmasterとslaveノードを切り替える(メソッド名により)

DataSourceAop.java
@Aspect
@Component
public class DataSourceAop {

    @Pointcut("!@annotation(com.example.annotation.Master) " +
            "&& (execution(* com.example.service..*.select*(..)) " +
            "|| execution(* com.example.service..*.get*(..)))")
    public void readPointcut() {

    }

    @Pointcut("@annotation(com.example.annotation.Master) " +
            "|| execution(* com.example.service..*.insert*(..)) " +
            "|| execution(* com.example.service..*.add*(..)) " +
            "|| execution(* com.example.service..*.update*(..)) " +
            "|| execution(* com.example.service..*.edit*(..)) " +
            "|| execution(* com.example.service..*.delete*(..)) " +
            "|| execution(* com.example.service..*.remove*(..))")
    public void writePointcut() {

    }

    @Before("readPointcut()")
    public void read() {
        DBContextHolder.slave();
    }

    @Before("writePointcut()")
    public void write() {
        DBContextHolder.master();
    }
}

⑤強制的にmasterノードへ向かせるアノテーション

Master.java
public @interface Master {
}

Service実装例

MemberService.java
@Service
public class MemberService {

    @Autowired
    private MemberMapper memberMapper;

    @Transactional
    @Override
    public int insert(Member member) {
        return memberMapper.insert(member);
    }

    @Master
    @Override
    public int save(Member member) {
        return memberMapper.insert(member);
    }

    @Override
    public List<Member> selectAll() {
        return memberMapper.selectByExample(new MemberExample());
    }

    @Master
    @Override
    public String getToken(String appId) {
        //強制的にMasterノードから取得
        return null;
    }
}

案2:ShardingSphereを利用する(Sharding-JDBCかSharding-Proxy)

ShardingSphereはDBシャーディングのOSSプロダクトで、Apacheのトッププロジェクトでもある。
(シャーディングだけではなく、Read-Writeの分離もできる)
ShardingSphere
以下の3つのサブプロダクトがある
1.Sharding-JDBC
クライアント側のjavaライブラリ

2.Sharding-Proxy
DBプロキシ

3.Sharding-Sidecar
DB Meshみたいなもの(開発中)

1と2を使用し、それぞれの対応案は以下となります。

案2-1:Sharding-JDBCを使用する

①build.gradleにSharding-JDBCの依存関係を追加する。

build.gradle
    implementation 'io.shardingsphere:sharding-jdbc-spring-boot-starter:3.1.0'
    implementation 'io.shardingsphere:sharding-jdbc-spring-namespace:3.1.0'

②application.yaml

application.yaml
sharding:
  jdbc:
    datasource:
      names: ds0,ds1,ds2
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://xxxxmaster.ap-northeast-2.rds.amazonaws.com:3306/test
        username: master
        password: password#

      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://xxxxslave1.ap-northeast-2.rds.amazonaws.com:3306/test
        username: slave1
        password: password#

      ds2:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://xxxxslave2.ap-northeast-2.rds.amazonaws.com:3306/test
        username: slave2
        password: password#

    config:
      masterslave:
        load-balance-algorithm-type: round_robin
        name: dataSource
        master-data-source-name: ds0
        slave-data-source-names: ds1,ds2

以上だけで、ほかの実装はいつもどおりでRead-Writeの自動分離と負荷ロードバランシング(ラウンドロビン)が実現できます。
簡単でしょう。

案2-2:Sharding-Proxyを使用する

インストールなどの詳細は以下をご参考
Sharding-Proxy

①Sharding-Proxyサーバの設定

config-master_slave.yaml
schemaName: master_slave_db
#
dataSources:
  master_ds:
    url: jdbc:mysql://xxxxmaster.ap-northeast-2.rds.amazonaws.com:3306/test
    username: master
    password: password#
  slave_ds_0:
    url: jdbc:mysql://xxxxslave1.ap-northeast-2.rds.amazonaws.com:3306/test
    username: slave1
    password: password#
  slave_ds_1:
    url: jdbc:mysql://xxxxslave2.ap-northeast-2.rds.amazonaws.com:3306/test?serverTimezone=UTC&useSSL=false
    username: slave2
    password: password#

masterSlaveRule:
  name: ms_ds
  masterDataSourceName: master_ds
  slaveDataSourceNames:
    - slave_ds_0
    - slave_ds_1

②application.yamlはいつもどおりでいい、build.gradleに依存関係の追加が不要

application.yaml
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://xxxxxx:3307/master_slave_db
    username: shardingProxy
    password: shardingProxy

AWSのRDS-ProxyもGAされていますが、Sharding-ProxyのRead-Write自動振り分けと負荷分散ロードバランシング機能がないようです。

まとめ

  • PolarDBはアプリ側にとってシームレスです。
  • Auroraに対して、アプリ側はRead-Write振り分けと負荷分散ロードバランシング機能を実装する必要があります。
  • Auroraに対して、アプリ側は3つの案があります。(個別実装、Sharding-JDBCとSharding-Proxy)
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?