학습 목표

왜 외부 메타데이터 저장소를 없앴는가 — KIP-500

Kafka는 2.x까지 두 개의 분산 시스템을 함께 운영해야 했습니다. 데이터는 Kafka 브로커에, 메타데이터는 외부 앙상블에 있었고, 각각 별도의 설정·모니터링·보안·백업·업그레이드 절차를 요구했습니다. KIP-500은 이 구조를 걷어내고 메타데이터를 Kafka 안으로 옮기는 제안이었습니다. 4.0에서 그 이행이 완료되어 KRaft가 유일한 모드가 되었습니다.

해결하려 한 세 가지 문제

외부 메타데이터 저장소 구조의 한계와 KRaft의 대응
문제 이전 구조에서 무슨 일이 일어났는가 KRaft에서 어떻게 달라졌는가
이중 운영 부담 서로 다른 두 시스템의 설정 언어·보안 모델·모니터링 지표·업그레이드 절차를 각각 익혀야 했습니다. 장애 시 어느 쪽 문제인지 판단하는 것부터 일이었습니다 배포·설정·인증·모니터링이 Kafka 하나로 통합됩니다. 컨트롤러도 kafka-server-start.sh로 기동하는 같은 프로세스입니다
메타데이터 확장 한계 브로커와 토픽 수가 늘어나면 외부 저장소 의존이 병목이 되어 성능과 확장성에 영향을 줬습니다. 메타데이터 상태가 두 시스템 사이에서 어긋날 여지도 있었습니다 메타데이터가 단일 파티션 로그가 되고, 브로커는 컨트롤러가 밀어 주는 것을 받는 대신 리더로부터 직접 pull합니다. 이 설계가 더 높은 파티션 수를 지원하는 근거입니다
컨트롤러 페일오버 지연 컨트롤러가 바뀌면 새 컨트롤러가 메타데이터를 처음부터 다시 읽어 상태를 만들어야 했습니다. 파티션이 많은 클러스터에서는 이 로딩 시간이 그대로 장애 시간이었습니다 스탠바이 컨트롤러가 이미 같은 로그를 따라오고 있습니다. 공식 문서 표현대로 각 컨트롤러는 "활성 컨트롤러이거나 그것의 핫 스탠바이"이므로, 전환 시 새로 로딩할 것이 없습니다
ZooKeeper 모드와 KRaft 모드 비교 — Kafka 2.x 와 4.x 아키텍처 왼쪽 Kafka 2.x 는 ZooKeeper 앙상블과 브로커라는 두 종류의 프로세스를 운영하며 메타데이터는 ZooKeeper znode 에 저장됩니다. 브로커 중 한 대가 컨트롤러를 겸하고 일부 CLI 가 --zookeeper 옵션을 사용하며 메타데이터 변경은 znode watch 로 전파됩니다. 오른쪽 Kafka 4.x 는 KRaft 전용으로 프로세스가 한 종류이며 롤만 다릅니다. controller 롤 노드들이 Raft 로 합의해 메타데이터를 __cluster_metadata 내부 로그에 저장하고 브로커는 그 로그를 복제해 캐시합니다. --zookeeper 옵션은 제거되었습니다. 맨 아래 경고는 ZooKeeper 지원이 4.0 에서 제거되었고 마지막 브리지 릴리스가 3.9 이므로 ZooKeeper 를 쓰는 클러스터는 3.9 에서 KRaft 로 먼저 마이그레이션해야 4.x 로 올릴 수 있다는 내용입니다. ZooKeeper 모드(2.x)와 KRaft 모드(4.x) 비교 Kafka 2.x — 메타데이터는 ZooKeeper znode ZooKeeper zk-1 ZooKeeper zk-2 ZooKeeper zk-3 broker-1 컨트롤러 broker-2 일반 broker-3 일반 프로세스 2종 — ZooKeeper 와 브로커를 따로 운영합니다. 컨트롤러는 브로커 중 한 대가 겸합니다. 일부 CLI 가 --zookeeper 옵션을 씁니다. 메타데이터 변경이 znode watch 로 전파됩니다. Kafka 4.x — 메타데이터는 __cluster_metadata controller 액티브 controller 팔로워 controller 팔로워 broker-1 브로커 롤 broker-2 브로커 롤 broker-3 브로커 롤 프로세스 1종 — 롤만 다른 같은 실행 파일입니다. 컨트롤러는 별도 롤이며 Raft 로 합의합니다. --zookeeper 옵션은 제거되었습니다. 브로커는 메타데이터 로그를 복제해 캐시합니다. ZooKeeper 지원은 Kafka 4.0 에서 제거되었습니다. 마지막 브리지 릴리스는 3.9 이므로, ZooKeeper 를 쓰는 클러스터는 3.9 에서 KRaft 로 먼저 마이그레이션한 뒤에야 4.x 로 올릴 수 있습니다. 이 사이트의 본문은 4.3 기준이며, 2.x 의 ZooKeeper 서술은 레거시 부록에서만 다룹니다.
이전 아키텍처와 KRaft 아키텍처 비교 — 외부 앙상블에 메타데이터를 두던 2.x 구조(좌)와 컨트롤러 쿼럼이 __cluster_metadata를 소유하는 4.x 구조(우)

