Producer와 Consumer의 동작 원리를 이해했다면, 다음으로 마주하는 질문은 이것이다. "Broker가 죽으면 어떻게 되는가?" 개발 환경에서는 단일 Broker로도 충분하지만, 프로덕션에서는 장애가 전제 조건이다. 서버는 반드시 죽고, 디스크는 반드시 고장 나며, 네트워크는 반드시 끊긴다.
Kafka가 Fortune 100 기업의 핵심 인프라로 자리 잡은 이유는 단순히 빠르기 때문만이 아니다. 장애 상황에서도 데이터를 잃지 않고 서비스를 지속할 수 있는 고가용성(High Availability) 메커니즘 때문이다. 이 글에서는 Kafka의 복제 구조를 깊이 들여다보고, 클러스터 메타데이터 관리의 진화(ZooKeeper에서 KRaft로), 그리고 실제 프로덕션 운영에 필요한 전략을 다룬다.
복제(Replication) 메커니즘
Leader와 Follower의 역할
Kafka의 복제는 파티션 단위로 이루어진다. 하나의 파티션에 대해 하나의 Leader 복제본과 0개 이상의 Follower 복제본이 존재하며, 이들은 서로 다른 Broker에 배치된다.
- Leader: 해당 파티션에 대한 모든 읽기/쓰기 요청을 처리한다. Producer가 메시지를 보내면 Leader가 수신하고, Consumer가 메시지를 읽을 때도 Leader에서 가져간다.
- Follower: Leader의 로그를 지속적으로 fetch하여 자신의 로그에 복제한다. 평상시에는 클라이언트 요청을 처리하지 않고, Leader 장애 시 새로운 Leader로 승격될 준비를 한다.
Broker 1 Broker 2 Broker 3
┌──────────┐ ┌──────────┐ ┌──────────┐
│ P0 Leader │ │ P0 Follower│ │ P0 Follower│
│ P1 Follower│ │ P1 Leader │ │ P1 Follower│
│ P2 Follower│ │ P2 Follower│ │ P2 Leader │
└──────────┘ └──────────┘ └──────────┘
각 파티션의 Leader가 서로 다른 Broker에 분산되어 있어야 부하가 고르게 분배된다. 이를 Preferred Leader 배치라고 하며, Kafka는 토픽 생성 시 자동으로 Leader를 분산 배치한다.
ISR(In-Sync Replicas) 심화
ISR(In-Sync Replicas) 은 Leader와 충분히 동기화된 복제본의 목록이다. "충분히 동기화"의 기준은 replica.lag.time.max.ms 설정으로 결정된다. Follower가 이 시간 이내에 Leader의 최신 메시지를 fetch하지 못하면, ISR에서 탈락한다.
ISR의 동작을 단계별로 살펴보자.
- Producer가 Leader에 메시지를 전송한다.
- Leader가 메시지를 자신의 로그에 기록한다.
- Follower들이 Leader에게 fetch 요청을 보내 새 메시지를 가져간다.
- 모든 ISR 복제본이 해당 메시지를 기록하면, Leader는 HW(High Watermark) 를 전진시킨다.
- Consumer는 HW 이전의 메시지만 읽을 수 있다.
여기서 High Watermark가 핵심이다. HW는 모든 ISR이 복제를 완료한 지점을 나타내는 오프셋이다. 아직 모든 ISR에 복제되지 않은 메시지는 Consumer에게 노출되지 않는다.
Leader 로그: [0] [1] [2] [3] [4] [5]
↑ HW ↑ LEO
Follower A: [0] [1] [2] [3] [4]
Follower B: [0] [1] [2] [3]
- LEO(Log End Offset): Leader가 기록한 마지막 오프셋
- HW(High Watermark): 모든 ISR이 복제 완료한 오프셋
min.insync.replicas 설정
acks=all을 사용하더라도, ISR에 Leader만 남아 있다면 실질적으로 acks=1과 다를 바 없다. min.insync.replicas 설정은 이 문제를 방지한다.
# 토픽 또는 브로커 레벨 설정
min.insync.replicas=2
Replication Factor가 3이고 min.insync.replicas=2인 경우:
- ISR이 3개(Leader + Follower 2): 정상 동작
- Follower 1개가 ISR에서 탈락하여 ISR이 2개: 정상 동작
- Follower 1개가 더 탈락하여 ISR이 1개(Leader만):
acks=all인 Producer는NotEnoughReplicasException을 받는다
즉, 데이터 안전성이 보장되지 않는 상황에서는 차라리 쓰기를 거부하는 전략이다. 프로덕션에서 가장 널리 사용되는 조합은 다음과 같다.
| 설정 | 값 |
|---|---|
| replication.factor | 3 |
| min.insync.replicas | 2 |
| acks (Producer) | all |
이 조합이면 Broker 1대가 완전히 손실되어도 데이터 유실 없이 서비스를 지속할 수 있다.
Unclean Leader Election
모든 ISR 복제본이 죽으면 어떻게 될까? ISR에 속하지 않은, 즉 동기화가 뒤처진 Follower를 Leader로 승격시킬 것인지의 문제가 Unclean Leader Election이다.
# 기본값은 false (Kafka 0.11.0 이후)
unclean.leader.election.enable=false
false(기본값): ISR 복제본이 복구될 때까지 해당 파티션은 사용 불가가 된다. 데이터 정합성을 우선시하는 선택이다.true: 동기화가 뒤처진 Follower라도 Leader로 승격시켜 가용성을 유지한다. 대신 복제되지 않은 메시지는 영구적으로 유실된다.
이것은 전형적인 가용성(Availability) vs 일관성(Consistency) 트레이드오프다. 금융 거래처럼 유실이 절대 허용되지 않는 경우에는 반드시 false로 유지해야 한다.
컨트롤러와 KRaft
Controller Broker의 역할
Kafka 클러스터에서 Broker 중 하나는 Controller 역할을 맡는다. Controller는 클러스터 전체의 메타데이터 관리와 조율을 담당한다.
- Leader 선출: Broker 장애 시 영향받는 파티션의 새로운 Leader를 결정한다
- ISR 변경 관리: 복제본이 ISR에 합류하거나 탈락할 때 메타데이터를 갱신한다
- 토픽/파티션 관리: 토픽 생성, 삭제, 파티션 추가 등의 변경 사항을 클러스터에 전파한다
- Broker 등록/해제: 새 Broker가 클러스터에 합류하거나 기존 Broker가 이탈할 때 처리한다
ZooKeeper 기반 아키텍처의 문제점
Kafka는 오랫동안 클러스터 메타데이터 관리를 Apache ZooKeeper에 의존했다. 그러나 ZooKeeper 의존성은 여러 문제를 야기했다.
운영 복잡도 증가: Kafka 클러스터를 운영하려면 ZooKeeper 앙상블(보통 3~5대)도 함께 관리해야 한다. 두 개의 분산 시스템을 동시에 모니터링하고 업그레이드하는 부담이 상당하다.
확장성 한계: 파티션 수가 수십만 개에 달하는 대규모 클러스터에서는 Controller가 ZooKeeper로부터 메타데이터를 읽고 전파하는 데 상당한 시간이 소요되었다. Controller 장애 복구 시 전체 메타데이터를 읽어오는 과정이 수 분까지 걸릴 수 있었다.
Controller 병목: 단일 Controller가 모든 메타데이터 변경을 처리해야 하므로, 대규모 클러스터에서 병목이 발생했다.
KRaft 모드: Kafka의 자체 합의 프로토콜
KRaft(Kafka Raft) 는 ZooKeeper 의존성을 완전히 제거하고, Kafka 자체적으로 메타데이터를 관리하는 새로운 아키텍처이다. Kafka 3.3에서 프로덕션 준비(Production Ready)로 선언되었고, Kafka 4.0에서 ZooKeeper 지원이 완전히 제거되었다.
KRaft의 핵심 아이디어는 메타데이터를 Kafka 토픽 자체에 저장하는 것이다.
ZooKeeper 모드:
Broker ──── ZooKeeper 앙상블
│
Controller ──┘
KRaft 모드:
Controller (Raft 합의) ── 메타데이터 로그 (내부 토픽)
│
Broker들이 메타데이터 로그를 구독
KRaft 모드에서는 Controller Quorum이라는 별도의 Controller 노드 그룹이 존재한다. 이들은 Raft 합의 알고리즘을 사용하여 메타데이터 변경에 대한 합의를 이룬다. 모든 메타데이터 변경은 __cluster_metadata라는 내부 토픽에 이벤트로 기록되며, 일반 Broker들은 이 토픽을 구독하여 메타데이터를 동기화한다.
KRaft가 가져온 주요 개선점:
- 운영 단순화: ZooKeeper를 별도로 관리할 필요가 없다
- 빠른 Controller 장애 복구: 새 Controller가 메타데이터 로그를 이미 가지고 있으므로 수 초 내에 복구된다
- 확장성 향상: 수백만 개의 파티션을 지원할 수 있다
- 단일 보안 모델: Kafka의 인증/인가 메커니즘만 관리하면 된다
클러스터 운영 전략
파티션 수 결정 기준
파티션 수는 토픽 생성 후 늘릴 수는 있지만 줄일 수는 없다. 신중하게 결정해야 하는 이유이다.
목표 처리량 기반 산정: 단일 파티션에서 Producer가 달성할 수 있는 처리량을 P, Consumer가 달성할 수 있는 처리량을 C라고 하면, 목표 처리량 T를 달성하기 위한 최소 파티션 수는 max(T/P, T/C)이다.
Consumer 병렬도: 파티션 수는 Consumer Group 내 최대 병렬 Consumer 수의 상한이다. Consumer를 10개까지 확장할 계획이라면, 파티션도 최소 10개가 필요하다.
과도한 파티션의 부작용: 파티션이 많을수록 좋은 것은 아니다. Broker당 수천 개 이상의 파티션은 리밸런싱 시간 증가, 메모리 사용량 증가 등의 문제를 야기할 수 있다. 일반적인 가이드라인으로 Broker당 4,000개 이하, 클러스터 전체 200,000개 이하가 안정적인 범위다(KRaft 모드 기준).
Retention 정책
Kafka는 메시지를 영구 보존하지 않는다. Retention 정책에 따라 오래된 메시지를 삭제하거나 압축한다.
시간 기반 보존(Time-based Retention)
# 기본값: 7일 (168시간)
log.retention.hours=168
크기 기반 보존(Size-based Retention)
# 파티션당 최대 크기. -1이면 무제한 (기본값)
log.retention.bytes=1073741824 # 1GB
로그 압축(Log Compaction)
log.cleanup.policy=compact
로그 압축은 각 키의 마지막 값만 유지한다. 사용자 프로필 업데이트처럼 최신 상태만 중요한 경우에 적합하다.
압축 전: [K1:v1] [K2:v1] [K1:v2] [K3:v1] [K2:v2] [K1:v3]
압축 후: [K3:v1] [K2:v2] [K1:v3]
브로커 증설과 파티션 재분배
클러스터에 새로운 Broker를 추가하는 것 자체는 간단하다. 새 Broker를 기동하면 자동으로 클러스터에 합류한다. 하지만 기존 파티션이 자동으로 새 Broker에 분배되지는 않는다. 기존 파티션을 새 Broker에 재분배하려면 파티션 재할당(Partition Reassignment) 을 수동으로 실행해야 한다.
# 재할당 계획 생성
kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
--topics-to-move-json-file topics.json \
--broker-list "1,2,3,4" \
--generate
# 재할당 실행
kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
--reassignment-json-file reassignment.json \
--execute
파티션 재할당은 대량의 데이터 복제를 수반하므로, 프로덕션 트래픽이 적은 시간대에 수행하는 것이 좋다.
모니터링과 장애 대응
핵심 모니터링 메트릭
Under-Replicated Partitions (URP): ISR 수가 Replication Factor보다 적은 파티션의 수. 이 값이 0이 아니면 복제가 지연되고 있다는 신호다.
Consumer Lag: Consumer Group이 토픽의 최신 메시지로부터 얼마나 뒤처져 있는지를 나타낸다. Lag이 지속적으로 증가한다면, Consumer의 처리 속도가 Producer를 따라가지 못한다는 의미이다.
# Consumer Lag 확인
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--group my-consumer-group \
--describe
Active Controller Count: 정상 상태에서는 정확히 1이어야 한다. 0이면 Controller가 없는 비정상 상태이고, 2 이상이면 Split Brain이다.
Request Latency: 각 요청 유형별 지연 시간. 급격한 증가는 리소스 고갈이나 네트워크 문제를 나타낸다.
주요 장애 시나리오
Broker 1대 다운: Replication Factor 3, min.insync.replicas=2 환경에서는 자동으로 Leader 선출이 이루어지며 서비스는 정상 동작한다. 다운된 Broker가 복구되면 자동으로 Follower로 합류하여 데이터를 동기화한다.
Consumer Lag 급증: Consumer 자체의 처리 지연, 외부 시스템 응답 지연, GC 문제 등이 원인일 수 있다. Consumer 수를 늘리거나 처리 로직을 최적화해야 한다.
디스크 풀(Disk Full): Broker의 디스크가 가득 차면 쓰기를 거부한다. Retention 기간을 단축하거나 불필요한 토픽을 정리하고, 장기적으로 디스크 증설이나 Broker 추가를 검토한다.
운영 도구
| 도구 | 용도 |
|---|---|
kafka-topics.sh | 토픽 생성, 목록 조회, 설정 변경, 삭제 |
kafka-consumer-groups.sh | Consumer Group 목록, Lag 확인, 오프셋 리셋 |
kafka-configs.sh | Broker/토픽의 동적 설정 변경 |
kafka-reassign-partitions.sh | 파티션 재할당 |
kafka-metadata.sh | KRaft 메타데이터 로그 조회 |
프로덕션 환경에서는 Prometheus + Grafana 조합으로 메트릭을 시각화하거나, Kafka UI, Burrow 같은 전용 도구를 활용하는 것이 일반적이다.
정리
- 복제(Replication): Leader-Follower 구조와 ISR 메커니즘으로 데이터 내구성을 보장한다.
replication.factor=3,min.insync.replicas=2,acks=all조합이 프로덕션의 기본이다 - KRaft: ZooKeeper 의존성을 제거하고 Kafka 자체의 Raft 합의 프로토콜로 메타데이터를 관리한다. 운영 단순화, 빠른 장애 복구, 확장성 향상을 가져왔다
- 운영 전략: 파티션 수는 처리량과 Consumer 병렬도를 기반으로 산정하고, Retention 정책은 데이터 특성에 맞게 선택한다
- 모니터링: Under-Replicated Partitions, Consumer Lag, Active Controller Count가 가장 중요한 지표이다
Kafka는 설정 하나로 동작이 크게 달라지는 시스템이다. 각 설정의 의미와 트레이드오프를 정확히 이해하고, 자신의 시스템 요구 사항에 맞게 조율하는 것이 결국 Kafka 운영의 본질이다.