🗄️ 데이터베이스

ScyllaDB

ScyllaDB

C++로 재작성된 Cassandra 호환 DB. 더 빠른 성능.

📖 상세 설명

ScyllaDB(스킬라DB)는 Apache Cassandra와 API 호환되면서 C++로 완전히 재작성된 고성능 분산 NoSQL 데이터베이스입니다. 2015년 이스라엘에서 시작된 이 프로젝트는 Cassandra의 Java 기반 아키텍처의 한계(GC 일시 정지, 스레드 경합, 메모리 비효율)를 해결하고자 했습니다. 결과적으로 동일한 하드웨어에서 Cassandra 대비 10배 이상의 처리량과 P99 지연 시간 10배 감소를 달성했습니다.

ScyllaDB의 핵심 기술은 Shard-per-core 아키텍처입니다. 각 CPU 코어가 독립적인 샤드를 담당하며, 코어 간 락(lock) 경합 없이 데이터를 처리합니다. Seastar 프레임워크 기반의 비동기 I/O와 사용자 공간 스케줄링을 통해 커널 오버헤드를 최소화합니다. 이 아키텍처 덕분에 JVM의 Stop-the-World GC 없이 예측 가능한 밀리초 단위 지연 시간을 보장합니다.

ScyllaDB는 CQL(Cassandra Query Language)을 완벽히 지원하여 기존 Cassandra 애플리케이션을 코드 변경 없이 마이그레이션할 수 있습니다. 또한 Amazon DynamoDB 호환 API(Alternator)도 제공하여 DynamoDB 워크로드를 셀프 호스팅하거나 하이브리드 환경을 구성할 수 있습니다. Lightweight Transactions(LWT), Materialized Views, Secondary Index, CDC(Change Data Capture) 등 고급 기능도 지원합니다.

ScyllaDB는 IoT, 게임, 광고 기술, 금융 서비스 등 초저지연이 요구되는 실시간 애플리케이션에 적합합니다. Discord(수조 건 메시지), Comcast(스마트 홈), Grab(동남아 슈퍼앱) 등 대규모 서비스에서 사용 중입니다. ScyllaDB Cloud(관리형), ScyllaDB Enterprise(상용), ScyllaDB Open Source(오픈소스) 세 가지 에디션으로 제공됩니다.

💻 코드 예제

Docker로 ScyllaDB 실행

# 단일 노드 ScyllaDB 실행
docker run --name scylla -d \
  -p 9042:9042 \
  -p 9160:9160 \
  scylladb/scylla

# 클러스터 노드 상태 확인
docker exec -it scylla nodetool status

# cqlsh 접속
docker exec -it scylla cqlsh

# 3노드 클러스터 구성 (docker-compose.yml)
# version: '3'
# services:
#   scylla-node1:
#     image: scylladb/scylla
#     command: --seeds=scylla-node1
#   scylla-node2:
#     image: scylladb/scylla
#     command: --seeds=scylla-node1
#   scylla-node3:
#     image: scylladb/scylla
#     command: --seeds=scylla-node1

CQL 스키마 및 쿼리

-- Keyspace 생성 (Replication Factor 3)
CREATE KEYSPACE IF NOT EXISTS my_app
WITH replication = {
    'class': 'NetworkTopologyStrategy',
    'datacenter1': 3
}
AND durable_writes = true;

USE my_app;

-- 사용자 활동 테이블 (시계열 데이터)
CREATE TABLE user_activities (
    user_id UUID,
    activity_date DATE,
    activity_time TIMESTAMP,
    activity_type TEXT,
    details MAP,
    PRIMARY KEY ((user_id, activity_date), activity_time)
) WITH CLUSTERING ORDER BY (activity_time DESC)
  AND compaction = {
    'class': 'TimeWindowCompactionStrategy',
    'compaction_window_unit': 'DAYS',
    'compaction_window_size': 1
  };

-- 보조 인덱스 (activity_type으로 조회)
CREATE INDEX idx_activity_type ON user_activities (activity_type);

-- Materialized View (타입별 조회용)
CREATE MATERIALIZED VIEW activities_by_type AS
    SELECT * FROM user_activities
    WHERE activity_type IS NOT NULL
    AND user_id IS NOT NULL
    AND activity_date IS NOT NULL
    AND activity_time IS NOT NULL
    PRIMARY KEY ((activity_type, activity_date), activity_time, user_id);

