programming language/Kakfa

[Kafka 활용] Kafka 심화 2 - 안정적인 운영을 위한 지식 + 모니터링 도구 설정

공대키메라 2026. 7. 6. 23:31

지난 글에서 [Kafka 활용] Kafka 심화 1 - Uber System Design와 Kafka 사용에서 어느 곳에 Kafka를 적용하면 좋을지 연습했다.

 

아 드디어 올것이 왔다! 그래서 카프카를 어떻게 관리하고 구성할 것인가?

 

하드웨어적으로 메모리양이나 설정은 어떻게 하는것이 좋고 모니터링은 어떻게 하는지를 정리해서 알아보도록 하겠다.

하 귀찮아~

토끼는 귀찮아!


1. 


0. 들어가기 전에

KRaft에 대해서 다른 글도 찾게 되었다.

 

DEVOCEAN에서 KRaft에 대해서 또 잘 정리해서 참고로 읽으면 좋을 것 같아 필자도 읽어보고 다시 글 정리를 진행했다.

 

Apache Kafka의 새로운 협의 프로토콜인 KRaft에 대해(1)

Apache Kafka의 새로운 협의 프로토콜인 KRaft에 대해(2)

 

무엇보다도 현업자의 경험 공유가 내 맘에 쏙 들었다.

내 마음속에 저장~ EW

 

Kafka 시리즈를 혼자 공부하고 정리하면서 큰 개념들 위주로 보던 키메라에게 좋은 정보! 감사합니다.

 

요약을 하면 성능적인 부분, 관리적인 부분 그리고 모니터링 부분에서 

 

1. 권장 메모리 설정

다음 공식문서 Configure and Monitor KRaft 에서 다음과 같이 말한다.

 

A production KRaft server can cover a wide variety of use cases.
In general, you should run KRaft on a server with similar specifications to the following:

- Minimum of 4 GB of RAM
- Dedicated CPU core should be considered when the server is shared
- An SSD disk at least 64 GB in size is highly recommended
- JVM heap size of at least 1 GB is recommended

Currently, it is recommended that you run at least three (3) KRaft controllers in production.

출처 : https://docs.confluent.io/platform/current/kafka-metadata/config-kraft.html#hardware-and-jvm-requirements

 

KRaft도 카프카 브로커처럼 독립된 서버(프로세스)로 띄우며, 프로덕션 환경에서는 이를 분리해서 운영하는 것이 원칙이다.

 

노드는 '하나의 독립적인 실행 단위'를 뜻하는데 KRaft를 사용하면서 데이터를 저장하는 브로커 서버, 메타데이터를 관리하는 KRaft 컨트롤러 서버를 Kafka에서는 Node라고 한다.

 

그래서 Node 하나당...

 

(1) 최소 RAM의 크기는 4GB, 

(2) 서버 리소스를 다른 프로세스와 공유해서 쓰는 환경이라면, KRaft에게 전용(독점) CPU 코어를 할당하는 것을 고려

(3) 적어도 64GB 크기에 SSD disk를 추천

(4) JVM 힙메모리 사이즈 적어도 1GB를 추천

 

정리하자면 Dedicated CPU, SSD, 1GB Heap 이 세 가지 권장 사항의 목적은 모두 하나다.

 

"KRaft 프로세스가 딴짓하느라(CPU 뺏김, 디스크 지연, GC 멈춤) 합의 타이밍을 놓치는 일이 없게 만들어라"

 

이전에 실습했던 구조에서는 이렇게 구축하는게 좋다고 했다.

 

Combined(결합된) 모드 보다는 Dedicated(따로 분리된) 모드로 따로 분리해서 관리를 추천한다.

또한 카프카 관리자는 보통 컨트롤러 역할로 3대 또는 5대의 서버를 선택한다. 클러스터가 N대의 동시 장애를 견디기 위해서는 2N + 1대 (홀수)의 컨트롤러가 필요하다고 하니 잘 생각해서 하자.


물론 필자는 학습용이라서 그렇게 안할거다

 

어 잠깐... Controller 와 Broker가 그러면 노드 하나당...

 

(1) 최소 RAM의 크기는 4GB, 

(2) 서버 리소스를 다른 프로세스와 공유해서 쓰는 환경이라면, KRaft에게 전용(독점) CPU 코어를 할당하는 것을 고려

(3) 적어도 64GB 크기에 SSD disk를 추천

(4) JVM 힙메모리 사이즈 적어도 1GB를 추천

 

을 따라야 하는건가? 뭔가 이상한데 

 

