본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로데이터 › 메시징 › Kafka

[Kafka] 4. Kafka Cluster

목차

Kafka 클러스터: 복제와 장애 전환을 직접 확인하기

Kafka 클러스터는 여러 서버가 토픽의 파티션을 나누어 저장하고, 각 파티션의 복제본을 다른 브로커에 배치하는 구조다. 한 브로커가 멈추어도 다른 브로커에 동기화된 복제본이 있고 클러스터 메타데이터를 관리하는 컨트롤러 정족수가 유지되면 처리를 이어갈 수 있다. 단순히 프로세스를 세 개 띄웠다는 사실만으로 장애 허용이 보장되지는 않는다. 복제 계수, 동기 복제본 수, 프로듀서 확인 설정, 서로 다른 물리 호스트에 분산했는지를 함께 봐야 한다.

이 글의 원래 실습은 2019년 ZooKeeper 기반 Kafka를 한 PC에서 실행한 기록이다. 그 당시 화면은 마지막에 보존한다. Kafka 4.x에서는 ZooKeeper 모드가 제거되었으므로, 새 실습은 Apache Kafka 4.3.1 공식 Docker 예제의 KRaft 구성을 사용한다. 아래의 세 노드는 모두 한 호스트의 컨테이너다. 따라서 브로커 프로세스 장애는 시험할 수 있지만 호스트 장애에 대한 내구성은 검증하지 못한다.

브로커, 컨트롤러, 파티션은 어떤 일을 할까

브로커는 레코드를 저장하고 클라이언트의 읽기·쓰기 요청을 처리한다. 토픽은 여러 파티션으로 나뉘며 각 파티션에는 하나의 리더와 0개 이상의 팔로어 복제본이 있다. 프로듀서는 해당 파티션의 리더에게 쓰고, 팔로어는 리더의 로그를 따라간다. ISR(in-sync replicas)은 리더와 충분히 동기화된 복제본의 집합이다. Replicas는 배치된 전체 복제본 목록이고 Isr는 그중 현재 동기 상태인 목록이므로 두 값이 항상 같지는 않다.

컨트롤러는 클러스터 메타데이터와 리더 선출을 관리한다. KRaft에서는 컨트롤러들이 정족수를 형성하며, 일반적으로 3개 컨트롤러 중 2개가 살아 있어야 과반을 유지한다. 아래 실습은 브로커와 컨트롤러 역할을 한 노드에 합친 combined 모드다. 학습에는 간단하지만 운영 환경에서는 역할을 분리하면 서로 다른 부하와 장애 범위를 독립적으로 다룰 수 있다. 자세한 조건은 Kafka KRaft 운영 문서를 따른다.

flowchart LR
  P[프로듀서] --> L[파티션 리더: broker 1]
  L --> F2[팔로어: broker 2]
  L --> F3[팔로어: broker 3]
  C[컨트롤러 정족수] --> M[메타데이터와 리더 선출]
  M --> L

그림은 한 파티션만 표현했다. 파티션마다 리더가 다를 수 있고, 토픽 전체의 저장·처리 부하는 여러 리더에 분산된다. 파티션을 늘리면 소비자 그룹에서 병렬 작업의 상한을 늘릴 수 있지만 순서는 파티션 내부에서만 유지된다. 파티션 수와 복제 계수는 서로 다른 설정이다. 복제본을 세 개 둔다고 소비자가 같은 레코드를 세 번 처리하는 것은 아니다.

구성확인할 수 있는 것한계
한 브로커, 한 호스트생산·소비 API와 토픽 기본 동작브로커 장애 시 대체 복제본 없음
여러 브로커, 한 호스트리더 선출·프로세스 장애 전환호스트/디스크 전체 장애에는 취약
여러 브로커, 여러 호스트장애 도메인 분산네트워크·보안·운영 복잡도 증가

공식 3노드 예제로 클러스터 시작

Docker Engine과 Compose 플러그인이 필요하다. 실습용으로 빈 디렉터리에서 공식 Kafka 저장소의 4.3.1 태그를 가져온다. 이미 포트 29092, 39092, 49092를 쓰는 서비스가 있다면 충돌하므로 먼저 확인한다.

git clone --branch 4.3.1 --depth 1 https://github.com/apache/kafka.git kafka-4.3.1
cd kafka-4.3.1
IMAGE=apache/kafka:4.3.1 docker compose \
  -f docker/examples/docker-compose-files/cluster/combined/plaintext/docker-compose.yml \
  up -d
IMAGE=apache/kafka:4.3.1 docker compose \
  -f docker/examples/docker-compose-files/cluster/combined/plaintext/docker-compose.yml \
  ps