KRaft 아키텍처 — 컨트롤러 쿼럼과 __cluster_metadata

컨트롤러 쿼럼과 메타데이터 로그 — Raft 복제와 스냅샷 컨트롤러 3대가 쿼럼을 이루고 그중 하나가 액티브 컨트롤러이자 Raft 리더입니다. 각 컨트롤러는 __cluster_metadata 파티션 0 의 복제본을 가지며, 쓰기는 항상 액티브 컨트롤러가 수행하고 팔로워가 복제합니다. 레코드는 과반이 복제하면 커밋됩니다. 로그가 무한정 길어지지 않도록 스냅샷으로 상태를 저장하고 앞부분을 잘라내므로 재시작 시 스냅샷과 이후 로그만 재생하면 됩니다. 브로커는 투표에 참여하지 않는 옵서버로서 메타데이터 로그만 복제해 캐시하므로 컨트롤러가 잠시 멈춰도 이미 아는 메타데이터로 읽기와 쓰기를 계속할 수 있습니다. 쿼럼 크기는 3 또는 5 를 권장하며 3대면 1대, 5대면 2대 장애까지 견딥니다. 주소는 controller.quorum.bootstrap.servers 로 지정하고 상태는 kafka-metadata-quorum.sh 로 확인합니다. 컨트롤러 쿼럼과 __cluster_metadata — 메타데이터가 복제되는 방식 컨트롤러 쿼럼 (voter) — 과반이 살아 있어야 메타데이터 쓰기가 가능합니다 controller-1 액티브 · Raft 리더 __cluster_metadata-0 1 2 3 4 5 controller-2 팔로워 (voter) __cluster_metadata-0 1 2 3 4 5 controller-3 팔로워 (voter) __cluster_metadata-0 1 2 3 4 5 쓰기는 항상 액티브 컨트롤러가 하고, 팔로워는 그 로그를 복제합니다 (Raft). 레코드는 과반 복제되면 커밋됩니다. 스냅샷 로그가 계속 길어지지 않도록 상태를 스냅샷으로 저장하고 앞부분을 잘라냅니다. 재시작 시 스냅샷 + 이후 로그만 재생하면 되므로 복구가 빠릅니다. 브로커 = 옵서버 (voter 아님) 브로커는 쿼럼 투표에 참여하지 않고 메타데이터 로그만 복제해 캐시합니다. 그래서 컨트롤러가 잠시 멈춰도 이미 아는 메타데이터로 계속 동작합니다. 쿼럼 크기는 3 또는 5 를 권장합니다 — 3대면 1대 장애, 5대면 2대 장애까지 견딥니다. 주소는 controller.quorum.bootstrap.servers 로 지정합니다 — controller.quorum.voters 는 deprecated 입니다. 상태 확인 kafka-metadata-quorum.sh --bootstrap-controller :9093 describe --status
컨트롤러 쿼럼과 메타데이터 로그 — 활성 컨트롤러가 __cluster_metadata에 쓰고, 스탠바이 컨트롤러와 브로커가 그것을 복제·재생하며, 스냅샷으로 로그를 잘라내는 구조

메타데이터도 하나의 토픽입니다

클러스터 메타데이터는 __cluster_metadata라는 내부 토픽의 단일 파티션(__cluster_metadata-0)에 기록됩니다. 토픽 생성, 파티션 리더 변경, ISR 변경, 브로커 등록, ACL, 동적 설정 변경이 모두 이 로그의 레코드입니다.

