그림을 글로 읽기
- Producer: evt-0..5 · acks=all
- Kafka broker: lab-events/P0
- Consumer poll: auto commit 없음
- 파일 inbox: event ID unique
- Group committed: 재시작 경계
- Producer → Kafka broker: send
- Kafka broker → Consumer poll: poll
- Consumer poll → 파일 inbox: effect
- 파일 inbox → Group committed: commit after contiguous completion
실습 목적과 두 실행 track
이 실습은 poll 전달 위치, 외부 inbox 효과, consumer group committed offset을 서로 다른 상태로 추적합니다. examples/knowledge/kafka-lab/compose.yaml은 빠른 단일 broker RF1 track을 보존하고, compose.multi.yaml은 실제 3-broker KRaft combined cluster에서 RF3/minISR2, ISR 축소·복귀, broker 중단·복구와 consumer 재시작을 확인하는 확장 track입니다.
단일 broker는 replication failover를 증명하지 않습니다. 3-broker track만 kafka-topics.sh --describe의 Replicas와 Isr를 사용해 복제 상태를 검사합니다. ISR 상태, consumer group offset, inbox 업무 효과는 서로 다른 증거입니다.
3-broker KRaft 구성
compose.multi.yaml은 Apache Kafka 공식 plaintext combined KRaft Docker 예제의 구조를 따릅니다. 세 노드는 KAFKA_NODE_ID 1·2·3, KAFKA_PROCESS_ROLES=broker,controller, 동일한 KAFKA_CONTROLLER_QUORUM_VOTERS, 내부 PLAINTEXT://:19092, controller CONTROLLER://:9093, host PLAINTEXT_HOST://:9092를 사용합니다. host ports는 127.0.0.1:29092, 39092, 49092로 loopback에만 매핑하고 advertised host endpoint는 각 localhost port를 가리킵니다.
세 노드는 KAFKA_MIN_INSYNC_REPLICAS=2, KAFKA_DEFAULT_REPLICATION_FACTOR=3, offsets/transaction/share coordinator replication factor 3, 관련 min ISR 2, KAFKA_UNCLEAN_LEADER_ELECTION_ENABLE=false를 사용합니다. lab-events는 자동 생성하지 않고 partition 1, RF3, topic min.insync.replicas=2로 명시 생성합니다. image는 apache/kafka:4.3.1 tag로 고정했으며 tag 불변 digest나 최신·지원 상태를 주장하지 않습니다.
공식 구성 근거는 Apache Kafka repository의 https://raw.githubusercontent.com/apache/kafka/trunk/docker/examples/docker-compose-files/cluster/combined/plaintext/docker-compose.yml입니다. 공식 Docker 문서의 실행 예시는 apache/kafka:4.3.1입니다.
상태 모델과 Java 코드
producer는 acct-7 key와 evt-0부터 evt-5까지의 header를 acks=all, idempotence로 전송합니다. KILL_BEFORE_ACK_EVENT_ID는 send 결과를 기다리기 전 종료하는 ACK 불확정 주입이며, replication 성공이나 broker 장애 증거가 아닙니다.
consumer는 enable.auto.commit=false, auto.offset.reset=earliest로 owner thread에서 poll합니다. Checkpoint는 전달 deque와 완료 집합을 사용해 contiguous next commit만 허용합니다. InboxStore는 event ID를 새 파일을 force한 뒤 원자 교체하고 프로세스 시작 때 reload하므로 commit 전 종료 뒤 동일 ID 재전달을 inboxApplied=false로 흡수합니다. 파일 저장소는 실제 DB unique constraint와 업무 변경을 하나의 원자 transaction으로 묶지 않는 교육용 대체물입니다. 한 writer만 허용하는 실습이며 여러 consumer 프로세스가 같은 파일에 동시에 쓰면 안 됩니다. 파일 교체의 원자성이 없는 파일시스템에서는 실패하며 전원 손실·디렉터리 fsync 내구성은 검증하지 않습니다.
InboxWorker는 ConsumerRebalanceListener의 revoked/assigned/lost callback을 로그로 남깁니다. 이 로그는 group ownership 증거이며, inbox 효과나 commit 성공을 뜻하지 않습니다. onPartitionsLost는 이미 소유권을 잃은 상태이므로 마지막 commit 기회로 사용하지 않습니다.
결정적 consumer 재시작 검사
make multi-integration은 RF3/minISR2 topic을 만들고 초기 Replicas/Isr를 검사한 뒤 6개 event를 전송합니다. 첫 consumer는 STOP_AFTER_EVENT_ID=evt-2 KILL_BEFORE_COMMIT=true MAX_RECORDS=3으로 실행되어 inbox 효과 세 개를 남기고 exit 42로 끝납니다. 로그 marker와 inbox의 정확한 ID 집합 evt-0..evt-2를 검사합니다.
같은 group으로 consumer를 재시작하면 이미 적용된 ID에서 inboxApplied=false가 출력되고, 최종 committed partition=0 offset=6이 관찰되며, inbox는 evt-0..evt-5 각 한 번으로 남아야 합니다. 이것은 물리 재전달과 외부 효과 중복 제거를 검증하며, group coordinator의 실제 상태는 commit 응답과 별도로 해석해야 합니다.
broker 중단·복구 검사
make multi-failure의 순서는 다음과 같습니다.
- 세 broker를 기동하고 RF3/minISR2 topic을 생성합니다.
- 중단 전 describe에서 RF3와 ISR 3을 검사합니다.
- producer와 pre-commit consumer kill을 수행하고 exit 42 및 inbox ID 세 개를 검사합니다.
docker compose -f compose.multi.yaml stop kafka-1을 실행합니다.- kafka-2에서 describe를 조회해 broker ID 1이 ISR에서 빠지고 ISR이 정확히 2인지 검사합니다. 이는 ISR 증거이며 leader 변경을 자동으로 주장하지 않습니다.
- kafka-1이 중단된 동안 같은 consumer group을 재시작해 exit 0, commit offset 6, inbox ID 정확한 집합을 검사합니다. 이는 consumer 재시작/효과 증거이며 ISR 증거와 별개입니다.
start kafka-1뒤 describe를 다시 조회해 RF3와 ISR 3을 검사합니다. 이는 broker 복구와 ISR catch-up 증거입니다.
Makefile은 각 단계의 describe와 consumer 로그를 .lab-inbox/에 저장하고, startup 이전에 종료 trap을 등록하여 해당 disposable project의 volume을 정리합니다. 로그와 원장은 보존합니다. assert_replication.py는 Replicas와 Isr를, assert_consumer.py는 kill marker와 evt-2의 중복 효과 억제를, OffsetAdmin과 assert_offset.py는 broker coordinator의 실제 offset을, assert_inbox.py는 정확한 ID 집합을 검사합니다.
실행 범위
cd examples/knowledge/kafka-lab
make python-test
make test
make integration
make multi-integration
make multi-failure
make multi-down
현재 작성 환경은 macOS 27 arm64이며 JDK 21(/opt/homebrew/opt/openjdk@21/bin)과 Gradle 9.6.0으로 Java compile·JUnit 4개·installDist를 통과했습니다. 격리 Maven cache를 사용한 JUnit 실행도 4개 테스트 모두 통과했습니다. Docker/Compose가 없어 Compose parse, image pull, broker readiness, 실제 producer/consumer, process kill, consumer rebalance, broker stop/recovery, RF/ISR assertions는 모두 NOT_RUN입니다. Python과 shell static checks도 통과했습니다.
공식 근거
- Apache Kafka Docker:
https://kafka.apache.org/43/getting-started/docker/ - Apache Kafka official combined plaintext cluster compose:
https://raw.githubusercontent.com/apache/kafka/trunk/docker/examples/docker-compose-files/cluster/combined/plaintext/docker-compose.yml - Apache Kafka basic operations:
https://kafka.apache.org/43/operations/basic-kafka-operations/ - KafkaConsumer Javadoc:
https://kafka.apache.org/43/javadoc/org/apache/kafka/clients/consumer/KafkaConsumer.html - ConsumerRebalanceListener Javadoc:
https://kafka.apache.org/43/javadoc/org/apache/kafka/clients/consumer/ConsumerRebalanceListener.html - Producer configs:
https://kafka.apache.org/43/configuration/producer-configs/