예제는 kafka-1, kafka-2, kafka-3에 서로 다른 node.id를 주고 같은 클러스터 ID와 컨트롤러 정족수 설정을 공유한다. 컨테이너 내부에서는 kafka-1:19092 같은 주소를 사용하지만 호스트에서는 각기 localhost:29092, localhost:39092, localhost:49092에 연결한다. Kafka 클라이언트는 시작 주소(bootstrap.servers)로 메타데이터를 받은 뒤 광고된 주소(advertised.listeners)에 다시 접속한다. 그래서 시작 주소에 접속돼도 광고 주소가 클라이언트에서 보이지 않으면 이후 생산·소비가 실패한다.

ps에 세 컨테이너가 Up으로 나타나야 한다. 시작 직후에는 컨트롤러 선출 중일 수 있으므로 다음 명령을 몇 차례 다시 실행해 본다. 계속 실패하면 docker logs kafka-1에서 설정·포트·저장소 오류를 먼저 찾는다.

docker exec kafka-1 /opt/kafka/bin/kafka-topics.sh \
  --bootstrap-server kafka-1:19092 --list

이 실습 예제의 PLAINTEXT 리스너는 암호화와 인증을 제공하지 않는다. 인터넷에 연결된 서버에서 그대로 포트를 공개하지 않는다. 배포 환경은 TLS·인증·접근 제어와 영속 볼륨을 별도로 설계해야 한다.

복제된 토픽에 쓰고 읽기

다음 명령은 파티션 3개, 복제 계수 3인 토픽을 만든다. min.insync.replicas=2는 acks=all 쓰기가 성공하려면 필요한 동기 복제본의 최소 수다. 이 설정은 리더와 다른 복제본 한 개가 동기 상태인 동안 한 브로커 장애를 견디는 실습을 가능하게 한다. 토픽 생성은 브로커 세 개가 모두 준비된 뒤 실행한다.

docker exec kafka-1 /opt/kafka/bin/kafka-topics.sh \
  --bootstrap-server kafka-1:19092 --create \
  --topic lab-replicated --partitions 3 --replication-factor 3 \
  --config min.insync.replicas=2

docker exec kafka-1 /opt/kafka/bin/kafka-topics.sh \
  --bootstrap-server kafka-1:19092 --describe --topic lab-replicated

출력에서 PartitionCount: 3, ReplicationFactor: 3을 확인한다. 각 파티션의 Leader는 한 브로커 ID이고 Replicas에는 세 브로커 ID가 나타난다. 안정화 후 Isr에도 세 ID가 있으면 팔로어가 따라잡은 상태다. 복제 계수 3은 서로 다른 브로커 세 개에 복제본을 배치한다는 뜻이지 레코드를 서로 다른 파티션 세 개에 복사한다는 뜻이 아니다.

printf 'before-failure\n' | docker exec -i kafka-1 \
  /opt/kafka/bin/kafka-console-producer.sh \
  --bootstrap-server kafka-1:19092 --topic lab-replicated \
  --producer-property acks=all

docker exec kafka-2 /opt/kafka/bin/kafka-console-consumer.sh \
  --bootstrap-server kafka-2:19092 --topic lab-replicated \
  --from-beginning --max-messages 1 --timeout-ms 20000

소비자 출력에 before-failure가 보여야 한다. --from-beginning은 이 새 소비자 그룹에 저장된 오프셋이 없을 때 초기 위치를 앞쪽으로 잡는 데 사용한다. 기존 그룹의 재실행처럼 이미 오프셋이 있다면 같은 옵션만으로 과거부터 다시 읽지 않는다. 메시지가 없을 때 소비자가 시간 초과되는 경우, 먼저 프로듀서 종료 상태와 토픽 이름, 컨테이너 로그를 확인한다.

브로커 하나를 멈추고 리더와 ISR 관찰

kafka-1을 멈추면 세 컨트롤러 중 두 개가 남아 메타데이터 정족수가 유지된다. 토픽의 리더가 kafka-1이었다면 살아 있는 복제본에서 새 리더가 선출된다. 이 과정은 즉시 끝난다고 가정하지 말고 --describe로 확인한다.

docker stop kafka-1
docker exec kafka-2 /opt/kafka/bin/kafka-topics.sh \
  --bootstrap-server kafka-2:19092 --describe --topic lab-replicated

printf 'after-failure\n' | docker exec -i kafka-2 \
  /opt/kafka/bin/kafka-console-producer.sh \
  --bootstrap-server kafka-2:19092 --topic lab-replicated \
  --producer-property acks=all

