이번 회고에서는 서로 다른 리전에서 읽은 타임스탬프를 직접 비교하다 발생한 팬텀 리드 사례를 뜯어봤다. 원인은 애플리케이션 로직 버그가 아니라 로컬 wall clock 값을 인과 순서의 근거로 쓴 데 있었다. 결론부터 말하면, 수정 방향은 시계를 더 정확히 맞추는 쪽이 아니라 불확실성 구간을 명시적으로 다루는 쪽이었다.

클록 스큐만으로 팬텀 리드가 발생할 수 있나?

가능하다. 노드 A와 노드 B의 로컬 시계가 수 밀리초만 어긋나도, 그 값을 직접 비교해 순서를 매기면 실제 인과 순서와 타임스탬프 순서가 뒤바뀔 수 있다. Spanner가 TrueTime을 단일 시각이 아니라 [earliest, latest] 구간으로 다루는 이유도 여기에 있다. TT.now()는 실제 시간이 존재한다고 보장되는 구간을 반환하고, 그 폭(ε)은 로컬 드리프트·시간 서버 통신 지연·서버 자체 불확실성을 합쳐 계산된다.Spanner 논문

  • 로컬 wall clock 값만으로 노드 간 이벤트 순서를 비교하면 수 ms의 clock skew만으로도 팬텀 리드가 발생할 수 있다.
  • TrueTime은 시각 하나가 아니라 구간으로 불확실성을 노출하고, commit wait로 커밋 타임스탬프가 실제 과거임을 확인한 뒤에야 커밋을 완료한다.
  • 공개된 1-replica 실험에서 commit wait는 약 5ms, Paxos 지연은 약 9ms였지만, 이는 4KB 단독 읽기·쓰기라는 특정 조건의 수치다.
  • 최종 일관성으로 낮추면 commit wait는 줄지만, 오래된 버전·재정렬된 이벤트·재시도 결과를 처리할 책임이 애플리케이션으로 넘어간다.
  • 운영에서는 평균 지연보다 true_time_interval_uncertainty의 p95·p99와 커밋 지연의 동시 상승을 봐야 한다.Spanner Omni 문서

증상은 멀티 리전 배포의 읽기 일관성 점검 스크립트에서 시작됐다. 한 리전에서 쓰기를 커밋한 직후 다른 리전에서 같은 키를 읽으면 간헐적으로 이전 값이 반환됐다. 로그를 보면 쓰기 타임스탬프가 읽기 타임스탬프보다 나중으로 찍힌 경우가 발생 빈도의 대부분을 차지했고, 같은 요청을 재시도하면 증상이 사라졌다. 초기에는 복제 지연으로 보였지만, 복제 완료 플래그가 이미 true로 찍힌 상태에서도 같은 현상이 재현됐다는 점이 복제 지연 가설과 맞지 않았다.

복제 지연이 원인이었을까?

아니었다. 복제 완료 플래그가 true인 시점 이후에도 동일한 역전 현상이 재현됐기 때문에, 데이터 자체가 아직 도착하지 않은 상태라는 가설은 성립하지 않았다. Spanner 논문에서 리플리카 수를 늘렸을 때 지연 평균은 대체로 일정하고 표준편차만 줄었다는 관찰과 맞춰보면, 복제 경로의 변동성은 이 사고의 재현 빈도를 설명하기에 부족했다.Spanner 논문 복제는 끝났는데 "누가 먼저 썼는지"를 판정하는 비교 로직이 틀렸다는 쪽으로 의심이 옮겨갔다.

NTP 동기화가 깨진 것이었을까?

이것도 아니었다. NTP 동기화 상태를 확인했을 때 오프셋은 정상 범위 안에 있었다. 문제는 그 "정상 범위" 자체가 수 ms 수준의 오차를 구조적으로 허용한다는 점이었다. Spanner가 TrueTime 불확실성을 1~7ms, 평상시 약 4ms로 두는 것도 시계가 고장 났기 때문이 아니라 정상 동작 범위 안에서 발생하는 드리프트·통신 지연을 반영한 값이다.Spanner 논문 즉 NTP가 알림을 띄울 만큼 깨지지 않아도, 애플리케이션이 그 수 ms를 "0으로 취급"하면 순서가 뒤집힐 여지는 그대로 남는다.

애플리케이션 재시도 로직 버그였을까?

처음엔 그렇게 보였다. 재시도하면 사라지는 증상은 전형적인 경쟁 조건 버그의 모양을 하고 있었다. 하지만 동시성 수준, clock offset 주입값, offset 지속 시간을 고정해 재현 조건을 통제하자, 특정 offset 구간에서는 재시도 여부와 무관하게 항상 역전이 발생했다. 이는 재시도 로직의 결함이 아니라, 비교 대상인 타임스탬프 자체가 구조적으로 신뢰할 수 없는 값이었다는 뜻이다. 재시도가 증상을 가린 것은 우연히 그 사이 실제 복제·커밋이 진행돼 비교 시점의 타임스탬프 차이가 벌어졌기 때문이었다.

그래서 진짜 원인은 무엇이었나?

