Skip to content
Published on

CockroachDB 실전 운영 가이드: 분산 SQL NewSQL 아키텍처와 프로덕션 전략

Authors
  • Name
    Twitter
CockroachDB

들어가며

전통적인 RDBMS는 단일 노드의 수직 확장(Scale-Up)에 의존하며, 글로벌 서비스에서 요구되는 수평 확장성과 지리적 분산에 한계를 보인다. NoSQL은 확장성 문제를 해결했지만 ACID 트랜잭션과 SQL 호환성을 포기해야 했다. NewSQL은 이 두 세계의 장점을 결합하려는 시도로, CockroachDB는 Google Spanner 논문에서 영감을 받아 탄생한 대표적인 오픈소스 분산 SQL 데이터베이스다.

CockroachDB는 PostgreSQL 와이어 프로토콜과 호환되면서도 자동 샤딩, 멀티 리전 복제, 직렬화 가능(Serializable) 격리 수준의 분산 트랜잭션을 기본 제공한다. 이름 그대로 바퀴벌레처럼 어떤 장애 상황에서도 살아남는 것을 설계 철학으로 삼고 있다. 이 글에서는 CockroachDB의 내부 아키텍처를 깊이 분석하고, 실제 프로덕션 환경에서의 클러스터 구축, 스키마 설계, 성능 튜닝, 장애 복구 절차를 체계적으로 다룬다.

1. NewSQL과 분산 SQL의 개념 정리

NewSQL이란 무엇인가

NewSQL은 2011년 매튜 애슬릿(Matthew Aslett)이 처음 사용한 용어로, 전통적인 RDBMS의 ACID 보장과 SQL 인터페이스를 유지하면서 NoSQL 수준의 수평 확장성을 제공하는 데이터베이스 범주를 가리킨다. 핵심 특징은 다음과 같다.

특성전통 RDBMSNoSQLNewSQL
SQL 지원완전 지원제한적/미지원완전 지원
ACID 트랜잭션완전 지원일부만 (최종 일관성)완전 지원
수평 확장불가/제한적네이티브네이티브
자동 샤딩미지원지원지원
분산 트랜잭션미지원미지원지원
지리적 분산수동 복제만가능네이티브

분산 SQL과 NewSQL의 관계

분산 SQL은 NewSQL의 구현 방식 중 하나다. 모든 노드가 SQL 쿼리를 처리할 수 있고, 데이터가 여러 노드에 자동으로 분산되며, 분산 트랜잭션이 합의 프로토콜을 통해 일관성을 보장한다. CockroachDB, YugabyteDB, TiDB가 이 범주에 해당하며, Google Cloud Spanner는 이 분야의 선구자적 존재다.

2. CockroachDB 아키텍처 심층 분석

계층화된 아키텍처

CockroachDB는 네 개의 주요 계층으로 구성된다. 각 계층은 독립적으로 동작하면서 상위 계층에 추상화된 인터페이스를 제공한다.

┌─────────────────────────────────────────────────────────┐
SQL Layer  (SQL 파싱, 최적화, 실행 계획, PostgreSQL 호환)├─────────────────────────────────────────────────────────┤
Transaction Layer  (분산 ACID 트랜잭션, MVCC, 타임스탬프 오라클)├─────────────────────────────────────────────────────────┤
Distribution Layer  (Range 분할, 리스홀더 선출, 메타 레인지 관리)├─────────────────────────────────────────────────────────┤
Replication Layer  (Raft 합의, MultiRaft, 스냅샷 전송)├─────────────────────────────────────────────────────────┤
Storage Layer  (Pebble - LSM Tree 기반 키-값 스토리지 엔진)└─────────────────────────────────────────────────────────┘

SQL Layer: PostgreSQL 와이어 프로토콜을 구현하여 기존 PostgreSQL 클라이언트 드라이버를 그대로 사용할 수 있다. SQL 파서가 쿼리를 AST로 변환한 뒤 비용 기반 옵티마이저(CBO)가 최적의 실행 계획을 생성한다.

Transaction Layer: 다중 버전 동시성 제어(MVCC)를 사용하여 읽기와 쓰기가 서로 차단하지 않도록 한다. HLC(Hybrid Logical Clock)로 분산 환경에서의 인과 관계를 추적하며, 기본 격리 수준이 Serializable이다.

