일반적인 Kafka 성능 병목 현상 해결: 실용 핸드북

이 실용 핸드북은 Apache Kafka에서 흔히 발생하는 성능 병목 현상을 식별하고 해결하는 방법을 안내합니다. 처리량 제한, 높은 지연 시간, 컨슈머 지연(lag)을 해결하기 위한 실행 가능한 조언과 구성 예제를 제공합니다. 주요 지표를 이해하고 입증된 문제 해결 기법을 적용하여 Kafka 클러스터를 최적화하고 더 효율적인 이벤트 스트리밍 플랫폼을 구축하세요.

일반적인 Kafka 성능 병목 현상 해결: 실용 핸드북

Kafka 성능 작업은 모든 느린 현상을 Kafka 문제로 진단할 때 혼란스러워집니다. 때로는 브로커가 포화 상태일 수 있습니다. 때로는 프로듀서가 작은 압축되지 않은 레코드를 보내고 있을 수 있습니다. 때로는 컨슈머가 데이터베이스를 기다리고 있고 Kafka는 단지 메신저일 뿐입니다. 유용한 문제 해결 과정은 시간이 소비되는 위치를 파악하는 것에서 시작합니다: 프로듀서 전송, 브로커 추가 및 복제, 컨슈머 가져오기(fetch), 또는 가져오기 후 애플리케이션 처리.

이 핸드북은 그러한 조사를 위해 작성되었습니다. 관찰 가능한 증상, 가능한 원인, 그리고 한 번에 하나씩 테스트할 가치가 있는 변경 사항에 초점을 맞춥니다.

Kafka 성능 지표 이해하기

문제 해결에 들어가기 전에 성능 상태를 나타내는 주요 지표를 이해하는 것이 중요합니다. 이러한 지표를 정기적으로 모니터링하면 이상 징후를 조기에 발견할 수 있습니다:

  • 브로커 지표:
    • BytesInPerSecBytesOutPerSec: 들어오고 나가는 데이터 속도를 측정합니다. 높은 값은 높은 부하를 나타낼 수 있으며, 낮은 값은 다른 곳에 병목이 있음을 시사할 수 있습니다.
    • RequestQueueTimeMs: 요청이 요청 큐에서 대기하는 평균 시간입니다. 높은 값은 브로커 과부하를 나타냅니다.
    • NetworkProcessorAvgIdlePercent: 네트워크 스레드가 유휴 상태인 시간의 백분율입니다. 낮은 백분율은 높은 네트워크 I/O 부하를 나타냅니다.
    • LogFlushRateAndTimeMs: 디스크 플러시 작업을 측정합니다. 여기서 높은 지연 시간은 프로듀서 및 팔로워 복제에 직접적인 영향을 미칩니다.
    • UnderReplicatedPartitions: 원하는 것보다 복제본이 적은 파티션 수입니다. 이는 복제 지연 및 잠재적인 데이터 손실을 나타낼 수 있습니다.
  • 프로듀서 지표:
    • RecordBatchSize: 레코드 배치의 평균 크기입니다. 큰 배치는 처리량을 향상시킬 수 있지만 지연 시간을 증가시킵니다.
    • RecordSendRate: 초당 전송된 레코드 수입니다.
    • CompressionRate: 압축 효율성입니다. 높은 비율은 더 적은 데이터가 전송됨을 의미합니다.
  • 컨슈머 지표:
    • FetchRate: 초당 가져오기 요청 수입니다.
    • BytesConsumedPerSec: 초당 소비된 데이터 양입니다.
    • OffsetLagMax: 컨슈머 그룹의 최대 오프셋 지연입니다. 이는 컨슈머 성능의 중요한 지표입니다.
  • 컨트롤러 메타데이터 지표: ZooKeeper 기반 클러스터에서는 ZooKeeper 요청 지연 시간과 연결 상태를 확인하세요. KRaft 기반 클러스터에서는 컨트롤러 쿼럼 상태와 메타데이터 요청 지연 시간을 확인하세요. 정확한 지표 이름은 Kafka 버전 및 모니터링 스택에 따라 다릅니다.

일반적인 병목 현상 시나리오 및 해결 방법

