- 1 1. データ層スケーリングの概要と前提
- 2 2. RDS Proxy接続プーリング
- 3 3. テナント別シャーディング戦略
- 4 4. read replicaスケーリング
- 5 5. DynamoDBマルチテナントデータ層設計
- 6 6. データ層のnoisy neighbor対策
- 7 7. 運用監視とコスト最適化
- 8 8. まとめとVol1/Vol2との連携
1. データ層スケーリングの概要と前提

- Vol1=テナント分離(Silo/Pool/Bridge)・セル・課金、Vol2=エンタープライズ認証連携・認可・M2M を扱いました。
- 本Vol3はデータ層のスケーリング(RDS Proxy接続プーリング・シャーディング・リードレプリカ・DynamoDB設計・noisy neighbor対策)に焦点を当てます。テナント分離概念や認証認可は再掲せず、データ層がテナント増に耐える設計に集中します。
1-1. なぜデータ層スケーリングが課題になるか
SaaSアーキテクチャにおいて、テナント数とトラフィックが増加するにつれて、データ層は最初にボトルネックが顕在化するレイヤーのひとつです。
接続枯渇問題
Vol1で解説したPool型テナント分離では、複数テナントが単一のRDSインスタンスを共有します。アプリケーション層にLambda等のサーバーレス関数を採用した構成では、各関数インスタンスが起動のたびに新規DB接続を確立し、終了時に破棄します。同時リクエスト数の増加に比例してDB接続数がRDSのmax_connections上限に到達し、「too many connections」エラーが頻発します。スケールアウトするほどDB接続数が増えるという逆説が生じるため、アプリケーション層の横スケールだけでは解消できず、データ層への専用対策が不可欠となります。
noisy neighbor問題
特定テナントが突発的な高トラフィックを発生させると、同じデータ層を共有する他テナントのパフォーマンス劣化を招くnoisy neighbor問題を引き起こします。リレーショナルDBでは接続競合やCPU過負荷として、DynamoDBでは特定パーティションへの集中アクセス(ホットパーティション)として発現します。テナント数が増加するほど一部テナントのスパイクが他テナントに波及する確率は高まり、SaaSの信頼性・SLA達成に直結する課題となります。
読み取り負荷の集中
レポーティングやダッシュボードなど読み取り集約ワークロードが加わると、単一ライターへのアクセス集中が書き込みレイテンシを圧迫します。読み取り負荷をライターから分離するread replicaの活用が、本番SaaSにおける基本的な対策となります。
課題が顕在化するタイミング
一般的に、テナント数が数十〜100を超えたあたりで接続枯渇とnoisy neighborの問題が本番環境で顕在化し始めます。スタートアップ初期はシンプルな共有DBで問題なくても、テナント数増加とともに設計の見直しが必要な臨界点に達します。本記事で扱うデータ層対策はこの臨界点を先延ばしにしながら、スケールに追従できるアーキテクチャを実現します。
早期にデータ層のスケーリング設計を組み込んでおくことで、後からの大規模リアーキテクチャを回避できます。
1-2. 本記事の守備範囲
本記事はVol1(テナント分離・セル・課金・オンボーディング)・Vol2(エンタープライズ認証連携・認可・M2M)の基盤が整った続編として、データ層の負荷対策に特化して解説します。
| § | テーマ | 概要 |
|---|---|---|
| §2 | RDS Proxy接続プーリング | shared pool / pool-per-tenant・接続ピン留め・IAM認証 |
| §3 | テナント別シャーディング | リレーショナルDB水平分割・シャードキー・リバランス |
| §4 | read replicaスケーリング | Aurora/RDS read replica・ティア別ルーティング |
| §5 | DynamoDBマルチテナント設計 | パーティションキー戦略・write sharding・adaptive capacity |
| §6 | noisy neighbor対策 | 接続上限・専用レプリカ・Silo化判断 |
扱わない範囲(Vol1/Vol2との境界)
以下はVol1/Vol2で確立済みのため、本記事では再掲しません。
- テナント分離モデル(Silo/Pool/Bridge)の概念・選定基準 → Vol1
- セルベースアーキテクチャ・課金・オンボーディング → Vol1
- エンタープライズ認証連携(SAML/OIDC federation)・Verified Permissions(ABAC/RBAC) → Vol2
- M2Mクレデンシャル管理 → Vol2
データ層がテナント増・トラフィック増に耐える設計の核心に絞って解説します。
▶ Vol1: テナント分離・セル・課金・オンボーディング編
2. RDS Proxy接続プーリング

2-1. 接続多重化の仕組み
RDS Proxyはクライアントとデータベースの間に配置されるAWSマネージドの接続プールサービスです。クライアント(Lambda関数・アプリケーションサーバー等)はRDS ProxyのエンドポイントへMySQLまたはPostgreSQLプロトコルで接続します。図2で示すように、RDS Proxy側では受け取った多数のクライアント接続を、DB側ではより少数のDB接続にまとめる多重化(multiplexing)を実施します。クライアント接続数がスパイクしても、Proxy→DBの接続数は一定の範囲内に抑えられます。
多重化により得られる主な効果を以下に示します。
| 効果 | 説明 |
|---|---|
| too many connectionsエラーの削減 | クライアント接続数に関わらずProxy→DBの接続数をDB上限以内に維持 |
| メモリオーバーヘッドの削減 | DBプロセスの接続管理コストが低下し、同じインスタンスサイズでより多くのテナントをサポートできる |
| フェイルオーバーの高速化 | AuroraのフェイルオーバーをProxyが吸収し、クライアントへの影響を最小化 |
| サーバーレスとの親和性 | 短命なLambda関数が起動ごとに接続を作成しても、Proxyが接続を再利用して実効接続数の増加を抑制 |
対応サービスと東京リージョン
RDS ProxyはAurora(MySQL/PostgreSQL互換)・RDS(MySQL/PostgreSQL/SQL Server)に対応し、東京リージョン(ap-northeast-1)で長期GA提供中です。
- Auroraクラスター: ライターエンドポイントまたはReaderエンドポイントをターゲットに設定
- RDSインスタンス: 個別エンドポイントをターゲットに設定
RDS Proxy作成時の主要設定項目
エンジン互換性: MySQL / PostgreSQL
ターゲット : 対象RDS/Auroraクラスター
IAM認証 : 有効/無効 (詳細は§2-4)
Secrets Manager : DB接続資格情報を登録
VPC : RDS/Auroraと同一VPC内に配置
Secrets ManagerへのアクセスにはProxyが引き受けるIAMロールにsecretsmanager:GetSecretValue権限が必要です。
2-2. pool-per-tenant vs shared pool
RDS Proxyによる接続プールには2つの基本モードがあります。
shared pool(基本モード)
全テナントのクライアント接続が単一のRDS Proxyを共有し、Proxy→DBの接続プールも共有されます。RDS Proxyのデフォルト動作で、設定・運用コストが最も低い構成です。
- 設定: 単一RDS Proxyと1つのターゲットグループ
- 適用場面: Pool型マルチテナントアーキテクチャ全般
- 利点: 設定シンプル、全テナントの接続を集約してDB負荷を最小化
- 制約: テナント間でDB接続が共有されるため、接続面の論理隔離はない
pool-per-tenant(テナント別隔離)
テナントごと(またはテナントグループごと)に専用のRDS Proxyまたはターゲットグループを割り当てる構成です。各テナントのProxy→DB接続が分離され、接続面のnoisy neighborリスクを軽減します。
- 設定: Proxy数またはターゲットグループ数がテナント数に比例
- 適用場面: 接続面の強隔離が必要な重要テナント・上位ティア
- 利点: テナント間での接続占有リスクを低減
- 制約: Proxyのプロビジョニング・管理コストがテナント数に比例して増大
| 比較項目 | shared pool | pool-per-tenant |
|---|---|---|
| 接続隔離 | なし(共有) | あり(テナント別) |
| 設定・運用コスト | 低い | テナント数に比例して増大 |
| noisy neighbor(接続面) | 発生しうる | 軽減 |
| 適用場面 | Pool型・デフォルト | 重要テナントのSilo化と連動 |
実践的な組み合わせ
大多数のテナントをshared poolで扱い、接続レベルの隔離が必要な重要テナントのみpool-per-tenantに昇格させるハイブリッド構成が現実解です。Vol1で解説したPool→Silo移行判断と連動させ、接続面の隔離をSilo化の一要素として位置づけると設計の一貫性が保たれます。
2-3. 接続ピン留め(pinning)の落とし穴(重要な前提)
RDS Proxyの多重化が機能しないケースとして接続ピン留め(pinning)があります。接続プーリング設計において正確な理解が重要です。
ピン留めとは
RDS Proxyがあるクライアントセッションを特定のDB接続に固定し、そのセッションが終了するまで同じDB接続を他のクライアントへ再割り当てできない状態を指します。ピン留めが頻発すると、Proxyを経由していても実質的に接続が専有され、接続多重化の効果が大幅に低下します。
ピン留めが発生する主な操作
以下の操作ではProxyが接続再利用の安全性を保証できないと判断し、ピン留めが発生します。
| 操作 | 説明 |
|---|---|
| テキストサイズ16KB超のステートメント | SQLステートメント全体が16KBを超える場合にピン留め |
| SET文によるセッション変数の変更 | SET sql_mode='...'・SET time_zone='...'等のセッション状態変更 |
| マルチステートメント | セミコロン区切りで複数のSQL文を一度に送信 |
| 一部のストアドプロシージャ | セッション固有のステートを変更するストアドプロシージャ内の操作 |
ピン留め最小化のベストプラクティス
| 対策レイヤー | 具体的な対策 |
|---|---|
| クエリ設計 | 大きなIN句や長いテキストリテラルを分割してステートメントサイズを16KB未満に抑える |
| セッション管理 | SET文はアプリケーション初期化時のみ実行し、リクエストのたびに実行しない |
| ドライバ設定 | マルチステートメント送信を無効化(allowMultiQueries=false等) |
| 監視 | DatabaseConnectionsとClientConnectionsの比をCloudWatchで監視し、多重化効率を定期確認 |
ピン留め状況の監視
DatabaseConnections: Proxy→DBの実際の接続数
ClientConnections : クライアント→Proxyの接続数
ClientConnectionsに対してDatabaseConnectionsが近い値になっている場合、多重化が効いていない(ピン留め多発の疑い)です。両者の比率を定期的に確認し、過剰なピン留めが発生していないかチェックします。ピン留め率が高い場合は、アプリケーションコードのSET文使用箇所や大きなクエリを見直します。
2-4. IAM認証と接続上限
IAM認証の仕組み
RDS ProxyではIAM認証を有効化できます。有効時の接続フローは以下のとおりです。
クライアント → RDS Proxy : IAM DB認証(generate-db-auth-tokenで生成した一時トークンをパスワードとして使用)
RDS Proxy → DB : IAM認証またはSecrets Manager格納の資格情報で接続
クライアントはIAMロールに付与された権限を使ってaws rds generate-db-auth-tokenで一時トークン(15分有効)を取得し、DB接続のパスワードとして使用します。Lambda等のサーバーレス関数はIAMロールでデプロイされるため、IAM DB認証との相性がよく、パスワードのローテーションをSecrets Managerで自動管理できます。
MaxConnectionsPercentによる接続上限制御
MaxConnectionsPercentパラメータは、RDS ProxyがDB(RDS/Aurora)に確立できる最大接続数を、そのDBのmax_connectionsに対するパーセンテージで指定します。
Proxy→DBの最大接続数 = max_connections × MaxConnectionsPercent / 100
例: max_connections=1000、MaxConnectionsPercent=80の場合
– Proxy→DBの最大接続数=800に制限
– 残り20%(200接続)は管理・緊急接続用に確保
pool-per-tenant構成での注意点
pool-per-tenantで複数のProxyが同一DBをターゲットとする場合、各ProxyのMaxConnectionsPercentの合計がDBのmax_connectionsの100%を超えないよう設計する必要があります。超過するとDBへの接続要求が拒否されます。
| 構成 | MaxConnectionsPercent設定例 |
|---|---|
| shared pool(単一Proxy) | 80〜90%を1つのProxyに割当、残りを管理用に確保 |
| pool-per-tenant(N台のProxy) | 各Proxyに 80 / N以下を設定し、合計がDB上限の80%以内に収まるよう調整 |
例: max_connections=1000に4台のProxyが接続する場合、各ProxyにMaxConnectionsPercent=18(合計72%)を設定することで、DB接続の28%(280接続)を管理・緊急用として確保できます。MaxConnectionsPercentは100%に設定せず、管理接続用の余裕を残すことで接続圧迫時でも運用操作を維持できます。
3. テナント別シャーディング戦略