필자가 참고한 공식문서의 이름 KRaft Configuration for Confluent Platform의 이름에서부터 알 수 있듯이 Controller설정시 맞는 정보이다.

 

그러면 Broker설정은 어떻게 해야하나? 하... 일일히 들여다보고 찾아보기 힘들어서 공식문서를 제공하고 정리해달라고 했다.

(공식문서 : Confluent Platform System Requirements)

 

내용은 다음과 같다.

Confluent 공식 문서(System Requirements)와 실무 베스트 프랙티스를 종합하여, 데이터를 직접 다루는 '브로커(Broker) 노드'의 프로덕션(운영) 환경 권장 사양을 요청하신 양식에 맞춰 정리해 드립니다. 메타데이터만 관리하는 컨트롤러와 달리, 엄청난 양의 데이터를 받아내야 하는 브로커는 체급 자체가 훨씬 큽니다.

[ Kafka Broker 노드 권장 사양 정리 ]
(1) 최소 RAM의 크기는 64GB를 권장
이유: 카프카 브로커 성능의 핵심은 'OS 페이지 캐시(Page Cache)'입니다. 엄청난 양의 메시지를 디스크에 쓰기 전에 램(RAM)에 임시로 올려두고 초고속으로 처리해야 하므로, 4GB로는 어림도 없으며 최소 64GB 이상의 넉넉한 물리 메모리가 필요합니다.

(2) CPU는 멀티 코어 환경(일반적으로 24 코어)을 권장 - 이유: 브로커는 컨트롤러보다 CPU 코어 수가 훨씬 많이 필요합니다. 수만 명의 클라이언트(프로듀서/컨슈머)와 동시에 네트워크 통신을 해야 하고, 전송된 대용량 데이터의 압축(Snappy, LZ4 등)을 풀거나 다시 압축하는 연산 작업에 CPU 리소스를 많이 소모하기 때문입니다.

(3) 스토리지(Disk)는 1TB 디스크 12개(총 12TB) 수준의 분산 구성(JBOD) 및 OS 디스크와의 물리적 분리를 권장
이유: 브로커는 결국 데이터를 저장하는 거대한 창고입니다. 용량 자체도 테라바이트(TB) 단위로 커야 하지만, 성능을 위해 리눅스 OS가 돌아가는 디스크(/)와 카프카 데이터가 저장되는 디스크(/var/lib/kafka/data)를 물리적으로 완전히 분리하여 디스크 I/O 병목을 막는 것이 핵심입니다.

(4) JVM 힙 메모리 사이즈는 6GB를 권장 (-Xms6G -Xmx6G)
이유: 전체 램(64GB)이 크다고 해서 JVM 힙 메모리도 똑같이 크게 잡으면 절대 안 됩니다! 자바의 가비지 컬렉션(GC)이 발생할 때 서버가 멈추는 시간(Stop-the-world)을 최소화하기 위해 힙 메모리는 6GB 정도로 작게 제한해야 합니다. 이렇게 되면 남는 50GB 이상의 광활한 메모리를 리눅스 OS가 '페이지 캐시'로 알아서 가져다 쓰며 카프카의 미친 처리 속도를 만들어냅니다.

[ 한 줄 요약 ]
- KRaft 컨트롤러: 머리는 좋아야 하지만 덩치는 작아도 되는 '지휘관' (4GB RAM, 4 Core, 64GB SSD)
- 브로커: 무거운 짐을 빠르게, 많이 날라야 하는 근육질 '운반꾼' (64GB RAM, 24 Core, 대용량 다중 디스크)

 

 

하... 이렇게 RAM과 Core갯수를 이렇게 무시무시하게 구축하라고?

 

말도안되게 무지막지한 사양을 scale up 으로 구축하기에는 비용이 말도 안되기 때문에 초기에는 조그마하게 시작해서, 여러개 연결을 하는게 맞는 것 같다. 그리고 점점 부하를 늘려서 이를 테스트해보는 것이다. 애초에 감당될 수준으로 하면 되는것이지, AWS에서 저 용량으로 서버 세팅하면 개털이 되어버릴 것이다!

 

 2. 로그 설정

카프카는 애플리케이션 로그 관리를 위해 자바 기반의 로깅 유틸리티인 아파치 log4j2를 이용한다.

 

yml 파일을 다시 공유하고 log 설정이 어떻게 되어 있는지 읽어보도록 하자.

 

다만 JMX를 사용해 메트릭 모니터링을 하기 위해 environment 설정에 KAFKA_JMX_OPTS 설정을 추가햇다.

 