1. 처리량 제한

제한된 처리량은 느린 데이터 수집 또는 소비로 나타나 이벤트 스트림의 전반적인 속도에 영향을 미칠 수 있습니다.

1.1. 네트워크 대역폭 부족
  • 증상: BytesInPerSec 또는 BytesOutPerSec이 네트워크 인터페이스 한계에 근접, 느린 프로듀서/컨슈머 처리량.
  • 진단: 브로커, 프로듀서, 컨슈머의 네트워크 사용률을 모니터링합니다. 사용 가능한 대역폭과 비교합니다.
  • 해결 방법:
    • 네트워크 확장: 브로커 머신의 네트워크 인터페이스 또는 NIC를 업그레이드합니다.
    • 부하 분산: 브로커를 추가하여 네트워크 트래픽을 분산합니다. 토픽이 브로커 전체에 적절히 파티셔닝되었는지 확인합니다.
    • 직렬화 최적화: 비효율적인 형식(예: JSON)보다 효율적인 직렬화 형식(예: Avro, Protobuf)을 사용합니다.
    • 압축: 프로듀서 측 압축(Gzip, Snappy, LZ4, Zstd)을 활성화하여 네트워크를 통해 전송되는 데이터 양을 줄입니다. 예를 들어, 프로듀서를 다음과 같이 구성합니다:
    # producer.properties
    compression.type=snappy
    
1.2. 디스크 I/O 병목
  • 증상: 높은 LogFlushRateAndTimeMs 지표, 느린 디스크 읽기/쓰기 작업, 프로듀서 및 팔로워가 뒤처짐.
  • 진단: 브로커 머신의 디스크 I/O 사용률(IOPS, 처리량)을 모니터링합니다. Kafka는 순차 디스크 쓰기에 크게 의존합니다.
  • 해결 방법:
    • 더 빠른 디스크: Kafka 로그에 더 빠른 SSD 또는 NVMe 드라이브를 사용합니다. 워크로드에 적합한 IOPS와 처리량을 보장합니다.
    • RAID 구성: 쓰기 성능에 유리한 RAID 구성(예: RAID 0, RAID 10)을 사용하되, 중복성과의 절충을 고려합니다.
    • 디스크 분리: Kafka 로그를 여러 물리적 디스크에 분산하여 I/O 작업을 병렬화합니다.
    • log.flush.interval.messageslog.flush.interval.ms 조정: 이러한 설정은 로그가 디스크에 플러시되는 빈도를 제어합니다. 더 큰 값은 플러시 빈도를 줄여 처리량을 향상시킬 수 있지만, 플러시 전에 브로커가 실패하면 데이터 손실 위험이 증가합니다.
    • 내구성 절충에 주의: 브로커 플러시 설정과 프로듀서 acks는 허용되는 실패 위험에 영향을 미칩니다. 내구성 기대치를 낮추면 일부 워크로드에서 지연 시간을 줄일 수 있지만, 이는 문서화된 장애 모델을 포함한 비즈니스 결정이어야 하며, 가벼운 튜닝 트릭이 아닙니다.
1.3. 브로커 리소스 부족 (CPU/메모리)
  • 증상: 브로커의 높은 CPU 사용률, 높은 RequestQueueTimeMs, 낮은 NetworkProcessorAvgIdlePercent.
  • 진단: 브로커 머신의 CPU 및 메모리 사용량을 모니터링합니다.
  • 해결 방법:
    • 스케일 업: 기존 브로커 인스턴스의 CPU 코어 또는 RAM을 늘립니다.
    • 스케일 아웃: 클러스터에 브로커를 추가합니다. 부하를 분산하기 위해 토픽이 잘 파티셔닝되었는지 확인합니다.
    • JVM 힙 조정: Kafka 브로커의 JVM 힙 크기를 조정합니다. 너무 작은 힙은 빈번한 가비지 컬렉션 일시 중지를 초래할 수 있고, 너무 큰 힙도 문제를 일으킬 수 있습니다. 많은 워크로드에서 일반적인 시작점은 6GB 또는 8GB입니다.
    • 작업 오프로드: Kafka 브로커 머신에서 다른 리소스 집약적인 애플리케이션을 실행하지 않도록 합니다.