Distribution Layer: 전체 키 공간을 512MiB 단위의 Range로 분할하여 클러스터 전체에 분산한다. 각 Range에는 Leaseholder가 존재하며 읽기 요청을 직접 처리한다.

Replication Layer: 각 Range를 기본 3개의 복제본으로 유지하며, Raft 합의 알고리즘으로 복제본 간 일관성을 보장한다.

Storage Layer: 과거 RocksDB를 사용했으나 현재는 CockroachDB팀이 Go로 직접 개발한 Pebble 엔진으로 전환했다. LSM(Log-Structured Merge-Tree) 기반으로 쓰기 성능에 최적화되어 있다.

Raft 합의 알고리즘과 MultiRaft

CockroachDB는 Raft 합의 프로토콜을 통해 데이터의 일관성을 보장한다. 모든 쓰기 연산은 Range의 Raft 리더에게 전달되며, 과반수(Quorum)의 복제본이 해당 쓰기를 승인해야 커밋된다. 3개 복제본 기준으로 2개의 승인이 필요하고, 5개 복제본에서는 3개의 승인이 필요하다.

단일 CockroachDB 클러스터에는 수십만 개의 Range가 존재할 수 있다. 각 Range마다 독립적인 Raft 그룹을 운영하면 하트비트와 메시지 처리 오버헤드가 급증한다. 이를 해결하기 위해 CockroachDB는 MultiRaft를 구현했다. 동일 노드에 위치한 여러 Range의 Raft 메시지를 배치로 묶어 전송하고, 하트비트도 노드 단위로 통합하여 네트워크 오버헤드를 대폭 줄인다.

MultiRaft 메시지 흐름:

Node 1                    Node 2                    Node 3
┌──────────────┐          ┌──────────────┐          ┌──────────────┐
Range 1 (L)  │──┐       │ Range 1 (F)  │          │ Range 1 (F)Range 5 (F)  │  │ 배치  │ Range 5 (L)  │          │ Range 5 (F)Range 9 (F)  │  ├──────▶│ Range 9 (F)  │          │ Range 9 (L)Range 12 (L) │──┘       │ Range 12 (F) │          │ Range 12 (F)└──────────────┘          └──────────────┘          └──────────────┘
(L)=Leader, (F)=Follower

분산 트랜잭션의 생명주기

CockroachDB에서 분산 트랜잭션이 처리되는 과정을 단계별로 살펴본다.

  1. 게이트웨이 노드 수신: 클라이언트가 임의의 노드에 SQL 문을 전송한다.
  2. SQL 파싱 및 실행 계획: SQL Layer에서 쿼리를 파싱하고 최적화한 실행 계획을 생성한다.
  3. 트랜잭션 레코드 생성: Transaction Layer가 트랜잭션의 첫 번째 쓰기가 발생하는 Range에 트랜잭션 레코드를 PENDING 상태로 생성한다.
  4. Intent 쓰기: 각 대상 키에 Intent(잠정적 쓰기)를 기록한다. Intent는 다른 트랜잭션에게 해당 키가 진행 중인 트랜잭션에 의해 수정되고 있음을 알린다.
  5. Raft를 통한 합의: 각 Intent 쓰기는 해당 Range의 Raft 리더를 통해 과반수 합의를 얻어야 한다.
  6. 병렬 커밋(Parallel Commit): 트랜잭션 레코드의 상태를 STAGING으로 변경하면서 동시에 모든 Intent의 합의를 병렬로 진행한다. 모든 Intent가 합의를 얻으면 트랜잭션은 암묵적으로 COMMITTED가 된다.
  7. Intent 해소: 비동기적으로 각 Intent를 일반 MVCC 값으로 변환하고 트랜잭션 레코드를 COMMITTED로 갱신한다.

3. 클러스터 설치와 프로덕션 구성

최소 프로덕션 토폴로지

프로덕션 환경에서는 최소 3개 노드를 서로 다른 가용 영역(AZ)에 배치해야 한다. 단일 AZ 장애 시에도 Raft 쿼럼이 유지되어 서비스를 지속할 수 있다.

# 노드 1 (AZ-a) 시작
cockroach start \
  --insecure \
  --advertise-addr=node1.example.com:26257 \
  --join=node1.example.com:26257,node2.example.com:26257,node3.example.com:26257 \
  --locality=region=ap-northeast-2,zone=az-a \
  --store=path=/data/cockroach,attrs=ssd \
  --max-offset=500ms \
  --cache=.25 \
  --max-sql-memory=.25 \
  --background