차이가 하나 있습니다. 일반 토픽은 리더가 팔로워의 fetch를 기다려 high watermark를 올리는 ISR 방식이지만, 이 파티션은 Raft 합의로 동작합니다. 즉 과반(majority)이 기록하면 커밋됩니다. 컨트롤러 3대면 2대, 5대면 3대가 받으면 확정입니다.

브로커는 이 로그의 observer입니다. 투표에 참여하지 않고 복제만 받아 자기 메모리에 메타데이터 캐시를 만듭니다. 그래서 브로커는 "메타데이터를 조회한다"기보다 "메타데이터 스트림을 구독한다"에 가깝습니다.

활성 컨트롤러가 하는 일

쓰기는 활성 컨트롤러(Raft 리더)만 합니다. 토픽 생성 요청이 오면 활성 컨트롤러가 메타데이터 레코드를 만들어 로그에 append하고, 과반이 복제하면 커밋된 것으로 처리합니다. 브로커들은 그 레코드를 받아 각자 행동합니다.

활성 컨트롤러가 죽으면 남은 컨트롤러들이 새 리더를 뽑습니다. 이때 controller.quorum.fetch.timeout.ms(기본 2000ms) 안에 리더로부터 fetch 응답을 받지 못한 팔로워가 선거를 시작하고, controller.quorum.election.timeout.ms (기본 1000ms) 안에 결론이 나지 않으면 재시도합니다. 새 리더는 이미 같은 로그를 갖고 있으므로 즉시 일을 이어받습니다.

메타데이터 스냅샷

메타데이터 로그는 계속 자랍니다. 그대로 두면 새 노드가 합류할 때마다 처음부터 전부 재생해야 하므로, Kafka는 주기적으로 스냅샷(snapshot)을 만들어 그 시점의 상태를 하나의 파일로 저장하고 그 앞의 로그를 잘라냅니다. 파일 이름은 {끝오프셋}-{에폭}.checkpoint 형태입니다.

스냅샷 생성 기준은 두 가지입니다.

메타데이터 스냅샷 관련 설정
설정기본값의미
metadata.log.max.record.bytes.between.snapshots 20971520 (20MiB) 이만큼의 레코드가 쌓이면 새 스냅샷을 만듭니다
metadata.log.max.snapshot.interval.ms 3600000 (1시간) 레코드가 적어도 이 시간이 지나면 스냅샷을 만듭니다
metadata.log.segment.bytes 1073741824 (1GiB) 메타데이터 로그의 세그먼트 롤링 크기
metadata.max.idle.interval.ms 500 변경이 없어도 이 주기로 하트비트 레코드를 append해 로그가 정지하지 않게 합니다

노드 롤 — process.roles

서버 프로세스의 역할은 process.roles로 정합니다. 허용되는 값은 broker, controller, broker,controller이며, 마지막 조합을 공식 문서는 "combined" 서버라고 부릅니다.

