1;지난 글에서는 전반적인 Kafka 내부에서 사용되는 개념, 프로듀서의 작동 방식과 필자가 띄운 실습 환경의 topic 상세 정보를 살펴보았다. (지난 글 여기 클릭!)
이번 시간에는 하이워터마크와 리더 에포크가 무엇인지 알아보겠다.
0. 들어가기 전에
이번 글을 정리하기 전에 짚고 넘어가려는 개념을 정리하고자 한다.
0.1) ISR(InSyncReplica)
In Sync Replica의 약어로 현재 리플리케이션이 되고 있는 리플리케이션 그룹을 말한다.
카프카는 오직 완전히 복사된 메시지만 컨슈머에게 전송한다.
여기서 프로듀서는 미시지를 리더에게만 보내고 컨슈머도 리더에서서만 메시지를 받는다.
그러면 다른 팔로워들은? 말 그대로 팔로워들은 리더의 데이터들을 팔로우한다.
팔로워들은 현재 리더에 문제가 생길 시 새로운 리더가 되기 위해 준비한다. 이것은 ISR이라는 논리적인 그룹에서 행해진다.
리더와 팔로워는 ISR이라는 논리적 그룹을 묶여있다.
이렇게 리더와 폴로워를 별도의 그룹으로 나누는 이유는 그본적으로 해당 그룹 안에 속한 팔로워들만이 새로운 리더의 자격을 가질 수 있기 때문이다.
ISR내 팔로워들은 리더와 데이터 일치를 유지하기 위해 지속적으로 리더의 데이터를 따라가며 리더는 ISR내 모든 팔로워가 메시지를 받을 때 까지 기다린다.
사실 이에 대해서 이전 글에서 내부 topic 정보 분석하기를 하면서 본 적이 있다.

Isr: 1,2,3은 해당 파티션의 데이터 복제에 정상적으로 참여하여
리더(Leader)와 완벽하게 동기화된 상태를 유지하고 있는 카프카 브로커 노드의 ID가 1번, 2번, 3번이라는 의미다.
- Leader: 3 해당 파티션(Partition 0)의 모든 읽기(Read)와 쓰기(Write) 트래픽을 전담하는 메인 브로커가 3번 노드
- Replicas: 3,1,2 데이터의 안전을 위해 파티션 0의 복제본을 저장하기로 지정된 전체 브로커 목록. 이는 초기 생성 시 할당된 정적(Static)인 값으로, 브로커가 죽어 있더라도 변하지 않는다.
- Isr (In-Sync Replicas): 1,2,3 동적(Dynamic)으로 변하는 현재의 건강 상태를 나타낸다. 현재 3번 브로커(리더)에 쓰인 최신 데이터를 1번과 2번 브로커(팔로워)가 지연 없이 실시간으로 잘 가져가서(Fetch) 복제하고 있다는 뜻
Configs: min.insync.replicas=1 설정으로 최소 1개의 ISR만 살아 있다면 쓰기를 성공하겠다는 말이다.
이게 무슨 말이냐?
프로듀서(Producer, 즉 백엔드 애플리케이션)가 카프카 브로커에게 데이터를 전송한 후,
브로커로부터 데이터가 안전하게 저장되었다는 '확인 응답(ACK, Acknowledgment)'을 정상적으로 수신했다는 것을 의미한다.
1. 파티션 내부 메시지 누락과 커밋
카프카에서 커밋이란 래플리케이션 팩터 수의 모든 리플리케이션이 전부 메시지를 저장함을 의미한다.

우선 이해를 위해 필자가 그림을 그렸다.
프로듀서와 컨슈머는 리더에게 메시지를 주고 받는다. 그 과정에서 팔로워들은 리더의 메시지를 업데이트한다.
현재 "test message1" 까지만 전송을 완벽하게 하고 복제도 한 상황이다. 즉, 커밋한 상태이다.
커밋을 해야만 컨슈머가 메시지를 받을 수 있음을 기억하자!
만약에 커밋 전의 메시지를 읽게 된다면?