# 노드 2 (AZ-b) 시작
cockroach start \
  --insecure \
  --advertise-addr=node2.example.com:26257 \
  --join=node1.example.com:26257,node2.example.com:26257,node3.example.com:26257 \
  --locality=region=ap-northeast-2,zone=az-b \
  --store=path=/data/cockroach,attrs=ssd \
  --max-offset=500ms \
  --cache=.25 \
  --max-sql-memory=.25 \
  --background

# 노드 3 (AZ-c) 시작
cockroach start \
  --insecure \
  --advertise-addr=node3.example.com:26257 \
  --join=node1.example.com:26257,node2.example.com:26257,node3.example.com:26257 \
  --locality=region=ap-northeast-2,zone=az-c \
  --store=path=/data/cockroach,attrs=ssd \
  --max-offset=500ms \
  --cache=.25 \
  --max-sql-memory=.25 \
  --background

# 클러스터 초기화 (최초 1회)
cockroach init --insecure --host=node1.example.com:26257

# 클러스터 상태 확인
cockroach node status --insecure --host=node1.example.com:26257

주요 옵션 설명:

  • --locality: 노드의 지리적 위치를 지정한다. CockroachDB는 이 정보를 기반으로 복제본을 가용 영역과 리전에 걸쳐 분산 배치한다.
  • --max-offset: 노드 간 허용되는 최대 시계 오차. 기본값은 500ms이며, 멀티 리전에서는 250ms로 낮추는 것을 권장한다.
  • --cache: 노드 메모리 중 Pebble 블록 캐시에 할당할 비율. 전체 메모리의 25%가 기본값이다.
  • --max-sql-memory: SQL 실행에 사용할 메모리 비율. 캐시와 합쳐서 50%를 넘지 않도록 설정한다.

Kubernetes 기반 배포

프로덕션에서 CockroachDB를 Kubernetes에 배포할 때는 StatefulSet을 사용한다. 각 Pod가 고유한 네트워크 정체성과 영구 볼륨을 유지해야 하기 때문이다.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cockroachdb
  namespace: crdb
spec:
  serviceName: cockroachdb
  replicas: 3
  selector:
    matchLabels:
      app: cockroachdb
  template:
    metadata:
      labels:
        app: cockroachdb
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - cockroachdb
              topologyKey: topology.kubernetes.io/zone
      containers:
        - name: cockroachdb
          image: cockroachdb/cockroach:v24.3.4
          ports:
            - containerPort: 26257
              name: grpc
            - containerPort: 8080
              name: http
          env:
            - name: COCKROACH_CHANNEL
              value: kubernetes-helm
          command:
            - /cockroach/cockroach
            - start
            - --advertise-addr=$(POD_NAME).cockroachdb.crdb.svc.cluster.local
            - --join=cockroachdb-0.cockroachdb.crdb.svc.cluster.local:26257,cockroachdb-1.cockroachdb.crdb.svc.cluster.local:26257,cockroachdb-2.cockroachdb.crdb.svc.cluster.local:26257
            - --locality=region=ap-northeast-2,zone=$(NODE_ZONE)
            - --cache=2GiB
            - --max-sql-memory=2GiB
            - --logtostderr=WARNING
          resources:
            requests:
              cpu: '2'
              memory: '8Gi'
            limits:
              cpu: '4'
              memory: '8Gi'
          volumeMounts:
            - name: datadir
              mountPath: /cockroach/cockroach-data
  volumeClaimTemplates:
    - metadata:
        name: datadir
      spec:
        accessModes: ['ReadWriteOnce']
        storageClassName: gp3-encrypted
        resources:
          requests:
            storage: 100Gi

핵심 설정 포인트:

  • podAntiAffinity: 동일 AZ에 두 개의 CockroachDB Pod가 배치되지 않도록 강제한다.
  • resources.limits.memory와 requests.memory 동일하게 설정: OOM Killer에 의한 예기치 않은 Pod 종료를 방지한다.
  • storageClassName: 높은 IOPS를 제공하는 스토리지 클래스를 사용한다. AWS에서는 gp3 이상을 권장한다.

4. 멀티 리전 토폴로지 설계

서바이벌 목표와 테이블 로컬리티

CockroachDB의 멀티 리전 기능은 두 가지 핵심 개념으로 구성된다.

