午前2時15分、PagerDutyのアラートが鳴り響く。本番環境のMySQLクラスタのCPU使用率は99%に達し、コネクションプールは枯渇、注文キューには12,000件以上のリクエストが滞留している。ordersテーブルはちょうど4,500万レコードを超え、データサイズは85GBを突破したところだ。
インデックスは適切に設計されているにもかかわらず、ディスクI/Oのボトルネックにより、user_idを指定した単純なSELECTクエリにすら3.8秒もかかっている。この状況でRAMを64GBから128GBへと増強(垂直スケーリング / Vertical Scaling)しても、コストが高くつく上に数週間の時間稼ぎにしかならない。システムを長期的に安定稼働させるには、データを複数サーバーに分割するデータベースシャーディング(Database Sharding)が不可欠となる。そこで活躍するのがApache ShardingSphereだ。
1. クイックスタート:5分で立ち上げるShardingSphere-Proxyクラスタ
迅速な障害対応や導入が求められる場面では、ShardingSphere-Proxyが最適な選択肢となる。アプリケーションとMySQLクラスタの間に位置し、標準的なMySQLプロトコル準拠のプロキシレイヤーとして機能する。そのため、Spring BootやGo、Node.jsなどを扱うバックエンドチームは、接続コードを1行も修正する必要がない。
ポート3306で稼働する2つの物理MySQLインスタンスを用意する:
- Node 1:
192.168.1.10:3306(データベースds_0を格納) - Node 2:
192.168.1.11:3306(データベースds_1を格納)
両方のノードでデータベースとテーブルを作成する:
CREATE DATABASE IF NOT EXISTS ds_0;
CREATE DATABASE IF NOT EXISTS ds_1;
-- ds_0 と ds_1 の両ノードでこのDDLを実行:
CREATE TABLE t_order (
order_id BIGINT PRIMARY KEY,
user_id INT NOT NULL,
amount DECIMAL(10, 2),
status VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
config-sharding.yaml ファイルにルーティングルールを設定する:
databaseName: sharding_db
dataSources:
ds_0:
url: jdbc:mysql://192.168.1.10:3306/ds_0?serverTimezone=UTC&useSSL=false
username: root
password: SecretPassword123
ds_1:
url: jdbc:mysql://192.168.1.11:3306/ds_1?serverTimezone=UTC&useSSL=false
username: root
password: SecretPassword123
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: inline_db
shardingAlgorithms:
inline_db:
type: INLINE
props:
algorithm-expression: ds_${user_id % 2}
DockerコンテナでProxyを起動する:
docker run -d --name shardingsphere-proxy \
-p 3307:3307 \
-v $(pwd)/config-sharding.yaml:/opt/shardingsphere-proxy/conf/config-sharding.yaml \
apache/shardingsphere-proxy:latest
この手順が完了したら、アプリケーションの接続先を 127.0.0.1:3307、データベースを sharding_db に向ける。user_id = 101 のレコードを書き込むと、データは自動的に ds_1 へ振り分けられる(101 % 2 = 1)。user_id = 100 の場合は ds_0 へルーティングされる。アプリケーション側からは、あたかも単一のデータベースとやり取りしているように見える。
2. シャーディングとShardingSphereの技術的本質
データ肥大化によってインデックスが効かなくなる理由
レコード数が500万件未満のテーブルであれば軽快に動作する。しかし5,000万件に達すると、B-Treeインデックスツリーが肥大化し、<a href="https://itfromzero.com/ja/database/mysql-ja-database/mysql%e5%86%8d%e8%b5%b7%e5%8b%95%e5%be%8c%e3%81%ae%e6%bf%80%e9%87%8d%e5%95%8f%e9%a1%8c%e3%82%92%e8%a7%a3%e6%b6%88%ef%bc%81buffer-pool%e3%82%92%e5%b8%b8%e3%81%ab%e3%80%8c%e3%83%9b%e3%83%83%e3%83%88.html">innodb_buffer_pool_size</a>の容量を超過してしまう。MySQLはインデックス全体をメモリ(RAM)上に保持できなくなり、クエリごとにランダムディスクI/Oが発生して、I/O waitが40〜60%にまで跳ね上がる。
MySQLのリードレプリカ構成は読み取りクエリの分散にしか寄与しない。書き込み負荷(Writeスループット)やストレージ容量の限界は、依然としてマスターノードに集中する。水平シャーディング(Horizontal Sharding)は、読み取り負荷、書き込み負荷、ストレージ容量のすべてを複数の独立したサーバーに均等分散することで、この課題を根本から解決する。
Apache ShardingSphereにおける2つのアーキテクチャ形態
ShardingSphereエコシステムでは、以下の2つのデプロイモードが提供されている:
- ShardingSphere-JDBC: カスタムJDBCドライバとしてアプリケーションに直接組み込むライブラリ。追加のネットワークホップが発生しないため、オーバーヘッドはほぼゼロ。デメリットとして、JVM(Java、Kotlinなど)環境に依存する点が挙げられる。
- ShardingSphere-Proxy: MySQLクラスタの前に配置される独立したデータベースプロキシ。Go、Python、PHP、C#など、あらゆる言語から標準MySQLポート経由で透過的に接続可能。異なる技術スタックが混在するマイクロサービス環境に最適。
データルーティングアルゴリズムの仕組み
設定例の INLINE ルールでは、ds_${user_id % 2} という数式を使用している。ここで user_id カラムがシャーディングキー(Sharding Key)としての役割を果たす。以下のSQLを受け取った場合:
SELECT * FROM t_order WHERE user_id = 100;
ProxyはクエリをAST(抽象構文木)にパースし、100 % 2 = 0 を即座に計算して、クエリを ds_0 に直接転送する。ノード ds_1 は一切関与せず、CPUサイクルやI/Oリソースを消費することもない。
3. 分散IDの採番と多層シャーディング
SnowflakeアルゴリズムによるID衝突の回避
シャーディング環境では、MySQL標準の <a href="https://itfromzero.com/ja/database/mysql-ja-database/mysql-uuid%e3%81%8bauto_increment%e3%81%8b%ef%bc%9fuuid-v7%e3%81%ab%e3%82%88%e3%82%8b%e6%9c%80%e9%81%a9%e5%8c%96%e6%88%a6%e7%95%a5.html">AUTO_INCREMENT</a> を絶対に使用してはならない。そのまま利用すると、ds_0 と ds_1 の双方が同じ id = 1 を生成してしまい、データの整合性が崩壊する。ShardingSphereには Snowflake アルゴリズムが標準組み込みされており、クラスタ全体で重複のない一意な64ビット整数のIDを自動生成できる。
ルール設定にジェネレータを定義する:
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order_${0..1}
keyGenerateStrategy:
column: order_id
keyGeneratorName: snowflake_gen
keyGenerators:
snowflake_gen:
type: SNOWFLAKE
2層シャーディング戦略(データベース分割 + テーブル分割)
データ規模が数億レコード規模に達した場合は、データベース分割と物理テーブル分割を組み合わせた構成が推奨される。例えば、2つのデータベース(ds_0、ds_1)を用意し、各データベース内に2つの子テーブル(t_order_0、t_order_1)を配置することで、合計4つの物理テーブルに分散させる:
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order_${0..1}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: inline_db
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: inline_tbl
shardingAlgorithms:
inline_db:
type: INLINE
props:
algorithm-expression: ds_${user_id % 2}
inline_tbl:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 2}
4. 本番運用における実践的プラクティス
シャーディングキーを指定しないクエリの大きな代償
以下のクエリを見てみよう:
SELECT * FROM t_order WHERE status = 'COMPLETED';
このクエリには user_id が含まれていないため、Proxyはどのノードにデータがあるかを判別できない。その結果、すべてのノードへクエリを一斉送信(Scatter-Gatherパターン)し、数百万行ものデータをProxyのメモリ上に集約してソートやマージを試みることになる。これにより、Proxyは即座にOut-Of-Memory(OOM)を引き起こす。注文検索APIではシャーディングキーの指定を必須とするなど、設計段階でこのリスクを確実に遮断しておく必要がある。
分散トランザクションへの適切な対処法
複数の物理データベースをまたぐデータ書き込みでは、MySQL本来のローカルACIDトランザクションが効かなくなる。ShardingSphereはXA仕様(2フェーズコミット)やApache Seata経由のSagaパターンをサポートしている。しかし、XAはリソースロックの保持時間が長くなるため、スループットが4〜5分の1に激減するトレードオフがある。本番運用のベストプラクティスは、単一の業務処理における操作が同一のシャーディングキー内に収まるようテーブル設計を工夫することだ。
マスタテーブルにはブロードキャストテーブル(Broadcast Table)を活用
変更頻度は低いものの頻繁に JOIN される provinces(都道府県)や product_categories(商品カテゴリ)などのテーブルは、ブロードキャストテーブル(Broadcast Table)として設定すべきである。ShardingSphereがこれらのテーブルを全ノードに自動レプリケーションするため、注文データとマスタデータの JOIN が同一ノード内で完結し、ネットワーク経由のデータ転送コストを完全に排除できる。