1) 컨슈머가 cluster-test 토픽을 컨슘한다.
2) 컨슈머가 읽는 메시지는 현재 리더에 2개가 있다. "test message1" 과 "test message2"가 있다.
3) 그런데 중간에 리더가 고장이 나서 cluster-test-1(팔로워)가 리더가 된다.

4) 현재 새롭게 선출된 뉴리더는 현재 메시지가 "test message1" 밖에 없다.
결과적으로 컨슈머 A와 컨슈머 B는 cluster-test 라는 동일한 토픽의 파티션을 읽었지만 다른 메시지를 가져갔다.
그래서 이렇게 커밋이 중요한데, 그러면 어떻게 커밋의 위치를 우리는 알 수 있을까?
바로 replication-offset-checkpouint라는 파일에 마지막 커밋 오프셋 위치를 저장한다.
다음을 보면 알 수 있다.

# 첫 두 줄(버전, 개수)을 제외하고, 1열(토픽명) 문자열 기준 정렬 후 2열(파티션 번호) 숫자 기준 정렬
tail -n +3 /tmp/kafka-logs/replication-offset-checkpoint | sort -k1,1 -k2,2n
필자는 이렇게 해서 다음 결과를 받았다.
__consumer_offsets 0 0
__consumer_offsets 1 4
__consumer_offsets 2 0
__consumer_offsets 3 0
__consumer_offsets 4 0
__consumer_offsets 5 0
__consumer_offsets 6 0
__consumer_offsets 7 0
__consumer_offsets 8 0
__consumer_offsets 9 0
__consumer_offsets 10 0
__consumer_offsets 11 0
__consumer_offsets 12 0
__consumer_offsets 13 0
__consumer_offsets 14 0
__consumer_offsets 15 0
__consumer_offsets 16 0
__consumer_offsets 17 0
__consumer_offsets 18 0
__consumer_offsets 19 0
__consumer_offsets 20 0
__consumer_offsets 21 0
__consumer_offsets 22 0
__consumer_offsets 23 4
__consumer_offsets 24 0
__consumer_offsets 25 0
__consumer_offsets 26 0
__consumer_offsets 27 0
__consumer_offsets 28 0
__consumer_offsets 29 0
__consumer_offsets 30 0
__consumer_offsets 31 0
__consumer_offsets 32 0
__consumer_offsets 33 0
__consumer_offsets 34 0
__consumer_offsets 35 0
__consumer_offsets 36 4
__consumer_offsets 37 4
__consumer_offsets 38 0
__consumer_offsets 39 0
__consumer_offsets 40 0
__consumer_offsets 41 0
__consumer_offsets 42 0
__consumer_offsets 43 0
__consumer_offsets 44 0
__consumer_offsets 45 0
__consumer_offsets 46 0
__consumer_offsets 47 0
__consumer_offsets 48 0
__consumer_offsets 49 0
cluster-test 0 8
cluster-test 1 0
cluster-test 2 0
cluster-test 0 8
cluster-test 토픽의 0번 파티션은 현재 오프셋 8번까지 복제가 완벽히 완료되어 안전한 상태
cluster-test 1 0
Cluster-test 토픽의 1번 파티션은 현재 복제 완료된 오프셋이 0번
cluster-test 2 0
cluster-test 토픽의 2번 파티션 역시 현재 복제 완료된 오프셋이 0번
그러면 그 위에 __consumer_offsets 하는 것들은 무엇인가?
수많은 컨슈머들의 '책갈피' 정보를 안전하게 보관하기 위해 카프카 시스템이 내부적으로 만들어둔 토픽(그중 29번 파티션)의 데이터 복제 상태 라고 보면 된다.
앞에서 파티션 내부 커밋을 열심히 설명햇는데 컨슈머 커밋은 뭔가 많이 다르다.
| 구분 | 파티션 내부 커밋(Message Commit) | 컨슈머 커밋(Offet Commit) |
| 주체 | Kafka 클러스터 (리더 브로커) | 컨슈머 애플리케이션 |
| 목적 | 데이터의 내구성(Durability) 및 유실 방지 | 데이터의 처리 상태(Processing State) 기록 |
| 완료 조건 | ISR 내 모든 팔로워의 복제 완료 (HW 상승) | 비즈니스 로직 처리 후 __consumer_offsets 토픽에 기록 |
| 주요 연관 속성 | acks=all, 복제 지연(Replication Latency) | enable.auto.commit, 멱등성(Idempotency) |
2. 하이워터마크의 정의와 작동 방식
그래서 하이워터마크란 무엇인가?
아파치 카프카에서 하이워터마크(HWM)는 파티션의 모든 복제본(Replica)에 성공적으로 복제가 완료된 마지막 메시지의 오프셋을 의미한다.
결국에는 위에서 설명 하던 것이 하이워터마크가 왜 필요한지 설명한 것이다.
다시 그러면 하이워터마크가 있는 경우 어떻게 동작하는지 정리하자.
카프카 복제 및 HWM 갱신 타임라인 (Offset 100 기준)
[1단계: 리더의 물리적 쓰기]
- 프로듀서가 리더에게 오프셋 100번 메시지를 보냅니다.
- 리더는 자신의 로컬 디스크에 기록합니다. (리더 LEO = 100, 리더 HWM = 99)
[2단계: 팔로워의 물리적 쓰기]
3. 팔로워가 리더에게 100번부터 달라고 Fetch 요청을 보냅니다.
4. 리더는 오프셋 100번 메시지와 함께, 현재 자신이 알고 있는 HWM(99) 정보를 담아 팔로워에게 응답합니다.
5. 팔로워는 받은 100번 메시지를 자신의 로컬 디스크에 기록합니다. (팔로워 LEO = 100, 팔로워 HWM = 99) (이 시점에서 클러스터 내 모든 브로커의 디스크에 데이터가 물리적으로 쓰여졌지만, 리더와 팔로워 모두 HWM은 여전히 99입니다.)
[3단계: 리더의 HWM 갱신 (커밋 발생 지점)]
6. 팔로워는 이제 101번부터 달라고 다음 Fetch 요청을 리더에게 보냅니다. (요청이 100번을 잘 받았다는 암묵적 Ack가 됩니다.)
7. 리더는 이 요청을 받고 "팔로워가 100번까지 복제했구나"라고 인지합니다.
8. ISR 내 모든 복제가 확인되었으므로, 리더가 자신의 HWM을 100으로 갱신합니다. (이 순간이 시스템적으로 '커밋'이 완료되는 지점입니다.)
[4단계: 팔로워의 HWM 갱신 (가장 마지막 단계)]
9. 리더는 6번의 Fetch 요청에 대한 응답을 팔로워에게 보낼 때, 새로 갱신된 HWM(100) 정보를 함께 담아서 보냅니다.
10. 팔로워는 이 응답을 받고 나서야, "아, 리더가 100번까지 커밋처리 했구나"라고 인지하고 자신의 HWM을 100으로 갱신합니다.
LEO (Log End Offset) : 각 레플리카에서 다음에 쓸 오프셋 위치.
HWM(High Watermark) : ISR 내 모든 레플리카의 LEO중 최솟값
결국 메시지가 commit이 되어야지만 이게 올라가겠지?
하이워터마크를 다시 요약하자.
앞에서는 "아파치 카프카에서 하이워터마크(HWM)는 파티션의 모든 복제본(Replica)에 성공적으로 복제가 완료된 마지막 메시지의 오프셋을 의미" 라고 했지만
역할을 한 문장으로 요약하면 "분산 환경에서 데이터가 유실되거나 상태가 꼬이지 않도록 막아주는 논리적인 커밋(Commit) 기준선 이다.
어떻게 안 꼬이게 막을 수 있을까?
컨슈머의 읽기 한계선 (데이터 가시성 제어)와 리더 장애(Failover) 시 데이터 정합성 보장을 통해서 말이다.
컨슈머 읽기 한계선에 대해서는 commit을 설명하면서 이해했다.
그런데 리더 장애시 데이터 정합성 보장은 뭐지?
이를 다음 예로 다시 알아보자.
[리더 장애시 데이터 정합성 보장 시나리오 - 새로운 리더 승격]
초기 상태: 리더(브로커 A)와 팔로워(브로커 B)가 있습니다. 두 브로커 모두 오프셋 99까지 복제를 마쳐서 HWM은 99입니다.
- 장애 발생 직전: 프로듀서가 새로운 메시지(오프셋 100)를 리더 A에게 보냅니다. 리더 A는 로컬 디스크에 기록했습니다(LEO=100). 하지만 팔로워 B가 아직 복제해 가지 않아 HWM은 여전히 99입니다.
- 장애 발생: 이때 리더 A가 갑자기 다운됩니다. 팔로워 B가 새로운 리더로 승격됩니다.
- 결과: 새로운 리더 B의 LEO와 HWM은 99입니다. 오프셋 100번 메시지는 유실되었습니다.
여기까지는 뭐 그럴 수 있다고 하자.
그런데 갑자기 이전의 리더가 다시 살아나서 팔로워로 합류하는 경우 어떤 일이 발생할까?
[ 구 리더 복구 시나리오: 로그 잘라내기 (Log Truncation)]
- 초기 상태 : 리더 A가 다시 살아나서 클러스터에 '팔로워' 자격으로 재합류할 때 발생
- 현재 리더(브로커 B)는 새로운 프로듀서 요청을 받아 오프셋 100 자리에 '새로운 메시지 Z'를 기록했습니다.
- 방금 살아난 구 리더(브로커 A)의 로컬 디스크에는 과거에 기록했던 '옛날 메시지 X'가 오프셋 100 자리에 남아있습니다.
- 만약 이 상태로 브로커 A가 복제를 재개한다면, 같은 오프셋 100번에 대해 브로커 간에 서로 다른 데이터가 존재하는 심각한 'Split Brain(스플릿 브레인)' 상태가 됩니다.
여기서 스플릿 브레인이라는 단어가 재미있다. 쪼개진 뇌? 잘라진 뇌? 무언가 뇌가 하나의 정보를 위해서 동일해야하는데, 잘라진 관계로 다른 정보를 가지고 있는 것이다. (으 무서워!)
위 구 리더 복구 시나리오처럼 스플릿 브레인이 발생하면 안되기 때문에...
카프카는 브로커가 재기동될 때 자신의 로컬 디스크에 있는 데이터를 무조건 신뢰하지 않는다.
이를 해결하기 위해서 다음처럼 작동한다.
[HWM을 이용한 Log Truncation 아키텍처]
- 브로커 A가 재기동되면, 자신이 죽기 직전에 알고 있었던 자신의 HWM(99)을 확인합니다.
- 디스크에 기록된 데이터 중 HWM(99)보다 높은 오프셋의 데이터(즉, 오프셋 100인 '옛날 메시지 X')를 물리적으로 모두 삭제(Truncate)합니다.
- 자신의 LEO를 HWM과 동일하게 99로 맞춥니다.
- 이제 디스크 상태가 완벽하게 안전한 상태(커밋된 상태)로 초기화되었으므로, 현재 리더인 브로커 B에게 오프셋 100번부터 다시 복제해 달라고 Fetch 요청을 보냅니다.
- 브로커 B로부터 '새로운 메시지 Z'를 받아 자신의 오프셋 100번에 덮어씁니다.
이렇게 HWM은 분산 환경에서 데이터가 유실되거나 상태가 꼬이지 않도록 막아주는 논리적인 커밋(Commit) 기준 으로
컨슈머의 읽기 한계선 (데이터 가시성 제어)와 리더 장애(Failover) 시 데이터 정합성 보장의 역할을 한다.
그런데 여기에도 문제가 있다.
3. 리더에포크
HWM만으로는 해결되지 않는 문제가 있다고 한다. 팔로워의 HWM은 늘 늦는다고 한다.
이게 무슨 말이지? 다음의 유실 시나리오를 보자.
[데이터 유실 시나리오: KIP-101]
- 정상 상태: 리더 A와 팔로워 B가 있습니다. 오프셋 100번 메시지가 들어옵니다.
- 물리적 복제: B가 100번을 Fetch해 디스크에 씁니다. (A의 LEO=100, B의 LEO=100)
- 리더 HWM 갱신: A는 B가 100번을 가져간 것을 보고 자신의 HWM을 100으로 올립니다. (시스템적 커밋 완료)
- 장애 발생 1 (팔로워 다운): B가 A로부터 갱신된 HWM(100) 정보를 받기 직전에 전원선이 뽑혀 다운됩니다. (B의 디스크에는 100번 메시지가 있지만, B의 로컬 HWM은 여전히 99입니다.)
- 팔로워 재기동 및 HWM Truncation: B가 다시 켜집니다. 앞서 배운 복구 로직에 따라 B는 자신의 HWM(99)을 맹신하고, 자신의 디스크에 있던 100번 메시지를 삭제(Truncate)해버립니다. (B의 LEO=99가 됨)
- 장애 발생 2 (리더 다운): B가 A에게 다시 100번을 달라고 요청하려는 찰나, 이번에는 리더 A가 하드웨어 고장으로 영구 다운됩니다.
- 새로운 리더 선출: 클러스터는 살아있는 B를 새로운 리더로 승격시킵니다.
- 결과 (데이터 증발): 오프셋 100번 메시지는 분명히 A와 B의 디스크에 모두 쓰였고 커밋까지 완료되었지만, B가 로컬 HWM을 믿고 스스로 지워버린 후 리더가 되었기 때문에 시스템에서 영원히 사라졌습니다.
결국 두 개의 파티션이 시간차를 두고 번갈아서 다운되는 경우 이러한 문제가 생긴다.
그래서, Kafka에서는 리더에포크 라는 것을 도입했다.
이를 정리하면 노드가 연달아 죽는 최악의 인프라 환경에서도, 브로커가 부정확한 정보(로컬 HWM)를 맹신하여 자기 데이터를 스스로 지워버리는 '자해 행위'를 막기 위해! 리더에포크를 도입했다.
이제는 복구 시 기준점을 '나의 과거 HWM'에서 '리더들의 세대(Generation)별 히스토리 로 변경했다!
[리더에포크 도입 시나리오]
- B가 다운되었다가 재기동됩니다. (B의 디스크에 100번 메시지가 있고, B의 로컬 HWM은 99입니다.)
- (핵심 변경점) B는 무작정 HWM 99를 기준으로 100번을 잘라내지 않습니다. 대신, 현재 리더인 A에게 OffsetsForLeaderEpoch API 요청을 보냅니다.
- B의 질문: "A야, 내가 가진 가장 최신 데이터의 에포크가 0이고 오프셋은 100인데, 이거 유효한 데이터니?"
- A의 답변: "어, 내 기록을 보니 에포크 0은 오프셋 100까지 쓰여진 게 맞아."
- B는 A의 답변을 듣고, 100번이 삭제해서는 안 될 유효한 데이터임을 깨닫고 Truncation을 수행하지 않습니다.
- 이후 리더 A가 죽고 B가 새로운 리더가 되더라도, 100번 데이터는 B의 디스크에 안전하게 남아있으므로 데이터 유실이 발생하지 않습니다. (이때 B가 리더가 되면서 에포크는 1로 올라갑니다.)
그런데 이 리더에포크는 어디에 저장이 되어 있나?
필자가 세팅한 cluster-test 토픽에서 kafka-2에 들어가서 확인해보았다.

