Apache ShardingSphereによるシャーディング

## 要約
- Apache ShardingSphere-JDBCを使用し、SQL Serverのシャーディングを行う設定ファイルを紹介します
- 本番運用を見据えて、シャーディング設定情報をZooKeeperやEtcdで管理する方法を紹介します


## Apache ShardingSphereとは何か

Apache ShardingSphereはRDBのシャーディング（水平パーティショニング）を行うためのライブラリです。ShardingSphere-JDBCとShardingSphere-Proxyから構成されています。

### ShardingSphere-JDBC

ShardingSphere-JDBCは、アプリケーションのJDBCドライバ層に組み込むライブラリで、既存のJDBC APIを拡張して水平パーティショニングやレプリケーションを透過的に実現します。依存ライブラリを追加するだけで、シャード間ルーティング、分散トランザクション、フェイルオーバーなどがサポートされ、Spring BootやMyBatisなどのフレームワークを意識せずに導入可能です。いくつかの制約はありますが、多数のRDBMSに対応しています。

### ShardingSphere-Proxy

ShardingSphere-Proxyは、MySQL／PostgreSQLプロトコルをサポートする独立したミドルウェアで、アプリケーションとDBの間に配置することでシャーディング、読み書き分離、マスキングなどをプロキシレベルで透過的に提供します。複数言語やフレームワークから利用可能ですが、対応DBMSが限られている点に注意が必要です。

