프로듀서 신뢰성과 컨슈머 그룹 — acks, 멱등성, 트랜잭션, 리밸런싱
40 Kafka Interview Questions for Java Developers (blog) kafk_ref_03(Section 3 프로듀서, Section 4 컨슈머 메커니즘), kafka_ref_01(Q35-58,75), kafka_ref_05(Q9) 종합. kafk_ref_03의 '리밸런싱은 항상 stop-the-world' 서술을 CooperativeStickyAssignor(2.4+) 및 KIP-848(Kafka 4.0 GA) 기준으로 수정했고, 'EOS는 약 10~20% 지연 오버헤드를 추가한다'는 구체 수치 주장은 근거 불명확하여 '워크로드에 따라 달라지는 실질적 오버헤드'로 완화.
이 모듈을 다 읽으면
- acks=0/1/all의 차이와 durability-latency 트레이드오프를 설명할 수 있다
- 멱등성 프로듀서와 트랜잭션 API가 각각 어떤 문제를 해결하는지 구분할 수 있다
- exactly-once semantics를 구성하는 세 가지 메커니즘을 설명할 수 있다
- 컨슈머 그룹 리밸런싱이 왜 발생하고, eager 방식과 cooperative(incremental) 방식이 무엇이 다른지 판단할 수 있다
프로듀서의 신뢰성 설정(acks, 멱등성, 트랜잭션)과 컨슈머 그룹의 파티션 할당·리밸런싱 메커니즘을 정리하고, 특히 '리밸런싱은 항상 전체 stop-the-world'라는 자료 속 서술을 최신 cooperative 리밸런싱 관점에서 바로잡는다.
acks와 durability-latency 트레이드오프
acks는 프로듀서가 쓰기 성공으로 간주하기 전에 몇 개의 브로커 확인을 기다릴지를 결정한다. acks=0은 확인을 전혀 기다리지 않아(fire-and-forget) 지연은 가장 낮지만 브로커 장애 시 메시지 유실 가능성이 가장 크다. acks=1은 리더의 확인만 기다리므로 어느 정도 durability를 얻지만, 팔로워가 복제하기 전에 리더가 죽으면 여전히 유실될 수 있다. acks=all(=-1)은 ISR에 속한 모든 복제본이 확인해야 성공으로 간주하므로 지연은 가장 크지만 durability가 가장 강하다.
acks=all은 min.insync.replicas와 함께 사용해야 의미가 있다 — min.insync.replicas가 설정되어 있지 않거나 1이면 ISR이 리더 하나만 남아 있어도 쓰기가 성공 처리되어, acks=all이 주는 보장이 사실상 acks=1과 다를 바 없어진다. 실무에서 금융 거래나 과금처럼 유실을 허용할 수 없는 데이터는 acks=all + min.insync.replicas≥2 조합을, 지표/로그처럼 약간의 유실을 감내할 수 있는 데이터는 더 가벼운 설정을 선택하는 것이 일반적이다.
핵심 포인트
- acks=0/1/all은 각각 확인 불필요/리더만 확인/ISR 전체 확인으로, 지연과 durability가 반비례한다
- acks=all은 min.insync.replicas와 함께 설정해야 실질적인 보장이 생긴다
- 데이터의 유실 허용 범위에 따라 acks 설정을 다르게 가져가는 것이 실무적이다
멱등성 프로듀서와 트랜잭션 — 서로 다른 문제를 푼다
멱등성 프로듀서(enable.idempotence=true)는 프로듀서 재시도로 인한 중복 쓰기를 막는다. Kafka는 프로듀서마다 고유한 Producer ID(PID)와 파티션별 순차 시퀀스 번호를 부여하고, 브로커는 같은 PID·시퀀스 번호의 메시지가 다시 오면 중복으로 판단해 조용히 버린다. 이것만으로는 단일 파티션 내 재시도 중복만 막을 뿐, 여러 토픽/파티션에 걸친 "읽고-처리하고-쓰기(read-process-write)" 흐름 전체가 원자적으로 성공/실패하는 것은 보장하지 못한다.
이를 위한 것이 트랜잭션 API(transactional.id)다 — 트랜잭션은 여러 파티션에 대한 쓰기와 컨슈머 오프셋 커밋을 하나의 원자적 단위로 묶어, 커밋 마커가 찍히기 전까지는 아무것도 확정되지 않고, 커밋되면 전부, 아니면 전부 취소(abort)된다. 컨슈머 쪽에서는 isolation.level=read_committed를 설정해야 커밋되지 않고 중단된 트랜잭션의 쓰기를 걸러내고 커밋된 데이터만 보게 된다. 즉 멱등성 프로듀서 + 트랜잭션 + read_committed 격리 수준, 이 세 가지가 함께 있어야 end-to-end exactly-once semantics(EOS)가 성립한다 — 어느 하나만으로는 부족하다. EOS는 처리량과 지연에 실질적인 오버헤드를 더하므로(정확한 수치는 워크로드에 따라 크게 달라 일반화하기 어렵다), 결제·재고·정산처럼 정확히 한 번 처리가 실제로 요구되는 워크로드에만 선택적으로 적용하는 것이 합리적이다.
핵심 포인트
- 멱등성 프로듀서는 PID+시퀀스 번호로 재시도 중복만 방지한다
- 트랜잭션 API는 여러 파티션에 걸친 쓰기+오프셋 커밋을 하나의 원자적 단위로 묶는다
- 컨슈머는 isolation.level=read_committed로 중단된 트랜잭션의 데이터를 걸러야 한다
- exactly-once semantics는 멱등성+트랜잭션+read_committed 세 가지가 함께 있어야 성립한다
컨슈머 그룹 리밸런싱 — eager에서 cooperative/incremental로
컨슈머 그룹은 파티션을 그룹 내 컨슈머들에게 분배해 병렬 처리를 가능케 하며, 컨슈머가 추가/제거되거나 하트비트가 끊기거나(세션 타임아웃) 처리 지연으로 max.poll.interval.ms를 초과하면 리밸런싱이 트리거된다.
과거의 "eager" 리밸런싱 프로토콜(RangeAssignor, RoundRobinAssignor 등)에서는 리밸런싱이 시작되면 그룹의 모든 컨슈머가 자신이 가진 파티션 전부를 일단 반납하고 재배정을 기다려야 했다 — 이 때문에 변경과 무관한 파티션을 담당하던 컨슈머까지 처리가 잠시 멈추는 "stop-the-world" 현상이 그룹 전체에 발생했다. 하지만 이는 협력적이지 않은(non-cooperative) 프로토콜에 국한된 이야기이며, "리밸런싱은 항상 전체가 멈춘다"고 일반화하는 것은 부정확하다. Kafka 2.4부터 도입된 CooperativeStickyAssignor는 점진적(incremental) 재배정 방식을 사용해, 실제로 이동이 필요한 파티션만 반납시키고 나머지 컨슈머는 처리를 계속하게 한다. 여기서 한 걸음 더 나아가 KIP-848(Kafka 4.0에서 GA)은 리밸런싱 로직 대부분을 서버(그룹 코디네이터) 쪽으로 옮기고 기본적으로 협력적·점진적 재배정을 사용하는 새 컨슈머 그룹 프로토콜을 도입해, 대규모 컨슈머 그룹에서도 리밸런싱이 훨씬 빠르고 덜 파괴적으로 일어나게 만들었다.
실무에서는 (1) CooperativeStickyAssignor/KIP-848 프로토콜 사용, (2) group.instance.id를 통한 정적 그룹 멤버십으로 일시적 재시작 시 불필요한 리밸런싱 회피, (3) max.poll.records를 낮추거나 처리 로직을 비동기화해 max.poll.interval.ms 초과를 방지하는 세 가지가 리밸런싱 빈도와 영향을 줄이는 핵심 대응책이다.
핵심 포인트
- 리밸런싱은 컨슈머 추가/제거, 세션 타임아웃, max.poll.interval.ms 초과로 발생한다
- 과거 eager 프로토콜에서는 리밸런싱 시 그룹 전체 컨슈머가 파티션을 반납해야 했다
- CooperativeStickyAssignor(2.4+)와 KIP-848(4.0 GA)은 실제로 이동이 필요한 파티션만 재배정하는 점진적 방식이다
- '리밸런싱은 항상 stop-the-world'라는 서술은 최신 협력적 프로토콜을 반영하지 못한 설명이다
- 정적 그룹 멤버십(group.instance.id)은 일시적 재시작 시 리밸런싱을 회피하는 수단이다