Apache Kafka 이론 - 컨슈머 그룹 & 컨슈머 오프셋

이건·2025년 4월 29일
0

Kafka

목록 보기
5/18

컨슈머 그룹이란?

Kafka에서 데이터를 읽는 애플리케이션은 여러 컨슈머(Consumer) 인스턴스를 실행할 수 있다. 이 컨슈머들이 그룹을 이루어 데이터를 읽는 구조를 ‘컨슈머 그룹’이라고 부른다.
컨슈머 그룹은 확장성과 병렬 처리의 핵심이다.


컨슈머 그룹과 파티션 할당

예를 들어, 5개의 파티션을 가진 Kafka 토픽이 있다고 해보자
컨슈머 그룹(예: consumer-group-application)을 만들고, 그 안에 컨슈머 1, 2, 3이 있다고 가정하면,
각 컨슈머는 서로 다른 파티션에서 데이터를 읽게 된다.

  • 컨슈머 1: 파티션 0, 1
  • 컨슈머 2: 파티션 2, 3
  • 컨슈머 3: 파티션 4

즉, 컨슈머 그룹 내의 컨슈머들은 파티션을 나누어 맡아 전체 토픽 데이터를 병렬로 소비한다.


컨슈머 수와 파티션 수의 관계

  • 컨슈머 수 ≤ 파티션 수: 각 컨슈머는 하나 이상의 파티션에서 데이터를 읽는다.

  • 컨슈머 수 > 파티션 수: 일부 컨슈머는 할당받는 파티션이 없어 대기(standby) 상태가 된다.
    예를 들어, 3개의 파티션에 4명의 컨슈머가 있으면, 컨슈머 4는 데이터를 읽지 않고 대기한다.

이 구조는 확장성과 장애 복구에 유리하다. 만약 한 컨슈머가 장애가 나면, 대기 중인 컨슈머가 그 파티션을 대신 맡아 데이터를 이어서 읽을 수 있다.


여러 컨슈머 그룹

하나의 토픽에 여러 컨슈머 그룹이 존재할 수 있다.
각 컨슈머 그룹은 독립적으로 오프셋을 관리하며, 동일한 데이터를 여러 서비스가 각자 읽어 처리할 수 있다.

예를 들어,

  • 컨슈머 그룹 1: 위치 서비스
  • 컨슈머 그룹 2: 알림 서비스

이렇게 하면 두 서비스가 같은 데이터 스트림을 독립적으로 소비할 수 있다.


컨슈머 그룹 설정 방법

Kafka 컨슈머는 group.id라는 프로퍼티로 자신이 속한 그룹을 지정한다.
이 설정만으로도 컨슈머는 그룹 내에서 자동으로 파티션을 할당받아 데이터를 읽게 된다.


오프셋(Offset)과 커밋

컨슈머 그룹의 또 다른 핵심은 오프셋 관리다.

  • 오프셋: 각 파티션에서 컨슈머가 마지막으로 읽은 메시지의 위치(번호)다.
  • Kafka는 각 컨슈머 그룹의 오프셋을 내부 토픽인 __consumer_offsets에 저장한다.

  • 오프셋을 커밋(Commit)하면, 컨슈머가 어디까지 성공적으로 데이터를 읽었는지 Kafka에 기록된다.

오프셋 커밋의 필요성

컨슈머가 데이터를 읽고 처리한 후 오프셋을 커밋하면, 만약 컨슈머가 장애로 중단되었다가 다시 시작해도, 마지막으로 커밋한 오프셋 이후부터 데이터를 이어서 읽을 수 있다.

이 덕분에 데이터 손실 없이 장애 복구가 가능하다.


주요 오프셋 커밋 전략

1. 자동 오프셋 커밋 (enable.auto.commit=true)

이 전략은 가장 간단하고 널리 사용되는 방식으로, Kafka가 자동으로 오프셋을 커밋한다.

작동 방식:

  • enable.auto.commit 속성을 true로 설정 (기본값)
  • auto.commit.interval.ms에 지정된 간격(기본값 5초)마다 자동으로 오프셋 커밋
  • 커밋은 poll() 메서드가 호출될 때만 발생