docker-kraft-compose.yml

networks:
  kafka-network:
    name: kafka-kraft-net
    driver: bridge

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    networks:
      - kafka-network
  kafka-kraft-1:
    image: apache/kafka:latest
    container_name: kafka-kraft-1
    user: root  # 볼륨 디렉터리 권한 충돌 방지
    ports:
      - "19092:19092"
      - "9404:9404" # 프로메테우스가 데이터를 긁어갈 포트
    environment:
      # [1. 클러스터 고유 식별 키] 3대의 노드가 동일한 하나의 ID를 공유해야 같은 클러스터로 결합됩니다.
      CLUSTER_ID: 'O1N_F_D-S2K6V8-A_G-p-A'
      # [2. 노드 고유 번호] 클러스터 내부에서 이 노드를 식별하는 이름표입니다.
      KAFKA_NODE_ID: 1
      # [3. KRaft 아키텍처 역할 설정 (Combined Mode)]
      # 한 노드가 브로커(데이터 저장)와 컨트롤러(클러스터 통제망 및 투표권) 역할을 동시에 담당합니다.
      # 실무 유의사항: Confluent 및 카프카 공식 문서에서는 프로덕션 환경 운영 시 리소스 경합 및
      # 장애 전파를 방지하기 위해 두 역할을 분리(Isolated Mode)하여 서버를 독자 구축하도록 강력하게 권장합니다.
      # 다만, 현재처럼 아키텍처를 학습하고 장애 테스트를 하는 로컬 환경에서는 이 결합 모드가 매우 효율적입니다.
      KAFKA_PROCESS_ROLES: 'controller,broker'
      # [4. KRaft 이사회 명단 (Quorum Voters)]
      # 주키퍼 없이 합의(Raft 알고리즘)를 진행할 투표권자들의 통신 주소 명단입니다. (과반인 2대 이상 생존 필수)
      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-kraft-1:9093,2@kafka-kraft-2:9093,3@kafka-kraft-3:9093'
      # [5. 네트워크 격리 및 리스너 매핑]
      # 일반 데이터 송수신망(INTERNAL/EXTERNAL)과 통제용 비밀 통로(CONTROLLER) 네트워크 포트를 철저히 격리합니다.
      KAFKA_LISTENERS: 'INTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093,EXTERNAL://0.0.0.0:19092'
      KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://kafka-kraft-1:9092,EXTERNAL://localhost:19092'
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT'
      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
      KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'
      # [6. 로그 및 디스크 메타데이터 저장 디렉토리]
      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
      # [7. JMX 를 이용한 카프카 메트릭 모니터링을 위한 설]
      KAFKA_JMX_OPTS: "-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=0.0.0.0 -Dcom.sun.management.jmxremote.rmi.port=9991 -Dcom.sun.management.jmxremote.port=9991"
      KAFKA_OPTS: "-javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent.jar=9404:/opt/jmx_exporter/kafka-kraft.yml"
    volumes:      # [8. 데이터 지속성 보장 (장애 재선출 핵심 설정)]
      # 컨테이너를 강제로 중단(stop)시켰다가 재기동해도 과거 투표 에포크(Epoch) 이력이 유실되지 않도록 가상 볼륨을 매핑합니다.
      - kafka-kraft-1-data:/tmp/kraft-combined-logs
      # 추가: jar와 yml 파일을 컨테이너 안으로 복사
      - ./jmx_prometheus_javaagent.jar:/opt/jmx_exporter/jmx_prometheus_javaagent.jar
      - ./kafka-kraft.yml:/opt/jmx_exporter/kafka-kraft.yml
    networks:
      - kafka-network

  kafka-kraft-2:
    image: apache/kafka:latest
    container_name: kafka-kraft-2
    user: root
    ports:
      - "29092:29092"
      - "9405:9405"
    environment:
      CLUSTER_ID: 'O1N_F_D-S2K6V8-A_G-p-A'
      KAFKA_NODE_ID: 2
      KAFKA_PROCESS_ROLES: 'controller,broker'
      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-kraft-1:9093,2@kafka-kraft-2:9093,3@kafka-kraft-3:9093'
      KAFKA_LISTENERS: 'INTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093,EXTERNAL://0.0.0.0:29092'
      KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://kafka-kraft-2:9092,EXTERNAL://localhost:29092'
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT'
      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
      KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'
      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
      KAFKA_JMX_OPTS: "-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=0.0.0.0 -Dcom.sun.management.jmxremote.rmi.port=9992 -Dcom.sun.management.jmxremote.port=9992"
      KAFKA_OPTS: "-javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent.jar=9405:/opt/jmx_exporter/kafka-kraft.yml"
    volumes:
      - kafka-kraft-2-data:/tmp/kraft-combined-logs
      - ./jmx_prometheus_javaagent.jar:/opt/jmx_exporter/jmx_prometheus_javaagent.jar
      - ./kafka-kraft.yml:/opt/jmx_exporter/kafka-kraft.yml
    networks:
      - kafka-network

  kafka-kraft-3:
    image: apache/kafka:latest
    container_name: kafka-kraft-3
    user: root
    ports:
      - "39092:39092"
      - "9406:9406"
    environment:
      CLUSTER_ID: 'O1N_F_D-S2K6V8-A_G-p-A'
      KAFKA_NODE_ID: 3
      KAFKA_PROCESS_ROLES: 'controller,broker'
      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-kraft-1:9093,2@kafka-kraft-2:9093,3@kafka-kraft-3:9093'
      KAFKA_LISTENERS: 'INTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093,EXTERNAL://0.0.0.0:39092'
      KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://kafka-kraft-3:9092,EXTERNAL://localhost:39092'
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT'
      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
      KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'
      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
      KAFKA_JMX_OPTS: "-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=0.0.0.0 -Dcom.sun.management.jmxremote.rmi.port=9993 -Dcom.sun.management.jmxremote.port=9993"
      KAFKA_OPTS: "-javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent.jar=9406:/opt/jmx_exporter/kafka-kraft.yml"
    volumes:
      - kafka-kraft-3-data:/tmp/kraft-combined-logs
      - ./jmx_prometheus_javaagent.jar:/opt/jmx_exporter/jmx_prometheus_javaagent.jar
      - ./kafka-kraft.yml:/opt/jmx_exporter/kafka-kraft.yml
    networks:
      - kafka-network