このほか、ShardingSphereの詳細は、[公式サイト](https://shardingsphere.apache.org/document/5.5.2/en/)を参照してください 

## 実施したこと

今回は、ShardingSphere-JDBCを使用して、簡単なシャーディングルールに基づきデータの格納、検索を行うSpringBootアプリを作成しました。

- 簡単な注文テーブル（注文ID, 顧客ID, 詳細）を顧客IDでシャーディングする
- データの格納先は、顧客ID(INT)の2で割った余りで決定する
- DBはローカルにdockerで2インスタンス用意する

### 構成
- JDK 21
- Spring Boot 3.4.5
- MyBatis Spring Boot Starter 3.0.4
- Apache ShardingSphere-JDBC 5.5.2
- SQL Server

### ShardingSphere-JDBCの設定
シャーディングするデータソースとルールを定義します。
```yaml:sharding-sphere.yaml
# ShardingSphere 設定ファイル

# データソース定義
# dockerで起動した2台のSQL Serverへの接続情報
dataSources:
  ds0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    url: jdbc:sqlserver://localhost:14330;databaseName=db0;encrypt=false;trustServerCertificate=true;sendStringParametersAsUnicode=true
    username: sa
    password: xxxxxxxx
    driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
  ds1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    url: jdbc:sqlserver://localhost:14331;databaseName=db1;encrypt=false;trustServerCertificate=true;sendStringParametersAsUnicode=true
    username: sa
    password: xxxxxxxx
    driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver

# シャーディングルール定義
rules:
  - !SHARDING
    tables:
      # 注文テーブルのシャーディング設定
      t_order:
        actualDataNodes: ds${0..1}.t_order # ds0.t_order, ds1.t_order に展開される
        databaseStrategy:
          standard:
            shardingColumn: customer_id # 顧客IDでシャーディングする
            shardingAlgorithmName: db_mod_sharding_alg
    
    # シャーディングアルゴリズム定義
    shardingAlgorithms:
      db_mod_sharding_alg:
        type: INLINE
        props:
          # customer_id を 2 で割った余りで ds0 または ds1 に振り分ける INLINE 表現
          algorithm-expression: ds${customer_id % 2}
```
### application.properties

ShardingSphere-JDBCを使用するよう、application.propertiesを設定します。
```properties:application.properties
# ShardingSphere Driver設定
spring.datasource.driver-class-name=org.apache.shardingsphere.driver.ShardingSphereDriver
# ShardingSphere 設定ファイルの指定
spring.datasource.url=jdbc:shardingsphere:classpath:sharding-sphere.yaml
```

### 依存関係の設定
ShardingSphere-JDBCへの依存関係をpom.xmlに追加します
```xml:pom.xml
<dependency>
	<groupId>org.apache.shardingsphere</groupId>
	<artifactId>shardingsphere-jdbc</artifactId>
	<version>5.5.2</version>
</dependency>
```
### データベースの準備
dockerで2台のSQL Serverを起動します。

```yaml:docker-compose-sqlserver.yaml
# 2台のSQL Serverを起動する
services:
  sqlserver-0:
    image: mcr.microsoft.com/mssql/server:latest
    container_name: sqlserver-0
    environment:
      ACCEPT_EULA: "Y"
      SA_PASSWORD: "xxxxxxxx"
    # Map host port 14330 to container port 1433
    ports:
      - "14330:1433"

  sqlserver-1:
    image: mcr.microsoft.com/mssql/server:latest
    container_name: sqlserver-1
    environment:
      ACCEPT_EULA: "Y"
      SA_PASSWORD: "xxxxxxxx"
    # Map host port 14331 to container port 1433
    ports:
      - "14331:1433" 
```

立ち上げた2つのSQL Serverインスタンスに同じDDLを使用してテーブルを作成しておきます。
sqlserver-0で以下を実行
```sql
    CREATE DATABASE db0;
    GO

    USE db0;
    GO

    CREATE TABLE t_order (
        order_id BIGINT PRIMARY KEY,
        customer_id INT NOT NULL, -- シャーディングキー
        order_details NVARCHAR(255)
        -- 他に必要なカラム
    );
    GO
```

sqlserver-1では、db1に同じテーブルを作成しておきます。

### SpringBootアプリケーションの開発
通常のSpringBootアプリケーションの開発と同様です。
MyBatisもシャーディングを意識せずに使えます。

今回は /api/ordersに対する注文の登録と{orderId}を指定して注文を検索するAPIを実装しました。

### 動作確認
APIから注文を登録後、sqlcmd等で確認します。偶数の顧客IDからの注文が1台目のSQLServer(ds0)に、奇数顧客IDからの注文が2台目のSQLServer(ds1)に登録されていたら、成功です。
```bash
# 偶数顧客IDの確認
sqlcmd -S localhost,14330 -U sa -P xxxxxxxx -d db0 -Q "SELECT * FROM t_order;"
# 奇数顧客IDの確認
sqlcmd -S localhost,14331 -U sa -P xxxxxxxx -d db1 -Q "SELECT * FROM t_order;"
```

SpringBootアプリ内でのMyBatisからの検索はデータの登録先を意識することは不要です。
## 設定情報のクラスタ管理
ShardingSphere-JDBCでは、データストアやシャーディングルールなどをZooKeeperやEtcdを使用して分散管理するClusterモードを指定することができます。クライアント間でのルールの不整合等を防ぐことができ、本番運用では、Clusterモードの使用が推奨されています。

```yaml:sharding-sphere-cluster.yaml
# モード設定 (Cluster Mode with ZooKeeper)
mode:
  type: Cluster
  repository:
    type: ZooKeeper
    props:
      namespace: shardingsphere
      server-lists: localhost:2181,localhost:2182,localhost:2183 # ZooKeeperを3台構成で起動
      connectionTimeoutMilliseconds: 5000 # e.g., 5 seconds connection timeout
      retryIntervalMilliseconds: 500
      maxRetries: 3
      timeToLiveSeconds: 60
      operationTimeoutMilliseconds: 5000 # e.g., 5 seconds operation timeout
```

ZooKeeperは以下のように構成しました
```yaml:docker-compose-zookeeper.yaml
# ZooKeeper 3-node cluster for ShardingSphere Cluster Mode
version: '3.8'

services:
  zookeeper1:
    image: zookeeper:3.9
    container_name: zookeeper1
    hostname: zookeeper1
    ports:
      - "2181:2181" # Client port
    environment:
      ZOO_MY_ID: 1
      # server.<myid>=<hostname>:<peer port>:<leader election port>
      ZOO_SERVERS: server.1=zookeeper1:2888:3888 server.2=zookeeper2:2888:3888 server.3=zookeeper3:2888:3888
      ZOO_CFG_EXTRA: |
        metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
        metricsProvider.httpPort=7000
        metricsProvider.exportJvmInfo=true
        clientPort=2181
        clientPortAddress=0.0.0.0
    volumes:
      - zk-data1:/data # ZooKeeper data directory
      - zk-datalog1:/datalog # ZooKeeper transaction log directory (optional, can improve performance)
    networks:
      - zookeeper-net

  zookeeper2:
    image: zookeeper:3.9
    container_name: zookeeper2
    hostname: zookeeper2
    ports:
      - "2182:2181" # Client port
    environment:
      ZOO_MY_ID: 2
      ZOO_SERVERS: server.1=zookeeper1:2888:3888 server.2=zookeeper2:2888:3888 server.3=zookeeper3:2888:3888
      ZOO_CFG_EXTRA: |
        metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
        metricsProvider.httpPort=7000
        metricsProvider.exportJvmInfo=true
        clientPort=2181
        clientPortAddress=0.0.0.0
    volumes:
      - zk-data2:/data
      - zk-datalog2:/datalog
    networks:
      - zookeeper-net

  zookeeper3:
    image: zookeeper:3.9
    container_name: zookeeper3
    hostname: zookeeper3
    ports:
      - "2183:2181" # Client port
    environment:
      ZOO_MY_ID: 3
      ZOO_SERVERS: server.1=zookeeper1:2888:3888 server.2=zookeeper2:2888:3888 server.3=zookeeper3:2888:3888
      ZOO_CFG_EXTRA: |
        metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
        metricsProvider.httpPort=7000
        metricsProvider.exportJvmInfo=true
        clientPort=2181
        clientPortAddress=0.0.0.0
    volumes:
      - zk-data3:/data
      - zk-datalog3:/datalog
    networks:
      - zookeeper-net

volumes:
  zk-data1:
    driver: local
  zk-datalog1:
    driver: local
  zk-data2:
    driver: local
  zk-datalog2:
    driver: local
  zk-data3:
    driver: local
  zk-datalog3:
    driver: local

networks:
  zookeeper-net:
    driver: bridge 
```

## まとめ
ShardingSphere-JDBCを使用して、アプリケーションからは意識することなく、設定のみでシャーディングを実施することができました。