- TRAP③: ナイーブにtenant-idをパーティションキーに使うと、高トラフィックテナントが自パーティションを過負荷にするnoisy neighbor問題が生じます。
- TRAP④: write shardingの正しい実装は
TENANT#1-0/TENANT#1-1形式のシャードサフィックス付与です。 - TRAP⑤: adaptive capacity/split for heatは自動救済機能ですが、良いキー設計の代替にはなりません。
3-1. シャーディングの概念とマルチテナントへの適用
SaaSのPoolモデルで複数テナントが1つのDynamoDBテーブルを共有する場合、テナント数とトラフィックの増加に伴いパーティション設計が最重要課題になります。
DynamoDBパーティションの基本制約
DynamoDBはデータを内部的に複数のパーティションへ分散して格納します。各パーティションには明確なスループット上限があります。
- 読み取り上限: 1パーティションあたり最大3,000 RCU/s
- 書き込み上限: 1パーティションあたり最大1,000 WCU/s
この上限を超えるリクエストはスロットリング(ProvisionedThroughputExceededException)を引き起こします。プロビジョンドスループットをいくら増やしても、同じパーティションにリクエストが集中する限りスロットリングは発生します。
ナイーブなtenant-id設計の落とし穴
# NG例: tenant-idをそのままパーティションキーに使う
item = {
"PK": "TENANT#tenant-1",# パーティションキー
"SK": "ORDER#order-456",# ソートキー
}
この設計では、tenant-1のすべてのデータが同一パーティションに集中します。テナントAが急増トラフィックを発生させると次の問題が連鎖します。
- パーティション書き込みが1,000 WCU/s上限に到達
- テナントAの書き込みがスロットリング
- 同じノードを共有する他テナントまでレイテンシが悪化(noisy neighbor問題)
SaaSのPool設計ではこのnoisy neighbor問題がサービス品質(SLA)の根本的な脅威になります。
シャーディングで解決できる理由
シャーディングの本質は「1つの論理テナントのデータを複数の物理パーティションに分散すること」です。パーティションキーに識別子を付与することで、DynamoDBのデータ配置アルゴリズムを利用して書き込み負荷を自然に分散できます。シャード数を増やすほど1パーティションあたりの書き込み量が減り、上限超過のリスクを低下させられます。
3-2. write sharding: TENANT#ID-N 複合キー設計
write shardingの正しい実装パターンは、テナントIDにシャードサフィックス(-0, -1, …)を付与してパーティションキーを生成することです。
シャードサフィックス付与の設計
import random
SHARD_COUNT_DEFAULT = 1
SHARD_COUNT_HIGH_VOLUME = 4 # 高トラフィックテナントのシャード数
def get_shard_count(tenant_id: str) -> int:
"""テナント設定テーブルまたはSSMからシャード数を取得"""
config = tenant_config_table.get_item(Key={"PK": f"TENANT#{tenant_id}"})
return config["Item"].get("shard_count", SHARD_COUNT_DEFAULT)
def get_partition_key(tenant_id: str) -> str:
shard_count = get_shard_count(tenant_id)
if shard_count <= 1:
# 通常テナント: サフィックスなし
return f"TENANT#{tenant_id}"
# write sharding: ランダムサフィックスで書き込みを分散
suffix = random.randint(0, shard_count - 1)
return f"TENANT#{tenant_id}-{suffix}"
これにより生成されるパーティションキーの例を以下に示します。
| テナント | PK例 | パーティション数 |
|---|---|---|
| 低トラフィック | TENANT#tenant-small | 1 |
| 高トラフィック (シャード0) | TENANT#tenant-big-0 | 4のうち1 |
| 高トラフィック (シャード1) | TENANT#tenant-big-1 | 4のうち1 |
| 高トラフィック (シャード2) | TENANT#tenant-big-2 | 4のうち1 |
| 高トラフィック (シャード3) | TENANT#tenant-big-3 | 4のうち1 |
複合パーティションキーによる負荷分散
別のアプローチとして、tenantIdとエンティティIDを連結した複合パーティションキーも有効です。
# 複合パーティションキーの例
item = {
"PK": "TENANT#tenant-1|TICKET#T-1234", # テナント+エンティティIDの複合キー
"SK": "v0",
"status": "open",
"tenant_id": "tenant-1", # GSI用属性
}
この設計の特徴は次のとおりです。
- エンティティ単位にパーティションが分散 — 書き込み集中を構造的に防ぐ
- テナント横断クエリはGSIで対応 —
GSI-PK: TENANT#tenant-1で同一テナントの全エンティティを効率的に取得 - スケールに比例した自然な分散 — テナントのデータ量が増えるほどパーティションも自動的に分散
シャード数の決め方
シャード数は過剰設定すると読み取りコスト(scatter-gather)が増大するため、ピーク書き込み予測を基に決定します。
| ピーク書き込み予測 | 推奨シャード数 | 備考 |
|---|---|---|
| ~1,000 WCU/s | 1 (シャードなし) | 上限80%を超えたら増設を検討 |
| 1,000〜4,000 WCU/s | 4 | 各シャード最大1,000 WCU/s |
| 4,000〜8,000 WCU/s | 8 | 負荷試験で検証必須 |
| 8,000 WCU/s超 | 16以上 | Silo移行も並行検討 |
パーティション上限の80%を設計目標とし、バーストに備えて20%のマージンを確保します。
3-3. ルーティング層の設計
write shardingを導入すると、書き込み時と読み取り時でルーティングロジックが分かれます。
書き込みルーティング
書き込みはランダムなシャードサフィックスを付与することで自動的に分散されます。
def write_item(tenant_id: str, sk: str, data: dict) -> None:
pk = get_partition_key(tenant_id) # ランダムサフィックス付与
table.put_item(Item={"PK": pk, "SK": sk, **data})
読み取りルーティング: scatter-gather
単一テナントのデータを列挙する場合、全シャードに対して並列でクエリを発行します(scatter-gather)。
import asyncio
import aioboto3
async def query_tenant_items(
tenant_id: str,
shard_count: int,
limit: int = 100,
) -> list:
"""全シャードに並列クエリを投げてマージ (scatter-gather)"""
session = aioboto3.Session()
async with session.resource("dynamodb") as dynamodb:
table = await dynamodb.Table("saas-data")
tasks = []
for i in range(shard_count):
pk = f"TENANT#{tenant_id}-{i}" if shard_count > 1 else f"TENANT#{tenant_id}"
tasks.append(
table.query(
KeyConditionExpression="PK = :pk",
ExpressionAttributeValues={":pk": pk},
Limit=limit,
)
)
results = await asyncio.gather(*tasks)
all_items = [item for res in results for item in res.get("Items", [])]
# 時刻順ソートで一貫した結果を返す
return sorted(all_items, key=lambda x: x["created_at"], reverse=True)
scatter-gatherの考慮点を以下に整理します。
| 観点 | 説明 |
|---|---|
| レイテンシ | N並列クエリの応答時間 = 最も遅いシャードのレイテンシ。直列の1/Nにはならない |
| コスト | RCUはシャード数倍に増加。読み取り頻度が高い場合はGSIで補完 |
| ページネーション | 各シャードのLastEvaluatedKeyを個別に追跡し、全シャードで排他的StartKeyを管理 |
| 整合性 | eventually consistentが原則。strong consistentが必要な場合はConsistentRead=trueをシャード数分並列実行 |
GSIによる読み取り補完
テナント横断の集約クエリにはGSI(Global Secondary Index)を使い、scatter-gatherを回避します。
# GSI設計例: テナントIDを属性として保持し、GSIで集約
item = {
"PK": f"TENANT#{tenant_id}-{shard_suffix}", # シャードキー
"SK": f"ORDER#{order_id}",
"tenant_id": tenant_id,# GSI-1のPK用属性
"created_at": timestamp, # GSI-1のSK用属性
}
# GSI-1の設定
# GSI-PK: tenant_id
# GSI-SK: created_at
# → tenant_id="tenant-1" のクエリでシャードを意識せず全注文が取得できる
書き込み時にGSI用の属性を同時に設定することで、scatter-gatherなしの効率的な読み取りが実現します。
3-4. シャード追加・リバランスの運用
トラフィック増加に伴いシャード数を増やす場合、データ移行を伴うリバランスが必要です。
シャード追加の4ステップ手順
シャード数をNからMに増やす例(4→8シャード)を示します。
- DynamoDB Streams + Lambda で新シャードへデータ複製
# Lambda関数: DynamoDB Streamsのイベントを受け取り
# 旧キー(shard 0-3)のデータを新シャード(4-7)に複製
def handler(event, context):
for record in event["Records"]:
if record["eventName"] in ("INSERT", "MODIFY"):
old_item = record["dynamodb"]["NewImage"]
tenant_id = extract_tenant_id(old_item["PK"]["S"])
new_pk = f"TENANT#{tenant_id}-{random.randint(4, 7)}"
table.put_item(Item={**old_item, "PK": new_pk})
- 二重書き込み期間の設定: アプリケーションが旧シャード(0〜3)と新シャード(4〜7)の両方へ書き込む期間を設ける。
- 読み取り切り替え: 全データ複製完了後、scatter-gather対象を全シャード(0〜7)に変更。
- 旧シャードキーのクリーンアップ: TTL属性の設定またはバッチ削除で旧シャードのデータを削除。
シャード設定のSSM Parameter Store管理
テナントごとにシャード数が異なるため、設定を集中管理します。
import boto3
ssm = boto3.client("ssm")
def get_shard_count(tenant_id: str) -> int:
"""SSM Parameter Storeからシャード数をホットリロード"""
try:
param = ssm.get_parameter(
Name=f"/saas/sharding/{tenant_id}/shard_count"
)
return int(param["Parameter"]["Value"])
except ssm.exceptions.ParameterNotFound:
return 1 # デフォルト: シャードなし
SSM Parameter StoreはParameterStoreキャッシュSDKと組み合わせることで、Lambda/ECSを再デプロイせずにシャード数を動的に変更できます。
Pool→Silo移行との判断基準
シャード増設で対応できない重テナントは、Vol1で解説したPool→Silo移行を検討します。
| 指標 | 目安 | 推奨アクション |
|---|---|---|
| 必要シャード数 | 16超かつスロットリング継続 | Silo移行を検討 |
| コスト比率 | テナント単体でDynamoDBコストの30%超 | Silo移行を検討 |
| 複雑性 | シャード管理運用コストが過大 | Silo移行に切り替え |
まずシャード増設で吸収し、コスト・運用コスト・スロットリングが改善しない場合にSilo移行を判断するのが実践的なアプローチです。
ホットシャードの監視
シャード設計が適切でも、ビジネス的な事情でアクセスが特定シャードに偏ることがあります。CloudWatchカスタムメトリクスでシャード別のスロットリングを継続監視します。
cloudwatch = boto3.client("cloudwatch")
def record_throttle_event(tenant_id: str, shard_id: int) -> None:
cloudwatch.put_metric_data(
Namespace="SaaS/DynamoDB",
MetricData=[{
"MetricName": "ShardThrottleCount",
"Dimensions": [
{"Name": "TenantId", "Value": tenant_id},
{"Name": "ShardId","Value": str(shard_id)},
],
"Value": 1,
"Unit": "Count",
}]
)
このメトリクスに対してCloudWatchアラームを設定し、特定シャードのスロットリングが継続する場合はシャード増設を自動で通知する仕組みを作ると、運用の抜け漏れを防げます。
4. read replicaスケーリング