코드 구조:

while (true) {
    ConsumerRecords records = consumer.poll(Duration.ofMillis(100));
    // 배치 처리 (동시에 처리)
    for (ConsumerRecord record : records) {
        // 각 레코드 처리
    }
    // 다음 poll() 호출 시 auto.commit.interval.ms가 경과했다면 자동으로 오프셋 커밋
}

장점:

  • 구현이 간단하고 직관적
  • 개발자가 오프셋 관리에 신경 쓸 필요 없음

단점:

  • 메시지 처리와 오프셋 커밋 사이에 정확한 제어가 어려움
  • 컨슈머가 중단될 경우 일부 메시지가 손실될 가능성 있음
  • 최소 한 번(at-least once) 전달 의미론만 보장 가능

2. 수동 오프셋 커밋 + 배치 처리 (enable.auto.commit=false)

이 전략은 개발자가 오프셋 커밋 시점을 직접 제어하면서도 배치 처리의 효율성을 유지한다.

작동 방식:

  • enable.auto.commit 속성을 false로 설정
  • 배치 처리 후 명시적으로 commitSync() 또는 commitAsync() 호출
  • 처리 로직에 따라 커밋 시점 결정 (예: 배치 크기, 시간 간격 등)

코드 구조:

List> buffer = new ArrayList<>();

while (true) {
    ConsumerRecords records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord record : records) {
        buffer.add(record);
    }
    
    // 배치 크기가 충분하거나 일정 시간이 경과했을 때
    if (isReady(buffer)) {
        processBatch(buffer);
        consumer.commitAsync(); // 또는 commitSync()
        buffer.clear();
    }
}

장점:

  • 오프셋 커밋 시점을 정확히 제어 가능
  • 배치 처리 완료 후 커밋하여 데이터 손실 방지
  • 처리 로직에 맞게 커밋 전략 최적화 가능

단점:

  • 구현이 더 복잡함
  • 커밋 실패 처리 로직 필요

3. 외부 저장소를 활용한 오프셋 관리 (고급)

이 전략은 Kafka의 내부 오프셋 저장소 대신 외부 데이터베이스나 저장소를 사용하여 오프셋을 관리한다.

작동 방식:

  • enable.auto.commit 속성을 false로 설정
  • 처리된 데이터와 오프셋을 동일한 트랜잭션에서 외부 저장소에 저장
  • ConsumerRebalanceListener 인터페이스 구현하여 리밸런싱 시 오프셋 관리

장점:

  • 정확히 한 번(exactly once) 처리 의미론 구현 가능
  • 데이터 처리와 오프셋 저장을 원자적 트랜잭션으로 처리 가능

단점:

  • 구현이 매우 복잡하고 고급 기술 필요
  • 외부 저장소에 대한 의존성 발생
  • 성능 오버헤드 발생 가능

commitSync vs commitAsync

수동 커밋 시 사용할 수 있는 두 가지 메서드의 차이점을 이해하는 것이 중요하다.

commitSync()

  • 동기식 커밋: 브로커로부터 커밋 확인을 받을 때까지 블로킹
  • 재시도: 실패 시 자동으로 재시도 (비-복구 가능한 오류 제외)
  • 장점: 커밋 성공 여부 확실히 보장
  • 단점: 처리량 감소 (블로킹으로 인한 지연 발생)

commitAsync()

  • 비동기식 커밋: 커밋 요청 후 즉시 반환, 백그라운드에서 처리
  • 재시도: 실패 시 자동 재시도하지 않음 (콜백으로 실패 처리 가능)
  • 장점: 높은 처리량, 지연 시간 감소
  • 단점: 실패 시 자동 복구 없음, 콜백 처리 로직 필요