서바이벌 목표(Survival Goal):

  • ZONE: 단일 가용 영역 장애에서 생존 (기본값, 3개 복제본)
  • REGION: 전체 리전 장애에서 생존 (5개 복제본 필요, 최소 3개 리전)

테이블 로컬리티(Table Locality):

  • REGIONAL BY TABLE: 특정 리전에 Leaseholder를 고정하여 해당 리전의 읽기 지연을 최소화
  • REGIONAL BY ROW: 행 단위로 홈 리전을 지정하여 각 사용자의 데이터를 가장 가까운 리전에 배치
  • GLOBAL: 모든 리전에서 읽기 지연 없이 접근 가능 (쓰기 지연은 증가)
-- 데이터베이스에 리전 설정
ALTER DATABASE myapp PRIMARY REGION "ap-northeast-2";
ALTER DATABASE myapp ADD REGION "us-east-1";
ALTER DATABASE myapp ADD REGION "eu-west-1";

-- 리전 장애에도 생존하도록 설정
ALTER DATABASE myapp SURVIVE REGION FAILURE;

-- 사용자 테이블: 행 단위로 리전 분산
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email STRING NOT NULL UNIQUE,
    name STRING NOT NULL,
    region crdb_internal_region NOT NULL DEFAULT 'ap-northeast-2',
    created_at TIMESTAMPTZ DEFAULT now()
) LOCALITY REGIONAL BY ROW AS region;

-- 설정 테이블: 전 리전에서 빠른 읽기 (드물게 갱신)
CREATE TABLE app_config (
    key STRING PRIMARY KEY,
    value JSONB NOT NULL,
    updated_at TIMESTAMPTZ DEFAULT now()
) LOCALITY GLOBAL;

-- 주문 테이블: 한국 리전에 Leaseholder 고정
CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id),
    total_amount DECIMAL(12,2) NOT NULL,
    status STRING NOT NULL DEFAULT 'pending',
    created_at TIMESTAMPTZ DEFAULT now()
) LOCALITY REGIONAL BY TABLE IN "ap-northeast-2";

멀티 리전 쓰기 지연 최적화

멀티 리전 환경에서 쓰기 지연은 Raft 쿼럼을 달성하기 위해 리전 간 네트워크 왕복이 필요하므로 불가피하게 증가한다. 서울-버지니아 간 왕복 지연이 약 180ms라면, 쓰기 커밋에는 최소 180ms가 소요된다. 이를 완화하기 위한 전략은 다음과 같다.

  1. REGIONAL BY ROW를 적극 활용: 사용자의 데이터가 해당 사용자가 접속하는 리전에 위치하면 대부분의 쓰기가 리전 내에서 완료된다.
  2. 비동기 쓰기 패턴: 즉각적 응답이 필요한 요청은 로컬 큐에 넣고, 실제 분산 쓰기는 백그라운드에서 처리한다.
  3. Follower Reads 활용: 약간의 과거 데이터를 허용할 수 있는 읽기에는 Follower Read를 사용하여 원격 리전의 Leaseholder까지 가지 않아도 된다.
-- Follower Read: 4.8초 이전까지의 데이터를 로컬 복제본에서 읽기
SELECT * FROM users
AS OF SYSTEM TIME follower_read_timestamp()
WHERE region = 'us-east-1';

-- Bounded Staleness Read: 최소한 10초 이내의 데이터 보장
SELECT * FROM app_config
AS OF SYSTEM TIME with_max_staleness('10s');

5. 온라인 스키마 변경과 CDC

온라인 DDL의 작동 원리

CockroachDB의 온라인 스키마 변경은 테이블 잠금 없이 수행된다. 내부적으로 스키마 버전을 점진적으로 전환하는 브리징 전략을 사용하며, 클러스터의 모든 노드가 동시에 새 스키마로 전환되지 않아도 데이터 일관성이 유지된다.

-- 대용량 테이블에 컬럼 추가 (온라인, 무중단)
ALTER TABLE orders ADD COLUMN shipping_address STRING;

-- 인덱스 생성 (백그라운드에서 점진적으로 구축)
CREATE INDEX CONCURRENTLY idx_orders_status_created
ON orders (status, created_at DESC);

-- 스키마 변경 진행 상황 모니터링
SELECT job_id, job_type, description, status, fraction_completed
FROM [SHOW JOBS]
WHERE job_type = 'SCHEMA CHANGE'
ORDER BY created DESC
LIMIT 5;

