Skip to content
Published on

セルベースのアーキテクチャの設計と運用:障害爆発半径最小化戦略

Authors
  • Name
    Twitter

Cell-Based Architecture

##入り

大規模な分散システムを運営してみると、一つの不便な真実と向き合うことになる。いくら洗練された障害対応体系を備えていても、システム全体が共有する単一のコンポーネントに障害が発生すると、全体のサービスが中断されるという事実である。 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 4App  │ │ App  │ │ App  │ │ AppDB   │ │ DB   │ │ DB   │ │ DB│Cache │ │Cache │ │Cache │ │Cache │
└──────┘ └──────┘ └──────┘ └──────┘
  장애 시: 해당 셀 사용자만 영향 (Blast Radius = 25%)

アーキテクチャパターンの比較

比較項目Cell-BasedMulti-RegionActive-ActiveTraditional Monolith
障害分離レベルセル単位(5-10%ユーザー)リージョンユニット(30-50%ユーザー)リージョンユニットなし(100%ユーザー)
実装の複雑さ高い中 - 高高い低い
インフラコスト中高(セルあたりオーバーヘッド)高(リージョン複製)非常に高い低い
遅延時間Low(セル内部通信)可変(リージョン間遅延)可変低い
展開の柔軟性セル単位カナリ可能リージョンユニットリージョンユニットフルデプロイ

|データの一貫性セル内の強い一貫性最終的な一貫性競合解決が必要強い一貫性 |拡張方式セルを追加リージョンを追加リージョンを追加垂直拡張| |操作の複雑さ高(Nセル管理)|中高い|低い|

Bulkheadパターンと障害の分離

セルベースのアーキテクチャは、本質的にBulkheadパターンのインフラストラクチャレベルのアプリケーションです。バルクヘッドは船舶の隔壁に由来する概念であり、ある区画に浸水が発生しても隔壁が別の区画への水の流入を遮断して船全体の沈没を防止する。

Bulkhead 適用レベル

Bulkheadパターンは複数のレベルで適用でき、セルベースのアーキテクチャは最高レベルでの適用です。

  1. スレッドプールレベル: Hystrix/Resilience4jのThread Pool Bulkhead (最も小さい単位)
  2. プロセスレベル: 機能別プロセスの分離
  3. サービスレベル: マイクロサービス間の分離
  4. インフラストラクチャレベル: 個別のVM、クラスタ、VPCに分離
  5. セルレベル:スタック全体(コンピューティング+ 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ベースのルータの実装
  • セル単位カナリ配布パイプラインの構築
  • セル別ヘルスチェックエンドポイント実装
  • セルルータの多重化

運用段階:

  • セル単位の監視ダッシュボードの構築
  • セル単位の通知ルールの設定
  • セル障害時のトラフィックドレン自動化
  • セル間データ移行ツールの準備
  • 四半期ごとの細胞障害注入訓練計画
  • セル容量監視と自動拡張設定
  • グローバルメトリック集約パイプラインの構築
  • ルーティング設定変更検証の自動化

参考資料