Engineering Note
Consumer Group만으로는 Queue가 되지 않는다: Kafka Share Group의 의미
Kafka Share Group이 왜 추가되었는지, 기존 Consumer Group과 무엇이 다른지, 어떤 기준으로 적용해야 하는지 정리합니다.
Kafka를 오래 쓰다 보면 이런 순간이 옵니다.
메시지는 Kafka에 있고, 처리자는 여러 대로 늘리고 싶습니다. 그런데 topic partition 수가 적으면 consumer를 아무리 많이 띄워도 동시에 일하는 consumer 수가 partition 수를 넘기 어렵습니다. 반대로 처음부터 partition을 많이 잡으면 순서, 운영 비용, 리밸런스, 파일 핸들, broker 부하 같은 다른 문제가 따라옵니다.
이때 사람들은 Kafka topic을 queue처럼 쓰고 싶어집니다.
Apache Kafka의 Share Group은 이 간격을 메우기 위해 들어온 기능입니다. 정확히는 Kafka에 별도 queue 제품을 하나 더 붙이는 기능이 아니라, 기존 topic 위에서 여러 consumer가 같은 partition의 record를 협력적으로 처리할 수 있게 하는 새로운 group 모델입니다.
이 글은 KIP-932: Queues for Kafka와 Apache Kafka 4.3 문서를 기준으로 Share Group의 추가 배경, 의미, 적용 방법을 정리합니다. 버전 맥락도 중요합니다. Share Group은 Kafka 4.0에서 early access, 4.1에서 preview를 거쳐, Kafka 4.2에서 production-ready로 문서화되었습니다.
먼저 가져갈 결론
Share Group을 이해할 때는 이 다섯 가지가 핵심입니다.
- Consumer Group은 partition 단위 소유권 모델입니다.
- Share Group은 record 단위 획득과 acknowledgement 모델입니다.
- Share Group에서는 consumer 수가 partition 수보다 많아도 의미가 있습니다.
- 그 대신 partition 내부의 강한 처리 순서 보장은 내려놓아야 합니다.
- stream 처리보다 “각 record를 독립 작업처럼 처리하는 queue형 workload”에 맞습니다.
한 줄로 줄이면 이렇습니다.
Consumer Group은 ordered stream을 나눠 읽기 위한 모델이고,
Share Group은 Kafka topic 위에서 queue-like work sharing을 하기 위한 모델입니다.
기존 Consumer Group의 강점과 한계
Kafka Consumer Group은 매우 좋은 기본 모델입니다. 하나의 group 안에서 각 partition은 특정 consumer에게 배정되고, 그 consumer는 partition 안의 record를 offset 순서대로 처리합니다.
이 모델의 장점은 분명합니다.
- partition 단위 순서를 이해하기 쉽습니다.
- offset commit 모델이 단순합니다.
- 같은 group 안에서 record가 중복 처리되는 위험을 줄입니다.
- partition을 늘리면 병렬 처리 단위도 늘릴 수 있습니다.
하지만 이 모델은 queue workload와 완전히 같지 않습니다. partition이 3개인 topic에 consumer를 20개 붙여도, 동시에 active하게 일할 수 있는 consumer는 보통 3개입니다. 나머지는 대기하거나 다른 partition이 생기기를 기다립니다.
이 제약은 Kafka가 잘못 설계되어서 생긴 문제가 아닙니다. Consumer Group은 순서 있는 log를 병렬로 읽기 위한 모델이기 때문입니다. partition ownership을 선명하게 가져가는 대신, partition보다 더 잘게 일을 나누지는 않습니다.
Queue처럼 쓰고 싶은 순간
반대로 많은 작업 queue에서는 record 하나하나가 비교적 독립적입니다.
예를 들어 이런 workload를 생각해볼 수 있습니다.
- 이미지 썸네일 생성 작업
- 이메일 발송 작업
- webhook delivery 작업
- 외부 API 동기화 작업
- AI inference job dispatch
- 실패하면 나중에 다시 시도해도 되는 비동기 후처리
이런 경우 운영자가 원하는 것은 보통 “partition별 순서 유지”보다 “처리자 수를 더 쉽게 늘리는 것”입니다.
작업이 밀린다
-> worker를 더 띄운다
-> 같은 topic의 작업을 더 많은 worker가 나눠 처리한다
기존 Consumer Group에서는 이 흐름이 partition 수에 묶입니다. Share Group은 바로 이 지점에서 다른 선택지를 제공합니다.
Share Group은 무엇을 바꾸나
Share Group은 Consumer Group과 나란히 존재하는 새로운 group 종류입니다. 같은 topic을 여러 consumer group과 여러 share group이 독립적으로 읽을 수 있습니다. 하지만 group 내부의 처리 방식은 다릅니다.
Share Group에서 consumer는 partition을 독점 소유하지 않습니다. 여러 share consumer가 같은 partition에서 record를 받을 수 있습니다. broker는 record를 특정 consumer에게 전달할 때 그 record를 일정 시간 동안 획득 상태로 둡니다. 이 시간 동안 다른 consumer는 같은 record를 받지 않습니다.
처리 결과는 acknowledgement로 표현합니다.
| acknowledgement | 의미 |
|---|---|
ACCEPT | 처리 성공. record를 완료 처리합니다. |
RELEASE | 일시 실패. 다시 delivery 대상이 될 수 있게 합니다. |
REJECT | 처리 불가. 더 이상 재전달하지 않습니다. |
RENEW | 아직 처리 중. acquisition lock을 연장합니다. |
기본 lock duration은 30초이며, group 설정인 share.record.lock.duration.ms로 조정할 수 있습니다. Kafka 4.3 기준으로 share.delivery.count.limit, share.partition.max.record.locks, share.renew.acknowledge.enable 같은 share group 설정도 추가되어 있습니다.
여기서 중요한 변화는 offset 중심 사고에서 record 상태 중심 사고로 일부 이동한다는 점입니다.
적용 코드는 어떻게 달라지나
가장 단순한 share consumer는 기존 consumer와 비슷해 보입니다.
Properties props = new Properties();
props.setProperty("bootstrap.servers", "localhost:9092");
props.setProperty("group.id", "thumbnail-workers");
props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
try (KafkaShareConsumer<String, String> consumer = new KafkaShareConsumer<>(props)) {
consumer.subscribe(List.of("thumbnail-jobs"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record : records) {
createThumbnail(record.value());
}
}
}
기본 implicit acknowledgement 모드에서는 다음 poll() 호출 시 이전에 받은 record들이 성공 처리된 것으로 acknowledgement됩니다. 단순한 작업에는 편하지만, 실패를 record별로 나눠 다루고 싶다면 explicit acknowledgement를 쓰는 편이 낫습니다.
Properties props = new Properties();
props.setProperty("bootstrap.servers", "localhost:9092");
props.setProperty("group.id", "thumbnail-workers");
props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.setProperty("share.acknowledgement.mode", "explicit");
try (KafkaShareConsumer<String, String> consumer = new KafkaShareConsumer<>(props)) {
consumer.subscribe(List.of("thumbnail-jobs"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record : records) {
try {
createThumbnail(record.value());
consumer.acknowledge(record, AcknowledgeType.ACCEPT);
} catch (TransientStorageException e) {
consumer.acknowledge(record, AcknowledgeType.RELEASE);
} catch (InvalidJobException e) {
consumer.acknowledge(record, AcknowledgeType.REJECT);
}
}
consumer.commitSync();
}
}
이 코드는 중요한 판단을 드러냅니다. 모든 실패가 같은 실패가 아닙니다. storage timeout은 다시 시도할 수 있지만, 잘못된 job payload는 다시 받아도 성공하지 못할 가능성이 큽니다.
무엇을 얻고 무엇을 잃나
Share Group이 주는 가장 큰 이점은 partition 수와 consumer 병렬성의 결합을 느슨하게 만든다는 점입니다.
| 관점 | Consumer Group | Share Group |
|---|---|---|
| 작업 분배 단위 | partition | record |
| active consumer 수 | partition 수에 강하게 묶임 | partition 수보다 많아질 수 있음 |
| 순서 보장 | partition 안에서 강함 | 약해짐 |
| 실패 처리 | offset commit, retry 설계 | record acknowledgement, delivery count |
| 잘 맞는 작업 | ordered stream processing | 독립 작업 처리, queue-like workload |
이 표만 보면 Share Group이 더 좋아 보일 수 있습니다. 하지만 그렇지 않습니다. Share Group은 Consumer Group의 대체재라기보다 다른 도구입니다.
순서가 중요한 event stream에서는 Consumer Group이 여전히 자연스럽습니다. 예를 들어 계좌별 transaction ordering, user timeline event ordering, CDC stream 처리처럼 “앞 record가 먼저 처리되어야 한다”는 의미가 강하면 Share Group은 조심해야 합니다.
반대로 record별 작업이 독립적이고, 일시 실패와 영구 실패를 record 단위로 구분하고 싶고, partition 수보다 더 탄력적으로 worker를 늘리고 싶다면 Share Group이 좋은 후보가 됩니다.
운영에서 먼저 봐야 할 설정
Share Group은 broker와 group coordinator 쪽에도 상태를 더 만듭니다. Kafka는 share group 상태를 위해 __share_group_state라는 내부 topic을 사용합니다. broker가 3대 미만인 테스트 클러스터에서 share group을 쓰려면 share.coordinator.state.topic.replication.factor와 share.coordinator.state.topic.min.isr를 1로 낮춰야 할 수 있습니다.
애플리케이션과 운영자가 먼저 확인할 설정은 다음 정도입니다.
| 설정 | 볼 것 |
|---|---|
share.record.lock.duration.ms | 한 record 처리 시간이 lock duration보다 길어지는지 |
share.delivery.count.limit | 반복 실패 record를 몇 번까지 delivery할지 |
share.partition.max.record.locks | share-partition별 in-flight record 수 상한 |
share.acquire.mode | batch 효율을 우선할지, max.poll.records 제어를 엄격히 할지 |
share.acknowledgement.mode | implicit으로 단순하게 갈지, explicit으로 record별 결과를 남길지 |
처리 시간이 길다면 RENEW를 검토할 수 있습니다. 다만 renewal은 record를 오래 붙잡는 방식이므로 남용하면 group 전체의 delivery progress에 영향을 줄 수 있습니다. 오래 걸리는 작업이라면 먼저 작업을 더 작게 쪼갤 수 있는지, 외부 상태 저장소에 job state를 두는 편이 나은지부터 봐야 합니다.
알림과 대시보드도 달라진다
Consumer Group을 운영할 때는 lag, commit offset, rebalance, partition skew를 봅니다. Share Group에서도 backlog와 처리량은 여전히 중요하지만, record-level 상태가 더 중요해집니다.
특히 다음 질문을 대시보드에 올려야 합니다.
- acquired record가 계속 쌓이는가?
- lock timeout 이후 redelivery가 늘어나는가?
RELEASE가 많은가,REJECT가 많은가?- delivery count limit에 닿는 record가 있는가?
- 특정 partition이나 특정 key 계열에서 실패가 몰리는가?
- worker 수를 늘렸을 때 처리량이 실제로 늘어나는가?
Share Group의 운영은 “lag이 얼마인가”만으로 끝나지 않습니다. record가 어떤 상태로 머물고 있는지, 실패가 일시적인지 영구적인지, worker 확장이 효과를 내는지를 같이 봐야 합니다.
Kafka 같은 클러스터형 리소스는 기능 자체보다 운영 신호를 어떻게 읽느냐가 더 중요해지는 순간이 많습니다. Konduo는 Kafka 같은 운영 대상을 플러그인으로 연결해 broker, topic, consumer group, metric evidence, alert response를 하나의 흐름에서 확인할 수 있도록 설계된 통합 관리 운영 플랫폼입니다.
적용 전 체크리스트
Share Group을 바로 쓰기 전에 다음 질문을 통과해보는 편이 좋습니다.
- 이 workload는 partition 내부 순서를 반드시 지켜야 하는가?
- record 하나가 독립 작업처럼 처리되어도 되는가?
- 실패를
ACCEPT,RELEASE,REJECT로 나눌 수 있는가? - record 처리 시간이 lock duration 안에 안정적으로 들어오는가?
- 처리 시간이 긴 record를
RENEW로 붙잡는 것이 정말 맞는가? - worker 수를 partition 수보다 더 많이 늘릴 이유가 있는가?
- poison record를 어디서 관측하고 어떻게 복구할 것인가?
- 기존 Consumer Group으로도 충분한 문제를 새 모델로 복잡하게 만들고 있지는 않은가?
제 기준은 단순합니다.
순서 있는 event stream이면 Consumer Group을 먼저 본다.
독립 작업 queue에 가깝고 worker 탄력성이 중요하면 Share Group을 검토한다.
Share Group은 Kafka를 queue처럼 “흉내 내는” 기존 패턴을 줄여줄 수 있습니다. 하지만 Kafka의 모든 소비 모델을 대체하는 기능은 아닙니다. 이 기능의 의미는 Kafka가 ordered log와 queue-like workload 사이에 더 공식적인 선택지를 갖게 되었다는 데 있습니다.
함께 읽기 좋은 글
이 글의 관점과 이어지는 주제를 더 보려면 아래 글도 함께 읽어볼 수 있습니다.
- Consumer Lag은 건강 점수가 아니다: Kafka Consuming Pressure라는 관점 - consumer lag을 처리 압력 관점으로 다시 해석합니다.
- Consumer Group인데 Consumer가 아니다: Kafka 안에서 노드들이 서로를 찾는 법 - consumer group을 소비가 아닌 노드 coordination 관점에서 봅니다.
- Redis에도 Consumer Group이 있다: Streams를 캐시 너머로 보기 - Redis Streams를 cache 너머의 메시징 구조로 살펴봅니다.