왜 복제본 불일치가 문제인가
Cassandra는 여러 노드에 데이터를 복제(replication)하면서 가용성을 확보한다. 하지만 특정 노드가 GC 스톨, 네트워크 파티션, 재시작 등으로 잠깐 쓰기를 놓치면 복제본 간 데이터가 벌어진다. 이 상태를 방치하면 QUORUM이 아닌 낮은 일관성 레벨로 읽을 때 오래된 값이 반환되고, 최악의 경우 삭제된 데이터가 되살아나는 좀비(zombie data) 현상까지 발생한다.
Cassandra는 이 불일치를 세 가지 안티 엔트로피 메커니즘으로 메운다. 실시간에 가까운 힌티드 핸드오프(hinted handoff)와 읽기 복구(read repair), 그리고 주기적인 리페어(nodetool repair)다. 앞의 둘을 정확히 이해해야 리페어 주기와 일관성 레벨을 합리적으로 설계할 수 있다.
힌티드 핸드오프: 다운된 노드를 위한 임시 보관
쓰기 시점에 대상 복제본이 다운 상태면, 코디네이터 노드가 그 쓰기를 "힌트(hint)"로 로컬에 저장해 둔다. 다운된 노드가 살아나면 코디네이터가 보관하던 힌트를 재전송해 놓친 쓰기를 따라잡게 한다. 즉 "네가 없는 동안 이 쓰기들이 있었어"라는 메모를 대신 들고 있는 것이다.
핵심은 힌트에 유효기간이 있다는 점이다. max_hint_window_in_ms(기본 3시간)를 넘겨 다운되어 있으면 그 이후 쓰기는 힌트로 저장되지 않는다. 오래 죽었던 노드는 힌트만으로 복구되지 않으므로 반드시 리페어가 필요하다.
# cassandra.yaml
hinted_handoff_enabled: true
max_hint_window_in_ms: 10800000 # 3시간
hinted_handoff_throttle_in_kb: 1024 # 재전송 속도 제한
max_hints_delivery_threads: 2
hints_directory: /var/lib/cassandra/hints
# 힌트 상태 확인 및 수동 재전송
nodetool statushandoff
nodetool listpendinghints
nodetool handoff resumehandoff
# 특정 노드가 오래 죽었다 살아나면 힌트만 믿지 말고 리페어
nodetool repair -pr my_keyspace
주의: 힌트는 코디네이터 디스크에 쌓인다. 대규모 노드가 장시간 다운되면 힌트가 폭증해 코디네이터 디스크를 압박하고 재전송 시 부하가 튄다. 이때는 힌트 재전송보다 리페어가 더 안전하다.
읽기 복구: 읽는 김에 고친다
읽기 복구는 읽기 요청이 왔을 때 복제본들의 값을 비교해 최신값으로 맞춰주는 메커니즘이다. 두 가지 경로가 있다. 첫째, 읽기에 참여한 복제본 간 불일치가 발견되면 블로킹 읽기 복구로 최신값을 뒤처진 노드에 써주고 나서 결과를 반환한다. 둘째, 요청에 포함되지 않은 나머지 복제본까지 확률적으로 검사하는 read_repair_chance(백그라운드 읽기 복구)가 있었다.
다만 3.0 이후, 특히 4.0에서는 테이블 옵션 read_repair_chance/dclocal_read_repair_chance가 제거되고 read_repair = 'BLOCKING'|'NONE' 옵션으로 정리됐다. 자주 읽히는 데이터는 읽기 복구만으로도 상당 부분 수렴하지만, 읽히지 않는 데이터는 절대 복구되지 않는다는 한계가 있다.
-- 4.0+ 읽기 복구 정책 설정
ALTER TABLE my_keyspace.events
WITH read_repair = 'BLOCKING';
-- 읽기 복구를 확실히 유발하려면 일관성 레벨을 높인다
CONSISTENCY QUORUM;
SELECT * FROM my_keyspace.events WHERE id = 42;
일관성 레벨과의 관계
읽기 복구가 실제로 얼마나 작동하는지는 읽기/쓰기 일관성 레벨에 달렸다. ONE으로만 읽으면 한 복제본만 보므로 비교 자체가 없어 복구가 거의 일어나지 않는다. 강한 일관성이 필요하면 W + R > RF 규칙을 지켜야 한다.
# RF=3 기준 조합 (Python, cassandra-driver)
from cassandra.cluster import Cluster
from cassandra import ConsistencyLevel
from cassandra.query import SimpleStatement
session = Cluster(["10.0.0.1"]).connect("my_keyspace")
stmt = SimpleStatement(
"SELECT * FROM events WHERE id=%s",
consistency_level=ConsistencyLevel.QUORUM, # W=QUORUM와 함께 W+R>RF 보장
)
print(session.execute(stmt, (42,)).one())
세 메커니즘 비교
| 구분 | 트리거 | 커버 범위 | 한계 |
|---|---|---|---|
| 힌티드 핸드오프 | 쓰기 시 노드 다운 | 다운 기간 내 놓친 쓰기 | hint window 초과분·오래된 다운은 못 메움 |
| 읽기 복구 | 읽기 발생 시 | 읽히는 데이터 | 안 읽히는 데이터는 영원히 방치 |
| nodetool repair | 수동/스케줄 | 전체 데이터 | 비용 큼, 주기 관리 필요 |
결론적으로 이 셋은 대체재가 아니라 보완재다. 힌티드 핸드오프와 읽기 복구가 실시간 오차를 줄여주지만, 완전성은 리페어만 보장한다.
실무 운영 체크포인트
- gc_grace_seconds와 리페어 주기: 툼스톤 만료(
gc_grace_seconds, 기본 10일) 이전에 반드시 전체 리페어를 완료해야 좀비 데이터를 막는다. 리페어 주기는 이 값보다 짧게 잡는다. - 힌트 디스크 모니터링:
hints_directory용량과nodetool listpendinghints를 알람에 포함한다. - 읽기 복구에 의존 금지: 콜드 데이터는 읽기 복구가 닿지 않으므로 Reaper 등으로 정기 리페어를 자동화한다.
- 일관성 레벨 검증:
ONE읽기/쓰기를 남발하면 위 메커니즘이 무력화된다. 정합성이 중요한 테이블은QUORUM조합을 강제한다.