CockroachDB 아키텍처 개요

CockroachDB 아키텍처 개요

CockroachDB가 "확장 가능하면서도 일관된" 데이터베이스를 어떻게 만들어 냈는지 궁금해하는 분들이 많아요. 이 페이지에서는 그 궁금증의 핵심을 풀어드리려고 해요. 물론 아키텍처를 몰라도 CockroachDB를 쓰는 데는 전혀 지장이 없지만, 내부가 어떻게 돌아가는지 알면 더 어려운 배포나 운영을 만났을 때 훨씬 수월해져요.

출처: CockroachDB 공식 문서 — Architecture Overview

CockroachDB가 설계된 이유

CockroachDB는 처음부터 다음 목표를 갖고 만들어졌어요.

  • 사람에게 편한 데이터베이스 — 운영은 가볍고 자동화되어야 하고, 개발자가 로직을 이해하기 쉬워야 해요.
  • 대규모로 확장해도 업계 수준의 일관성을 지키는 것 — 분산 트랜잭션을 지원해 결국 일관성(eventual consistency) 문제나 오래된 읽기(stale read)를 없애려는 거예요.
  • 모든 노드에서 읽기와 쓰기를 동시에 받아들이고, 충돌 없이 항상 켜져 있는 데이터베이스.
  • 특정 플랫폼이나 벤더에 묶이지 않고 어느 환경에서든 유연하게 배포되는 것.
  • 관계형 데이터 작업에 익숙한 도구(즉 SQL)를 그대로 지원하는 것.

동작 방식 개요

클러스터는 두 가지 명령으로 시작해요. 초기 노드 전부에 --join 플래그를 준 cockroach start로 프로세스가 서로 통신할 상대 머신들을 알게 하고, cockroach init으로 한 번만 클러스터를 초기화해요.

초기화가 끝나면 개발자는 SQL API를 통해 CockroachDB와 대화해요. 클러스터의 모든 노드는 대칭적으로 동작해서 어떤 노드에 요청을 보내도 되고, 그래서 로드 밸런서와도 잘 어울려요. 노드가 SQL 원격 프로시저 호출(RPC)을 받으면 키-값(KV) 연산으로 바꾸고, 이 KV 연산이 노드에 데이터를 쌓기 시작해요. 이때 데이터는 range라는 덩어리로 쪼개지고, 각 range는 최소 몇 개의 복제본으로 복제돼 생존성을 보장해요. 노드 몇 개가 죽어도 데이터 사본이 남아서 읽기·쓰기를 계속 제공하고, 데이터를 다른 노드로 일관되게 전달할 수 있죠.

노드가 직접 처리할 수 없는 읽기·쓰기 요청을 받으면, 그 요청을 처리할 수 있는 노드를 찾아서 통신해요. 데이터가 클러스터 어디에 있는지 알 필요 없이 CockroachDB가 추적하고, 각 노드에서 대칭적인 읽기·쓰기가 가능해져요. range의 데이터를 바꿀 때는 **쿼럼(quorum)**에 의존해서, range 복제본의 과반이 변경을 승인해야 커밋돼요. 이것이 어떤 노드와 통신하든 일관된 읽기·쓰기를 보장하는 업계 수준의 격리(isolation)를 가능하게 해요. 최종적으로 데이터는 디스크에서 타임스탬프를 추적하는 효율적인 저장 엔진을 통해 읽고 써지는데, 덕분에 SQL 표준의 AS OF SYSTEM TIME 절을 지원해 과거 시점의 데이터를 찾을 수 있어요.

계층 구조

최상위에서 보면 CockroachDB는 클라이언트의 SQL 문장을 키-값(KV) 데이터로 바꾸고, 그 데이터를 노드들에 분산시킨 뒤 디스크에 저장해요. 아키텍처는 여러 계층으로 나타나는데, 각 계층은 위아래 계층과 "불투명한 서비스"처럼 상호작용해요. 그래서 대부분의 경우 다른 계층의 세부사항을 몰라도 각 계층의 역할을 설명할 수 있어요.

