본문 바로가기

Server/Kafka

[Kafka] Consumer Lag이 쌓였을 때 먼저 봐야 할 것들

반응형

Kafka를 운영하다 보면 consumer lag이 쌓이는 상황을 자주 마주합니다. 이때 가장 위험한 대응은 lag 수치만 보고 바로 컨슈머를 늘리거나 파티션을 늘리는 것입니다. Consumer lag은 원인이 아니라 결과 지표입니다.


lag이 쌓였다는 것은 컨슈머 그룹이 현재 토픽에 쌓이는 메시지를 처리 속도만큼 따라가지 못하고 있다는 뜻이며 원인은 다양합니다. 따라서 Kafka consumer lag을 해결할 때는 먼저 원인을 분리해야 합니다.

  • 유입량이 갑자기 늘었을 수 있습니다.
  • 컨슈머 처리 로직이 느려졌을 수 있습니다.
  • DB나 외부 API가 병목일 수 있습니다.
  • 특정 key로 메시지가 몰려 핫파티션이 생겼을 수 있습니다.
  • 리밸런스가 반복되면서 컨슈머가 안정적으로 메시지를 읽지 못하고 있을 수도 있습니다.

이 글에서는 consumer lag의 의미를 정리하고, lag이 쌓였을 때 어떤 순서로 확인하고 대응해야 하는지 실무 관점에서 정리합니다.

Consumer Lag이란 무엇인가

Consumer lag은 Consumer Group이 특정 Partition에서 로그의 끝을 얼마나 따라가지 못하고 있는지를 나타내는 지표입니다. 일반적으로 다음과 같이 계산합니다.

consumer lag = log end offset - committed offset

 

여기서 log end offset은 Partition 로그의 끝 위치입니다. 즉, 해당 Partition에 다음 메시지가 기록된다면 부여될 offset입니다. 예를 들어 Partition에 offset 0부터 9,999까지 메시지가 기록되어 있다면, log end offset10,000입니다.

 

committed offset은 Consumer Group이 Kafka에 저장해 둔 다음 읽을 위치입니다. 예를 들어 Consumer가 offset 0부터 7,999까지 처리한 뒤 commit했다면, committed offset은 8,000입니다. 즉, committed offset은 “마지막으로 처리한 offset”이 아니라 “재시작하거나 리밸런싱되었을 때 다시 읽기 시작할 offset”입니다.

 

예를 들어 어떤 Partition의 상태가 다음과 같다고 해보겠습니다.

consumer lag(2,000) = log end offset(10,000) - committed offset(8,000)

이 경우 Consumer Group은 로그의 끝보다 offset 기준으로 2,000만큼 뒤처져 있다고 볼 수 있습니다. 흔히 “아직 처리하지 못한 메시지가 2,000개 남아 있다”고 표현하지만, 더 정확히는 “아직 처리 완료로 commit하지 못한 offset 차이가 2,000이다”라고 보는 것이 좋습니다.

 

이 차이가 중요한 이유는 Consumer의 commit 전략 때문입니다. Consumer가 실제 비즈니스 처리를 끝내기 전에 offset을 먼저 commit하면 lag은 작게 보일 수 있지만, 실제 처리는 아직 끝나지 않았을 수 있습니다. 반대로 비즈니스 처리는 끝났지만 offset commit이 늦어지면 lag은 크게 보일 수 있습니다.

 

또한 lag은 단순히 offset 차이이기 때문에, 같은 lag 값이라도 운영상 의미는 다를 수 있습니다. 메시지 하나를 처리하는 데 1ms가 걸리는 Consumer와, 메시지 하나마다 외부 API를 호출해 300ms가 걸리는 Consumer는 같은 lag 2,000이어도 장애 위험도와 복구 시간은 전혀 다릅니다.

 

따라서 Consumer lag은 단독으로 판단하기보다 처리량, 처리 시간, 에러율, 재시도 횟수, commit 전략과 함께 봐야 합니다. Lag은 Consumer가 Kafka 로그를 얼마나 따라가고 있는지 확인하는 출발점이지, 그 자체만으로 Consumer가 정상인지 비정상인지를 확정하는 지표는 아닙니다.

  • log end offset = Partition 로그의 끝 위치, 다음 메시지가 기록될 offset 
  • committed offset = Consumer Group이 Kafka에 저장한 다음 읽을 offset 
  • consumer lag = 로그 끝과 commit된 위치 사이의 offset 차이

