이전 글에서 Kafka의 기본 구성 요소인 Topic, Partition, Broker, Producer, Consumer를 살펴보았다. 기본 개념만으로도 Kafka를 사용할 수 있지만, 프로덕션 환경에서는 훨씬 더 많은 것을 고려해야 한다. 메시지가 반드시 전달되는가? 중복은 없는가? Consumer가 죽으면 어떻게 되는가?
이 글에서는 Producer의 전송 보장 수준과 파티셔닝 전략, Consumer의 Offset 관리와 Rebalancing 메커니즘을 심층적으로 다룬다.
Producer 심화
acks 설정: 전송 보장 수준
Producer가 메시지를 보낸 후, Broker로부터 어떤 수준의 확인(acknowledgement) 을 받을지를 결정하는 설정이다. 이 하나의 설정이 처리량(throughput) 과 내구성(durability) 사이의 트레이드오프를 결정한다.
acks=0: Producer는 Broker의 응답을 기다리지 않는다. 메시지를 보내고 즉시 다음 메시지를 전송한다. 가장 빠르지만, Broker가 메시지를 받지 못해도 Producer는 알 수 없다. 메시지 유실 가능성이 있다.
acks=1: Leader Broker가 메시지를 자신의 로그에 기록한 후 응답한다. Leader가 확인했으므로 대부분의 경우 안전하지만, Leader가 Follower에 복제하기 전에 죽으면 메시지를 잃을 수 있다.
acks=all (또는 acks=-1): Leader가 모든 ISR(In-Sync Replicas) 에 복제가 완료된 후에야 응답한다. 가장 느리지만 가장 안전하다.
# Producer 설정
acks=all
| acks | 처리량 | 내구성 | 메시지 유실 가능성 |
|---|---|---|---|
| 0 | 최고 | 없음 | 있음 |
| 1 | 중간 | Leader 기록 | Leader 장애 시 |
| all | 낮음 | ISR 전체 복제 | ISR 전체 장애 시만 |
프로덕션에서는 acks=all이 기본이다. 처리량이 중요한 로그 수집 등에서만 acks=0 또는 acks=1을 고려한다.
Partitioner 전략
Producer는 메시지를 어떤 Partition에 보낼지 결정해야 한다. 이 결정을 담당하는 것이 Partitioner이다.
Key가 있는 경우: 기본적으로 키의 해시값을 파티션 수로 나눈 나머지로 파티션을 결정한다. 같은 키는 항상 같은 파티션으로 간다. 이는 키 단위의 순서 보장을 가능하게 한다.
# 같은 user_id를 가진 이벤트는 항상 같은 파티션으로
producer.send("user-events", key=b"user-123", value=b"clicked")
producer.send("user-events", key=b"user-123", value=b"purchased")
# → 두 메시지는 같은 파티션에 순서대로 저장된다
Key가 없는 경우: Kafka 2.4 이전에는 Round-Robin 방식으로 파티션을 선택했다. Kafka 2.4부터는 Sticky Partitioner가 기본이다. Sticky Partitioner는 하나의 배치가 채워질 때까지 같은 파티션에 계속 보내고, 배치가 전송되면 다른 파티션으로 전환한다. 이렇게 하면 배치 효율이 높아져 처리량이 개선된다.
배치 전송과 압축
Producer는 메시지를 하나씩 보내는 것이 아니라, 배치(batch) 로 모아서 전송한다. 이를 제어하는 핵심 설정이 두 가지이다.
# 배치 크기 (바이트). 이 크기가 차면 즉시 전송
batch.size=16384
# 배치가 안 찼어도 이 시간이 지나면 전송 (밀리초)
linger.ms=5
linger.ms=0(기본값)이면 메시지가 들어오는 즉시 전송한다. linger.ms를 늘리면 더 많은 메시지를 모아서 보낼 수 있어 처리량이 증가하지만, 지연 시간도 그만큼 늘어난다.
압축도 배치 단위로 적용된다.
# 압축 알고리즘: none, gzip, snappy, lz4, zstd
compression.type=lz4
| 알고리즘 | 압축률 | CPU 사용량 | 속도 |
|---|---|---|---|
| gzip | 높음 | 높음 | 느림 |
| snappy | 중간 | 낮음 | 빠름 |
| lz4 | 중간 | 낮음 | 매우 빠름 |
| zstd | 높음 | 중간 | 빠름 |
네트워크 대역폭이 병목이라면 압축이 전체 처리량을 크게 개선할 수 있다. lz4가 압축률과 속도의 균형이 좋아 가장 널리 사용된다.
Idempotent Producer
네트워크 오류로 Producer가 Broker의 응답을 받지 못하면, 같은 메시지를 재전송한다. 이때 Broker가 실제로는 메시지를 성공적으로 저장했다면, 중복 메시지가 발생한다.
Idempotent Producer는 이 문제를 해결한다. 각 Producer에게 고유한 PID(Producer ID) 를 부여하고, 메시지에 시퀀스 번호를 매긴다. Broker는 PID + 시퀀스 번호의 조합을 확인하여 중복 메시지를 자동으로 걸러낸다.
# Idempotent Producer 활성화 (Kafka 3.0+에서는 기본 활성화)
enable.idempotence=true
Idempotent Producer를 활성화하면 자동으로 acks=all, retries=Integer.MAX_VALUE, max.in.flight.requests.per.connection=5가 설정된다.
Consumer 심화
Offset 관리
Offset은 Consumer가 각 파티션에서 어디까지 읽었는지를 나타내는 위치 정보이다. Kafka는 이 Offset을 __consumer_offsets라는 내부 토픽에 저장한다.
자동 커밋(Auto Commit)
enable.auto.commit=true
auto.commit.interval.ms=5000
기본 설정에서 Consumer는 5초마다 현재까지 읽은 Offset을 자동으로 커밋한다. 편리하지만 위험성이 있다.
- 메시지 유실: Offset이 커밋된 후, 실제 처리가 완료되기 전에 Consumer가 죽으면, 해당 메시지는 다시 읽히지 않는다
- 중복 처리: 메시지를 처리한 후, Offset 커밋 전에 Consumer가 죽으면, 재시작 후 같은 메시지를 다시 처리한다
수동 커밋(Manual Commit)
// 동기 커밋: 커밋이 완료될 때까지 블로킹
consumer.commitSync();
// 비동기 커밋: 블로킹 없이 커밋 요청. 실패 시 콜백
consumer.commitAsync((offsets, exception) -> {
if (exception != null) {
log.error("Commit failed", exception);
}
});
수동 커밋을 사용하면 처리가 완료된 후에만 Offset을 커밋하여 메시지 유실을 방지할 수 있다. 일반적인 패턴은 비동기 커밋을 기본으로 사용하되, Consumer 종료 시에만 동기 커밋을 수행하는 것이다.
Consumer Group과 Rebalancing
Consumer Group 내의 Consumer 수가 변하면 Rebalancing이 발생한다. 파티션을 Consumer들에게 재분배하는 과정이다.
Rebalancing이 발생하는 상황:
- Consumer가 Group에 합류하거나 이탈할 때
- Consumer가
session.timeout.ms이내에 heartbeat를 보내지 못할 때 - Consumer가
max.poll.interval.ms이내에poll()을 호출하지 못할 때 - 구독 중인 토픽의 파티션 수가 변할 때
Rebalancing 전략
Eager Rebalancing(기존 방식): Rebalancing이 시작되면 모든 Consumer가 모든 파티션을 반납하고, 처음부터 다시 분배한다. 이 과정에서 전체 Consumer Group이 일시 정지되는 Stop-the-World 현상이 발생한다.
Cooperative(Incremental) Rebalancing: 변경이 필요한 파티션만 재분배한다. 나머지 파티션은 기존 Consumer가 계속 소유하므로, 전체 정지 없이 점진적으로 전환된다. Kafka 2.4에서 도입되었다.
# Cooperative Rebalancing 활성화
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor
파티션 할당 전략
Rebalancing 시 파티션을 Consumer에게 배분하는 알고리즘이다.
| 전략 | 동작 | 특징 |
|---|---|---|
| RangeAssignor | 토픽별로 파티션을 연속 범위로 분배 | 기본값. 토픽이 많으면 불균형 발생 가능 |
| RoundRobinAssignor | 모든 파티션을 순환 배분 | 균등 분배에 유리 |
| StickyAssignor | 기존 할당 최대한 유지 + 균등 분배 | Rebalancing 시 파티션 이동 최소화 |
| CooperativeStickyAssignor | StickyAssignor + Cooperative Rebalancing | 가장 권장되는 전략 |
CooperativeStickyAssignor가 가장 현대적이고 권장되는 전략이다. 기존 할당을 최대한 유지하면서도 균등한 분배를 보장하고, Stop-the-World 없는 점진적 Rebalancing을 지원한다.
메시지 전달 보장 수준
분산 메시징 시스템에서 메시지 전달 보장은 세 가지 수준으로 구분된다.
At-most-once (최대 한 번)
메시지가 유실될 수 있지만, 중복은 없다. Producer가 acks=0으로 전송하거나, Consumer가 처리 전에 Offset을 커밋하면 이 수준이 된다. 일부 유실이 허용되는 메트릭 수집 등에 사용된다.
At-least-once (최소 한 번)
메시지가 유실되지는 않지만, 중복이 발생할 수 있다. Producer가 acks=all로 전송하고, Consumer가 처리 후에 Offset을 커밋하면 이 수준이다. 대부분의 시스템이 이 수준을 기본으로 설계하며, Consumer 측에서 멱등 처리(idempotent processing) 를 구현하여 중복의 영향을 제거한다.
Exactly-once (정확히 한 번)
메시지가 유실되지도, 중복되지도 않는다. 가장 이상적이지만 구현이 가장 어렵다. Kafka는 두 가지 메커니즘으로 Exactly-once를 지원한다.
1. Idempotent Producer: 앞서 다룬 대로, Producer 측의 중복 전송을 제거한다. 단일 파티션 내에서의 Exactly-once를 보장한다.
2. Transactional API: 여러 파티션에 걸친 원자적 쓰기를 지원한다. "메시지를 읽고 → 처리하고 → 결과를 쓰고 → Offset을 커밋하는" 전체 과정을 하나의 트랜잭션으로 묶을 수 있다.
producer.initTransactions();
try {
producer.beginTransaction();
producer.send(new ProducerRecord<>("output-topic", key, value));
producer.sendOffsetsToTransaction(offsets, consumerGroupId);
producer.commitTransaction();
} catch (Exception e) {
producer.abortTransaction();
}
Kafka Streams는 내부적으로 이 Transactional API를 사용하여 End-to-End Exactly-once 처리를 제공한다.
실전 설정 가이드
Producer 핵심 설정
# 필수
bootstrap.servers=broker1:9092,broker2:9092,broker3:9092
acks=all
enable.idempotence=true
# 성능
batch.size=32768
linger.ms=5
compression.type=lz4
buffer.memory=67108864
# 안정성
retries=2147483647
delivery.timeout.ms=120000
max.in.flight.requests.per.connection=5
Consumer 핵심 설정
# 필수
bootstrap.servers=broker1:9092,broker2:9092,broker3:9092
group.id=my-consumer-group
auto.offset.reset=earliest
# Offset 관리
enable.auto.commit=false
# Rebalancing
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor
session.timeout.ms=45000
heartbeat.interval.ms=15000
max.poll.interval.ms=300000
# 배치 처리
max.poll.records=500
fetch.min.bytes=1
fetch.max.wait.ms=500
auto.offset.reset은 Consumer Group이 처음 시작하거나, 커밋된 Offset이 이미 삭제된 경우의 동작을 결정한다.
earliest: 가장 처음부터 읽는다 (데이터 유실 방지)latest: 최신 메시지부터 읽는다 (과거 데이터 스킵)
정리
- Producer acks:
acks=all이 프로덕션 기본. 처리량과 내구성의 트레이드오프를 이해하고 선택한다 - Partitioner: Key 기반 파티셔닝으로 순서 보장, Sticky Partitioner로 배치 효율 극대화
- Idempotent Producer: PID + 시퀀스 번호로 Producer 측 중복 전송을 자동 제거한다
- Offset 관리: 수동 커밋으로 "처리 완료 후 커밋" 패턴을 구현하여 메시지 유실을 방지한다
- Rebalancing: CooperativeStickyAssignor로 Stop-the-World 없는 점진적 Rebalancing을 적용한다
- 전달 보장: At-least-once를 기본으로, 필요시 Transactional API로 Exactly-once를 구현한다
Producer와 Consumer의 설정은 시스템 요구 사항에 따라 크게 달라진다. 처리량이 중요한 로그 수집 파이프라인과, 정확성이 중요한 결제 시스템은 같은 Kafka를 쓰더라도 설정이 완전히 다르다. 자신의 유스케이스에 맞는 트레이드오프를 이해하고 설정을 조율하는 것이 핵심이다.