Skip to content

Consumer Internals

컨슈머는 토픽의 메시지를 읽어 처리하는 클라이언트이며, 내부적으로는 컨슈머 그룹이라는 개념을 통해 확장성과 안정성을 보장받는다.

graph TD
T[Topic: 4 Partitions] --> P0[P0]
T --> P1[P1]
T --> P2[P2]
T --> P3[P3]
subgraph CG [Consumer Group]
C1[Consumer 1]
C2[Consumer 2]
end
P0 --> C1
P1 --> C1
P2 --> C2
P3 --> C2

컨슈머 그룹은 단순히 컨슈머의 집합이 아니라, 카프카 브로커에 의해 중앙에서 관리되는 하나의 단위이다.

  • 컨슈머(Consumer): 토픽에서 이벤트를 가져와서(Pull) 처리하는 클라이언트 애플리케이션
    • 각 컨슈머는 자신이 읽은 이벤트의 오프셋(Offset)을 관리하며, 필요에 따라 오프셋을 커밋하여 어디까지 읽었는지 기록
    • 오프셋 커밋 방식에는 자동 커밋(auto-commit)과 수동 커밋(manual commit)이 있으며, 일반적으로 처리 완료 후 배치 단위로 커밋하는 방식을 권장
  • 컨슈머 그룹(Consumer Group): 동일한 목적을 위해 특정 토픽을 구독하는 컨슈머들의 집합
    • 카프카 컨슈머의 확장성과 고가용성을 구현하는 핵심 개념
    • 토픽의 각 파티션은 컨슈머 그룹 내 단 하나의 컨슈머에게만 할당
      • 예시
        • 10개의 파티션을 가진 토픽이 있다면, 한 컨슈머 그룹은 최대 10개의 컨슈머를 투입하여 병렬로 데이터를 처리 가능
          • 더 추가하더라도 파티션 수가 부족하여 추가 컨슈머는 유휴 상태
        • 만약 그룹 내 컨슈머 중 하나에 장애가 발생하면, 카프카는 리밸런스(Rebalance) 과정을 통해 다른 컨슈머에게 자동으로 재할당
    • 각 그룹이 독립적인 오프셋 관리
      • 동일한 토픽을 여러 컨슈머 그룹이 각자 독립적으로 구독 가능(동일한 이벤트를 서로 다른 목적으로 처리 가능)

각 컨슈머 그룹은 브로커 중 하나를 그룹 코디네이터(Group Coordinator)로 할당받아 관리된다.

  • 그룹 코디네이터(Group Coordinator)
    • 각 컨슈머 그룹은 브로커 중 하나를 코디네이터로 할당
    • 코디네이터는 그룹 내 컨슈머들의 상태를 추적하고, 새로운 컨슈머가 참여하거나 기존 컨슈머가 이탈할 때 파티션 재할당(리밸런싱)을 주도하는 역할
  • 컨슈머의 그룹 참여 과정
    • 컨슈머는 시작 시 코디네이터에게 요청을 보내 그룹에 참여
    • 그룹 내 첫 번째로 참여한 컨슈머가 리더 역할 수행
      • 리더는 코디네이터로부터 그룹 멤버 목록과 구독 토픽 정보를 받아 어떤 컨슈머가 어떤 파티션을 할당받을지 결정
    • 리더가 결정한 할당 계획을 코디네이터에게 전달하면, 코디네이터는 이 계획을 모든 그룹 멤버에게 전파하여 각자 자신이 담당할 파티션을 인지하고 메시지 처리 시작

코디네이터는 컨슈머가 정상 동작 중인지 주기적으로 확인하며, 이를 위해 하트비트(Heartbeat) 메커니즘을 사용한다.

  • heartbeat.interval.ms
    • 컨슈머가 코디네이터에게 자신이 살아있음을 알리기 위해 하트비트를 보내는 주기
    • session.timeout.ms보다 반드시 낮게 설정
  • session.timeout.ms
    • 코디네이터가 컨슈머로부터 하트비트를 받지 못했을 때, 해당 컨슈머가 비정상 종료되었다고 판단하기까지 기다리는 최대 시간
    • 이 시간이 초과되면 코디네이터는 해당 컨슈머를 그룹에서 제외하고 리밸런싱을 시작