volumes:
  kafka-kraft-1-data:
  kafka-kraft-2-data:
  kafka-kraft-3-data:

 

 

해당 yml 파일을 이용해서 docker 을 실행했다.

 

$ docker compose -f docker-kraft-compose.yml up -d

 

그리고 다음에 접속한다.

$ docker exec -it kafka-kraft-1 /bin/bash

 

그 다음 kafka config 내용을 확인한다.

 

826946ab6420:/opt/kafka/config# ls -al
total 100
drwxr-xr-x 1 appuser appuser 4096 Jul  5 05:15 .
drwxr-xr-x 1 appuser appuser 4096 Jul  5 05:15 ..
-rw-r--r-- 1 appuser appuser 6101 Feb 10 09:27 broker.properties
-rw-r--r-- 1 appuser appuser  906 Feb 10 09:27 connect-console-sink.properties
-rw-r--r-- 1 appuser appuser  909 Feb 10 09:27 connect-console-source.properties
-rw-r--r-- 1 appuser appuser 5475 Feb 10 09:27 connect-distributed.properties
-rw-r--r-- 1 appuser appuser  883 Feb 10 09:27 connect-file-sink.properties
-rw-r--r-- 1 appuser appuser  881 Feb 10 09:27 connect-file-source.properties
-rw-r--r-- 1 appuser appuser 1486 Feb 10 09:27 connect-log4j2.yaml
-rw-r--r-- 1 appuser appuser 2540 Feb 10 09:27 connect-mirror-maker.properties
-rw-r--r-- 1 appuser appuser 2262 Feb 10 09:27 connect-standalone.properties
-rw-r--r-- 1 appuser appuser 5863 Feb 10 09:27 consumer.properties
-rw-r--r-- 1 appuser appuser 6075 Feb 10 09:27 controller.properties
-rw-r--r-- 1 root    root    3407 Jul  5 05:15 log4j2.yaml
-rw-r--r-- 1 appuser appuser 5676 Feb 10 09:27 producer.properties
-rw-r--r-- 1 appuser appuser  490 Jul  5 05:15 server.properties
-rw-r--r-- 1 root    root     311 Jul  5 05:15 tools-log4j2.yaml
-rw-r--r-- 1 appuser appuser 1169 Feb 10 09:27 trogdor.conf

 

그런데 나는 connect 관련해서 뭐한게 없는데 connnet 관련 설정 파일이 있다. 

 

생각해보면 Kafka connect 니 Kafka Stream이니 지원해주는 것이 많다. 

 

 

  • "일단 외부 시스템과 데이터 연동이 필요해?" -> Kafka Connect (설정으로 1시간 만에 끝내자)
  • "데이터를 실시간으로 가공해서 보여줘야 해?" -> Kafka Streams (코드 몇 줄로 집계 로직을 짜자)

 