2. 높은 지연 시간

높은 지연 시간은 이벤트가 생성되고 소비될 때까지 눈에 띄는 지연이 있음을 의미합니다.

2.1. 프로듀서 지연 시간
  • 증상: 프로듀서가 request.timeout.ms 또는 delivery.timeout.ms 값에 도달했다고 보고합니다.
  • 진단: 프로듀서 구성 및 네트워크 상태를 분석합니다.
  • 해결 방법:
    • acks 설정: acks=all은 필요한 동기식 복제본을 기다리며, 내구성이 중요할 때 일반적으로 올바른 선택입니다. 이를 적절한 min.insync.replicas(일반적으로 복제된 프로덕션 토픽에서 1보다 큼)와 함께 사용합니다. acks=1은 대기 시간을 줄일 수 있지만 브로커 장애 시 더 많은 손실 위험을 감수합니다.
    • linger.ms: linger.ms를 작은 값(예: 0-10ms)으로 설정하면 메시지를 즉시 전송하여 지연 시간을 줄이지만 요청 오버헤드가 증가할 수 있습니다. 값을 늘리면 더 많은 메시지를 배치하여 처리량은 향상되지만 지연 시간이 증가합니다.
    • batch.size: 더 큰 배치 크기는 처리량을 향상시키지만 지연 시간을 증가시킬 수 있습니다. 지연 시간 요구 사항에 따라 조정합니다.
    • 네트워크: 프로듀서와 브로커 간의 낮은 지연 시간 네트워크 경로를 보장합니다.
    • 브로커 부하: 브로커가 과부하되면 프로듀서 요청이 큐에 쌓입니다.
2.2. 컨슈머 지연 시간 (오프셋 지연)
  • 증상: 컨슈머가 컨슈머 그룹에 대해 상당한 OffsetLagMax를 보고합니다.
  • 진단: kafka-consumer-groups.sh 또는 모니터링 대시보드와 같은 도구를 사용하여 컨슈머 그룹 지연을 모니터링합니다.
  • 해결 방법:
    • 컨슈머 확장: 컨슈머 그룹 내 컨슈머 인스턴스 수를 토픽의 파티션 수까지 늘립니다. 각 컨슈머 인스턴스는 하나 이상의 파티션에서만 메시지를 처리할 수 있으며, 동일한 그룹 내 여러 컨슈머가 파티션을 공유할 수 없습니다.
    • 파티션 증가: 토픽의 파티션이 프로듀서의 쓰기 속도를 따라잡기에 너무 적은 경우 파티션 수를 늘립니다. 참고: 이는 영구적인 변경이며 기존 컨슈머와 프로듀서에 영향을 미치므로 신중한 고려가 필요합니다.
    # 토픽의 파티션을 늘리는 예제
    kafka-topics.sh --bootstrap-server localhost:9092 --alter --topic my-topic --partitions 12
    
    • 컨슈머 로직 최적화: 컨슈머 내 처리 로직이 효율적인지 확인합니다. 차단 작업이나 장기 실행 작업을 피합니다. 가능하면 메시지를 배치로 처리합니다.
    • 가져오기 구성: 컨슈머에서 fetch.min.bytesfetch.max.wait.ms를 조정합니다. 더 큰 fetch.min.bytes는 처리량을 향상시킬 수 있지만 지연 시간을 증가시키고, fetch.max.wait.ms는 최소 바이트가 충족되지 않더라도 컨슈머가 데이터를 반환하기 전에 기다리는 시간을 제어합니다.
    • 브로커 성능: 브로커가 어려움을 겪고 있으면(디스크, 네트워크, CPU) 가져오기 요청과 컨슈머 지연에 직접적인 영향을 미칩니다.

3. ZooKeeper 병목 현상

