[Kafka 운영 ①] Consumer Lag 완전 정복 — 측정·원인 분석·해소
Kafka Consumer Lag의 정확한 정의(LEO − committed offset)부터 오프셋 커밋 메커니즘, kafka-consumer-groups·JMX·Burrow를 활용한 측정, 그리고 정상 lag와 위험한 lag를 구분하는 읽기 방법까지 운영 관점에서 정리합니다.
새벽 3시, PagerDuty가 울립니다. "Consumer lag이 200만을 넘었습니다." 졸린 눈을 비비며 대시보드를 열어 보니 숫자가 계속 올라가고 있습니다. 이게 정말 장애일까요, 아니면 트래픽 피크에 따른 일시적 현상일까요? Kafka를 운영하다 보면 lag만큼 자주 마주치면서도, 막상 "정확히 무엇이고 얼마면 위험한가"를 설명하기 어려운 지표도 드뭅니다. 이 시리즈의 첫 글에서는 lag을 정의하고, 측정하고, 올바르게 읽는 법을 끝까지 파고듭니다.
이 글에서 배우는 것
- Consumer Lag의 정확한 정의:
lag = LEO − committed offset과 offset lag vs time lag의 차이- 오프셋이
__consumer_offsets에 저장되고 커밋되는 메커니즘, position vs committed offsetkafka-consumer-groups.sh·JMXrecords-lag-max·Burrow/Kafka Lag Exporter로 lag을 측정하는 법- 정상적인 상수 lag, 증가하는 lag, 스파이크 lag을 구분하는 읽기 기술
- 빠른 분류(triage) 체크리스트와 Part 2(원인 심화)로 넘어가기 전 준비
1. Lag이란 정확히 무엇인가
Consumer Lag은 한 문장으로 정의됩니다.
lag = log-end-offset(LEO) − last committed offset — 파티션 단위로.
즉, 해당 파티션에 쌓인 마지막 메시지의 위치(LEO) 와 컨슈머가 마지막으로 처리했다고 커밋한 위치(committed offset) 사이의 거리입니다. 이 거리가 곧 "아직 처리하지 못한 메시지 수"입니다.
핵심은 lag이 파티션 단위 지표라는 점입니다. 컨슈머 그룹의 총 lag은 그룹이 구독한 모든 파티션의 lag을 합한 값입니다.
group lag = Σ (LEO_p − committed_offset_p) for all partitions p assigned to the group
그룹 전체 합만 보면 "어느 파티션이 문제인지"를 놓치기 쉽습니다. 한 파티션이 핫스팟(hot partition)이 되어 혼자 lag을 만들어내는 경우가 흔하기 때문에, 항상 파티션별로 분해해서 봐야 합니다.
Offset lag vs Time lag
lag을 표현하는 단위는 두 가지입니다. 둘은 전혀 다른 질문에 답합니다.
| 구분 | 정의 | 답하는 질문 | 단위 |
|---|---|---|---|
| Offset lag | LEO − committed offset | "몇 개의 메시지가 밀려 있나?" | 레코드 수 |
| Time lag | now − (가장 오래된 미처리 메시지의 timestamp) | "벽시계 기준 얼마나 뒤처져 있나?" | 시간(ms/s) |
왜 둘 다 필요할까요? 메시지 크기와 처리 비용이 균일하지 않기 때문입니다.
- Offset lag이 100만이라도, 메시지당 처리가 0.1ms면 실제로는 100초만 뒤처진 것일 수 있습니다.
- Offset lag이 1만이라도, 메시지당 무거운 DB 조인이 걸려 있으면 time lag은 30분일 수 있습니다.
SLA가 "데이터는 5분 이내에 처리되어야 한다"라면 정작 중요한 건 offset lag이 아니라 time lag입니다. 반대로 토픽 retention을 넘겨 메시지가 유실될 위험을 보려면 offset lag(파티션 크기 대비)을 봐야 합니다. 운영에서는 둘을 함께 봐야 그림이 완성됩니다.
2. 오프셋과 커밋은 어떻게 동작하는가
lag을 이해하려면 먼저 컨슈머가 "어디까지 읽었는지"를 어떻게 기록하는지 알아야 합니다.
__consumer_offsets 내부 토픽
Kafka는 각 컨슈머 그룹의 커밋된 오프셋을 __consumer_offsets라는 내부 토픽에 저장합니다. 이 토픽은 다음과 같은 특징을 가집니다.
| 속성 | 값 | 의미 |
|---|---|---|
| 파티션 수 | offsets.topic.num.partitions (기본 50) | 그룹은 hash(group.id) % 50으로 한 파티션에 매핑 |
| Cleanup policy | compact | 같은 (group, topic, partition) 키의 최신 오프셋만 보존 |
| 복제 계수 | offsets.topic.replication.factor (기본 3) | 오프셋 정보의 내구성 보장 |
커밋 시 컨슈머는 (group.id, topic, partition) → offset, metadata, commit-timestamp를 메시지로 이 토픽에 씁니다. compaction 덕분에 같은 키의 과거 커밋은 정리되고 최신 값만 남습니다.
Position vs Committed offset
여기서 자주 혼동되는 두 개념을 구분해야 합니다.
- Position (current position): 컨슈머가 다음에
poll()로 가져올 오프셋. 메모리상의 값이며, fetch할 때마다 앞으로 전진합니다. - Committed offset: 컨슈머가
__consumer_offsets에 마지막으로 기록한 오프셋. 리밸런스나 재시작 후 여기서부터 다시 시작합니다.
... [읽었지만 아직 커밋 안 함] ...
LEO ──────────────────────────────► 오른쪽 끝(생산자가 쓴 마지막+1)
▲ position (다음에 읽을 위치)
▲ committed offset (장애 시 복구 지점)
lag은 committed offset 기준으로 계산됩니다(kafka-consumer-groups의 CURRENT-OFFSET이 committed offset입니다). 따라서 컨슈머가 메시지를 빠르게 처리하더라도 커밋을 늦게/드물게 하면 lag 지표는 실제보다 크게 보일 수 있습니다.
| 커밋 방식 | 설정 | lag 지표 영향 |
|---|---|---|
| 자동 커밋 | enable.auto.commit=true, auto.commit.interval.ms=5000 | 최대 5초치 처리분이 미커밋 → lag이 항상 약간 부풀려 보임 |
| 수동 동기 커밋 | commitSync() | 정확하지만 처리량 저하 가능 |
| 수동 비동기 커밋 | commitAsync() | 처리량 우수, 실패 시 재시도 전략 필요 |
주의: "처리는 끝났는데 lag이 안 떨어진다"의 흔한 원인 중 하나가 커밋 주기/실패입니다. 이 함정은 Part 2에서 자세히 다룹니다.
3. Lag 측정하기
3.1 CLI — kafka-consumer-groups.sh
가장 기본이자 정확한 도구입니다. 브로커에 직접 질의해 그룹 상태를 보여줍니다.
kafka-consumer-groups.sh \
--bootstrap-server kafka-1:9092 \
--describe \
--group order-processor출력 예시:
GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID
order-processor orders 0 1048210 1048210 0 consumer-1-a1b2c3-... /10.0.1.21 consumer-1
order-processor orders 1 982134 1003221 21087 consumer-2-d4e5f6-... /10.0.1.22 consumer-2
order-processor orders 2 1120004 1120010 6 consumer-3-g7h8i9-... /10.0.1.23 consumer-3
order-processor orders 3 550120 770540 220420 - - -읽는 법:
- CURRENT-OFFSET = committed offset, LOG-END-OFFSET = LEO, LAG = 그 차이.
- 파티션 1은 lag 21,087 — 처리 중이지만 약간 뒤처짐.
- 파티션 3은 lag 220,420인데 CONSUMER-ID가
-— 즉 할당된 컨슈머가 없습니다. 인스턴스 수 부족 또는 리밸런스 중이라는 강력한 신호입니다. 그룹 합만 봤다면 놓쳤을 결정적 단서입니다.
상태만 빠르게 보려면:
# 모든 그룹 나열
kafka-consumer-groups.sh --bootstrap-server kafka-1:9092 --list
# 그룹 상태(STATE) 요약 — Stable / Rebalancing / Empty 확인
kafka-consumer-groups.sh --bootstrap-server kafka-1:9092 --describe --group order-processor --state3.2 JMX 메트릭 — records-lag-max
CLI는 스냅샷이고 브로커에 질의 부하를 줍니다. 지속 모니터링에는 컨슈머가 자체 노출하는 JMX 메트릭이 적합합니다.
kafka.consumer:type=consumer-fetch-manager-metrics,client-id=<id>
├─ records-lag-max # 이 컨슈머의 모든 파티션 중 최대 lag (가장 중요)
├─ records-lag # 파티션별 현재 lag (partition 태그 포함)
└─ records-lead-min # 파티션 시작(log-start-offset)으로부터의 여유 — 작아지면 유실 임박records-lag-max는 컨슈머 측에서 직접 계산하므로 브로커 조회 없이 Prometheus(JMX Exporter)로 긁어 알람을 걸 수 있습니다. records-lead-min도 함께 모니터링하세요 — 이 값이 0에 가까워지면 retention에 의해 미처리 메시지가 삭제될 위험이 있다는 경고입니다.
3.3 외부 도구
| 도구 | 특징 | 적합한 상황 |
|---|---|---|
| Burrow (LinkedIn) | 임계값 대신 **소비 추세(evaluation rule)**로 상태(OK/WARN/ERR) 판정. committer 정지·느림 자동 감지 | 임계값 튜닝 없이 그룹 건강도를 보고 싶을 때 |
| Kafka Lag Exporter | Prometheus 메트릭으로 offset lag과 time lag(추정) 동시 노출 | Grafana 대시보드/알람 표준화 |
| Cruise Control (LinkedIn) | lag 자체보다 파티션 리밸런싱·부하 분산 자동화 | 핫 파티션·브로커 불균형 해소 |
| kafka-consumer-groups | 추가 인프라 없이 정확한 스냅샷 | 즉석 디버깅 |
Burrow의 핵심 아이디어는 "lag 절대값에 임계값을 거는 것은 트래픽에 따라 오탐이 많다"는 통찰입니다. 그래서 Burrow는 오프셋 진행 추세를 보고, 컨슈머가 멈췄거나(committed offset이 안 움직임) 생산 속도를 못 따라가는지를 판정합니다.
4. Lag을 올바르게 읽기
숫자 하나로 패닉에 빠지면 안 됩니다. lag은 시간에 따른 패턴으로 읽어야 합니다.
패턴별 해석
| 패턴 | 그래프 모양 | 보통의 의미 | 대응 |
|---|---|---|---|
| 상수(steady-state) lag | 0이 아니지만 평평 | 정상 — in-flight 버퍼. 컨슈머가 생산을 따라잡고 있음 | 임계값을 0이 아닌 합리적 값으로 |
| 증가(growing) lag | 꾸준히 우상향 | 위험 — 소비 < 생산. 용량/병목/정지 | 즉시 조사(Part 2) |
| 스파이크(spiky) lag | 톱니/펄스 후 하강 | 보통 정상 — 배치 생산·트래픽 피크 흡수 | 회복 시간이 SLA 내인지 확인 |
| 계단식 정체 | 올라간 뒤 평평 유지 | 컨슈머 일부 정지/리밸런스 후 미할당 | 파티션별·consumer-id 확인 |
왜 작은 상수 lag은 정상인가
lag이 항상 정확히 0이면 오히려 의심해야 합니다. 그건 보통 "생산이 멈췄거나 컨슈머가 과하게 프로비저닝됐다"는 뜻입니다. 정상적인 스트리밍 파이프라인은 항상 약간의 in-flight 메시지(fetch된 배치, 처리 큐)를 가지므로 작고 일정한 lag이 건강한 상태입니다. 따라서 알람 임계값은 "lag > 0"이 아니라 "lag이 N분 동안 지속 상승" 또는 "time lag > SLA" 형태여야 합니다.
Records vs Time, 다시
같은 offset lag 50,000이라도:
- 메시지가 작고 stateless 변환이면 → time lag 몇 초, 무시 가능.
- 메시지마다 외부 API 호출이 있으면 → time lag 수십 분, SLA 위반.
그래서 알람은 가능하면 time lag 기준으로 거는 것이 오탐을 줄입니다(Kafka Lag Exporter의 추정 time lag 또는 메시지 timestamp 기반 계산 사용).
5. 빠른 분류(Triage) 체크리스트
새벽 알람을 받았을 때 순서대로 확인하세요.
체크리스트:
- 추세 확인: 증가하는가, 평평한가, 회복 중인가? (단일 스냅샷 금지)
- 파티션별 분해:
--describe로 특정 파티션 편중인지 확인. - 할당 상태: CONSUMER-ID가
-인 파티션이 있는가? → 인스턴스 부족/리밸런스. - 그룹 STATE:
--state로Rebalancing이 반복되는가? → 리밸런스 스톰 의심. - 생산 측 변화: 생산 rate가 급증했는가? (lag은 소비가 아니라 생산 폭증 때문일 수도)
- time lag 확인: 레코드 수가 커도 SLA 시간 내인가?
- retention 여유:
records-lead-min이 0에 근접하는가? → 유실 임박, 최우선 대응.
이 7단계로 대부분 "정상/회복 중"과 "진짜 장애"를 구분할 수 있습니다. "진짜 장애"로 분류된 경우의 원인 진단과 해소(컨슈머가 따라잡지 못하는 이유, 리밸런스 스톰, 처리 병목, 파티션 스케일링)는 이 시리즈 Part 2: "Lag이 줄지 않을 때" 에서 본격적으로 다룹니다.
마치며 — 핵심 요약
- lag = LEO − committed offset, 파티션 단위. 그룹 lag은 합. 항상 파티션별로 분해해서 보세요.
- offset lag(레코드 수)과 time lag(벽시계) 는 다른 질문에 답합니다. SLA가 시간 기준이면 알람도 time lag 기준으로.
- 오프셋은
__consumer_offsets(compact 토픽)에 커밋됩니다. lag은 committed offset 기준이므로 커밋 주기가 지표를 부풀릴 수 있습니다. - 측정은
kafka-consumer-groups(스냅샷) + JMXrecords-lag-max(지속 모니터링) + Burrow/Kafka Lag Exporter(추세·time lag)를 조합하세요. - 작고 일정한 lag은 정상입니다. 위험 신호는 "지속 증가"와 "time lag > SLA"입니다. 알람을 "lag > 0"으로 걸지 마세요.
- 다음 글(Part 2)에서는 "lag이 줄지 않을 때"의 근본 원인과 해소 전략을 다룹니다.
참고 자료
- Apache Kafka Documentation — https://kafka.apache.org/documentation/
- Apache Kafka — Consumer Group / Offset Management — https://kafka.apache.org/documentation/#impl_offsettracking
kafka-consumer-groups도구 (Kafka Operations) — https://kafka.apache.org/documentation/#basic_ops_consumer_group- LinkedIn Burrow — Kafka Consumer Lag Checking — https://github.com/linkedin/Burrow
- Kafka Lag Exporter — https://github.com/seglo/kafka-lag-exporter
— Data Dynamics 엔지니어링 팀