이 문서는 Kafka를 사용했다는 수준에서 벗어나 파티션 키, 리밸런스, 컨슈머 지연을 운영 판단으로 설명하기 위해 만들었다. 학습 목표는 순서·처리량·복구 시간의 관계를 이해하고, 장애를 재현해 지표와 로그로 원인을 구분하는 것이다. 완료 기준은 로컬 클러스터에서 파티션 쏠림, 느린 컨슈머, 리밸런스를 각각 재현하고 대응 전후를 측정하는 것이다.
Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프를 먼저 읽으면 좋다. 이 글은 개념을 반복하지 않고 운영 진단과 장애 실습에 집중한다.
Kafka의 파티션은 병렬 처리 단위이면서 순서 보장 경계다. 프로듀서가 같은 키를 사용하면 같은 토픽 안에서 같은 파티션으로 라우팅된다. 따라서 키를 잘못 고르면 순서가 깨지거나 특정 파티션에 부하가 몰린다.
금액 이동 이벤트라면 거래 식별자보다 계좌 식별자가 순서 보장에 더 적합할 수 있다. 같은 계좌의 출금과 취소가 서로 다른 파티션에 들어가면 여러 컨슈머가 역순으로 처리할 수 있기 때문이다. 반대로 전체 고객 식별자를 키로 쓰면 활동량이 큰 고객 하나가 핫 파티션을 만들 수 있다.
키 선택 전에 다음 질문을 적는다.
한 컨슈머 그룹에서 한 파티션은 한 시점에 한 컨슈머에게만 배정된다. 파티션이 12개인데 컨슈머가 20개면 최소 8개는 놀게 된다. 하지만 파티션 수를 무작정 늘리면 브로커 메타데이터, 파일 핸들, 복제 트래픽, 장애 복구 비용도 증가한다.
처리량 기반의 거친 계산은 다음과 같다.
필요 파티션 수 = ceil(목표 초당 메시지 수 / 단일 파티션에서 검증한 초당 처리량)이 계산에는 다음 여유를 더한다.
계산값은 정답이 아니라 부하 시험의 시작점이다.
컨슈머 그룹 구성이나 구독 대상이 바뀌면 파티션을 다시 배정한다.
프로세스 배포, 컨슈머 장애, 세션 타임아웃, poll() 지연, 파티션 추가가 대표 원인이다.
리밸런스 중에는 파티션 소유권이 이동한다. 처리 중이던 레코드와 커밋된 오프셋의 경계가 어긋나면 중복 처리가 발생할 수 있다. 그래서 리밸런스는 단순한 로그 소음이 아니라 처리 정지 시간과 중복 가능성을 함께 봐야 하는 사건이다.
Apache Kafka 4.0부터 제공되는 새 consumer rebalance protocol은 완전 증분 방식으로 전역 동기화 장벽을 줄인다.
다만 클라이언트에서 자동 활성화되는 것이 아니며 group.protocol=consumer 설정과 호환성 검토가 필요하다.
기존 classic protocol을 사용하는 환경에서는 협력적 할당과 static membership이 배포 중 파티션 이동을 줄이는 선택지가 될 수 있다.
classic consumer에서 max.poll.interval.ms 안에 다음 poll()이 호출되지 않으면 컨슈머가 정상 처리 중이어도 실패한 것으로 판단될 수 있다.
레코드 한 묶음 처리 시간이 이 값을 넘으면 리밸런스가 반복된다.
대응은 타임아웃만 크게 늘리는 것이 아니다.
max.poll.records를 줄여 한 번에 가져오는 작업량을 제한한다.spring:
kafka:
consumer:
enable-auto-commit: false
max-poll-records: 100
properties:
max.poll.interval.ms: 300000설정값은 예시일 뿐이다. 처리 시간의 p99와 장애 복구 목표를 측정한 뒤 정해야 한다.
컨슈머 지연(lag)은 파티션의 최신 오프셋과 컨슈머 그룹의 커밋 오프셋 차이다. lag가 늘었다는 사실만으로 원인을 알 수 없다.
원인 후보는 다음과 같다.
lag를 볼 때는 합계만 보지 않는다. 파티션별 lag, 증가 속도, 가장 오래된 메시지의 나이, 처리 성공률을 함께 본다.
kafka-consumer-groups.sh \
--bootstrap-server localhost:9092 \
--group transfer-worker \
--describe한 파티션만 lag가 증가하면 컨슈머 수 증설보다 키 분포와 해당 파티션의 메시지 특성을 먼저 확인한다.
재시도는 일시적 실패에만 의미가 있다. 검증 오류나 지원하지 않는 스키마를 같은 입력으로 반복 처리하면 lag만 키운다.
오류를 세 종류로 분류한다.
짧은 재시도는 컨슈머 안에서 수행할 수 있다. 긴 재시도는 retry topic으로 보내 원본 파티션을 막지 않는다. DLQ에는 원본 토픽, 파티션, 오프셋, 예외 유형, 재시도 횟수, 추적 식별자를 보존한다.
DLQ는 쓰레기통이 아니다. 유입률, 체류 시간, 재처리 결과, 반복 실패 원인을 운영 지표로 관리한다.
lag 경보를 받았을 때 운영자가 즉흥적으로 오프셋을 이동하면 원인과 복구 범위가 더 불명확해진다. 먼저 영향 범위를 고정하고, 원인을 분류하고, 처리량을 복구한 뒤, 누락과 중복을 검증하는 순서를 따른다.
첫 단계에서는 토픽 전체가 아니라 어느 컨슈머 그룹과 파티션이 밀리는지 확인한다. 프로듀서 유입률이 평소와 같은지, 최근 배포와 컨슈머 수 변화가 있었는지 함께 본다. 특정 파티션만 밀리면 키 쏠림이나 독성 메시지를 의심한다. 모든 파티션이 비슷하게 밀리면 DB와 외부 API, CPU, GC처럼 공통 의존성을 확인한다.
두 번째 단계에서는 처리량 복구 조치가 데이터 의미를 바꾸는지 검토한다. 컨슈머 증설은 파티션 수 이내에서만 병렬성을 높인다. 문제 메시지를 건너뛰면 서비스는 살아나지만 해당 거래의 처리가 누락될 수 있다. 오프셋을 앞으로 옮기는 조치는 대상 범위와 재처리 계획을 승인받은 뒤 수행해야 한다.
세 번째 단계에서는 장애 구간의 시작 오프셋과 종료 오프셋을 기록한다. 복구 뒤 업무 DB의 처리 건수와 Kafka 소비 결과를 비교해 중복과 누락을 찾는다. 금융 이벤트라면 단순 소비 성공률뿐 아니라 원장 반영과 대사 결과까지 확인한다.
운영 기록에는 다음 근거를 남긴다.
Kafka의 idempotent producer는 프로듀서 재시도로 같은 레코드가 브로커 로그에 중복 기록되는 문제를 줄인다. Kafka transaction은 Kafka 안의 읽기·처리·쓰기와 오프셋 커밋을 하나의 트랜잭션으로 묶을 수 있다.
하지만 컨슈머가 MySQL을 갱신하고 외부 API를 호출하는 순간 Kafka transaction만으로 전체 작업이 정확히 한 번 처리되지는 않는다. DB 고유 제약, Inbox, 멱등한 상태 전이, Outbox 같은 애플리케이션 설계가 여전히 필요하다.
관련 경계는 Spring Kafka 리스너의 오프셋 커밋과 트랜잭션 경계에서 확인한다.
핫 파티션이나 느린 DB가 원인이면 컨슈머를 늘려도 효과가 없다. 파티션별 lag와 처리 시간 분해를 먼저 수행한다.
장애 감지 시간까지 같이 늘어나 복구가 늦어진다. 처리 묶음 크기와 외부 IO 경계를 먼저 줄이고 타임아웃을 조정한다.
영구 오류 한 건이 파티션 전체를 막는다. 오류 분류, 제한된 재시도, 격리, 승인된 재처리 흐름을 만든다.
처리 실패 뒤 메시지를 다시 읽지 못해 유실로 이어진다. 업무 반영 완료 뒤 커밋하되 중복 가능성을 멱등성으로 흡수한다.
기존 Kafka 로컬 환경을 띄운 뒤 payments 토픽을 파티션 6개로 만든다.
kafka-topics.sh \
--bootstrap-server localhost:9092 \
--create \
--topic payments \
--partitions 6 \
--replication-factor 1세 가지 실험을 수행한다.
전체 메시지의 대부분에 같은 키를 사용한다. 파티션별 레코드 수와 lag를 비교한다. 그다음 분산 가능한 업무 키로 바꾸고 분포를 다시 측정한다.
특정 메시지에서 DB 호출 대신 의도적인 지연을 넣는다.
max.poll.records와 처리 시간을 바꾸며 리밸런스 발생 횟수를 기록한다.
컨슈머 세 개를 실행한 뒤 하나씩 종료하고 재시작한다. 파티션 이동, 처리 중단 시간, 중복 처리 건수를 로그로 남긴다. 가능하면 classic과 새 consumer protocol을 각각 실험하되 같은 클러스터에서 지원 여부를 먼저 확인한다.
파티션 키는 같은 업무 개체의 순서를 보장하는 최소 범위로 정하고, 분포 쏠림을 함께 검증합니다. lag는 원인이 아니라 결과이므로 파티션별 lag, 메시지 나이, 처리 시간, 리밸런스 빈도를 같이 봅니다. 리밸런스가 반복되면 poll 주기와 처리 묶음, 외부 IO 지연을 확인하고 설정만 늘리지 않습니다. 전달 보장은 보통 at-least-once를 전제로 두고 DB 고유 제약과 멱등한 컨슈머로 중복을 흡수합니다. Kafka transaction의 exactly-once 범위가 외부 DB나 API까지 자동으로 확장되지는 않는다고 구분합니다.
max.poll.interval.ms와 처리 시간의 관계를 설명할 수 있다.