Kafka가 컨트롤러 쿼럼을 위해 KRaft(Kafka Raft)로 전환하고 있지만, 많은 배포가 여전히 ZooKeeper에 의존합니다. ZooKeeper 문제는 Kafka 작업을 마비시킬 수 있습니다.

  • 증상: 느린 브로커 시작, 토픽/파티션 재할당 문제, zk_avg_latency 높음, 브로커가 ZooKeeper에 연결 오류 보고.
  • 진단: ZooKeeper 성능 지표를 모니터링합니다. ZooKeeper 로그에서 오류를 확인합니다.
  • 해결 방법:
    • 전용 ZooKeeper 클러스터: Kafka 브로커와 분리된 전용 머신에서 ZooKeeper를 실행합니다.
    • 충분한 리소스: ZooKeeper 노드에 적절한 CPU, 메모리 및 빠른 I/O(특히 SSD)가 있는지 확인합니다.
    • ZooKeeper 튜닝: 네트워크 및 클러스터 크기에 따라 ZooKeeper의 tickTime, syncLimit, initLimit 설정을 조정합니다.
    • ZooKeeper 트래픽 감소: 빈번한 토픽 생성/삭제 또는 공격적인 컨트롤러 장애 조치와 같이 ZooKeeper를 자주 업데이트하는 작업을 최소화합니다.
    • KRaft로 마이그레이션: ZooKeeper 의존성을 제거하기 위해 KRaft 모드로 마이그레이션하는 것을 고려합니다.

성능 최적화를 위한 모범 사례

  • 지속적인 모니터링: 모든 주요 Kafka 및 ZooKeeper 지표에 대한 강력한 모니터링 및 알림을 구현합니다.
  • 구성 튜닝: 각 구성 매개변수의 영향을 이해하고 특정 워크로드와 하드웨어에 맞게 조정합니다. 합리적인 기본값에서 시작하여 반복적으로 개선합니다.
  • 파티셔닝 전략: 토픽당 적절한 파티션 수를 선택합니다. 너무 적으면 병렬 처리가 제한되고, 너무 많으면 오버헤드가 증가할 수 있습니다.
  • 하드웨어 선택: Kafka 브로커에 적합한 하드웨어, 특히 빠른 디스크와 충분한 네트워크 대역폭에 투자합니다.
  • 프로듀서 및 컨슈머 튜닝: 프로듀서의 batch.size, linger.ms, acks와 컨슈머의 fetch.min.bytes, fetch.max.wait.ms, max.poll.records를 최적화합니다.
  • Kafka 업데이트 유지: 최신 버전은 종종 성능 개선 및 버그 수정을 제공합니다.
  • 부하 테스트: 정기적으로 부하 테스트를 수행하여 프로덕션 트래픽을 시뮬레이션하고 라이브 시스템에 영향을 미치기 전에 잠재적인 병목 현상을 식별합니다.

성능 조사 수행 방법

한 번에 한 계층씩 변경합니다. 프로듀서가 느린 경우 먼저 요청 지연 시간, 배치 크기, 압축 비율, 재시도, 버퍼 고갈과 같은 프로듀서 지표를 확인합니다. 브로커가 느린 경우 요청 큐 시간, 네트워크 스레드 유휴 비율, 디스크 대기 시간, 페이지 캐시 압력, 복제 미달 파티션, 컨트롤러 안정성을 확인합니다. 컨슈머가 느린 경우 파티션별 지연, 배치당 처리 시간, 다운스트림 종속성 지연 시간, 리밸런스 빈도를 확인합니다.

실제 예: 마케팅 캠페인 후 주문 토픽에서 지연이 증가하고 있습니다. 브로커 CPU는 괜찮고, 디스크 쓰기도 괜찮으며, 프로듀서 재시도도 정상입니다. kafka-consumer-groups.sh --describe는 하나의 파티션이 대부분의 지연을 보여줍니다. 이는 브로커 용량 문제보다는 파티션 편향을 가리킵니다. 레코드가 고객 ID로 키 지정되고 한 명의 대규모 고객이 대부분의 이벤트를 생성하는 경우, 컨슈머를 추가해도 해당 파티션에 도움이 되지 않습니다. 파티션은 여전히 그룹 내 하나의 컨슈머에만 할당되기 때문입니다. 향후 데이터에 대한 키 지정 전략을 변경하거나, 토픽별로 워크로드를 분할하거나, 해당 컨슈머 경로를 더 빠르게 만들어야 할 수 있습니다.