스키마 변경 시 주의사항:

  • 대용량 테이블의 컬럼 타입 변경(예: STRING -> INT)은 전체 데이터를 백필해야 하므로 오래 걸린다.
  • 동시에 같은 테이블에 여러 스키마 변경을 실행하면 순차적으로 처리되므로 하나의 ALTER TABLE 문에 여러 변경을 묶는 것이 효율적이다.
  • NOT NULL 제약 조건 추가 시 기존 NULL 값이 있으면 실패한다. 먼저 데이터를 정리한 후 제약 조건을 추가해야 한다.

Change Data Capture (CDC) 설정

CockroachDB의 Changefeed는 테이블의 변경 사항을 외부 시스템(Kafka, Cloud Storage 등)으로 실시간 스트리밍한다. 이벤트 기반 아키텍처의 핵심 구성 요소로 활용된다.

-- Kafka로 변경 이벤트 스트리밍
CREATE CHANGEFEED FOR TABLE orders, users
INTO 'kafka://kafka-broker1:9092?topic_prefix=crdb_'
WITH
  format = 'json',
  updated,
  resolved = '10s',
  min_checkpoint_frequency = '30s',
  schema_change_policy = 'backfill',
  kafka_sink_config = '{"Flush": {"MaxMessages": 1000, "Frequency": "1s"}}';

-- Changefeed 상태 확인
SELECT job_id, description, status, error, running_status
FROM [SHOW JOBS]
WHERE job_type = 'CHANGEFEED'
ORDER BY created DESC;

-- 문제 발생 시 Changefeed 일시 중지 및 재개
PAUSE JOB (SELECT job_id FROM [SHOW JOBS] WHERE job_type = 'CHANGEFEED' AND status = 'running' LIMIT 1);
RESUME JOB <job_id>;

Changefeed 운영 시 핵심 권장사항:

  • 클러스터당 Changefeed 수를 80개 이하로 유지한다.
  • 수백 개 테이블을 하나의 Changefeed로 감시하면 스키마 변경 시 전체 성능에 영향을 준다. 테이블을 논리적 그룹으로 나누어 별도의 Changefeed를 생성하는 것이 좋다.
  • Rangefeed 활성화 시 평균 5-10%의 성능 비용이 발생한다.
  • schema_locked 파라미터를 설정하면 스키마 변경이 없는 테이블에서 Changefeed의 지연을 줄일 수 있다.

6. NewSQL 데이터베이스 비교

CockroachDB vs TiDB vs YugabyteDB vs Spanner

비교 항목CockroachDBTiDBYugabyteDBGoogle Cloud Spanner
기반 논문Google SpannerGoogle Spanner + F1Google SpannerGoogle 자체 개발
SQL 호환PostgreSQLMySQLPostgreSQL + Cassandra QL독자 SQL (GoogleSQL)
합의 알고리즘Raft (MultiRaft)Raft (TiKV 레벨)Raft (DocDB 레벨)Paxos
스토리지 엔진Pebble (Go, LSM)RocksDB (C++, LSM)DocDB (C++, LSM)Colossus (자체)
기본 격리 수준SerializableSnapshot IsolationSnapshot IsolationExternal Consistency
시간 동기화HLC + NTPTSO 서버 (중앙 집중)HLC + NTPTrueTime (원자시계)
HTAP 지원제한적TiFlash 통합 (강력)제한적제한적
라이선스BSL (3년 후 Apache)Apache 2.0Apache 2.0관리형만 제공
관리형 서비스CockroachDB CloudTiDB CloudYugabyteDB ManagedGCP 전용
멀티 리전네이티브TiDB Dashboard네이티브네이티브
무료 티어Serverless (무료)Cloud 무료 플랜체험판90일 무료

각 데이터베이스의 적합한 사용 사례

CockroachDB가 적합한 경우:

  • PostgreSQL 호환이 필요하면서 글로벌 분산이 필수인 서비스
  • 강력한 일관성(Serializable)이 기본적으로 필요한 금융/결제 시스템
  • 멀티 클라우드 또는 멀티 리전 배포가 요구되는 아키텍처

TiDB가 적합한 경우:

  • MySQL 호환이 필요한 레거시 시스템의 마이그레이션
  • OLTP와 OLAP을 동시에 처리해야 하는 HTAP 워크로드
  • 중앙 집중식 배포에서의 높은 쓰기 처리량이 필요한 경우

