✍️ 필사 모드: Apache Kafka 내부 구조 완전 가이드 2025: Partitions, Replication, ISR, KRaft, Tiered Storage
한국어TL;DR
- Kafka = 분산 commit log: 단순한 추상화로 페타바이트 처리
- 파티션이 모든 것: 병렬성, 확장성, 순서 보장의 단위
- ISR이 데이터 안전성의 핵심: min.insync.replicas로 손실 방지
- KRaft가 ZooKeeper 대체: Kafka 3.3+ 안정. 운영 단순화
- Exactly-Once 가능: 멱등 producer + 트랜잭션 + read_committed
1. Kafka의 본질
1.1 한 줄 정의
Kafka = 분산 commit log + 게시-구독 메시징.
핵심 추상화: 로그.
- Append-only
- 순차 읽기/쓰기
- 영속화
1.2 왜 로그가 강력한가?
전통적 메시지 큐 (RabbitMQ):
- 메시지를 큐에 넣음
- 컨슈머가 읽으면 큐에서 제거
- 재처리 불가능
Kafka:
- 메시지를 로그에 append
- 컨슈머가 offset을 추적
- 언제든 재처리 가능
- 여러 컨슈머가 같은 데이터 읽음
1.3 Kafka의 주요 사용 사례
| 사용 사례 | 예시 |
|---|---|
| 메시지 큐 | 이메일, SMS 발송 작업 |
| 이벤트 소싱 | 도메인 이벤트 영구 저장 |
| 로그 집계 | 여러 서버의 로그를 중앙으로 |
| 메트릭 수집 | 시계열 데이터 |
| 스트림 처리 | 실시간 분석 |
| CDC | DB 변경 사항 전파 |
| 마이크로서비스 통신 | 비동기 이벤트 |
1.4 Kafka의 핵심 통계 (LinkedIn)
- 수조 메시지/일 처리
- 수십 페타바이트 저장
- 수천 노드 클러스터
- 밀리초 latency
2. 토픽과 파티션
2.1 토픽
토픽 = 메시지 스트림의 이름. 사실상 폴더처럼.
orders → 주문 이벤트
users → 사용자 이벤트
metrics → 메트릭
logs.apache → Apache 로그
2.2 파티션
파티션 = 토픽의 분할 단위.
orders 토픽 (3 파티션):
├─ partition 0: msg1 → msg2 → msg3 → ...
├─ partition 1: msg4 → msg5 → msg6 → ...
└─ partition 2: msg7 → msg8 → msg9 → ...
각 파티션은:
- 정렬된 로그 (offset 순서)
- 불변 (append-only)
- 영속화 (디스크에 저장)
2.3 왜 파티션이 중요한가?
1. 병렬성:
- 컨슈머 1: partition 0 처리
- 컨슈머 2: partition 1 처리
- 컨슈머 3: partition 2 처리 → 3배 처리량
2. 순서 보장 (파티션 내):
- 같은 파티션 안의 메시지는 순서 보장
- 다른 파티션 간에는 순서 X
3. 확장성:
- 더 많은 파티션 = 더 많은 컨슈머 = 더 많은 처리량
- 단, 파티션 수는 한 번 만들면 거의 안 변함
2.4 파티션 수 결정
너무 적음: 처리량 부족, 확장 못 함 너무 많음: 메모리 사용, ZK/KRaft 부담, latency 증가
가이드라인:
- 시작:
목표 처리량 / 단일 파티션 처리량 - 단일 파티션: ~50 MB/s
- 일반: 6-30 파티션
- 큰 토픽: 100+
LinkedIn 권장: 브로커당 4,000 파티션 이하, 클러스터당 200,000 이하.
2.5 파티션 키와 라우팅
producer.send(
topic='orders',
key=str(user_id).encode(), # 같은 user_id = 같은 파티션
value=json.dumps(order).encode()
)
라우팅:
partition = hash(key) % num_partitions
왜 키?:
- 같은 user의 모든 이벤트가 같은 파티션 → 순서 보장
- 같은 entity의 데이터를 같이 처리
키 없으면: round-robin 분산.
3. 세그먼트 파일
3.1 파티션 = 세그먼트들의 모음
partition_0/
├─ 00000000000000000000.log ← 메시지 데이터
├─ 00000000000000000000.index ← offset → byte 매핑
├─ 00000000000000000000.timeindex ← timestamp → offset
├─ 00000000000001000000.log
├─ 00000000000001000000.index
└─ 00000000000001000000.timeindex
각 세그먼트는 보통 1 GB.
3.2 왜 세그먼트로 나누나?
1. 효율적인 삭제:
- 오래된 데이터 삭제 = 옛 세그먼트 파일 삭제
- 단일 파일이면 매번 일부 삭제 어려움
2. 효율적인 압축:
- 세그먼트 단위 압축 (gzip, snappy, lz4)
- 옛 세그먼트는 더 압축 (cold)
3. 빠른 검색:
- index 파일로 offset → byte 위치 빠르게
- O(log N)
3.3 Log Compaction
특정 토픽은 최신 값만 유지 (옛 값 삭제):
# 일반 토픽: 모든 메시지 유지
[k1=v1, k2=v2, k1=v1', k3=v3, k1=v1'']
# Compacted: 같은 키의 최신 값만
[k2=v2, k3=v3, k1=v1'']
사용: KTable, 사용자 프로필 같은 "최신 상태" 유지.
kafka-topics.sh --create --topic user-profiles \
--partitions 10 \
--config cleanup.policy=compact
4. Replication
4.1 왜 복제?
단일 브로커 = 단일 장애점. 브로커 죽으면 데이터 손실.
복제: 같은 파티션을 여러 브로커에.
partition 0:
├─ Broker 1: Leader ← 모든 읽기/쓰기
├─ Broker 2: Follower ← 동기화
└─ Broker 3: Follower ← 동기화
4.2 Leader-Follower 모델
Leader: 모든 읽기/쓰기 처리. Follower: Leader의 복제본. 백업 + failover.
왜 follower에서 못 읽나? (Kafka 2.4 이전)
- 단순함, 일관성 보장
- 2.4+:
KIP-392로 follower fetch 가능
4.3 ISR (In-Sync Replica)
ISR = Leader와 동기화된 follower 집합.
파티션의 ISR: [broker-1, broker-2, broker-3]
(leader) (follower) (follower)
만약 broker-3이 뒤처지면:
ISR: [broker-1, broker-2] ← broker-3 제외
replica.lag.time.max.ms (기본 30초): 이 시간 안에 따라잡지 못하면 ISR에서 제거.
4.4 acks 설정
Producer가 메시지 보낼 때:
producer = KafkaProducer(
bootstrap_servers=['kafka:9092'],
acks='all' # 또는 0, 1
)
acks=0: Leader 응답 대기 X. 가장 빠름, 가장 위험. acks=1: Leader가 받으면 OK. Follower 동기화 대기 X. acks=all (-1): 모든 ISR이 동기화된 후 OK. 가장 느림, 가장 안전.
4.5 min.insync.replicas
min.insync.replicas = 2
acks = all
→ "최소 2개의 ISR이 동기화되어야 commit".
시나리오:
- ISR 3개 → 정상
- ISR 2개 → 정상
- ISR 1개 → producer가 거부됨 (writes block)
효과: 데이터 손실 위험을 사용자에게 알림.
4.6 데이터 손실 시나리오
시나리오 A: acks=1
- Producer → Leader: OK
- Leader가 follower에 복제 전 Leader 죽음
- 메시지 손실
시나리오 B: acks=all, min.insync.replicas=1
- ISR 1개만 남음
- Producer → Leader: OK (혼자 ack)
- Leader 죽음
- 메시지 손실
시나리오 C: acks=all, min.insync.replicas=2
- ISR 1개 → producer 거부
- 데이터 손실 없음
4.7 unclean.leader.election
ISR이 모두 죽었을 때 어떻게?
unclean.leader.election.enable=false (권장):
- ISR 부활 대기
- 가용성 X, 일관성 O
unclean.leader.election.enable=true:
- 비-ISR follower를 leader로 승격
- 가용성 O, 데이터 손실 가능
5. KRaft — ZooKeeper의 종말
5.1 ZooKeeper의 역할 (이전)
Kafka는 메타데이터 관리에 ZooKeeper 사용:
- 브로커 등록
- 토픽/파티션 메타데이터
- ACL
- Leader election
문제:
- 별도 클러스터 운영
- ZK 자체가 SPOF
- ZK 한계 (200K 파티션)
- 운영 복잡도
5.2 KRaft (Kafka Raft)
Kafka 2.8 (2021): KRaft 미리보기. Kafka 3.3 (2022): KRaft GA. Kafka 3.5+ (2023): 새 클러스터 권장. Kafka 4.0+ (2025 예정): ZooKeeper 제거.
KRaft: Kafka가 자체 Raft 합의로 메타데이터 관리.
5.3 아키텍처 비교
ZooKeeper 기반:
[Kafka Cluster] [ZooKeeper Ensemble]
↓ ↑
[Brokers] ───────→ [ZK Nodes]
↑
[Controller (one)]
KRaft 기반:
[Kafka Cluster]
↓
[Brokers]
↑
[Controller Quorum] ← Raft 합의
5.4 KRaft의 장점
1. 단순화:
- ZK 클러스터 운영 불필요
- 단일 시스템
2. 확장성:
- 백만+ 파티션 지원
- 더 빠른 메타데이터 작업
3. 빠른 복구:
- ZK는 메타데이터 로드 시 시간 소요
- KRaft는 메모리에 항상 최신
4. 단순한 보안:
- ZK 보안 구성 불필요
5.5 마이그레이션
전략: ZK → KRaft 무중단 마이그레이션 (KIP-866).
# 1. KRaft mode로 새 controller 추가 (혼합 모드)
# 2. 메타데이터 동기화
# 3. ZK 연결 제거
# 4. ZK 클러스터 폐기
6. Producer 깊이
6.1 Producer 흐름
producer = KafkaProducer(
bootstrap_servers=['kafka:9092'],
acks='all',
compression_type='lz4',
batch_size=16384,
linger_ms=10,
max_in_flight_requests_per_connection=5
)
producer.send('topic', key=b'key', value=b'value')
producer.flush()
내부 동작:
- Serializer: key, value를 bytes로
- Partitioner: 어느 파티션?
- Accumulator: 메시지를 배치로 모음
- Sender 스레드: 배치를 broker로 전송
6.2 배치와 압축
배치 (batch.size):
- 작은 메시지를 모아 한 번에 전송
- 처리량 ↑, latency ↑
linger.ms:
- 배치를 채우기 위해 대기하는 시간
linger.ms=0: 즉시 전송linger.ms=10: 10ms 대기 (배치 효율 ↑)
압축 (compression.type):
gzip: 압축률 높음, 느림snappy: 균형lz4: 빠름zstd: 최고 압축 + 빠름 (Kafka 2.1+)
6.3 Idempotent Producer
문제: Network retry → 메시지 중복.
해결: Idempotent producer.
producer = KafkaProducer(
enable_idempotence=True # 자동으로 acks=all
)
작동:
- 각 producer에 PID (Producer ID) 할당
- 메시지마다 sequence number
- Broker가 중복 sequence 거부
결과: 같은 produce 호출의 retry는 한 번만 처리.
6.4 Transactional Producer
여러 파티션에 원자적 쓰기:
producer = KafkaProducer(
transactional_id='my-app',
enable_idempotence=True
)
producer.init_transactions()
try:
producer.begin_transaction()
producer.send('orders', value=order_data)
producer.send('inventory', value=inventory_data)
producer.commit_transaction()
except Exception:
producer.abort_transaction()
효과: orders와 inventory에 모두 쓰거나, 모두 실패.
7. Consumer 깊이
7.1 Consumer Group
consumer = KafkaConsumer(
'orders',
group_id='order-processors',
bootstrap_servers=['kafka:9092'],
auto_offset_reset='earliest'
)
for message in consumer:
print(message.value)
Consumer Group: 같은 group_id를 가진 컨슈머들의 그룹.
규칙:
- 한 파티션은 한 그룹의 한 컨슈머만 처리
- 컨슈머 N개 + 파티션 M개 → min(N, M) 컨슈머 활성
파티션 6개 + 컨슈머 6개 → 1:1 매핑
파티션 6개 + 컨슈머 3개 → 각 컨슈머 2 파티션
파티션 6개 + 컨슈머 8개 → 6 컨슈머 활성, 2 idle
7.2 Offset 관리
Offset = 컨슈머가 어디까지 읽었는지.
파티션 0: msg1(offset=0), msg2(offset=1), msg3(offset=2), ...
↑
current
저장: __consumer_offsets 토픽에 저장.
Auto commit:
enable_auto_commit=True # 5초마다 자동
Manual commit:
for message in consumer:
process(message)
consumer.commit() # 처리 후 commit
7.3 Rebalance
컨슈머 추가/제거 시 파티션 재할당.
문제:
- Stop the world (모든 컨슈머 일시 중지)
- Latency spike
Cooperative Rebalance (Kafka 2.4+):
- 점진적 재할당
- 영향 받는 파티션만 일시 중지
consumer = KafkaConsumer(
partition_assignment_strategy=[CooperativeStickyAssignor]
)
7.4 Heartbeat
컨슈머가 살아있다는 신호:
session.timeout.ms = 45000 # 45초
heartbeat.interval.ms = 3000 # 3초
max.poll.interval.ms = 300000 # 5분
- session.timeout: 이 시간 내 heartbeat 없으면 dead
- max.poll.interval: poll() 사이 최대 시간. 초과 시 그룹에서 제거
흔한 함정: process(message)가 5분 이상 걸리면 → 그룹에서 제거 → rebalance.
7.5 Read Isolation
consumer = KafkaConsumer(
isolation_level='read_committed'
)
read_uncommitted (기본): 모든 메시지 읽음 (트랜잭션 진행 중도). read_committed: commit된 트랜잭션만 읽음 (Exactly-once에 필수).
8. Exactly-Once Semantics
8.1 3가지 보장 수준
At-most-once: 메시지가 0회 또는 1회 처리. 손실 가능. At-least-once: 메시지가 1회 이상 처리. 중복 가능. Exactly-once: 정확히 1회 처리. 이상.
8.2 Exactly-Once는 불가능?
오랫동안 "분산 시스템에서 exactly-once는 불가능"이라고 여겨졌습니다.
Kafka의 답 (2017):
- Idempotent producer: producer 측 중복 방지
- Transactional API: 여러 파티션 원자적 쓰기
- read_committed: 컨슈머가 commit된 것만 읽음
조합 = Exactly-Once Semantics (EOS).
8.3 EOS 구현
# Producer
producer = KafkaProducer(
enable_idempotence=True,
transactional_id='my-app'
)
# Consumer
consumer = KafkaConsumer(
'input-topic',
group_id='my-group',
isolation_level='read_committed',
enable_auto_commit=False
)
producer.init_transactions()
while True:
records = consumer.poll(1000)
for tp, messages in records.items():
producer.begin_transaction()
for msg in messages:
result = process(msg.value)
producer.send('output-topic', value=result)
# offset도 트랜잭션의 일부
producer.send_offsets_to_transaction(
consumer.position(),
consumer.group_id
)
producer.commit_transaction()
핵심: 처리 + offset commit이 같은 트랜잭션 → 원자성.
8.4 EOS의 한계
- Kafka 안에서만: 외부 시스템(DB, REST API)은 X
- 성능 저하: ~5-10%
- 복잡도: 트랜잭션 관리
대안: At-least-once + 멱등 처리.
9. Tiered Storage (Kafka 3.6+)
9.1 문제
Kafka 데이터 = 디스크 비쌈.
- 30일 보관 = 큰 디스크
- 한 번 쓰고 거의 안 읽음
9.2 Tiered Storage
핫 데이터 = 로컬 디스크 콜드 데이터 = 객체 스토리지 (S3, GCS)
[브로커 디스크: 7일]
↓ 자동 이동
[S3: 영구]
효과:
- 저장 비용 90% 절감
- 거의 무제한 보관
- 로컬 디스크 작게 가능
Kafka 3.6 (2023): 미리보기. Kafka 3.7+: 안정.
Confluent Cloud: 이미 사용.
10. 운영 베스트 프랙티스
10.1 클러스터 사이징
브로커 수: 최소 3개 (replication factor 3).
파티션 수:
- 시작: 6-10
- 처리량 증가 시 확대 (추가만 가능, 줄이기 어려움)
디스크: SSD 권장, RAID 10.
메모리: Page cache 활용 → JVM heap 6-8 GB만, 나머지는 OS.
10.2 토픽 설정
kafka-topics.sh --create \
--topic orders \
--partitions 12 \
--replication-factor 3 \
--config min.insync.replicas=2 \
--config retention.ms=604800000 \
--config compression.type=lz4
핵심:
replication-factor=3: 데이터 안전성min.insync.replicas=2: 데이터 손실 방지compression.type=lz4: 네트워크/디스크 절약
10.3 모니터링
핵심 메트릭:
- Under-Replicated Partitions: > 0이면 문제
- Leader Imbalance: 한 브로커에 leader 집중
- Consumer Lag: 컨슈머가 뒤처짐
- Producer Send Rate
- Network/Disk I/O
도구: Prometheus + Grafana, Cruise Control, Confluent Control Center.
10.4 흔한 함정
1. 너무 많은 파티션:
- 메모리 폭증
- Leader election 느림
- Rebalance 느림
2. acks=1 + 데이터 손실:
- "성능 위해 acks=1" → 손실 위험
3. unclean.leader.election=true:
- "가용성 우선" → 데이터 손실
4. 모니터링 부족:
- ISR shrink를 못 봄
- Disk full
- Consumer lag 급증
퀴즈
1. Kafka 파티션의 핵심 역할은?
답: (1) 병렬성 — 파티션 수만큼 컨슈머가 동시에 처리 → 처리량 증가, (2) 순서 보장 — 같은 파티션 안에서는 순서 보장 (다른 파티션 간에는 X), (3) 확장성 — 더 많은 브로커에 분산, (4) 로드 분산 — 키 기반 라우팅으로 균등 분배. 파티션 키 선택이 중요: 같은 user_id면 같은 파티션 → 한 사용자의 이벤트 순서 보장. 파티션 수는 한 번 만들면 거의 변경 어려우므로 신중히.
2. ISR과 min.insync.replicas의 역할은?
답: ISR (In-Sync Replica): Leader와 동기화된 follower 집합. 뒤처지면 자동 제거 (replica.lag.time.max.ms 30초). min.insync.replicas: producer가 acks=all로 보낼 때 최소 동기화된 replica 수. 예: replication=3, min.insync=2면 ISR이 1개로 줄면 producer 거부 → 데이터 손실 방지. acks=all + min.insync.replicas=2 + replication=3 = 데이터 안전성의 황금 조합.
3. KRaft가 ZooKeeper를 대체한 이유는?
답: ZK의 문제: (1) 별도 클러스터 운영 부담, (2) 백만 파티션 한계, (3) Leader election 느림, (4) 운영 복잡도. KRaft (Kafka Raft): Kafka가 자체 Raft 합의로 메타데이터 관리. 장점: 단일 시스템, 백만+ 파티션 지원, 빠른 복구, 단순한 운영. Kafka 3.3+ GA, Kafka 4.0에서 ZooKeeper 완전 제거. 새 클러스터는 KRaft 권장.
4. Exactly-Once Semantics를 어떻게 구현하나요?
답: 3가지 메커니즘 조합: (1) Idempotent Producer — 각 producer에 PID + sequence number, broker가 중복 거부, (2) Transactional API — 여러 파티션에 원자적 쓰기 (begin_transaction, commit_transaction), (3) read_committed — 컨슈머가 commit된 트랜잭션만 읽음. 핵심: 처리 + offset commit을 같은 트랜잭션 안에 → 원자성. 한계: Kafka 내부에서만, 외부 시스템(DB)은 X. 5-10% 성능 저하.
5. Consumer Rebalance를 최소화하려면?
답: (1) Cooperative Rebalance — Kafka 2.4+의 CooperativeStickyAssignor 사용. 점진적 재할당으로 stop-the-world 회피, (2) Heartbeat 적절히 — session.timeout.ms, heartbeat.interval.ms, (3) max.poll.interval.ms 충분히 — 처리 시간보다 길게, 너무 길면 죽은 컨슈머 감지 늦음, (4) 컨슈머 안정성 — 잦은 재시작 X, (5) group.instance.id 사용 — Static membership으로 짧은 disconnect 시 rebalance 안 함.
참고 자료
- Apache Kafka Documentation
- Kafka: The Definitive Guide — Confluent
- KRaft (KIP-500)
- Confluent Kafka Internals
- Kafka Improvement Proposals
- Designing Event-Driven Systems — Ben Stopford
- Kafka Streams in Action
- Strimzi — Kubernetes Kafka operator
- Cruise Control — LinkedIn Kafka operations
- Tiered Storage (KIP-405)
현재 단락 (1/407)
- **Kafka = 분산 commit log**: 단순한 추상화로 페타바이트 처리