Docker compose로 Kafka cluster 구성하기

Park
17 min readDec 6, 2023

--

이전에 kafka 서버 하나로 pub/sub 예제를 구현해본 적이 있었는데, 기록을 안하니 잘 안떠오르는 것 같아서 이번에는 정리해보기로 했다.

1. docker-desktop 또는 OrbStack 설치

얘네 실행을 안한 채로 docker-compose 명령을 실행하면 아래 오류가 뜬다.
여기서는 OrbStack을 사용했다.

Cannot connect to the Docker daemon at unix:///Users/username/.orbstack/run/docker.sock. Is the docker daemon running?

2. docker-compose.yml 작성

파일은 아래 주소에 있는 것을 복사해서 사용했다.
kafka 3개와 kafka-ui를 받는다.

networks:
kafka_network:

volumes:
Kafka00:
driver: local
Kafka01:
driver: local
Kafka02:
driver: local

services:
##Kafka 00
Kafka00Service:
image: bitnami/kafka:3.5.1-debian-11-r72
restart: unless-stopped
container_name: Kafka00Container
ports:
- '10000:9094'
environment:
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true
# KRaft settings
- KAFKA_CFG_BROKER_ID=0
- KAFKA_CFG_NODE_ID=0
- KAFKA_KRAFT_CLUSTER_ID=HsDBs9l6UUmQq7Y5E6bNlw
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@Kafka00Service:9093,1@Kafka01Service:9093,2@Kafka02Service:9093
- KAFKA_CFG_PROCESS_ROLES=controller,broker
# Listeners
- ALLOW_PLAINTEXT_LISTENER=yes
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://Kafka00Service:9092,EXTERNAL://127.0.0.1:10000
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
# Clustering
- KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
networks:
- kafka_network
volumes:
- "Kafka00:/bitnami/kafka"
##Kafka 01
Kafka01Service:
image: bitnami/kafka:3.5.1-debian-11-r72
restart: unless-stopped
container_name: Kafka01Container
ports:
- '10001:9094'
environment:
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true
# KRaft settings
- KAFKA_CFG_BROKER_ID=1
- KAFKA_CFG_NODE_ID=1
- KAFKA_KRAFT_CLUSTER_ID=HsDBs9l6UUmQq7Y5E6bNlw
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@Kafka00Service:9093,1@Kafka01Service:9093,2@Kafka02Service:9093
- KAFKA_CFG_PROCESS_ROLES=controller,broker
# Listeners
- ALLOW_PLAINTEXT_LISTENER=yes
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://Kafka01Service:9092,EXTERNAL://127.0.0.1:10001
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
# Clustering
- KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
networks:
- kafka_network
volumes:
- "Kafka01:/bitnami/kafka"
##Kafka 02
Kafka02Service:
image: bitnami/kafka:3.5.1-debian-11-r72
restart: unless-stopped
container_name: Kafka02Container
ports:
- '10002:9094'
environment:
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true
# KRaft settings
- KAFKA_CFG_BROKER_ID=2
- KAFKA_CFG_NODE_ID=2
- KAFKA_KRAFT_CLUSTER_ID=HsDBs9l6UUmQq7Y5E6bNlw
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@Kafka00Service:9093,1@Kafka01Service:9093,2@Kafka02Service:9093
- KAFKA_CFG_PROCESS_ROLES=controller,broker
# Listeners
- ALLOW_PLAINTEXT_LISTENER=yes
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://Kafka02Service:9092,EXTERNAL://127.0.0.1:10002
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
# Clustering
- KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
networks:
- kafka_network
volumes:
- "Kafka02:/bitnami/kafka"

KafkaWebUiService:
image: provectuslabs/kafka-ui:latest
restart: unless-stopped
container_name: KafkaWebUiContainer
ports:
- '8080:8080'
environment:
- KAFKA_CLUSTERS_0_NAME=Local-Kraft-Cluster
- KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=Kafka00Service:9092,Kafka01Service:9092,Kafka02Service:9092
- DYNAMIC_CONFIG_ENABLED=true
- KAFKA_CLUSTERS_0_AUDIT_TOPICAUDITENABLED=true
- KAFKA_CLUSTERS_0_AUDIT_CONSOLEAUDITENABLED=true
#- KAFKA_CLUSTERS_0_METRICS_PORT=9999
networks:
- kafka_network
  • container_name: 컨테이너 이름을 지정
  • ports: docker 내부 포트: docker 외부 포트
    터미널에서 쓸 때는 왼쪽, 타 시스템에서 접속할 때는 오른쪽 포트를 사용
  • ALLOW_PLAINTEXT_LISTENER: 카프카 브로커가 Plaintext 프로토콜을 사용하는 리스너(Listener)를 허용할지 여부를 결정하는 속성
  • KAFKA_CFG_LISTENERS: 클라이언트가 Kafka 브로커에 연결을 연결할 때 사용할 리스너(Listener)를 정의
  • KAFKA_CFG_ADVERTISED_LISTENERS: 클라이언트가 브로커에 연결할 때 사용할 주소를 결정
    - PLAINTEXT://Kafka02Service:9092 => Docker 내부에서 브로커에 접속할 때 사용하는 리스너 (터미널에서 사용)
    - EXTERNAL://127.0.0.1:10002 => Docker 외부에서 브로커에 접속할 때 사용하는 리스너 (spring boot에서 사용)

docker-compose.yml 설정에서 겪었던 오류

  1. topic 생성할 때 오류