Lag이 쌓인다는 것은 무엇을 의미할까

Consumer lag이 증가한다는 것은 기본적으로 다음 상태입니다.

메시지 유입 속도 > 컨슈머 처리 속도

 

즉, producer가 Kafka에 메시지를 넣는 속도보다 consumer group이 메시지를 처리하고 offset을 commit하는 속도가 느린 상태입니다. 이 상태가 잠깐 발생하는 것은 자연스럽습니다. 프로모션, 배치 작업, 장애 복구 이후의 재유입처럼 특정 시간대에 메시지가 몰리면 lag은 일시적으로 증가할 수 있습니다. 문제는 lag이 계속 증가하는 경우입니다. 이때는 시스템이 현재 유입량을 처리하지 못하고 있다는 뜻입니다. 시간이 지나도 줄어들지 않는 lag은 반드시 원인을 분리해야 합니다.

첫 번째로 볼 것: 파티션별 Lag 분포

Lag이 쌓였을 때 가장 먼저 볼 지표는 전체 lag이 아니라 파티션별 lag 분포입니다. 전체 lag만 보면 원인을 잘못 판단하기 쉽습니다.
예를 들어 전체 lag이 2,000이라고 해도 모든 파티션에 고르게 쌓였는지, 특정 파티션 하나에만 몰렸는지에 따라 대응이 완전히 달라집니다.

모든 파티션이 고르게 밀리는 경우

모든 파티션의 lag이 비슷하게 증가한다면 전체 처리량이 부족한 상태입니다. 이 경우 원인은 보통 세 가지입니다.

  1. producer 유입량 증가
  2. consumer 인스턴스 수나 concurrency가 부족
  3. DB, 외부 API, CPU 같은 공통 병목 때문에 각 컨슈머의 처리 속도 저하

이때는 컨슈머를 늘리기 전에 현재 컨슈머가 실제로 처리 병목에 걸려 있는지 확인해야 합니다.

확인할 지표는 다음과 같습니다.

- 초당 유입 메시지 수
- 초당 처리 메시지 수
- 레코드 처리 시간 p95, p99
- 처리 실패율
- retry 횟수
- DB connection pool 사용률
- DB slow query
- 외부 API latency
- CPU, GC, memory

 

컨슈머 애플리케이션이 여유가 있고 단순히 인스턴스 수가 부족하다면 스케일아웃이 효과적입니다. 반대로 DB connection pool이 이미 가득 차 있거나 외부 API timeout이 증가하고 있다면 컨슈머를 늘리는 것은 장애를 키웁니다.

특정 파티션만 밀리는 경우

특정 파티션 하나 또는 일부 파티션에만 lag이 몰린다면 핫파티션을 의심해야 합니다. Kafka는 같은 consumer group 안에서 하나의 파티션을 동시에 여러 컨슈머가 나눠 읽지 않습니다. 따라서 특정 파티션에 메시지가 몰리면 그 파티션을 맡은 컨슈머 하나가 병목이 됩니다.

 

예를 들어 12개 파티션 중 1번 파티션에만 lag이 계속 증가한다면 컨슈머를 12개까지 늘려도 1번 파티션의 처리 병목은 그대로 남습니다.

핫파티션의 대표 원인은 key 쏠림입니다.

key = centerId
key = userId
key = productId
key = sellerId

위와 같은 key를 기준으로 메시지를 발행할 때 특정 센터, 특정 사용자, 특정 상품에 이벤트가 몰리면 해당 key가 매핑된 파티션에 메시지가 집중됩니다. 이 경우 단순한 컨슈머 스케일아웃으로 해결되지 않습니다. 핫파티션은 key 설계 문제로 봐야 합니다.

두 번째로 볼 것: 현재 병렬성의 상한

Kafka consumer group의 병렬성은 파티션 수를 넘을 수 없습니다. 파티션이 10개라면 같은 consumer group에서 동시에 의미 있게 처리할 수 있는 최대 병렬성은 10입니다. 컨슈머 인스턴스를 20개로 늘려도 10개만 파티션을 할당받고, 나머지 컨슈머는 처리할 파티션이 없습니다. 정리하면 다음과 같습니다.