KRaft 노드 롤 조합 — broker, controller, combined process.roles 를 broker 로 두면 토픽 데이터를 저장하고 클라이언트 요청을 처리합니다. listeners 에는 클라이언트용 리스너만 두고, controller.listener.names 와 그 보안 설정은 필요하지만 listeners 에는 넣지 않으며 controller.quorum.bootstrap.servers 로 컨트롤러 쿼럼을 찾습니다. controller 로 두면 메타데이터만 담당하고 토픽 데이터를 저장하지 않으며, controller.listener.names 는 inter.broker.listener.name 과 같은 값일 수 없고 클라이언트는 여기에 접속하지 않습니다. 권장 대수는 3대 또는 5대입니다. broker 와 controller 를 함께 두는 combined 모드는 한 프로세스가 두 역할을 겸해 리스너를 모두 두어야 하고, 컨트롤러를 브로커와 따로 재시작하거나 확장할 수 없으며 브로커 부하로부터 격리되지 않으므로 공식 문서는 중요한 운영 환경에 권장하지 않습니다. 롤은 kafka-storage.sh format 으로 포맷할 때 정해집니다. process.roles 조합 3가지 — broker · controller · combined process.roles=broker 운영 환경 기본 · 토픽 데이터를 저장하고 클라이언트 요청을 처리합니다. listeners 에는 클라이언트용 리스너만 둡니다. · controller.listener.names 와 그 보안 설정은 필요하지만 listeners 에는 넣지 않습니다. · controller.quorum.bootstrap.servers 로 컨트롤러 쿼럼을 찾아갑니다. process.roles=controller 3대 또는 5대 · 메타데이터만 담당하고 토픽 데이터는 저장하지 않습니다. listeners 에 컨트롤러 리스너를 둡니다. · controller.listener.names 는 inter.broker.listener.name 과 같은 값일 수 없습니다. · 클라이언트는 컨트롤러에 접속하지 않습니다 — 붙어도 데이터를 읽을 수 없습니다. process.roles=broker,controller 개발 · 소규모 전용 · 한 프로세스가 두 역할을 겸합니다. listeners 에 클라이언트용과 컨트롤러용을 모두 둡니다. · 컨트롤러를 브로커와 따로 재시작하거나 확장할 수 없고, 브로커 부하로부터 격리되지 않습니다. · 공식 문서는 중요한 운영 환경에는 권장하지 않습니다. 어느 조합이든 클러스터에는 controller 롤 노드가 반드시 있어야 하고, 그 수는 홀수(3 또는 5)를 권장합니다. 롤은 포맷 시점에 정해집니다 kafka-storage.sh format --standalone -c config/server.properties
노드 롤 조합 3가지 — 전용 브로커, 전용 컨트롤러, combined(브로커+컨트롤러) 배치와 각 구성에서 클라이언트·컨트롤러 리스너가 어디에 열리는지
process.roles 값별 특징
역할 쿼럼 투표 권장 용도
broker 파티션 로그 저장, 클라이언트 요청 처리 없음 (observer) 프로덕션 데이터 노드
controller 메타데이터 관리, Raft 쿼럼 참여 참여 (voter) 프로덕션 컨트롤러 노드 3 또는 5대
broker,controller 둘 다 참여 (voter) 개발·테스트 환경. 프로덕션 비권장

combined 모드는 언제 쓰는가

공식 문서는 combined 서버가 개발 환경처럼 작은 용도에서 운영이 더 단순하다고 인정하면서, 동시에 두 가지 단점을 분명히 지적합니다.

  1. 컨트롤러가 나머지 시스템으로부터 격리되지 않습니다. 같은 JVM에서 데이터 트래픽을 처리하므로, 브로커 쪽 GC 정지나 디스크 포화가 컨트롤러의 Raft 하트비트에 그대로 영향을 줍니다.
  2. 컨트롤러와 브로커를 따로 롤링·스케일할 수 없습니다. 브로커를 재시작하면 컨트롤러도 함께 내려갑니다.

컨트롤러 수는 3 또는 5

공식 문서는 컨트롤러 노드를 보통 3대 또는 5대 선택한다고 하고, 내성 공식을 이렇게 정리합니다.

컨트롤러 수와 동시 장애 내성 — 공식 문서의 2N+1 공식
컨트롤러 수 과반 견딜 수 있는 동시 장애 비고
110단일 장애점. 개발용만
321가장 흔한 프로덕션 구성
532랙/AZ 2개 손실을 견뎌야 할 때

N대의 동시 장애를 견디려면 컨트롤러가 2N+1 필요합니다. 짝수를 쓰는 이점은 없습니다 — 4대는 3대와 같은 1대 내성을 가지면서 비용만 늘어납니다.

용량 산정도 공식 문서에 있습니다. 컨트롤러는 모든 메타데이터를 메모리와 디스크에 보관하며, 전형적인 Kafka 클러스터라면 메인 메모리 5GB와 메타데이터 로그 디렉터리 디스크 5GB로 충분하다고 명시합니다.

클러스터 부트스트랩 — kafka-storage.sh

KRaft에서는 저장 디렉터리를 사람이 명시적으로 포맷해야 합니다. 공식 문서는 이 변경의 이유를 밝히고 있습니다 — 자동 포맷은 오류 상황을 가려버리기 때문입니다. 특히 메타데이터 로그가 중요합니다. 컨트롤러 과반이 빈 로그 디렉터리로 기동할 수 있다면, 커밋된 데이터가 빠진 상태로 리더가 선출될 수 있습니다.

1단계 — 클러스터 ID 생성

클러스터 UUID 생성 (클러스터 전체에서 한 번만)
$ KAFKA_CLUSTER_ID="$(bin/kafka-storage.sh random-uuid)"
$ echo $KAFKA_CLUSTER_ID

이 ID는 클러스터에 속한 모든 노드를 포맷할 때 같은 값을 써야 합니다. 노드마다 다른 ID로 포맷하면 그 노드는 클러스터에 합류하지 못합니다.