- Aurora read replicaはwriter + 最大15台・同一クラスターストレージ共有で通常数十ms以下の低ラグを実現
- reader endpointは単一エンドポイントで全replicaへ自動ラウンドロビン分散、カスタムエンドポイントでテナントティア別のreplicaグループを定義可能
- 重テナント/上位ティアを専用replicaへルーティングすることでnoisy neighborを抑制し、読み取り品質を保証
4-1. Auroraクラスター構成とread replica上限
Auroraクラスターは共有分散ストレージを核としたアーキテクチャを採用しています。writerインスタンス(プライマリ)とread replica(Auroraレプリカ)は同一のクラスターストレージを参照し、データは3つのAZにまたがる6ストレージノードへ自動複製されます。
この構造の最大の特性は、read replicaがストレージを共有しているため物理的なデータコピーを必要としない点です。writerがページを更新すると、そのログレコードのみがreplicaへ伝達されます。ストレージ全体のデータ転送が不要であるため、replicaラグが極めて小さく保たれます。
Auroraクラスターの主要エンドポイント:
| エンドポイント種別 | 用途 | 接続先 |
|---|---|---|
| writerエンドポイント | 書き込み・強整合性読み取り | プライマリインスタンス(1台) |
| readerエンドポイント | 通常の読み取り(負荷分散) | 全read replica(ラウンドロビン) |
| インスタンスエンドポイント | 特定インスタンスへの直接接続 | 個別インスタンス |
| カスタムエンドポイント | ティア別ルーティング | 任意のreplicaサブセット |
Aurora vs RDS リードレプリカ上限:
| 項目 | Aurora | RDS(非Aurora) |
|---|---|---|
| 最大read replica数 | 15台 | 最大5台(ソースDBあたり) |
| ストレージ方式 | 共有クラスターストレージ | 独立ストレージ(物理レプリケーション) |
| 通常レプリカラグ | 数十ms以下 | 数百ms〜数秒(書き込み負荷依存) |
| フェイルオーバー | 約30秒以内(自動昇格) | 数分(手動またはDNS切替) |
| Auto Scaling | Aurora Auto Scaling対応 | 非対応 |
テナント数の増加に伴う読み取り需要の急増には、Aurora Auto ScalingでCPU使用率やreplicaラグを指標にread replicaを自動スケールアウト/インします。
resource "aws_appautoscaling_target" "aurora_replicas" {
max_capacity = 15
min_capacity = 2
resource_id = "cluster:${aws_rds_cluster.saas_db.cluster_identifier}"
scalable_dimension = "rds:cluster:ReadReplicaCount"
service_namespace = "rds"
}
resource "aws_appautoscaling_policy" "aurora_replica_cpu_policy" {
name= "saas-aurora-replica-cpu-tracking"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.aurora_replicas.resource_id
scalable_dimension = aws_appautoscaling_target.aurora_replicas.scalable_dimension
service_namespace = aws_appautoscaling_target.aurora_replicas.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "RDSReaderAverageCPUUtilization"
}
target_value = 70.0
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}
4-2. reader endpointとカスタムエンドポイント
Auroraは読み取りトラフィックを制御するための2種類のエンドポイントを提供します。
reader endpoint(クラスター読み取りエンドポイント):
reader endpointは単一のDNS名で全Auroraレplicaへ読み取り接続をラウンドロビン分散します。アプリケーションは1つのエンドポイントを参照するだけで、Aurora側がreplicaの増減に応じて自動的に負荷分散先を調整します。レプリカが追加・削除されてもアプリケーション側の接続設定変更は不要です。
# reader endpoint: 単一エンドポイントで全replicaへ自動ラウンドロビン分散
READER_ENDPOINT = "saas-cluster.cluster-ro-xxxx.ap-northeast-1.rds.amazonaws.com"
conn = psycopg2.connect(
host=READER_ENDPOINT,
port=5432,
dbname="saas_db",
user="app_reader",
password=get_secret("saas/db/reader_password")
)
カスタムエンドポイント:
カスタムエンドポイントは特定のreplicaグループに向けた専用エンドポイントを定義する仕組みです。SaaSマルチテナント環境では、テナントのティアに応じてエンドポイントを分割することで、上位ティアテナントが高スペックreplicaを優先的に利用できます。
Auroraクラスター構成例(テナントティア別エンドポイント分割):
├── writerエンドポイント→ primary(書き込み専用)
├── readerエンドポイント→ 全replica(デフォルト読み取り・ラウンドロビン)
├── カスタム: premium-ep → replica-1, replica-2, replica-3 (大メモリインスタンス)
└── カスタム: standard-ep → replica-4, replica-5 (標準インスタンス)
# Premiumティア専用カスタムエンドポイント(高スペックreplica指定)
resource "aws_rds_cluster_endpoint" "premium_readers" {
cluster_identifier = aws_rds_cluster.saas_db.id
cluster_endpoint_identifier = "premium-readers"
custom_endpoint_type = "READER"
# static_members: 指定replicaのみに接続(意図しないreplica追加を防止)
static_members = [
aws_rds_cluster_instance.replica_1.id,
aws_rds_cluster_instance.replica_2.id,
aws_rds_cluster_instance.replica_3.id,
]
}
# Standardティア用カスタムエンドポイント
resource "aws_rds_cluster_endpoint" "standard_readers" {
cluster_identifier = aws_rds_cluster.saas_db.id
cluster_endpoint_identifier = "standard-readers"
custom_endpoint_type = "READER"
static_members = [
aws_rds_cluster_instance.replica_4.id,
aws_rds_cluster_instance.replica_5.id,
]
}
カスタムエンドポイントには static_members(指定replicaのみ) と excluded_members(指定replica以外すべて) の2方式があります。上位ティア専用グループには static_members で高スペックインスタンスを明示指定する設計が、Auto Scalingで追加されたreplicaが意図せずPremiumグループへ混入するのを防止できます。
4-3. テナント別読み取りルーティング
テナントのティア・トラフィック規模に応じて読み取り接続先のreplicaを動的に切り替えることで、ヘビーテナントの読み取り負荷が他テナントへ波及するnoisy neighborを抑制します。
ルーティングパターン比較:
| パターン | 適用場面 | メリット | 考慮点 |
|---|---|---|---|
| ティア別カスタムエンドポイント | PremiumとStandardを分離したい | QoS保証が確実 | replica台数の按分設計が必要 |
| 重テナント専用replica | 特定のヘビーテナントが突出 | ピンポイント隔離 | テナント数増で管理コスト増 |
| デフォルトreader endpoint | 同質テナントが多いPool型 | 運用シンプル | 重テナント出現時の対策が後手に |
アプリケーション側のルーティング実装例:
READER_ENDPOINTS = {
"premium": "premium-readers.cluster-custom-xxxx.ap-northeast-1.rds.amazonaws.com",
"standard": "standard-readers.cluster-custom-xxxx.ap-northeast-1.rds.amazonaws.com",
"free": "saas-cluster.cluster-ro-xxxx.ap-northeast-1.rds.amazonaws.com",
}
def get_read_conn(tenant_context: dict):
tier = tenant_context.get("tier", "free")
endpoint = READER_ENDPOINTS.get(tier, READER_ENDPOINTS["free"])
return psycopg2.connect(host=endpoint, dbname="saas_db",
user="app_reader", password=get_secret("saas/db/reader_password"))
replicaラグへの対処:
Auroraのreplicaラグは通常数十ms以下と低く、大多数の読み取りユースケースに適合します。ただし次の状況では読み取り先を明示的に制御する必要があります。
- 書き込み直後に同一データを読み取る場合 → writerエンドポイントを使用、またはセッション変数
aurora_replica_read_consistencyで整合性レベルを上げる - 財務・在庫など厳密な整合性が必要な操作 → 常にwriterエンドポイントへ誘導
- セッション変数やトランザクション中のステートに依存する操作 → replicaでは反映されない可能性がある
def query_with_consistency(tenant_id: str, sql: str, require_strong: bool = False):
"""整合性要件に応じてwriter/readerを自動選択する。"""
if require_strong:
conn = get_write_conn()# 財務・決済など強整合性ケースはwriter
else:
tier = get_tenant_tier(tenant_id)
conn = get_read_conn({"tier": tier})
return execute(conn, sql)
CloudWatchメトリクス AuroraReplicaLag をreplicaグループ別に監視し、閾値(例: 100ms)超過時にアラームを発報することで、SLAへの影響を早期検知します。
4-4. RDS read replicaとの比較
Auroraを採用しない構成では、Amazon RDS(非Aurora)のread replicaでも読み取りスケールアウトを実現できます。しかしAuroraとRDS(非Aurora)では根本的なアーキテクチャが異なり、SaaSマルチテナントの要件に与える影響も大きく違います。
アーキテクチャの根本的差異:
Auroraのread replicaはクラスターストレージ共有のため、writerがデータを更新してもreplicaは同一ストレージを参照するだけで最新状態を取得できます。物理的なデータ転送が不要であるため、ラグが通常数十ms以下に収まります。
RDS(非Aurora)のread replicaは物理レプリケーション(MySQLではbinlog、PostgreSQLではWAL)によりプライマリからreplicaへデータを転送します。ネットワーク帯域・ディスクI/O・replicationスレッドの処理速度に依存するため、書き込み負荷が高い環境ではラグが数百ms〜数秒に達します。
詳細比較:
| 比較項目 | Aurora read replica | RDS read replica |
|---|---|---|
| ストレージ | 共有クラスターストレージ(物理複製不要) | 独立ストレージ(binlog/WAL転送) |
| 最大台数 | 15台 | 5台(ソースDBインスタンスあたり) |
| 通常ラグ | 数十ms以下 | 数百ms〜数秒(書き込み負荷依存) |
| クロスリージョン | Aurora Global Database | RDS cross-region read replica |
| フェイルオーバー | 約30秒以内(自動昇格) | 数分(手動またはDNS切替) |
| Auto Scaling | Aurora Auto Scalingで自動化 | 非対応 |
SaaSマルチテナント環境での選択指針:
テナント数が多く読み取り需要の急増が見込まれる本番SaaS環境では、次の理由からAuroraを優先します。
- 最大15台 + Auto Scalingでテナント増に追随したread scaleoutが可能
- カスタムエンドポイントによるティア別QoS保証がネイティブに使える
- フェイルオーバー約30秒以内でマルチテナントのSLAを守りやすい
RDS(非Aurora)を採用するのは、コスト最優先でテナント数が少なく、アプリケーション設計でreplicaラグを許容できる場合に限定されます。
- 「read replicaは最大15台」はAurora固有の仕様です。RDS(非Aurora)のMySQLおよびPostgreSQLはソースDBインスタンスあたり最大5台です。
- AuroraのreplicaはWriterとストレージを共有するため物理データ転送が不要でラグが小さい。RDSはbinlog/WALの物理転送が発生しラグが大きくなりやすい。
- reader endpointは全replicaへの自動ラウンドロビン、カスタムエンドポイントは特定replicaサブセットへの専用ルーティングです。両者の役割を混同しないこと。
5. DynamoDBマルチテナントデータ層設計