-- 데이터 삽입
INSERT INTO user_activities (user_id, activity_date, activity_time, activity_type, details)
VALUES (
    uuid(),
    '2024-01-15',
    '2024-01-15 10:30:00+0000',
    'login',
    {'ip': '192.168.1.1', 'device': 'mobile'}
);

-- TTL 적용 (30일 후 자동 삭제)
INSERT INTO user_activities (user_id, activity_date, activity_time, activity_type, details)
VALUES (uuid(), '2024-01-15', toTimestamp(now()), 'pageview', {'page': '/home'})
USING TTL 2592000;

-- 범위 쿼리 (특정 날짜의 활동)
SELECT * FROM user_activities
WHERE user_id = 123e4567-e89b-12d3-a456-426614174000
AND activity_date = '2024-01-15'
AND activity_time >= '2024-01-15 00:00:00+0000'
LIMIT 100;

-- Lightweight Transaction (조건부 삽입)
INSERT INTO user_activities (user_id, activity_date, activity_time, activity_type, details)
VALUES (uuid(), '2024-01-15', toTimestamp(now()), 'signup', {})
IF NOT EXISTS;

Python 드라이버 사용 예제

from cassandra.cluster import Cluster
from cassandra.auth import PlainTextAuthProvider
from cassandra.policies import DCAwareRoundRobinPolicy, TokenAwarePolicy
from cassandra.query import SimpleStatement, ConsistencyLevel
from cassandra import WriteTimeout, ReadTimeout
import uuid
from datetime import datetime, date

# 클러스터 연결 설정
auth_provider = PlainTextAuthProvider(
    username='scylla',
    password='password'
)

cluster = Cluster(
    contact_points=['scylla-node1', 'scylla-node2', 'scylla-node3'],
    port=9042,
    auth_provider=auth_provider,
    load_balancing_policy=TokenAwarePolicy(
        DCAwareRoundRobinPolicy(local_dc='datacenter1')
    ),
    protocol_version=4
)

session = cluster.connect('my_app')

# Prepared Statement (성능 최적화)
insert_stmt = session.prepare("""
    INSERT INTO user_activities
    (user_id, activity_date, activity_time, activity_type, details)
    VALUES (?, ?, ?, ?, ?)
""")

select_stmt = session.prepare("""
    SELECT * FROM user_activities
    WHERE user_id = ? AND activity_date = ?
    LIMIT ?
""")

# 데이터 삽입 (배치)
from cassandra.query import BatchStatement, BatchType

def insert_activities_batch(activities: list):
    """여러 활동을 배치로 삽입"""
    batch = BatchStatement(batch_type=BatchType.UNLOGGED)

    for activity in activities:
        batch.add(insert_stmt, (
            activity['user_id'],
            activity['date'],
            activity['timestamp'],
            activity['type'],
            activity.get('details', {})
        ))

    try:
        session.execute(batch)
    except WriteTimeout:
        # 개별 재시도 로직
        for activity in activities:
            session.execute(insert_stmt, (
                activity['user_id'],
                activity['date'],
                activity['timestamp'],
                activity['type'],
                activity.get('details', {})
            ))

# 비동기 쿼리
from cassandra.concurrent import execute_concurrent_with_args

def get_activities_for_users(user_ids: list, target_date: date, limit: int = 100):
    """여러 사용자의 활동을 동시 조회"""
    parameters = [(uid, target_date, limit) for uid in user_ids]

    results = execute_concurrent_with_args(
        session,
        select_stmt,
        parameters,
        concurrency=50,
        raise_on_first_error=False
    )

    all_activities = []
    for success, result in results:
        if success:
            all_activities.extend(list(result))

    return all_activities

# Consistency Level 조정
def read_with_consistency(user_id: uuid.UUID, target_date: date):
    """높은 일관성 수준으로 읽기"""
    stmt = SimpleStatement(
        "SELECT * FROM user_activities WHERE user_id = %s AND activity_date = %s",
        consistency_level=ConsistencyLevel.LOCAL_QUORUM
    )
    return session.execute(stmt, (user_id, target_date))

# 페이지네이션 (대용량 결과)
def get_all_activities_paged(user_id: uuid.UUID, target_date: date, page_size: int = 1000):
    """페이지네이션으로 대용량 데이터 조회"""
    stmt = SimpleStatement(
        "SELECT * FROM user_activities WHERE user_id = %s AND activity_date = %s",
        fetch_size=page_size
    )

    result = session.execute(stmt, (user_id, target_date))

    for row in result:
        yield row
        # result.has_more_pages로 더 많은 페이지 확인 가능

# 연결 종료
# cluster.shutdown()