2단계 — 첫 컨트롤러를 standalone으로 포맷

공식 문서가 권장하는 방법은 voter 한 대로 부트스트랩한 뒤 나머지 컨트롤러를 동적으로 추가하는 것입니다.

첫 컨트롤러 부트스트랩
$ bin/kafka-storage.sh format \
    --cluster-id $KAFKA_CLUSTER_ID \
    --standalone \
    --config config/controller.properties

공식 문서가 설명하는 이 명령의 결과는 두 가지입니다.

  1. metadata.log.dirmeta.properties를 만들고 무작위 directory.id를 부여합니다.
  2. 00000000000000000000-0000000000.checkpoint 스냅샷을 만들어 이 노드를 쿼럼의 유일한 voter로 지정하는 제어 레코드 (KRaftVersionRecord, VotersRecord)를 기록합니다.

대안 — 여러 컨트롤러를 한 번에 부트스트랩

--initial-controllers를 쓰면 처음부터 여러 voter로 시작할 수 있습니다. 이때 모든 컨트롤러에서 같은 값을 써야 합니다.

컨트롤러 3대를 동시에 부트스트랩
CLUSTER_ID="$(bin/kafka-storage.sh random-uuid)"
CONTROLLER_0_UUID="$(bin/kafka-storage.sh random-uuid)"
CONTROLLER_1_UUID="$(bin/kafka-storage.sh random-uuid)"
CONTROLLER_2_UUID="$(bin/kafka-storage.sh random-uuid)"

# 각 컨트롤러에서 실행 — --initial-controllers 값은 세 노드 모두 동일해야 합니다
bin/kafka-storage.sh format --cluster-id ${CLUSTER_ID} \
    --initial-controllers "0@controller-0:1234:${CONTROLLER_0_UUID},1@controller-1:1234:${CONTROLLER_1_UUID},2@controller-2:1234:${CONTROLLER_2_UUID}" \
    --config config/controller.properties

0@controller-0:1234:3Db5QLSqSZieL3rJBUUegA 형식에서 0은 replica id, controller-0은 호스트, 1234는 포트, 마지막 값은 replica directory id입니다.

3단계 — 브로커와 추가 컨트롤러 포맷

이미 존재하는 클러스터에 붙일 노드는 --no-initial-controllers로 포맷합니다.

기존 클러스터에 합류할 브로커 포맷
$ bin/kafka-storage.sh format \
    --cluster-id $KAFKA_CLUSTER_ID \
    --config config/server.properties \
    --no-initial-controllers

4단계 — 기동

컨트롤러와 브로커 기동 — 같은 스크립트를 씁니다
$ bin/kafka-server-start.sh config/controller.properties   # 컨트롤러 노드
$ bin/kafka-server-start.sh config/server.properties       # 브로커 노드

최소 설정 예시

config/controller.properties — 전용 컨트롤러 3대 중 1번
process.roles=controller
node.id=1

# 컨트롤러는 컨트롤러 리스너만 엽니다 (클라이언트 리스너 없음)
listeners=CONTROLLER://controller1.example.com:9093
controller.listener.names=CONTROLLER

# 쿼럼 위치. 모든 브로커와 컨트롤러가 이 값을 가져야 합니다
controller.quorum.bootstrap.servers=controller1.example.com:9093,controller2.example.com:9093,controller3.example.com:9093

log.dirs=/var/lib/kafka/metadata
config/server.properties — 전용 브로커
process.roles=broker
node.id=101

listeners=INTERNAL://:9092,EXTERNAL://:9094
advertised.listeners=INTERNAL://broker1.internal:9092,EXTERNAL://kafka.example.com:9094
listener.security.protocol.map=INTERNAL:PLAINTEXT,EXTERNAL:SASL_SSL,CONTROLLER:PLAINTEXT
inter.broker.listener.name=INTERNAL

# 브로커도 컨트롤러 리스너 이름과 쿼럼 위치를 알아야 합니다
controller.listener.names=CONTROLLER
controller.quorum.bootstrap.servers=controller1.example.com:9093,controller2.example.com:9093,controller3.example.com:9093

log.dirs=/var/lib/kafka/data
num.partitions=6
default.replication.factor=3
min.insync.replicas=2

정적 쿼럼과 동적 쿼럼 — voters vs bootstrap.servers