컨슈머 수 < 파티션 수
→ 한 컨슈머가 여러 파티션을 처리한다.

컨슈머 수 = 파티션 수
→ 각 컨슈머가 대체로 하나의 파티션을 처리한다.

컨슈머 수 > 파티션 수
→ 일부 컨슈머는 idle 상태가 된다.

 

여기서 자주 나오는 오해가 있습니다. 컨슈머 수가 파티션 수보다 적다고 해서 일부 파티션이 처리되지 않는 것은 아닙니다. Kafka는 파티션을 컨슈머들에게 분배합니다. 컨슈머가 적으면 한 컨슈머가 여러 파티션을 맡습니다. 문제는 컨슈머 하나가 여러 파티션을 처리해야 하므로 전체 처리량이 부족해질 수 있다는 점입니다. Spring Kafka를 사용한다면 병렬성은 보통 다음처럼 계산합니다.

실제 consumer thread 수 = Pod 수 × concurrency
실제 병렬성 상한 = min(파티션 수, Pod 수 × concurrency)

 

예를 들어 파티션이 12개이고, Pod가 3개이며, 각 Pod의 concurrency가 2라면 실제 consumer thread 수는 6개입니다.

Pod 수 = 3
concurrency = 2
consumer thread 수 = 6
partition 수 = 12

실제 병렬성 = min(12, 6) = 6

 

이 경우 컨슈머를 더 늘리면 처리량이 증가할 여지가 있습니다.

반대로 파티션이 12개이고, Pod가 6개이며, 각 Pod의 concurrency가 3이라면 consumer thread 수는 18개입니다.

Pod 수 = 6
concurrency = 3
consumer thread 수 = 18
partition 수 = 12

실제 병렬성 = min(12, 18) = 12

이 상태에서는 이미 파티션 수가 병렬성의 상한입니다. 컨슈머를 더 늘려도 idle consumer만 늘어납니다.

세 번째로 볼 것: 컨슈머가 느린 이유

파티션과 컨슈머 수가 충분한데도 lag이 줄지 않는다면, 이제 컨슈머 처리 로직을 봐야 합니다. Kafka consumer는 메시지를 읽는 것보다 메시지를 처리하는 데 더 많은 시간을 쓰는 경우가 많습니다. 대표적인 병목은 다음과 같습니다.

DB 병목

컨슈머가 메시지마다 DB insert, update, select를 수행한다면 DB가 가장 먼저 병목이 됩니다. 

특히 다음 상황에서 lag이 급격히 증가합니다.

  • 인덱스를 타지 않는 쿼리
  • 대량 update로 인한 lock 경합
  • connection pool 고갈
  • 트랜잭션 범위 과다
  • N+1 query
  • batch job과 consumer 처리 시간대 충돌

이 경우 컨슈머를 늘리면 DB 요청이 더 증가합니다. 결과적으로 DB latency가 더 커지고, timeout이 늘고, retry가 증가하면서 lag이 더 쌓입니다. DB가 병목일 때는 컨슈머 스케일아웃보다 처리 로직 개선이 먼저입니다.

  • bulk insert/update
  • 불필요한 select 제거
  • 중복 조회 캐싱
  • 인덱스 보완
  • 트랜잭션 범위 축소
  • 쓰기 모델 분리

외부 API 병목

컨슈머가 외부 API를 호출한다면 외부 시스템의 지연이 곧 consumer lag으로 이어집니다. 외부 API timeout이 늘어나면 consumer thread가 오래 점유됩니다. 그동안 다음 poll이 늦어지고, 처리량은 감소합니다. 이 경우 확인해야 할 지표는 다음입니다.

  • 외부 API latency p95, p99
  • timeout 비율
  • HTTP 5xx 비율
  • retry 횟수
  • circuit breaker 상태
  • thread pool queue

외부 API 장애 상황에서 무제한 retry를 수행하면 컨슈머가 장애 전파 지점이 됩니다.
이때는 timeout을 짧게 가져가고, retry 횟수를 제한하고, 실패 메시지를 retry topic이나 DLQ로 분리해야 합니다.

CPU와 GC 병목

