1P by GN⁺ 1일전 | ★ favorite | 댓글 1개
  • 분산 메시징 시스템 NATS JetStream의 내구성과 일관성을 Jepsen이 다양한 장애 환경에서 검증
  • 테스트 결과, 파일 손상(.blk, snapshot)전원 장애 시뮬레이션에서 데이터 손실과 split-brain 현상이 발생
  • JetStream은 기본적으로 fsync를 2분마다 수행해, 최근 승인된 메시지가 디스크에 기록되지 않은 상태로 남을 수 있음
  • 단일 노드의 OS 크래시만으로도 데이터 손실과 복제본 불일치가 유발될 수 있음
  • Jepsen은 NATS에 fsync=always 기본 설정 변경 또는 데이터 손실 위험 명시적 문서화를 권고

1. 배경

  • NATS는 메시지를 스트림으로 발행·구독하는 인기 있는 스트리밍 시스템
    • JetStream은 Raft 합의 알고리듬을 사용해 데이터를 복제하고, 최소 한 번(at-least-once) 전달을 보장
  • JetStream은 문서상 Linearizable 일관성항상 가용성을 주장하지만, CAP 정리에 따라 두 조건을 동시에 만족할 수 없음
  • NATS 문서에 따르면 3노드 스트림은 1대, 5노드 스트림은 2대의 서버 손실을 견딜 수 있음
  • 메시지는 서버가 publish 요청을 acknowledge한 시점에 “성공적으로 저장됨”으로 간주
  • 데이터 일관성을 위해 과반수(quorum) 노드가 필요하며, 5노드 클러스터에서는 최소 3대가 동작해야 새 메시지를 저장 가능

2. 테스트 설계

  • Jepsen은 JNATS 2.24.0 클라이언트와 Debian 12 LXC 컨테이너 환경에서 테스트 수행
    • 일부 테스트는 Antithesis 환경에서 공식 NATS Docker 이미지를 사용
  • 단일 JetStream 스트림(복제 5)을 구성하고, 프로세스 중단·충돌·네트워크 분할·패킷 손실·파일 손상 등을 주입
  • LazyFS 파일시스템을 사용해 fsync되지 않은 쓰기를 손실시키는 전원 장애 시뮬레이션 수행
  • 각 프로세스는 고유 메시지를 발행하고, 테스트 종료 후 모든 노드에서 acknowledged 메시지의 존재 여부를 검증
  • 메시지가 일부 노드에서만 존재할 경우 divergence(복제 불일치) 로 분류

3. 주요 결과