YugabyteDB가 적합한 경우:

  • PostgreSQL과 Cassandra 두 API 모두 필요한 환경
  • Apache 2.0 라이선스가 필수 요건인 조직
  • 기존 Cassandra 워크로드를 SQL로 마이그레이션하려는 경우

Google Cloud Spanner가 적합한 경우:

  • Google Cloud에 올인한 조직
  • 인프라 운영 부담을 완전히 제거하고 싶은 경우
  • TrueTime 기반의 최고 수준 일관성이 필요한 경우

7. 성능 튜닝 실전

쿼리 성능 분석

CockroachDB는 PostgreSQL과 유사한 EXPLAIN ANALYZE 기능을 제공하지만, 분산 실행 계획이라는 고유한 특성이 있다.

-- 분산 실행 계획 확인
EXPLAIN ANALYZE
SELECT o.id, o.total_amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
  AND o.created_at > now() - INTERVAL '24 hours'
ORDER BY o.created_at DESC
LIMIT 50;

-- 실행 결과 예시:
-- planning time: 2ms
-- execution time: 45ms
-- distribution: full (3개 노드에서 병렬 실행)
-- vectorized: true
--
-- • limit (count: 50)
-- │ estimated row count: 50
-- │
-- └── • sort (order: created_at DESC)
--     │
--     └── • lookup join
--         │ table: users@primary
--         │ equality: (user_id) = (id)
--         │
--         └── • filter
--             │ filter: (status = 'pending') AND (created_at > ...)
--             │
--             └── • scan
--                   table: orders@idx_orders_status_created
--                   spans: [/'pending' - /'pending']

핵심 성능 튜닝 파라미터

-- 클러스터 레벨 설정
SET CLUSTER SETTING kv.rangefeed.enabled = true;
SET CLUSTER SETTING kv.range_merge.queue_enabled = true;
SET CLUSTER SETTING sql.defaults.distsql = 'auto';

-- 통계 자동 수집 주기 조정 (대규모 테이블)
SET CLUSTER SETTING sql.stats.automatic_collection.min_stale_rows = 500;

-- 세션 레벨 최적화
SET vectorize = 'on';
SET distsql = 'auto';
SET reorder_joins_limit = 6;

-- 대용량 일괄 작업 시 범위 잠금 최소화
SET CLUSTER SETTING kv.bulk_io_write.max_rate = '256MiB';

인덱스 전략

CockroachDB에서 인덱스 설계는 분산 환경의 특성을 고려해야 한다.

-- 복합 인덱스: 쿼리 패턴에 맞게 컬럼 순서 설계
CREATE INDEX idx_orders_user_status ON orders (user_id, status)
STORING (total_amount, created_at);

-- 부분 인덱스: 활성 주문만 인덱싱하여 스토리지 절약
CREATE INDEX idx_active_orders ON orders (user_id, created_at DESC)
WHERE status IN ('pending', 'processing');

-- 역순 인덱스: 최신 데이터를 자주 조회하는 패턴
CREATE INDEX idx_orders_recent ON orders (created_at DESC)
STORING (status, total_amount);

-- 인덱스 사용 현황 확인
SELECT ti.index_name,
       ts.range_count,
       ts.approximate_disk_bytes / 1024 / 1024 AS size_mb,
       s.statistics->>'query_count' AS query_count
FROM crdb_internal.table_indexes ti
JOIN crdb_internal.index_usage_statistics s
  ON ti.index_id = s.index_id AND ti.descriptor_id = s.table_id
JOIN crdb_internal.table_span_stats ts
  ON ti.descriptor_id = ts.table_id
WHERE ti.descriptor_name = 'orders'
ORDER BY query_count DESC;

8. 장애 시나리오와 복구 절차

시나리오 1: 단일 노드 장애

가장 흔한 장애 유형이다. 3개 복제본 구성에서 1개 노드가 다운되어도 나머지 2개가 쿼럼을 형성하므로 서비스는 중단되지 않는다.

탐지: CockroachDB 내장 UI의 Node Status에서 DEAD 또는 SUSPECT 상태를 확인한다.

# 노드 상태 확인
cockroach node status --insecure --host=node1.example.com:26257

# 특정 노드의 로그 확인
cockroach debug zip /tmp/debug.zip --insecure --host=node1.example.com:26257