권장 사항

  1. 일반적인 사용 사례: 자동 커밋(전략 1)을 사용하되, 메시지 처리가 완료되기 전에 다음 poll()을 호출하지 않도록 주의

  2. 데이터 손실에 민감한 경우: 수동 커밋(전략 2)을 사용하여 배치 처리 완료 후 명시적으로 커밋

  3. 정확히 한 번 처리가 필요한 경우: 외부 저장소를 활용한 오프셋 관리(전략 3) 또는 Kafka Streams API 사용 고려

  4. 성능과 신뢰성 균형: commitAsync()를 주로 사용하고, 애플리케이션 종료 전에는 commitSync()로 마무리

요약

Kafka 컨슈머의 오프셋 커밋 전략은 애플리케이션의 요구사항과 처리 의미론(최대 한 번, 최소 한 번, 정확히 한 번)에 따라 신중하게 선택해야 한다. 자동 커밋은 간단하지만 제한적이며, 수동 커밋은 더 많은 제어를 제공하지만 구현이 복잡하다. 외부 저장소를 활용한 오프셋 관리는 정확히 한 번 처리를 보장할 수 있지만, 고급 기술이 필요하고 성능 오버헤드가 발생할 수 있다.

애플리케이션의 요구사항을 명확히 이해하고, 적절한 오프셋 커밋 전략을 선택하여 Kafka 컨슈머의 신뢰성과 성능을 최적화해야한다.


메시지 전달 의미론(Delivery Semantics)

Apache Kafka에서 메시지 처리 방식을 결정하는 전달 의미론(Delivery Semantics)은 데이터 파이프라인의 신뢰성과 일관성에 직접적인 영향을 미친다.

최대 한 번(At-most Once) 전달

최대 한 번 전달 방식은 메시지가 최대 한 번만 처리되며, 경우에 따라 메시지가 처리되지 않을 수 있는 방식이다.

작동 방식

  1. 컨슈머가 메시지 배치를 읽는다.
  2. 즉시 오프셋을 커밋한다.
  3. 그 후에 메시지 처리를 시작한다.

실패 시나리오

컨슈머가 오프셋을 커밋한 후 메시지 처리 중에 충돌하면:

  • 처리 중이던 메시지는 손실된다.
  • 컨슈머가 재시작되면 이미 커밋된 오프셋 이후부터 읽기 시작한다.
  • 충돌 시점에 처리 중이던 메시지는 다시 읽히지 않는다.

적합한 사용 사례

  • 모니터링 메트릭과 같이 일부 데이터 손실이 허용되는 경우
  • 처리 속도가 데이터 완전성보다 중요한 경우

최소 한 번(At-least Once) 전달

최소 한 번 전달 방식은 모든 메시지가 최소 한 번 이상 처리되며, 일부 메시지는 여러 번 처리될 수 있는 방식이다.

작동 방식

  1. 컨슈머가 메시지 배치를 읽는다
  2. 메시지를 처리한다.
  3. 처리가 완료된 후에 오프셋을 커밋한다.

실패 시나리오

컨슈머가 메시지 처리 후 오프셋을 커밋하기 전에 충돌하면:

  • 컨슈머가 재시작되면 마지막으로 커밋된 오프셋부터 읽기 시작한다.
  • 이미 처리했지만 오프셋을 커밋하지 못한 메시지를 다시 처리하게 된다.
  • 결과적으로 일부 메시지는 두 번 이상 처리될 수 있다.

중요 고려사항

  • 멱등성(Idempotence): 동일한 메시지를 여러 번 처리해도 최종 결과가 동일해야 한다.
  • 데이터베이스 업데이트, 외부 API 호출 등의 작업이 멱등적이어야 한다.

멱등 컨슈머 패턴 구현 방법

  1. 각 메시지에 고유 식별자(idempotency key) 부여
  2. 처리된 메시지 ID를 데이터베이스에 기록
  3. 메시지 처리 전 데이터베이스에서 중복 확인
  4. 중복된 메시지는 오프셋만 커밋하고 처리 건너뛰기

정확히 한 번(Exactly Once) 전달

정확히 한 번 전달 방식은 모든 메시지가 정확히 한 번만 처리되는 이상적인 방식이다.

Kafka에서의 구현