핵심 용어

  • 일관성(Consistency) — 트랜잭션이 영향을 주는 데이터를 허용된 방식으로만 바꾸는 요구사항이에요. CockroachDB는 ACID 의미론과 CAP 정리의 두 가지 의미로 "일관성"을 쓰지만, 둘 다 공식 정의보다는 덜 엄격하게 써요.
  • 격리(Isolation) — 동시에 실행되는 다른 트랜잭션이 내 트랜잭션에 영향을 주는 정도예요. CockroachDB는 SERIALIZABLEREAD COMMITTED 격리 수준을 제공해요.
  • 합의(Consensus) — 트랜잭션이 커밋될지 중단될지에 대해 합의에 도달하는 과정이에요. CockroachDB는 Raft 합의 프로토콜을 사용해요. range에 쓰기가 들어오면 그 range의 복제본을 가진 노드들의 쿼럼이 쓰기를 인정해요. 일부 노드가 오프라인이어도 데이터는 안전하게 저장되고 과반이 동의한다는 뜻이에요. 합의에 도달하지 못하면 일관성을 지키기 위해 진행이 멈춰요.
  • 복제(Replication) — 데이터 사본을 만들고 분산하며 사본들이 일관되게 유지되도록 하는 과정이에요. 쓰기는 커밋으로 간주되기 전에 데이터 사본의 쿼럼까지 전파되어야 해요.
  • 트랜잭션(Transaction) — ACID 의미론 요구사항을 만족하는 데이터베이스 작업 묶음이에요. 일관된 시스템에서 개발자가 데이터를 신뢰할 수 있게 해주는 핵심 기능이에요.
  • 트랜잭션 경합(Transaction contention) — 동시 또는 최근 트랜잭션이 같은 데이터에 쓰려고 해서 트랜잭션이 완료되지 못하거나(잠금 경합), 실행 중인 트랜잭션들 사이의 직렬화 순서에 끼어들지 못해 재시작되는(직렬화 충돌) 상황이에요. 자동 재시도가 불가능하거나 실패하면 클라이언트에 오류가 나가고, SERIALIZABLE 격리에서 클라이언트 앱이 직접 재시도해야 해요.
  • 다중 활성 가용성(Multi-active availability) — 각 노드가 저장된 데이터의 일부(range 단위)에 대해 읽기·쓰기를 처리하는 합의 기반 고가용성 개념이에요. 요청의 100%를 활성 노드가 받는 active-passive 복제나, 모든 노드가 요청을 받지만 읽기가 항상 최신이자 빠르다고 보장하기 어려운 active-active 복제와는 달라요.
  • Cluster / Node / Range / Replica — 클러스터는 하나의 분산 SQL 데이터베이스 서버로 동작하는 노드들의 집합이에요. 노드는 CockroachDB 인스턴스 하나예요. Range는 키-값 쌍의 정렬된 맵인 키스페이스를 나눈 연속 덩어리로, 모든 키는 정확히 하나의 range에 속해요. 복제본(Replica)은 range를 복제한 사본으로, 기본적으로 각 range는 서로 다른 노드에 세 개씩 있어요.
  • Leaseholder — range의 "lease"를 가진 복제본으로, range의 모든 읽기·쓰기 요청을 받고 조정해요. 대부분의 테이블·쿼리에서 일관된 읽기를 제공할 수 있는 유일한 복제본이 되기도 해요. Raft 리더와 항상 같은 복제본이에요.
  • Raft 프로토콜 / 리더 / 로그 — 데이터를 여러 노드에 안전하게 저장하고, 일부 노드가 잠시 끊겨도 노드들이 현재 상태에 동의하도록 보장하는 합의 프로토콜이에요. Raft 리더는 각 range에서 쓰기 요청의 "리더" 역할을 하고, 과반(리더 + 충분한 팔로워)이 Raft 로그 기반으로 동의해야 쓰기를 커밋해요. Raft 로그는 range에 대한 쓰기의 시간순 로그로, 복제본들이 합의한 명령들이며 일관된 복제의 원천이에요.

더 알아보기

  • SQL 계층 — SQL 문장이 어떻게 KV 연산으로 바뀌는지
  • 복제 계층 — 데이터가 노드 사이에서 어떻게 복제·일관성 유지되는지
  • 트랜잭션 계층 — ACID 트랜잭션이 어떻게 구현되는지
  • 스토리지 계층 — 디스크에서 데이터를 읽고 쓰는 방법