Skip to content
Published on

Apache Kafka 내부 구조 완전 가이드 2025: Partitions, Replication, ISR, KRaft, Tiered Storage

Authors

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 발송 작업
이벤트 소싱도메인 이벤트 영구 저장
로그 집계여러 서버의 로그를 중앙으로
메트릭 수집시계열 데이터
스트림 처리실시간 분석
CDCDB 변경 사항 전파
마이크로서비스 통신비동기 이벤트

1.4 Kafka의 핵심 통계 (LinkedIn)

  • 수조 메시지/일 처리
  • 수십 페타바이트 저장
  • 수천 노드 클러스터
  • 밀리초 latency

2. 토픽과 파티션

2.1 토픽

토픽 = 메시지 스트림의 이름. 사실상 폴더처럼.

orders          → 주문 이벤트
users           → 사용자 이벤트
metrics         → 메트릭
logs.apacheApache 로그

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

  1. Producer → Leader: OK
  2. Leader가 follower에 복제 전 Leader 죽음
  3. 메시지 손실

시나리오 B: acks=all, min.insync.replicas=1

  1. ISR 1개만 남음
  2. Producer → Leader: OK (혼자 ack)
  3. Leader 죽음
  4. 메시지 손실

시나리오 C: acks=all, min.insync.replicas=2

  1. ISR 1개 → producer 거부
  2. 데이터 손실 없음

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()

내부 동작:

  1. Serializer: key, value를 bytes로
  2. Partitioner: 어느 파티션?
  3. Accumulator: 메시지를 배치로 모음
  4. 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        # 45heartbeat.interval.ms = 3000      # 3max.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 안 함.


참고 자료