공식 문서는 KRaft를 운영하는 두 가지 방식을 구분합니다 — KIP-853 동적 컨트롤러 쿼럼기존의 정적 쿼럼입니다. 이 구분이 설정 두 개로 드러납니다.

정적 쿼럼과 동적 쿼럼 비교
구분 정적 쿼럼 동적 쿼럼 (KIP-853)
사용하는 설정 controller.quorum.voters controller.quorum.bootstrap.servers
값의 형식 {id}@{host}:{port} 목록 — 모든 컨트롤러의 id·호스트·포트를 전부 {host}:{port} 목록 — 클라이언트의 bootstrap.servers와 같은 성격
전부 나열해야 하는가 예. 브로커·컨트롤러 모든 설정 파일에 동일하게 아니오. 다만 가능한 많이 적는 편이 좋습니다
컨트롤러 추가·제거 불가 — 전체 재구성·재시작 필요 가능 — kafka-metadata-quorum.sh add-controller / remove-controller
kraft.version 0 또는 없음 1 이상
결정 시점 포맷 시점에 결정됩니다. controller.quorum.voters가 없고 --standalone · --initial-controllers · --no-initial-controllers 중 하나를 쓰면 동적 쿼럼이 됩니다

지금 어느 쪽인지 확인하는 방법

feature 버전으로 정적/동적 판별
$ bin/kafka-features.sh --bootstrap-controller localhost:9093 describe
Feature: kraft.version  SupportedMinVersion: 0  SupportedMaxVersion: 1  FinalizedVersionLevel: 0        Epoch: 7
Feature: metadata.version       SupportedMinVersion: 3.3-IV3    SupportedMaxVersion: 4.0-IV3    FinalizedVersionLevel: 4.0-IV3  Epoch: 7

kraft.versionFinalizedVersionLevel0이거나 항목이 없으면 정적 쿼럼, 1 이상이면 동적 쿼럼입니다.

feature 버전 올리기
# 전체 feature를 특정 릴리스 수준으로
$ bin/kafka-features.sh --bootstrap-server localhost:9092 upgrade --release-version 4.1

# kraft.version 만 올리기
$ bin/kafka-features.sh --bootstrap-server localhost:9092 upgrade --feature kraft.version=1

컨트롤러 추가와 제거 (동적 쿼럼)

추가는 새 컨트롤러를 포맷·기동한 뒤 복제가 따라잡은 것을 확인하고 쿼럼에 넣는 순서입니다. 제거는 반드시 프로세스를 내리기 전에 쿼럼에서 빼야 합니다.

컨트롤러 추가·제거
# 복제 진행 확인
$ bin/kafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --replication

# 추가 (브로커 엔드포인트를 쓰는 경우)
$ bin/kafka-metadata-quorum.sh --command-config config/controller.properties \
    --bootstrap-server localhost:9092 add-controller

# 제거 — 프로세스를 내리기 전에 먼저 실행하세요
$ bin/kafka-metadata-quorum.sh --bootstrap-server localhost:9092 \
    remove-controller --controller-id 3 --controller-directory-id ILZ5MPTeRWakmJu99uBJCA

KRaft 클러스터를 들여다보는 도구

kafka-metadata-quorum.sh — 쿼럼 상태

쿼럼 요약 조회
$ bin/kafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --status
ClusterId:              fMCL8kv1SWm87L_Md-I2hg
LeaderId:               3002
LeaderEpoch:            2
HighWatermark:          10
MaxFollowerLag:         0
MaxFollowerLagTimeMs:   -1
CurrentVoters:          [{"id": 3000, "directoryId": "ILZ5MPTeRWakmJu99uBJCA", "endpoints": ["CONTROLLER://localhost:9093"]},
                         {"id": 3001, "directoryId": "b-DwmhtOheTqZzPoh52kfA", "endpoints": ["CONTROLLER://localhost:9094"]},
                         {"id": 3002, "directoryId": "g42deArWBTRM5A1yuVpMCg", "endpoints": ["CONTROLLER://localhost:9095"]}]
CurrentObservers:       [{"id": 0, "directoryId": "3Db5QLSqSZieL3rJBUUegA"},
                         {"id": 1, "directoryId": "UegA3Db5QLSqSZieL3rJBU"},
                         {"id": 2, "directoryId": "L3rJBUUegA3Db5QLSqSZie"}]

