Apache Kafka 입문 정리 5편. 카프카가 어떻게 빠르고 안전한지 내부 동작을 풀어 — Replication Factor와 Leader/Follower, ISR과 High Watermark, 장애 시나리오와 복구, min.insync.replicas, Unclean Leader Election의 함정, 컨트롤러 역할, 로그 세그먼트와 인덱스, 페이지 캐시·Zero-Copy 빠른 비결까지 친절하게 정리.
이 글은 Apache Kafka 입문 정리 시리즈의 다섯 번째 편입니다. 1~4편을 거치며 카프카의 큰 그림(중앙 우체국 비유)·핵심 아키텍처(토픽·파티션·오프셋)·프로듀서 옵션·컨슈머 옵션까지 차곡차곡 쌓아 왔어요. 여기까지 오면 "카프카에 데이터를 넣고 빼는 흐름"은 이제 머릿속에 그려질 겁니다.
5편의 주제는 한 단어로 Replication입니다. 카프카가 어떻게 데이터를 여러 브로커에 안전하게 복제(Replication)하고, 한 서버가 죽었을 때 어떻게 복구하며, 왜 그렇게나 빠른지 — 이 세 가지 질문에 답하는 단원이에요. Replication·ISR·High Watermark·min.insync.replicas·Unclean Leader Election·컨트롤러·로그 세그먼트·Zero-Copy 같은 용어가 한꺼번에 나오는 자리라, 처음 보면 "이게 다 뭐야?" 싶을 수 있어요. 그래도 큰 그림은 단순합니다 — Replication이 카프카의 안전망이고, 나머지 개념은 그 위에 얹히는 디테일이에요.
왜 이 단원이 처음엔 어렵게 느껴질까요
이유는 세 가지예요.
첫째, 용어가 영어 약자 폭격이에요. ISR·HW·LEO·LSO·URP — 한 페이지에 약자가 너무 많이 나와서 머리가 핑 돕니다. 게다가 비슷하게 생겨서 어떤 게 어떤 건지 헷갈리기 시작해요.
둘째, 분산 시스템 특유의 "동시성·합의" 개념이 처음 등장해요. "여러 브로커가 서로 합의해서 누가 리더고 누가 살아 있는지 추적한다"는 그림이 익숙하지 않으면, 글로 읽어도 잘 그려지지 않아요.
셋째, 장애 시나리오가 너무 많아요. 브로커 1개 다운·2개 다운·네트워크 분할·컨트롤러 사망·ISR 전부 죽음 — 케이스마다 동작이 조금씩 달라서 한 번에 외우기 힘듭니다.
해결법은 한 가지예요. 카프카 복제를 "회사 자료 백업 보관소 시스템" 으로 비유로 잡고 풀어 갑니다. 같은 자료를 여러 보관소에 복사해 두고, 그중 한 곳을 "이 자료를 책임지고 받는 본점(리더)"으로 정하고, 나머지(팔로워)는 본점을 따라 백업해요. 본점이 사고 나면 백업 보관소 중 하나가 새 본점이 되고, 그동안 자료를 못 쫓아간 보관소는 "ISR 그룹"에서 빠져요. 이 그림 위에 디테일을 얹으면 갑자기 명확해집니다.
이번 편에서는 이 비유를 따라 — 복제가 왜 필요한가 → 복제 팩터를 어떻게 정하나 → ISR이 뭔가 → 장애가 나면 어떻게 되나 → 컨트롤러는 뭘 하나 → 디스크에는 어떻게 저장되나 → 왜 빠른가 순으로 풀어 갑니다.
복제(Replication) — 같은 자료를 여러 보관소에 복사하는 이유
카프카는 분산 시스템이에요. 즉 여러 브로커(서버)가 묶여 함께 일합니다. 그러면 자연스럽게 한 가지 질문이 생겨요. "브로커 한 대가 죽으면 거기 저장된 데이터는 어떻게 되나?"
답은 한 줄로 — 같은 데이터를 여러 브로커에 복사해 둔다. 이게 복제(Replication)예요. 더 깊이 파고 싶으면 Apache Kafka 공식 문서의 Replication 섹션이 가장 정확한 출처입니다.
복제가 없는 경우와 있는 경우를 비교해 봅시다.
복제 없음 (replication-factor = 1):
Broker 1: P0 [data]
→ Broker 1 장애 발생 → P0 데이터 모두 소실, 서비스 중단
복제 있음 (replication-factor = 3):
Broker 1: P0 [Leader, 데이터 보관]
Broker 2: P0 [Follower, 데이터 백업]
Broker 3: P0 [Follower, 데이터 백업]
→ Broker 1 장애 발생 → Broker 2 또는 3이 자동으로 리더 승격, 서비스 계속
회사 비유로 — 회사의 모든 계약서를 본사 한 곳에만 보관하면, 본사가 화재로 없어지는 순간 모든 계약 정보가 사라집니다. 그래서 같은 계약서를 본사·강남 지점·판교 지점 — 이렇게 세 곳에 복사 보관하는 거예요. 한 곳이 사고 나도 나머지 두 곳이 살아 있으니 자료를 잃지 않아요.
Replication Factor — 복사본 개수
복사본을 몇 개 둘지 정하는 게 Replication Factor예요. 보통 운영에서 3을 씁니다. 그 이유를 표로 봅시다.
| 복제 팩터 | 필요 브로커 | 내결함성 | 스토리지 비용 | 권장 환경 |
|---|---|---|---|---|
| 1 | 1 이상 | 없음 | 1배 | 개발·테스트 |
| 2 | 2 이상 | 1개 장애 견딤 | 2배 | 비용 민감 환경 |
| 3 | 3 이상 | 2개 장애 견딤 | 3배 | 운영 표준 |
| 4 이상 | 4 이상 | 3개 이상 장애 견딤 | 4배 이상 | 매우 중요한 데이터 |
여기서 시험 함정이 하나 있어요. 복제 팩터를 브로커 수보다 크게 설정할 수 없습니다. 브로커가 3대인데 복제 팩터를 4로 잡으면 토픽 생성 자체가 실패해요. 같은 자료를 같은 보관소에 두 번 두는 건 의미가 없잖아요. 복사본은 반드시 다른 브로커에 두는 게 원칙이라, 복사본 개수가 브로커 수를 넘을 수 없어요.
운영 표준이 왜 3인지도 한 번 짚고 가요. 1은 사실상 복제가 아니에요. 2는 한 대만 사고 나도 더는 여유가 없어 위태로워요(다음 한 대가 더 죽으면 끝). 3이면 두 대까지 사고 나도 한 대가 살아 있어 서비스가 가능합니다. 4 이상은 비용이 너무 커져서 정말 중요한 자료가 아니면 잘 안 써요.
리더(Leader)와 팔로워(Follower) — 책임 보관소와 백업 보관소
복사본 3개가 있다고 해서 셋 다 똑같이 일하는 건 아니에요. 그중 하나만 "리더"가 되고, 나머지는 "팔로워" 예요.
- 리더(Leader) — 이 자료를 책임지고 받는 보관소. 프로듀서는 항상 리더에게만 메시지를 보내요. 컨슈머도 보통은 리더에게서 읽어요(2.4부터는 가까운 팔로워에서도 읽을 수 있게 됐지만, 기본은 리더).
- 팔로워(Follower) — 리더를 따라 백업하는 보관소. 리더가 받은 메시지를 그대로 복사해 와요. 평소에는 클라이언트와 직접 거래하지 않고, 리더가 죽었을 때 새 리더가 될 후보로 대기해요.
회사 비유로 — 본사가 리더예요. 모든 계약서는 일단 본사로 들어가고, 본사가 그걸 강남·판교 지점에 복사 전송해요. 강남·판교 지점은 평소에 손님을 직접 응대하지 않고 백업 역할만 하다가, 본사가 사고 나면 그중 한 곳이 본사 역할을 이어받아요.
복제 흐름을 단계별로 풀면 이렇게 돼요(acks=all 기준).
1. Producer가 Broker 1 (리더)에 메시지 전송
2. Broker 1이 자기 로컬 로그에 메시지 저장
3. Broker 2, 3 (팔로워)이 Broker 1에서 메시지 페치 (pull 방식)
4. 모든 ISR(In-Sync Replicas) 팔로워가 저장 완료
5. Broker 1이 Producer에게 ACK 반환
여기서 시험 함정이 하나 있어요. 팔로워는 리더가 "밀어 보내 주는(push)" 게 아니라, 리더에게서 "당겨 가져가요(pull)" . 팔로워가 자기 페이스로 리더에게 "내 다음 메시지 줘"라고 요청해요. 이 pull 방식 덕분에 팔로워가 잠깐 느려져도 리더가 부담 없이 자기 일을 계속할 수 있어요.
ISR(In-Sync Replicas) — Replication을 따라잡은 백업 그룹
복제 이야기를 하다 보면 자연스럽게 의문이 생겨요. "팔로워가 게을러서 한참 뒤처지면 어떡하지?" 네트워크가 잠깐 끊기거나 디스크가 느려지면 팔로워가 리더를 못 따라잡을 수 있어요. 그럼 그 팔로워에 보관된 데이터는 옛날 데이터예요. 이런 팔로워를 새 리더로 뽑으면 큰일 나죠 — 옛날 데이터로 서비스가 돌아가니까요.
그래서 카프카는 "최신 상태를 따라잡은 팔로워들"만 따로 모아서 ISR(In-Sync Replicas) 이라고 부릅니다. 직역하면 "동기화된 복제본 집합"이에요.
정상 상태 (모두 ISR):
Leader: Broker 1 (ISR에 항상 포함)
Follower: Broker 2 (ISR ✓ - 최신 따라잡음)
Follower: Broker 3 (ISR ✓ - 최신 따라잡음)
ISR = {Broker 1, Broker 2, Broker 3}
Broker 3이 게을러진 경우:
Broker 1: [offset 0~150] Leader
Broker 2: [offset 0~150] Follower (ISR ✓)
Broker 3: [offset 0~80] Follower (ISR ✗ → Out-of-Sync)
ISR = {Broker 1, Broker 2}
ISR 탈퇴·재진입 조건
팔로워가 어느 정도 뒤처지면 ISR에서 빠지냐 — 이 기준이 replica.lag.time.max.ms 설정이에요. 기본값은 30초.
ISR 탈퇴 조건 — 팔로워가 30초 동안 리더의 가장 최근 메시지를 못 따라잡으면 ISR에서 빠져요. "30초 동안 한 발도 못 따라옴"이 기준이에요.
ISR 재진입 조건 — 빠진 팔로워가 다시 따라잡으면 자동으로 ISR에 재편입됩니다. 영구 추방이 아니에요.
회사 비유로 — 백업 지점이 30초 넘게 본사 자료를 못 따라 베끼면 "최신 상태 그룹"에서 빠져요. 다시 부지런히 따라잡으면 그룹에 복귀하고요.
여기서 시험 함정이 하나 있어요. 리더는 항상 ISR에 포함됩니다. ISR이 "최신 상태를 따라잡은 복제본 집합"인데, 리더가 곧 최신 상태의 기준이니 당연히 자기 자신이 포함돼요. ISR이 1개라는 말은 "리더만 살아 있고 팔로워는 모두 뒤처졌거나 죽었다"는 뜻이에요. 이 상태에서 리더가 죽으면 정말 위험해져요.
High Watermark와 Log End Offset — 안전 지점과 끝 지점
ISR 이야기와 함께 자주 나오는 두 개념이 High Watermark(HW) 와 Log End Offset(LEO) 예요.
- Log End Offset (LEO) — 리더 또는 팔로워의 로그에서 마지막으로 저장된 메시지의 다음 오프셋. "여기까지 적었다"의 끝 지점.
- High Watermark (HW) — 모든 ISR 팔로워가 따라잡은 지점. "여기까지는 모든 백업이 동기화 완료한 안전 지점."
회사 비유로 — 본사 게시판에 글이 어제 100개, 오늘 50개가 추가로 올라가 있다고 해요. 본사 LEO는 150이에요(150개 전부 게시됨). 그런데 강남 지점은 아직 130까지만 베껴 갔고, 판교 지점은 140까지 베껴 갔어요. 그럼 모든 백업이 따라잡은 지점은 130이에요. 이게 HW.
Leader (Broker 1): [offset 0 ~ 150] LEO = 150
Follower (Broker 2): [offset 0 ~ 140] LEO = 140
Follower (Broker 3): [offset 0 ~ 130] LEO = 130
High Watermark = min(140, 130, 150) = 130
→ "150까지 적혀 있지만, 130까지가 안전한 지점"
여기서 정말 중요한 시험 함정이 있어요. 컨슈머는 High Watermark 이후의 메시지는 못 읽어요. 즉 메시지가 리더에는 도착했지만 모든 ISR이 아직 따라잡지 못했다면, 컨슈머한테는 그 메시지가 안 보여요. 왜냐? 만약 그 메시지를 보여 주고 나서 리더가 죽으면, 새 리더로 뽑힌 팔로워에는 그 메시지가 없을 수도 있거든요. 그러면 "분명 봤던 메시지가 사라지는" 일관성 깨짐 사고가 나요. 그래서 카프카는 "안전하게 복제된 부분까지만 컨슈머에 노출" 해요. 이게 HW의 핵심 역할이에요.
min.insync.replicas — Replication이 최소 N개 동기화돼야 쓰기 허용
ISR을 알면 이 설정이 자연스럽게 풀려요. min.insync.replicas는 "쓰기를 받으려면 ISR에 최소 몇 개가 있어야 한다"는 기준 이에요.
회사 비유로 — "계약서를 받으려면 최소 본사 + 1개 지점이 동시에 받을 수 있어야 한다"는 사내 규칙. 만약 본사 외에 모든 지점이 마비됐다면, 본사만 받고 끝내는 건 위험하니까 그냥 받기를 거부해요.
설정 조합과 동작을 풀어 봅시다.
설정: replication-factor=3, min.insync.replicas=2, acks=all
시나리오 1 — 3개 브로커 모두 정상
ISR = {B1, B2, B3} (3개) >= min.insync.replicas (2)
→ 정상 동작, Producer 성공 응답
시나리오 2 — 브로커 1개 다운
ISR = {B1, B2} (2개) >= min.insync.replicas (2)
→ 아슬아슬하지만 동작, Producer 성공 응답
시나리오 3 — 브로커 2개 다운
ISR = {B1} (1개) < min.insync.replicas (2)
→ NotEnoughReplicasException, Producer 쓰기 실패
(컨슈머 읽기는 계속 가능)
시나리오 4 — min.insync.replicas=1로 낮추면
ISR이 1개만 있어도 쓰기 허용
→ 가용성은 높지만 데이터 손실 위험 증가
여기서 시험 함정이 하나 있어요. acks=all과 min.insync.replicas=2는 항상 같이 봐야 의미가 있어요. acks=all만 설정하면 "현재 ISR 모두에게 ACK 받자"인데, ISR이 1개로 줄어 있으면 사실상 "리더 1개만 받으면 OK"가 돼요. 그러면 acks=1이랑 별 차이 없게 돼 버려요. 그래서 acks=all + min.insync.replicas=2 조합으로 "최소 2개에 안전하게 복제될 때만 쓰기 허용"을 강제하는 거예요. 운영 표준 조합이에요.
복제 팩터 3과의 관계도 짚어 둡시다. 복제 팩터 3 + min.insync.replicas 2 + acks=all — 이게 보통 권장되는 운영 조합이에요. 이러면 브로커 1대 사고가 나도 서비스는 계속되고, 2대 사고가 나면 쓰기는 멈추되 데이터 손실은 없어요.
리더 선출 — 리더가 죽으면 누가 새 리더가 되나
리더 브로커가 사고 나면 누군가 새 리더로 올라와야 해요. 이 결정을 누가 어떻게 하는지가 카프카 안의 큰 챕터예요.
정상 리더 선출 흐름
정상 상태:
P0 Leader = Broker 1
P0 ISR = {Broker 1, Broker 2, Broker 3}
Broker 1 장애 발생:
1. 컨트롤러(예: Broker 3)가 Broker 1 장애 감지
2. ISR에서 새 리더 후보 선정: {Broker 2, Broker 3}
3. Preferred Replica 또는 ISR 첫 번째 항목 기준으로 선출
4. P0 Leader = Broker 2로 업데이트
5. 메타데이터 변경 (KRaft에서는 메타데이터 로그)
6. 모든 브로커·클라이언트에 새 리더 정보 전파
회사 비유로 — 본사가 화재로 마비됐어요. 클러스터의 행정 책임자(컨트롤러)가 그걸 감지하고, "지금 살아 있는 ISR 지점 중에 강남이 우선순위가 가장 높다, 강남을 새 본사로 지정"하고 모든 직원·고객에게 공지해요.
Preferred Replica — 원래 정해진 1순위 리더
카프카는 토픽 생성 시 각 파티션마다 "이 복제본이 1순위 리더" 라고 선호 리더를 미리 정해 둬요. 보통 ISR 목록의 맨 첫 번째 복제본이 선호 리더가 돼요.
장애 후 복구되면 — 기본 설정(auto.leader.rebalance.enable=true)에서는 카프카가 자동으로 원래 선호 리더에게 리더십을 돌려줘요. 그래야 클러스터 전체에서 리더 분포가 균등해져요. 안 그러면 한 브로커가 너무 많은 파티션의 리더를 떠맡게 되거든요.
Unclean Leader Election — 데이터 손실을 감수하는 비상 선택
여기가 이번 편에서 가장 중요한 함정 포인트예요.
상황 — ISR이 비어 버렸어요(전부 죽음). 그런데 ISR에 없는, 한참 뒤처진 팔로워(Broker 4) 하나가 살아 있어요. 어떻게 할까요?
두 가지 선택지가 있어요.
unclean.leader.election.enable = false (기본값):
→ ISR이 비었으니 리더를 못 뽑음
→ 파티션 오프라인 (사용 불가)
→ 누가 살아날 때까지 기다림
→ 데이터 무결성 보장 (옛날 데이터로 덮어쓰는 일 없음)
unclean.leader.election.enable = true:
→ 비록 ISR 밖이어도 살아 있는 Broker 4를 강제 리더로 승격
→ 서비스 재개
→ 하지만 Broker 4에는 옛날 데이터까지만 있음
→ 그 사이에 들어왔던 최신 데이터는 영구 손실
회사 비유로 — 본사·강남·판교 다 화재로 마비됐고, 한참 뒤처진 인천 지점만 살아 있어요. 인천에는 일주일 전 자료까지만 있어요.
- false 선택 — "옛날 자료로 운영 못 한다, 본사 살아날 때까지 기다리자." → 서비스 멈추지만 자료 무결성 유지.
- true 선택 — "그래도 일단 인천을 본사로 써서 영업하자." → 서비스 재개하지만 일주일치 자료 손실.
여기서 시험 함정이 하나 있어요. 기본값은 false예요. 즉 카프카는 기본적으로 "데이터 손실보다 일시 멈춤이 낫다"는 입장이에요. 이걸 true로 바꿔야 할 자리는 명확합니다.
- false 유지 — 금융·결제·의료·주문 — 데이터 무결성이 절대적인 자리.
- true로 바꿈 — 로그·메트릭·클릭 스트림 — 일부 데이터 손실보다 가용성이 더 중요한 자리.
기본값을 함부로 바꾸면 정말 큰 사고가 날 수 있는 설정이라, 한 번 더 강조할게요.
컨트롤러(Controller) — 클러스터 행정 책임자
리더 선출·장애 감지 같은 클러스터 관리 작업을 누군가는 해야 해요. 그 역할을 맡는 게 컨트롤러예요. 클러스터 안 모든 브로커 중 하나가 컨트롤러 역할을 해요.
컨트롤러의 역할
- 파티션 리더 선출
- 브로커 장애 감지·처리
- 토픽 생성·삭제 처리
- ISR 변경 추적
- 메타데이터 변경 전파
회사 비유로 — 클러스터 행정실이에요. 어느 지점이 어느 자료의 본사 역할을 하는지, 어느 지점이 살아 있고 죽었는지, 새 자료실(토픽)이 생기면 어느 지점들에 분산할지 — 이걸 결정하고 모든 지점에 공지해요.
Zookeeper 시대 vs KRaft 시대
옛날 카프카(3.x 이전)는 컨트롤러를 뽑는 데 Zookeeper를 썼어요.
Zookeeper 방식:
브로커들이 Zookeeper에 임시 노드를 먼저 만드는 경쟁
→ 가장 빨리 만든 브로커가 컨트롤러
→ 컨트롤러 사고 나면 다시 경쟁
문제점:
- Zookeeper에 의존적 (외부 시스템 추가 운영)
- 컨트롤러 페일오버에 시간 소요 (몇 초)
- 클러스터 확장 시 Zookeeper 병목
KRaft 모드부터는 카프카가 자체적으로 처리해요. 자세한 내부 구조는 Apache Kafka 공식 KRaft 문서에서 한 번 더 확인하면 좋아요.
KRaft 방식:
전용 Controller 노드들(Quorum, 보통 3개)이 Raft 알고리즘으로 합의
→ 그중 하나가 Active Controller
→ 페일오버 시간 밀리초 수준
Controller Quorum 구조:
Controller 1 (Active) ←→ Controller 2 ←→ Controller 3
↕ (메타데이터 복제)
Broker 1, Broker 2, Broker 3 ...
여기서 시험 함정이 하나 있어요. 클러스터에는 항상 Active Controller가 정확히 1개 있어야 해요. 0개면 클러스터 관리가 안 되고(심각한 장애), 2개 이상이면 "스플릿 브레인" — 두 행정실이 서로 다른 결정을 내려서 클러스터가 갈라지는 사고예요. KRaft의 Raft 합의는 이 "정확히 1개" 보장을 알고리즘 수준에서 해 줘요.
Raft 합의 알고리즘 — 과반수 원칙
KRaft의 Raft가 어떻게 "정확히 1개"를 보장하는지 핵심만 짚어 둡시다.
Raft의 핵심:
1. 리더 선출 — 노드들이 투표로 Active Controller 선출
2. 로그 복제 — 리더가 팔로워에게 메타데이터 변경 로그 복제
3. 과반수 원칙 — 과반수(quorum)가 확인해야 변경 커밋
내결함성 (Controller 개수별):
3개 Controller → 1개 장애 견딤 (과반수 = 2개)
5개 Controller → 2개 장애 견딤 (과반수 = 3개)
회사 비유로 — 행정 책임자를 정할 때 모든 후보가 "과반수의 표를 받은 사람만 책임자가 된다"는 규칙으로 투표해요. 두 명이 동시에 과반수를 받는 건 수학적으로 불가능하니, 항상 0명 또는 1명만 뽑혀요. 이게 스플릿 브레인을 막는 비결.
장애 시나리오 — 케이스별로 풀어 보기
여기까지 부품을 다 봤으니 이제 케이스별로 풀어 봅시다.
시나리오 1 — 브로커 1대 다운 (복제 팩터 3)
Before:
Partition 0: Leader = Broker 1, ISR = {B1, B2, B3}
Broker 1 다운:
컨트롤러가 감지 (Zookeeper 세션 만료 또는 KRaft heartbeat 누락)
After:
Partition 0: Leader = Broker 2 (ISR에서 새 리더), ISR = {B2, B3}
→ 서비스 계속 운영, 데이터 손실 없음
→ Producer는 자동으로 새 리더로 재라우팅
가장 흔하고 가장 안전한 케이스. min.insync.replicas=2 조건도 만족(ISR 2개)해서 쓰기·읽기 모두 정상 동작.
시나리오 2 — 브로커 2대 다운 (복제 팩터 3)
Before:
Partition 0: Leader = Broker 1, ISR = {B1, B2, B3}, min.insync.replicas = 2
Broker 1, 2 다운:
ISR = {B3} (1개) < min.insync.replicas (2)
acks = all 인 경우:
→ Producer 쓰기 실패 (NotEnoughReplicasException)
→ Consumer 읽기는 계속 가능 (B3에 있는 만큼)
→ 운영자가 죽은 브로커 복구하면 자동 복구
unclean.leader.election.enable = true 인 경우:
→ ISR 밖 후보까지 동원해 강제 리더 선출
→ 데이터 손실 가능성 있는 비상 모드
여기서 시험 함정이 하나 있어요. 시나리오 2에서 "쓰기 실패"는 사실 의도된 안전 동작 이에요. "옛날 데이터로 잘못 덮어쓰느니 차라리 잠시 막자"는 정책이 작동하는 거예요. 운영자가 사고를 인지하고 복구할 시간을 벌어 주는 안전핀이에요.
시나리오 3 — 네트워크 파티션(Network Partition)
분산 환경에서 네트워크가 갈라짐:
그룹 A: Broker 1, 2 (서로 통신 가능)
그룹 B: Broker 3 (그룹 A와 통신 불가)
Partition 0 Leader = Broker 1 (그룹 A에 위치)
→ Broker 3은 리더에 접근 못 함 → ISR에서 제거
→ Producer·Consumer는 Broker 1과 정상 통신
네트워크 복구:
Broker 3이 Broker 1에서 누락된 데이터 페치
→ 따라잡으면 ISR에 자동 재편입
회사 비유로 — 인천 지점 회선이 잠깐 끊겼어요. 본사·강남은 서로 통화 가능, 인천만 고립. 인천은 그동안 ISR에서 빠지고, 회선 복구되면 다시 자료 베껴서 ISR로 복귀해요.
시나리오 4 — 컨트롤러 자체 사망
컨트롤러였던 Broker 3이 사고 났어요.
Zookeeper 방식:
남은 브로커들이 /controller 임시 노드 생성 경쟁
→ 가장 빠른 브로커가 새 컨트롤러 (몇 초 소요)
KRaft 방식:
Controller Quorum 안에서 Raft 투표로 새 Active Controller 선출
→ 밀리초 수준 페일오버
KRaft가 Zookeeper 시절 대비 가장 크게 빨라진 부분이 이 컨트롤러 페일오버 시간이에요.
로그 스토리지 — 디스크에는 어떻게 저장되나
이제 디스크 안쪽으로 들어갈 차례예요. 카프카가 메시지를 디스크에 어떻게 적는지 알아야, 다음 절의 "왜 빠른가"가 설명돼요.
로그 세그먼트(Log Segment) 구조
각 파티션은 여러 개의 세그먼트 파일로 잘려서 저장돼요. 한 파일에 다 적는 게 아니라 일정 크기·시간이 차면 새 파일로 넘겨요.
/kafka-data/
└── first_topic-0/ ← 토픽이름-파티션번호
├── 00000000000000000000.log ← 실제 메시지 데이터
├── 00000000000000000000.index ← 오프셋 인덱스
├── 00000000000000000000.timeindex ← 타임스탬프 인덱스
├── 00000000000001234567.log ← 다음 세그먼트
├── 00000000000001234567.index
└── 00000000000001234567.timeindex
파일 이름 = 그 세그먼트의 첫 번째 오프셋 번호. 위 예시는 첫 세그먼트가 오프셋 0~1234566을 담았고, 다음 세그먼트가 1234567부터 시작한다는 뜻이에요.
세그먼트 롤오버 — 새 파일로 넘어가는 시점
log.segment.bytes = 1073741824 # 1GB (기본값)
log.roll.hours = 168 # 7일 (기본값)
세그먼트가 1GB에 도달하거나, 7일이 지나면 새 세그먼트를 만들어요. 둘 중 먼저 도달하는 조건이 발동.
Active Segment — 현재 쓰기가 진행 중인 가장 최신 세그먼트. 이 세그먼트는 절대 삭제·압축 대상이 아니에요. "지금 쓰고 있는 노트는 안 버린다"는 원칙.
여기서 시험 함정이 하나 있어요. "메시지 보존 기간 7일"과 "세그먼트 롤오버 7일"은 다른 설정이에요. 세그먼트 롤오버는 "파일을 언제 새로 시작할지"고, 메시지 보존은 "오래된 파일을 언제 삭제할지"예요. 다음 절에서 보존 정책을 따로 다룹니다.
인덱스 파일 — 빠른 오프셋 탐색의 비결
.log 파일에는 메시지가 순서대로 쌓여 있어요. "오프셋 12345 메시지"를 찾으려고 파일을 처음부터 다 읽으면 너무 느리겠죠. 그래서 카프카는 인덱스 파일 을 따로 둬요.
.index 파일: 오프셋 → 파일 안 위치(바이트) 매핑
오프셋 0 → 파일 바이트 0
오프셋 100 → 파일 바이트 4096
오프셋 200 → 파일 바이트 8192
...
.timeindex 파일: 타임스탬프 → 오프셋 매핑
타임스탬프 1672531200000 → 오프셋 50
타임스탬프 1672531260000 → 오프셋 150
...
회사 비유로 — 두꺼운 자료집 맨 앞에 있는 색인. "주제 X는 350쪽" 이라고 적혀 있으니 책 전체를 뒤지지 않고 350쪽으로 점프할 수 있어요. .index는 오프셋 색인, .timeindex는 시간 색인.
log.index.interval.bytes=4096 설정이 기본값. 4KB마다 인덱스 항목을 하나 추가해요. 너무 촘촘하면 인덱스 파일이 커지고, 너무 듬성듬성하면 탐색이 느려져요. 4KB가 보통 무난한 균형점이에요.
메시지 보존 정책 — 오래된 데이터를 언제 지울까
카프카는 메시지를 영구 보관하지 않아요(원하면 가능하지만 보통은 일정 기간 후 삭제). 이 정책을 Log Retention 이라고 해요.
Delete 정책 — 시간·크기 기준 삭제 (기본)
log.retention.hours = 168 # 168시간 = 7일 (기본값)
log.retention.bytes = -1 # 파티션당 크기 제한 (-1 = 무제한, 기본값)
- 시간 기준 — 메시지가 7일 지나면 삭제.
- 크기 기준 — 파티션 크기가 N바이트를 넘으면 오래된 세그먼트부터 삭제.
토픽별로 다르게 설정도 가능해요.
kafka-configs.sh --bootstrap-server localhost:9092 \
--entity-type topics --entity-name my-topic \
--alter --add-config retention.ms=86400000 # 1일
Compact 정책 — 키 기준 최신값만 유지
같은 키의 메시지가 여러 번 들어오면 가장 최신 값만 남기고 나머지를 지우는 정책이에요. 시간 기준이 아니라 키 기준이에요.
원본 로그:
[0: K=user1, V=A]
[1: K=user2, V=B]
[2: K=user1, V=C]
[3: K=user3, V=D]
[4: K=user2, V=E]
[5: K=user1, V=F]
컴팩션 후 (K별 최신값만 남김):
[3: K=user3, V=D]
[4: K=user2, V=E]
[5: K=user1, V=F]
회사 비유로 — 직원 명부 게시판인데, 김철수가 부서를 다섯 번 옮기는 동안 매번 새 글이 올라왔어요. 컴팩션은 "각 직원당 가장 최근 부서 정보 한 줄만 남기자"예요.
여기서 시험 함정이 하나 있어요. Compact 정책은 "오래된 메시지 삭제"가 아니라 "같은 키의 옛날 값 삭제" 예요. 따라서 키가 다 다르면 컴팩션해도 아무것도 안 줄어들어요. 컴팩션은 현재 상태 스냅샷이 필요한 자리(예: 직원 정보·구독 상태·캐시)에 어울리고, 모든 이벤트 이력이 중요한 자리(예: 결제 로그·감사 로그)에는 안 어울려요.
Tombstone — 키를 완전히 지우는 마커
Compact 정책에서 어떤 키를 완전히 없애고 싶으면 — 그 키에 value가 null인 메시지 를 보내요. 이걸 Tombstone(묘비)라고 해요. 컴팩션 시 Tombstone이 뒤에 있으면 "이 키 삭제하자"는 신호로 작동해요. 일정 기간(delete.retention.ms, 기본 1일) 후 Tombstone 자체도 사라져요.
Delete + Compact 혼합
kafka-topics.sh --create --topic my-topic \
--config cleanup.policy=compact,delete
둘 다 적용하면 — 키별 최신값 유지(Compact) + 오래된 데이터 삭제(Delete). 둘의 장점을 동시에 가져갈 수 있어요.
페이지 캐시·Zero-Copy — Replication 위에서 카프카가 빠른 비결
카프카가 디스크 기반인데 메모리 기반 시스템만큼 빠른 이유 — 가장 큰 두 비결이 OS 페이지 캐시 활용과 Zero-Copy 전송이에요.
OS 페이지 캐시 활용
카프카는 데이터를 디스크에 쓸 때 자체 캐시를 두지 않고 OS의 페이지 캐시를 그대로 활용해요. 자바 힙 메모리에 따로 캐싱하지 않아요.
회사 비유로 — 카프카는 자기 사무실 안에 별도 서류함을 두지 않고, 회사 빌딩 공용 자료실(OS 페이지 캐시)을 그대로 써요. 빌딩이 이미 알아서 자주 쓰는 자료를 기억해 주니까, 카프카는 거기에 일을 맡기고 자기는 다른 일에 집중해요.
이게 왜 좋냐 — 자바 힙에 캐싱하면 GC(Garbage Collection) 오버헤드가 커지고, OS 캐시랑 중복돼요. OS 캐시를 그냥 쓰면 GC 부담 없고, 자주 읽는 데이터는 자동으로 메모리에 유지돼요.
Zero-Copy — 디스크 거치지 않고 바로 네트워크로
컨슈머가 메시지를 읽을 때 — 일반적으로는 다음 4단계를 거쳐요.
일반 방식 (4번 복사):
디스크 → 커널 버퍼 → 사용자 공간 메모리 → 소켓 버퍼 → 네트워크
카프카는 OS의 sendfile() 시스템 콜을 써서 디스크에서 곧장 네트워크 카드로 보내요.
Zero-Copy 방식 (2번 복사):
디스크 → 커널 버퍼 → 네트워크
회사 비유로 — 옛날 방식은 자료실 책을 일일이 사무실로 들고 와서 → 복사기로 복사한 다음 → 다시 우편실로 들고 가서 → 부치는 식이에요. Zero-Copy는 자료실에서 곧장 우편실로 자동 전송돼요. 사무실(사용자 공간)을 거치지 않아요.
여기서 시험 함정이 하나 있어요. Zero-Copy가 가능한 이유는 카프카가 메시지 내용을 안 본다는 점이에요. 카프카는 메시지를 "그냥 바이트 덩어리"로 취급하지, 안의 JSON 필드를 파싱하지 않아요. 만약 변환·필터링 같은 처리가 필요하면 사용자 공간으로 데이터를 가져와야 하니 Zero-Copy가 안 돼요. 그래서 카프카는 운반 메커니즘 자리에 충실하고, 처리는 Kafka Streams·컨슈머·외부 시스템에 맡기는 거예요. 이게 1편에서 강조한 "카프카는 운반 메커니즘이지 처리 시스템이 아니다"의 또 다른 얼굴.
배치·압축의 시너지
빠른 비결이 하나 더 있어요. 카프카는 메시지를 배치 단위로 처리·압축해요.
- 배치가 크면 압축 효율 좋아짐(중복 데이터 많이 제거)
- 배치가 크면 네트워크·디스크 시스템 콜 횟수 줄어듦
- 권장 조합 —
linger.ms=20,batch.size=32KB,compression.type=snappy또는lz4
3편(프로듀서) 단원에서 다룬 옵션이 여기서 다시 등장해요. 모든 부품이 결국 같은 "빠른 운반" 목표를 향해 맞물리는 거예요.
카프카 내부 토픽 — 카프카가 자기 자신에게 적는 자료
카프카는 자기 동작에 필요한 메타데이터까지 자기 자신의 토픽 에 적어요. 이걸 내부 토픽이라고 해요.
__consumer_offsets
컨슈머 그룹이 어디까지 읽었는지(오프셋)를 저장. 4편에서 다룬 그 토픽이에요. 기본 50개 파티션으로 자동 생성돼요.
__transaction_state
카프카 트랜잭션 상태를 저장. Exactly Once 처리에 쓰여요.
__cluster_metadata (KRaft 전용)
KRaft 모드에서 클러스터 메타데이터(토픽 정보·파티션 리더·브로커 상태 등)를 저장. Zookeeper의 자리를 대신해요.
여기서 시험 함정이 하나 있어요. 밑줄 두 개(__)로 시작하는 토픽은 카프카 내부용 예약 이름이에요. 사용자가 만든 토픽 이름을 __ 로 시작하면 충돌·오작동 위험이 있어서 안 됩니다. 보이는 게 신기해서 직접 읽어 보고 싶어도 — 일반적으로 직접 건드리지 않는 게 원칙이에요.
모니터링 핵심 지표 — 운영에서 보는 숫자
카프카 운영의 8할은 적절한 지표를 모니터링하는 거예요. 외울 만한 핵심 4개만 짚어 둡시다.
Under-Replicated Partitions (URP)
ISR 개수 < 복제 팩터 인 파티션 수
복제 팩터 3인데 ISR이 2인 파티션이 있으면 URP가 1. URP > 0이면 어딘가 브로커가 느려졌거나 죽은 거예요.
kafka-topics.sh --bootstrap-server localhost:9092 \
--describe --under-replicated-partitions
Under Min ISR Partitions
ISR 개수 < min.insync.replicas 인 파티션 수
이 상태에서는 acks=all 프로듀서가 쓰기 실패해요. URP보다 더 심각한 단계.
kafka-topics.sh --bootstrap-server localhost:9092 \
--describe --under-min-isr-partitions
Offline Partitions
ISR이 0개인 파티션 수 (리더 없음)
읽기·쓰기 모두 불가. 즉각 복구 필요.
Active Controller 수
0 → 클러스터 관리 불가 (심각)
1 → 정상
2+ → 스플릿 브레인 (즉각 조사)
여기서 시험 함정이 하나 있어요. URP > 0이라고 해서 즉시 데이터 손실은 아니에요. "지금 누가 복제를 따라잡지 못하고 있다"는 신호일 뿐, 리더는 살아 있고 데이터도 있어요. 다만 이 상태가 오래 지속되면 위험 한데 — 리더가 그 사이에 죽으면 살아 있는 ISR이 부족해 데이터 손실로 이어질 수 있거든요. 그래서 URP는 "지금 당장 문제는 아니지만 빨리 살펴봐야 할 신호"로 봐요.
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 카프카 5편의 핵심입니다. 실무에서나 면접에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
복제·ISR·리더십
- 복제(Replication) — 같은 파티션 데이터를 여러 브로커에 복사 보관. 한 대 죽어도 데이터 유지.
- Replication Factor — 복사본 개수. 운영 표준 3. 브로커 수보다 클 수 없음.
- Leader — 그 파티션 데이터를 책임지고 받는 브로커. 프로듀서·컨슈머는 기본 리더와 거래.
- Follower — 리더를 따라 데이터 백업하는 브로커. 평소엔 클라이언트와 직접 거래 안 함. pull 방식으로 리더에게서 가져감.
- ISR (In-Sync Replicas) — 최신 상태를 따라잡은 복제본 집합. 리더는 항상 ISR에 포함.
replica.lag.time.max.ms— ISR 탈퇴 기준. 기본 30초. 30초 동안 못 따라잡으면 빠짐, 따라잡으면 자동 재편입.- Log End Offset (LEO) — 각 복제본의 마지막 메시지 다음 오프셋.
- High Watermark (HW) — 모든 ISR 팔로워가 따라잡은 안전 지점. 컨슈머는 HW까지만 읽을 수 있음 — 일관성 보장.
min.insync.replicas— 쓰기를 허용하는 ISR 최소 개수. 보통 2 (복제 팩터 3 환경).- 운영 표준 조합 —
replication-factor=3+min.insync.replicas=2+acks=all. 1대 사고에도 무손실 운영. - Preferred Replica — 토픽 생성 시 정해 둔 1순위 리더.
auto.leader.rebalance.enable=true면 복구 후 자동 환원. - Unclean Leader Election — ISR이 비었을 때 ISR 밖 복제본을 강제 리더로. 기본값 false — 데이터 무결성 우선. 로그·메트릭처럼 손실 감수해도 가용성이 더 중요한 자리만 true.
Controller·KRaft
- Controller — 클러스터 행정 책임자. 리더 선출·장애 감지·메타데이터 전파 담당.
- Zookeeper → KRaft — KRaft에서는 Controller Quorum + Raft로 컨트롤러 페일오버가 밀리초 수준.
- Active Controller 수 — 항상 1. 0이면 클러스터 마비, 2+면 스플릿 브레인.
로그 세그먼트·정책
- 로그 세그먼트 — 파티션을 여러 파일로 나눠 저장. 파일명은 첫 오프셋 번호. 1GB 또는 7일마다 롤오버.
- 인덱스 파일 —
.index(오프셋→위치),.timeindex(시간→오프셋). 4KB마다 항목 추가. - Active Segment — 현재 쓰기 중인 세그먼트. 삭제·압축 대상 아님.
- Delete 정책 — 시간(기본 7일)·크기 기준으로 오래된 세그먼트 삭제.
- Compact 정책 — 같은 키의 최신값만 유지. 시간 기준 아님. 현재 상태 스냅샷용.
- Tombstone — value=null 메시지. Compact 정책에서 키 완전 삭제.
성능 비결·내부 토픽·운영 지표
- OS 페이지 캐시 활용 — 자바 힙 캐싱 안 함, GC 부담 없음.
- Zero-Copy (
sendfile) — 디스크 → 커널 버퍼 → 네트워크. 사용자 공간 안 거침. 카프카가 메시지 내용 안 보기 때문에 가능. - 카프카 빠른 비결 — 페이지 캐시 + Zero-Copy + 배치 + 압축의 합작.
- 내부 토픽 —
__consumer_offsets(오프셋),__transaction_state(트랜잭션),__cluster_metadata(KRaft 메타데이터). - URP — ISR < 복제 팩터인 파티션. 0 초과면 조사 필요(아직 손실은 아님).
- Under Min ISR — ISR < min.insync.replicas.
acks=all프로듀서 쓰기 실패 상태. - Offline Partition — ISR 0개. 읽기·쓰기 모두 불가. 즉각 복구.
다음 글(6편)에서는 카프카 보안과 운영 — SASL·SSL·ACL 같은 인증·인가, 멀티 클러스터 구성(MirrorMaker 2), 모니터링 도구 스택(Prometheus·Grafana·JMX), 운영 베스트 프랙티스를 풀어 갑니다. 5편의 Replication 내부 동작을 이해했으니, 그 위에 보안과 운영 레이어를 얹는 흐름이에요.
시리즈 다른 편
같은 시리즈의 다른 글들도 같은 친절 톤으로 묶어 정리되어 있어요.
- 1편 — 카프카 소개
- 2편 — 아키텍처
- 3편 — Producer
- 4편 — Consumer
- 5편 — 내부 동작 · 장애 복구 (현재 글)
- 6편 — 운영 · CLI · 보안
- 7편 — Connect · Streams · Schema Registry (완)