Veri Bilimi Okulu

Docker Compose ile 3 Node KRaft Kafka Cluster Kurulumu (Kafka 4.1.0)
Docker Compose ile 3 Node KRaft Kafka Cluster Kurulumu (Kafka 4.1.0)
kafka_cluster_kapak_gorseli

Loading

Neden KRaft ve Neden Kafka 4.1.0?

Daha önce bu yazımda Zookeeper-less Kafka cluster kurulumundan bahsetmiştim. Aradan epey zaman geçti ve resmi imajlar kullanarak yeni bir molti-node kafka cluster kurulumunu docker compose ile ele almak istedim. Kafka 4.0 ile beraber ZooKeeper bağımlılığı kaldırıldı ve KRaft (Kafka Raft) varsayılan hâle geldi. Bu hem yönetimi basitleştirir (ayrı bir ZK kümesi yok), hem de failover/topology yönetimini Kafka’nın kendi meta-quorum’unda toplamaya yarar. 4.1.0 sürümü ise 2 Eylül 2025’te yayınlandı ve Docker tarafında iki resmî imaj var: JVM tabanlı apache/kafka:4.1.0 ve GraalVM native apache/kafka-native:4.1.0. Biz burada olgunluk ve geniş uyumluluk için JVM tabanlı apache/kafka:4.1.0 kullanacağız. (Apache Kafka)

Not: apache/kafka ve apache/kafka-native etiketlerini Docker Hub’da da görebilirsin; 4.1.0 tag’i mevcut. (Docker Hub)

Mimarinin Özeti

  • 3 node: Her biri broker+controller (combined roles) çalıştırır.
  • KRaft Meta-Quorum: controller.quorum.voters ile üç controller tanımlıdır.
  • İki listener:
    • İç ağ için PLAINTEXT://:9092 (container’lar arası trafik),
    • Host’tan bağlantı için EXTERNAL://:9094 (host portlarını 19092/29092/39092’ye map ediyoruz).
  • Disk formatlama: İlk açılışta kafka-storage.sh format çağrısı, KRaft için minimal bir config dosyasıyla yapılır (storage tool sadece dosyadan okur). Ardından broker runtime’da tüm ayarları --override ile alır. Bu, Compose içinde env değişkenleri ile ayar yapmayı çok esnek hâle getirir. (Storage tool’un config okuma davranışı ve KRaft ayarları için bkz. referanslar). (docs.confluent.io)

Hazırlık

  • Docker ve Docker Compose kurulu olmalı.
  • Makinede 3 container için yeterli RAM/CPU.
  • Linux’ta host ağından bağlanacaksan localhost uygundur; macOS/Windows’ta başka Docker projelerinden erişeceksen host.docker.internal kullanmayı düşünebilirsin.

Aşağıdaki dosyayı docker-compose.yml olarak kaydet:

Önemli: YAML içinde komut satırındaki $ karakterleri Compose tarafından env interpolasyonu sanılmasın diye $$ olarak kaçışlanmıştır. Kopyala–yapıştır ile sorunsuz çalışır.

# ---------- Reusable anchors ----------
x-env-kafka: &env-kafka
  KAFKA_CFG_PROCESS_ROLES: "broker,controller"
  KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093"
  KAFKA_CFG_CONTROLLER_LISTENER_NAMES: "CONTROLLER"
  KAFKA_CFG_LISTENERS: "PLAINTEXT://:9092,EXTERNAL://:9094,CONTROLLER://:9093"
  KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT"
  KAFKA_CFG_INTER_BROKER_LISTENER_NAME: "PLAINTEXT"
  KAFKA_CFG_LOG_DIRS: "/var/lib/kafka/data"
  KAFKA_CFG_NUM_PARTITIONS: "3"
  KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR: "3"
  KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: "3"
  KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR: "2"
  KAFKA_CFG_DELETE_TOPIC_ENABLE: "true"
  KAFKA_CFG_CONTROLLED_SHUTDOWN_ENABLE: "true"
  # Tüm nodelar için sabit cluster id
  KAFKA_CLUSTER_ID: "q4W8eM8mQzSv0oPqsW1r1Q"
  KAFKA_HEAP_OPTS: "-Xms512m -Xmx512m"