메시지 처리 과정에서 JSON 역직렬화, 암복호화, 대량 연산, 큰 객체 생성이 많다면 CPU와 GC가 병목이 됩니다. 이 경우 DB나 외부 API 지표는 정상인데 consumer 처리 시간이 증가합니다. 확인할 지표는 다음입니다.

  • CPU 사용률
  • GC pause time
  • heap 사용량
  • object allocation rate
  • consumer 처리 시간

배포 이후 lag이 증가했다면 최근 코드 변경으로 객체 생성량이나 처리 비용이 증가했는지 확인해야 합니다.

네 번째로 볼 것: Rebalance가 반복되는지

Consumer lag이 갑자기 쌓일 때 리밸런스도 반드시 확인해야 합니다. Kafka consumer group은 멤버가 추가되거나 제거되면 파티션 할당을 다시 조정합니다. 이 과정을 rebalance라고 합니다. 리밸런스 자체는 정상 동작입니다. 문제는 리밸런스가 자주 발생하는 경우입니다. 리밸런스가 반복되면 컨슈머는 안정적으로 메시지를 처리하지 못합니다. 파티션이 회수되고 다시 할당되는 과정에서 처리 흐름이 끊기고, offset commit 타이밍에 따라 재처리도 발생합니다. 대표적인 원인은 다음입니다.

- 컨슈머 애플리케이션의 잦은 재시작
- readiness/liveness probe 설정 오류
- 긴 처리 시간으로 인한 max.poll.interval.ms 초과
- GC pause
- 네트워크 불안정
- 배포 과정에서 다수 인스턴스 동시 재시작

 

특히 max.poll.interval.ms는 중요합니다. 컨슈머가 정해진 시간 안에 다시 poll()을 호출하지 못하면 Kafka는 해당 컨슈머가 정상적으로 동작하지 않는다고 판단하고 리밸런스를 발생시킵니다. 처리 시간이 긴 consumer라면 다음을 함께 조정해야 합니다.

  • max.poll.records를 줄여 한 번에 처리할 레코드 수를 낮춘다.
  • max.poll.interval.ms를 처리 시간에 맞게 조정한다.
  • 오래 걸리는 작업을 별도 worker로 분리한다.
  • offset commit 시점을 명확히 한다.

다만 max.poll.interval.ms를 무작정 늘리는 것은 좋은 해결책이 아닙니다. 장애 난 consumer를 늦게 감지하게 만들기 때문입니다. 처리 시간이 길다면 consumer loop 자체를 가볍게 유지하고, 무거운 작업은 별도 실행 모델로 분리하는 것이 더 안정적입니다.


Consumer 설정으로 처리량을 올릴 때 주의할 점

Lag이 쌓였을 때 consumer 설정을 조정하는 것도 방법입니다. 하지만 설정 튜닝은 병목 원인을 확인한 뒤 적용해야 합니다.

max.poll.records

max.poll.records는 한 번의 poll() 호출에서 애플리케이션 코드로 반환되는 최대 레코드 수입니다. 기본값은 500입니다.

이 값을 늘리면 한 번에 더 많은 메시지를 처리할 수 있습니다. 하지만 메시지 처리 시간이 길다면 오히려 다음 poll()이 늦어지고 max.poll.interval.ms를 초과할 수 있습니다. 반대로 이 값을 줄이면 한 번에 처리하는 레코드 수가 줄어 안정성은 좋아질 수 있지만, 전체 처리량은 낮아질 수 있습니다. 따라서 max.poll.records는 처리 시간과 함께 봐야 합니다.

한 레코드 처리 시간 × max.poll.records < max.poll.interval.ms

 

이 관계를 만족하지 못하면 리밸런스가 발생합니다.

fetch.min.bytes, fetch.max.wait.ms

fetch.min.bytes fetch.max.wait.ms는 consumer가 broker에서 데이터를 가져올 때의 batch 성격에 영향을 줍니다. 처리량 중심이면 더 많은 데이터를 모아 가져오도록 조정할 수 있습니다. 낮은 지연 시간이 중요하면 오래 기다리지 않도록 조정해야 합니다. 다만 이 설정들은 애플리케이션 처리 병목을 해결하지 않습니다. DB나 외부 API가 느린 상태에서 fetch 설정만 바꿔도 lag은 줄지 않습니다.


