1P by GN⁺ | ★ favorite | 댓글 1개
  • Kafka 호환 스트리밍 시스템인 Bufstream 0.1.0~0.1.3 검증에서 Bufstream 자체의 가용성 문제 2건과 안전성 문제 3건이 발견됐고, 0.1.3 기준으로 5건 모두 수정됨
  • 테스트는 Java Kafka Client 3.8.0과 기존 Kafka/Redpanda Jepsen 테스트를 바탕으로 했으며, acks = all, enable.idempotence = true, enable.auto.commit = false, read_committed안전성 우선 설정을 사용함
  • Bufstream 문제는 소비자·생산자 정지, 잘못된 offset 0 응답, 트랜잭션 커밋 손실, fetch API 응답 크기 필터링 버그로 인한 인정된 쓰기 손실을 포함함
  • 조사 과정에서 Kafka Java client와 Kafka 트랜잭션 프로토콜에서도 Consumer.close() 무기한 블록, 예측 불가능한 consumer offset, aborted read·lost write·torn transaction 문제가 드러남
  • Jepsen은 Kafka 트랜잭션 프로토콜이 클라이언트 요청 순서와 트랜잭션 번호를 명시적으로 보장하지 않아, 공식 Java client 사용 시 Kafka와 Kafka 호환 시스템의 트랜잭션 안전성이 깨질 수 있다고 봄

Bufstream 구조와 검증 범위

  • Kafka는 복제·샤딩된 append-only 로그를 제공하는 스트리밍 시스템이며, Bufstream은 클라우드 환경의 데이터 거버넌스와 비용 효율을 우선하는 Kafka 대체 구현임
  • Bufstream은 Kafka처럼 topic과 partition을 제공하고, 표준 Kafka client와 동작함
    • producer는 producer.send()로 record를 append함
    • consumer는 consumer.assign() 또는 consumer.subscribe()로 partition에 바인딩된 뒤 consumer.poll()로 record를 읽음
    • consumer group은 topic 집합의 record 처리를 나눠 맡음
  • Buf Schema Registry와 연동하면 Protocol Buffer record를 검사해 record 검증, field-level access control, 다른 시스템과의 데이터 형식 변환을 지원할 수 있음
  • Kafka가 로컬 디스크와 자체 복제 프로토콜을 쓰는 것과 달리, Bufstream은 데이터를 object storage에 직접 기록함
    • object storage의 복제 트래픽 비용 구조를 활용해 비용 절감을 노림
    • Bufstream node는 stateless auto-scaled VM으로 동작할 수 있음
  • 세 하위 시스템이 Bufstream을 구성함
    • agent: Kafka API를 제공하는 stateless 서비스
    • object store: record chunk를 저장하고 reader에 제공
    • coordination service: 현재 etcd를 사용하며, 어떤 chunk가 commit됐는지와 record 순서를 정함
  • 2024년 10월 기준 Bufstream은 일부 고객에게만 배포됐고, 문서는 “Apache Kafka의 drop-in replacement”와 Kafka transactions 및 exactly-once semantics 호환성을 내세웠지만 구체적인 안전성 주장은 많지 않았음

클라이언트 설정과 트랜잭션 전제

  • Jepsen은 Kafka 호환 시스템의 이전 테스트처럼 더 안전한 동작을 얻기 위해 client 설정을 조정함
  • Producer 설정

    • 기본 acks = all을 사용함
    • Bufstream에서 acks = 0은 storage 대기 없이 쓰기를 인정할 수 있어 commit된 쓰기를 잃을 수 있음
    • acks = 1acks = all은 Bufstream이 durable persist를 확신할 때까지 block함
    • Kafka producer의 자동 재시도에서 중복 append를 막기 위해 기본값인 enable.idempotence = true를 사용함
  • Consumer 설정

    • auto-commit이 데이터 손실로 이어질 수 있다는 문서들이 있어 대체로 enable.auto.commit = false를 사용함
    • committed offset이 없을 때 기본 auto.offset.reset은 최신 offset에서 시작하므로 at-least-once delivery를 보장하지 않음
    • consumer가 전체 로그를 관찰할 수 있도록 auto.offset.reset = earliest를 사용함
    • Kafka 트랜잭션은 producer가 보낸 record 집합과 consumer가 poll한 partition별 최대 offset map으로 구성됨
    • transaction이 commit될 때에만 보낸 record가 durable하고 read_committed consumer에 결국 보이며, committed offset도 transaction에 지정된 offset 이상으로 올라감
    • transaction이 commit되지 않으면 committed offset은 전진하지 않고, 쓰기 가시성은 consumer 설정에 따라 달라질 수 있음
    • read_uncommitted consumer가 abort된 transaction의 값을 읽는 현상은 aborted read(G1a) 로 분류됨
    • Kafka 문서는 read_committed가 G1a를 막고 transaction의 모든 쓰기가 보이거나 아무것도 보이지 않는 성질을 어느 정도 보장한다고 하지만, Jepsen의 Kafka·Redpanda·Bufstream 테스트에서는 write cycle(G0 유사 현상)과 일부 G1c 형태가 관찰됨