Error while executing topic command : Call(callName=createTopics, deadlineMs=1701844016429, tries=1, nextAllowedTryMs=1701844016530) timed out at 1701844016430 after 1 attempt(s)
[2023-12-06 06:26:56,437] ERROR org.apache.kafka.common.errors.TimeoutException: Call(callName=createTopics, deadlineMs=1701844016429, tries=1, nextAllowedTryMs=1701844016530) timed out at 1701844016430 after 1 attempt(s)
Caused by: org.apache.kafka.common.errors.TimeoutException: Timed out waiting for a node assignment. Call: createTopics
(kafka.admin.TopicCommand$)

2. produce, consume 명령할 때 오류

Connection to node -1 (kafka1/ip:port) could not be established. Broker may not be available. (org.apache.kafka.clients.NetworkClient)

kafka의 environment 설정 내에 아래 속성을 host.docker.internal로 했을 때 위의 오류가 났다.
1. 컨테이너에 접속해서 토픽을 생성할 때 (안날 때도 있음)
2. produce, consume 명령어를 칠 때 (안날 때도 있음)
3. 스프링 부트에서 카프카를 연결하고 앱을 실행할 때 (계속 남)

KAFKA_ADVERTISED_HOST_NAME: host.docker.internal
BOOTSTRAP_SERVERS: host.docker.internal:9092

이런 broker에 접속 불가 오류가 뜰 때는 server.properties 파일에 listeners=PLAINTEXT://localhost:9092 를 포함시키라는 답변이 많았다.

그러나 docker로 kafka를 사용하기 때문에 이 파일에 접근하기가 어려웠고 (방법은 찾으면 있겠지만 그게 더 어려워 보였다.) 그래서 docker-compose.yml 내에서 처음부터 설정하고 시작하는 게 맞다고 생각하고 다시 처음부터 진행했다.

위 docker-compose.yml 에서는 KAFKA_CFG_ADVERTISED_LISTENERS에 EXTERNAL: 로 외부에서 접속하는 주소임을 명시했기 때문에 127.0.0.1으로 호스트를 작성해도 docker 내부 주소로 간주되지 않아서 스프링부트에서 접속됐던 것 같다.

  • host.docker.internal: 도커 컨테이너에서 localhost를 실행하고싶을 때, 컨테이너 내의 localhost가 아니라 PC의 localhost(127.0.0.1)를 실행하고싶을 때 사용하는 도메인.
    Docker 컨테이너는 자체의 독립적인 네트워크 네임스페이스를 가지고 있어서 호스트 시스템의 localhost를 참조할 때 컨테이너의 localhost를 참조하게 된다.

3. docker-compose.yml 실행

파일에 적어준 대로 컨테이너를 받기 위해 위에서 만든 파일을 실행해야한다.

터미널을 키고 위에서 yml 파일이 있는 곳으로 이동한 후, 아래 명령어를 친다.

나의 경우 yml 파일 이름을 docker-compose-cluster.yml으로 했기 때문에 아래처럼 쳤다.

docker-compose -f docker-compose-cluster.yml up -d

4. 실행중인 컨테이너 확인

docker ps

아래와 같이 뜨면 잘 실행된 것

OrbStack에서도 확인할 수 있다.

참고로 실행중인 것 외에도 설치되어 있는 컨테이너를 전부 확인하는 명령어는 아래와 같다.

docker ps -a

5. 카프카 클러스터 구성 확인

Kafka-ui를 받았기 때문에 127.0.0.1:8080 에 들어가서 확인할 수 있다.

docker-compose.yml가 잘못되었을 때는 offline으로 떴었다…

참고) 주키퍼 사용시 클러스터 구성 확인

여기서는 주키퍼를 사용하지 않았지만 적어본다.

zookeeper:2181 부분은 주키퍼의 hostname:post를 쓰는 부분이다.

zookeeper-shell.sh zk1:2181 ls /brokers/ids

주소를 잘못 적으면 아래 오류가 뜬다.

제대로 실행한 결과는 아래처럼 출력된다.

[1, 2, 3]에서 숫자들은 각 카프카 브로커들의 아이디(KAFKA_BROKER_ID)를 의미한다.

6. 토픽 만들기

먼저 kafka1(카프카) 컨테이너에 접근한다.

docker container exec -it Kafka00Container bash

토픽을 만든다.

kafka-topics.sh --create --topic my-topic --bootstrap-server Kafka00Service:9092 --replication-factor 3 --partitions 3

7. 만든 토픽 정보 확인하기

kafka-topics.sh --describe --topic my-topic --bootstrap-server Kafka00Service:9092

출력

bootstrap-server를 Kafka01Service:9092, Kafka02Service:9092으로 해도 결과는 같다.

8. 메세지 주고받기

터미널을 두 개 켜서 하나는 producer, 하나는 consumer 역할을 하도록 한다.

메세지를 받도록 하는 명령어

kafka-console-consumer.sh --topic my-topic --bootstrap-server Kafka00Service:9092

메세지를 생산하도록 하는 명령어

kafka-console-producer.sh --topic my-topic --broker-list Kafka00Service:9092

결과

오른쪽 consumer를 켜놓고 왼쪽 producer 에서 메세지를 입력한다.
엔터를 칠 때 마다 오른쪽에 메세지가 뜬다.

참고 문서

https://sup2is.github.io/2020/06/02/java-with-kafka-cluster.html

https://colevelup.tistory.com/17#recentComments

https://github.com/ArminShoeibi/KafkaDockerCompose/blob/main/docker-compose-cluster.yml

https://victorydntmd.tistory.com/347#recentComments

--

--