도구 리얼타임 처리 관점 한 줄 정의
Kafka Connect 데이터 이동 속도에 의존 (이동형) 외부 시스템과 카프카 사이의 배달 파이프라인
Kafka Streams 마이크로초(μs) 단위의 실시간 처리 (가공형) 카프카 토픽 위에서 돌아가는 실시간 가공 엔진

 

이러한 것들을 사용해서 최대한 작업하되, 세부적인 커스터마이징이 필요한 순간이 오면 Producer, Consumer 까지 low-level한 코드를 손봐야 하는 상황이 오는 것이다.

 

하여간 log4j2.yml 파일을 살펴보면 다음과 같다.

 

826946ab6420:/opt/kafka/config# cat log4j2.yaml
Configuration:
  Properties:
    Property:
    - name: "kafka.logs.dir"
      value: "."
    - name: "logPattern"
      value: "[%d] %p %m (%c)%n"
  Appenders:
    Console:
      name: "STDOUT"
      PatternLayout:
        pattern: "${logPattern}"
    RollingFile:
    - name: "KafkaAppender"
      fileName: "${sys:kafka.logs.dir}/server.log"
      filePattern: "${sys:kafka.logs.dir}/server.log.%d{yyyy-MM-dd-HH}"
      PatternLayout:
        pattern: "${logPattern}"
      TimeBasedTriggeringPolicy:
        modulate: true
        interval: 1
    - name: "StateChangeAppender"
      fileName: "${sys:kafka.logs.dir}/state-change.log"
      filePattern: "${sys:kafka.logs.dir}/state-change.log.%d{yyyy-MM-dd-HH}"
      PatternLayout:
        pattern: "${logPattern}"
      TimeBasedTriggeringPolicy:
        modulate: true
        interval: 1
    - name: "RequestAppender"
      fileName: "${sys:kafka.logs.dir}/kafka-request.log"
      filePattern: "${sys:kafka.logs.dir}/kafka-request.log.%d{yyyy-MM-dd-HH}"
      PatternLayout:
        pattern: "${logPattern}"
      TimeBasedTriggeringPolicy:
        modulate: true
        interval: 1
    - name: "CleanerAppender"
      fileName: "${sys:kafka.logs.dir}/log-cleaner.log"
      filePattern: "${sys:kafka.logs.dir}/log-cleaner.log.%d{yyyy-MM-dd-HH}"
      PatternLayout:
        pattern: "${logPattern}"
      TimeBasedTriggeringPolicy:
        modulate: true
        interval: 1
    - name: "ControllerAppender"
      fileName: "${sys:kafka.logs.dir}/controller.log"
      filePattern: "${sys:kafka.logs.dir}/controller.log.%d{yyyy-MM-dd-HH}"
      PatternLayout:
        pattern: "${logPattern}"
      TimeBasedTriggeringPolicy:
        modulate: true
        interval: 1
    - name: "AuthorizerAppender"
      fileName: "${sys:kafka.logs.dir}/kafka-authorizer.log"
      filePattern: "${sys:kafka.logs.dir}/kafka-authorizer.log.%d{yyyy-MM-dd-HH}"
      PatternLayout:
        pattern: "${logPattern}"
      TimeBasedTriggeringPolicy:
        modulate: true
        interval: 1
  Loggers:
    Root:
      level: "INFO"
      AppenderRef:
      - ref: "STDOUT"
      - ref: "KafkaAppender"
    Logger:
    - name: "org.apache.kafka.controller"
      level: "INFO"
      additivity: false
      AppenderRef:
        ref: "ControllerAppender"
    - name: "org.apache.kafka.storage.internals.log.LogCleaner"
      level: "INFO"
      additivity: false
      AppenderRef:
        ref: "CleanerAppender"
    - name: "org.apache.kafka.storage.internals.log.Cleaner"
      level: "INFO"
      additivity: false
      AppenderRef:
        ref: "CleanerAppender"
    - name: "org.apache.kafka"
      level: "INFO"
    - name: "kafka.request.logger"
      level: "WARN"
      additivity: false
      AppenderRef:
        ref: "RequestAppender"
    - name: "kafka"
      level: "INFO"
    - name: "kafka.authorizer.logger"
      level: "INFO"
      additivity: false
      AppenderRef:
        ref: "AuthorizerAppender"
    - name: "kafka.network.RequestChannel$"
      level: "WARN"
      additivity: false
      AppenderRef:
        ref: "RequestAppender"
    - name: "state.change.logger"
      level: "INFO"
      additivity: false
      AppenderRef:
        ref: "StateChangeAppender"
    - name: "org.apache.kafka.storage.internals.log.LogCleaner$CleanerThread"
      level: "INFO"
      additivity: false
      AppenderRef:
        ref: "CleanerAppender"

 