파티션을 늘리면 해결될까

파티션을 늘리면 consumer group의 병렬성 상한이 증가합니다. 따라서 현재 병목이 “파티션 수 부족으로 인한 병렬성 한계”라면 파티션 증설은 효과가 있습니다. 하지만 파티션 증설은 신중하게 해야 합니다.

  1. 파티션은 줄이기 어렵습니다.
    운영 중 파티션을 늘리는 것은 가능하지만 줄이는 것은 일반적인 운영 명령으로 지원되지 않습니다. 처음부터 목표 처리량과 성장 여지를 보고 설계해야 합니다.
  2. key 기반 메시지 순서 보장에 영향을 줄 수 있습니다.
    Kafka는 key를 기준으로 파티션을 결정합니다. 일반적인 전략에서는 key hash와 파티션 수를 기준으로 대상 파티션이 정해집니다.파티션 수가 바뀌면 같은 key라도 이후 메시지가 기존과 다른 파티션으로 갈 수 있습니다. 기존 메시지는 자동으로 새 파티션에 재분배되지 않습니다.

즉, 파티션 증설 전에는 다음 질문에 답해야 합니다.

  • key 단위 순서 보장이 필요한가?
  • 같은 key의 과거 메시지와 신규 메시지가 다른 파티션에 있어도 되는가?
  • 파티션 증설 후 consumer 처리 순서가 비즈니스적으로 안전한가?
  • 파티션을 늘린 뒤 broker, file handle, replication 비용을 감당할 수 있는가?

순서가 중요한 토픽이라면 파티션 증설보다 key 설계 변경이나 토픽 분리를 먼저 검토해야 합니다.


핫파티션은 어떻게 해결할까

핫파티션은 단순히 컨슈머를 늘려서는 해결되지 않습니다. 핫파티션을 해결하려면 메시지가 특정 파티션에 몰리는 구조를 바꿔야 합니다.

1. Key salting

가장 직접적인 방법은 key에 salt를 붙여 분산하는 것입니다.

기존 key
center-1

변경 key
center-1#0
center-1#1
center-1#2
center-1#3

이렇게 하면 하나의 logical key를 여러 physical key로 나눠 여러 파티션에 분산시킬 수 있습니다. 단점도 명확합니다. 기존에 보장하던 “같은 key 내 순서”가 깨집니다. 따라서 key salting은 순서 보장이 필요 없는 처리이거나, 순서 보장 범위를 더 작게 나눌 수 있을 때만 사용해야 합니다.

2. 핫키 전용 토픽 분리

특정 key가 반복적으로 문제를 만든다면 해당 key를 별도 토픽으로 분리할 수 있습니다. 예를 들어 일반 이벤트는 기존 토픽으로 보내고, 대량 이벤트가 발생하는 센터나 상품은 별도 토픽으로 분리합니다.

normal-order-event-topic
hot-center-order-event-topic

 

이 방식은 본선 처리를 보호하는 데 효과적입니다. 핫키가 전체 consumer group을 막는 상황을 줄일 수 있습니다.

3. 2단계 토픽 구조

순서가 필요한 구간과 대량 병렬 처리가 필요한 구간을 분리하는 방법도 있습니다. 1차 토픽에서는 key 기준으로 정합성과 순서를 유지합니다. 이후 라우터 consumer가 메시지를 읽어 2차 토픽으로 재분배합니다. 2차 토픽에서는 순서 요구를 낮추고 병렬 처리량을 높입니다.

원본 이벤트 토픽
→ 라우터 consumer
→ 병렬 처리용 토픽
→ worker consumer group

 

이 구조는 복잡도가 증가하지만, 정합성 구간과 대량 처리 구간을 분리할 수 있습니다.

Lag이 너무 많이 쌓이면 생기는 문제

Lag이 쌓였다고 해서 즉시 장애는 아닙니다. Kafka는 consumer가 뒤처져도 나중에 따라잡을 수 있도록 설계된 시스템입니다. 하지만 lag이 오래 지속되면 운영 리스크가 커집니다.

처리 지연으로 인한 비즈니스 문제

