
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
veapache/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).
- İç ağ için
- 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şeceksenhost.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 birformat-kraft.properties
oluşturuyoruz. Runtime’da ise env →--override
ile bütün ayarları geçiriyoruz. Bu iki aşama, KRaft’ın gerektirdiğicontroller.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:
- Format adımını minimal bir KRaft config dosyasıyla garantile,
- Çalıştır adımında tüm broker ayarlarını env →
--override
ile besle, - 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…