log4j2.yml 설정 관련해서는 다음 공식문서를 참고하면 된다. (참고 : Configuration file)

 

크게 정리하면 다음과 같다.

 

  • server.log: Kafka 브로커의 전반적인 동작 상태와 일반적인 예외를 기록하는 메인 애플리케이션 로그입니다.
  • state-change.log: 파티션의 리더 선출, 동기화(ISR) 상태 등 클러스터 내 상태 변경 이벤트를 기록하여 고가용성(HA) 이슈 추적에 사용됩니다.
  • kafka-request.log: 네트워크 요청의 처리 지연(Latency) 및 실패 내역을 기록하여 대용량 트래픽 처리 시 병목과 성능 저하 원인을 파악합니다.
  • log-cleaner.log: 메시지 보존 주기(Retention)에 따른 삭제 및 로그 압축(Compaction) 등 디스크 스토리지 정리 작업의 내역을 기록합니다.
  • controller.log: 토픽 생성/삭제, 브로커 합류 등 클러스터 전체의 메타데이터와 상태를 제어하는 컨트롤러의 의사결정 과정을 기록합니다.
  • kafka-authorizer.log: 클라이언트의 접근 권한(ACL) 검증 및 보안 인증(Allow/Deny) 결과를 기록하는 감사(Audit) 로그입니다.

 

이것은 맨 상단에 보면 저장되는 directory가 있다.

 

name: "kafka.logs.dir"

 

기본적으로 log4j2 를 사용하니 설정을 바꾸면 log 가 원하는 형식으로 저장될 것이다.

 

3. 모니터링 설정 - 프로메테우스와 그라파나

문제는 카프카와 프로메테우스가 서로 다른 언어를 쓴다는 데서 출발한다.

  • 카프카는 JVM 애플리케이션이라 내부 메트릭(초당 메시지 수, 파티션 상태, 컨트롤러 정보 등)을 JMX MBean이라는 자바 표준 방식으로만 노출한다. JConsole 같은 자바 도구로만 읽을 수 있는 형태
  • 프로메테우스는 자바를 모른다(아모른직다). 오직 HTTP GET으로 /metrics 엔드포인트를 긁어서 텍스트 형식으로 수집하는 것만 할 줄 안다.

그래서 둘 사이에 통역사가 필요하고, 그 역할이 jmx_prometheus_javaagent.jar(JMX Exporter)이다.

 

JMX Exporter가 카프카 JVM 안에 상주하면서 MBean을 읽어 프로메테우스가 이해하는 HTTP 텍스트로 변환해준다.

 

위의 docker-kraft-compose.yml 파일을 설정하기 위해서는 prometheus.yml 파일도 생성해줘야하고, promethenus용 jar 파일도 다운받아야 한다.

 

프로메테우스 공식 문서에서는 다음과 같은 아키텍처를 그림으로 보여준다.

 

https://prometheus.io/docs/introduction/overview/

 

시간이 되면 프로메테우스에 대해서도 깊게 공부하는게 좋겠지만, 현재는 Kafka에 집중하기로 하자.

 

프로메테우스도 그래프를 그릴 수 있고, 그라파나로도 그릴 수 있는데 그라파나의 경우 대시보드를 커스텀할 수 있고 다른 사람이 만든 대시보드를 이용할 수 있다고 한다.

 

그렇기 때문에 필자도 그라파나를 달아보도록 하겠다.

 

3.1) 흐름도 파악하기

카프카 JVM 내부 (JMX MBean)
      │
      ▼  -javaagent 로 JVM에 얹힘
JMX Exporter (jar)  ── kafka-kraft.yml rule로 이름 변환
      │
      ▼  HTTP :9404 /metrics 노출
프로메테우스  ── prometheus.yml 설정대로 15초마다 scrape

 

3.2) jar 다운로드 (호스트에서 1회)

wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/1.0.1/jmx_prometheus_javaagent-1.0.1.jar -O jmx_prometheus_javaagent.jar

 

3.3) compose에서 jar와 rule 파일을 컨테이너 안으로 마운트