DynamoDBはスキーマレスかつサーバーレスで自動スケールする特性から、マルチテナントSaaSのデータ層に採用されるケースが増えています。しかし、マルチテナント特有の偏ったアクセスパターンを考慮しないキー設計はスロットリングとnoisy neighborを招きます。本節では、パーティションキー戦略・write sharding・ホットパーティション上限の仕組み、そしてGlobal Tablesを用いたマルチリージョン設計を解説します。
- DynamoDBの1パーティション上限(3,000 RCU/s・1,000 WCU/s)を超えると即座にスロットリングが発生します。
- ナイーブにtenant-idのみをパーティションキーにすると、大テナントが1パーティションを独占するnoisy neighbor問題が起きます。
- write sharding(サフィックス付与)と複合パーティションキーで書き込みを複数パーティションに分散させます。
- adaptive capacity / split for heatは自動緩和機能ですが、良いキー設計の代替にはなりません。
5-1. パーティションキー戦略(TENANT#プレフィックス・GSI活用)
マルチテナントDynamoDBのキー設計でまず決めるのがパーティションキー(PK)の粒度です。Pool型テナントでは複数テナントが同一テーブルを共有するため、アイテムをテナント単位で確実に識別しつつ書き込みが特定パーティションに偏らない設計が求められます。
TENANT#プレフィックスによる名前空間分離
最も一般的なアプローチは、PKに TENANT#<tenantId> というプレフィックスを付与し、ソートキー(SK)にエンティティ種別と識別子を持たせるシングルテーブル設計です。
PK: TENANT#tenant-001 SK: ORDER#order-9001
PK: TENANT#tenant-001 SK: TICKET#ticket-5555
PK: TENANT#tenant-002 SK: ORDER#order-0042
このプレフィックス規則により、TENANT#tenant-001 のデータが誤って別テナントから参照されるアクセス漏えいリスクを低減できます。IAMポリシーの dynamodb:LeadingKeys 条件キーと組み合わせることで、テナントが自身のPKプレフィックス以外にアクセスできないよう強制できます。
{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:Query", "dynamodb:PutItem"],
"Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/SaaSAppTable",
"Condition": {
"ForAllValues:StringLike": {
"dynamodb:LeadingKeys": ["TENANT#${aws:PrincipalTag/tenantId}*"]
}
}
}
Cognitoのカスタム属性に tenantId を設定し、IdP発行のJWTから aws:PrincipalTag/tenantId を引き継ぐことで、テナントごとに独立したデータアクセス制御をIAMレイヤーで実現できます。
GSIを用いたクロステナントクエリ
管理者ダッシュボードや課金集計では、全テナントを横断してデータを集計したいケースがあります。テナントPKでデータが分散しているため、単純なScanは全パーティションを走査しコスト・レイテンシが増大します。GSI(Global Secondary Index)を活用することでこの課題を緩和できます。
例えば、テナントのティア(tier)をGSI-PKに設定した tier-createdAt-index を作成すると、ENTERPRISE ティアの全テナントを日付範囲で効率よくクエリできます。
メインテーブル:
PK: TENANT#tenant-001 SK: ORDER#2026-06-01#order-9001
GSI-PK: TIER#enterprise GSI-SK: 2026-06-01T09:00:00Z
ただしGSI-PKのカーディナリティが低い(例: tier=3種類)場合、GSI内でもパーティション偏りを生じさせます。GSI-PKにシャードサフィックスを付与してカーディナリティを上げるか、GSIへのアクセスをバッチ用途に限定する設計を推奨します。
ソートキーによる範囲クエリの最適化
begins_with や between などのSK条件式を使うことで、テナント内の特定エンティティ・期間・IDレンジを効率的に取得できます。
KeyConditionExpression: PK = "TENANT#tenant-001"
AND begins_with(SK, "ORDER#2026-06")
ScanIndexForward: false
SKに日付プレフィックス(ORDER#YYYY-MM-)を含めることで、月次・年次の範囲クエリも単一QueryAPIで完結し、Scan不要でコストを抑えられます。
5-2. write shardingとDynamoDB
テナント数が増えると、特定の大テナントが単一パーティションへ書き込みを集中させスロットリングが発生します。write shardingはこの問題を解決するための設計パターンです。
サフィックスによるwrite sharding(TRAP④)
write shardingでは、tenant-idにランダムまたは決定論的なサフィックスを付与し、書き込みを複数パーティションに分散させます。
PK: TENANT#tenant-001-0 # シャード0
PK: TENANT#tenant-001-1 # シャード1
PK: TENANT#tenant-001-2 # シャード2
PK: TENANT#tenant-001-3 # シャード3
書き込み時はアイテムIDのハッシュでシャード番号を決定し、読み取り時は全シャードをParallel Queryで集約します。
import hashlib
import concurrent.futures
import boto3
from boto3.dynamodb.conditions import Key
N_SHARDS = 4
table = boto3.resource('dynamodb').Table('SaaSAppTable')
def write_item(tenant_id, item_id, data):
shard_id = int(hashlib.md5(item_id.encode()).hexdigest(), 16) % N_SHARDS
pk = f"TENANT#{tenant_id}-{shard_id}"
table.put_item(Item={"PK": pk, "SK": f"ORDER#{item_id}", **data})
def read_all_shards(tenant_id):
def query_shard(shard_id):
return table.query(
KeyConditionExpression=Key('PK').eq(f'TENANT#{tenant_id}-{shard_id}')
)['Items']
with concurrent.futures.ThreadPoolExecutor(max_workers=N_SHARDS) as ex:
results = list(ex.map(query_shard, range(N_SHARDS)))
return [item for sublist in results for item in sublist]
設計上の注意: シャード数は後から変更しにくい点に注意してください(既存アイテムのPKハッシュ変更に伴う移行コスト増大のため)。大テナントの最大書き込みスループットを初期設計で見積もり、シャード数を決定してください。1,000 WCU/s以内のテナントであればshardingは不要です。
複合パーティションキーによる自然な負荷分散
サフィックスによるshardingの代わりに、PKをより細かい粒度のエンティティに分解することで自然に負荷を分散させる方法もあります。
# 変更前: 大テナントの全Orderが同一パーティションに集中
PK: TENANT#tenant-001 SK: ORDER#order-9001
# 変更後: テナント×カテゴリでパーティション分散
PK: TENANT#tenant-001|CAT#electronics SK: ORDER#order-9001
PK: TENANT#tenant-001|CAT#apparel SK: ORDER#order-0042
カテゴリ・日付・地域など業務的に意味のある次元をPKに組み込むことで、アクセスパターンと分散が自然に一致したshardingを実現できます。
on-demandキャパシティモードの活用
テナントごとのトラフィックパターンが予測困難な場合は、on-demandキャパシティモードの採用を検討してください。on-demandはアクセス量に応じて自動でキャパシティをスケールし、事前見積もりが不要です。ただしコストがprovisionedより高くなる傾向があるため、安定したトラフィックが見込めるテーブルはprovisionedへの移行を検討します。
5-3. パーティション上限とnoisy neighborの関係
DynamoDBのスケーリング機構を正しく理解することは、マルチテナントアーキテクチャの安定運用に不可欠です。
パーティション上限の正確な理解(TRAP③)
DynamoDBテーブルのデータは内部的に複数パーティションに分散して保存されます。1パーティションが提供できるスループット上限は3,000 RCU/s・1,000 WCU/sです。テーブル全体のスループットはパーティション数に比例して増加しますが、特定パーティションへのアクセスがこの上限を超えると ProvisionedThroughputExceededException によるスロットリングが発生します。
DynamoDBテーブル
┌──────────────────────────────────────────────────────────────┐
│ パーティション A │ パーティション B│
│ TENANT#tenant-001 〜 100 │ TENANT#tenant-101 〜 200 │
│ 上限: 3,000 RCU/s, 1,000 WCU/s │ 上限: 3,000 RCU/s, 1,000 WCU/s│
│ ││
│ ⚠️ tenant-001が急増 │ 問題なし │
│ → 上限超過 → スロットリング││
│ → 同パーティションの ││
│ tenant-002〜100も巻き込まれる│ │
└──────────────────────────────────────────────────────────────┘
ナイーブに TENANT#<tenantId> をPKとすると、大テナントのデータが単一パーティションに集中し、そのテナントが上限を超えた瞬間に同パーティション上の全テナントがスロットリングの影響を受けるのがnoisy neighbor問題の本質です。
adaptive capacityとsplit for heat — 自動対応の限界(TRAP⑤)
DynamoDBにはホットパーティション問題を緩和する自動機能があります。
adaptive capacityは、テーブル全体のプロビジョンドスループットに余裕がある場合に、高トラフィックパーティションへのキャパシティを自動的にブーストします。例えばテーブル全体10,000 WCU/sをプロビジョンし利用が6,000 WCU/sの状態で特定パーティションへ書き込みが急増した場合、adaptive capacityが一時的にそのパーティションへのキャパシティを増やします。
split for heatは、持続的な高スループットが検出されたパーティションを2分割し、各々にキャパシティを配分します。パーティション分割はバックグラウンドで自動実行されます。
- adaptive capacity: テーブル全体に余剰キャパシティがあることが前提。余剰ゼロの場合はスロットリングを回避できません。
- split for heat: 分割はパーティション内のデータ分布を変えません。単一アイテム(1 PK)への集中アクセスは分割しても改善しません。
- これらはあくまで緊急・事後的な緩和機能であり、良いパーティションキー設計の代替にはなりません。設計段階で適切なPK選定を行うことが根本対策です。
CloudWatchによるホットパーティション検知
ThrottledRequests メトリクスがスパイクしたらホットパーティションの兆候です。ConsumedWriteCapacityUnits と ProvisionedWriteCapacityUnits の比率を継続的に監視し、特定テナントの書き込み急増をいち早く検知します。
推奨CloudWatchアラーム:
- ThrottledRequests > 0 (5分間合計)→ アラーム(即時調査)
- ConsumedWCU/ProvisionedWCU > 0.80 (15分平均) → 警告(容量見直し)
- SystemErrors > 0 (5分間合計)→ アラーム(内部障害)
5-4. Global Tablesとマルチリージョンテナント
グローバル展開するSaaSでは、テナントのデータをそのテナントが所在するリージョンに物理的に近い場所に置くことでレイテンシを削減できます。DynamoDB Global Tablesはこれを実現するマネージドなマルチリージョンレプリケーション機能です。
Global Tablesの仕組み
Global Tablesを有効化すると、指定した複数リージョン間でテーブルデータが自動的にレプリケーションされます。各リージョンのレプリカは完全な読み書き権限を持ち、どのリージョンからも書き込めます。同一アイテムへの同時書き込みが競合した場合はlast-writer-wins(タイムスタンプベース)方式で自動解決されます。
東京リージョン (ap-northeast-1) バージニア (us-east-1)
┌───────────────────────────────┐ ┌───────────────────────────────┐
│ Global Table レプリカ│ ◀──────▶ │ Global Table レプリカ│
│ (読み書き可能) │ 双方向│ (読み書き可能) │
└───────────────────────────────┘ レプリカ └───────────────────────────────┘
▲ ▲
│ アジアテナントが書き込み │ 北米テナントが書き込み
レプリケーションの伝播遅延(通常数百ミリ秒〜数秒)があるため、強整合性が必要な読み取りには ConsistentRead: true を設定し、ローカルレプリカから最新データを取得してください。
テナント別リージョン割り当て設計
マルチリージョン構成で重要なのは、テナントが常にホームリージョンへ読み書きする設計です。全リージョンへの書き込み混在はlast-writer-wins競合を頻発させ、データ不整合リスクを高めます。
推奨する設計フロー:
- テナントオンボーディング時にホームリージョン(最寄りリージョン)を決定し、Control Planeのテナントメタデータ(例: Cognitoカスタム属性・Control Plane専用DynamoDBテーブル)に記録します。
- リクエストルーティングはRoute 53のレイテンシルーティングポリシーまたはCloudFront + Lambda@Edgeで、テナントのリクエストを常にホームリージョンのAPIエンドポイントへ誘導します。
- 読み取り整合性は原則ホームリージョンからの結果整合性読み取りとし、金融系操作など厳密な整合性が必要な場面では
ConsistentRead: trueを使います。
データレジデンシーとGlobal Tables
欧州テナント(GDPR対象)が「データをEUリージョン外に保管しない」と要求する場合、Global TablesでEU以外のリージョン(例: 東京・バージニア)にもレプリカを作成するとデータレジデンシー要件に違反します。
対策として:
– EU専用テーブル: EUリージョン(eu-west-1等)のみにレプリカを持つGlobal Tablesグループを別途作成し、EUテナントを分離します。
– Silo化: コンプライアンス要件の厳しいテナントはPool型からSilo型へ移行し、EU専用AWSアカウント・リージョンに完全分離します。
Global Tablesの使用と各国データローカライゼーション法の適合関係は、法務・コンプライアンスチームと連携して設計段階で確認してください。
6. データ層のnoisy neighbor対策

6-1. noisy neighborのデータ層パターン
noisy neighbor問題とは、マルチテナント環境において一部の高負荷テナントが共有リソース(DB接続・DynamoDBパーティション・ストレージIOPS)を占有し、他テナントのレイテンシ増加・スロットリング・接続エラーを引き起こすパターンです。
データ層での主なnoisy neighborパターンを整理します。
| パターン | 発生場所 | 症状 |
|---|---|---|
| 接続枯渇 | RDS/Aurora | 高負荷テナントが接続を占有し、他テナントが「too many connections」エラーに |
| ホットパーティション | DynamoDB | 高トラフィックテナントのパーティションが上限(3,000 RCU/s・1,000 WCU/s)を超過しスロットリング発生 |
| IOPS競合 | RDS gp2 | I/O大テナントがバーストクレジットを消費し、他テナントのディスク性能に影響 |
| レプリカラグ拡大 | Aurora/RDS | 書き込み集中テナントがレプリカラグを拡大させ、読み取り整合性に影響 |
対策は2方向あります。ガバナンス(リミット設定)でテナントごとのリソース上限を設け、設計(データモデル・ストレージ選定)でそもそも競合が起きにくい構造にすることです。6-2〜6-4でそれぞれを具体的に説明します。
noisy neighborの早期検知と対処フロー
noisy neighbor問題は発生してから対処するよりも、閾値アラームで早期に検知し、段階的に対処することが重要です。以下の3段階エスカレーションが実践的です。
| 段階 | 検知シグナル | 対処アクション |
|---|---|---|
| Level 1 (警告) | per-tenantクエリ時間がp99 > 500ms / DynamoDB ThrottledRequests > 0件 | アラート発報・担当者確認 |
| Level 2 (影響あり) | 他テナントのエラー率上昇 / ClientConnections上限80%超 | 問題テナントの接続数制限・一時的なwrite sharding追加 |
| Level 3 (緊急) | 複数テナントのSLA違反 / DB接続枯渇 | 問題テナントをSilo化・即時リソース拡張 |
テナント分類とリソース制限の事前設計
noisy neighbor問題の発生確率を下げるには、テナント登録時から利用量の分類(ティア)を設け、各ティアに対してリソース上限を事前設定しておくことが重要です。
| ティア | 最大同時接続数 | DynamoDB WCU上限 | 対象テナント |
|---|---|---|---|
| Enterprise | 制限なし(専用リソース) | 10,000 WCU/s | 大規模顧客 |
| Professional | 50接続 | 2,000 WCU/s | 中規模顧客 |
| Starter | 10接続 | 200 WCU/s | 小規模顧客 |
6-2. RDS Proxyのリミット(MaxConnectionsPercent per tenant)
RDS Proxyは接続多重化でテナント増に耐えますが、noisy neighborテナントが大量接続を占有するリスクは残ります。これを抑制するのがMaxConnectionsPercentパラメータです。
MaxConnectionsPercentの仕組み
MaxConnectionsPercentは、プロキシがDBに対して確立できる最大接続数を、DBのmax_connectionsに対するパーセンテージで制限するパラメータです。
# 例: DBのmax_connections=1,000の場合
MaxConnectionsPercent=100 → プロキシの最大接続数=1,000
MaxConnectionsPercent=50 → プロキシの最大接続数=500
pool-per-tenant構成(テナントごとに専用Proxyを用意する方式)では、各Proxyに異なるMaxConnectionsPercentを設定することで、テナントNがDB接続の何%まで使えるかを制御できます。
| テナントティア | MaxConnectionsPercent | 意図 |
|---|---|---|
| エンタープライズ | 50% | 接続数の半分を確保 |
| プロフェッショナル | 30% | 中程度の接続枠を保証 |
| スタータープラン | 20% | 最小限の接続枠 |
pinningとnoisy neighborの関係
接続ピン留め(pinning)もnoisy neighborの文脈で重要です。テキストサイズ16KB超のステートメントやSET文・一時テーブル操作など、接続の再利用が安全でない操作ではRDS Proxyがセッションを特定接続にピン留め(pinning)し、セッション終了まで多重化が行われません。
ピン留めが多発するテナントは接続プーリング効果を享受できず、実質的に接続を占有し続ける状態になります。アプリケーション設計でセッション固有のステートを排除し、ピン留めを最小化することがnoisy neighbor対策としても有効です。
MaxConnectionsPercent適用後の監視
MaxConnectionsPercentを設定した後は、以下のCloudWatchメトリクスで設定効果を確認します。
| メトリクス | 確認ポイント |
|---|---|
DatabaseConnections (Proxy→DB接続数) | 上限に張り付いていないか確認。上限到達が頻発する場合はMaxConnectionsPercentを引き上げるか、DBインスタンスサイズを増強 |
ConnectionBorrowTimeout | 接続が払い出せなかった件数。0件が理想。増加時は問題テナントの特定とpool分離を検討 |
ClientConnections/DatabaseConnections比 | 比率が1に近づくとプーリング効果が低下。ピン留め多発の疑い |
また、pool-per-tenant構成では複数ProxyのMaxConnectionsPercent合計がDBのmax_connectionsを超えないよう、テナントオンボーディング時に計算するプロビジョニングチェックを自動化することを推奨します。
6-3. DynamoDBのadaptive capacity(限界を含む)
DynamoDBのadaptive capacityはnoisy neighborに対する自動緩和機能ですが、その限界を正しく把握することが重要です。
adaptive capacityとsplit for heatの動作
| 機能 | 発動条件 | 効果 |
|---|---|---|
| adaptive capacity | 特定パーティションへの高スループット集中 | テーブル内の余剰キャパシティを動的に再配分 |
| split for heat | 持続的な高スループット検出 | ホットパーティションを2分割し各分割にキャパシティを再配分 |
adaptive capacityの限界
adaptive capacityとsplit for heatはあくまで自動緩和機能であり、良いキー設計の代替にはなりません。
- adaptive capacityはパーティション上限(3,000 RCU/s・1,000 WCU/s)を恒常的に超えるアクセスには対応できません
- split for heatは分割後も同じパーティション範囲にアクセスが集中すると、繰り返しスロットリングが発生します
- on-demand課金モードでもパーティション単位の上限制約は同様に存在します
根本対策はデータモデル設計です。write sharding(TENANT#1-0/TENANT#1-1)や複合パーティションキー(TENANT#1|TICKET#4)で高ボリュームテナントのアクセスを複数パーティションに分散させることが必須です。adaptive capacityへの過度な依存はスロットリングと見えにくいレイテンシ悪化を招きます。
- adaptive capacityはスロットリングを「即座に」解消する機能ではなく、数秒〜数十秒かけてキャパシティを再配分します。
- 高トラフィックテナントが他テナントの余剰キャパシティを「借りる」仕組みのため、テーブル全体のプロビジョンドキャパシティが不足している場合は機能しません。
6-4. プロビジョンドIOPS分離
RDS/Auroraのストレージ層では、テナント間のI/O競合もnoisy neighbor問題として顕在化します。
ストレージタイプとIOPS特性
| ストレージタイプ | IOPS特性 | マルチテナント適性 |
|---|---|---|
| gp2 | バースト型(3 IOPS/GB・クレジット消費) | 高負荷テナントがクレジットを消費すると全テナントに影響が及ぶ |
| gp3 | ベースライン固定(3,000 IOPS標準・追加プロビジョニング可) | クレジット依存なし。安定したIOPSを保証 |
| io1/io2 | プロビジョンドIOPS保証 | IOPS保証。最大64,000 IOPS(io2 Block Express: 256,000) |
重テナントのI/O集中が問題になる場合の対策オプションです。
- gp3へのアップグレード: gp2のバーストクレジット依存を解消し、安定したベースラインIOPSを確保します。コスト効率も改善する場合があります。
- io1/io2への移行: SLAが厳しいテナント群に対してIOPS保証ストレージを適用し、予測可能なパフォーマンスを提供します。
- Pool→Silo移行: I/O集中テナントを専用DBインスタンスに移行することで物理的にI/Oを分離します(Vol1のPool/Silo選定ロジックとの接続点)。
- Auroraへの移行: Auroraは分散ストレージ(6コピー)でI/O特性がRDSと異なります。特定テナントのI/Oが他に波及しにくい構造のため、多数の中小テナントが混在する構成に適します。
必要IOPSの見積もり
ストレージタイプを決定する前に、必要IOPSを以下の手順で見積もります。
- 現状把握: CloudWatchの
WriteIOPS・ReadIOPS・BurstBalance(gp2のみ)を1週間分確認 - ピーク算出: 最大テナント数×テナントあたり最大IOPS = 必要最大IOPS
- タイプ選定判断:
BurstBalance が繰り返し 0% に近づく → gp3またはio1/io2に移行
ピーク IOPS > 16,000 → io1/io2(高IOPS特化)を検討
ストレージ料金を抑えつつ安定化 → gp3(コストバランス最良)
IOPS SLA保証が必須 → io2 Block Express
gp3の場合、追加IOPSをプロビジョニングする際のコスト計算: 3,000 IOPS(標準)を超えるIOPS = (追加IOPS) × $0.02/IOPS-month(東京リージョン参考値)。必要IOPSを正確に見積もることでコストの無駄なオーバープロビジョニングを防ぎます。
noisy neighbor対策の全体まとめ
| 対策分類 | 具体手法 | 効果 |
|---|---|---|
| 接続ガバナンス | MaxConnectionsPercent・pool-per-tenant | RDS/Aurora接続の占有防止 |
| DynanoDBキー設計 | write sharding・複合パーティションキー | ホットパーティション構造的解消 |
| ストレージ分離 | gp3/io2への移行・Pool→Silo | IOPS競合の物理的排除 |
| 自動緩和 | adaptive capacity・split for heat | 一時的なスパイクの自動吸収(設計補完) |
7. 運用監視とコスト最適化
7-1. データ層メトリクスの監視
SaaSマルチテナント環境では、テナント単位でのメトリクス可視化が運用品質の鍵です。CloudWatchを中心に監視すべき指標を整理します。
RDS Proxy / RDS / Aurora 監視メトリクス
| メトリクス名 | Namespace | 閾値の目安 |
|---|---|---|
ClientConnections | AWS/RDS | 上限の80%超で警告 |
DatabaseConnections | AWS/RDS | max_connectionsの90%超でアラート |
ConnectionBorrowTimeout | AWS/RDS(Proxy) | 1件以上の発生で要調査 |
ReplicaLag | AWS/RDS | 5秒超でアラート |
FreeableMemory | AWS/RDS | 総メモリの20%未満で警告 |
WriteIOPS / ReadIOPS | AWS/RDS | ストレージ上限の70%超で警告 |
CPUUtilization | AWS/RDS | 80%超でスケールアップ検討 |
DynamoDB 監視メトリクス
| メトリクス名 | 閾値の目安 |
|---|---|
ConsumedReadCapacityUnits | プロビジョンド上限の80%超でスケーリング検討 |
ConsumedWriteCapacityUnits | 同上 |
ThrottledRequests | 0件超で要調査(理想は常時0件) |
SuccessfulRequestLatency | p99 > 10msで調査 |
SystemErrors | 発生時は即アラート |
per-tenant観測の実現方法
CloudWatchのデフォルトメトリクスはインスタンス・テーブル単位であり、テナント単位の可視化は行えません。テナントごとのメトリクスにはCloudWatch Custom Metricsへの書き込みが有効です。
import boto3, time
cloudwatch = boto3.client('cloudwatch', region_name='ap-northeast-1')
cloudwatch.put_metric_data(
Namespace='SaaS/TenantMetrics',
MetricData=[{
'MetricName': 'DBQueryDuration',
'Dimensions': [{'Name': 'TenantId', 'Value': tenant_id}],
'Value': duration_ms,
'Unit': 'Milliseconds',
'Timestamp': time.time()
}]
)
アプリケーション層でテナントIDをディメンションとしてメトリクスを送信することで、CloudWatchダッシュボードにてテナントごとのクエリ時間・エラー率・接続数を可視化できます。CloudWatch Logs Insightsでテナントフィールドを持つログをクエリすることも有効です。
DynamoDB Contributor Insightsによるホットテナント検知
DynamoDB Contributor Insightsを有効化すると、最もアクセスが集中しているパーティションキーをリアルタイムで特定できます。
aws dynamodb update-contributor-insights \
--table-name TenantData \
--contributor-insights-action ENABLE
Contributor Insightsの「Most accessed items」ビューでnoisy neighborテナントのパーティションキーが上位に現れた場合、write shardingへの移行またはSilo化を優先的に検討します。なお、Contributor Insightsはデータ量に応じた追加コストが発生するため、問題解決後は無効化することも選択肢の一つです。
CloudWatch Logs Insightsでは以下のクエリでテナントごとの遅いクエリを特定できます。
fields @timestamp, tenant_id, query_duration_ms
| filter query_duration_ms > 500
| stats count(*) as slow_count, avg(query_duration_ms) as avg_ms by tenant_id
| sort slow_count desc
| limit 20
アラーム設計のベストプラクティス
CloudWatch AlarmではANOMALY_DETECTION_BAND(異常検知バンド)を活用すると、静的閾値では捉えにくいトラフィックパターンの異常を検知できます。SaaSでは曜日・時間帯によるアクセス量の変動が大きいため、静的閾値よりも機械学習ベースの異常検知を優先することを推奨します。
CloudWatch Dashboardの推奨構成
SaaSデータ層の運用ダッシュボードは、以下の3ペインで構成することを推奨します。
| ペイン | 表示内容 | 目的 |
|---|---|---|
| テナントヘルス概覧 | per-tenant p99レイテンシ・エラー率をヒートマップ表示 | noisy neighbor候補テナントを一目で特定 |
| データ層リソース状況 | RDS接続数・DynamoDBスロットリング・レプリカラグをリアルタイム表示 | インフラ負荷を俯瞰 |
| コストトレンド | テナントあたりのDynamoDB消費キャパシティ・RDS I/O費用を時系列 | コスト配賦と利用量の逸脱テナント検出 |
ダッシュボードはAWS CDKでコード管理し、テナント増加に伴うカスタムディメンション追加を自動化することが理想的です。
7-2. コスト最適化
データ層のスケールアウトはパフォーマンス向上と同時にコスト増を招きます。以下の施策を組み合わせてコストとパフォーマンスのバランスをとります。
RDS / Aurora コスト最適化
| 施策 | 効果 | 注意点 |
|---|---|---|
| RDS Proxy導入 | DB接続数削減により小インスタンスでも多テナントに対応 | Proxy自体のコスト(vCPU時間課金)を試算に含める |
| Aurora Serverless v2 | ACU自動スケールでトラフィック変動に追従 | 最小ACU設定分の固定コストが常時発生 |
| リードレプリカのAuto Scaling | 低トラフィック時間帯にレプリカ数を自動削減 | スケールインのクールダウンを適切に設定 |
| Reserved Instances | 1年/3年の予約でオンデマンド比最大72%削減 | 長期安定トラフィックが前提 |
| gp2→gp3への移行 | 同IOPS比で約20%のストレージコスト削減 | 追加IOPSのプロビジョニング量を適切に設定 |
Aurora Application Auto Scalingでリードレプリカ数を自動調整する設定例です。
import boto3
autoscaling = boto3.client('application-autoscaling')
autoscaling.register_scalable_target(
ServiceNamespace='rds',
ResourceId='cluster:my-aurora-cluster',
ScalableDimension='rds:cluster:ReadReplicaCount',
MinCapacity=1,
MaxCapacity=5
)
autoscaling.put_scaling_policy(
ServiceNamespace='rds',
ResourceId='cluster:my-aurora-cluster',
ScalableDimension='rds:cluster:ReadReplicaCount',
PolicyName='aurora-reader-scaling',
PolicyType='TargetTrackingScaling',
TargetTrackingScalingPolicyConfiguration={
'TargetValue': 70.0,
'PredefinedMetricSpecification': {
'PredefinedMetricType': 'RDSReaderAverageCPUUtilization'
},
'ScaleOutCooldown': 300,
'ScaleInCooldown': 300
}
)
スケールアウト時は新規レプリカが読み取りトラフィックを受け入れられるまでのウォームアップ時間(通常数分)を考慮した閾値設定が重要です。
DynamoDB コスト最適化
| 施策 | 効果 | 適用場面 |
|---|---|---|
| on-demand vs provisioned選択 | トラフィック予測精度により削減効果が大きく変わる | 予測可能→provisioned、スパイク多→on-demand |
| Auto Scaling設定 | 需要に合わせてRCU/WCU自動調整 | provisioned課金の前提 |
| DynamoDB Standard-IA | アクセス頻度が低いテーブルのストレージ削減 | 古いテナントデータのアーカイブ用途 |
| TTL活用 | 期限切れデータ自動削除でストレージ節約 | セッション・監査ログ・一時データ |
| Contributor Insights無効化 | 問題解決後の監視コスト削減 | ホットパーティション問題が落ち着いた後 |
on-demand/provisionedの選択基準として、テナントティア別に使い分けることも有効です。エンタープライズティアはトラフィックが予測しやすいためprovisionedで最適化し、スタータープランはon-demandで低コストを維持するというパターンが実践されます。
- 第1優先: アーキテクチャ最適化 — RDS Proxy導入・接続プール適正化でインスタンスサイズを抑制する効果が最も大きい。
- 第2優先: ストレージ最適化 — gp2→gp3移行・Standard-IA・TTL設定は設定変更だけで即効果。
- 第3優先: 予約割引 — 利用パターンが安定したあとにRIやSavings Plansを購入。
8. まとめとVol1/Vol2との連携
8-1. 本記事のまとめ
本記事では、SaaSマルチテナントのデータ層がテナント増加・トラフィック増に耐える設計を解説しました。重要な技術ポイントを整理します。
RDS Proxy接続プーリング
– 多数のクライアント接続を多重化しDB側接続数を削減。「too many connections」とメモリ過負荷を防ぎます
– pool-per-tenantはProxy/ターゲットグループ分離で実現。大多数のテナントはshared poolを基本とします
– pinning(16KB超ステートメント・セッション固有ステート)はプーリング効果を低下させるため、設計で最小化します
テナント別シャーディング
– 高成長テナントを水平シャーディングで分散。シャードキー設計と将来のリバランスを事前に考慮します
– Pool→Silo移行の判断軸(Vol1参照)と連携し、重テナントは専用DBインスタンスへ移行します
read replicaスケーリング
– Aurora: writer + 最大15リードレプリカ、reader endpointで読み取りを負荷分散
– テナント/ティア別ルーティングで重テナントを専用レプリカへ。レプリカラグのSLA設定を忘れずに
DynamoDB設計
– パーティション上限(3,000 RCU/s・1,000 WCU/s)を超えるテナントにはwrite shardingまたは複合パーティションキーが必須
– adaptive capacity/split for heatは自動緩和機能であり、良いキー設計の代替ではありません
noisy neighbor対策
– MaxConnectionsPercentでテナントごとの接続上限を設定し、接続占有リスクを抑制します
– DynamoDB Contributor Insightsでホットテナントを早期に検知します
– 繰り返しnoisy neighborになるテナントはSilo化を検討します
運用監視とコスト最適化
– per-tenant Custom MetricsとContributor Insightsで問題テナントを可視化します
– RDS Proxy導入でインスタンスサイズを抑制し、Aurora Serverless v2でACUを自動スケールします
– DynamoDB: on-demand/provisioned選択とAuto Scalingでコストとパフォーマンスのバランスをとります
– CloudWatch Logs InsightsとANOMALY_DETECTION_BANDの組み合わせで、曜日・時間帯の変動を考慮した動的閾値監視を実現します
設計判断のクイックリファレンス
| 状況 | 優先対策 |
|---|---|
| 接続エラーが頻発する | RDS Proxy導入またはインスタンスサイズ増強 |
| DynamoDBのスロットリングが続く | write sharding/複合キーへの設計変更 |
| 特定テナントがI/Oを独占する | gp3移行またはPool→Silo移行 |
| 読み取りレイテンシが高い | リードレプリカ追加とreader endpointルーティング見直し |
| コストが想定を超えている | RDS Proxy接続削減・DynamoDB provisioned化・TTL設定 |
8-2. シリーズ連携
本シリーズ3巻でSaaS本番運用の主要レイヤーを網羅しました。
| Volume | テーマ | カバー範囲 |
|---|---|---|
| Vol1 | テナント分離基盤 | Silo/Pool/Bridge設計・セルアーキテクチャ・課金・オンボーディング |
| Vol2 | エンタープライズ認証認可 | SAML/OIDC連携・Verified Permissions・M2M |
| Vol3(本記事) | データ層スケーリング | RDS Proxy・シャーディング・リードレプリカ・DynamoDB設計・noisy neighbor |
3つのレイヤー(分離基盤→認証認可→データ層スケーリング)を重ね合わせることで、エンタープライズ対応のSaaS基盤が完成します。データ層のnoisy neighbor問題が発生した際はVol1のPool/Silo移行判断と組み合わせ、根本的な分離強化を検討してください。
各Volumeの判断軸との連携
3巻の内容は独立していますが、実務では以下のように連携して意思決定します。
| 課題 | 参照先 | 組み合わせ方 |
|---|---|---|
| テナントA(重量テナント)のnoisy neighbor対策 | Vol1のPool/Silo選定 + Vol3の§6 | Silo移行でDB分離 + MaxConnectionsPercent/プロビジョンドIOPS設定 |
| 新テナントのオンボーディング自動化 | Vol1のオンボーディング設計 + Vol3のDynamoDB設計 | テナント登録時にパーティションキー設計・ティア別接続上限を自動設定 |
| エンタープライズ顧客のSLA要件充足 | Vol2のティア別認可 + Vol3のread replicaルーティング | 上位ティアのみカスタムエンドポイントで専用レプリカへルーティング |
今後のデータ層拡張ポイント
本記事の設計が定着したあと、さらにスケールさせる場合の検討事項を以下に示します。
- グローバル展開: Amazon Aurora Global Databaseを活用し、ホームリージョンでの書き込みと海外リージョンでの読み取りを分離。テナントごとのデータレジデンシー要件に対応
- 分析基盤の分離: 重いレポート・集計クエリはAmazon Redshift Serverlessまたはゼロ-ETL連携でOLTPから切り離し、運用DBへの影響を排除
- イベント駆動アーキテクチャ: DynamoDB StreamsとLambdaを活用し、テナントデータの変更イベントを下流サービスへファンアウト。データ層スケーリングとイベントバスの統合が次の拡張テーマです
- Vol1: テナント分離(Silo/Pool/Bridge)・セル・課金・オンボーディング
- Vol2: エンタープライズ認証連携・Verified Permissions・M2M
- Vol3(本記事): データ層スケーリング — RDS Proxy・シャーディング・DynamoDB・noisy neighbor対策