왜 복제본 불일치가 문제인가

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 조합을 강제한다.