Kafka 0.11 버전부터 도입된 정확히 한 번 의미론은 다음 두 가지 메커니즘을 통해 구현된다:

  1. 멱등 프로듀서(Idempotent Producer):

    • 각 프로듀서에 고유 ID(PID) 할당
    • 각 메시지에 시퀀스 번호 부여
    • 브로커가 중복 메시지 감지 및 제거
  2. 트랜잭션 API:

    • 여러 토픽/파티션에 걸친 원자적 쓰기 작업
    • 컨슈머 오프셋과 프로듀서 쓰기를 하나의 트랜잭션으로 처리

적용 가능한 시나리오

  • Kafka Streams API: 읽기-처리-쓰기 작업을 하나의 트랜잭션으로 처리
  • Kafka → Kafka 파이프라인: 입력 토픽에서 읽고 출력 토픽에 쓰는 작업

외부 시스템과의 통합

Kafka에서 외부 시스템(예: OpenSearch, 데이터베이스)으로 데이터를 전송할 때는 완전한 정확히 한 번 의미론을 달성하기 어렵다. 이 경우 멱등 컨슈머 패턴을 구현해야 한다.

요약

의미론설명
At-least-once메시지 처리 후 오프셋을 커밋. 장애 시 메시지가 중복 처리될 수 있음(기본값).
At-most-once메시지 수신 즉시 오프셋을 커밋. 장애 시 일부 메시지가 유실될 수 있음.
Exactly-once메시지가 정확히 한 번만 처리됨. 트랜잭션 API 등 추가 기능 필요.
  • At-least-once: 메시지를 처리한 뒤 오프셋을 커밋한다. 만약 장애가 발생하면, 커밋 이전에 처리한 메시지를 다시 읽을 수 있으므로 중복 처리가 발생할 수 있다. 이때는 시스템이 멱등(idempotent)해야 한다(같은 메시지를 여러 번 처리해도 결과가 변하지 않아야 함).
  • At-most-once: 메시지를 받자마자 오프셋을 커밋한다. 장애가 발생하면 일부 메시지가 유실될 수 있지만, 중복 처리는 없다.
  • Exactly-once: 메시지를 딱 한 번만 처리한다. Kafka의 트랜잭션 API, 스트림 API 등을 활용해야 하며, 외부 시스템과 연동 시에는 멱등성 처리가 필요하다.

대부분의 실제 애플리케이션에서는 최소 한 번(At-least Once) 전달 방식이 권장된다. 이 방식은 데이터 손실을 방지하면서도 구현이 상대적으로 간단하다. 다만, 멱등적인 처리 로직을 구현하여 중복 처리에 대비해야 한다.

정확히 한 번(Exactly Once) 의미론은 이상적이지만 구현이 복잡하고 성능 오버헤드가 있으므로, 금융 거래와 같이 데이터 정확성이 절대적으로 중요한 경우에만 고려하는 것이 좋다.

어떤 전달 의미론을 선택하든, 시스템의 요구사항과 장애 시나리오를 철저히 이해하고 적절한 오류 처리 메커니즘을 구현하는 것이 중요하다.


Kafka 컨슈머 오프셋 재설정 동작 이해하기

Kafka 컨슈머는 일반적으로 토픽의 로그에서 지속적으로 데이터를 읽어들이지만, 애플리케이션에 버그가 발생하거나 시스템 장애가 발생하면 컨슈머가 일정 기간 동안 중단될 수 있다. 이런 상황에서 컨슈머가 다시 시작될 때 어떤 오프셋부터 읽어야 하는지 결정하는 메커니즘을 이해하는 것이 중요하다.

오프셋 만료와 리텐션 기간

Kafka에는 두 가지 중요한 리텐션(보존) 기간이 있다:

  1. 데이터 리텐션: 토픽에 저장된 실제 메시지가 유지되는 기간
  2. 오프셋 리텐션: 컨슈머 그룹의 커밋된 오프셋 정보가 유지되는 기간

