Kafka 마스터 — Consumer Group·리밸런싱

2026-05-03확률과 통계 마스터 노트

카프카 마스터 노트 시리즈 4편. Consumer Group이 Kafka 병렬 처리의 단위가 되는 원리, 그룹 코디네이터의 역할, 4가지 파티션 할당 전략(Range·Round-Robin·Sticky·Cooperative Sticky), 리밸런싱이 발생하는 5가지 시점과 그 비용, Heartbeat·Session Timeout 설정, Static Membership으로 리밸런싱 회피까지.

이 글은 카프카 마스터 노트 시리즈의 네 번째 편입니다. 3편(Producer/Consumer)에서 단일 컨슈머를 봤다면, 이번엔 여러 컨슈머가 협력하는 단위 — Consumer Group.

Consumer Group이 Kafka의 모든 병렬 처리·확장성·고가용성의 핵심. 동시에 리밸런싱은 운영 환경의 가장 큰 함정 — 한 인스턴스 추가만 해도 전체가 잠시 멈춥니다.

처음 Consumer Group이 어렵게 느껴지는 이유

처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, "같은 group.id면 한 그룹"이라는 단순 룰이 강력해서. 그게 모든 동작을 결정하는데, 단순해 보여서 디테일을 놓치기 쉽습니다. 둘째, 리밸런싱이 어떻게 일어나는지 그림이 잘 안 잡힙니다. 한 컨슈머 죽으면 누가 결정해서 누가 새 파티션을 받는가?

해결법은 한 가지예요. "같은 영업팀의 영업사원들" 비유로 묶는 것. Consumer Group = 영업팀, Consumer = 영업사원, Partition = 담당 구역, Group Coordinator = 팀장. 영업사원 한 명 그만두면 팀장이 구역을 재분배. 이 그림이 잡히면 모든 동작이 따라옵니다.

Consumer Group — 단순한 약속

같은 group.id = 같은 그룹.

props.put("group.id", "order-processor");

그룹 안에서:

  • 각 파티션은 1개 컨슈머에만 할당
  • 컨슈머는 여러 파티션 받을 수 있음
order-events 토픽 (3 파티션)
group=order-processor (3 컨슈머)
   ├── consumer-1 → partition 0
   ├── consumer-2 → partition 1
   └── consumer-3 → partition 2

컨슈머 추가 시:

group (4 컨슈머, 3 파티션)
   ├── consumer-1 → partition 0
   ├── consumer-2 → partition 1
   ├── consumer-3 → partition 2
   └── consumer-4 → 놀음 (파티션 X)

여기서 정말 중요한 시험 함정 — 컨슈머 수 > 파티션 수면 일부 컨슈머는 놀게 됨. 파티션이 병렬도의 상한. 더 많은 병렬이 필요하면 파티션 수 늘려야.

다중 그룹 — 독립적 소비

order-events 토픽
   ├── group=order-processor (3 컨슈머)
   ├── group=analytics-team (2 컨슈머)
   └── group=audit-system (1 컨슈머)

각 그룹은 독립적으로 모든 메시지 소비. 같은 메시지를 3 그룹이 각자 처리.

이게 Kafka의 강점 — N개 시스템이 같은 데이터를 자기 페이스로 처리.

Group Coordinator — 그룹 관리자

각 그룹에 하나의 코디네이터 브로커 할당. 역할:

  1. 그룹 멤버 추적 (누가 살아있나)
  2. 파티션 할당 결정
  3. Offset commit 저장 (__consumer_offsets)
  4. 리밸런싱 트리거

코디네이터 결정:

hash(group.id) % numPartitionsOf(__consumer_offsets) → 그 파티션의 Leader

여기서 시험 함정이 하나 있어요. 코디네이터 다운 시 자동 재선출. __consumer_offsets 토픽이 복제됨 → 다른 브로커가 코디네이터 인계.

파티션 할당 전략 4종

Consumer Group이 결정. partition.assignment.strategy 옵션.

1. Range Assignor (기본, 옛 방식)