이벤트는 시간이 중요합니다. 알림 이벤트가 몇 시간 뒤 처리되면 사용자 경험이 깨집니다. 재고 이벤트가 늦게 처리되면 판매 가능 수량이 실제와 달라집니다. 쿠폰, 정산, 상태 동기화처럼 시점이 중요한 이벤트는 늦게 처리될수록 의미가 줄어듭니다.

Retention 만료로 인한 누락

Kafka는 토픽 설정에 따라 메시지를 보관합니다. lag이 retention 기간보다 길어지면 아직 처리하지 못한 메시지가 삭제될 수 있습니다. 이 경우 컨슈머가 나중에 따라잡으려고 해도 읽을 메시지가 없습니다. 운영에서는 lag 시간과 topic retention을 함께 봐야 합니다.

현재 lag을 모두 처리하는 데 걸리는 시간 < topic retention

 

이 조건을 만족하지 못하면 retention 연장, 처리량 확장, 재처리 전략을 함께 준비해야 합니다.

Catch-up 과정에서 다운스트림 폭주

Lag을 줄이기 위해 컨슈머를 갑자기 늘리면 DB나 외부 API로 요청이 몰립니다. 이때 다운스트림이 버티지 못하면 timeout이 증가합니다. timeout은 retry를 만들고, retry는 다시 처리량을 갉아먹습니다. 결과적으로 lag이 더 쌓입니다. 따라서 catch-up은 처리량만 보고 접근하면 안 됩니다. 다운스트림이 감당할 수 있는 속도 안에서 따라잡아야 합니다.


Consumer pause와 Circuit Breaker는 언제 써야 할까

Lag이 쌓였을 때 “서킷을 열어야 하나?”라는 질문을 자주 합니다. 여기서 먼저 구분해야 합니다. Consumer pause는 Kafka 소비를 늦추거나 멈추는 수단입니다. Circuit breaker는 외부 API나 다운스트림 호출을 빠르게 실패시키는 수단입니다. 둘은 목적이 다릅니다.

Consumer pause

Consumer pause는 컨슈머가 특정 파티션에서 메시지를 더 가져오지 않도록 제어하는 방식입니다. 이 방식은 다운스트림을 보호할 때 사용합니다. 예를 들어 DB connection pool이 이미 고갈되었고, consumer가 계속 메시지를 처리하려고 하면 DB 장애가 더 커집니다. 이때 consumer를 잠시 pause하면 Kafka lag은 쌓이지만 DB는 회복할 시간을 얻습니다. Consumer pause는 “lag을 줄이는 방법”이 아닙니다. 정확히는 “lag이 쌓이는 것을 감수하고 시스템을 보호하는 방법”입니다. 사용 기준은 명확합니다.

- DB connection pool 고갈
- lock timeout 증가
- 외부 API rate limit 초과
- consumer retry 폭증
- 장애 전파 방지가 우선인 상황

 

이런 상황에서는 처리량 확장보다 유입 제어가 먼저입니다.

Circuit Breaker

Circuit breaker는 외부 API 장애를 다룰 때 사용합니다. 외부 API가 느리거나 실패하는데 consumer가 계속 호출을 시도하면 thread가 timeout까지 묶입니다. 이 상태가 반복되면 consumer 전체 처리량이 떨어지고 lag이 증가합니다. Circuit breaker를 열면 외부 API 호출을 실제로 보내지 않고 빠르게 실패시킵니다. 이렇게 하면 timeout 대기로 thread가 묶이는 상황을 줄일 수 있습니다. 다만 circuit breaker를 사용할 때는 실패 메시지를 어떻게 처리할지 반드시 정해야 합니다.

- retry topic으로 보낼 것인가
- DLQ로 보낼 것인가
- 나중에 보상 처리할 것인가
- 일부 기능을 degrade할 것인가
- offset commit은 언제 할 것인가

 

가장 위험한 설계는 외부 API 호출에 실패했는데 offset을 commit해버리는 것입니다. 이 경우 Kafka 입장에서는 처리 완료지만, 실제 비즈니스 작업은 수행되지 않습니다. 따라서 circuit breaker는 retry topic, DLQ, 멱등 처리와 함께 설계해야 합니다.


Lag 대응 순서

Kafka consumer lag이 쌓였을 때는 다음 순서로 보면 됩니다.