기본적으로 Kafka는 7일의 오프셋 리텐션 기간을 가지고 있다. 이는 컨슈머 그룹이 7일 이상 비활성 상태(즉, 해당 그룹의 컨슈머가 없음)가 되면 해당 그룹의 커밋된 오프셋 정보가 삭제된다는 의미다.

참고: Kafka 2.0 이전 버전에서는 오프셋 리텐션 기간이 단 24시간이었다.

auto.offset.reset 설정의 역할

컨슈머 그룹의 오프셋 정보가 없거나 유효하지 않을 때(만료되었거나 범위를 벗어난 경우), auto.offset.reset 설정이 적용된다. 이 설정은 다음 세 가지 값 중 하나를 가질 수 있다:

  1. latest (기본값): 컨슈머가 토픽의 가장 최신 메시지부터 읽기 시작한다. 즉, 컨슈머가 시작된 시점 이후에 생성된 메시지만 읽는다.

  2. earliest: 컨슈머가 토픽의 가장 처음 메시지부터 읽기 시작한다. 이는 토픽에 있는 모든 메시지(리텐션 기간 내)를 처리하고자 할 때 유용하다.

  3. none: 오프셋 정보가 없거나 유효하지 않을 때 예외를 발생시킨다. 이는 데이터 손실이 발생했을 때 자동으로 처리하지 않고 수동 개입을 원할 때 사용한다.

오프셋 리텐션 기간 조정하기

컨슈머 오프셋 리텐션 기간은 브로커 설정의 offsets.retention.minutes 파라미터로 조정할 수 있다. 많은 프로덕션 환경에서는 이 값을 기본값인 7일보다 더 길게(예: 30일) 설정하는 것이 일반적이다.

# 오프셋 리텐션 기간을 30일(43,200분)로 설정
offsets.retention.minutes=43200

컨슈머 그룹 오프셋 재설정하기

특정 시점부터 데이터를 다시 처리해야 할 경우, 다음과 같은 절차를 따를 수 있다:

  1. 해당 컨슈머 그룹의 모든 컨슈머를 중지한다.
  2. kafka-consumer-groups 명령을 사용하여 원하는 오프셋으로 재설정한다.
  3. 컨슈머를 다시 시작한다.

이 방법을 통해 특정 시점부터 데이터를 다시 처리할 수 있다.

권장 사항

  1. 적절한 오프셋 리텐션 기간 설정: 애플리케이션의 요구사항에 맞게 offsets.retention.minutes 값을 조정해야한다. 장기간 중단될 가능성이 있는 경우 더 긴 기간을 설정하는 것이 좋다.

  2. 적절한 auto.offset.reset 설정: 애플리케이션의 데이터 처리 요구사항에 맞는 값을 선택해야한다.

    • 모든 데이터를 처리해야 하는 경우: earliest
    • 최신 데이터만 처리하면 되는 경우: latest
    • 오프셋 문제 발생 시 수동 개입이 필요한 경우: none
  3. 재처리 메커니즘 이해: 필요할 때 데이터를 다시 처리할 수 있도록 오프셋 재설정 방법을 숙지해야한다.

이러한 설정과 메커니즘을 이해하면 Kafka 컨슈머의 예기치 않은 동작을 방지하고, 데이터 처리의 신뢰성을 높일 수 있다.


실전 예시와 활용

  • 동일한 토픽을 여러 컨슈머 그룹이 독립적으로 읽을 수 있다. 예를 들어, 위치 추적 서비스와 알림 서비스가 각각 독립적으로 데이터를 소비한다.
  • 컨슈머 그룹은 장애 복구, 확장성, 병렬 처리 효율성 등 실시간 데이터 처리 시스템의 핵심 역할을 담당한다.

결론

Kafka 컨슈머 그룹은 대규모 데이터 스트림을 병렬로 처리하고, 장애 복구와 확장성을 보장하는 핵심 구조다.

오프셋 커밋 전략과 메시지 전달 의미론을 이해하고 적절히 활용하면, 신뢰성 높은 실시간 데이터 파이프라인을 구축할 수 있다.

0개의 댓글