CurrentVoters가 컨트롤러, CurrentObservers가 브로커입니다. MaxFollowerLag가 계속 커지면 컨트롤러 한 대가 뒤처지고 있다는 뜻이므로 디스크·네트워크를 확인해야 합니다.

kafka-dump-log.sh — 메타데이터 레코드 해독

메타데이터 로그와 스냅샷 디코딩
# 로그 세그먼트
$ bin/kafka-dump-log.sh --cluster-metadata-decoder \
    --files metadata_log_dir/__cluster_metadata-0/00000000000000000000.log

# 스냅샷
$ bin/kafka-dump-log.sh --cluster-metadata-decoder \
    --files metadata_log_dir/__cluster_metadata-0/00000000000000000100-0000000001.checkpoint

kafka-metadata-shell.sh — 대화형 탐색

스냅샷을 파일시스템처럼 탐색
$ bin/kafka-metadata-shell.sh --snapshot metadata_log_dir/__cluster_metadata-0/00000000000000007228-0000000001.checkpoint
>> ls /
brokers  local  metadataQuorum  topicIds  topics
>> ls /topics
foo
>> cat /topics/foo/0/data
{
  "partitionId" : 0,
  "topicId" : "5zoAlv-xEh9xRANKXt1Lbg",
  "replicas" : [ 1 ],
  "isr" : [ 1 ],
  "removingReplicas" : null,
  "addingReplicas" : null,
  "leader" : 1,
  "leaderEpoch" : 0,
  "partitionEpoch" : 0
}
>> exit

컨트롤러 로그 레벨 변경

동적 로그 레벨 변경은 KRaft에서도 그대로 동작합니다. 다만 컨트롤러를 대상으로 할 때는 --bootstrap-controller를 써야 합니다.

브로커와 컨트롤러의 로그 레벨 변경 — 플래그가 다릅니다
# 브로커
$ bin/kafka-configs.sh --bootstrap-server localhost:9092 \
    --entity-type broker-loggers --entity-name 1 \
    --alter --add-config org.apache.kafka.raft.KafkaNetworkChannel=TRACE

# 컨트롤러 — --bootstrap-controller 를 씁니다
$ bin/kafka-configs.sh --bootstrap-controller localhost:9093 \
    --entity-type broker-loggers --entity-name 3000 \
    --alter --add-config org.apache.kafka.raft.KafkaNetworkChannel=TRACE

3.x에서 오는 경우 — 마이그레이션 개요

4.x는 KRaft 클러스터만 업그레이드 대상으로 받습니다. 4.3 업그레이드 문서는 소프트웨어와 메타데이터 버전이 최소 3.3.x여야 한다고 명시합니다. 이전 메타데이터 저장소를 쓰던 클러스터는 4.x로 올리기 전에 마이그레이션을 끝내야 합니다.

업그레이드 경로 요약
현재 버전경로핵심 제약
2.x 2.x → 3.9 → (마이그레이션) → 4.x 3.9가 마지막 브리지 릴리스입니다. 반드시 경유해야 합니다
3.0 ~ 3.8 (구 메타데이터 저장소) 3.9로 업그레이드 → 마이그레이션 → 4.x 마이그레이션 절차는 3.9 문서에만 존재합니다. 4.x 문서에는 없습니다
3.3 ~ 3.9 (이미 KRaft) 바로 4.x로 롤링 업그레이드 소프트웨어·metadata.version 모두 3.3 이상이어야 합니다

4.x에서 사라진 설정

3.x 설정 파일을 그대로 가져오면 브로커가 알 수 없는 설정으로 기동을 거부합니다. 공식 문서가 열거하는 제거 항목 중 실무에서 자주 걸리는 것들입니다.

4.x에서 제거된 주요 설정과 대체 수단
제거된 설정무엇을 하던 설정인가지금은
zookeeper.connectzookeeper.* 전체 외부 메타데이터 저장소 접속 정보와 TLS 설정 controller.quorum.bootstrap.servers + controller.listener.names
inter.broker.protocol.version 브로커 간 프로토콜 버전 고정 metadata.version feature로 관리 — kafka-features.sh
control.plane.listener.name 컨트롤러·브로커 통신용 내부 컨트롤 플레인 controller.listener.names + listeners + listener.security.protocol.map
broker.id.generation.enable, reserved.broker.max.id 브로커 ID 자동 생성 node.id를 명시합니다 (자동 생성 없음)
leader.imbalance.per.broker.percentage preferred 리더 선출 빈도 제한 제거됨. auto.leader.rebalance.enableleader.imbalance.check.interval.seconds만 사용
controlled.shutdown.max.retries, controlled.shutdown.retry.backoff.ms 정상 종료 재시도 제어 제거됨. 종료 절차를 쿼럼 컨트롤러가 관리합니다
password.encoder.* 동적 설정의 민감 값 암호화 키 제거됨. 민감 값은 레코드에 저장되며 Kafka가 암호화하지 않습니다

