
![]()
Bu kafka listeners ve advertised listeners gerçekten gıcık bir konu. Kafka’ya bağlanamayığ saç baş yoldurabiliyor bazen. Şimdiye dek Kafka konfigürasyonunda listeners ve advertised.listeners‘ı en az bir kez Google’da aratmış olabilirsin ama artık yapay zekalara soruyorsunuzdur muhtemelen YZ da bu işi çözüyordur ve tıkır tıkır Kafka’ya bağlanıyorsunuzdur. peki YZ’nin çözdüğünü siz çözdünüz mü? Çözemediyseniz bu iki ayarı, neden bu kadar yanıltıcı olduklarını ve Docker Compose içinde nasıl doğru kurulduklarını birlikte çözelim.
Önce sahnedeki kahramanları tanıyalım
Bir Kafka broker’ı bir veya birden fazla listener açar. Listener, Kafka’nın “şu IP’nin şu portuna gelen bağlantıları kabul edeceğim” dediği bir ağ uç noktasıdır. Yani Kafka aslında çok basit bir TCP sunucusu gibi davranıyor: belirli bir hostname:port ikilisinde bir soket açıyor ve istemcileri (client) bekliyor [1][2].
Burada bir ikinci yapı daha var: advertised listeners. Kafka, istemci ona ilk bağlandığında ona “ben buradayım, beni bir daha aramak istersen şu adresten ara” diye geri cevap veriyor. İşte bu geri verilen adresler advertised.listeners parametresine yazdığımız adreslerdir [1][3].
Tek cümleyle: LISTENERS Kafka’nın bağlanmak için hangi arayüzleri açtığını, ADVERTISED_LISTENERS ise istemcilerin Kafka’ya nasıl bağlanacağını söyler [1].
Bu ayrımı kaçırırsak Kafka biz farkında olmadan bizi yanlış adrese yönlendirir. Çoğu sıkıntının altında bu yatıyor.
Kafka’ya bağlanmak iki adımdır, tek adım değil
Bu kısım çok kritik. Bir Kafka istemcisi (producer ya da consumer) iki fazlı bir el sıkışması yapar [4][5]:
- Önyükleme aşaması (bootstrap phase). İstemci,
bootstrap.serverslistesindeki adreslerden birine TCP açar ve bir metadata isteği (metadata request) yollar. Bu istek “selam, hangi broker’lar var, bu topiğin lideri kim?” demek için kullanılır [4][6]. - Doğrudan bağlantı aşaması (direct connection phase). Kafka cevabında bütün broker’ların advertised adreslerini ve hangi partition’ın hangi broker’da olduğunu döner. İstemci artık
bootstrap.servers‘ı unutur ve metadata’da gördüğü adreslere doğrudan bağlanır [5][6].
Sürpriz burada: bootstrap.servers sadece tanışma için kullanılır. Asıl iletişim, Kafka’nın bize bildirdiği adresler üzerinden olur [7]. Yani bootstrap.servers‘ın çalışması Kafka’nın çalışacağı anlamına gelmez — bildirilen (advertised) adres istemciden erişilebilir değilse hat orada kopar.
Bir limonata standı analojisi yapalım: Sokağın başındaki çocuk size “limonata almak istiyorsan annemi ara, numarası şu” diyor. Bu telefon numarası anneye gerçekten ulaşıyorsa harika. Ama anne komşu binada oturuyorsa ve verdiği numara sadece o binanın iç telefonuysa — siz sokaktan o numarayı çevirdiğinizde hiçbir şey olmaz. İşte advertised.listeners‘ı yanlış vermek tam olarak budur [1][8].
Bu el sıkışma neden var? — Tasarımın altında yatan fikir
İlk bakışta “tek adımda bağlansak ya, neden bu kadar zahmet?” diye sorabiliriz. Kafka tek bir makine değil, bir cluster. Cluster’ın doğası gereği istemcinin ilk bağlandığı broker, gerçekte konuşması gereken broker olmak zorunda değil. Bu bildirim mekanizması olmasaydı Kafka’nın dağıtık (distributed) bir sistem olarak çalışması mümkün olmazdı. Birkaç sebebi var.
Bootstrap sadece bir tanışmadır, asıl iş başka yerde döner. Canlı ortamlarda tipik bir Kafka cluster 3, 10, hatta 50 broker’dan oluşabilir. bootstrap.servers‘a yazdığımız iki üç adres yalnızca tanışma kapısı; “merhaba” dedikten sonra Kafka bize ekibinin tam listesini veriyor [5][7]. Bu olmasaydı her istemcinin bütün broker’ları elle konfigüre etmesi gerekirdi. Yeni broker eklendiği anda istemcileri tek tek yeniden konfigüre etmek zorunda kalırdık — çok kırılgan bir yapı.
Her bölümün (partition) belirli bir lideri (leader) vardır ve bu lider sabit değildir. Kafka’da bir konuya (topic) yazdığımızda mesaj bir partition’a (parça) gider; her parçaya yazma işlemleri zorunlu olarak lider broker’a yapılır [6][7]. Diyelim ki bootstrap.servers‘da broker A’ya bağlandık ama yazmak istediğimiz parçanın lideri broker C’de — A bize “C’ye git” demek zorunda, yoksa yazamayız. Üstelik liderlik dinamik: bir broker çöktüğünde başka bir broker o parçanın liderliğini devralır ve istemci bunu anında öğrenmek zorunda. Bu yüzden istemci metadata’yı düzenli olarak tazeler (metadata refresh) [4].
Aynı broker farklı yerlere farklı yüz gösterebilir. Bizim Docker hikâyemizdeki asıl konu da bu. Aynı Kafka konteyneri Docker ağındaki bir konteynere kafka:29092 olarak görünür, host makineye localhost:9092 olarak. Kafka istemciye sabit tek bir adres dayatsaydı bu mümkün olmazdı. Bildirilen dinleyiciler (advertised listeners) mekanizması sayesinde broker, istemcinin hangi dinleyiciden geldiğine bakıp “sen bu kanaldan geldiğine göre sana bu adresi söyleyeyim” diyebiliyor [1][11]. Aynı broker, farklı istemciye farklı cevap.
Yük doğrudan lidere gider, dolambaçsız. Eğer her istek bootstrap broker üzerinden geçseydi o broker tıkanır ve dağıtıklığın bütün anlamı kaybolurdu. Bildirim mekanizması sayesinde istemci tanışmadan sonra ilgili broker’a doğrudan bağlanıyor [4]. Yani bootstrap broker bir santral değil, sadece danışma masası gibi davranır.
Cluster topolojisi (topology) değişir. Broker eklersiniz, çıkarırsınız, taşırsınız. Bütün bunlar olurken istemcilerin konfigürasyonuna dokunmadan değişikliği yansıtmanın tek pratik yolu metadata’yı broker’ın söylemesi. Sistemde tek doğruluk kaynağı (single source of truth) Kafka’nın kendisi oluyor.
Bu lensten bakınca advertised.listeners‘ın varlığı fazladan bir adım değil — Kafka’nın ölçeklenebilirliğinin (scalability) ve esnekliğinin temeli. Tasarım kararı olarak gayet zekice.
İlk compose neden patladı?
Şimdi sorunlu konfigürasyona dönelim. Kafka servisindeki kritik satırlar şunlardı:
KAFKA_LISTENERS: PLAINTEXT://localhost:9092,CONTROLLER://localhost:9093 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
Ve ports: bölümü yoktu. Yani Kafka konteynerinden dışarı 9092 portu yayınlanmamıştı.
Üç ayrı sorun var burada:
1. localhost, konteynerin kendi localhost’udur. Konteyner içinde localhost:9092 demek “bu konteynerin loopback arayüzü” anlamına gelir. Spark konteyneri Kafka konteynerine localhost:9092 ile bağlanmaya çalıştığında kendi içinde 9092’yi arar — orada hiçbir şey yoktur [8][9].
2. Bildirilen adres dış istemciyi yanıltıyor. Diyelim Spark konteyneri bir şekilde kafka:9092 bootstrap’iyle bağlandı (Docker DNS sayesinde). Metadata’da Kafka geri “beni localhost:9092 üzerinden ara” diyor. Spark client da artık localhost:9092‘ye dönüyor — yine kendi içinde. Aksi halde istemci içerideki host adresine bağlanmaya çalışır ve bu erişilemezse sorunlar başlar [1].
3. Host makineden hiç ulaşılamaz. ports: ile 9092 yayınlanmadığı için Docker host’undaki bir araç (mesela yerel Python kafka-python betiği) Kafka’yı göremez bile.
Cherry on top: KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093 da KRaft sürecinin “kendisi”ne hangi adresten ulaşacağını söylüyor. Tek node’lu bir cluster’da şanslıysak çalışır, ama multi-node’da aynı tuzak.
Düzeltilmiş compose ne yapıyor?
İşte sihir:
ports: - "9092:9092" environment: KAFKA_LISTENERS: INTERNAL://0.0.0.0:29092,HOST://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:29092,HOST://localhost:9092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,HOST:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
Tek tek inceleyelim çünkü her satırın bir nedeni var.
0.0.0.0 üzerinden bağlama — “Her arayüzü dinle”
KAFKA_LISTENERS artık localhost değil 0.0.0.0. HOSTNAME değeri olarak ‘0.0.0.0’ kullanılması dinleyiciyi tüm arayüzlere bağlar [10]. Bu sayede konteynerin bütün ağ arayüzlerinden (Docker ağı, host’a açılan port, ne varsa) gelen bağlantıyı kabul ediyor. localhost ise sadece kendi loopback’i. Tek harf değil ama anlam dünyasının iki ucu.
İki ayrı veri dinleyicisi: INTERNAL ve HOST
Burası işin ruhu. Kafka çoklu dinleyici (multiple listeners) destekler ve her birine kendi adını verebiliriz. Genel pratik şudur: aynı cluster’a farklı yerlerden bağlanan istemciler farklı adresleri görmelidir [11][12].
INTERNAL://kafka:29092→ Docker ağındaki diğer konteynerler için. Spark konteyneri “kafka” hostname’ini Docker DNS sayesinde aynıvboağındaki Kafka konteynerine çözer. Bu yüzden Spark’tankafka:29092‘ye bağlanılır, sorun çıkmaz [13][14].HOST://localhost:9092→ Host makineden (laptop’tan, IDE’den) bağlanan istemciler için.ports: 9092:9092sayesinde 9092 dışarı yayınlandı, yani host’talocalhost:9092Kafka’ya gerçekten ulaşıyor [12][15].
Aynı broker, kim sorduğuna göre farklı yanıt veriyor. Tıpkı bir restoranın masa numaralarını içerideki garsona “5 numara”, sokağa servis yapan kuryeye “Atatürk Bulvarı No:42” diye söylemesi gibi. İki cevap da doğru, ama bağlama göre kullanışlı olan değişiyor.
Güvenlik protokolü haritası
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,HOST:PLAINTEXT
Birden fazla dinleyici tanımladığımız anda Kafka her birinin hangi güvenlik protokolüyle (PLAINTEXT, SSL, SASL_SSL gibi) çalışacağını bilmek ister. Tek dinleyici olduğunda bu otomatikti. Şimdi her dinleyici adını uygun protokole eşliyoruz [11][12]. Lokalde geliştirme yaptığımız için hepsi PLAINTEXT, yani şifresiz. Canlıda böyle bir şey yapmıyoruz tabii.
KAFKA_INTER_BROKER_LISTENER_NAME — Broker’lar kendi aralarında nasıl konuşacak?
Birden fazla dinleyicimiz varken Kafka’ya “kardeşim, başka bir broker’la konuşman gerektiğinde hangi dinleyiciyi kullan?” diye söylememiz gerekir. Cevap: INTERNAL. Çünkü brokerler her zaman Docker ağı içinde yaşıyorlar, dışarıdaki dünyayı tanımaları gerekmiyor [16][17].
Bu listenerin temel amacı bölüm replikasyonudur. Tanımlanmazsa, broker-arası listener security.inter.broker.protocol ile belirlenen güvenlik protokolüne göre seçilir, ki varsayılanı PLAINTEXT’tir [17]. Tek node’lu cluster’da replikasyon yok ama Kafka yine de bu ayarı net görmek ister.
KAFKA_CONTROLLER_LISTENER_NAMES — KRaft sahnesi
Kafka 4’te ZooKeeper resmi olarak yok; metadata yönetimini KRaft denen mekanizma yapıyor [18][19]. KRaft modunda her node broker, controller veya ikisi birden olabilir. Controller’lar kendi aralarında cluster durumunu yönetmek için ayrı bir dinleyici kullanır [16][17].
Bu yüzden:
KAFKA_PROCESS_ROLES: broker,controller→ Bu node hem broker hem controller (combined mode, lokal geliştirme için ideal).KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER→ Controller trafiğiCONTROLLERadlı dinleyici üzerinden akar.KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093→ Quorum üyelerini (KRaft konsensüs sürüsü) tanıtıyoruz. Buradalocalhostyerinekafkayazmamızın sebebi: birden fazla controller olduğu senaryoya geçtiğimizde aynı şablon çalışsın. Tek node’da da zaten konteynerin hostname’ikafka.
CONTROLLER dinleyicisi de 0.0.0.0:9093‘te bağlı ama dışarı yayınlanmıyor. Çünkü istemcilerimizin onunla işi yok; o sadece Kafka’nın iç organlarından biri.
ports: 9092:9092 — Köprüyü kurmak
Konteyner içindeki 9092’yi host makineye yayınladık. Bu olmadan HOST://localhost:9092 bildirimi havada kalırdı: Kafka istemciye “localhost:9092’den ara” der ama host’tan oraya gerçekten bir kanal açılmamıştır. Docker ports ifadesi tam bu kanalı kuruyor.
Not: INTERNAL için (29092) port yayınlamamızın gereği yok, çünkü o sadece Docker ağı içindeki konuşmalar için.
“Hangi adresi ne zaman göreceğim?” — Akış diyagramı kafamızda
Şimdi son bir kontrol yapalım. Bir istemci geldi diyelim:
- Spark konteyneri
bootstrap.servers=kafka:29092ile geldi → İlk bağlantı Docker DNS sayesinde Kafka konteynerine düşer. Kafka metadata’daINTERNAL://kafka:29092döner. Sparkkafka:29092‘ye doğrudan bağlanır, yine aynı ağda olduğu için sorun yok. ✓ - Laptop’taki Python betiği
bootstrap.servers=localhost:9092ile geldi → İlk bağlantı host’taki 9092’ye düşer, Docker port forwarding sayesinde konteynere ulaşır. Kafka metadata’da bu kezHOST://localhost:9092döner. Betiklocalhost:9092‘ye doğrudan bağlanır, host’ta zaten bu portta Kafka açıkmış gibi görünür. ✓ - Spark
bootstrap.servers=localhost:9092ile gelseydi → Spark konteynerinden “localhost” Spark’ın kendi loopback’idir, Kafka’ya hiç ulaşamaz. ✗
Anahtar fikir tek bir cümleye iniyor: advertised.listeners istemciler ilk bağlandığında onlara geri gönderilen ve nasıl iletişime devam edeceklerini söyleyen adresleri içerir [3]. Yani Kafka kendisi nasıl çağrılmak istediğine karar verir — biz de o kararı dürüstçe veririz ki istemci bizi bulabilsin.
Sık yapılan üç hata
Aynı tuzaklara bir daha düşmeyelim diye not düşüyorum:
1. Tek dinleyici, iki dünya umudu. Sadece PLAINTEXT://localhost:9092 bildirilmiş ve hem Docker hem host’tan bağlanmaya çalışılıyor. Bu kombinasyon sadece host’tan çalışır; başka konteynerden bağlanmayı denediğin anda patlar [4][8].
2. KAFKA_LISTENERS‘da localhost kullanmak. İçeride loopback’e bağlandığın için Docker ağındaki diğer konteynerler asla bağlanamaz. 0.0.0.0 ya da konteyner hostname’i kullanmak gerekir [10][20].
3. ports: yayınlamadan host’tan bağlanmaya çalışmak. Compose’da port yayınlamadıysan host makinende nc -zv localhost 9092 bile başarısız olur. Bildirilen adres ne kadar süslü olursa olsun fark etmez [12][21].
Hızlı kontrol listesi
Bir sonraki sefer bağlanamadığımızda şu sırayla bakacağız:
docker logs kafkaçıktısında “started” satırını gör. Servis ayakta mı?docker exec kafka /opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092ile konteynerin kendi içinden Kafka cevap veriyor mu?- Host’tan
nc -zv localhost 9092(ya datelnet) ile port erişiliyor mu? Yoksaports:eksik. - Başka konteynerden
kcat -b kafka:29092 -L(eski adıyla kafkacat) ile metadata bak. Metadata’da hangi adres dönüyor? Eğer istemcinin erişemediği bir adres dönüyorsa advertised.listeners yanlış [22]. KAFKA_LISTENER_SECURITY_PROTOCOL_MAPher dinleyici adını içeriyor mu?- KRaft modundaysan
KAFKA_CONTROLLER_QUORUM_VOTERScontroller’a erişilebilir bir hostname mi gösteriyor?
Kapanış
Kafka’nın Docker’daki ağ ayarı ilk bakışta aşırı sembollü görünüyor ama altta yatan fikir gerçekten basit: broker hangi kapıları açtığını listeners ile, istemcileri hangi adrese yönlendirdiğini ise advertised.listeners ile söyler. Bu iki şey birbirinden farklı olabilir, hatta çoğu zaman olmak zorundadır — çünkü Docker konteyneri içinde “kim olduğu” ile dışarıdan “nasıl çağrıldığı” farklı şeyler.
Dolayısıyla bir sonraki sefer “Spark’tan Kafka’ya bağlanamıyorum” dediğinde önce docker logs‘a bakacağız, sonra Kafka’nın metadata’da hangi adresi döndürdüğünü kontrol edeceğiz. Çoğu zaman cevap hemen orada görünecek.
Bootcamp boyunca bu küçük tuzakların bizi yaktığı bir şey kesin, ama dert değil — bir kez anlayınca limonata içmek kadar kolay oluyor.
Kafka da dahil olmak üzere modern veri mühendisliğini uygulamalı olarak öğrenmek istiyorsan VBO Data Engineering Bootcamp sana göre.
Kaynaklar
[1] Robin Moffatt, “Kafka Listeners – Explained,” Confluent Blog. https://www.confluent.io/blog/kafka-listeners-explained/
[2] Apache Kafka, “Listener Configuration,” Kafka Documentation 4.1. https://kafka.apache.org/41/security/listener-configuration/
[3] FinTechDev, “Kafka listeners and advertised.listeners — how do I configure a Kafka Broker?” Medium. https://medium.com/@fintechdevlondon/kafka-listeners-and-advertised-listeners-how-do-i-configure-a-kafka-broker-b062de171ce8
[4] “How Kafka Bootstrap Connection Works,” Blog Notes (vitalvas). https://blog.vitalvas.com/post/2025/10/08/how-kafka-bootstrap-connection-works/
[5] “What is a Kafka Bootstrap Server? Apache Kafka Bootstrap Servers Explained,” Pulse. https://pulse.support/kb/what-is-apache-kafka-bootstrap-server
[6] “Kafka Bootstrap Server: What It Is, How It Works, and Best Practices,” Confluent. https://www.confluent.io/learn/kafka-bootstrap-server/
[7] “bootstrap-server in Kafka Configuration,” Baeldung. https://www.baeldung.com/java-kafka-bootstrap-server
[8] Robin Moffatt, “Why Can’t I Connect to Kafka? Troubleshoot Connectivity,” Confluent Blog. https://www.confluent.io/blog/kafka-client-cannot-connect-to-broker-on-aws-on-docker-etc/
[9] “Cannot connect to kafka-docker outside of docker network even after setting up ADVERTISED_LISTENERS/LISTENERS,” GitHub issue. https://github.com/wurstmeister/kafka-docker/issues/424
[10] Brett Johnson, “Kafka Listeners,” Automated Ramblings. https://sdbrett.com/post/2022-08-01-kafka-listeners/
[11] “Configure Kafka Listeners in Confluent Platform,” Confluent Documentation. https://docs.confluent.io/platform/current/kafka/listeners.html
[12] “Connect to Apache Kafka Running in Docker,” Baeldung. https://www.baeldung.com/kafka-docker-connection
[13] Robin Moffatt, “Docker-Compose for Kafka and Zookeeper with internal and external listeners,” GitHub Gist. https://gist.github.com/rmoff/fb7c39cc189fc6082a5fbd390ec92b3d
[14] “Developing event-driven applications with Kafka and Docker,” Docker Docs. https://docs.docker.com/guides/kafka/
[15] Sara M., “Unlock the Power of Apache Kafka with The Official Docker Image,” Towards Data Engineering on Medium. https://medium.com/towards-data-engineering/unlock-the-power-of-apache-kafka-with-the-official-docker-image-5a65192e618b
[16] “Configure a Multi-Node Confluent Platform Environment with Docker,” Confluent Documentation. https://docs.confluent.io/platform/current/kafka/multi-node.html
[17] Apache Kafka, “Listener Configuration — Inter-broker and Controller listeners,” Kafka 4.1 Docs. https://kafka.apache.org/41/security/listener-configuration/
[18] Katya Gorshkova, “Understanding Kafka KRaft: How Controllers and Brokers Talk in the Zookeeper-less World,” Medium. https://medium.com/@katyagorshkova/understanding-kafka-kraft-how-controllers-and-brokers-talk-in-the-zookeeper-less-world-a5e05a063f34
[19] Kinneko-De, “Kafka 4 + KRaft + Docker Compose,” Medium. https://medium.com/@kinneko-de/kafka-4-kraft-docker-compose-874d8f1ffd9b
[20] “Kafka Listeners vs. Advertised.Listeners: Understanding the Difference and When to Use Each,” codestudy.net. https://www.codestudy.net/blog/kafka-server-configuration-listeners-vs-advertised-listeners/
[21] Suraj Mishra, “What is Advertised.listeners in Kafka?” Medium. https://i-sammy.medium.com/what-is-advertised-listeners-in-kafka-9e2a216e070
[22] Robin Moffatt, “Kafka Listeners – Explained (metadata returns advertised hostname),” Confluent Blog. https://www.confluent.io/blog/kafka-listeners-explained/
[23] Clasence Neba Shu, “Multi-Broker, Multi-Controller Kafka (KRaft Mode) with Docker Compose,” Medium. https://clasence.medium.com/multi-broker-multi-controller-kafka-kraft-mode-with-docker-compose-and-confluentinc-fe32fd02e1ab
[24] “Kafka Docker Explained: Setup, Best Practices & Tips,” DataCamp. https://www.datacamp.com/tutorial/kafka-docker-explained
[25] “apache/kafka — Docker Image,” Docker Hub. https://hub.docker.com/r/apache/kafka
[26] “Configuring Kafka to advertise inter broker listener for client access explained,” Zenduty Community. https://community.zenduty.com/t/configuring-kafka-to-advertise-inter-broker-listener-for-client-access-explained/1207
[27] Marcelo Hossomi, “Running Kafka in Docker Machine,” Medium. https://medium.com/@marcelo.hossomi/running-kafka-in-docker-machine-64d1501d6f0b
[28] Željko Šević, “Kafka containers with Docker Compose,” DEV Community. https://dev.to/zsevic/kafka-containers-with-docker-compose-4pag
[29] “kafka testcontainer always advertises localhost,” testcontainers-python GitHub Issue #170. https://github.com/testcontainers/testcontainers-python/issues/170
[30] “Kafka Listener and advertised.listener,” Confluent Community Forum. https://forum.confluent.io/t/kafka-listener-and-advertised-listener/2645
[31] Kapak Görseli: Photo by Nick Fewings on Unsplash