3.1 NATS 2.10.22의 전체 데이터 손실 (#6888)

  • 단순 프로세스 충돌만으로 JetStream 스트림 전체가 사라지는 현상 발견
  • "No matching streams for subject" 오류 발생 후 수 시간 동안 복구되지 않음
  • 원인은 리더 스냅샷 역전, Raft 상태 삭제 등으로, 2.10.23 버전에서 수정됨

3.2 .blk 파일 손상 시 데이터 손실 (#7549)

  • JetStream의 .blk 파일에 단일 비트 오류나 잘림(truncation) 발생 시 수십만 건의 승인된 쓰기 손실
    • 예: 1,367,069건 중 679,153건 손실
  • 일부 노드만 손상되어도 대규모 데이터 손실 및 split-brain 발생
    • 예: 노드 n1, n3, n5에서 최대 78% 메시지 손실
  • NATS는 해당 문제를 조사 중

3.3 스냅샷 파일 손상 시 전체 데이터 삭제 (#7556)

  • data/jetstream/$SYS/_js_/ 내 스냅샷 파일이 손상되면, 노드가 스트림을 고아(orphaned) 로 판단하고 데이터 전체 삭제
  • 소수 노드만 손상되어도 클러스터 과반수 불가 및 스트림 영구 불가용
  • 예: 노드 n3, n5 손상 → n3이 리더로 선출되어 jepsen-stream 전체 삭제
  • Jepsen은 리더 선출 시 손상된 노드가 리더가 되는 위험성을 지적

3.4 기본 fsync 설정으로 인한 데이터 손실 (#7564)

  • JetStream은 기본적으로 2분마다만 fsync 수행, 메시지는 즉시 승인
    • 결과적으로 최근 승인된 메시지가 디스크에 기록되지 않은 상태로 남음
  • 전원 장애나 커널 크래시 시 수십 초 분량의 승인된 메시지 손실 발생
    • 예: 930,005건 중 131,418건 손실
  • 단일 노드 장애가 연속 발생해도 전체 스트림 삭제 가능
  • 문서에는 이 동작이 거의 언급되지 않음
  • Jepsen은 fsync=always 기본값 변경 또는 데이터 손실 위험 명시적 경고를 권장

3.5 단일 OS 크래시로 인한 split-brain (#7567)

  • 단일 노드의 전원 장애 또는 커널 크래시만으로도 데이터 손실 및 복제 불일치 발생 가능
  • 리더-팔로워 구조에서 일부 노드가 메모리에만 커밋된 상태로 승인 후 장애 발생 시,
    다수 노드가 해당 쓰기를 잃고 새로운 상태로 진행
  • 테스트에서 단일 전원 장애 후 지속적 split-brain 발생
    • 노드별로 서로 다른 구간의 승인 메시지 손실 확인
  • Jepsen은 Kafka의 유사 사례를 인용하며, Raft 기반 시스템에서도 동일 위험 존재를 강조

4. 논의 및 결론

  • 2.10.22의 전체 데이터 손실 문제는 2.10.23에서 해결
  • 2.12.1에서는 파일 손상 및 OS 크래시로 인한 데이터 손실·split-brain이 여전히 발생
  • .blk 및 스냅샷 파일 손상 시 일부 노드에서 메시지 누락 또는 전체 스트림 삭제 발생
  • 기본 fsync 주기가 길어 복수 노드 동시 장애 시 승인된 데이터 손실 위험 존재
  • Jepsen은 fsync=always 설정 또는 문서 내 명확한 위험 고지를 제안
  • JetStream의 “항상 가용” 주장은 CAP 정리상 불가능, 문서 수정 필요
  • Jepsen은 버그 존재는 입증 가능하지만, 안전성의 부재는 증명 불가함을 명시

4.1 LazyFS의 역할

  • LazyFS를 이용해 fsync되지 않은 쓰기 손실을 시뮬레이션
  • 전원 장애 시 부분적 쓰기 손상(torn write) 등 다양한 스토리지 오류 재현 가능
  • 관련 연구 *When Amnesia Strikes (VLDB 2024)*에서 PostgreSQL, Redis, ZooKeeper 등에서도 유사 버그 보고

4.2 향후 과제

  • 단일 소비자 수준의 메시지 손실, 메시지 순서, Linearizable/Serializable 보장 검증은 미실시
  • 정확히 한 번(exactly-once) 전달 보장도 향후 연구 대상
  • 노드 추가·제거 시 문서 오류 및 필수 health check 단계 누락 발견 (#7545)
  • 안전한 클러스터 구성 변경 절차는 아직 불명확

Hacker News 의견
  • 누군가 복잡한 이론을 건너뛰고 이런 시스템을 만들 때마다 aphyr가 그걸 무너뜨리는 걸 봄
    이제는 AI가 프로젝트 문서를 읽고 데이터 손실 가능성을 마케팅 문구만으로 예측할 수 있을지도 궁금해짐
    • 긴 수염을 쓰다듬으며 고개를 끄덕이는 기분임
      사람들은 늘 “이론은 과대평가됐다”거나 “학교 교육보다 해킹이 낫다”고 말하지만, 결국 문서화된 문제 공간에서 스스로 발목을 잡는 꼴이 됨
    • 나도 LLM에게 비슷한 일을 시켜봤는데, 결과가 꽤 유용했음
  • NATS가 CAP 이론을 무시하는 듯한 느낌임
    • 과소평가된 발언 같음
  • 나는 NATS를 in-memory pub/sub 용도로 써왔는데, 그 부분에서는 훌륭했음
    미묘한 스케일링 디테일도 잘 처리했음
    하지만 persistence는 써본 적 없고, 이렇게까지 취약할 줄은 몰랐음
    단일 비트 파일 손상에도 취약하다니 당황스러움
  • 관련된 자료로, Jepsen과 Antithesis가 최근에 분산 시스템 용어집을 공개했음
    참고하기에 아주 좋은 자료임 → Jepsen Glossary
  • aphyr.com/tags/jepsen과 jepsen.io/analyses의 콘텐츠 차이가 궁금했음
    최근 aphyr.com을 발견했는데, 통찰이 많을 것 같아 기대 중임
    • Jepsen은 원래 개인 블로그 시리즈로 시작했음
      이후 jepsen.io는 전문 프로젝트로 발전했고, 약 10년 전부터 본격적으로 운영하기 시작했음
  • “Lazy fsync by Default” 설정이 왜 존재하는지 의문임
    벤치마크 성능을 높이려는 이유인가? 작은 클러스터에서는 이런 설정이 문제의 원인이 되곤 함
    • 단순히 지연 시간뿐 아니라 처리량(throughput) 향상도 있음
      많은 애플리케이션은 완전한 내구성을 요구하지 않기 때문에 lazy fsync가 유용할 수 있음
      다만 기본값으로 두는 건 논란의 여지가 있음
    • fsync를 왜 반드시 지연시켜야 하는지 늘 궁금했음
      TCP corking처럼 묶음 처리(batch) 로 해결할 수 있을 것 같음
    • 분산 시스템이라 가능한 일 중 하나임
      lazy fsync로 인한 실패는 대부분의 노드에서 동시에 일어나지 않기 때문임
    • 성능 향상을 위한 선택이 맞음
    • 복제와 분산을 통한 내구성과, lazy fsync로 확보한 처리량을 함께 노림
  • JetStream의 서버리스 대안으로 s2.dev를 추천함
    장점: 객체 스토리지 수준의 내구성을 가진 무제한 스트림 지원
    단점: 아직 consumer group 기능이 없음
    • Jepsen 테스트를 돌려본 적 있는지 궁금함
  • NATS가 기본적으로 2분마다만 fsync를 수행하고, 즉시 ack를 반환한다는 점이 문제임
    여러 노드가 동시에 장애를 겪으면 커밋된 데이터 손실이 발생할 수 있음
    MongoDB 초창기 시절의 “웹 스케일” 마케팅이 떠오름
    기본값은 항상 가장 안전한 옵션이어야 한다고 생각함
    • NATS는 클러스터 가용성만 보장한다고 명확히 밝힘
      그 점이 오히려 좋았고, 그 위에 시스템을 설계할 수 있었음
      2018년에 사용했을 때는 성능도 좋고 관리도 쉬웠음
    • 대부분의 현대 DB도 기본값이 완전 안전하지 않음
      예를 들어 PostgreSQL의 기본 트랜잭션 격리 수준은 read committed
      Redis도 기본적으로 1초마다 fsync함
    • Redis 클러스터는 다수 노드에 복제된 후에만 ack를 반환함
      standalone Redis에서도 fsync 후 ack 설정이 가능하지만, OS 버퍼링 때문에 완전 보장은 어려움
      결국 ack의 의미를 정확히 이해하는 게 중요함
    • 대부분의 시스템은 속도와 내구성의 절충을 택함
      안전한 기본값만 고집하면 성능이 크게 떨어지고, 사용자가 직접 튜닝해야 하는 부담이 커짐
      예를 들어 Postgres의 기본 격리 수준도 약해서 race condition이 발생할 수 있음
      참고: Hermitage 테스트 글
    • fsync는 극단적인 선택지밖에 없는 게 문제임
      SSD 시대에는 group-commit 같은 중간 단계가 사라졌고, 이제는 syscall 전환 비용이 병목임
      2분은 너무 긴 주기임 (fdatasync vs fsync 차이도 고려해야 함)
  • 솔직히 예상은 했지만, 이렇게까지 심각할 줄은 몰랐음
    그냥 Redpanda를 쓰는 게 나을 듯함
  • NATS의 fsync 성능 경고를 개선할 수 있을지 궁금했음
    일정 주기로 배치 플러시(batch flush) 를 하면 지연은 늘겠지만 처리량은 유지할 수 있지 않을까 생각함
    • 고정된 주기가 아니라, fsync가 진행 중일 때 쓰기 요청을 큐잉하고 다음 배치에 함께 처리하면 됨
      이는 Paxos 라운드를 묶는 방식과 유사함
      한 라운드가 끝나면 바로 다음 배치를 시작하는 식으로 처리해야 함