Apache Kafka 구성 요소와 동작 원리: 토픽부터 컨슈머 그룹까지

아파치 카프카(Apache Kafka)는 대규모 데이터 스트리밍을 위한 분산형 메시징 플랫폼입니다. 초기에는 LinkedIn의 내부 프로젝트로 시작해 지금은 오픈소스로 여러 기업과 환경에서 쓰이고 있습니다. 이름은 익숙한데 브로커, 파티션, 컨슈머 그룹이 각각 무엇을 맡는지는 헷갈리기 쉽습니다. 아래는 Kafka 4.3 공식 문서에서 확인한 정의와 기본값을 기준으로 구성 요소와 동작 방식을 정리한 내용입니다.
Kafka의 구성 요소: 클러스터, 프로듀서, 컨슈머, 브로커
클러스터는 여러 대의 컴퓨터를 연결해 하나의 시스템처럼 동작하도록 묶은 것입니다. 그래서 카프카는 서버를 더 붙여 처리 능력을 늘리고, 일부 서버가 멈춰도 나머지로 서비스를 이어갑니다. Kafka 공식 문서의 Introduction은 Kafka를 이벤트 스트리밍 플랫폼으로 정의하고, 서버와 클라이언트로 이루어진 분산 시스템이라고 설명합니다. 이 가운데 저장 계층을 이루는 서버가 브로커입니다.
프로듀서는 데이터를 만들어 Kafka로 보냅니다. 로그, 애플리케이션 이벤트, 센서 값처럼 출처가 달라도 같은 토픽으로 흘려보낼 수 있습니다. 컨슈머는 브로커에 쌓인 데이터를 읽어다 저장하거나 가공합니다. 공식 문서는 이 둘이 완전히 분리되어 있으며 서로를 알지 못한다고 못박습니다. 프로듀서는 누가 자기 데이터를 읽는지 모르고, 컨슈머도 누가 썼는지 모릅니다. 서로를 모르기 때문에 뒤에 나올 컨슈머 그룹과 재할당이 성립합니다.
브로커는 프로듀서가 보낸 데이터를 받아 두는 서버입니다. 받은 데이터는 컨슈머가 읽어갈 때까지 저장하고 관리합니다.
토픽과 파티션, 그리고 순서 보장의 범위
토픽은 메시지를 종류별로 묶고, 파티션은 그 토픽을 더 작은 단위로 쪼갭니다. 파티션을 늘리면 그만큼 나눠 처리할 수 있습니다. 공식 문서는 토픽이 서로 다른 브로커에 놓인 여러 개의 버킷으로 흩어진다고 설명합니다. 여기서 제약이 하나 따라 나옵니다. 순서 보장의 단위가 토픽 전체가 아니라 파티션이라는 점입니다. 같은 이벤트 키를 가진 이벤트는 같은 파티션에 기록되고, 특정 토픽-파티션의 컨슈머는 그 파티션의 이벤트를 기록된 순서와 정확히 같은 순서로 읽습니다.
키를 주지 않으면 어느 파티션으로 들어가는지는 프로듀서 설정 문서에 적혀 있습니다. 파티션을 직접 지정하지 않고 키만 있으면 키의 해시로 파티션을 고르고, 파티션도 키도 없으면 sticky partition을 쓰다가 그 파티션에 batch.size 바이트 이상이 쌓이면 다른 파티션으로 바꿉니다. 순서가 걸린 이벤트라면 키를 지정해야 한다는 뜻입니다.
컨슈머가 어디까지 읽었는지는 오프셋으로 관리합니다. Design 문서는 각 파티션에서 컨슈머의 위치가 다음에 소비할 메시지의 오프셋, 즉 정수 하나라고 적습니다. 값이 하나뿐이라 컨슈머가 의도적으로 예전 오프셋으로 되감아 데이터를 다시 소비할 수도 있습니다.
토픽을 만들고 메시지를 주고받는 절차는 빠른 시작 문서에 명령 그대로 나옵니다.
bin/kafka-topics.sh --create --topic quickstart-events --bootstrap-server localhost:9092
bin/kafka-topics.sh --describe --topic quickstart-events --bootstrap-server localhost:9092
bin/kafka-console-producer.sh --topic quickstart-events --bootstrap-server localhost:9092
bin/kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092
컨슈머 그룹, 큐와 발행-구독을 한 추상화로
발행-구독 모델에서는 보내는 쪽이 수신자를 지정하지 않습니다. 발행자는 토픽에 쓰고, 구독자는 그 토픽을 읽습니다. 양쪽이 서로를 모르기 때문에 한쪽을 늘리거나 줄여도 다른 쪽 코드를 건드리지 않습니다.
실제 배분을 맡는 것은 컨슈머 그룹입니다. KafkaConsumer 문서는 구독한 토픽의 각 메시지를 컨슈머 그룹마다 한 프로세스에 전달하며, 그룹의 모든 멤버 사이에 파티션을 배분해 각 파티션이 그룹 안에서 정확히 하나의 컨슈머에 할당되도록 한다고 설명합니다. 프로세스가 죽으면 그 프로세스에 할당돼 있던 파티션이 같은 그룹의 다른 컨슈머로 재할당되고, 새 컨슈머가 그룹에 들어오면 기존 컨슈머의 파티션 일부가 그쪽으로 옮겨갑니다.
그래서 그룹을 어떻게 묶느냐에 따라 메시징 모델이 두 가지로 나뉩니다.
- 모든 프로세스가 한 그룹: 전통적인 큐처럼 그룹 안에서 분배
- 프로세스마다 각자의 그룹: pub-sub처럼 각 프로세스가 토픽의 모든 레코드를 구독
각 파티션이 그룹 안에서 한 컨슈머에만 할당되므로, 컨슈머를 파티션 수보다 많이 띄우면 남는 컨슈머는 아무것도 받지 못하고 놉니다. 그룹의 병렬 처리 폭은 파티션 개수가 정합니다.
복제와 ISR, 그리고 메시지 보존 기간
지속성은 메시지를 디스크에 남겨, 브로커가 재시작해도 데이터가 사라지지 않게 하는 성질입니다. Design 문서는 모든 데이터가 즉시 파일시스템의 영속 로그에 기록되며 반드시 디스크로 flush되지는 않는다고, 사실상 커널 페이지캐시로 넘어가는 것이라고 설명합니다. 또한 모든 연산이 O(1)이고 읽기가 쓰기를 막지 않아, 데이터가 아무리 쌓여도 성능이 떨어지지 않는다고 적습니다. 그래서 Kafka는 메시지를 소비 즉시 지우지 않고 정해진 기간 동안 그대로 둡니다.
장애 대비는 복제로 합니다. Kafka의 각 파티션에는 리더 하나와 팔로워 0개 이상이 있고, 쓰기는 전부 리더로 갑니다. 리더는 in sync 상태인 복제본 집합, 즉 ISR을 추적합니다. Design 문서는 이 ISR 모델에서 f+1개의 복제본이 있으면 커밋된 메시지를 잃지 않고 f개의 장애를 견딜 수 있다고 정리합니다. Introduction 문서는 운영 환경에서 흔한 설정으로 복제 팩터 3을 듭니다.
기본값은 생각보다 낮습니다. 브로커 설정 문서 기준으로 default.replication.factor가 1이고 min.insync.replicas도 1입니다. min.insync.replicas는 프로듀서가 acks를 all로 둘 때 쓰기가 성공하려면 필요한 최소 in-sync 복제본 수를 뜻하며, 리더를 포함해 셉니다. 보존 기간 쪽은 log.retention.hours가 168이고, 문서는 이 값이 log.retention.ms와 log.retention.minutes가 설정되지 않았을 때에만 쓰이는 세 번째 우선순위 설정이라고 적습니다. log.retention.ms의 기본값은 null이며 -1로 두면 시간 제한이 사라집니다.
처리량과 중복을 가르는 프로듀서 설정
처리량과 중복은 저절로 정해지지 않고, 프로듀서 쪽 설정이 결과를 상당 부분 결정합니다.
프로듀서 설정 문서에서 확인한 기본값은 다음과 같습니다.
acks=all
enable.idempotence=true
batch.size=16384
linger.ms=5
compression.type=none
acks는 요청을 완료로 보기 전에 리더가 받아야 하는 확인 응답 수이고, 레코드의 내구성을 조절합니다. enable.idempotence가 true면 각 메시지의 사본이 스트림에 정확히 하나만 기록됩니다. batch.size는 같은 파티션으로 가는 레코드를 묶는 배치 크기이고, linger.ms는 그 배치를 채우려고 일부러 두는 지연입니다. compression.type은 기본이 none이며 none, gzip, snappy, lz4, zstd 중에서 고릅니다.
전달 보장은 Design 문서에 세 가지로 정리돼 있습니다. at most once는 메시지가 유실될 수 있지만 재전송되지 않고, at least once는 유실되지 않지만 재전송될 수 있으며, exactly once는 각 메시지가 정확히 한 번만 처리됩니다. exactly once는 컨슈머의 위치가 내부 토픽에 메시지로 저장된다는 점을 이용해, 출력 토픽과 같은 트랜잭션 안에서 오프셋을 함께 기록하는 방식으로 구현합니다.
Kafka가 실제로 쓰이는 자리
공식 문서의 Use cases 페이지는 쓰임새를 일곱 가지로 나눕니다.
- 메시징: 전통적인 메시지 브로커의 대체
- 웹사이트 활동 추적: 페이지뷰나 검색 같은 사이트 활동을 중앙 토픽으로 발행
- 지표: 분산 애플리케이션의 운영 통계를 중앙 피드로 집계
- 로그 수집: 파일 단위 대신 로그와 이벤트를 메시지 스트림으로 추상화
- 스트림 처리: 원본 토픽을 가공해 새 토픽으로 내보내는 파이프라인
- 이벤트 소싱: 상태 변경을 시간순 레코드로 기록
- 커밋 로그: 노드 간 데이터 복제와 재동기화 수단
Introduction 문서가 드는 사례도 겹치는 부분이 많습니다. 실시간 결제와 금융 거래 처리, 차량과 화물 추적, IoT 센서 데이터 분석, 의료 환자 모니터링, 부서 간 데이터 연동 등이 나열돼 있습니다. 어느 쪽이든 공통점은 하나입니다. 데이터가 한 번에 몰려 오는 것이 아니라 계속 흘러 들어오고, 그것을 여러 소비자가 각자의 속도로 읽어야 한다는 점입니다.
Kafka Design 문서로그 저장과 pull 모델, ISR을 그렇게 만든 이유 Kafka Introduction이벤트, 토픽, 파티션의 공식 정의
무엇을 먼저 정할까
이 글의 내용만으로 정할 수 있는 건 두 가지입니다. 순서가 중요하면 파티션 수보다 어떤 키를 쓸지 먼저 정하고, 같은 데이터를 여러 용도로 읽어야 하면 컨슈머 그룹을 용도별로 나눕니다. 다만 default.replication.factor 기본값 1은 브로커 한 대를 잃으면 되돌릴 방법이 없다는 뜻이라, 지금 쓰는 클러스터가 이 값을 그대로 두고 있는지부터 봐야 합니다.
보관도 같습니다. log.retention.hours가 168이어도 log.retention.ms가 걸려 있으면 그쪽이 이겨서, 남아 있을 줄 알았던 데이터가 이미 없을 수 있습니다.
출처: Apache Kafka Introduction, Design, Use cases, Quickstart, Broker Configs, Producer Configs, KafkaConsumer javadoc