리밸런싱은 컨슈머 그룹의 확장성과 고가용성을 위해 파티션의 소유권을 동적으로 재분배하는 과정이다.

  • 리밸런싱 발생 시점
    • 그룹에 새로운 컨슈머가 참여할 때
    • 그룹의 기존 컨슈머가 종료되거나, 장애로 인해 세션 타임아웃을 초과할 때
    • 구독 중인 토픽의 파티션 수가 변경될 때
  • 리밸런싱 과정과 영향
    • 리밸런싱이 발생하는 동안, 해당 컨슈머 그룹의 모든 컨슈머는 일시적으로 메시지 처리를 중단
    • 이 중단 시간을 최소화하는 것이 중요
      • session.timeout.ms, heartbeat.interval.ms 등의 설정을 통해 리밸런스 감지 민감도 조절 가능

각 컨슈머 그룹 내에서 파티션을 컨슈머들에게 어떻게 분배할지 결정하는 전략이다.

  • Range: 토픽별로 파티션을 연속된 범위로 계산하여 할당
  • RoundRobin: 모든 토픽의 파티션을 모아 라운드로빈 방식으로 순서대로 할당
  • Sticky: 기존의 파티션 할당을 최대한 유지하려 시도(리밸런싱 시 최소한의 파티션 이동만 발생시켜 안정성을 높임)
  • CooperativeSticky: Sticky 전략을 개선하여, “Stop-the-world” 없이 일부 컨슈머는 계속해서 기존 파티션을 처리하도록 허용하는 점진적 리밸런싱을 지원

컨슈머는 브로커가 밀어주는(Push) 방식이 아닌, 능동적으로 데이터를 가져오는(Pull) 모델을 사용하며, poll() 메소드를 통해 주기적으로 데이터를 요청한다.

  • max.poll.interval.ms
    • poll() 호출 사이의 최대 허용 시간.
    • 만약 메시지 처리 로직이 너무 오래 걸려 이 시간을 초과하면, 컨슈머는 비정상으로 간주되어 그룹에서 이탈되고 리밸런싱 발생
  • 데이터 페치 관련 주요 설정
    • fetch.min.bytes: 브로커가 컨슈머에게 응답을 주기 전까지 기다리는 데이터의 최소 크기
    • fetch.max.wait.ms: fetch.min.bytes에 도달하지 못하더라도, 브로커가 응답을 주기까지 대기하는 최대 시간
    • max.poll.records: poll() 한번의 호출로 반환받는 최대 레코드 수

트랜잭셔널 프로듀서가 보낸 메시지를 처리할 때, 컨슈머는 어떤 상태의 메시지까지 읽을지 결정할 수 있다.

  • read_uncommitted: 트랜잭션의 커밋 여부와 상관없이 모든 메시지를 읽음
  • read_committed: 성공적으로 커밋된 트랜잭션의 메시지만 읽음

컨슈머가 메시지를 어디까지 처리했는지에 대한 오프셋(Offset)을 기록하는 행위를 커밋(Commit)이라고 하며, 이 커밋 전략에 따라 메시지 처리의 신뢰성 수준이 결정된다.

  • 자동 커밋(enable.auto.commit=true)
    • poll() 호출 시, auto.commit.interval.ms 주기에 맞춰 이전에 poll()로 반환된 마지막 오프셋을 자동으로 커밋
    • 메시지 처리 완료 여부와 관계없이 커밋이 발생할 수 있어 데이터 유실이나 중복의 위험 존재
      • 유실: 메시지 처리에 실패했으나 다음 poll()에서 자동 커밋되면, 해당 메시지는 처리된 것으로 간주
      • 중복: 메시지 처리는 성공했으나 커밋 전에 장애가 발생하면, 재시작 후 마지막 커밋 지점부터 다시 처리
  • 수동 커밋(enable.auto.commit=false)
    • 개발자가 코드에서 명시적으로 커밋 시점을 제어하는 방식
    • 동기 커밋(commitSync)
      • 브로커로부터 커밋 성공 응답을 받을 때까지 블로킹
      • 커밋이 확실하게 보장되어 신뢰성이 높음
    • 비동기 커밋(commitAsync)
      • 커밋 요청 후 응답을 기다리지 않고 즉시 다음 로직을 수행
      • 처리량은 높지만 커밋 실패 시 재시도가 복잡하고, 순서가 중요한 경우 주의 필요

Last updated:

Kafka