# 장애 노드 안전하게 퇴역 (복구 불가 시)
cockroach node decommission <node_id> --insecure --host=node1.example.com:26257

복구 절차:

  1. 장애 원인 파악 (디스크 장애, OOM, 네트워크 등)
  2. 원인 해결 후 노드 재시작 - CockroachDB가 자동으로 Raft 로그를 적용하여 동기화
  3. 복구 불가 시 node decommission으로 안전하게 제거 후 새 노드 추가
  4. 복제본 수가 부족한 Range가 자동으로 다른 노드에 새 복제본을 생성하는 것을 확인

시나리오 2: 리전 전체 장애

SURVIVE REGION FAILURE로 설정된 데이터베이스는 하나의 리전이 완전히 사라져도 서비스를 지속한다. 다만 장애 리전에 Leaseholder가 있던 Range는 새로운 Leaseholder를 선출하는 동안 일시적인 지연이 발생한다.

복구 절차:

  1. 장애 리전의 노드들이 자동으로 DEAD 판정될 때까지 대기 (기본 5분)
  2. 나머지 리전에서 Raft 리더 재선출이 자동으로 진행됨을 확인
  3. 장애 리전 복구 후 노드들을 재시작하면 자동으로 클러스터에 재합류
  4. Leaseholder 재배치가 완료될 때까지 지연 모니터링

시나리오 3: 시계 동기화 실패

CockroachDB는 노드 간 시계 오차가 --max-offset 값의 80%를 초과하면 해당 노드가 자발적으로 종료된다. NTP 서비스 장애가 이 상황을 유발할 수 있다.

# 노드 간 시계 오차 확인
cockroach debug timeutil --insecure --host=node1.example.com:26257

# NTP 동기화 상태 확인
chronyc tracking
chronyc sources -v

# NTP 강제 동기화
sudo chronyc -a makestep

예방 조치:

  • 모든 노드에 chrony 또는 ntpd를 설치하고 상시 동기화 상태를 모니터링한다.
  • --max-offset을 지나치게 낮게 설정하면 네트워크 지터에 의한 불필요한 노드 종료가 발생할 수 있으므로 환경에 맞게 조정한다.
  • 클라우드 환경에서는 해당 클라우드의 NTP 서비스(AWS의 169.254.169.123 등)를 사용한다.

시나리오 4: 레인지 언더레플리케이션

복제본 수가 목표(기본 3)보다 부족한 Range가 발생하면 데이터 안전성이 위협받는다.

-- 언더레플리케이트된 레인지 확인
SELECT range_id, start_key, end_key, replicas, lease_holder
FROM crdb_internal.ranges_no_leases
WHERE array_length(replicas, 1) < 3;

-- 레플리케이션 큐 상태 확인
SELECT store_id, queue_name, process_count, process_failure_count
FROM crdb_internal.kv_store_status
WHERE queue_name = 'replicate';

9. Jepsen 테스트와 일관성 보장

CockroachDB는 분산 시스템의 일관성을 검증하는 Jepsen 테스트를 지속적으로 수행한다. Jepsen은 Kyle Kingsbury(Aphyr)가 개발한 프레임워크로, 네트워크 파티션, 프로세스 크래시, 시계 오차 등의 장애를 주입하면서 데이터베이스의 일관성 보장을 검증한다.

CockroachDB의 일관성 특성:

  • Serializable Isolation: 시계가 올바르게 동기화된 상태에서 모든 트랜잭션이 직렬화 가능한 순서로 실행됨을 보장한다.
  • Linearizable per Key: 개별 키에 대해서는 선형화 가능성(Linearizability)을 제공한다.
  • Strict Serializability는 아님: 서로 다른 키에 걸친 트랜잭션에서 실시간 순서와 일치하지 않는 이상 현상이 발생할 수 있다. 이는 TrueTime 없이 HLC만 사용하는 아키텍처적 제약이다.

CockroachDB 팀은 매일 밤 5노드 클러스터에서 7가지 워크로드와 다양한 장애 시나리오를 조합한 Jepsen 테스트를 자동 실행하며, 2년 이상의 연속 테스트를 통해 발견된 버그(타임스탬프 캐시 관련 불일치, 내부 재시도에 의한 중복 적용 등)를 모두 수정했다.

10. 운영 모니터링과 알림

핵심 모니터링 지표

CockroachDB의 내장 UI(기본 포트 8080)는 풍부한 대시보드를 제공한다. Prometheus와 Grafana를 함께 사용하면 장기 추이 분석과 맞춤 알림이 가능하다.