volumes:
  - ./jmx_prometheus_javaagent.jar:/opt/jmx_exporter/jmx_prometheus_javaagent.jar
  - ./kafka-kraft.yml:/opt/jmx_exporter/kafka-kraft.yml

 

3.4) 카프카 JVM 기동 시 javaagent로 로딩

KAFKA_OPTS: "-javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent.jar=9404:/opt/jmx_exporter/kafka-kraft.yml"

형식은 -javaagent:{jar경로}={노출포트}:{rule파일경로}입니다. 즉 "이 jar를 JVM에 붙이고, 9404 포트로 메트릭을 노출하되, 변환 규칙은 이 yml을 따라라"는 뜻이에요. 노드별로 9404 / 9405 / 9406을 씁니다.

 

3.5) prometheus.yml 설정 + kafka-kraft.yml 설정

prometheus.yml 설정

global:
  scrape_interval: 15s       # 15초마다 타겟(카프카)에 접속해서 메트릭 데이터를 가져옵니다.
  evaluation_interval: 15s   # 규칙(알람 등)을 15초마다 평가합니다.

scrape_configs:
  - job_name: 'kafka-kraft'   # 이 작업의 이름을 지정합니다. (그라파나에서 이 이름으로 그룹화됨)
    static_configs:
      - targets:               # 데이터를 긁어올 대상 서버의 주소와 포트 리스트입니다.
          - 'kafka-kraft-1:9404'
          - 'kafka-kraft-2:9405'
          - 'kafka-kraft-3:9406'
        labels:                # 수집된 모든 데이터에 'cluster: local-kraft'라는 꼬리표를 붙입니다.
          cluster: 'local-kraft' # 나중에 여러 클러스터를 모니터링할 때, 누구네 데이터인지 구분하는 핵심 기준이 됩니다.

 

 kafka-kraft.yml 

lowercaseOutputName: true  # 대문자를 소문자로 바꾸기 (프로메테우스 표준)
rules:
- pattern: "kafka.server<type=(.+)><>(.+):" # JMX 데이터의 패턴을 찾음
  name: "kafka_server_$1_$2"               # 데이터를 이 이름으로 바꿈

 

카프카가 노출하는 JMX 데이터는 아주 상세하지만, 읽기 불편한 형태로 kafka-kraft.yml 를 이용해서 읽기 쉽게 가공을 해준다고 한다.

 

그렇게 설정된 파일을 실행하면 다음과 같이 prometheus가 띄워진 것을 확인할 수 있다.

$ docker ps
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS          PORTS                                                                                                    NAMES
08f3042e629b   apache/kafka:latest      "/__cacert_entrypoin…"   18 minutes ago   Up 18 minutes   0.0.0.0:9406->9406/tcp, [::]:9406->9406/tcp, 9092/tcp, 0.0.0.0:39092->39092/tcp, [::]:39092->39092/tcp   kafka-kraft-3
c3c7c17c6997   apache/kafka:latest      "/__cacert_entrypoin…"   18 minutes ago   Up 18 minutes   0.0.0.0:9404->9404/tcp, [::]:9404->9404/tcp, 9092/tcp, 0.0.0.0:19092->19092/tcp, [::]:19092->19092/tcp   kafka-kraft-1
bd292d0cc134   apache/kafka:latest      "/__cacert_entrypoin…"   18 minutes ago   Up 18 minutes   0.0.0.0:9405->9405/tcp, [::]:9405->9405/tcp, 9092/tcp, 0.0.0.0:29092->29092/tcp, [::]:29092->29092/tcp   kafka-kraft-2
0c6d6c87fe29   prom/prometheus:latest   "/bin/prometheus --c…"   18 minutes ago   Up 18 minutes   0.0.0.0:9090->9090/tcp, [::]:9090->9090/tcp                                                              prometheus

 

자! 이제 각각 kafka 서버에 대해서 9404, 9405, 9406으로 포트를 뚫어 놨으니 metrics 분석 요청을 보낼 수 있다.

 

curl -s http://localhost:9404/metrics   # kafka-kraft-1
curl -s http://localhost:9405/metrics   # kafka-kraft-2
curl -s http://localhost:9406/metrics   # kafka-kraft-3

 

요청을 보내면 어마어마한 로그가 쏟아진다.

 

$ curl -s http://localhost:9404/metrics | grep -c "^kafka" 
1106

 

kafka 관련 로그가 1106개나 있다는 것이다. 그 말은 즉 JMX Exporter 가 kafka 의 응답을 잘 읽어내고 있다는 말이다.

 

docker 에서 우리는 또 prometheus 서버를 띄웠는데 9090 포트를 이용했다.

 

 