1. 컨슈머가 살아 있는지 확인한다

먼저 consumer group이 정상적으로 동작하는지 확인합니다.

- consumer group member 수
- assigned partition 수
- rebalance 발생 여부
- consumer error log
- offset commit 여부

컨슈머가 죽었거나 리밸런스 중이라면 스케일아웃보다 안정화가 먼저입니다.

2. 파티션별 lag 분포를 본다

전체 lag이 아니라 파티션별 lag을 봅니다.

모든 파티션이 고르게 증가
→ 전체 처리량 부족 또는 공통 병목

일부 파티션만 증가
→ key 쏠림 또는 핫파티션

이 단계에서 대응 방향이 나뉩니다.

3. 유입 TPS와 처리 TPS를 비교한다

Lag의 본질은 유입 속도와 처리 속도의 차이입니다.

producer input rate > consumer processing rate

이 관계가 지속되면 lag은 계속 증가합니다. 따라서 현재 consumer group이 초당 몇 건을 처리하는지, producer가 초당 몇 건을 넣는지 비교해야 합니다.

4. 다운스트림 병목을 확인한다

DB, 외부 API, CPU, GC를 확인합니다.

- DB latency
- DB connection pool
- slow query
- 외부 API latency
- timeout
- retry
- CPU
- GC

다운스트림이 이미 한계라면 컨슈머를 늘리면 안 됩니다.

5. 병렬성 상한을 계산한다

현재 병렬성이 파티션 수에 막혀 있는지 계산합니다.

실제 병렬성 = min(파티션 수, Pod 수 × concurrency)

컨슈머 수가 부족하면 스케일아웃합니다. 이미 파티션 수에 도달했다면 파티션 증설이나 처리 로직 개선을 검토합니다.

6. 처리량 확장 또는 유입 제어를 선택한다

다운스트림이 여유가 있으면 처리량을 확장합니다.

- consumer scale-out
- concurrency 조정
- batch 처리
- DB bulk 처리
- 파티션 증설

 

다운스트림이 위험하면 유입을 제어합니다.

- consumer pause/resume
- rate limit
- circuit breaker
- retry topic
- DLQ

핵심은 lag을 무조건 빨리 줄이는 것이 아닙니다. 시스템 전체가 감당 가능한 속도로 정상 상태를 회복하는 것입니다.


결론

Kafka consumer lag은 단순히 “컨슈머가 느리다”는 뜻이 아닙니다. Producer 유입량, consumer 병렬성, 파티션 수, key 분포, DB 성능, 외부 API 상태, 리밸런스까지 함께 반영된 결과 지표입니다. 그래서 lag이 쌓였을 때는 바로 컨슈머를 늘리거나 파티션을 늘리면 안 됩니다. 먼저 파티션별 lag 분포를 봐야 합니다. 모든 파티션이 고르게 밀리면 전체 처리량 부족이나 공통 병목을 확인합니다. 특정 파티션만 밀리면 key 쏠림과 핫파티션을 확인합니다. 그다음 현재 병렬성 상한을 계산합니다.

실제 병렬성 = min(파티션 수, Pod 수 × concurrency)

 

컨슈머가 부족하면 스케일아웃합니다. 파티션 수가 상한이면 파티션 증설을 검토합니다. 다만 key 기반 순서 보장이 필요한 토픽에서는 파티션 증설이 순서 보장에 영향을 줄 수 있으므로 신중하게 접근해야 합니다. 다운스트림이 병목이면 컨슈머를 늘리는 것이 아니라 DB, 외부 API, retry, timeout, DLQ 설계를 먼저 봐야 합니다. 마지막으로 consumer pause와 circuit breaker는 lag을 줄이는 도구가 아닙니다.
시스템을 보호하기 위한 도구입니다. 다운스트림이 위험하면 lag이 쌓이는 것을 감수하고 소비를 늦춰야 합니다. 반대로 다운스트림이 여유롭다면 처리량을 확장해서 따라잡아야 합니다. Kafka lag 대응의 핵심은 수치를 줄이는 것이 아니라 원인을 분리하는 것입니다. 원인을 분리해야 스케일아웃, 파티션 증설, 처리 로직 개선, 유입 제어 중 어떤 선택이 맞는지 판단할 수 있습니다.

반응형