- Authors
- Name
- セル(Cell)とは何か
- 伝統的なアーキテクチャとの違い
- アーキテクチャパターンの比較
- Bulkheadパターンと障害の分離
- セルルーティング戦略
- Kubernetesベースのセルの実装
- AWSベースのセルの実装
- セル展開戦略 (Canary per Cell)
- 実際のケース
- データパーティショニング
- モニタリングと観察性
- トラブルシューティング
- 失敗事例と回復
- チェックリスト
- 参考資料

##入り
大規模な分散システムを運営してみると、一つの不便な真実と向き合うことになる。いくら洗練された障害対応体系を備えていても、システム全体が共有する単一のコンポーネントに障害が発生すると、全体のサービスが中断されるという事実である。 2021年Facebook(現Meta)の6時間全体障害、2023年Microsoft AzureのWAN設定エラーによる広範囲障害、2024年CloudflareのAPI Gateway障害が代表的だ。これらのケースの共通点は、「障害爆発半径(Blast Radius)」がシステム全体に及んだということです。
セルベースのアーキテクチャは、この問題に対する構造的な解決策です。システムを独立したセル(Cell)に分割して、あるセルで障害が発生しても他のセルに影響が及ばないように分離するパターンである。 AWSが独自のインフラストラクチャに適用し、Well-Architected Frameworkに公式ドキュメントとして登録し、Slack、DoorDash、Salesforceなどの企業が本番で検証したアーキテクチャだ。
この記事では、セルベースのアーキテクチャの中心的な原則からBulkheadパターンとの関係、セルルーティング戦略(Consistent Hashing、Partition Key)、KubernetesとAWSベースの実装、セル単位のカナリデプロイ、データの分割、監視と観察性、実際の運用事例とトラブルシューティングまで、運用レベルで総合的に取り上げます。
##セルベースのアーキテクチャコアコンセプト
セル(Cell)とは何か
セルは、サービスの全体的な機能を独立して実行することができる自己完結型の配布単位です。各セルは、独自のコンピューティングリソース、データストア、メッセージキュー、キャッシュを保持し、他のセルと状態を共有しません。 1つのセルが完全に故障しても、残りのセルは正常に動作する。
セルベースのアーキテクチャの重要な属性は次のとおりです。
- 分離性:セル間でコンピューティング、ストレージ、ネットワークリソースを共有しない
- 独立展開(Independent Deployment): 各セルは独立して展開、アップグレード、ロールバックが可能
- 水平拡張(Horizontal Scaling): セルを追加してシステム全体の容量を拡張する
- 障害分離(Fault Isolation): あるセル障害が別のセルに伝播されない
- 容量制限(Capacity Capping): 各セルは固定された最大容量を有する
伝統的なアーキテクチャとの違い
전통적 아키텍처 (공유 인프라):
┌─────────────────────────────────────────────┐
│ Load Balancer │
├─────────────────────────────────────────────┤
│ App Server 1 | App Server 2 | App Server 3│ <-- 공유 컴퓨팅
├─────────────────────────────────────────────┤
│ Shared Database │ <-- 단일 장애점
├─────────────────────────────────────────────┤
│ Shared Cache │ <-- 단일 장애점
└─────────────────────────────────────────────┘
장애 시: 전체 사용자 영향 (Blast Radius = 100%)
셀 기반 아키텍처 (격리된 인프라):
┌──────────────┐
│ Cell Router │ <-- 유일한 공유 컴포넌트 (최소화)
└──────┬───────┘
│
┌────┴────┬────────┬────────┐
▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│Cell 1│ │Cell 2│ │Cell 3│ │Cell 4│
│ App │ │ App │ │ App │ │ App │
│ DB │ │ DB │ │ DB │ │ DB │
│Cache │ │Cache │ │Cache │ │Cache │
└──────┘ └──────┘ └──────┘ └──────┘
장애 시: 해당 셀 사용자만 영향 (Blast Radius = 25%)
アーキテクチャパターンの比較
| 比較項目 | Cell-Based | Multi-Region | Active-Active | Traditional Monolith |
|---|---|---|---|---|
| 障害分離レベルセル単位(5-10%ユーザー) | リージョンユニット(30-50%ユーザー) | リージョンユニット | なし(100%ユーザー) | |
| 実装の複雑さ高い | 中 - 高 | 高い | 低い | |
| インフラコスト中高(セルあたりオーバーヘッド) | 高(リージョン複製) | 非常に高い | 低い | |
| 遅延時間 | Low(セル内部通信) | 可変(リージョン間遅延) | 可変低い | |
| 展開の柔軟性セル単位カナリ可能 | リージョンユニット | リージョンユニット | フルデプロイ |
|データの一貫性セル内の強い一貫性最終的な一貫性競合解決が必要強い一貫性 |拡張方式セルを追加リージョンを追加リージョンを追加垂直拡張| |操作の複雑さ高(Nセル管理)|中高い|低い|
Bulkheadパターンと障害の分離
セルベースのアーキテクチャは、本質的にBulkheadパターンのインフラストラクチャレベルのアプリケーションです。バルクヘッドは船舶の隔壁に由来する概念であり、ある区画に浸水が発生しても隔壁が別の区画への水の流入を遮断して船全体の沈没を防止する。
Bulkhead 適用レベル
Bulkheadパターンは複数のレベルで適用でき、セルベースのアーキテクチャは最高レベルでの適用です。
- スレッドプールレベル: Hystrix/Resilience4jのThread Pool Bulkhead (最も小さい単位)
- プロセスレベル: 機能別プロセスの分離
- サービスレベル: マイクロサービス間の分離
- インフラストラクチャレベル: 個別のVM、クラスタ、VPCに分離
- セルレベル:スタック全体(コンピューティング+ DB +キャッシュ+キュー)を1つの分離単位で囲む(最も大きい単位)
Blast Radius 計算
障害爆発半径は次の式で計算できます。
단일 셀 장애 시 영향받는 사용자 비율 = 1 / N (N = 셀 수)
예시:
- 셀 4개: 장애 시 최대 25% 사용자 영향
- 셀 10개: 장애 시 최대 10% 사용자 영향
- 셀 20개: 장애 시 최대 5% 사용자 영향
ただし、セル数を無限に増やすと、運用複雑度とコストが指数関数的に増加する。一般的に、**セルあたりのユーザー比率5〜10%**は、コストと分離レベルの間の適切なバランス点です。
セルルーティング戦略
セルベースのアーキテクチャで最も重要な設計決定は、どのユーザーをどのセルにルーティングするかです。ルーティングは決定論的でなければならず、同じユーザーは常に同じセルに向かわなければなりません。
Consistent Hashingベースのルーティング
Consistent Hashingは、セルが追加または削除されたときに最小限のキーのみを再配置することを保証します。
# cell_router.py - Consistent Hashing 기반 셀 라우터
import hashlib
from bisect import bisect_right
from typing import Optional
class CellRouter:
"""Consistent Hashing 기반 셀 라우터.
가상 노드(virtual node)를 사용하여 해시 링 위에
셀을 균등하게 분산 배치한다.
"""
def __init__(self, virtual_nodes: int = 150):
self.virtual_nodes = virtual_nodes
self.ring: list[int] = []
self.ring_to_cell: dict[int, str] = {}
self.cells: dict[str, dict] = {}
def _hash(self, key: str) -> int:
"""SHA-256 해시로 키를 정수 값으로 변환한다."""
digest = hashlib.sha256(key.encode()).hexdigest()
return int(digest[:16], 16)
def add_cell(self, cell_id: str, metadata: Optional[dict] = None) -> None:
"""셀을 해시 링에 추가한다.
Args:
cell_id: 셀 식별자 (예: "cell-us-east-001")
metadata: 셀 메타데이터 (endpoint, capacity 등)
"""
self.cells[cell_id] = metadata or {}
for i in range(self.virtual_nodes):
virtual_key = f"{cell_id}:vn{i}"
hash_value = self._hash(virtual_key)
self.ring.append(hash_value)
self.ring_to_cell[hash_value] = cell_id
self.ring.sort()
def remove_cell(self, cell_id: str) -> None:
"""셀을 해시 링에서 제거한다.
제거된 셀의 트래픽은 인접 셀로 자동 재배치된다.
"""
self.ring = [
h for h in self.ring
if self.ring_to_cell.get(h) != cell_id
]
self.ring_to_cell = {
h: c for h, c in self.ring_to_cell.items()
if c != cell_id
}
del self.cells[cell_id]
def route(self, partition_key: str) -> str:
"""파티션 키로 대상 셀을 결정한다.
Args:
partition_key: 라우팅 키 (예: tenant_id, user_id, org_id)
Returns:
대상 셀 ID
"""
if not self.ring:
raise ValueError("해시 링에 셀이 없습니다")
hash_value = self._hash(partition_key)
idx = bisect_right(self.ring, hash_value)
if idx == len(self.ring):
idx = 0
return self.ring_to_cell[self.ring[idx]]
def get_cell_distribution(self) -> dict[str, int]:
"""각 셀이 해시 링에서 차지하는 비율을 반환한다."""
distribution: dict[str, int] = {}
for cell_id in self.ring_to_cell.values():
distribution[cell_id] = distribution.get(cell_id, 0) + 1
return distribution
# 사용 예시
if __name__ == "__main__":
router = CellRouter(virtual_nodes=150)
# 셀 등록
router.add_cell("cell-001", {"region": "us-east-1", "capacity": 10000})
router.add_cell("cell-002", {"region": "us-east-1", "capacity": 10000})
router.add_cell("cell-003", {"region": "us-west-2", "capacity": 10000})
router.add_cell("cell-004", {"region": "eu-west-1", "capacity": 10000})
# 사용자 라우팅
user_ids = ["user-12345", "user-67890", "org-acme", "org-globex"]
for uid in user_ids:
target_cell = router.route(uid)
print(f"{uid} -> {target_cell}")
# 셀 분포 확인
dist = router.get_cell_distribution()
total = sum(dist.values())
for cell_id, count in sorted(dist.items()):
pct = (count / total) * 100
print(f"{cell_id}: {pct:.1f}%")
Partition Key 選択基準
ルーティングに使用するパーティションキーは、ビジネスドメインによって決定されます。
| ドメインパーティションキー | 利点 | 注意事項 |
|---|---|---|
| SaaSマルチテナント | tenant_id / org_id | テナント間のシームレスな分離大型テナントのホットスポットが可能 |
| ソーシャルメディア | user_id | ユーザー固有のデータの地域性を確保するインフルエンサーアカウントの不均衡 |
| イコマースregion + user_id | 地理的遅延最適化 | 地域間の注文処理の複雑さ |
| メッセージングworkspace_id | ワークスペース内通信ローカルリティワークスペースのサイズ偏差 | |
| お支払いmerchant_id | 加盟店単位規制の遵守が容易大規模な加盟店別のセルが必要 |
大規模なテナントの場合は、専用セルを割り当てる「専用セル」戦略を適用する必要があります。
Kubernetesベースのセルの実装
Kubernetes環境でセルを実装する最も一般的な方法は、名前空間ベースの分離またはクラスタベースの分離です。障害分離レベルが高い場合はセルごとに別々のクラスタを選択し、コスト効率が重要な場合は名前空間分離を選択します。
セル展開マニフェスト
# cell-deployment.yaml
# 각 셀은 독립적인 네임스페이스에 배포된다
apiVersion: v1
kind: Namespace
metadata:
name: cell-001
labels:
cell-id: 'cell-001'
region: 'us-east-1'
tier: 'standard'
---
# 셀 전용 리소스 제한 (noisy neighbor 방지)
apiVersion: v1
kind: ResourceQuota
metadata:
name: cell-001-quota
namespace: cell-001
spec:
hard:
requests.cpu: '16'
requests.memory: '32Gi'
limits.cpu: '32'
limits.memory: '64Gi'
pods: '100'
services: '20'
persistentvolumeclaims: '10'
---
# 셀 애플리케이션 배포
apiVersion: apps/v1
kind: Deployment
metadata:
name: cell-app
namespace: cell-001
labels:
app: cell-app
cell-id: 'cell-001'
spec:
replicas: 3
selector:
matchLabels:
app: cell-app
cell-id: 'cell-001'
template:
metadata:
labels:
app: cell-app
cell-id: 'cell-001'
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: cell-app
cell-id: 'cell-001'
containers:
- name: app
image: myregistry/cell-app:v2.4.1
ports:
- containerPort: 8080
env:
- name: CELL_ID
value: 'cell-001'
- name: DB_HOST
value: 'cell-001-db.cell-001.svc.cluster.local'
- name: CACHE_HOST
value: 'cell-001-redis.cell-001.svc.cluster.local'
- name: QUEUE_URL
value: 'https://sqs.us-east-1.amazonaws.com/123456789/cell-001-queue'
resources:
requests:
cpu: '500m'
memory: '1Gi'
limits:
cpu: '1000m'
memory: '2Gi'
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
---
# 셀 전용 데이터베이스 (StatefulSet)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: cell-001-db
namespace: cell-001
spec:
serviceName: cell-001-db
replicas: 3
selector:
matchLabels:
app: cell-db
cell-id: 'cell-001'
template:
metadata:
labels:
app: cell-db
cell-id: 'cell-001'
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: 'cell_001'
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ['ReadWriteOnce']
storageClassName: gp3-encrypted
resources:
requests:
storage: 100Gi
---
# 셀 전용 Redis 캐시
apiVersion: apps/v1
kind: Deployment
metadata:
name: cell-001-redis
namespace: cell-001
spec:
replicas: 1
selector:
matchLabels:
app: cell-redis
cell-id: 'cell-001'
template:
metadata:
labels:
app: cell-redis
cell-id: 'cell-001'
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
args: ['--maxmemory', '2gb', '--maxmemory-policy', 'allkeys-lru']
---
# 셀 간 트래픽 차단을 위한 NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: cell-isolation
namespace: cell-001
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
# 같은 셀 내부 트래픽만 허용
- from:
- namespaceSelector:
matchLabels:
cell-id: 'cell-001'
# Cell Router에서 오는 트래픽 허용
- from:
- namespaceSelector:
matchLabels:
role: 'cell-router'
egress:
# 같은 셀 내부로의 트래픽
- to:
- namespaceSelector:
matchLabels:
cell-id: 'cell-001'
# DNS 허용
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- port: 53
protocol: UDP
# 외부 AWS 서비스 접근 허용 (SQS, S3 등)
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
ports:
- port: 443
protocol: TCP
このマニフェストの中核はNetworkPolicyだ。セル間ネットワークトラフィックを明示的にブロックし、障害伝播をネットワークレベルで分離します。
AWSベースのセルの実装
AWS 環境では、VPC、サブネット、セキュリティグループを活用して、より強力な分離を実現できます。各セルを別々のVPCまたはAWSアカウントに分割すると、IAM境界までの完全な分離が可能になります。
Terraformベースのセルインフラストラクチャ
# modules/cell/main.tf
# 재사용 가능한 셀 인프라 모듈
variable "cell_id" {
description = "셀 고유 식별자"
type = string
}
variable "cell_cidr" {
description = "셀 VPC CIDR 블록"
type = string
}
variable "environment" {
description = "배포 환경"
type = string
default = "production"
}
variable "max_capacity" {
description = "셀 최대 사용자 수"
type = number
default = 10000
}
# 셀 전용 VPC
resource "aws_vpc" "cell" {
cidr_block = var.cell_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "cell-${var.cell_id}-vpc"
CellId = var.cell_id
Environment = var.environment
}
}
# 가용 영역별 프라이빗 서브넷
resource "aws_subnet" "private" {
count = 3
vpc_id = aws_vpc.cell.id
cidr_block = cidrsubnet(var.cell_cidr, 4, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "cell-${var.cell_id}-private-${count.index}"
CellId = var.cell_id
}
}
# 셀 전용 ECS 클러스터
resource "aws_ecs_cluster" "cell" {
name = "cell-${var.cell_id}"
setting {
name = "containerInsights"
value = "enabled"
}
tags = {
CellId = var.cell_id
}
}
# 셀 전용 RDS (Multi-AZ)
resource "aws_db_instance" "cell" {
identifier = "cell-${var.cell_id}-db"
engine = "postgres"
engine_version = "16.4"
instance_class = "db.r6g.xlarge"
allocated_storage = 100
max_allocated_storage = 500
storage_encrypted = true
storage_type = "gp3"
multi_az = true
db_subnet_group_name = aws_db_subnet_group.cell.name
vpc_security_group_ids = [aws_security_group.cell_db.id]
db_name = "cell_${replace(var.cell_id, "-", "_")}"
username = "cell_admin"
password = data.aws_secretsmanager_secret_version.db_password.secret_string
backup_retention_period = 14
deletion_protection = true
tags = {
CellId = var.cell_id
}
}
# 셀 전용 ElastiCache Redis
resource "aws_elasticache_replication_group" "cell" {
replication_group_id = "cell-${var.cell_id}-redis"
description = "Redis cluster for cell ${var.cell_id}"
node_type = "cache.r6g.large"
num_cache_clusters = 2
engine_version = "7.1"
port = 6379
subnet_group_name = aws_elasticache_subnet_group.cell.name
security_group_ids = [aws_security_group.cell_cache.id]
at_rest_encryption_enabled = true
transit_encryption_enabled = true
automatic_failover_enabled = true
tags = {
CellId = var.cell_id
}
}
# 셀 전용 SQS 큐
resource "aws_sqs_queue" "cell" {
name = "cell-${var.cell_id}-events"
visibility_timeout_seconds = 60
message_retention_seconds = 1209600 # 14일
receive_wait_time_seconds = 20
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.cell_dlq.arn
maxReceiveCount = 3
})
tags = {
CellId = var.cell_id
}
}
# 셀 Auto Scaling
resource "aws_appautoscaling_target" "cell_ecs" {
max_capacity = var.max_capacity / 100 # 인스턴스당 100명 기준
min_capacity = 3
resource_id = "service/${aws_ecs_cluster.cell.name}/${aws_ecs_service.cell.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "cell_cpu" {
name = "cell-${var.cell_id}-cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.cell_ecs.resource_id
scalable_dimension = aws_appautoscaling_target.cell_ecs.scalable_dimension
service_namespace = aws_appautoscaling_target.cell_ecs.service_namespace
target_tracking_scaling_policy_configuration {
target_value = 60.0
scale_in_cooldown = 300
scale_out_cooldown = 60
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
}
}
# 출력
output "cell_vpc_id" {
value = aws_vpc.cell.id
}
output "cell_db_endpoint" {
value = aws_db_instance.cell.endpoint
}
output "cell_redis_endpoint" {
value = aws_elasticache_replication_group.cell.primary_endpoint_address
}
output "cell_sqs_url" {
value = aws_sqs_queue.cell.url
}
セルのプロビジョニング
# environments/production/main.tf
# 실제 셀 프로비저닝
module "cell_001" {
source = "../../modules/cell"
cell_id = "001"
cell_cidr = "10.1.0.0/16"
environment = "production"
max_capacity = 10000
}
module "cell_002" {
source = "../../modules/cell"
cell_id = "002"
cell_cidr = "10.2.0.0/16"
environment = "production"
max_capacity = 10000
}
module "cell_003" {
source = "../../modules/cell"
cell_id = "003"
cell_cidr = "10.3.0.0/16"
environment = "production"
max_capacity = 10000
}
セル展開戦略 (Canary per Cell)
セルベースのアーキテクチャの最も強力な利点の1つは、セル単位のカナリ展開です。新しいバージョンを最初に1つのセルにのみ展開し、問題がなければ徐々に別のセルに展開します。障害が発生しても、そのセルのユーザーだけが影響を受けます。
配布パイプライン
# .github/workflows/cell-canary-deploy.yaml
# 셀 단위 카나리 배포 파이프라인
name: Cell Canary Deployment
on:
push:
branches: [main]
env:
IMAGE_TAG: ${{ github.sha }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push container image
run: |
docker build -t myregistry/cell-app:${{ env.IMAGE_TAG }} .
docker push myregistry/cell-app:${{ env.IMAGE_TAG }}
# Phase 1: 카나리 셀에 배포
deploy-canary:
needs: build
runs-on: ubuntu-latest
environment: canary
steps:
- name: Deploy to canary cell (cell-001)
run: |
kubectl set image deployment/cell-app \
app=myregistry/cell-app:${{ env.IMAGE_TAG }} \
-n cell-001
kubectl rollout status deployment/cell-app \
-n cell-001 --timeout=300s
- name: Run smoke tests against canary cell
run: |
./scripts/smoke-test.sh cell-001
- name: Monitor canary metrics (10 minutes)
run: |
./scripts/canary-monitor.sh cell-001 600
# Phase 2: 첫 번째 배치 (30% 셀)
deploy-batch-1:
needs: deploy-canary
runs-on: ubuntu-latest
environment: production-batch-1
strategy:
max-parallel: 2
steps:
- name: Deploy to batch 1 cells
run: |
for CELL in cell-002 cell-003 cell-004; do
kubectl set image deployment/cell-app \
app=myregistry/cell-app:${{ env.IMAGE_TAG }} \
-n $CELL
kubectl rollout status deployment/cell-app \
-n $CELL --timeout=300s
done
# Phase 3: 나머지 전체 셀
deploy-remaining:
needs: deploy-batch-1
runs-on: ubuntu-latest
environment: production-all
steps:
- name: Deploy to all remaining cells
run: |
REMAINING_CELLS=$(kubectl get ns -l cell-id \
--no-headers -o custom-columns=":metadata.name" | \
grep -v -E "cell-001|cell-002|cell-003|cell-004")
for CELL in $REMAINING_CELLS; do
kubectl set image deployment/cell-app \
app=myregistry/cell-app:${{ env.IMAGE_TAG }} \
-n $CELL
kubectl rollout status deployment/cell-app \
-n $CELL --timeout=300s
done
実際のケース
Slackのセルベースのアーキテクチャ
Slackは2022年から大規模なワークスペースベースのセルアーキテクチャに移行しました。パーティションキーはworkspace_idであり、1つのワークスペース内のすべてのチャネル、メッセージ、ファイルが同じセルに格納されます。 Slackのセル切り替え前は、2022年2月のサービス全体の障害がすべてのユーザーに影響を与えましたが、セル切り替え後、個々のセルの障害はそのセルユーザー(全体の約5〜8%)にのみ影響を与えました。
Slackの重要な設計決定事項は次のとおりです。
- VitessベースのMySQLシャーディングをセル単位で構成
- セルルーター(Cell Router)を最小限のロジックのみを含むThin Layerで設計
- 大規模なエンタープライズ顧客(IBM、Amazonなど)に専用セルを割り当てる
- セル間通信が必要な場合(クロスワークスペース検索など)は、非同期メッセージバスを使用
DoorDashのセルアーキテクチャ
DoorDashは2023年に地域ベースのセルアーキテクチャを導入しました。パーティションキーは** geographic_region **であり、米国をいくつかの地理的セルに分割しました。これにより、特定地域のトラフィック爆症(スーパーボールシーズンの特定都市など)が他の地域に影響を与えないように隔離した。
- DynamoDB をセル単位のデータストアとして使用
- Apache Kafkaクラスターもセル単位で分離
- セル単位 Feature Flag による機能ロールアウト制御
- 障害発生時、該当セルのトラフィックを隣接セルにドレインする自動化構築
SalesforceのPodアーキテクチャ
Salesforceは、セルベースのアーキテクチャの先駆者の1つです。 Salesforceでは、セルをPodと呼び、各Podには完全なSalesforceインスタンスが含まれています。数十のポッドが世界中に分散しており、顧客(テナント)は特定のポッドに固定されています。
- テナント別インスタンスURL(例:na1.salesforce.com、eu5.salesforce.com)へのセルルーティング
- ポッド間のデータ移行のためのOrg Migrationツール自体の開発
- ポッドユニットメンテナンスウィンドウとは無関係のリリースサイクル操作
データパーティショニング
セルベースのアーキテクチャで最も要求の厳しい課題は、データパーティショニングです。セルの独立性を維持しながら、クロスセルクエリの要件を満たす必要があります。
パーティショニング戦略
1。セルローカルデータ (Cell-Local Data)
セル内でのみアクセスするデータで、そのセルのデータベースにのみ存在します。例えば、ユーザのメッセージ、注文履歴、セッション情報がこれに該当する。
2。グローバル参照データ (Global Reference Data)
すべてのセルで同じように必要な読み取り専用データです。為替レート情報、商品カタログ、国コード等が該当する。このデータは、グローバルデータストアから各セルに非同期複製します。
3. クロスセル集計データ (Cross-Cell Aggregate Data)
全体システム統計、グローバルダッシュボードなど、すべてのセルのデータを集計しなければならない場合だ。各セルで集計されたメトリックを中央分析プラットフォーム(Snowflake、BigQueryなど)にエクスポートして処理します。
データの移行
セルの再配置時にデータの移行が必要です。主な原則は次のとおりです。
- デュアルライト(Dual Write):移行期間中にソースセルとターゲットセルの両方に書き込み
- プログレッシブトランジション:読み出しを先にターゲットセルに切り替え、次に書き込みを切り替える
- ロールバック可能性:少なくとも48時間は元のセルデータを保持
モニタリングと観察性
セルベースのアーキテクチャでは、セル単位のメトリックとグローバル集約メトリックの両方が必要です。
セルヘルスチェックスクリプト
#!/bin/bash
# cell-health-check.sh
# 전체 셀의 상태를 점검하는 헬스체크 스크립트
set -euo pipefail
CELL_ROUTER_URL="${CELL_ROUTER_URL:-http://cell-router.internal:8080}"
ALERT_WEBHOOK="${ALERT_WEBHOOK:-}"
HEALTH_THRESHOLD=3 # 연속 실패 횟수 임계값
declare -A FAILURE_COUNT
check_cell_health() {
local cell_id="$1"
local cell_endpoint="$2"
# 애플리케이션 헬스 체크
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout 5 --max-time 10 \
"${cell_endpoint}/health/ready" 2>/dev/null || echo "000")
if [[ "$http_code" != "200" ]]; then
FAILURE_COUNT[$cell_id]=$(( ${FAILURE_COUNT[$cell_id]:-0} + 1 ))
echo "[WARN] ${cell_id}: health check failed (HTTP ${http_code}, consecutive: ${FAILURE_COUNT[$cell_id]})"
if [[ ${FAILURE_COUNT[$cell_id]} -ge $HEALTH_THRESHOLD ]]; then
echo "[CRITICAL] ${cell_id}: ${HEALTH_THRESHOLD} consecutive failures - triggering alert"
send_alert "$cell_id" "$http_code"
fi
return 1
fi
# 성공 시 카운터 리셋
FAILURE_COUNT[$cell_id]=0
# 셀 메트릭 수집
local metrics
metrics=$(curl -s --max-time 5 "${cell_endpoint}/metrics/cell" 2>/dev/null || echo "{}")
local active_connections error_rate p99_latency
active_connections=$(echo "$metrics" | jq -r '.active_connections // "N/A"')
error_rate=$(echo "$metrics" | jq -r '.error_rate_percent // "N/A"')
p99_latency=$(echo "$metrics" | jq -r '.p99_latency_ms // "N/A"')
echo "[OK] ${cell_id}: connections=${active_connections}, errors=${error_rate}%, p99=${p99_latency}ms"
return 0
}
send_alert() {
local cell_id="$1"
local http_code="$2"
local timestamp
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
if [[ -n "$ALERT_WEBHOOK" ]]; then
curl -s -X POST "$ALERT_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{
\"severity\": \"critical\",
\"cell_id\": \"${cell_id}\",
\"message\": \"Cell ${cell_id} health check failed (HTTP ${http_code})\",
\"timestamp\": \"${timestamp}\",
\"action\": \"investigate_and_consider_drain\"
}"
fi
}
# 셀 목록 조회 및 점검
echo "=== Cell Health Check: $(date -u +"%Y-%m-%d %H:%M:%S UTC") ==="
CELLS=$(curl -s "${CELL_ROUTER_URL}/cells" | jq -r '.[] | "\(.cell_id)|\(.endpoint)"')
TOTAL=0
HEALTHY=0
UNHEALTHY=0
while IFS='|' read -r cell_id endpoint; do
if check_cell_health "$cell_id" "$endpoint"; then
HEALTHY=$((HEALTHY + 1))
else
UNHEALTHY=$((UNHEALTHY + 1))
fi
TOTAL=$((TOTAL + 1))
done <<< "$CELLS"
echo ""
echo "=== Summary: Total=${TOTAL}, Healthy=${HEALTHY}, Unhealthy=${UNHEALTHY} ==="
# 전체 중 50% 이상 비정상이면 글로벌 알림
if [[ $TOTAL -gt 0 ]] && [[ $((UNHEALTHY * 100 / TOTAL)) -ge 50 ]]; then
echo "[GLOBAL CRITICAL] More than 50% cells unhealthy - possible global issue"
fi
コアモニタリングメトリック
セルベースのアーキテクチャで監視する必要があるメトリックは次のとおりです。
セルレベルメトリック:
- セルごとの要求スループット(RPS)とエラー率
- セル別P50/P95/P99遅延時間
- セル別DBコネクションプール使用率
- セル別CPU/メモリ使用率
- セル別キュー深度(Queue Depth)
グローバルレベルメトリック:
- セルルータの要求スループットと遅延時間
- セル間トラフィックの不均衡率
- 異常なセル数
- グローバルエラー率(全セル合計)
ルーティングルールの設定
# cell-routing-rules.yaml
# 셀 라우팅 규칙 정의
apiVersion: v1
kind: ConfigMap
metadata:
name: cell-routing-config
namespace: cell-router
data:
routing-rules.yaml: |
version: "2.0"
default_strategy: "consistent-hashing"
partition_key: "X-Tenant-Id"
cells:
- id: "cell-001"
endpoint: "https://cell-001.internal.example.com"
region: "us-east-1"
status: "active"
capacity: 10000
weight: 100
- id: "cell-002"
endpoint: "https://cell-002.internal.example.com"
region: "us-east-1"
status: "active"
capacity: 10000
weight: 100
- id: "cell-003"
endpoint: "https://cell-003.internal.example.com"
region: "us-west-2"
status: "active"
capacity: 10000
weight: 100
- id: "cell-004"
endpoint: "https://cell-004.internal.example.com"
region: "eu-west-1"
status: "draining"
capacity: 10000
weight: 0
drain_target: "cell-003"
# 전용 셀 (대형 테넌트)
dedicated_cells:
- tenant_id: "tenant-enterprise-001"
cell_id: "cell-dedicated-001"
reason: "SLA 요구사항 - 99.99% 가용성"
- tenant_id: "tenant-enterprise-002"
cell_id: "cell-dedicated-002"
reason: "데이터 레지던시 - EU GDPR 준수"
# 장애 시 트래픽 전환 규칙
failover_rules:
health_check_interval_seconds: 10
consecutive_failures_threshold: 3
failover_strategy: "nearest-healthy-cell"
auto_failback: true
failback_delay_minutes: 15
# 셀 용량 제한
capacity_management:
overflow_strategy: "reject-with-retry-after"
overflow_http_status: 503
retry_after_seconds: 30
capacity_warning_threshold_percent: 80
capacity_critical_threshold_percent: 95
トラブルシューティング
###問題1:セル間トラフィックの不均衡
症状:特定のセルのCPU使用率が90%を超え、他のセルは30%に留まります。
原因: 大型テナントが一般セルに配置され、該当セルに過剰な負荷が発生する。
解決方法: 大きなテナントを識別して専用セルに移行します。ルーティングテーブルの対応するテナントを専用セルにマッピングし、データ移行後にトラフィックを切り替えます。
###問題2:セルルータ自体の障害
症状:セルルータが応答しなくなり、システム全体が麻痺してしまう。
原因:セルルータがSPOF(Single Point of Failure)になる。
解決方法:セルルータを多重化し、DNSベースのロードバランシングでセルルータ自体の可用性を確保します。セルルータのロジックは最小限に保ち、障害の可能性を低くします。クライアントにセル割り当て情報をキャッシュして、ルータ障害時にも既存のセルに直接アクセスできるようにします。
###問題3:クロスセルデータの照会
症状:管理者ダッシュボードでユーザー統計全体を照会すると、応答時間が数十秒遅くなります。
原因:すべてのセルに順次クエリを送信して集計する方法を使用します。
解決方法:各セルから定期的に集計データを中央分析データベース(OLAP)にエクスポートします。管理者ダッシュボードはこの中央リポジトリで照会します。リアルタイム性が必要な場合は、Change Data Capture(CDC)でストリーミングアグリゲーションを設定します。
###問題4:セル間のデータ移行中のデータの不一致
症状:セルの再配置後、一部のユーザーのデータが欠落または重複しています。
原因:デュアルライト期間中にソースセルとターゲットセルとの間の同期不整合が発生します。
解決方法:移行の前後にデータチェックサム検証を実行します。イベントソーシングベースであれば、イベントリプレイで対象セルの状態を正確に回復することができる。移行ロールバックのために元のセルデータを少なくとも48時間保存します。
##操作時の注意事項
セルルータは必ずThin Layerで
セルルータはシステム内で唯一の共有コンポーネントです。ここに複雑なビジネスロジックを置くことは、セルベースの分離の利点を無力化します。セルルータは、パーティションキー抽出、ハッシュ計算、セルマッピングの3つの機能のみを実行する必要があります。
セルサイズの黄金比
セルが大きすぎると障害時の影響範囲が広がり、小さすぎると操作の複雑さとコストが増加します。 AWSの推奨事項によれば、ユーザー全体の5〜10%を収容するサイズが適切です。これは10〜20個のセルを意味します。
グローバル展開時のデータレジデンシー
GDPR、個人情報保護法などのデータ規制を遵守するには、特定の地域のユーザーデータをその地域のセルでのみ処理する必要があります。セルルーティングの際には、ローカル制約をパーティションキーとともに考慮する必要があります。
テスト戦略
セルベースのアーキテクチャでは、セル単位のカオスエンジニアリングが必須です。 AWS Fault Injection Service(FIS)またはChaos Meshを使用して個々のセルの障害をシミュレートし、他のセルが影響を受けないことを定期的に検証する必要があります。最小四半期の1回のセル単位の障害注入トレーニングをお勧めします。
失敗事例と回復
ケース 1: セル ルータの設定エラーによる全体的な障害
ある企業でセルルーティングテーブルを更新すると誤った設定が配布され、すべてのユーザーが1つのセルにルーティングされる事故が発生しました。対応するセルが容量を超えて503エラーを返し始め、他のセルはアイドル状態のままであった。
レッスン:ルーティング設定を変更するときにもカナリ展開を適用する必要があります。ルーティングテーブルへの変更は、セルトラフィック分布の検証を含む自動化されたテストパイプラインを介して行われるべきです。
###ケース2:セル間の隠された依存関係
セル間の完全な分離を実装したと思ったが、すべてのセルが共有する外部API(決済ゲートウェイ)が障害を起こし、セル全体が同時に影響を受けた。
レッスン:セル外の共有依存性を識別し、可能であればセルごとに分離するか、少なくともCircuit BreakerとFallbackを適用する必要があります。アーキテクチャレビューでは、依存性マップを作成して隠し共有ポイントを見つける必要があります。
ケース 3: データの移行に失敗しました
セルの再配置中にネットワーク障害により二重書き込みが中断され、ソースセルとターゲットセルのデータが一致しない状態が発生した。ロールバックしようとしましたが、元のセルで既に一部のデータが整理された状態でした。
レッスン:移行は必ず等性を保証するように設計されており、元のデータ削除は移行完了後に十分な安定化期間(最小48時間)が経過してから行わなければならない。
チェックリスト
セルベースのアーキテクチャを導入する前に検討すべき項目をまとめます。
設計段階:
- パーティションキー選択完了(テナント、ユーザー、地域など)
- セルサイズの決定(ユーザー比率5-10%推奨)
- セルルータの設計(Thin Layerの原則)
- クロスセルデータアクセスパターンの定義
- グローバル参照データ複製戦略の確立
- データレジデンシー要件の確認
実装段階:
- セル別独立インフラストラクチャのプロビジョニング(VPC、DB、Cache、Queue)
- NetworkPolicy またはセキュリティグループによるセル間分離の実装
- Consistent Hashingベースのルータの実装
- セル単位カナリ配布パイプラインの構築
- セル別ヘルスチェックエンドポイント実装
- セルルータの多重化
運用段階:
- セル単位の監視ダッシュボードの構築
- セル単位の通知ルールの設定
- セル障害時のトラフィックドレン自動化
- セル間データ移行ツールの準備
- 四半期ごとの細胞障害注入訓練計画
- セル容量監視と自動拡張設定
- グローバルメトリック集約パイプラインの構築
- ルーティング設定変更検証の自動化
参考資料
- [AWS Well-Architected - Reducing Scope of Impact with Cell-Based Architecture] (https://docs.aws.amazon.com/wellarchitected/latest/reducing-scope-of-impact-with-cell-based-architecture/)
- [InfoQ - Cell-Based Architecture for Distributed Systems] (https://www.infoq.com/articles/cell-based-architecture-distributed-systems/)
- System Design Newsletter - Cell-Based Architecture
- [AWS Solutions Library - Guidance for Cell-Based Architecture on AWS] (https://github.com/aws-solutions-library-samples/guidance-for-cell-based-architecture-on-aws)
- InfoQ Minibook - Cell-Based Architecture 2024
- Slack Engineering - Building Reliable Distributed Systems
- DoorDash Engineering Blog