3 파티션, 2 컨슈머:
  consumer-1 → [partition 0, 1]
  consumer-2 → [partition 2]

토픽 단위로 연속 할당. 단순하지만 불균형 위험 (다중 토픽 시).

2. Round-Robin Assignor

3 파티션, 2 컨슈머:
  consumer-1 → [partition 0, 2]
  consumer-2 → [partition 1]

전체 파티션을 round-robin. 더 균등.

3. Sticky Assignor

Round-Robin과 비슷하지만, 리밸런싱 시 기존 할당 최대한 유지:

Before: c1=[0,1], c2=[2,3]
c2 죽음 → 리밸런싱
After: c1=[0,1,2,3]  (1만 받음, 0,1은 그대로)

장점 — 처리 컨텍스트 유지·캐시 활용.

4. Cooperative Sticky Assignor (권장, Kafka 2.4+)

Sticky의 무중단 버전. 리밸런싱 중에도 영향 없는 컨슈머는 계속 처리.

여기서 정말 중요한 시험 함정 — Cooperative Sticky가 신규 표준. Kafka 2.4+. 기존엔 모든 컨슈머가 stop-the-world 리밸런싱. 이젠 점진적 협력.

props.put("partition.assignment.strategy",
    "org.apache.kafka.clients.consumer.CooperativeStickyAssignor");

리밸런싱 — 그룹 변경 시 재할당

트리거 5가지

  1. 새 컨슈머 추가
  2. 컨슈머 종료 (unsubscribe() 또는 정상 종료)
  3. 컨슈머 죽음 (heartbeat 안 옴)
  4. 토픽 파티션 수 변경 (늘리기)
  5. 구독 토픽 변경

비용

[stop-the-world 리밸런싱 (옛 방식)]
모든 컨슈머가 멈춤
→ 코디네이터에 join 요청
→ 새 할당 받음
→ 처리 재개

= 수십 ms ~ 수 분 (그룹 크기·파티션 수 비례)

대규모 그룹에선 체감 가능한 지연. 운영의 핵심 함정.

여기서 정말 중요한 시험 함정 — 리밸런싱 = 일시 정지. 한 컨슈머 추가만 해도 전체 그룹이 멈춤. 무중단 배포 어려움. Cooperative Sticky로 완화.

Heartbeat·Session Timeout

컨슈머 생존 확인 메커니즘.

heartbeat.interval.ms = 3000   # 3초마다 heartbeat
session.timeout.ms = 45000     # 45초 이상 안 오면 죽었다고 판단

컨슈머가 정해진 시간 내 heartbeat 안 보내면 → 죽음 판정 → 리밸런싱.

max.poll.interval.ms — 별도 시계

max.poll.interval.ms = 300000  # 5분

처리가 너무 오래 걸리면도 죽음 판정. Heartbeat은 백그라운드 스레드에서 잘 가도, poll() 호출이 5분 이상 멈추면 추방.

여기서 시험 함정이 하나 있어요. 무거운 처리 = max.poll.interval.ms 늘리기. 한 메시지 처리에 10분 걸리면 600000(10분)으로. 또는 메시지를 별도 큐에 넘겨 비동기 처리.

Static Membership — 리밸런싱 회피

Kafka 2.3+. 컨슈머에 고정 ID.

props.put("group.instance.id", "consumer-prod-1");

이 컨슈머가 잠깐 재시작해도 같은 ID로 다시 들어오면 리밸런싱 X. 일시적 재시작에 강건.

session.timeout.ms 안에 같은 group.instance.id로 복귀
→ 같은 파티션 그대로 받음

운영의 무중단 배포·rolling restart에 필수.

Offset Commit 위치

__consumer_offsets 내부 토픽:

key:    (group.id, topic, partition)
value:  offset, metadata, timestamp

50개 파티션 (기본). Compaction enabled — 같은 key 옛 값 자동 정리.

# 그룹 상태 확인
kafka-consumer-groups --bootstrap-server localhost:9092 \
  --group order-processor --describe