3.6) 그라파나 세팅

 

자 그럼 이제 그라파나 세팅해보자!

 

services: 밑에 들여쓰기 잘 해서 다음 코드를 추가하자.

 

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - grafana-data:/var/lib/grafana
    networks:
      - kafka-network
    depends_on:
      - prometheus
      
 ... 
 
 맨 하단 volumn 에 추가
 volumes:
  grafana-data:

 

아오! 다시 할 때 마다 뭐가 꼬인다. 어짜피 뭐... 테스트중이니까 다 지워버리고 다시 깔끔하게 하겠다.

 

docker compose -f docker-kraft-compose.yml down -v
docker compose -f docker-kraft-compose.yml up -d

 

다 지워버려!

 

그 다음 grafana가 실행되었는지 확인해보았다.

 

$ docker ps | grep grafana
8bb44583805e   grafana/grafana:latest   "/run.sh"    17 seconds ago   Up 17 seconds   0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp  grafana
$

 

 

 

아이디 : admin / 비밀번호 : admin 으로 설정하고 들어갔다. 비밀번호 변경은 skip했다.

 

그 다음으로 자세히 보면 왼쪽 메뉴에 Connections > Data souces 를 열고 http://prometheus:9090 을 등록하고 맨 아래에 저장 버튼을 클릭하면 된다.

Grafana에 프로메테우스를 연결하는 모습

 

아니 뭐가 이렇게 많아?

 

해당 화면은 프로메테우스라는 시계열 데이터베이스에 적재된 원시 데이터(Raw Data)를 RDBMS의 SELECT * 쿼리처럼 날것 그대로 조회하는 화면으로 실무에서는 그렇게 하지 않는단다.

 

필자는 11962 대시보드를 import했다.

 

Kafka는 높은 성능을 내기 위해 OS의 페이지 캐시(Page Cache)를 적극적으로 활용하고 디스크 I/O에 크게 의존한다고 한다. 

그래서 브로커 서버의 CPU, 메모리, 디스크 I/O, 네트워크 대역폭을 모니터링하는 것이 필수적이니 11074 대시보드를 기본적으로 권장한다.

 

 

여기서 찾다보니 더 흥미로운 점은 kakaocloud 의 공식문서에서 Prometheus를 활용한 Kafka consumer lag 모니터링을 하는 방법에 대해 소개한다. (Prometheus를 활용한 Kafka consumer lag 모니터링

 

필자에게는 이 그라파나와 친해지는게 급선무니 무시하도록 위 lag 모니터링은 무시하도록 하겠다.

 

 

오잉? 하나도 안나오는데? 

 

이유는 import 해오는 dashboard인 11962 에 있다.

 

지금 그라파나에 새로 씌워두신 카프카 전용 대시보드(11962)는 내부적으로 데이터베이스(프로메테우스)에 쿼리를 날릴 때 "이름표(job)가 kafka로 붙어있는 데이터들 싹 다 가져와!" 라고 하드코딩(또는 기본값) 되어 있다.

 

prometheus.yml 파일 수정

scrape_configs:
  - job_name: 'kafka'  # '-kraft' 부분을 지워주세요!
    static_configs:
      - targets:
          - 'kafka-kraft-1:9404'
# ... (아래는 동일)

 

기존의 job_name을 수정하도록 하자.

 

요로콤 수정!

 

이전것들은 다 삭제하고...

 

 

 

아참! Data sources도 prometheus 를 추가해주는걸 잊지 말자.

 

11962 variables를 적절하게 설정하자. 

 

 

우측에 설정 버튼을 클릭하면 Settings 버튼이 나온다. 이걸 클릭하면 다음이 나온다.

 

 

그 다음 해당 명령어를 실행해서 test-topic을 생성하자.

 

docker exec -it kafka-kraft-1 /opt/kafka/bin/kafka-topics.sh \
  --create --topic test-topic --bootstrap-server localhost:9092 \
  --partitions 3 --replication-factor 3

 

그러면 다음과 같이 topic갯수랑 replication 갯수도 나온다.

 

 


 

내가 이거까지 해야하는가... 싶기도 햇고 책의 내용을 따라가다보니 막혀서 최대한 AI를 활용해서 하루 종일 물어봤다.

 

AI가 정말 많은것을 알려주긴 하지만 모르는것을 명확하게 물어보는것도 문제다.

 

덕분에 Grafana, prometheus같은 툴이 있고 어찌저찌 연결하면 우선 띄우는것 까지는 알아보았다.