
![]()
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/kafkaveapache/kafka-nativeetiketlerini Docker Hub’da da görebilirsin;4.1.0tag’i mevcut. (Docker Hub)
Mimarinin Özeti
- 3 node: Her biri broker+controller (combined roles) çalıştırır.
- KRaft Meta-Quorum:
controller.quorum.votersile üç 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ı--overrideile 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
localhostuygundur; macOS/Windows’ta başka Docker projelerinden erişeceksenhost.docker.internalkullanmayı 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 formatyalnızca dosyadan okur; bu nedenle format adımı için minimal birformat-kraft.propertiesoluşturuyoruz. Runtime’da ise env →--overrideile bütün ayarları geçiriyoruz. Bu iki aşama, KRaft’ın gerektirdiğicontroller.quorum.votersbilgisiyle 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 →
--overrideile 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/kafka4.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…