DynamoDB 호환 API (Alternator)

import boto3
from botocore.config import Config

# ScyllaDB Alternator에 연결
dynamodb = boto3.resource(
    'dynamodb',
    endpoint_url='http://scylla-alternator:8000',
    region_name='us-east-1',
    aws_access_key_id='none',
    aws_secret_access_key='none',
    config=Config(
        retries={'max_attempts': 3},
        connect_timeout=5,
        read_timeout=30
    )
)

# 테이블 생성
table = dynamodb.create_table(
    TableName='UserActivities',
    KeySchema=[
        {'AttributeName': 'pk', 'KeyType': 'HASH'},
        {'AttributeName': 'sk', 'KeyType': 'RANGE'}
    ],
    AttributeDefinitions=[
        {'AttributeName': 'pk', 'AttributeType': 'S'},
        {'AttributeName': 'sk', 'AttributeType': 'S'}
    ],
    BillingMode='PAY_PER_REQUEST'
)

# 아이템 삽입
table = dynamodb.Table('UserActivities')
table.put_item(
    Item={
        'pk': 'USER#123',
        'sk': 'ACTIVITY#2024-01-15T10:30:00',
        'activity_type': 'login',
        'details': {'ip': '192.168.1.1'}
    }
)

# 쿼리 (DynamoDB API)
response = table.query(
    KeyConditionExpression='pk = :pk AND begins_with(sk, :sk_prefix)',
    ExpressionAttributeValues={
        ':pk': 'USER#123',
        ':sk_prefix': 'ACTIVITY#2024-01'
    }
)

🗣️ 실무에서 이렇게 말해요

  • "Cassandra에서 GC 일시 정지 때문에 P99 레이턴시가 튀어요. ScyllaDB로 마이그레이션하면 CQL 호환이라 코드 변경 없이 지연 시간을 줄일 수 있습니다."
  • "ScyllaDB Alternator 쓰면 DynamoDB 워크로드를 온프레미스에서 돌릴 수 있어요. AWS 비용 절감 효과가 큽니다."
  • "Shard-per-core 아키텍처라서 코어 수 늘리면 선형으로 성능이 증가해요. 수직 확장도 효율적입니다."
  • "실시간 분석 워크로드에 ScyllaDB가 적합해요. CDC로 변경 사항을 Kafka로 스트리밍할 수 있습니다."
  • "ScyllaDB가 Cassandra보다 빠른 이유는 무엇인가요? Shard-per-core 아키텍처를 설명해 주세요."
  • "ScyllaDB에서 적절한 파티션 키 설계가 중요한 이유는 무엇인가요? 핫스팟이 발생하면 어떻게 되나요?"
  • "Cassandra에서 ScyllaDB로 마이그레이션할 때 고려해야 할 사항은 무엇인가요?"
  • "ScyllaDB의 Compaction 전략 중 TimeWindowCompactionStrategy는 언제 사용하나요?"
  • "이 파티션 키 설계는 핫스팟을 유발할 수 있어요. user_id만으로 파티셔닝하면 특정 사용자에 쓰기가 집중됩니다. 날짜를 복합 키에 추가하세요."
  • "ALLOW FILTERING은 프로덕션에서 사용하면 안 됩니다. 보조 인덱스나 Materialized View를 고려하세요."
  • "배치 크기가 너무 커요. ScyllaDB에서 배치는 여러 파티션에 원자적 쓰기가 필요할 때만 사용하고, 크기는 작게 유지하세요."

⚠️ 주의사항

파티션 키 설계

ScyllaDB의 성능은 파티션 키 설계에 크게 좌우됩니다. 단일 파티션에 데이터가 집중되면 해당 샤드가 핫스팟이 되어 성능이 저하됩니다. 시계열 데이터의 경우 날짜를 파티션 키에 포함하여 데이터를 분산시키세요.

대용량 파티션 회피

단일 파티션이 100MB를 넘지 않도록 설계하세요. 파티션이 너무 크면 읽기/쓰기 성능이 저하되고, 복구 시간이 길어집니다. 버킷팅이나 복합 파티션 키로 데이터를 분산시키세요.

Compaction 전략 선택

워크로드에 맞는 Compaction 전략을 선택하세요. 시계열 데이터는 TimeWindowCompactionStrategy, 읽기 중심 워크로드는 LeveledCompactionStrategy가 적합합니다. 잘못된 선택은 디스크 사용량 증가와 성능 저하를 유발합니다.

🔗 관련 용어

📚 더 배우기