0 // 파일의 버전 (Version)
2 // 현재 기록된 에포크 히스토리의 개수
0 0 // [에포크 번호] [해당 에포크의 시작 오프셋]
3 5 // [에포크 번호] [해당 에포크의 시작 오프셋]
leader-epoch-checckpoint를 해석해보자.
"에포크 0번 리더가 오프셋 0번부터 쓰기 시작했고, 이후 리더가 바뀌어 에포크 3번 리더가 오프셋 5번부터 데이터를 썼다"
를 의미한다.
4. 실습을 통한 리더에포크 변화 과정 이해
이제 그러면 실습을 통해서 리더에포크 변화 과정을 정확하게 이해해보고자 한다.
cat << 'EOF' > docker-compose-epoch.yml
networks:
epoch-network:
name: epoch-local-network
driver: bridge
services:
epoch-kafka-1:
image: apache/kafka:latest
container_name: epoch-kafka-1
ports:
- "40092:19092"
environment:
CLUSTER_ID: 'EPOCH_TEST_CLUSTER_123'
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: 'controller,broker'
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@epoch-kafka-1:9093,2@epoch-kafka-2:9093,3@epoch-kafka-3:9093'
KAFKA_LISTENERS: 'INTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093,EXTERNAL://0.0.0.0:19092'
KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://epoch-kafka-1:9092,EXTERNAL://localhost:40092'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'
networks:
- epoch-network
epoch-kafka-2:
image: apache/kafka:latest
container_name: epoch-kafka-2
ports:
- "50092:29092"
environment:
CLUSTER_ID: 'EPOCH_TEST_CLUSTER_123'
KAFKA_NODE_ID: 2
KAFKA_PROCESS_ROLES: 'controller,broker'
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@epoch-kafka-1:9093,2@epoch-kafka-2:9093,3@epoch-kafka-3:9093'
KAFKA_LISTENERS: 'INTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093,EXTERNAL://0.0.0.0:29092'
KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://epoch-kafka-2:9092,EXTERNAL://localhost:50092'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'
networks:
- epoch-network
epoch-kafka-3:
image: apache/kafka:latest
container_name: epoch-kafka-3
ports:
- "60092:39092"
environment:
CLUSTER_ID: 'EPOCH_TEST_CLUSTER_123'
KAFKA_NODE_ID: 3
KAFKA_PROCESS_ROLES: 'controller,broker'
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@epoch-kafka-1:9093,2@epoch-kafka-2:9093,3@epoch-kafka-3:9093'
KAFKA_LISTENERS: 'INTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093,EXTERNAL://0.0.0.0:39092'
KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://epoch-kafka-3:9092,EXTERNAL://localhost:60092'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'
networks:
- epoch-network
epoch-setup:
image: apache/kafka:latest
container_name: epoch-setup
networks:
- epoch-network
depends_on:
- epoch-kafka-1
- epoch-kafka-2
- epoch-kafka-3
command: >
/bin/sh -c "
echo 'Waiting for Epoch Cluster to be ready...' &&
sleep 15 &&
/opt/kafka/bin/kafka-topics.sh --create --if-not-exists --topic epoch-test --partitions 2 --replication-factor 3 --bootstrap-server epoch-kafka-1:9092 &&
echo 'Epoch test topic created successfully!'
"
EOF
docker compose -f docker-compose-epoch.yml up -d
docker exec epoch-kafka-1 /opt/kafka/bin/kafka-topics.sh --describe --topic epoch-test --bootstrap-server localhost:9092
~/kafka-epoch-test$ docker exec epoch-kafka-1 /opt/kafka/bin/kafka-topics.sh --describe --topic epoch-test --bootstrap-server localhost:9092
Topic: epoch-test TopicId: Q3LH6a8NQzqSL3PyvmFgPw PartitionCount: 2 ReplicationFactor: 3 Configs: min.insync.replicas=1
Topic: epoch-test Partition: 0 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3 Elr: LastKnownElr:
Topic: epoch-test Partition: 1 Leader: 2 Replicas: 2,3,1 Isr: 2,3,1 Elr: LastKnownElr:
# 1. 메시지 하나 넣기 (프롬프트 > 가 나오면 아무 글자나 치고 엔터, 그 후 Ctrl+C 로 빠져나오기)
docker exec -it epoch-kafka-1 /opt/kafka/bin/kafka-console-producer.sh --topic epoch-test --bootstrap-server localhost:9092
# 2. 1번 브로커 강제 종료!
docker stop epoch-kafka-1
강제로 종료한 후 다음을 보면 이제... Partitions:0 의 Leader가 2로 변경되어 있음을 확인할 수 있다.
:~/kafka-epoch-test$ docker exec epoch-kafka-2 /opt/kafka/bin/kafka-topics.sh --describe --topic epoch-test --bootstrap-server localhost:9092
Topic: epoch-test TopicId: Q3LH6a8NQzqSL3PyvmFgPw PartitionCount: 2 ReplicationFactor: 3 Configs: min.insync.replicas=1
Topic: epoch-test Partition: 0 Leader: 2 Replicas: 1,2,3 Isr: 2,3 Elr: LastKnownElr:
Topic: epoch-test Partition: 1 Leader: 2 Replicas: 2,3,1 Isr: 2,3 Elr: LastKnownElr:
이것을 다음을 의미한다.
- 리더 승격: 파티션 0의 Leader가 죽은 1번에서 살아남은 2번 브로커로 교체됨
- ISR 퇴출: 1번 브로커가 통신 두절되었으므로 Isr 목록(2, 3)에서 즉시 쫓겨났다!
그러면 정말
docker exec -it epoch-kafka-2 /bin/bash
e1f86ad5ef0f:/tmp/kafka-logs/epoch-test-0$ cat leader-epoch-checkpoint
0
2
0 0
1 1
다음을 해석해 보자.
0 // 이 체크포인트 파일의 포맷 버전입니다. (버전 0)
2 // 지금까지 기록된 에포크(세대) 히스토리의 총 개수가 2개라는 뜻입니다.
0 0 // [에포크 0번] 시대는 [오프셋 0번]부터 시작되었다. (초기 클러스터 런칭 시점)
1 1 // [에포크 1번] 시대는 [오프셋 1번]부터 시작되었다. (방금 2번 브로커가 리더로 승격된 시점)
제 0대 리더(Epoch 0)의 시대는 오프셋 0번부터 시작되었다. 하는 것이 0 0,
제 1대 리더(Epoch 1)의 시대는 오프셋 1번부터 시작된다. 하는것이 1 1 이다.
현재 해본 것과는 별개로 좀 더 다르게 읽는 연습을 해보자.
- 0 0 👉 0대 리더 선출, 오프셋 0번부터 기록 시작.
- 1 150 👉 0대 리더 다운 후 1대 리더 선출. 오프셋 150번부터 기록 시작.
- 2 320 👉 1대 리더 다운 후 2대 리더 선출. 오프셋 320번부터 기록 시작.
- 3 400 👉 2대 리더 다운 후 과거 노드가 복구되어 3대 리더로 재선출. 오프셋 400번부터 기록 시작.
이제 좀 알겠다!
내용이 굉장히 헷갈리고 어렵다. 스크롤을 내려서 읽기에는 뭔가 ... 적어보이는듯 하지만 자세히 흐름을 따라서 읽고 이해하는것은 정말 별개의 일인듯 하다.
그래서 시나리오를 바탕으로 이해하기 위해서 AI에게 끊임없이 질문햇고 이를 정리했다.
또 최대한 그려보려고 했는데, 각각의 시나리오에 대해서 다 그림을 그리면 그림 그리는데 시간을 너무 할애할 것 같아서 최대한 친절한 글로(?) 마무리했다.
이게 AI를 써도 역시 ... 사용자의 능력에 따라서 결과도 천차만별일 수 있겠다는 생각이 들었다.
'programming language > Kakfa' 카테고리의 다른 글
| [Kafka 기초 학습] 카프카 기초 개념 4 - 메시지와 메시지 포맷 과 메시지 전송 및 분석 실습 + 파티셔닝 이해 (0) | 2026.06.09 |
|---|---|
| [Kafka 기초 학습] 카프카 기초 개념 2 - 개념훑기, 프로듀서의 작동 방식, 그리고 topic 상세 정보 보기 (0) | 2026.06.03 |
| [Kafka 기초 학습] 카프카 기초 개념 1 - Kafka 간단 정리 및 실습환경 세팅 (0) | 2026.05.31 |