테스트 설계

  • Jepsen은 Bufstream 0.1.0부터 0.1.3까지와 여러 release candidate build를 테스트함
  • 테스트 하네스는 Bufstream test harness, Jepsen testing library, Java Kafka Client 3.8.0을 사용함
  • 실행 환경

    • Debian Bookworm node 3~5개를 LXC container와 EC2 VM 양쪽에서 사용함
    • etcd용 node 1개, Minio용 node 1개, 나머지를 Bufstream agent로 사용함
    • producer, consumer, admin client는 bootstrap_servers에 단일 node만 넣어 초기화했지만, smart client discovery는 막지 않았음
  • 주요 안전 설정

    • auto-commit false
    • acks = all
    • retries 1,000
    • idempotence enabled
    • isolation level read_committed
    • auto_offset_reset = earliest
    • server-side automatic topic creation disabled
    • 장애 주입은 process pause(SIGSTOP), crash(SIGKILL), clock skew(clock_settime), network partition(iptables)을 포함함
    • Bufstream이 agent, object store, coordination service로 나뉘므로 특정 하위 시스템만 대상으로 장애를 주입할 수 있는 Jepsen 도구를 새로 만듦
    • 예를 들어 Bufstream node만 crash하거나 etcd coordinator만 pause하는 식으로 조합을 시간에 따라 바꿈

Queue workload와 Abort workload

  • Queue workload는 Kafka 데이터 모델에 맞춰 안전성을 분석함
    • 각 logical process는 producer, consumer, admin client를 실행함
    • numeric key는 특정 topic-partition을 식별함
    • key는 exponential frequency로 선택되어 일부 key는 자주, 일부 key는 드물게 접근됨
  • 세 가지 기본 operation을 사용함
    • crash: logical process를 종료하고 새 client로 대체함
    • subscribe 또는 assign: consumer가 poll할 topic 또는 partition 집합을 바꿈
    • txn, poll, send: poll 또는 send micro-operation의 sequence를 수행함
  • non-transactional workload에서는 각 send 또는 poll이 정확히 하나의 micro-operation만 포함함
  • transactional workload에서는 여러 micro-operation을 Kafka transaction으로 감쌈
  • 분석은 key별 offset-to-value mapping을 만든 뒤 오류를 찾음
    • 같은 offset에서 여러 value가 보이면 inconsistent offset
    • 같은 value가 여러 offset에서 보이면 duplicate error
    • 인정된 record가 전혀 관찰되지 않으면 lost 또는 unseen
    • abort된 operation이 보낸 value를 poll이 반환하면 aborted read
    • transaction이 자기 자신의 쓰기를 관찰하는지도 검사함
  • main test 뒤에는 장애를 해소하고 final reads 단계로 들어감
    • 각 process가 모든 topic-partition을 offset 0부터 읽어, 알려진 최고 written offset까지 poll함
    • final reads가 timeout되어 인정된 record가 여전히 관찰되지 않으면 unseen으로 분류함
  • Abort workload는 transaction abort 뒤 poll offset 동작을 추적하기 위해 추가됨
    • topic은 단일 partition, process, producer, consumer로 제한함
    • transaction이 record를 poll한 뒤 의도적으로 abort하고, 이후 poll offset을 advance, rewind, rewind-further, other로 분류함