# Prometheus scrape 설정
scrape_configs:
  - job_name: 'cockroachdb'
    metrics_path: '/_status/vars'
    scheme: 'http'
    static_configs:
      - targets:
          - 'node1.example.com:8080'
          - 'node2.example.com:8080'
          - 'node3.example.com:8080'
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance
        regex: '(.+):8080'
        replacement: '${1}'

핵심 알림 규칙:

지표임계값의미
liveness_livenodes기대값보다 낮음노드 장애 발생
ranges_underreplicated> 0 (5분 이상 지속)복제본 부족, 데이터 위험
ranges_unavailable> 0쿼럼 상실, 서비스 영향
sql_service_latency_p99> 500ms쿼리 성능 저하
admission_io_overload> 0.8디스크 I/O 병목
clock_offset_mean> max_offset * 0.6시계 동기화 위험
capacity_used_percent> 70%디스크 용량 부족 접근

11. 프로덕션 운영 체크리스트

클러스터 배포 전

  • 최소 3개 노드를 서로 다른 가용 영역에 배치할 계획 확인
  • 각 노드에 최소 4 CPU, 16GB RAM, SSD 스토리지 할당
  • --locality 플래그에 region과 zone을 정확히 지정
  • --max-offset을 환경에 맞게 설정 (멀티 리전: 250ms 권장)
  • NTP 동기화 구성 및 모니터링 설정 완료
  • 네트워크 방화벽에서 26257(gRPC), 8080(HTTP UI) 포트 개방
  • TLS 인증서 생성 및 적용 (프로덕션 필수)

스키마 설계

  • Primary Key에 UUID 또는 시퀀스를 사용하여 핫스팟 방지
  • SERIAL 대신 UUIDunique_rowid()를 Primary Key로 사용
  • 멀티 리전 시 REGIONAL BY ROW 적용 여부 검토
  • 인덱스에 STORING 절을 활용하여 커버링 인덱스 구성
  • 외래 키 제약 조건 사용 시 분산 환경에서의 성능 영향 측정

운영 중

  • ranges_underreplicatedranges_unavailable 알림 설정
  • 디스크 사용량 70% 경고, 85% 위험 알림 설정
  • 주기적으로 cockroach debug zip으로 진단 번들 수집
  • 메이저 버전 업그레이드 전 반드시 릴리스 노트 확인 및 스테이징 테스트
  • Changefeed 지연 모니터링 및 백프레셔 설정
  • 정기적인 BACKUP 스케줄 설정 및 복원 테스트 수행

장애 대비

  • cockroach node decommission 절차 사전 숙지 및 훈련
  • 리전 장애 시나리오 정기 모의 훈련 (연 1회 이상)
  • 백업에서 특정 테이블만 복원하는 절차 테스트
  • 클러스터 재시작 런북 작성 및 팀 공유

마치며

CockroachDB는 분산 SQL 데이터베이스의 복잡성을 상당 부분 추상화하여, PostgreSQL에 익숙한 개발자가 비교적 적은 학습 비용으로 글로벌 분산 시스템을 구축할 수 있게 한다. Raft 합의를 통한 강력한 일관성 보장, 멀티 리전 네이티브 지원, 온라인 스키마 변경, Serializable 격리 수준의 기본 제공은 CockroachDB를 금융, 이커머스, SaaS 등 데이터 일관성이 중요한 서비스에 매력적인 선택지로 만든다.

그러나 만능은 아니다. HLC 기반이므로 Google Spanner의 TrueTime만큼의 강력한 외부 일관성을 제공하지는 않으며, TiDB의 TiFlash 같은 강력한 HTAP 기능도 아직 부족하다. 멀티 리전 쓰기 지연은 물리 법칙의 제약이므로 아키텍처 설계 단계에서 이를 반영해야 한다. BSL 라이선스 정책도 도입 검토 시 법무 검토가 필요한 부분이다.

핵심은 자신의 워크로드와 요구 사항에 맞는 데이터베이스를 선택하는 것이다. 이 글에서 다룬 아키텍처 이해, 클러스터 구성, 멀티 리전 토폴로지 설계, 성능 튜닝, 장애 복구 절차가 CockroachDB 도입을 검토하거나 운영 중인 분들에게 실질적인 가이드가 되기를 바란다.

참고자료