또 다른 예: 모든 파티션이 함께 지연되고, 컨슈머 로그는 결제 API 호출이 몇 초가 걸리는 것을 보여줍니다. Kafka 가져오기 튜닝으로는 해결되지 않습니다. 컨슈머 내에서 제한된 동시성, Kafka와 느린 종속성 사이의 큐, 벌크 쓰기, 또는 백프레셔 및 재시도에 대한 제품 결정이 필요합니다.

좋은 Kafka 튜닝은 대부분 규율 있는 측정입니다. 기준선을 유지하고, 하나의 변경을 수행하고, 현실적인 레코드 크기와 키로 부하 테스트를 한 다음, 처리량뿐만 아니라 p95 및 p99 지연 시간을 비교합니다. 평균 지연 시간은 괜찮아 보일 수 있지만 소수의 파티션이 이미 뒤처지고 있을 수 있습니다.

구성을 변경하기 전에 확인하는 사항

Kafka를 튜닝하기 전에 병목 현상이 실제로 Kafka에 있는지 증명하는 것을 좋아합니다. 하나의 느린 경로를 선택하여 끝에서 끝까지 추적합니다. 생성된 이벤트의 경우, 프로듀서가 전송 완료를 기다리는 데 얼마나 오래 걸리나요? 레코드가 토픽에 나타나는 데 얼마나 걸리나요? 컨슈머가 가져오는 데 얼마나 걸리나요? 컨슈머가 가져오기 후에 얼마나 시간을 소비하나요? 이 네 가지 숫자는 많은 무작위 구성 변경을 방지합니다.

프로듀서 전송 시간이 높으면 배치, 압축, 재시도, acks, delivery.timeout.ms, 브로커 요청 지연 시간을 검사합니다. 브로커 추가가 느리면 디스크, 네트워크, ISR 변동, 컨트롤러 이벤트, 요청 큐를 검사합니다. 컨슈머 가져오기는 빠르지만 처리가 느리면 브로커 스레드 튜닝을 중단하고 애플리케이션 코드를 살펴봅니다. 다운스트림 데이터베이스 쓰기까지 모든 것이 빠르다면 Kafka가 병목이 아닙니다.

다음은 현실적인 패턴입니다. 팀이 높은 종단 간 지연 시간을 보고 브로커 메모리를 늘립니다. 아무 변화도 없습니다. 그런 다음 컨슈머 타이밍을 확인하고 각 메시지가 세 번의 직렬 HTTP 호출을 수행한다는 것을 발견합니다. Kafka는 배치를 빠르게 전달하고 있었습니다. 컨슈머는 클러스터 외부에서 대기하는 데 대부분의 시간을 소비하고 있었습니다. 유용한 수정은 제한된 동시성, 타임아웃, 반복적인 다운스트림 실패에 대한 데드 레터 경로였습니다.

또 다른 일반적인 패턴은 작은 프로듀서 배치입니다. 서비스가 지연 시간(ligner) 없이 압축 없이 하나의 작은 JSON 레코드를 한 번에 보냅니다. 브로커 CPU가 상승하고, 네트워크 오버헤드가 증가하며, 단일 머신이 완전히 포화 상태로 보이지 않더라도 처리량이 낮습니다. 작은 linger.ms, 더 큰 batch.size, 더 빠른 직렬화 형식이 브로커를 추가하는 것보다 처리량을 더 향상시킬 수 있습니다. 올바른 값은 지연 시간 허용 오차에 따라 달라지므로, 다른 시스템의 기본값을 복사하는 대신 실제 레코드 크기로 테스트하십시오.

가장 안전한 성능 변경은 되돌릴 수 있고 측정 가능합니다. 클라이언트 설정은 일반적으로 파티션 수 변경보다 롤백하기 쉽습니다. 압축 변경은 일반적으로 하드웨어 변경보다 테스트하기 쉽습니다. 파티션 증가는 유용할 수 있지만 순서와 향후 키 분포에 영향을 미치므로 일반적인 클라이언트 측 튜닝 변경보다 더 많은 주의가 필요합니다.