이 장에서 나온 설정

KRaft 관련 설정 — Apache Kafka 4.3 공식 문서 기준 기본값
설정 기본값 설명 튜닝 포인트
process.roles (빈 값) 허용 값 broker · controller 프로덕션에서는 하나만. combined는 개발용
node.id (없음, 필수) 클러스터 안에서 이 노드를 식별하는 정수 (0 이상) 중복되면 클러스터가 깨집니다. 브로커·컨트롤러가 같은 번호 공간을 씁니다
controller.listener.names (없음, KRaft 필수) 컨트롤러 통신에 쓰는 리스너 이름 목록 브로커에도 설정해야 합니다. 브로커의 listeners에는 넣지 않습니다
controller.quorum.bootstrap.servers "" 컨트롤러 쿼럼을 찾기 위한 host:port 목록 (동적 쿼럼) 모든 브로커·컨트롤러에 설정. 가능한 많은 컨트롤러를 적으세요
controller.quorum.voters "" {id}@{host}:{port} 목록 (정적 쿼럼). 4.1에서 deprecated 동적 쿼럼을 쓸 때는 설정하지 않아야 합니다
controller.quorum.fetch.timeout.ms 2000 이 시간 안에 리더로부터 fetch를 못 받으면 새 선거를 시작 네트워크가 불안정한 환경에서 선거가 잦으면 올립니다
controller.quorum.election.timeout.ms 1000 선거가 결론나기를 기다리는 시간 초과 시 새 선거를 시작합니다
controller.quorum.append.linger.ms 25 리더가 쓰기를 모아서 복제하기까지 대기하는 시간 메타데이터 쓰기 처리량과 지연의 트레이드오프
controller.quorum.request.timeout.ms 2000 쿼럼 내부 요청 타임아웃 일반적으로 기본값을 유지합니다
controller.quorum.auto.join.enable false 컨트롤러가 voter 집합에 스스로 합류할지 여부 (KIP-853) true로 두면 voter에서 제거하기 전에 반드시 프로세스를 먼저 내려야 합니다 — 그러지 않으면 제거된 컨트롤러가 다시 자동 합류합니다
metadata.log.dir null 메타데이터 로그를 둘 디렉터리 null이면 log.dirs의 첫 번째를 씁니다. 데이터와 다른 디스크로 분리하는 것이 안전합니다
metadata.log.segment.bytes 1073741824 메타데이터 로그의 세그먼트 크기 보통 기본값을 유지합니다
metadata.log.max.record.bytes.between.snapshots 20971520 스냅샷 생성 기준 (누적 레코드 크기) 메타데이터 변경이 많으면 스냅샷이 자주 생깁니다
metadata.log.max.snapshot.interval.ms 3600000 스냅샷 생성 기준 (시간) 0으로 두면 시간 기준 생성을 끕니다
metadata.max.idle.interval.ms 500 변경이 없을 때 하트비트 레코드를 append하는 주기 0이면 비활성화됩니다
broker.heartbeat.interval.ms 2000 브로커가 컨트롤러에 하트비트를 보내는 주기 broker.session.timeout.ms와 함께 조정합니다
broker.session.timeout.ms 9000 컨트롤러가 브로커를 죽었다고 판정하는 시간 짧으면 GC 정지에 브로커가 fenced됩니다
initial.broker.registration.timeout.ms 60000 기동 시 컨트롤러에 등록을 기다리는 시간 초과하면 기동이 실패합니다. 쿼럼 주소·리스너 설정 오류의 대표 증상입니다

흔한 오해

시험 포인트 정리

확인 문제

단일 선택 · 복수 선택 · 연결형 · 순서 배열이 섞여 있습니다.

공식 문서 출처

이 장의 명령어·설정 기본값·권장 사항은 모두 아래에서 확인했습니다 (Apache Kafka 4.3 문서 기준).