원인은 두 노드의 로컬 wall clock 값을 그대로 가져와 "나중에 커밋된 쓰기"를 판정하는 비교 로직이었다. 이 로직은 외부 일관성이 보장되지 않는 환경에서, 트랜잭션 T1이 끝난 뒤 시작된 T2의 타임스탬프가 항상 T1보다 크다는 가정을 암묵적으로 깔고 있었다. 그러나 Spanner가 말하는 외부 일관성은 그 가정이 저절로 성립하는 것이 아니라, commit wait로 확보해야 하는 보장이다. T1 완료 후 T2가 시작되면 T2의 커밋 타임스탬프가 T1보다 커야 하고, 여러 리전·서버에 걸친 읽기에서 T1의 효과를 보지 못한 채 T2의 효과만 보는 상태를 허용하지 않는다는 것이 이 보장의 정의다.Spanner Omni 문서 이 사고의 비교 로직은 그 보장 없이 타임스탬프 순서와 인과 순서를 같은 것으로 취급했다.

재발을 막기 위해 무엇을 바꿨나?

타임스탬프를 단일 값이 아니라 구간으로 다루도록 비교 로직을 바꿨다. 읽기 유형(강한 읽기·스냅샷 읽기·stale read)을 명시적으로 구분하고, 순서 보장이 필요한 경로에는 실제 과거 확정을 기다리는 대기를 넣었다. 동시에 true_time_interval_uncertainty에 해당하는 지표를 모니터링에 추가해 p95·p99가 커밋 지연 증가와 함께 움직이는지 확인하도록 했다.Spanner Omni 문서 재현 보고서에는 제품·버전, 리전·존 배치, 리플리카 수, 트랜잭션 크기, 동시성, 네트워크 RTT, NTP 동기화 상태, 주입한 clock offset과 지속 시간을 모두 적도록 템플릿을 고정했다. 이 항목 중 하나라도 빠지면 다음 사고에서 같은 가설을 처음부터 다시 검증해야 하기 때문이다.

이 사고에서 일반화할 수 있는 교훈은 무엇인가?

시계는 "맞추는 대상"이 아니라 "불확실성을 측정하고 그 구간을 설계에 반영하는 대상"으로 다뤄야 한다는 것이 이 사고의 핵심 교훈이다. 강한 일관성과 최종 일관성의 차이는 지연과 복잡도 사이의 트레이드오프이지, 어느 한쪽이 팬텀 리드를 완전히 없앤다는 식의 단순 비교로 환원되지 않는다. 최종 일관성은 commit wait나 전역 순서 합의 비용을 줄이지만, 그 대가로 오래된 버전·재정렬된 이벤트·중복 이벤트를 처리하는 부담을 애플리케이션에 넘긴다. 2026년 기준으로도 이 선택을 가르는 질문은 "시계를 얼마나 정확히 맞출까"가 아니라 "불확실성 구간을 어디까지 허용하고, 그 구간을 넘는 읽기·쓰기를 언제 차단할까"에 가깝다.

핵심 정리

  • clock skew는 시계 고장이 아니어도 구조적으로 존재하며, 로컬 wall clock 비교만으로 순서를 판정하면 팬텀 리드로 이어질 수 있다.
  • Spanner는 시각을 [earliest, latest] 구간으로 노출하고 commit wait로 실제 과거 확정을 기다려 외부 일관성을 보장한다.
  • 공개된 1-replica 측정치(commit wait 약 5ms, Paxos 지연 약 9ms, 불확실성 1~7ms·평균 4ms)는 특정 실험 조건의 수치이며 모든 환경의 보장값이 아니다.
  • 최종 일관성은 대기 비용을 줄이는 대신 오래된 버전·재정렬·중복 처리 부담을 애플리케이션 쪽으로 옮긴다.
  • 디버깅에서는 평균 지연이 아니라 불확실성 지표의 p95·p99와 커밋 지연 동시 상승을 함께 봐야 한다.

자주 묻는 질문

TrueTime의 불확실성(ε) 값은 항상 몇 ms인가?

공개된 Spanner 논문 기준으로는 1~7ms 범위에서 변동하고 대부분 약 4ms였다. 다만 이는 2012년 측정치이며 2025~2026년 모든 리전·제품 구성에 그대로 적용되는 최신 보장값은 아니다.

최종 일관성으로 전환하면 이번 사고 같은 팬텀 리드가 완전히 사라지나?

아니다. commit wait나 전역 순서 합의 비용은 줄어들지만, 오래된 버전·재정렬된 이벤트·재시도 결과를 애플리케이션이 직접 처리해야 한다. 특정 제품·부하 조건을 통제한 팬텀 리드 비교 벤치마크는 현재 확인된 자료에 없다.

자체 관리형 환경에서는 TrueTime 구조가 어떻게 달라지나?

GPS·원자시계에 직접 의존하는 관리형 Spanner와 달리, 주 시간 서버와 호스트 클라이언트가 네트워크 지연과 clock drift를 계산해 시간 구간을 구성한다. 호스트 시계의 최대 오차율을 알아야 하며, 불확실성이 커지면 commit wait도 함께 늘어날 수 있다.

이런 장애를 다음에 더 빨리 재현하려면 무엇을 기록해야 하나?

제품·버전, 리전·존 배치, 리플리카 수, 트랜잭션 크기, 읽기 유형, 동시성, 네트워크 RTT, NTP/PTP 동기화 상태, 주입한 clock offset과 지속 시간을 모두 남겨야 한다. 이 중 하나라도 빠지면 가설 기각 과정을 처음부터 반복해야 한다.

더 알아보기

분산 일관성과 합의: 시스템 설계의 기본 축 — 이 주제의 종합 가이드