복제 계층
복제 계층 (Replication Layer)
고가용성(high availability)은 노드 몇 개가 오프라인이 되어도 애플리케이션에 중단 없이 서비스를 제공할 수 있어야 한다는 뜻이에요. 그걸 실현하려면 데이터를 노드 사이에 복제해 항상 접근 가능하게 만들어야 하죠. 그런데 노드가 꺼져 있는 상태에서도 일관성을 지키는 건 많은 데이터베이스가 풀지 못하는 난제예요. CockroachDB는 이 문제를 합의 알고리즘(consensus algorithm)으로 풀어요.
개요
CockroachDB는 range의 변경이 커밋되기 전에 복제본들의 **쿼럼(quorum)**이 동의하도록 만들어요. 쿼럼을 만들 수 있는 가장 작은 수는 3(3개 중 2개)이기 때문에, CockroachDB의 고가용성(다중 활성 가용성)은 기본적으로 3개 노드를 필요로 해요. 견딜 수 있는 장애 수는 (복제 팩터 - 1) / 2 로 계산해요. 예를 들어 3x 복제면 장애 1개, 5x 복제면 장애 2개를 견딜 수 있죠. 복제 팩터는 클러스터·데이터베이스·테이블 레벨에서 조절할 수 있어요.
장애가 나면 CockroachDB는 노드가 응답을 멈췄다는 걸 자동으로 감지하고, 생존성을 최대화하도록 데이터를 재분배해요. 반대로 새 노드가 클러스터에 합류하면 데이터가 자동으로 그 노드로 리밸런싱되어 부하가 고르게 분산돼요.
복제 계층은 분산 계층(distribution layer)에서 요청을 받아 응답을 보내고, 승인된 요청을 스토리지 계층에 기록해요.
Raft
Raft는 합의 프로토콜로, 데이터를 여러 머신에 안전하게 저장하고 일부 머신이 잠시 끊겨도 머신들이 현재 상태에 동의하도록 보장해요. Raft는 range의 복제본을 가진 모든 노드를 Raft 그룹으로 묶어요. 그룹의 각 복제본은 "리더" 또는 "팔로워"가 되는데, 리더는 Raft가 선출하고 오래 유지되며 모든 쓰기를 조정해요. 리더는 주기적으로 팔로워에게 하트비트를 보내고 로그를 복제해요. 하트비트가 없으면 팔로워들이 무작위 선출 타임아웃 후 후보가 되어 새 리더 선거를 진행해요. 참고로 low-latency 다중 지역 읽기를 가능하게 하는 non-voting 복제본이라는 세 번째 유형도 있어요.
노드가 자신이 가진 range에 대한 BatchRequest를 받으면 KV 연산을 Raft 명령으로 바꾸고, 그 명령을 Raft 그룹 리더(즉 leaseholder)에게 제안해서 Raft 로그에 기록해요.
Raft 로그
쓰기가 쿼럼을 얻고 Raft 그룹 리더가 커밋하면 Raft 로그에 추가돼요. 이 로그는 복제본들이 합의한 명령의 순서 있는 집합으로, 일관된 복제의 원천이에요. 로그는 직렬화 가능하게 취급되므로 지난 상태의 노드를 현재 상태로 되감아 재생(replay)할 수 있어요. 또한 잠시 오프라인이었던 노드가 스냅샷 형태로 데이터 사본을 받지 않고도 로그만으로 "따라잡을" 수 있게 해줘요.
Non-voting 복제본
v21.1 이전에는 voting 복제본만 지원했어요. 모든 복제본이 합의에 참여해야 하니 복제 팩터를 늘리면 추가 복제본이 Raft에 참여해야 해서 쓰기 지연이 커지는 문제가 있었죠. 이 때문에 읽기는 할 수 있지만 쿼럼에는 참여하지 않는 non-voting 복제본이 도입됐어요. non-voting 복제본은 Raft 로그를 따라가며 읽기를 서빙할 수 있지만 쿼럼에는 참여하지 않아 쓰기 지연에 거의 영향을 주지 않아요. read-only 복제본이라고도 불러요. 복제 구성을 통해 설정 가능한데, num_voters가 num_replicas보다 작으면 그 차이만큼이 non-voting 복제본이 돼요.
복제본별 서킷 브레이커 (Per-replica circuit breakers)
개별 복제본이 일시적으로 사용 불가능해지면, 요청이 무한정 매달리지 않도록 복제본별 "서킷 브레이커" 메커니즘이 그 range로 가는 요청을 거부해요. range가 결국 일시적으로 사용 불가능한 range에 접근해서 실패할 것이라면, 60초(기본값) 후에 그 range의 브레이커가 트립되어 ReplicaUnavailableError 오류를 사용자에게 알려줘요. 그동안 CockroachDB는 range의 가용성을 비동기로 계속 probe하다가, 복제본이 다시 가능해지면 브레이커를 리셋해 다시 요청을 서빙해요.
서킷 브레이커는 기본적으로 활성화되어 있고, 대부분의 사용자는 별도 설정이 필요 없어요. 타임아웃 값은 클러스터 설정 kv.replica_circuit_breaker.slow_replication_threshold로 제어되며 기본값은 1m0s(1분)예요. 알려진 제한 사항은, 브레이커는 복제본별로 동작하므로 range의 모든 복제본이 죽으면 트립되지 않는다는 점이에요.
Lease
Raft 그룹의 단일 노드가 leaseholder 역할을 해요. leaseholder는 range의 읽기를 서빙하거나 쓰기를 Raft 그룹 리더에게 제안할 수 있는 유일한 노드예요. CockroachDB는 Leader leases 메커니즘으로 leaseholder가 항상 Raft 그룹 리더이도록 보장해요. 이렇게 하면 쓰기 속도가 최적화되고 네트워크 파티션·노드 liveness 실패에 더 강해져요.
leaseholder가 쓰기 요청을 받으면 복제본 노드들의 과반이 서로 통신하며 쓰기를 조정해요. leaseholder가 없으면 요청을 받은 노드가 그 range의 leaseholder가 되려고 시도해요. 노드가 끊기면 store liveness 갱신을 멈추고 모든 lease를 잃어요. 일관된 읽기를 서빙할 때 leaseholder는 Raft를 우회하는데, leaseholder의 쓰기가 처음부터 커밋되려면 이미 합의를 달성했어야 하므로 같은 데이터에 대한 두 번째 합의는 불필요하기 때문이에요. 이 덕분에 Raft가 요구하는 네트워크 왕복 지연을 피하면서 읽기 속도를 크게 높일 수 있고, 일관성은 잃지 않아요.
CockroachDB는 CAP 정리 기준으로 CAP-Consistent(CP) 시스템으로 간주돼요. 데이터 일관성을 우선시하면서도 leaseholder와 복제본의 조정 덕분에 고가용성도 제공해서, 일관성과 가용성 사이의 트레이드오프를 최소화해요.
중요한 값과 타임아웃
| 상수 | 기본값 | 비고 |
|---|---|---|
| Raft election timeout | * | COCKROACH_RAFT_ELECTION_TIMEOUT_TICKS로 제어되고 기본 tick 간격에 곱해져 타임아웃 값이 정해져요. 선거 동률을 피하려고 1~2 사이 무작위 요소가 곱해져요. |
| Raft proposal timeout | * | COCKROACH_RAFT_REPROPOSAL_TIMEOUT_TICKS로 제어돼요. |
| Lease interval | COCKROACH_RANGE_LEASE_DURATION으로 제어돼요. |
|
| Lease acquisition timeout | ||
| Raft tick interval | COCKROACH_RAFT_TICK_INTERVAL로 제어되고 여러 복제 관련 타임아웃 계산에 쓰여요. |