x-cmd-kafka: &cmd-kafka
  - bash
  - -c
  - |
      set -euo pipefail
      KAFKA_HOME=$${KAFKA_HOME:-/opt/kafka}
      DATA_DIR=$${KAFKA_DATA:-/var/lib/kafka/data}
      RUNTIME_CFG=$${KAFKA_CONFIG:-$$KAFKA_HOME/config/server.properties}
      FORMAT_CFG="/tmp/format-kraft.properties"

      # Storage tool sadece DOSYADAN okur; format için minimal KRaft config üret
      cat > "$$FORMAT_CFG" <<EOF
      process.roles=$${KAFKA_CFG_PROCESS_ROLES}
      node.id=$${KAFKA_CFG_NODE_ID}
      controller.quorum.voters=$${KAFKA_CFG_CONTROLLER_QUORUM_VOTERS}
      controller.listener.names=$${KAFKA_CFG_CONTROLLER_LISTENER_NAMES}
      listeners=$${KAFKA_CFG_LISTENERS}
      listener.security.protocol.map=$${KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP}
      inter.broker.listener.name=$${KAFKA_CFG_INTER_BROKER_LISTENER_NAME}
      log.dirs=$${KAFKA_CFG_LOG_DIRS}
      EOF

      # Disk daha önce formatlanmadıysa formatla
      if [ ! -f "$$DATA_DIR/meta.properties" ]; then
        echo "Formatting storage (cluster-id $${KAFKA_CLUSTER_ID}) using $$FORMAT_CFG ..."
        "$$KAFKA_HOME/bin/kafka-storage.sh" format \
          --config "$$FORMAT_CFG" \
          --cluster-id "$${KAFKA_CLUSTER_ID}" \
          --ignore-formatted
      fi

      # Runtime için KAFKA_CFG_* env -> --override
      OVERRIDES=()
      while IFS="=" read -r name value; do
        if [[ "$$name" == KAFKA_CFG_* ]]; then
          key="$(echo "$${name#KAFKA_CFG_}" | tr '[:upper:]' '[:lower:]' | tr '_' '.')"
          OVERRIDES+=( --override "$${key}=$${value}" )
        fi
      done < <(env)

      echo "Starting Kafka with overrides: $${OVERRIDES[*]}"
      exec "$$KAFKA_HOME/bin/kafka-server-start.sh" "$$RUNTIME_CFG" "$${OVERRIDES[@]}"

x-svc-common: &svc-common
  image: apache/kafka:4.1.0
  restart: unless-stopped
  networks: [knet]
  command: *cmd-kafka

# ---------- Topology ----------
services:
  kafka-1:
    <<: *svc-common
    container_name: kafka-1
    hostname: kafka-1
    environment:
      <<: *env-kafka
      KAFKA_CFG_NODE_ID: "1"
      KAFKA_CFG_ADVERTISED_LISTENERS: "PLAINTEXT://kafka-1:9092,EXTERNAL://localhost:19092"
    ports:
      - "19092:9094"
    volumes:
      - kafka1-data:/var/lib/kafka/data

  kafka-2:
    <<: *svc-common
    container_name: kafka-2
    hostname: kafka-2
    environment:
      <<: *env-kafka
      KAFKA_CFG_NODE_ID: "2"
      KAFKA_CFG_ADVERTISED_LISTENERS: "PLAINTEXT://kafka-2:9092,EXTERNAL://localhost:29092"
    ports:
      - "29092:9094"
    volumes:
      - kafka2-data:/var/lib/kafka/data

  kafka-3:
    <<: *svc-common
    container_name: kafka-3
    hostname: kafka-3
    environment:
      <<: *env-kafka
      KAFKA_CFG_NODE_ID: "3"
      KAFKA_CFG_ADVERTISED_LISTENERS: "PLAINTEXT://kafka-3:9092,EXTERNAL://localhost:39092"
    ports:
      - "39092:9094"
    volumes:
      - kafka3-data:/var/lib/kafka/data

# ---------- Plumbing ----------
networks:
  knet:

volumes:
  kafka1-data:
  kafka2-data:
  kafka3-data:

Neden bu yaklaşım stabil?

  • kafka-storage.sh format yalnızca dosyadan okur; bu nedenle format adımı için minimal bir format-kraft.properties oluşturuyoruz. Runtime’da ise env → --override ile bütün ayarları geçiriyoruz. Bu iki aşama, KRaft’ın gerektirdiği controller.quorum.voters bilgisiyle formatı garanti altına alır. (docs.confluent.io)

Kurulum ve İlk Test

Temiz başlangıç:

docker compose down -v
docker compose up -d
docker compose ps

Quorum durumunu kontrol et:

docker exec -it kafka-1 /opt/kafka/bin/kafka-metadata-quorum.sh \
  --bootstrap-server kafka-1:9092 describe --status

Çıktıda LeaderId, CurrentVoters (1,2,3) gibi alanları görürsün; böylece meta-quorum’un sağlıklı olduğunu anlarsın. Bu komut KRaft kümelerinin health/debug’unda birincil araçtır. (docs.confluent.io)

Topic oluştur:

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

docker exec -it kafka-1 /opt/kafka/bin/kafka-topics.sh \
  --bootstrap-server kafka-1:9092 --describe --topic demo

Producer / Consumer (container içinden):

# Producer
docker exec -it kafka-1 /opt/kafka/bin/kafka-console-producer.sh \
  --bootstrap-server kafka-1:9092 --topic demo
# bir kaç satır yaz, Ctrl+C ile çık

# Consumer
docker exec -it kafka-2 /opt/kafka/bin/kafka-console-consumer.sh \
  --bootstrap-server kafka-2:9092 --topic demo --from-beginning

Host’tan (Docker dışı) bağlanmak için:

  • Bootstrap: localhost:19092,localhost:29092,localhost:39092

Sık Karşılaşılan Hatalar ve Çözümler

1) controller.quorum.voters yok uyarısı
kafka-storage.sh format sırasında config dosyasında gerekli KRaft alanlarını bulamazsa “--initial-controllers veya --standalone…“ şeklinde şikayet eder. Biz bu yüzden format için minimal dosyayı üretiyoruz. Eğer yine de görürsen, eski/boş meta.properties kalıntılarını temizle:

docker compose down -v
docker compose up -d

(Bu davranış ve “initial controllers” yaklaşımı KRaft belgelerinde anlatılır.) (Red Hat Developer)

2) server.properties yolu
Resmî imajda varsayılan config yolu /opt/kafka/config/server.properties’tir. Eski örneklerde görülebilen config/kraft/server.properties dosyası yoksa format komutu “NoSuchFileException” atar. Quickstart ve resmi imajlar bu yolu kullanır. (Apache Kafka)

3) EXTERNAL bağlantıda erişememe
Host’tan erişirken advertised.listeners içindeki EXTERNAL://localhost:<port> değerleriyle eşleşen port publish edildi mi (ports: kısmı)? macOS/Windows’ta başka Docker projesinden bağlanacaksan localhost yerine host.docker.internal tercih et.

4) ZK ayarlarıyla karışıklık
Kafka 4.x’te ZooKeeper artık yok; KRaft varsayılan. Eski dokümanlardan kalma zookeeper.connect gibi alanlar kullanma. (Apache Kafka)

Neden JVM Tabanlı İmajı Tercih Ettik?

apache/kafka-native (GraalVM native image) daha hızlı açılış ve daha düşük bellekle cazip gelebilir; ancak olgunluk, araç uyumluluğu ve topluluk deneyimi açısından JVM imajı hâlâ ana akım. Quickstart ve indirme sayfası da sürüm ve etiketleri net biçimde listeler. Üretim dışı denemeler için native imajı kurcalayabilirsin, ama bu rehber JVM apache/kafka:4.1.0 üzerine optimize edilmiştir. (Apache Kafka)

İsteğe Bağlı: Kafka UI Ekle

Tek satırda cluster’ı görselleştirmek istersen provectuslabs/kafka-ui çok işlevsel:

  kafka-ui:
    image: provectuslabs/kafka-ui
    restart: unless-stopped
    networks: [knet]
    ports: ["8080:8080"]
    environment:
      KAFKA_CLUSTERS_0_NAME: local
      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka-1:9092,kafka-2:9092,kafka-3:9092

Sonuç

Bu yazıdaki Compose yaklaşımıyla Kafka 4.1.0 üzerinde, 3 nodelu, KRaft tabanlı, ZooKeeper’sız bir küme dakikalar içinde ayağa kalkıyor. Ana fikir şu:

  1. Format adımını minimal bir KRaft config dosyasıyla garantile,
  2. Çalıştır adımında tüm broker ayarlarını env → --override ile besle,
  3. EXTERNAL listener’ları host’a publish ederek local araçlardan eriş.

Böylece hem modern Kafka mimarisine uyar, hem de Compose içinde son derece okunabilir, yeniden kullanılabilir (anchor’lı) bir kurulum elde edersin.

Kaynaklar

  • Apache Kafka Downloads — 4.1.0 (yayın tarihi ve Docker imaj etiketleri). (Apache Kafka)
  • Apache Kafka Quickstart — Docker ile Kafka çalıştırma örnekleri. (Apache Kafka)
  • Docker Hub — apache/kafka 4.1.0 tag’ı. (Docker Hub)
  • KRaft yapılandırma ve operasyonel notlar (Confluent docs). (docs.confluent.io)
  • KRaft quorum komut satırı aracı (kafka-metadata-quorum.sh) dokümantasyonu. (docs.confluent.io)
  • Dinamik/başlangıç controller set’i ve formatlama tartışması (Red Hat developers makalesi). (Red Hat Developer)

Dilerseniz sizler bir de “dedicated controller” (3 controller + 3 broker) topolojisini aynı anchor yaklaşımıyla ele alabilirsiniz ayrıca TLS/SASL eklemek de mümkün.

Başka bir yazıda görüşmek dileğiyle hoşça kalın…

0

Bir yanıt yazın

Password Requirements:

  • At least 8 characters
  • At least 1 lowercase letter
  • At least 1 uppercase letter
  • At least 1 numerical number
  • At least 1 special character