Bufstream에서 발견된 5개 문제

  • Stuck consumers (#1)

    • 0.1.0부터 0.1.3-rc.8까지 final read 단계가 자주 멈춤
    • consumer.poll()은 즉시 빈 결과를 반환했지만, 로그에는 인정된 record 수천 개가 남아 있었음
    • 이 상태는 수십 초에서 1시간 이상 지속됨
    • 한 테스트에서는 처음 120초 동안 인정된 record 691개를 보냈고, final reads 시작 시점에 40개가 어떤 poller에게도 관찰되지 않음
    • 이후 1시간 넘게 consumer.poll()이 결과를 반환하지 않아 테스트가 timeout됨
    • 원인은 재시작한 Bufstream node가 last stable offset과 high watermark의 stale cached value를 반환할 수 있었던 점임
    • 일부 client library는 더 뒤 record가 없다고 판단해 stall했으며, Bufstream은 startup 시 cache를 refresh하도록 0.1.3-rc.6에서 patch를 적용함
  • Stuck producers & consumers (#2)

    • 0.1.3-rc.6에서도 coordinator, storage, Bufstream node에 대한 pause, crash, partition 이후 unseen write 문제가 계속 관찰됨
    • 일부 경우 coordinator pause 뒤 모든 Bufstream node가 실행 중인데도 client가 InitProducerId를 기다리다 timeout되는 상태에 들어감
    • 다른 경우 listOffsetsnode ... being disconnected 또는 timed out waiting for a node assignment로 실패했고, poll은 완료되지만 결과를 반환하지 않음
    • Bufstream node를 kill 후 restart하면 문제가 해소됨
    • 원인은 etcd lease와 관련됨
    • Bufstream agent는 active agent 추적에 etcd leases를 사용함
    • 짧은 pause나 partition 때문에 etcd가 agent lease에 묶인 key를 삭제했지만, 삭제 update가 agent에 전달되지 않을 수 있었음
    • agent는 자기 lease를 잃었다는 사실을 모르는 상태가 됨
    • Bufstream 팀은 추가 polling logic을 넣었고, 0.1.3-rc.8에서 unseen write는 대체로 해결됨
  • Spurious zero offsets (#3)

    • 0.1.0부터 0.1.3-rc.2까지 sent value가 offset 0을 배정받은 뒤 더 높은 실제 offset에서 나타날 수 있었음
    • offset 0이 이미 훨씬 전에 배정된 경우에도 발생함
    • sender만 offset 0을 관찰했고, poller는 더 높은 offset을 관찰함
    • 단일 Bufstream node와 etcd process pause를 넣은 2분 테스트에서는 6개 write가 offset 0을 받았다가 더 높은 offset에 나타남
    • 원인은 Bufstream의 error response에 필요한 field가 빠져 있었던 점임
    • Bufstream이 etcd에 log commit 요청을 보내고 etcd가 처리했지만, pause나 partition 때문에 Bufstream이 response 대기 중 timeout할 수 있음
    • Bufstream은 client에 error code를 보냈지만, sent record offset을 error signal인 -1로 설정하지 않음
    • Java Kafka client는 이를 offset 0의 성공 응답으로 해석함
    • Bufstream test suite가 사용한 Franz-go는 이 message를 error로 해석해 해당 문제가 테스트에서 드러나지 않았음
    • Bufstream은 0.1.3-rc.6에서 수정했고, 이후 Jepsen은 재관찰하지 못함
  • Lost transaction writes (#4)

    • 0.1.2에서는 commit된 transaction의 일부 record가 사라져 다시 관찰되지 않는 write loss가 자주 발생함
    • 한 테스트에서는 100초와 write transaction 6,761개 동안 commit된 transaction이 쓴 record 240개가 손실됨
    • 예시에서 key 5의 value 141은 offset 274에 성공적으로 쓰인 것으로 반환됐지만, 모든 consumer.poll()은 해당 offset을 건너뜀
    • 원인은 0.1.2에 추가된 concurrency safety mechanism의 bug였음
    • 이 mechanism은 Kafka transaction protocol의 idempotence 부족을 완화하기 위해 producer epoch 내 transaction마다 unique number를 부여함
    • transaction number tracking logic의 bug 때문에 여러 epoch에 걸쳐 여러 transaction이 commit될 때 일부 commit이 잘못 무시됨
    • commit된 것처럼 보인 transaction이 실제로 abort되거나 그 반대가 될 수 있었음
    • Jepsen은 transaction timeout을 1초로 낮게 둔 덕분에 이 bug를 발견함
    • Bufstream은 0.1.2 release 후 몇 시간 안에 문제를 파악했고, 고객 upgrade를 막았으며, 고객은 0.1.2로 upgrade하지 않았음
    • 수정은 0.1.3-rc2에 포함됨
  • Server-side filtering으로 인한 lost writes (#5)

    • 0.1.3-rc.8에서는 Bufstream process나 coordinator pause, 양자 사이 partition 같은 작은 장애 뒤 짧은 write loss window가 자주 나타남
    • data loss는 transaction 사용 여부와 관계없이 발생함
    • 한 5분 테스트에서는 16,770개 record 중 22개가 acknowledge됐지만 어떤 consumer도 poll하지 못함
    • 일부 record는 한동안 poller에게 보이다가 나중에는 poll에서 사라지기도 함
    • 원인은 인기 있는 Kafka web GUI의 bug를 우회하기 위해 0.1.3-rc.8에 추가한 fetch API response size 제한 logic
    • filtering logic의 bug가 lagging consumer에게 record를 숨겼고, write loss처럼 나타남
    • Bufstream은 0.1.3-rc.12에서 수정함

Kafka Java client와 Kafka 프로토콜 문제

  • KIP-588: 오해를 부르는 ProducerFencedException

    • 테스트 중 ProducerFencedException: There is a newer producer with the same transactionalId which fences the current one. 오류가 자주 발생함
    • 모든 producer가 unique transactional ID를 받는 테스트에서도 이 오류가 나타나 원인 파악에 시간이 걸림
    • KIP-588은 transaction timeout에도 ProducerFencedException이 던져질 수 있다고 적고 있음
    • Kafka Java client는 대부분의 timeout에 전용 TimeoutException을 쓰지만, 이 경우에는 ProducerFencedException을 던짐
    • 실제로 충돌 producer가 없는데도 오류 메시지는 두 번째 producer instance가 존재한다고 말함
    • KIP-588은 2년째 열려 있으며, Jepsen은 Kafka 팀이 error message를 바꾸길 권고함
  • KAFKA-17734: Consumer.close()가 무기한 block 가능

    • Bufstream과 Kafka 양쪽 테스트에서 Java client bug 때문에 몇 시간마다 테스트가 멈춤
    • Consumer.close()는 기본적으로 network IO에서 block함
    • close()의 timeout parameter는 무기한 block을 막아야 하지만 동작하지 않았음
    • consumer.wakeup()을 별도 thread에서 호출해 IO에 stuck된 consumer를 interrupt하는 방식도 효과가 없었음
    • Jepsen은 장기 실행 프로그램이 network error에도 client, connection, thread, memory 같은 resource를 합리적 시간 안에 해제할 수 있어야 한다고 보고 KAFKA-17734를 등록함
  • KAFKA-17582: transaction 실패 뒤 consumer offset이 예측 불가능

    • Kafka 공식 문서는 transaction commit 실패 시 consumer offset이 어떻게 되어야 하는지 거의 말하지 않음
    • Confluent의 Kafka design documentation은 transaction이 abort되면 consumer position이 이전 값으로 돌아간다고 말하지만, 실제 Java client는 항상 그렇게 동작하지 않음
    • Abort workload 결과, healthy cluster에서도 abort 뒤 동작은 예측하기 어려웠음
    • 대부분의 transaction pair는 더 뒤 offset으로 advance함
    • 일부는 이전 offset으로 rewind됨
    • 모든 rewind는 rebalance event와 관련됐고, 모든 advance에는 rebalance가 없었음
    • Kafka 쪽 답변에 따르면 이 동작은 intentional임
    • consumer는 계속 advance함
    • rebalance가 발생하면 committed offset에 따라 임의 지점으로 rewind될 수 있음
    • 사용자는 transaction abort 시 consumer position을 수동으로 rewind해야 함
    • Jepsen은 KAFKA-17582를 열고, 이 동작 문서화와 transaction abort 시 기본 rewind 변경 검토를 제안함
    • Queue workload도 consumer를 명시적으로 rewind하도록 수정됨
  • KAFKA-17754: write loss, aborted read, torn transaction

    • Bufstream 0.1.0~0.1.3에서 Bufstream process pause, coordinator pause, crash, network partition만으로 aborted read, lost write, atomicity violation이 관찰됨
    • 분석은 Kafka transaction protocol의 근본적 결함으로 이어짐
    • 예시에서 client는 unique transactional ID jt1234로 transaction을 실행하고 EndTxncommitted = false를 보내 abort했지만, 15개의 poll() 호출이 abort된 transaction의 write를 관찰함
    • 같은 transaction의 다른 write는 어떤 poller도 관찰하지 못함
    • packet capture와 Bufstream log를 함께 보면 원인은 지연된 commit message였음
    • 몇 transaction 전에 보낸 commit EndTxn이 한 node에서 늦게 처리됨
    • client는 이미 다음 transaction들을 진행하고 있었음
    • 지연된 commit이 현재 transaction에 적용되어 transaction의 앞부분만 commit되고, 나머지는 별도 transaction처럼 처리되어 abort됨
    • Kafka protocol은 client가 여러 TCP connection과 여러 node에 request를 보낼 수 있도록 설계됐지만, 같은 client의 request 순서를 정하는 sequence number가 없음
    • transaction number 개념도 없어 server가 commit 또는 abort message를 받았을 때 client가 어느 transaction을 끝내려 했는지 알 수 없음
    • 그 결과 다음 상황이 가능해짐
      • commit된 것처럼 보인 transaction이 실제로 abort됨
      • abort된 transaction이 실제로 commit됨
      • transaction 일부 write만 보존되고 일부는 손실되는 torn transaction이 발생함
    • 공식 Java Kafka client는 timeout을 retryable로 취급하고 여러 EndTxn message를 자동으로 보낼 수 있어, 사용자가 commit 또는 abort를 transaction마다 한 번만 호출해도 문제가 생길 수 있음
    • Jepsen은 Kafka에서도 process pause로 aborted read와 torn transaction을 관찰했고 KAFKA-17754를 열었음
    • Kafka engineer들은 KIP-890이 이 문제를 고칠 가능성이 있다고 봄
    • KIP-890은 transaction마다 producer epoch를 올리는 방식으로 transaction protocol을 바꿈
    • server가 이전 epoch message를 거부하므로 과거 transaction의 commit message가 이후 transaction에 새어 들어가는 일을 막을 수 있음
    • Bufstream은 0.1.3에서 etcd revision을 logical clock으로 써 빈도를 줄이는 mechanism을 추가했지만, client와 Bufstream 사이 reorder까지 막지는 못함
    • Jepsen은 0.1.3에서도 aborted read, lost write, torn transaction을 계속 관찰했으며, client 쪽 해결이 필요하다고 봄

전체 결과 요약

  • Bufstream 자체 문제 5건은 모두 수정됨
    • #1: lagging highest stable offset 때문에 consumer가 stuck됨, 장애 불필요, 0.1.3-rc.6에서 수정
    • #2: etcd lease expiry 때문에 producer/consumer가 stuck됨, pause 필요, 0.1.3-rc.8에서 수정
    • #3: spurious zero offsets, pause 필요, 0.1.3-rc.6에서 수정
    • #4: lost transaction writes, 장애 불필요, 0.1.3-rc.2에서 수정
    • #5: server-side filtering 때문에 lost writes, pause 필요, 0.1.3-rc.12에서 수정
  • Kafka 관련 문제는 여전히 남아 있음
    • KIP-588: transaction timeout에서 잘못된 error message, 미해결
    • KAFKA-17734: ConsumerClient.close()가 무기한 block 가능, 미해결
    • KAFKA-17582: transaction 실패 뒤 consumer offset이 예측 불가능, 미해결
    • KAFKA-17754: write loss, aborted read, torn transaction, 미해결
  • Jepsen은 실험적 안전성 검증이 bug의 존재는 증명할 수 있지만 부재는 증명할 수 없다고 주의함
  • 특히 KAFKA-17754 때문에 Bufstream에 다른 write loss 사례가 있는지 판별하기 어렵다고 봄

Bufstream 사용자와 운영 권고

  • 공식 Java Kafka client로 Bufstream transaction을 사용하는 사용자는 현재 transaction이 안전하지 않을 수 있음을 고려해야 함
    • abort된 transaction이 실제로 commit될 수 있음
    • commit된 transaction이 실제로 abort될 수 있음
    • transaction이 반으로 찢겨 일부 효과만 보존될 수 있음
  • Bufstream은 Franz-go client가 이 문제에 덜 취약하다고 보지만, Jepsen은 본 작업과 같은 기법으로 Franz-go를 테스트하지 않았음
  • 다른 client는 취약할 수도 있고 아닐 수도 있음
  • Bufstream 0.1.3 이전 사용자는 다음 문제를 겪을 수 있음
    • producer.send()가 실제 offset 대신 0 offset을 잘못 반환
    • client가 stuck되는 metastable availability issue
  • Jepsen은 0.1.3 upgrade를 권고함
  • Bufstream의 전체 아키텍처는 sound해 보인다고 평가함
    • etcd 같은 coordination service로 immutable data chunk 순서를 정하는 방식은 OLTP와 streaming system에서 선례가 있는 비교적 단순한 접근임
  • 운영 측면에서는 두 가지 개선이 권고됨
    • startup 시 storage의 shared file 요청이 실패하면 cluster가 crash할 수 있어 retry를 추가하라고 권고했고, Bufstream은 retry layer를 추가함
    • dependency가 unavailable할 때 agent가 즉시 죽기보다 계속 실행되며 backpressure와 system status를 제공하고 더 부드럽게 recover하는 방향을 권고함
  • 0.1.3 기준 Bufstream은 etcd에 대한 추가 retry logic을 넣었지만, online 상태 유지를 위해 여전히 constant supervision이 필요함
  • 사용자는 process supervisor가 있으며 장기 outage 중에도 포기하지 않고 동작하는지 테스트해야 함

Kafka transaction 문서화와 프로토콜 수정 필요

  • Kafka 공식 문서는 transaction에 대해 거의 말하지 않아 사용자는 모호하고 상충되는 여러 source를 조합해야 함
  • Jepsen은 Kafka 팀에 transaction semantics를 명확히 정리한 중앙 문서를 만들 것을 권고했고 KAFKA-17671를 언급함
  • 해당 문서는 최소한 다음을 명시해야 함
    • consumer가 언제 monotonically increasing offset을 관찰하는지
    • consumer가 언제 acknowledge된 record를 건너뛸 수 있는지
    • rebalance가 transaction 중간에 영향을 줄 수 있는지
    • producer write offset이 언제 단조 증가하는지
    • G0, G1a, G1b, G1c, fractured read, 자기 transaction write read가 언제 합법인지
    • abort된 transaction 뒤 poll() 반환값과 offset이 어떤 의미를 갖는지
    • transaction error, abort 중 error, rewind 중 error를 어떻게 처리해야 하는지
  • Confluent 문서가 Kafka 기본값이 at-least-once delivery를 제공한다고 반복해서 말하지만, Jepsen은 이것이 사실이 아닌 것으로 보인다고 지적함
    • auto.offset.reset = latest는 처리되지 않은 record를 “committed”처럼 만들 수 있음
    • Confluent offset management 문서도 기본 auto-commit에서 crash 시 message progress 손실 위험을 말함
    • transaction abort 시 consumer가 rewind된다는 문서도 실제와 다름
  • Jepsen은 Kafka transaction protocol이 근본적으로 수정되어야 한다고 봄
    • protocol은 ordered reliable delivery를 암묵적으로 가정하지만, process pause, network unreliability, non-zero latency, 여러 TCP socket 간 unordered delivery가 존재함
    • Kafka protocol은 여러 node와 TCP socket에 message를 분산시키고 client는 message를 자동 retry함
    • 같은 client message 순서를 복원할 sequence number와 transaction 대상 확인용 transaction number가 없음
  • KIP-890은 transaction commit마다 epoch를 올려 더 엄격한 순서를 보장하려고 함
  • client library도 message가 acknowledge되지 않을 때 producer를 re-initialize해 epoch를 올리는 방식으로 도움을 줄 수 있음
  • Java Kafka Client 3.8.0은 이 문제에 취약함
  • Jepsen은 Franz-go가 timeout 시 re-initialize를 수행해 문제를 완화하거나 방지할 수 있다고 보지만, 다른 client library는 조사하지 않았음

향후 작업

  • 많은 사용자는 transaction을 직접 다루기보다 Kafka Streams API의 “exactly-once semantics”에 의존하므로, 향후 Streams application의 정확성을 조사할 수 있음
  • Jepsen은 KAFKA-17754를 조사하는 동안 Kafka에서도 unseen write를 만났지만 시간 제약으로 분석하지 못함
    • unseen write는 hanging transaction, stuck consumer, data loss의 신호일 수 있음
    • 지연된 Produce message가 미래 transaction에 들어가 transaction guarantee를 위반할 수 있는지도 의문으로 남음
    • Kafka Java Client가 request timeout 시 sequence number를 재사용해 write가 acknowledge됐지만 조용히 discard될 가능성도 의심함
  • rebalance event가 발생하면 consumer position이 앞뒤로 움직일 수 있으나, 그 규칙은 불명확함
  • Kafka가 의도한 동작을 문서화하면 Jepsen은 이를 검증하고 싶어함
  • Jepsen은 random process라 드문 anomaly를 탐색하기 어렵다고 설명함
    • 한 번 발생하는 문제는 debugging과 reproduction이 매우 어려움
  • Bufstream은 deterministic hypervisor와 simulated network에서 분산 시스템 전체를 실행하는 Antithesis도 사용함
    • Jepsen의 workload generation 및 history checking을 Antithesis의 deterministic, replayable environment와 결합하면 테스트 재현성을 높일 수 있음

댓글과 토론

Hacker News 의견들
  • KAFKA-17754 같은 이슈를 조사하다가 Kafka에서 보이지 않는 쓰기도 발견했다면, Jepsen이 Kafka를 다시 깊게 파볼 때가 된 듯함
    마지막 조사는 2013년(https://aphyr.com/posts/293-call-me-maybe-kafka, Kafka 0.8 베타)이었고, 지금은 Kafka 자체에서 여러 문제를 막 발견하려는 단계처럼 보임
    “쓰기가 확인됐는데 조용히 버려질 수 있다” 같은 내용은 꽤 무섭다

    • Kafka 분석은 꼭 해보고 싶음 :-)
  • 기본값 enable.auto.commit=true에서 Kafka 소비자가 애플리케이션이 실제 처리했는지와 무관하게 오프셋을 커밋할 수 있다는 부분이 매우 놀라움
    자동 커밋을 그렇게 이해한 적이 없고, 그런 기본값이라면 말이 안 된다고 봄
    문서 설명은 아주 명확하진 않지만, 전체적으로는 처리가 끝난 경우에만 오프셋이 커밋된다고 읽혔음
    자동 커밋 간격 조정은 적어도 한 번 처리(at-least-once)에서 기대하듯, 메시지 유실이 아니라 중복 처리 창을 줄이는 데 도움이 된다고 이해했음

    • 조금 놀랍고, 문서가 이 부분을 잘 설명하지 못한다는 데 동의함
      명시적으로 커밋하지 않으면 Kafka가 메시지를 처리했는지 알 방법이 없음
      Kafka는 넘겨준 메시지가 즉시 처리됐다고 가정함
      자동 커밋은 아이스크림콘을 건네고 바로 돌아서서 상대가 먹었다고 가정하는 것과 비슷함. 어떤 사람은 받자마자 떨어뜨리고 한 입도 못 먹을 수 있음
    • 핵심은 메시지가 Kafka 클라이언트에 성공적으로 전달됐다고 해서 애플리케이션이 처리했다는 뜻은 아니라는 것임
      그 보장을 원하면 명시적으로 확인 응답을 해야 함
      예를 들어 메시지를 데이터베이스에 쓰는 일만 한다면, 메시지가 클라이언트 핸들러 콜백에 들어온 순간 확인된 것으로 처리됨
      하지만 실제로는 DB 삽입이 성공한 뒤에 확인되길 원할 가능성이 큼
      DB가 네트워크, Kubernetes, 방화벽 설정 등으로 접근 불가가 되고, 그 와중에 엔지니어가 재시작을 시도하면서 클라이언트가 내려가면 처리되지 않은 메시지가 생기기 쉬움
    • 이 기능은 고성능 상황을 위한 것으로 이해함
      다른 시스템이 실패 여부를 판별할 수 있고, 이 기능으로 상한 위치를 옮겨 재처리를 줄일 수 있음
      다만 타이밍이 맞고 장애가 나면, 재시작 후 이미 처리한 일부를 다시 받을 수 있다고 가정해야 함
      문제는 자동 커밋 전에 이런 처리가 없는 경우임
      읽어보면 처리 이후 한참 뒤에 커밋되도록 의도된 것 같지만, 자동 커밋이면서 자동 커밋 시점보다 몇 밀리초 전의 항목만 커밋해야 한다는 점이 모순처럼 보이기도 함
    • 이 기능이 존재하는 이유는 어느 정도 정당화할 수 있음. 동기식 단일 스레드 소비자를 위해 설계됐고, 대략 poll을 호출한 뒤 메시지를 내구성 있게 처리하는 루프를 상정함
      혼동되는 지점은 자동 커밋 검사가 시간 초과 뒤 비동기로 일어나는 게 아니라 다음 poll 호출 시점에 일어난다는 것임
      따라서 다시 poll을 호출하기 전에 메시지를 내구성 있게 처리하지 않고 저장만 하는 경우, 예를 들어 비동기 처리·지연·큐 등을 쓰는 경우에만 쓰기를 떨어뜨릴 수 있어야 함
      이는 Java 클라이언트 라이브러리의 문서화된 동작(https://kafka.apache.org/32/javadoc/org/apache/kafka/clients...) 기준이며, 현재 구현이 실제로 그런지는 별개임
      Kafka 프로토콜은 고수준과 저수준 사이에 끼어 있어 둘 다 썩 잘하지 못함
      자동 커밋은 단순한 애플리케이션을 쉽게 만들도록 돕는 고수준 기능이지만, 기대한 방식으로 쓰지 않으면 당연히 실패할 수 있음
      요즘 최종 사용자는 Kafka 클라이언트를 직접 쓰기보다 세부 사항을 제대로 처리해 주는 고수준 구현을 써야 한다고 봄. 데이터 용도라면 스트림 처리 엔진, 애플리케이션 용도라면 지속 실행 엔진 같은 것임
  • 제품 페이지(https://buf.build/product/bufstream)를 보면 “AWS나 GCP VPC 안에서만 실행되고 외부로 연락하지 않는다”는 설명과 “압축 전 GiB당 $0.002”라는 사용량 기반 과금이 어떻게 양립하는지 궁금함
    설마 전체 사업을 명예 시스템으로 운영하진 않을 것 같음

    • 소개에 “2024년 10월 기준 Bufstream은 선별된 고객에게만 배포됐다”고 되어 있으니, 명예 시스템도 가능할 수 있다고 봄
      물론 악용 위험은 있지만, 특정 고객을 끌어들이기 위한 가치 있는 절충일 수 있음
    • 프로그램은 오픈소스이거나 아니거나 둘 중 하나임
      소스가 공개돼 있지 않다면 “외부로 연락하지 않는다”는 주장은 절대 믿으면 안 됨
  • “Kafka 트랜잭션 프로토콜은 근본적으로 망가졌고 개정되어야 한다”니 아프게 들림
    그래도 늘 그렇듯 조사와 글은 훌륭함

  • Kyle이 NATS JetStream을 검토한 적이 있는지 궁금함. 어떻게 생각할지 궁금함

    • 아직 검토하지 않았지만, 요청한 사람이 처음은 아님
      몇몇은 이게… 뭐라고 해야 하나… 재미있을 거라고 제안했음 :-)
  • bufstream GitHub 프로젝트를 못 찾겠는데, 어디 있는지 궁금함

  • 관련 블로그 글과 문서를 읽어보니, Kafka의 “정확히 한 번 전달”은 작업자가 토픽 1에서 읽고 토픽 2에 쓰며 두 토픽이 같은 논리적 Kafka 시스템 안에 있는 읽기-처리-쓰기 작업의 속성으로 정의되는 것 같음
    맞다면 이건 트랜잭션이라고 부르는 편이 더 낫지 않나 싶음

    • Kafka도 실제로 이것을 트랜잭션이라고 부름
      다만 “정확히 한 번”을 보는 방식은 두 가지가 있음
      하나는 데이터베이스 트랜잭션처럼 효과가 중복되거나 사라지면 안 된다는 의미임
      다른 하나는 토픽-파티션을 가로지르는 메시지 관계에 대한 데이터 흐름 그래프 속성에 가까우며, ACID의 일관성에 조금 더 가까움
      직렬화 가능 트랜잭션 시스템이 특정 도메인 수준 일관성을 보장하는 것과 비슷하게, 트랜잭션을 사용해 그 데이터 흐름 속성에 도달할 수 있음
      예를 들어 직렬화 가능성은 각 트랜잭션을 따로 봤을 때 보존되는 불변식이 동시 실행 이력에서도 보존됨을 보장함
      Kafka가 그런 방식으로 “정확히 한 번 의미론”에 도달하려는 것이라고 볼 수 있음
  • https://www.warpstream.com/와 혼동하면 안 됨

    • 맞음. WarpStream은 트랜잭션도 지원하지 않음
  • 정오표: “Transactions may observe none, part, or all”은 “Consumers may observe none, part, or all”이어야 할 것 같음

    • 둘 다 맞지만, 명확성을 위해 트랜잭션이라고 썼음
      트랜잭션 밖 소비자의 의미론은 더 흐릿함
      이 작업부하의 모든 읽기는 트랜잭션 맥락에서 일어나며, 트랜잭션 오프셋 커밋 경로를 거침
  • 이 소프트웨어가 어디에 쓰이는지 궁금함. 계측? 블랙박스?

    • Jepsen은 자신이 개발하는 데이터베이스를 테스트한다는 사실을 몰랐다면 울게 만드는 도구임
      물론 기쁨의 눈물임. Jepsen의 관심을 받는 것 자체가 하나의 성취이기 때문임
    • Kafka 클론임. Kafka는 대체로 내구성 있는 큐