docker exec kafka-3 /opt/kafka/bin/kafka-console-consumer.sh \
  --bootstrap-server kafka-3:19092 --topic lab-replicated \
  --from-beginning --max-messages 2 --timeout-ms 20000

Replicas에는 여전히 브로커 1이 포함되지만 Isr에서는 빠져야 한다. Leader는 멈춘 브로커를 가리키지 않아야 하며, 소비자에서 두 레코드가 확인되면 남은 브로커로 읽기·쓰기가 이어진 것이다. 장애 직후 일시적으로 리더 전환이나 메타데이터 갱신 중 예외가 나올 수 있으므로, 원인을 살펴본 뒤 잠시 후 재시도한다. 두 번째 브로커까지 멈추면 컨트롤러 과반과 min.insync.replicas=2를 모두 만족하지 못하므로 같은 결과를 기대해서는 안 된다.

docker start kafka-1
docker exec kafka-2 /opt/kafka/bin/kafka-topics.sh \
  --bootstrap-server kafka-2:19092 --describe --topic lab-replicated

돌아온 브로커가 로그를 따라잡으면 Isr에 다시 들어온다. 재시작 직후 바로 들어오지 않아도 팔로어 동기화 시간과 로그를 확인한다. 오래된 실습처럼 PID를 찾아 kill -9로 종료할 필요는 없다. 컨테이너 이름으로 중지·재시작하면 실습 범위가 분명하다.

실패 원인과 운영에서의 선택

증상먼저 확인할 것해석과 조치
토픽 생성 시 복제 계수 오류docker ps, 사용 가능한 브로커 수살아 있는 브로커 수보다 복제 계수를 크게 줄 수 없다
부트스트랩은 연결되지만 이후 타임아웃advertised.listeners, 클라이언트의 네트워크 위치클라이언트가 메타데이터에 적힌 주소로 재접속할 수 있어야 한다
acks=all 쓰기 실패토픽 min.insync.replicas와 Isr필요한 동기 복제본이 부족하면 데이터 손실을 피하려고 쓰기를 거부한다
컨슈머가 메시지를 못 봄토픽·그룹 오프셋·파티션 배정생산 성공 여부와 소비 시작 위치를 따로 확인한다
브로커 재기동 후 ISR 복귀 지연팔로어 로그, 디스크·네트워크 상태뒤처진 복제본이 리더를 따라잡을 시간을 확인한다

한 호스트에 세 브로커를 띄우면 장애 동작을 이해하기 좋지만 운영용 고가용성을 얻은 것은 아니다. 실제 배포에서는 서로 다른 호스트나 가용 영역에 복제본을 나누고, 디스크 영속성·모니터링·보안·업그레이드 절차를 마련한다. 파티션을 많이 만들면 병렬성이 늘 수 있지만 파일 핸들·메타데이터·재배치 비용도 증가한다. 파티션 수와 복제 계수는 트래픽, 순서 보장 범위, 장애 목표를 기준으로 결정한다.

실습을 끝내고 만든 컨테이너를 내릴 때는 아래 명령을 사용한다. 이 공식 예제의 데이터 경로는 컨테이너 내부의 임시 실습 저장소이므로 삭제·재생성하면 메시지가 사라질 수 있다. 보존이 필요한 환경에 그대로 사용하지 않는다.

IMAGE=apache/kafka:4.3.1 docker compose \
  -f docker/examples/docker-compose-files/cluster/combined/plaintext/docker-compose.yml \
  down

참고: Apache Kafka Docker 예제 설명, Kafka 4.3.1 클러스터 Compose 파일, Kafka KRaft, Kafka 설계: 복제와 ISR.

2019년 ZooKeeper 기반 실습 화면

다음 이미지는 원래 기록에 있던 단일 브로커 및 한 PC의 다중 브로커 실습 화면이다. 당시의 zookeeper.connect, --broker-list 등은 현재 4.x 실습 명령으로 복사하지 않는다. 옛 화면의 PID나 브로커 번호도 위의 새 예제와 관계가 없다.

2019년 단일 브로커 구성 그림 2019년 단일 브로커 토픽 목록 2019년 단일 브로커 생산·소비 결과 2019년 Kafka 데이터 흐름 그림 2019년 브로커 설정 파일 복사 2019년 여러 브로커 실행 2019년 복제 토픽 생성 2019년 최초 리더 확인 2019년 브로커 종료 화면 2019년 새 리더 확인 2019년 장애 후 생산·소비 결과 2019년 클러스터 구성 그림

같은 카테고리의 글