CockroachDBマルチリージョン:グローバル分散アクティブアクティブデータベースの5つのコアパターン
数据库
グローバルビジネスのデータレイテンシの痛み
東京のユーザーの注文に3秒の遅延、欧州のユーザーが5秒前の在庫を読み取り、GDPRがデータのEU内保存を要求、リージョン間書き込み競合でデータ損失——ビジネスがグローバルに展開すると、単一リージョンデータベースのレイテンシとコンプライアンス問題が致命的なボトルネックになります。CockroachDBのネイティブマルチリージョンアーキテクチャは、「単一リージョン主従レプリケーション」から「グローバルアクティブアクティブ書き込み」までの完全なソリューションを提供します。
本記事では5つのコアパターンを深掘りし、クラスタデプロイからコンプライアンスパーティショニングまで、本番レベルのグローバル分散アクティブアクティブデータベースを構築します。
コア概念
| 概念 | 説明 | キー設定 |
|---|---|---|
| CockroachDB | ネイティブ分散SQLデータベース、PostgreSQL互換 | 強整合性Serializable分離 |
| マルチリージョン | クロスデータセンターデプロイ、読み取りレイテンシ削減 | --locality起動パラメータ |
| Geoパーティショニング | 地理リージョン別データ分割、ローカル保存 | ALTER PARTITION ... CONFIGURE ZONE |
| Survival Goal | リージョン障害許容戦略 | ZONE/REGIONレベル |
| リージョン設定 | データベース/テーブルレベルのリージョントポロジ | ALTER DATABASE ... SET PRIMARY REGION |
| Lease設定 | Lease Holderのリージョン指定 | lease_preferences |
| レイテンシ最適化 | ローカル読み取りでリージョン間レイテンシ削減 | FOLLOWER READS |
| コンプライアンスパーティショニング | GDPR等のデータローカライゼーション要件を満たす | Regional By Rowテーブル |
問題分析:5つのマルチリージョンチャレンジ
- リージョン間レイテンシが高い:デフォルト3レプリカランダム分布、読み取りが大西洋を往復200ms+
- データ整合性の保証が困難:アクティブアクティブ書き込みでトランザクション競合率が急増
- アクティブアクティブ書き込み競合:同じ行を2つのリージョンで同時変更、Write Intent競合
- コンプライアンスデータローカライゼーション:GDPRがEUユーザーデータのEU内保存を要求
- リージョン障害復旧:単一リージョンダウン時にRPO=0、RTO<30秒をどう保証するか
パターン1:マルチリージョンクラスタデプロイ
-- ノード起動時にlocalityを指定
-- cockroach start --locality=region=us-east-1,zone=us-east-1a ...
-- データベースのプライマリリージョンを設定
ALTER DATABASE global_app SET PRIMARY REGION "us-east-1";
ALTER DATABASE global_app ADD REGION "eu-west-1";
ALTER DATABASE global_app ADD REGION "ap-southeast-1";
-- リージョン設定を確認
SHOW REGIONS FROM DATABASE global_app;
-- Survival Goalの設定(リージョンレベルの障害許容)
ALTER DATABASE global_app SURVIVE REGION FAILURE;
-- またはゾーンレベルの障害許容
ALTER DATABASE global_app SURVIVE ZONE FAILURE;
# 9ノードマルチリージョンクラスタ起動例
# us-east-1: 3ノード
cockroach start \
--locality=region=us-east-1,zone=us-east-1a \
--certs-dir=certs \
--join=node1:26257,node4:26257,node7:26257 \
--cache=.25 --max-sql-memory=.25 \
--background
# eu-west-1: 3ノード
cockroach start \
--locality=region=eu-west-1,zone=eu-west-1a \
--certs-dir=certs \
--join=node1:26257,node4:26257,node7:26257 \
--cache=.25 --max-sql-memory=.25 \
--background
パターン2:Geoパーティショニングとデータローカライゼーション
-- Regional By Rowテーブルの作成(各行がリージョンに関連)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name STRING NOT NULL,
email STRING NOT NULL,
region CRDB_INTERNAL_REGION NOT NULL,
INDEX idx_email (email)
) LOCALITY REGIONAL BY ROW;
-- データ挿入時に対応リージョンに自動ルーティング
INSERT INTO users (name, email, region) VALUES ('Alice', 'alice@eu.com', 'eu-west-1');
INSERT INTO users (name, email, region) VALUES ('Bob', 'bob@us.com', 'us-east-1');
-- クエリは自動的に最寄りのレプリカから読み取り
SELECT * FROM users WHERE id = 'alice-uuid';
-- Globalテーブルの作成(全リージョンに完全なレプリカ)
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name STRING NOT NULL,
price DECIMAL(10,2)
) LOCALITY GLOBAL;
-- Regionalテーブルの作成(プライマリリージョンのみLease保持)
CREATE TABLE config (
key STRING PRIMARY KEY,
value STRING
) LOCALITY REGIONAL IN "us-east-1";
パターン3:Survival Goal高可用性設定
-- ZONE Survival:単一AZ障害許容(デフォルト)
ALTER DATABASE global_app SURVIVE ZONE FAILURE;
-- レプリカ分布:各Range 3レプリカを同じリージョンの異なるAZに配置
-- REGION Survival:単一リージョン障害許容
ALTER DATABASE global_app SURVIVE REGION FAILURE;
-- レプリカ分布:各Range 5レプリカ、3リージョンにまたがる
-- 現在のSurvival Goalを確認
SHOW ZONE CONFIGURATION FROM DATABASE global_app;
-- リージョンフェイルオーバーの検証
-- eu-west-1リージョンのダウンをシミュレート
-- cockroach node drain --host=eu-node1 --host=eu-node2 --host=eu-node3
-- 検証:クラスタが自動的にLeaseを生存リージョンに移行
パターン4:Lease設定と読み取りレイテンシ最適化
-- Lease設定で読み取りリクエストをローカルに
ALTER TABLE users CONFIGURE ZONE USING
num_replicas = 5,
lease_preferences = '[[+region=us-east-1]]';
-- Follower Readsでローカル読み取りを実現
SELECT * FROM orders AS OF SYSTEM TIME INTERVAL '-5s'
WHERE user_id = 123;
package main
import (
"context"
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq"
)
type RegionalDB struct {
primaryDB *sql.DB
followerDBs map[string]*sql.DB
}
func NewRegionalDB(primaryConn string, followerConns map[string]string) (*RegionalDB, error) {
primary, err := sql.Open("postgres", primaryConn)
if err != nil {
return nil, err
}
primary.SetMaxOpenConns(25)
primary.SetMaxIdleConns(10)
followers := make(map[string]*sql.DB)
for region, conn := range followerConns {
db, err := sql.Open("postgres", conn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(15)
db.SetMaxIdleConns(5)
followers[region] = db
}
return &RegionalDB{primaryDB: primary, followerDBs: followers}, nil
}
func (r *RegionalDB) ReadWithFollower(ctx context.Context, region, query string, args ...interface{}) (*sql.Rows, error) {
db, ok := r.followerDBs[region]
if !ok {
db = r.primaryDB
}
followerQuery := fmt.Sprintf("SELECT * FROM (%s) AS OF SYSTEM TIME INTERVAL '-5s'", query)
return db.QueryContext(ctx, followerQuery, args...)
}
func (r *RegionalDB) Write(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
return r.primaryDB.ExecContext(ctx, query, args...)
}
func main() {
rdb, err := NewRegionalDB(
"postgresql://app:pass@us-east-1:26257/global_app?sslmode=require",
map[string]string{
"eu-west-1": "postgresql://app:pass@eu-west-1:26257/global_app?sslmode=require",
"ap-southeast-1": "postgresql://app:pass@ap-southeast-1:26257/global_app?sslmode=require",
},
)
if err != nil {
log.Fatal(err)
}
defer rdb.primaryDB.Close()
ctx := context.Background()
rows, err := rdb.ReadWithFollower(ctx, "eu-west-1",
"SELECT id, name FROM users WHERE region = $1", "eu-west-1")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id, name string
rows.Scan(&id, &name)
fmt.Println(id, name)
}
}
パターン5:コンプライアンスパーティショニングとデータレジデンシ
-- GDPRコンプライアンス:EUユーザーデータはEU内に保存必須
CREATE TABLE eu_user_data (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
personal_info STRING NOT NULL,
region CRDB_INTERNAL_REGION NOT NULL DEFAULT 'eu-west-1',
created_at TIMESTAMPTZ DEFAULT NOW()
) LOCALITY REGIONAL BY ROW;
-- EUデータのEU内残留を強制(制約チェック)
ALTER TABLE eu_user_data ADD CONSTRAINT chk_eu_region
CHECK (region = 'eu-west-1');
-- 中国データコンプライアンス(データ越境制限)
CREATE TABLE cn_user_data (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
personal_info STRING NOT NULL,
region CRDB_INTERNAL_REGION NOT NULL DEFAULT 'ap-southeast-1',
created_at TIMESTAMPTZ DEFAULT NOW()
) LOCALITY REGIONAL BY ROW IN "ap-southeast-1";
-- データレジデンシの検証
SELECT range_id, start_key, end_key,
array_length(replicas, 1) as replica_count
FROM [SHOW RANGES FROM TABLE eu_user_data]
WHERE locality LIKE '%eu-west-1%';
落とし穴ガイド
落とし穴1:❌ Survival Goalの無視でリージョン障害時にデータ損失
-- ❌ ZONE Survivalでリージョン間デプロイ、リージョンダウンでデータ損失
ALTER DATABASE global_app SURVIVE ZONE FAILURE;
-- ✅ リージョン間ではREGION Survivalが必須
ALTER DATABASE global_app SURVIVE REGION FAILURE;
落とし穴2:❌ Globalテーブルの乱用で書き込みレイテンシ増大
-- ❌ すべてのテーブルでLOCALITY GLOBALを使用
CREATE TABLE orders (...) LOCALITY GLOBAL;
-- ✅ ワークロードに応じて選択:読み多用Global、書き多用Regional
CREATE TABLE orders (...) LOCALITY REGIONAL BY ROW;
落とし穴3:❌ Follower Readの時間ウィンドウ不足
-- ❌ AS OF SYSTEM TIME '-1s' は未クローズデータを読み取る可能性
SELECT * FROM orders AS OF SYSTEM TIME '-1s';
-- ✅ 少なくとも5秒のクローズドループウィンドウを待つ
SELECT * FROM orders AS OF SYSTEM TIME '-5s';
落とし穴4:❌ Regional By Rowでregionデフォルト値未設定
-- ❌ 挿入時にregionの指定忘れ
INSERT INTO users (name, email) VALUES ('Test', 't@t.com');
-- ✅ デフォルトregionを設定またはアプリケーションで強制
ALTER TABLE users ALTER COLUMN region SET DEFAULT 'us-east-1';
落とし穴5:❌ リージョン間JOINのパフォーマンス災害
-- ❌ リージョン間テーブルJOIN
SELECT * FROM eu_users JOIN us_orders ON eu_users.id = us_orders.user_id;
-- ✅ リージョン別にクエリ、アプリケーションで集約
SELECT * FROM eu_users WHERE region = 'eu-west-1';
SELECT * FROM us_orders WHERE region = 'us-east-1';
エラートラブルシューティング
| # | エラーメッセージ | 原因 | 解決方法 |
|---|---|---|---|
| 1 | region "xxx" does not exist |
リージョンが未追加 | ALTER DATABASE db ADD REGION "xxx" |
| 2 | no lease holder for range |
リージョン切替中Lease未移行 | 30秒待機または手動Lease転送 |
| 3 | replication factor less than quorum |
REGION Survivalには5レプリカ必要 | 少なくとも3リージョン各2ノードを確保 |
| 4 | cannot set primary region |
既存データの移行が先 | データインポート前にPRIMARY REGIONを設定 |
| 5 | follower read timestamp too close |
Follower Read時間ウィンドウ不足 | -5s以上のオフセットを使用 |
| 6 | transaction retry error |
アクティブアクティブ書き込み競合 | トランザクションリトライラッパーを使用 |
| 7 | insufficient live replicas |
リージョン障害でレプリカ不足 | 他リージョンに十分なレプリカを確保 |
| 8 | locality constraint not satisfiable |
ノードlocalityとZone設定の不一致 | --localityパラメータとZone制約を確認 |
| 9 | cannot add region with data |
データありのテーブルにリージョン追加 | ALTER TABLE ... SET LOCALITYを使用 |
| 10 | zone config violates survival goal |
Zone設定がSurvival Goalと競合 | レプリカ数を増やすかSurvival Goalを下げる |
高度な最適化
1. 自動リージョンルーティングミドルウェア
func RouteQuery(ctx context.Context, rdb *RegionalDB, region string, isRead bool) *sql.DB {
if !isRead {
return rdb.primaryDB
}
if db, ok := rdb.followerDBs[region]; ok {
return db
}
return rdb.primaryDB
}
2. マルチリージョンCDCデータ同期
CREATE CHANGEFEED FOR TABLE users
INTO 'kafka://kafka-eu:9092?topic_prefix=eu_'
WITH updated, resolved='5s', region='eu-west-1';
3. リージョン間トランザクション優先度制御
SET TRANSACTION PRIORITY HIGH;
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
COMMIT;
4. マルチリージョンバックアップ戦略
cockroach backup DATABASE global_app
INTO 's3://backup-bucket/multi-region/?AWS_ENDPOINT=https://s3.eu-west-1.amazonaws.com'
WITH revision_history, encryption_passphrase='xxx';
比較分析
| 次元 | CockroachDB | TiDB | Spanner | YugabyteDB |
|---|---|---|---|---|
| マルチリージョンネイティブ | ✅ SQLレベル | 限定(Placement Rules) | ✅ SQLレベル | ✅ SQLレベル |
| Geoパーティショニング | Regional By Row | Placement Rules | Schema制約 | Tablespaces |
| Survival Goal | ZONE/REGION | ネイティブ概念なし | 障害許容ドメイン | 優先度 |
| Follower Reads | ✅ ネイティブ | ❌ 非対応 | ✅ Stale Reads | ✅ Stale Reads |
| コンプライアンスレジデンシ | ✅ Regional制約 | 限定 | ✅ | ✅ |
| アクティブアクティブ書き込み | ✅ ネイティブ | ❌ プライマリリージョン書き込み | ✅ | ✅ |
| SQL互換 | PostgreSQL | MySQL | PostgreSQL | PostgreSQL |
| オープンソース | ✅(BSL) | ✅ | ❌ | ✅(Apache 2.0) |
| マネージドサービス | CockroachCloud | TiDB Cloud | Spanner | YugabyteDB Managed |
まとめ:CockroachDBマルチリージョンアーキテクチャの核心は「データはユーザーに従う」です。5つのパターンは段階的に構築:マルチリージョンクラスタデプロイ→Geoパーティショニングデータローカライゼーション→Survival Goal高可用性→Lease設定レイテンシ最適化→コンプライアンスパーティショニングデータレジデンシ。重要原則:書き多用Regional By Row、読み多用Follower Reads、リージョン間は必ずREGION Survival、コンプライアンスはRegional制約。単一リージョンからグローバルアクティブアクティブへ、CockroachDBは運用スクリプトではなくSQL構文でマルチリージョン課題を解決します。
オンラインツール推奨
- JSONフォーマッター:/ja/json/format
- Base64エンコード/デコード:/ja/encode/base64
- cURLからコード:/ja/dev/curl-to-code
- SQLフォーマッター:/ja/dev/sql-format
ブラウザローカルツールを無料で試す →
#CockroachDB多区域#多活数据库#全球分布式#Geo分区#2026#数据库