# 결과:
# TOPIC          PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
# order-events   0          1234            1300            66
# order-events   1          2345            2350            5
# ...

Lag — 컨슈머 모니터링 핵심

LAG = LOG-END-OFFSET - CURRENT-OFFSET

지속 증가 = 컨슈머가 따라가지 못함. 처리량 ↓ 또는 트래픽 ↑.

대응:

  • 컨슈머 인스턴스 추가 (파티션 수까지)
  • 처리 로직 최적화
  • 파티션 수 늘리기

여기서 시험 함정이 하나 있어요. Lag 모니터링은 운영 필수. Burrow·Kafka Lag Exporter 같은 도구로 알람 설정.

subscribe() vs assign()

// 동적 — Consumer Group + 자동 할당
consumer.subscribe(List.of("order-events"));

// 정적 — 직접 파티션 지정 (Consumer Group X)
consumer.assign(List.of(
    new TopicPartition("order-events", 0),
    new TopicPartition("order-events", 1)
));

assignConsumer Group 안 씀. 리밸런싱 X. 특수 용도.

정확한 Offset Commit 패턴

안전 패턴

while (running) {
    var records = consumer.poll(Duration.ofMillis(100));
    for (var record : records) {
        try {
            process(record);
        } catch (Exception e) {
            // 에러 처리: 재시도 또는 DLQ (7편)
            handleError(record, e);
        }
    }
    consumer.commitSync();   // 처리 완료 후
}

핵심 — 처리 완료 후 commit. 처리 실패 시 commit 안 됨 → 다음 poll에서 재시도.

위험 패턴

while (running) {
    var records = consumer.poll(...);
    consumer.commitSync();   // X — 처리 전 commit
    for (var record : records) {
        process(record);  // 실패해도 commit 됨 → 손실
    }
}

여기서 정말 중요한 시험 함정 — At-least-once vs At-most-once. Commit 위치가 결정. 처리 후 commit = at-least-once (재처리 가능, 중복 위험), 처리 전 commit = at-most-once (손실 위험). 대부분 선택은 at-least-once + 멱등 처리.

시험 직전 한 번 더 — 자주 헷갈리는 함정 모음

여기까지가 4편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • Consumer Group = 같은 group.id
  • 그룹 안 — 각 파티션은 1 컨슈머에만
  • 컨슈머 수 > 파티션 수 = 일부 놀음
  • 다중 그룹 = 독립 소비 (각자 모든 메시지)
  • Group Coordinator = 그룹 관리 브로커
  • 코디네이터 다운 시 자동 재선출
  • Offset commit → __consumer_offsets 내부 토픽 (50 파티션)
  • 4 할당 전략 — Range / Round-Robin / Sticky / Cooperative Sticky
  • Cooperative Sticky가 신규 표준 (Kafka 2.4+)
  • 리밸런싱 트리거 — 추가·종료·죽음·파티션 수 변경·구독 변경
  • 리밸런싱 = 일시 정지 (옛 방식 stop-the-world)
  • Cooperative Sticky로 완화
  • Heartbeat 3초마다 / Session timeout 45초
  • max.poll.interval.ms = 처리 시간 한계 (5분 기본)
  • 무거운 처리는 늘리거나 비동기로
  • Static Membership (group.instance.id) = rolling restart 무중단
  • Lag = LOG-END-OFFSET - CURRENT-OFFSET
  • 지속 증가 = 컨슈머가 따라가지 못함
  • 도구 — Burrow·Kafka Lag Exporter
  • subscribe() 동적 / assign() 정적 (Group X)
  • At-least-once 권장 = 처리 후 commit + 멱등 처리
  • 처리 전 commit = at-most-once (손실 위험)

시리즈 다른 편

공식 문서: Kafka Consumer Group Protocol 에서 더 깊이.

다음 글(5편)에서는 Reactor Kafka — KafkaReceiver·KafkaSender·리액티브 파이프라인·백프레셔까지 풀어 갑니다.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!