- Authors
- Name
- 들어가며
- 셀 기반 아키텍처 핵심 개념
- Bulkhead 패턴과 장애 격리
- 셀 라우팅 전략
- Kubernetes 기반 셀 구현
- AWS 기반 셀 구현
- 셀 배포 전략(Canary per Cell)
- 실제 사례
- 데이터 파티셔닝
- 모니터링과 관찰성
- 트러블슈팅
- 운영 시 주의사항
- 실패 사례와 복구
- 체크리스트
- 참고자료

들어가며
대규모 분산 시스템을 운영하다 보면 한 가지 불편한 진실과 마주하게 된다. 아무리 정교한 장애 대응 체계를 갖추더라도, 시스템 전체가 공유하는 단일 컴포넌트에 장애가 발생하면 전체 서비스가 중단된다는 사실이다. 2021년 Facebook(현 Meta)의 6시간 전체 장애, 2023년 Microsoft Azure의 WAN 설정 오류로 인한 광범위 장애, 2024년 Cloudflare의 API Gateway 장애가 대표적이다. 이 사례들의 공통점은 **장애 폭발 반경(Blast Radius)**이 시스템 전체에 미쳤다는 것이다.
셀 기반 아키텍처(Cell-Based Architecture)는 이 문제에 대한 구조적 해결책이다. 시스템을 독립적인 셀(Cell)로 분할하여, 하나의 셀에서 장애가 발생하더라도 다른 셀에는 영향이 미치지 않도록 격리하는 패턴이다. AWS가 자체 인프라에 적용하여 Well-Architected Framework에 공식 문서로 등록했고, Slack, DoorDash, Salesforce 같은 기업들이 프로덕션에서 검증한 아키텍처다.
이 글에서는 셀 기반 아키텍처의 핵심 원리부터 Bulkhead 패턴과의 관계, 셀 라우팅 전략(Consistent Hashing, Partition Key), Kubernetes와 AWS 기반 구현, 셀 단위 카나리 배포, 데이터 파티셔닝, 모니터링과 관찰성, 실제 운영 사례와 트러블슈팅까지 운영 레벨에서 종합적으로 다룬다.
셀 기반 아키텍처 핵심 개념
셀(Cell)이란 무엇인가
셀은 서비스의 전체 기능을 독립적으로 수행할 수 있는 자기 완결적(self-contained) 배포 단위다. 각 셀은 자체 컴퓨팅 리소스, 데이터 저장소, 메시지 큐, 캐시를 보유하며, 다른 셀과 상태를 공유하지 않는다. 하나의 셀이 완전히 실패하더라도 나머지 셀은 정상적으로 동작한다.
셀 기반 아키텍처의 핵심 속성은 다음과 같다.
- 격리성(Isolation): 셀 간에 컴퓨팅, 스토리지, 네트워크 리소스를 공유하지 않는다
- 독립 배포(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% 사용자) |
| 구현 복잡도 | 높음 | 중간-높음 | 높음 | 낮음 |
| 인프라 비용 | 중간-높음 (셀당 오버헤드) | 높음 (리전 복제) | 매우 높음 | 낮음 |
| 지연 시간 | 낮음 (셀 내부 통신) | 가변 (리전 간 지연) | 가변 | 낮음 |
| 배포 유연성 | 셀 단위 카나리 가능 | 리전 단위 | 리전 단위 | 전체 배포 |
| 데이터 일관성 | 셀 내 강한 일관성 | 최종 일관성 | 충돌 해결 필요 | 강한 일관성 |
| 확장 방식 | 셀 추가 | 리전 추가 | 리전 추가 | 수직 확장 |
| 운영 복잡도 | 높음 (N개 셀 관리) | 중간 | 높음 | 낮음 |
Bulkhead 패턴과 장애 격리
셀 기반 아키텍처는 본질적으로 Bulkhead 패턴의 인프라 레벨 적용이다. Bulkhead는 선박의 격벽에서 유래한 개념으로, 한 구획에 침수가 발생해도 격벽이 다른 구획으로의 물 유입을 차단하여 선박 전체의 침몰을 방지한다.
Bulkhead 적용 레벨
Bulkhead 패턴은 여러 레벨에서 적용할 수 있으며, 셀 기반 아키텍처는 가장 높은 레벨에서의 적용이다.
- 스레드 풀 레벨: Hystrix/Resilience4j의 Thread Pool Bulkhead (가장 작은 단위)
- 프로세스 레벨: 기능별 프로세스 분리
- 서비스 레벨: 마이크로서비스 간 격리
- 인프라 레벨: 별도 VM, 클러스터, VPC로 격리
- 셀 레벨: 전체 스택(컴퓨팅+DB+캐시+큐)을 하나의 격리 단위로 묶음 (가장 큰 단위)
Blast Radius 계산
장애 폭발 반경은 다음 공식으로 계산할 수 있다.
단일 셀 장애 시 영향받는 사용자 비율 = 1 / N (N = 셀 수)
예시:
- 셀 4개: 장애 시 최대 25% 사용자 영향
- 셀 10개: 장애 시 최대 10% 사용자 영향
- 셀 20개: 장애 시 최대 5% 사용자 영향
단, 셀 수를 무한정 늘리면 운영 복잡도와 비용이 기하급수적으로 증가한다. 일반적으로 셀당 사용자 비율 5-10% 가 비용과 격리 수준 간의 적절한 균형점이다.
셀 라우팅 전략
셀 기반 아키텍처에서 가장 중요한 설계 결정은 어떤 사용자를 어떤 셀로 라우팅할 것인가다. 라우팅은 결정론적(deterministic)이어야 하며, 같은 사용자는 항상 같은 셀로 향해야 한다.
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 | 가맹점 단위 규제 준수 용이 | 대형 가맹점 별도 셀 필요 |
대형 테넌트(noisy neighbor)의 경우 전용 셀을 할당하는 전용 셀(Dedicated Cell) 전략을 함께 적용해야 한다.
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)
셀 기반 아키텍처의 가장 강력한 이점 중 하나는 셀 단위 카나리 배포다. 새로운 버전을 먼저 하나의 셀에만 배포하고, 문제가 없으면 점진적으로 다른 셀에 확장한다. 장애가 발생해도 해당 셀의 사용자만 영향을 받는다.
배포 파이프라인
# .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이며, 하나의 워크스페이스 내 모든 채널, 메시지, 파일이 동일한 셀에 저장된다. Slack의 셀 전환 이전에는 2022년 2월 전체 서비스 장애가 모든 사용자에게 영향을 미쳤지만, 셀 전환 이후에는 개별 셀의 장애가 해당 셀 사용자(전체의 약 5-8%)에만 영향을 미치게 되었다.
Slack의 핵심 설계 결정 사항은 다음과 같다.
- Vitess 기반 MySQL 샤딩을 셀 단위로 구성
- 셀 라우터(Cell Router)를 최소한의 로직만 포함하는 Thin Layer로 설계
- 대형 엔터프라이즈 고객(예: IBM, Amazon)에는 전용 셀 할당
- 셀 간 통신이 필요한 경우(크로스-워크스페이스 검색 등)는 비동기 메시지 버스 사용
DoorDash의 셀 아키텍처
DoorDash는 2023년 지역 기반 셀 아키텍처를 도입했다. 파티션 키는 geographic_region이며, 미국을 여러 지리적 셀로 분할했다. 이를 통해 특정 지역의 트래픽 폭증(슈퍼볼 시즌의 특정 도시 등)이 다른 지역에 영향을 미치지 않도록 격리했다.
- DynamoDB 를 셀 단위 데이터 저장소로 사용
- Apache Kafka 클러스터도 셀 단위로 분리
- 셀 단위 Feature Flag 를 통해 기능 롤아웃 제어
- 장애 발생 시 해당 셀의 트래픽을 인접 셀로 드레인(drain) 하는 자동화 구축
Salesforce의 Pod 아키텍처
Salesforce는 셀 기반 아키텍처의 선구자 중 하나다. Salesforce에서는 셀을 Pod라고 부르며, 각 Pod는 완전한 Salesforce 인스턴스를 포함한다. 수십 개의 Pod가 전 세계에 분산되어 있으며, 고객(테넌트)은 특정 Pod에 고정(pinned)된다.
- 테넌트별 인스턴스 URL(예: na1.salesforce.com, eu5.salesforce.com)로 셀 라우팅
- Pod 간 데이터 마이그레이션을 위한 Org Migration 도구 자체 개발
- Pod 단위 유지보수 윈도우와 독립적인 릴리스 주기 운영
데이터 파티셔닝
셀 기반 아키텍처에서 가장 까다로운 과제는 데이터 파티셔닝이다. 셀의 독립성을 유지하면서도 크로스-셀 쿼리 요구사항을 충족해야 한다.
파티셔닝 전략
1. 셀 로컬 데이터 (Cell-Local Data)
셀 내부에서만 접근하는 데이터로, 해당 셀의 데이터베이스에만 존재한다. 예를 들어 사용자의 메시지, 주문 내역, 세션 정보가 이에 해당한다.
2. 글로벌 참조 데이터 (Global Reference Data)
모든 셀에서 동일하게 필요한 읽기 전용 데이터다. 환율 정보, 상품 카탈로그, 국가 코드 등이 해당한다. 이 데이터는 글로벌 데이터 저장소에서 각 셀로 비동기 복제한다.
3. 크로스-셀 집계 데이터 (Cross-Cell Aggregate Data)
전체 시스템 통계, 글로벌 대시보드 등 모든 셀의 데이터를 집계해야 하는 경우다. 각 셀에서 집계된 메트릭을 중앙 분석 플랫폼(예: Snowflake, BigQuery)으로 내보내어 처리한다.
데이터 마이그레이션
셀 재배치(rebalancing) 시 데이터 마이그레이션이 필요하다. 핵심 원칙은 다음과 같다.
- 이중 쓰기(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로
셀 라우터는 시스템에서 유일한 공유 컴포넌트다. 여기에 복잡한 비즈니스 로직을 넣으면 셀 기반 격리의 이점이 무력화된다. 셀 라우터는 파티션 키 추출, 해시 계산, 셀 매핑 세 가지 기능만 수행해야 한다.
셀 크기의 황금 비율
셀이 너무 크면 장애 시 영향 범위가 넓어지고, 너무 작으면 운영 복잡도와 비용이 증가한다. AWS의 권장 사항에 따르면 전체 사용자의 5-10%를 수용하는 크기가 적절하다. 이는 10-20개의 셀을 의미한다.
글로벌 배포 시 데이터 레지던시
GDPR, 개인정보보호법 등 데이터 규제를 준수하려면 특정 지역의 사용자 데이터가 해당 지역의 셀에서만 처리되어야 한다. 셀 라우팅 시 지역 제약 조건을 파티션 키와 함께 고려해야 한다.
테스트 전략
셀 기반 아키텍처에서는 셀 단위 카오스 엔지니어링이 필수다. AWS Fault Injection Service(FIS)나 Chaos Mesh를 사용하여 개별 셀의 장애를 시뮬레이션하고, 다른 셀이 영향받지 않는지 정기적으로 검증해야 한다. 최소 분기 1회 셀 단위 장애 주입 훈련을 권장한다.
실패 사례와 복구
사례 1: 셀 라우터 설정 오류로 인한 전체 장애
한 기업에서 셀 라우팅 테이블 업데이트 시 잘못된 설정이 배포되어, 모든 사용자가 하나의 셀로 라우팅되는 사고가 발생했다. 해당 셀이 용량을 초과하여 503 에러를 반환하기 시작했고, 다른 셀은 유휴 상태로 남았다.
교훈: 라우팅 설정 변경 시에도 카나리 배포를 적용해야 한다. 라우팅 테이블에 대한 변경은 셀 트래픽 분포 검증을 포함하는 자동화된 테스트 파이프라인을 거쳐야 한다.
사례 2: 셀 간 숨겨진 의존성
셀 간 완전한 격리를 구현했다고 생각했지만, 모든 셀이 공유하는 외부 API(결제 게이트웨이)가 장애를 일으켜 전체 셀이 동시에 영향을 받았다.
교훈: 셀 외부의 공유 의존성을 식별하고, 가능하면 셀별로 분리하거나, 최소한 Circuit Breaker와 Fallback을 적용해야 한다. 아키텍처 리뷰 시 **의존성 맵(Dependency Map)**을 작성하여 숨겨진 공유 지점을 찾아야 한다.
사례 3: 데이터 마이그레이션 실패
셀 재배치 중 네트워크 장애로 이중 쓰기가 중단되어, 원본 셀과 대상 셀의 데이터가 일치하지 않는 상태가 발생했다. 롤백을 시도했지만, 원본 셀에서 이미 일부 데이터가 정리된 상태였다.
교훈: 마이그레이션은 반드시 **멱등성(idempotent)**을 보장하도록 설계하고, 원본 데이터 삭제는 마이그레이션 완료 후 충분한 안정화 기간(최소 48시간)이 지난 후에 수행해야 한다.
체크리스트
셀 기반 아키텍처 도입 전 검토해야 할 항목을 정리한다.
설계 단계:
- 파티션 키 선정 완료 (테넌트, 사용자, 지역 등)
- 셀 크기 결정 (사용자 비율 5-10% 권장)
- 셀 라우터 설계 (Thin Layer 원칙)
- 크로스-셀 데이터 접근 패턴 정의
- 글로벌 참조 데이터 복제 전략 수립
- 데이터 레지던시 요구사항 확인
구현 단계:
- 셀별 독립 인프라 프로비저닝 (VPC, DB, Cache, Queue)
- NetworkPolicy 또는 보안 그룹으로 셀 간 격리 구현
- Consistent Hashing 기반 라우터 구현
- 셀 단위 카나리 배포 파이프라인 구축
- 셀별 헬스체크 엔드포인트 구현
- 셀 라우터 다중화
운영 단계:
- 셀 단위 모니터링 대시보드 구축
- 셀 단위 알림 규칙 설정
- 셀 장애 시 트래픽 드레인 자동화
- 셀 간 데이터 마이그레이션 도구 준비
- 분기별 셀 장애 주입 훈련 계획
- 셀 용량 모니터링 및 자동 확장 설정
- 글로벌 메트릭 집계 파이프라인 구축
- 라우팅 설정 변경 검증 자동화
참고자료
- AWS Well-Architected - Reducing Scope of Impact with Cell-Based Architecture
- InfoQ - Cell-Based Architecture for Distributed Systems
- System Design Newsletter - Cell-Based Architecture
- AWS Solutions Library - Guidance for Cell-Based Architecture on AWS
- InfoQ Minibook - Cell-Based Architecture 2024
- Slack Engineering - Building Reliable Distributed Systems
- DoorDash Engineering Blog