Kafka 운영, 성능 튜닝과 장애 대응
40 Kafka Interview Questions for Java Developers (blog) kafk_ref_03(Section 5-6,10 실전 이슈/모니터링), kafka_ref_01(Q25-30,42-45,86-90), kafka_ref_04(성능 튜닝 문항), kafka_ref_05(Q7,Q12) 종합. kafk_ref_02(Q38 'Lack of Pace'를 Kafka 단점으로 서술)는 Kafka의 실제 강점(고처리량)과 상충하는 근거 불명확한 서술로 판단해 반영하지 않음.
이 모듈을 다 읽으면
- 보존(retention)과 로그 컴팩션의 차이와 각각의 적합한 사용 사례를 구분할 수 있다
- 프로듀서/컨슈머/브로커 각 지점에서 처리량을 높이는 설정 레버를 설명할 수 있다
- 컨슈머 랙이 커졌을 때 원인을 단계적으로 좁혀가는 진단 절차를 설명할 수 있다
- 프로덕션에서 반드시 모니터링해야 할 핵심 지표와 알림 기준을 나열할 수 있다
데이터 보존/컴팩션 정책, 처리량을 좌우하는 배치·압축·zero-copy 메커니즘, 그리고 컨슈머 랙·리밸런싱·언더 리플리케이션 같은 실전 장애를 진단하는 절차를 정리한다.
보존 정책과 로그 컴팩션 — 서로 다른 목적
Kafka는 메시지를 소비 여부와 무관하게 설정된 정책에 따라 보존한다. 시간 기반 보존(log.retention.hours, 기본 168시간=7일)은 오래된 세그먼트를 통째로 삭제하고, 크기 기반 보존(log.retention.bytes)은 파티션 크기가 한계를 넘으면 오래된 세그먼트부터 삭제한다. 두 정책을 함께 설정하면 먼저 도달하는 조건이 적용된다. 중요한 점은 Kafka가 메시지를 개별적으로 지우지 않고 로그 세그먼트(log.segment.bytes 단위, 기본 1GB) 전체를 통째로 삭제한다는 것이다 — 이는 보존 단위의 정밀도가 세그먼트 단위라는 뜻이기도 하다.
반면 로그 컴팩션(cleanup.policy=compact)은 시간/크기가 아니라 키 기준으로 동작한다 — 같은 키를 가진 레코드 중 더 최근 값만 남기고 오래된 값을 백그라운드의 로그 클리너 스레드가 제거해, "이 키의 최신 상태"를 항상 유지하는 체인지로그를 만든다. 이벤트 로그·감사 추적처럼 "무슨 일이 있었는지" 전체 이력이 중요한 토픽은 시간/크기 기반 보존을, 사용자 프로필·설정값·계좌 잔액처럼 "지금 상태가 무엇인지"만 중요한 토픽은 컴팩션을 쓰는 것이 적합하다. 컨슈머가 보존 기간보다 오래 다운되어 있으면 아직 읽지 않은 오래된 메시지가 이미 삭제되어 영구히 유실될 수 있으므로, auto.offset.reset 정책과 함께 보존 기간을 컨슈머의 예상 최대 다운타임보다 여유 있게 잡아야 한다.
핵심 포인트
- 시간 기반/크기 기반 보존은 오래된 로그 세그먼트를 통째로 삭제하며 삭제 단위는 개별 메시지가 아니라 세그먼트다
- 로그 컴팩션은 키 기준으로 최신 값만 남기는 별개의 메커니즘이다
- 이력이 중요하면 보존을, 최신 상태만 중요하면 컴팩션을 선택한다
- 컨슈머 다운타임이 보존 기간을 넘으면 아직 읽지 않은 메시지가 영구히 유실될 수 있다
처리량을 높이는 세 가지 레버 — 배치, 압축, zero-copy
Kafka가 커모디티 하드웨어에서도 높은 처리량을 내는 것은 몇 가지 설계 선택이 겹친 결과다. 프로듀서 쪽에서는 여러 레코드를 하나의 배치로 묶어 보내는 배치 처리가 네트워크 왕복과 디스크 쓰기 횟수를 줄인다 — batch.size는 배치의 최대 크기를, linger.ms는 배치를 채우기 위해 추가로 기다리는 시간을 제어하며, linger.ms를 늘리면 처리량은 오르고 지연은 늘어난다(기본값 0은 가능한 한 즉시 전송). 압축(compression.type: gzip/snappy/lz4/zstd)은 네트워크 대역폭과 디스크 사용량을 줄이는 대신 CPU를 더 쓴다.
브로커 쪽에서는 zero-copy가 핵심이다 — 컨슈머의 fetch 요청을 처리할 때 브로커는 OS의 sendfile() 시스템 콜을 사용해 디스크 페이지 캐시의 데이터를 애플리케이션(JVM) 레이어를 거치지 않고 곧바로 네트워크 소켓 버퍼로 전송한다. 이는 커널-유저 공간 간 불필요한 복사와 CPU 사이클을 없애 준다. 컨슈머 쪽에서는 fetch.min.bytes와 fetch.max.wait.ms로 한 번의 fetch에 더 많은 데이터를 모아 가져오게 하거나, 여러 스레드/인스턴스로 파티션을 병렬 소비하는 것이 처리량을 늘리는 레버가 된다. 다만 이 모든 튜닝의 상한은 결국 파티션 수다 — 파티션보다 컨슈머 인스턴스가 많아지면 초과분은 유휴 상태가 된다.
핵심 포인트
- batch.size와 linger.ms는 프로듀서 배치 크기와 대기 시간을 제어하며 처리량-지연 트레이드오프를 만든다
- 압축은 네트워크/디스크 사용량을 줄이는 대신 CPU를 더 쓴다
- zero-copy(sendfile)는 디스크 페이지 캐시에서 네트워크 소켓으로 애플리케이션 레이어를 거치지 않고 직접 전송한다
- 컨슈머 병렬 처리량의 실질적 상한은 파티션 수다
컨슈머 랙 진단과 프로덕션 모니터링 지표
컨슈머 랙은 파티션의 최신 오프셋과 해당 컨슈머 그룹이 커밋한 오프셋의 차이로, kafka-consumer-groups.sh --describe나 AdminClient API로 확인할 수 있다. 랙이 커졌을 때의 진단 순서는 대체로 다음과 같다: (1) 랙이 모든 파티션에 고르게 퍼져 있는지, 아니면 특정 파티션에 쏠려 있는지 확인한다 — 후자는 보통 핫 키(특정 키에 트래픽이 몰림)로 인한 파티션 불균형을 의심한다. (2) 컨슈머 로그에서 긴 GC 정지나 느린 DB/외부 API 호출처럼 처리 루프를 막는 요인이 있는지 확인한다. (3) max.poll.records가 지나치게 크면 한 번의 poll에서 처리해야 할 레코드가 많아져 max.poll.interval.ms를 넘기고, 이는 컨슈머가 죽은 것으로 간주되어 불필요한 리밸런싱을 유발할 수 있다. (4) 근본적으로 처리 속도가 유입 속도를 못 따라가면 파티션 수 한도 내에서 컨슈머 인스턴스를 늘려 병렬성을 높인다.
프로덕션에서 놓치지 말아야 할 핵심 지표는 컨슈머 랙(records-lag-max) 외에도 UnderReplicatedPartitions(0이어야 정상 — 0보다 크면 일부 파티션의 팔로워가 ISR에서 벗어나 있다는 뜻), ISR 축소/확장 비율, ActiveControllerCount(정확히 1이어야 함 — 0이면 컨트롤러 부재, 2 이상이면 스플릿 브레인 징후), 요청 큐 대기 시간(request queue time), 브로커의 디스크/네트워크/CPU 사용률이 있다. 이 지표들은 JMX로 노출되며 Prometheus JMX Exporter + Grafana 조합이나 Confluent Control Center 같은 상용 도구로 수집·시각화하는 것이 일반적이다.
핵심 포인트
- 컨슈머 랙은 최신 오프셋과 커밋된 오프셋의 차이이며 특정 파티션 쏠림은 핫 키를 의심할 신호다
- max.poll.records가 크면 처리 시간이 늘어 max.poll.interval.ms 초과로 불필요한 리밸런싱을 유발할 수 있다
- UnderReplicatedPartitions는 0이 정상이며 0보다 크면 복제 지연/장애 신호다
- ActiveControllerCount는 정확히 1이어야 하며 다른 값은 컨트롤러 부재 또는 스플릿 브레인 징후다