境搭建全攻略:從單機(jī)到集群的避坑指南)
有人問(wèn)我Kafka到底難不難學(xué)。我的回答一直是環(huán)境搭建這一關(guān)過(guò)了后面就順了過(guò)不了后面一天到晚都在跟各種報(bào)錯(cuò)打架。Kafka不是那種解壓就能跑的軟件它對(duì)操作系統(tǒng)、JDK版本、網(wǎng)絡(luò)配置、磁盤(pán)規(guī)劃都有要求任何一個(gè)環(huán)節(jié)不合適啟動(dòng)時(shí)可能不報(bào)錯(cuò)但生產(chǎn)環(huán)境一跑就出問(wèn)題。這篇是Kafka學(xué)習(xí)系列的第二篇我默認(rèn)你已經(jīng)了解了Kafka的基本概念主題、分區(qū)、消費(fèi)者組、副本這些如果你還不太清楚建議先看系列第一篇把核心概念過(guò)一遍。這篇的主要內(nèi)容很聚焦怎么把Kafka環(huán)境從零搭起來(lái)每一步該怎么做、為什么這么做、踩過(guò)哪些坑全部用實(shí)際操作的思路來(lái)講。我主要從三個(gè)場(chǎng)景來(lái)講單機(jī)開(kāi)發(fā)環(huán)境搭建、三節(jié)點(diǎn)集群搭建、以及環(huán)境搭建過(guò)程中必踩的坑和排查思路。中間會(huì)穿插我在實(shí)際項(xiàng)目中總結(jié)的一些經(jīng)驗(yàn)和教訓(xùn)比如版本怎么選、參數(shù)怎么調(diào)、磁盤(pán)怎么規(guī)劃這些常規(guī)文檔里不會(huì)細(xì)說(shuō)的東西。1. 為什么先別急著裝Kafka組件架構(gòu)和版本選擇比下載更重要1.1 從單機(jī)玩具到分布式事實(shí)Kafka到底需要哪些組件很多人第一次搭Kafka環(huán)境下載完壓縮包就想著趕緊解壓、啟動(dòng)、跑起來(lái)。但如果你對(duì)Kafka的運(yùn)行機(jī)制沒(méi)有一個(gè)整體認(rèn)知后面配置參數(shù)時(shí)會(huì)非常迷茫出了問(wèn)題也不知道從哪兒排查。Kafka本身是一個(gè)分布式消息系統(tǒng)它最少需要兩類角色才能完整跑起來(lái)一類是管理分布式協(xié)調(diào)狀態(tài)的組件另一類是真正存儲(chǔ)和轉(zhuǎn)發(fā)消息的Broker節(jié)點(diǎn)。在過(guò)去很長(zhǎng)一段時(shí)間里這個(gè)協(xié)調(diào)組件就是ZooKeeper它負(fù)責(zé)保存Kafka的元數(shù)據(jù)信息包括主題列表、分區(qū)分配情況、消費(fèi)者組進(jìn)度等。雖然從Kafka 2.8版本開(kāi)始官方引入了KRaft模式把元數(shù)據(jù)管理從ZooKeeper剝離了但到目前為止ZooKeeper模式依然是大量線上環(huán)境的主流選擇尤其是很多公司內(nèi)部的存量集群一時(shí)半會(huì)兒遷不過(guò)去的。所以環(huán)境搭建這件事本質(zhì)上就是在你選擇的操作系統(tǒng)上把JDK、ZooKeeper如果用的話、Kafka Broker按正確的順序和配置部署好讓它們能互相通信、協(xié)同工作。這里還要特別提一個(gè)概念Client和Server要分清楚。Kafka的Server端是Broker集群Client端是生產(chǎn)者和消費(fèi)者。你搭建的環(huán)境是Server端但驗(yàn)證環(huán)境是否成功必須用Client端工具或?qū)懸欢未a來(lái)測(cè)試。很多人搭建完環(huán)境說(shuō)不知道成功沒(méi)有那是因?yàn)橹粏?dòng)了進(jìn)程并沒(méi)有實(shí)際發(fā)消息、收消息來(lái)做驗(yàn)證。1.2 版本選擇陷阱2.x和3.x的差異不只是版本號(hào)Kafka的版本選擇是我見(jiàn)過(guò)的最容易讓人踩坑的地方?jīng)]有之一。你去看Kafka的Apache官網(wǎng)或者GitHub Releases頁(yè)面會(huì)發(fā)現(xiàn)同一時(shí)期有多個(gè)版本并存而且2.x版本和3.x版本不僅僅是數(shù)字上的差別架構(gòu)上都有了很大的變化。首先說(shuō)一下版本號(hào)的規(guī)則。從Kafka 2.0開(kāi)始Apache Kafka采用了新的版本命名方式比如2.13-3.6.0這樣的格式前面的2.13是Scala編譯版本后面的3.6.0才是Kafka的版本號(hào)。很多初學(xué)者看到2.13-3.6.0就以為這是Kafka 2.13是個(gè)舊版本實(shí)際上3.6.0才是真正的Kafka版本前面的2.13指的是用Scala 2.13編譯的。如果你下載的時(shí)候只看前半段數(shù)字很容易下載到不合適的構(gòu)建版本。然后是Kafka版本和生態(tài)組件版本之間的兼容性。比如你用Kafka 3.x配一個(gè)很老的ZooKeeper 3.4.x啟動(dòng)的時(shí)候可能正常但運(yùn)行一段時(shí)間會(huì)出現(xiàn)一些奇怪的連接超時(shí)或會(huì)話過(guò)期錯(cuò)誤。反過(guò)來(lái)Kafka 2.x配太新的ZooKeeper也會(huì)遇到配置項(xiàng)不兼容的問(wèn)題。我的建議是如果是學(xué)習(xí)或者做技術(shù)預(yù)研首選Kafka 3.x的最新穩(wěn)定版用KRaft模式或者ZooKeeper模式都可以但推薦先用ZooKeeper模式因?yàn)榫W(wǎng)上資料多遇到問(wèn)題好搜索。如果是公司生產(chǎn)環(huán)境一定要先確認(rèn)公司已有的監(jiān)控體系、運(yùn)維工具、客戶端版本是否兼容不要盲目上最新版。如果是給老項(xiàng)目升級(jí)優(yōu)先考慮跨版本升級(jí)路徑比如從2.x升到3.x時(shí)建議先看官方的升級(jí)文檔里有沒(méi)有標(biāo)記為不兼容的變更。表格是版本選擇時(shí)最直觀的參考我列一個(gè)我常用的簡(jiǎn)化對(duì)照用途推薦Kafka版本推薦ZooKeeper版本建議模式學(xué)習(xí)/測(cè)試3.4.x及以上3.7.x及以上KRaft或ZooKeeper生產(chǎn)小規(guī)模3.2.x或3.3.x3.6.x及以上ZooKeeper優(yōu)先老項(xiàng)目維護(hù)跟隨現(xiàn)網(wǎng)版本跟隨現(xiàn)網(wǎng)版本ZooKeeper模式關(guān)于Scala版本的選擇我個(gè)人的習(xí)慣是直接選擇官方推薦的那個(gè)不要隨便換。因?yàn)镵afka的啟動(dòng)腳本和部分工具在編譯時(shí)是跟Scala版本綁定的你換一個(gè)編譯版本可能出現(xiàn)微妙的兼容性問(wèn)題。2. 前置環(huán)境準(zhǔn)備JDK配置與ZooKeeper安裝2.1 JDK版本怎么選不是越新越好Kafka是基于JVM運(yùn)行的系統(tǒng)JDK版本直接影響它的運(yùn)行穩(wěn)定性和性能表現(xiàn)。很多新手上來(lái)就裝最新的JDK 17甚至JDK 21結(jié)果發(fā)現(xiàn)Kafka啟動(dòng)報(bào)一堆錯(cuò)或者明明可以啟動(dòng)但G1垃圾回收器一直報(bào)警告日志。Kafka官方對(duì)不同版本的JDK支持情況是有明確聲明的。以Kafka 3.x為例官方博客明確表示支持Java 8、11和17但不同的小版本支持力度不一樣。我自己的實(shí)際經(jīng)驗(yàn)是Kafka 3.0到3.3之間用JDK 8最省心3.4之后的版本用JDK 11或者17更好。但如果你用的是JDK 8u系列的老版本比如8u111之前要注意修一下JCE無(wú)限制強(qiáng)度權(quán)限的問(wèn)題否則加解密相關(guān)的功能會(huì)受限。這里有個(gè)很重要的認(rèn)識(shí)Kafka對(duì)JDK版本的要求不只看Kafka本身還要看你使用的客戶端庫(kù)。如果你的生產(chǎn)者、消費(fèi)者代碼是用JDK 8編譯的而B(niǎo)roker跑在JDK 17上雖然大多數(shù)情況能兼容但如果你用的高版本JDK對(duì)某些舊庫(kù)做了一些安全策略調(diào)整還是可能出現(xiàn)一些奇怪的異常。所以我的建議是環(huán)境統(tǒng)一、版本統(tǒng)一。開(kāi)發(fā)環(huán)境、測(cè)試環(huán)境、生產(chǎn)環(huán)境盡量保持一致。我常用的JDK配置步驟如下下載JDK建議使用tar.gz包而不是rpm或deb包方便自由切換版本并解壓到指定目錄。配置JAVA_HOME和PATH環(huán)境變量。驗(yàn)證版本java -version。這里有個(gè)細(xì)節(jié)值得注意Kafka的啟動(dòng)腳本kafka-server-start.sh默認(rèn)會(huì)找到JAVA_HOME對(duì)應(yīng)的java來(lái)執(zhí)行如果你配置了多個(gè)JDK版本一定要確認(rèn)JAVA_HOME指向了正確的那一個(gè)。2.2 ZooKeeper到底能不能省KRaft模式與ZooKeeper模式的取舍關(guān)于ZooKeeper的爭(zhēng)論在Kafka社區(qū)已經(jīng)持續(xù)了好幾年。Kafka 2.8引入KRaft模式時(shí)官方說(shuō)的是逐步去除ZooKeeper依賴到了3.3版本才把KRaft標(biāo)注為生產(chǎn)可用。但即便到今天很多生產(chǎn)環(huán)境依然用的是ZooKeeper模式。先說(shuō)結(jié)論如果你是新學(xué)Kafka我建議兩種模式都試一遍。先搭ZooKeeper模式理解清楚Kafka的元數(shù)據(jù)管理曾經(jīng)是怎么運(yùn)作的再搭KRaft模式感受一下新架構(gòu)的簡(jiǎn)潔。如果只是出于學(xué)習(xí)目的KRaft模式確實(shí)更簡(jiǎn)單你只需要下載Kafka一個(gè)包不用額外維護(hù)ZooKeeper集群配置也少了一半。但是說(shuō)到生產(chǎn)環(huán)境我目前的態(tài)度還是偏保守。ZooKeeper模式的積累足夠深很多運(yùn)維工具、監(jiān)控腳本、故障處理方案都是圍繞這套架構(gòu)寫(xiě)的KRaft模式雖然簡(jiǎn)化了部署但它在一些極端場(chǎng)景下的表現(xiàn)比如控制器切換、元數(shù)據(jù)同步延遲還沒(méi)有經(jīng)過(guò)足夠長(zhǎng)的時(shí)間驗(yàn)證。而且KRaft的配置參數(shù)和ZooKeeper模式差異很大網(wǎng)上能查到的參考資料相對(duì)少一些。如果你選擇了ZooKeeper模式那ZooKeeper的安裝和Kafka是兩個(gè)獨(dú)立的步驟。ZooKeeper本身是一個(gè)輕量級(jí)的分布式協(xié)調(diào)服務(wù)它也需要JDK配置相對(duì)簡(jiǎn)單核心就是dataDir、clientPort、tickTime這幾個(gè)參數(shù)。我推薦的ZooKeeper部署方式有兩種單機(jī)模式學(xué)習(xí)用配置一個(gè)dataDir和clientPort就能啟動(dòng)。集群模式至少三個(gè)節(jié)點(diǎn)通過(guò)myid文件和zoo.cfg里的server列表來(lái)做選舉。不少人在單機(jī)環(huán)境里圖省事會(huì)把ZooKeeper以單機(jī)模式跑Kafka的Broker也只跑一個(gè)節(jié)點(diǎn)兩者都在同一臺(tái)機(jī)器上。這在開(kāi)發(fā)環(huán)境沒(méi)問(wèn)題但要注意ZooKeeper的數(shù)據(jù)目錄和Kafka的日志目錄不要共用同一個(gè)磁盤(pán)分區(qū)否則日志量大的時(shí)候會(huì)把ZooKeeper的數(shù)據(jù)文件擠爆。3. 單節(jié)點(diǎn)Kafka環(huán)境搭建一步步從下載到收發(fā)消息3.1 下載、解壓和目錄結(jié)構(gòu)解析Kafka的二進(jìn)制包可以在Apache官網(wǎng)找到下載時(shí)認(rèn)準(zhǔn)帶bin后綴的tar.gz包不要下載source包。解壓之后你會(huì)看到一個(gè)很典型的目錄結(jié)構(gòu)我來(lái)逐個(gè)說(shuō)一下這些目錄的作用bin目錄存放所有的啟動(dòng)腳本和運(yùn)維腳本比如kafka-server-start.sh、kafka-topics.sh、kafka-console-producer.sh、kafka-console-consumer.sh。config目錄存放所有配置文件最核心的是server.properties、producer.properties、consumer.properties還有ZooKeeper模式下用到的log4j.properties。libs目錄Kafka運(yùn)行依賴的所有jar包包括Kafka本身的jar包、Scala標(biāo)準(zhǔn)庫(kù)、各種第三方依賴。logs目錄這個(gè)目錄默認(rèn)不一定存在Kafka啟動(dòng)后會(huì)創(chuàng)建里面存放的是Kafka運(yùn)行日志注意這個(gè)logs目錄跟Topic消息的數(shù)據(jù)目錄是兩個(gè)不同的概念。很多新手會(huì)把Kafka的運(yùn)行日志和數(shù)據(jù)目錄搞混。Kafka默認(rèn)的數(shù)據(jù)目錄由配置文件里的log.dirs指定存儲(chǔ)的是Topic分區(qū)的實(shí)際消息文件而logs目錄或者運(yùn)行時(shí)通過(guò)log4j配置指定的目錄存的是Kafka自身的運(yùn)行日志。這兩個(gè)一定要分開(kāi)如果一個(gè)磁盤(pán)滿了另一個(gè)也會(huì)被拖累。解壓后第一件事我建議先看一眼config/server.properties文件把所有帶注釋的配置都過(guò)一遍心里有個(gè)數(shù)。不要急著啟動(dòng)先理解每個(gè)核心參數(shù)是干嘛的。3.2 server.properties核心參數(shù)詳解broker.id、log.dirs、listeners、zookeeper.connectKafka的環(huán)境配置絕大多數(shù)工作量都集中在server.properties這一個(gè)文件上。這個(gè)文件里的參數(shù)非常多但關(guān)鍵的、需要你來(lái)配置的其實(shí)就那么十幾個(gè)其他的基本可以用默認(rèn)值。下面這些是我在每次搭建環(huán)境時(shí)都會(huì)逐一確認(rèn)的參數(shù)broker.id這個(gè)參數(shù)是Kafka集群里每個(gè)Broker節(jié)點(diǎn)的唯一標(biāo)識(shí)必須是整數(shù)。單機(jī)環(huán)境下默認(rèn)是0集群環(huán)境里每個(gè)節(jié)點(diǎn)必須不同。有一點(diǎn)提醒broker.id不要求跟IP綁定但運(yùn)維上建議做一個(gè)IP到broker.id的映射表否則節(jié)點(diǎn)多了以后根本分不清誰(shuí)是誰(shuí)。listeners這個(gè)參數(shù)決定了Kafka對(duì)外提供服務(wù)的地址和端口。默認(rèn)是PLAINTEXT://:9092也就是本機(jī)所有網(wǎng)卡的9092端口。如果你是單機(jī)測(cè)試用默認(rèn)就好但如果你要遠(yuǎn)端的客戶端來(lái)連接切記要配置成內(nèi)網(wǎng)IP或者0.0.0.0并且要同時(shí)關(guān)注advertised.listeners。我遇到過(guò)不止一次Kafka Broker看著啟動(dòng)成功了客戶端卻連不上最后發(fā)現(xiàn)是advertised.listeners寫(xiě)成了localhost客戶端拿到的地址自然是本機(jī)。log.dirs這個(gè)參數(shù)指定了消息數(shù)據(jù)的存儲(chǔ)目錄。默認(rèn)是/tmp/kafka-logs這個(gè)強(qiáng)烈建議改掉。原因很簡(jiǎn)單很多操作系統(tǒng)會(huì)定期清理/tmp下的文件把你的Kafka數(shù)據(jù)當(dāng)垃圾文件清掉。而且/tmp分區(qū)的大小往往受限消息量一大就會(huì)爆盤(pán)。我一般在規(guī)劃環(huán)境時(shí)會(huì)單獨(dú)掛一塊數(shù)據(jù)盤(pán)或者至少用一個(gè)專門(mén)的目錄比如/data/kafka-logs確保磁盤(pán)空間和業(yè)務(wù)數(shù)據(jù)的隔離。zookeeper.connect這是ZooKeeper模式下最重要的參數(shù)指定了Kafka連接ZooKeeper的地址。單機(jī)模式就是一個(gè)IP加2181端口集群模式可以用逗號(hào)分隔多個(gè)ZooKeeper節(jié)點(diǎn)比如192.168.1.10:2181,192.168.1.11:2181,192.168.1.12:2181。如果你只有一個(gè)ZooKeeper節(jié)點(diǎn)那直接寫(xiě)一個(gè)地址就行。還有一個(gè)可選配置chroot路徑比如192.168.1.10:2181/kafka這樣可以讓Kafka的元數(shù)據(jù)都保存在ZooKeeper的/kafka節(jié)點(diǎn)下避免跟其他使用共用ZooKeeper的服務(wù)沖突。num.partitions這個(gè)參數(shù)是Topic的默認(rèn)分區(qū)數(shù)默認(rèn)是1。如果你后續(xù)不打算手動(dòng)為每個(gè)Topic指定分區(qū)數(shù)建議提前改成一個(gè)合理的值比如3。我見(jiàn)過(guò)很多測(cè)試環(huán)境因?yàn)槟J(rèn)分區(qū)數(shù)是1導(dǎo)致消費(fèi)者并行度上不去還以為是代碼寫(xiě)錯(cuò)了。log.retention.hours消息保留時(shí)間默認(rèn)是168小時(shí)也就是7天。開(kāi)發(fā)測(cè)試環(huán)境下可以縮短比如24小時(shí)省磁盤(pán)。生產(chǎn)環(huán)境根據(jù)業(yè)務(wù)數(shù)據(jù)保留策略來(lái)定不是越長(zhǎng)越好因?yàn)镵afka清理過(guò)期數(shù)據(jù)是定期執(zhí)行的數(shù)據(jù)量大的時(shí)候清理線程本身也會(huì)占用I/O資源。auto.create.topics.enable這個(gè)參數(shù)默認(rèn)是true意思是當(dāng)你往一個(gè)不存在的Topic發(fā)送消息時(shí)Kafka會(huì)自動(dòng)創(chuàng)建這個(gè)Topic。開(kāi)發(fā)環(huán)境方便但生產(chǎn)環(huán)境我建議改成false。原因是自動(dòng)創(chuàng)建的Topic用的是默認(rèn)分區(qū)數(shù)和副本數(shù)容易打亂集群的資源規(guī)劃而且一旦有客戶端誤發(fā)了消息就會(huì)產(chǎn)生一堆意想不到的Topic增加運(yùn)維負(fù)擔(dān)。3.3 啟動(dòng)驗(yàn)證用兩條命令把數(shù)據(jù)跑起來(lái)環(huán)境配置完成后啟動(dòng)順序必須嚴(yán)格遵守先啟動(dòng)ZooKeeper再啟動(dòng)Kafka Broker。ZooKeeper的啟動(dòng)命令是bin/zookeeper-server-start.sh config/zookeeper.properties注意這個(gè)腳本在Kafka的bin目錄下是Kafka自帶的那份ZooKeeper啟動(dòng)腳本。如果你之前沒(méi)裝過(guò)ZooKeeper直接用這個(gè)就行如果你另外裝了獨(dú)立的ZooKeeper那最好用獨(dú)立安裝的那個(gè)來(lái)啟動(dòng)避免版本不一致帶來(lái)問(wèn)題。Kafka Broker的啟動(dòng)命令是bin/kafka-server-start.sh config/server.properties如果你希望進(jìn)程在后臺(tái)運(yùn)行可以加上-daemon參數(shù)bin/kafka-server-start.sh -daemon config/server.properties啟動(dòng)完之后我強(qiáng)烈建議做一次完整的功能驗(yàn)證而不僅僅是看進(jìn)程在不在。完整驗(yàn)證分三步走第一步確認(rèn)進(jìn)程存在。用jps或者ps -ef | grep kafka檢查是否有Kafka進(jìn)程同時(shí)確認(rèn)ZooKeeper進(jìn)程存在。第二步創(chuàng)建一個(gè)測(cè)試Topic。在Kafka的bin目錄下執(zhí)行bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic test-topic --partitions 3 --replication-factor 1如果沒(méi)有報(bào)錯(cuò)再執(zhí)行bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic test-topic這條命令應(yīng)該能看到Topic的分區(qū)分布信息比如Partition 0、Partition 1、Partition 2以及對(duì)應(yīng)的Leader和Replicas。第三步用控制臺(tái)生產(chǎn)者和消費(fèi)者實(shí)測(cè)消息通路。打開(kāi)一個(gè)終端啟動(dòng)消費(fèi)者bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test-topic --from-beginning再開(kāi)另一個(gè)終端啟動(dòng)生產(chǎn)者bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic test-topic在生產(chǎn)者終端輸入一條消息回車切到消費(fèi)者終端看能不能收到。能收到就說(shuō)明整個(gè)環(huán)境已經(jīng)通了。這里有一個(gè)細(xì)節(jié)新版Kafka推薦用--bootstrap-server來(lái)連接Broker但很多老教程還在用--zookeeper來(lái)操作Topic。如果你用的是3.x版本某些場(chǎng)景下--zookeeper已經(jīng)被標(biāo)記為過(guò)度方案比如創(chuàng)建Topic這種操作直接走Broker的API就行不需要再經(jīng)過(guò)ZooKeeper。另外消費(fèi)者用--from-beginning參數(shù)會(huì)把歷史消息全部拉下來(lái)如果你只想看新消息就不要加這個(gè)參數(shù)。4. 三節(jié)點(diǎn)集群環(huán)境搭建高可用不是加一臺(tái)機(jī)器那么簡(jiǎn)單4.1 多節(jié)點(diǎn)配置要點(diǎn)broker.id不能重復(fù)listeners端口不能沖突單節(jié)點(diǎn)環(huán)境跑通之后很多人會(huì)想著直接上集群。這個(gè)想法很好但集群環(huán)境跟單機(jī)環(huán)境有個(gè)本質(zhì)區(qū)別單機(jī)環(huán)境你只需要關(guān)心參數(shù)能不能跑起來(lái)集群環(huán)境你還得關(guān)心節(jié)點(diǎn)之間的網(wǎng)絡(luò)、角色、選舉、數(shù)據(jù)冗余任何一個(gè)環(huán)節(jié)沒(méi)有配好集群就是假集群表面上三個(gè)進(jìn)程都在實(shí)際上副本數(shù)一直處于不健康狀態(tài)。我搭建三節(jié)點(diǎn)集群時(shí)的規(guī)劃是這樣的三臺(tái)虛擬機(jī)或三臺(tái)物理機(jī)上每臺(tái)都部署一個(gè)ZooKeeper實(shí)例和一個(gè)Kafka Broker實(shí)例。ZooKeeper三節(jié)點(diǎn)組成一個(gè)集群Kafka三節(jié)點(diǎn)組成一個(gè)Broker集群。如果你機(jī)器資源緊張可以把ZooKeeper和Kafka混部在同一批機(jī)器上但生產(chǎn)環(huán)境強(qiáng)烈建議分開(kāi)部署因?yàn)閆ooKeeper對(duì)磁盤(pán)延遲比較敏感混部會(huì)導(dǎo)致它的選舉和同步性能受影響。先配置ZooKeeper集群。每個(gè)ZooKeeper節(jié)點(diǎn)需要一個(gè)myid文件存放在dataDir指定的目錄下內(nèi)容就是當(dāng)前節(jié)點(diǎn)的ID比如1、2、3。zoo.cfg里要配置dataDir/data/zookeeper clientPort2181 server.1192.168.1.10:2888:3888 server.2192.168.1.11:2888:3888 server.3192.168.1.12:2888:3888 tickTime2000 initLimit10 syncLimit5三個(gè)節(jié)點(diǎn)的zoo.cfg基本一樣唯一的區(qū)別就是myid文件不一樣。2888是ZooKeeper節(jié)點(diǎn)間同步數(shù)據(jù)的端口3888是選舉端口這兩個(gè)端口必須在防火墻里放行很多集群搭完發(fā)現(xiàn)ZooKeeper一直處于LOOKING狀態(tài)就是因?yàn)檫@兩個(gè)端口被防火墻攔了。Kafka集群的server.properties配置相比單機(jī)只多幾個(gè)關(guān)鍵點(diǎn)broker.id要分別設(shè)置為0、1、2。zookeeper.connect要配置成三個(gè)ZooKeeper地址的列表中間用逗號(hào)分隔。log.dirs建議每個(gè)節(jié)點(diǎn)用不同的目錄比如本機(jī)的/data/kafka-logs不要用共享存儲(chǔ)。listeners和advertised.listeners在每臺(tái)機(jī)器上要配置本機(jī)的內(nèi)網(wǎng)IP。這里最容易出錯(cuò)的是復(fù)制配置時(shí)忘了改IP。4.2 集群?jiǎn)?dòng)驗(yàn)證Topic副本數(shù)的意義和副本狀態(tài)檢查集群搭建完成后的驗(yàn)證跟單節(jié)點(diǎn)的驗(yàn)證不一樣重點(diǎn)從能不能收消息變成了數(shù)據(jù)有沒(méi)有副本、副本有沒(méi)有同步。啟動(dòng)完三臺(tái)Broker后先看集群狀態(tài)。在任意一臺(tái)機(jī)器上執(zhí)行bin/kafka-topics.sh --bootstrap-server 192.168.1.10:9092 --describe --topic test-topic如果這個(gè)Topic的ReplicationFactor大于1那么輸出結(jié)果里每個(gè)分區(qū)的Replicas應(yīng)該有三臺(tái)Broker的IDIsrIn-Sync Replicas里的節(jié)點(diǎn)數(shù)量應(yīng)該跟副本數(shù)一致。如果Isr里的節(jié)點(diǎn)數(shù)少了一個(gè)說(shuō)明有一個(gè)Broker的同步落后了這通常意味著這個(gè)節(jié)點(diǎn)的磁盤(pán)I/O或者網(wǎng)絡(luò)有問(wèn)題需要單獨(dú)排查不能簡(jiǎn)單重啟了事。創(chuàng)建集群Topic時(shí)的副本數(shù)建議設(shè)置成2或者3。副本數(shù)不是越大越好因?yàn)槊吭黾右粋€(gè)副本就意味著多一份磁盤(pán)空間占用和副本同步的流量開(kāi)銷。一般三節(jié)點(diǎn)集群副本數(shù)設(shè)2就能容忍單點(diǎn)故障設(shè)3能容忍更極端的故障場(chǎng)景但代價(jià)是磁盤(pán)占用翻倍。我個(gè)人習(xí)慣在測(cè)試環(huán)境設(shè)2生產(chǎn)環(huán)境設(shè)3。驗(yàn)證消息通路時(shí)生產(chǎn)者和消費(fèi)者的--bootstrap-server參數(shù)要指向集群中任意一個(gè)Broker的地址即可不用寫(xiě)三個(gè)。Kafka客戶端會(huì)自動(dòng)通過(guò)這個(gè)Broker拉取整個(gè)集群的元數(shù)據(jù)知道哪個(gè)分區(qū)在哪個(gè)Broker上。關(guān)于集群還有一個(gè)常見(jiàn)誤解以為連上集群中任何一個(gè)節(jié)點(diǎn)就能保證消息不丟。實(shí)際上消息不丟依賴的是副本同步機(jī)制生產(chǎn)者要設(shè)置acks參數(shù)。acksall代表等待所有副本都確認(rèn)才返回成功這是保證消息不丟的前提。acks0或acks1雖然性能高但節(jié)點(diǎn)故障時(shí)會(huì)有丟消息風(fēng)險(xiǎn)。這個(gè)參數(shù)雖然是在生產(chǎn)者端配置的但搭建環(huán)境時(shí)就應(yīng)該想清楚因?yàn)楹罄m(xù)所有測(cè)試都基于這個(gè)前提。4.3 KRaft模式集群搭建去掉ZooKeeper后的配置差異既然前面提到了KRaft模式這里順便補(bǔ)充一下KRaft模式下集群怎么搭。KRaft模式最大的變化是不需要ZooKeeperKafka自身用一組Controller節(jié)點(diǎn)來(lái)管理元數(shù)據(jù)。部署KRaft模式的集群首先要做的事跟ZooKeeper模式完全不同你要先格式化存儲(chǔ)目錄生成一個(gè)Cluster ID然后每個(gè)節(jié)點(diǎn)都要用到這個(gè)IDController節(jié)點(diǎn)和Broker節(jié)點(diǎn)的配置分開(kāi)放到不同文件。KRaft模式下核心的配置項(xiàng)是process.roles這個(gè)參數(shù)可以設(shè)置broker、controller或者broker,controller決定了節(jié)點(diǎn)的角色。如果是混部模式就寫(xiě)broker,controller如果是分角色部署Controller節(jié)點(diǎn)寫(xiě)controllerBroker節(jié)點(diǎn)寫(xiě)broker。node.id相當(dāng)于broker.id和controller.id的統(tǒng)一體集群內(nèi)唯一。controller.quorum.voters指定Controller節(jié)點(diǎn)列表的地址和ID類似ZooKeeper的server列表但格式不同比如1192.168.1.10:9093,2192.168.1.11:9093,3192.168.1.12:9093。我第一次試KRaft模式時(shí)卡得最久的就是格式化這一步。Kafka提供了一個(gè)腳本用于生成Cluster ID和格式化存儲(chǔ)目錄bin/kafka-storage.sh random-uuid bin/kafka-storage.sh format -t uuid -c config/kraft/server.properties如果你漏掉了格式化這一步直接啟動(dòng)Kafka會(huì)報(bào)一個(gè)Directory is not formatted的錯(cuò)誤。這個(gè)錯(cuò)倒是很直觀但如果你用的是舊版本的KRaft報(bào)錯(cuò)信息可能不一樣有時(shí)候是No meta.properties found這種經(jīng)驗(yàn)不夠的話很容易誤以為配置寫(xiě)錯(cuò)了。KRaft模式解決了ZooKeeper模式的兩個(gè)痛點(diǎn)一個(gè)是減少了需要維護(hù)的外部依賴另一個(gè)是元數(shù)據(jù)的讀寫(xiě)路徑更短Controller可以直接通過(guò)內(nèi)部Raft協(xié)議管理元數(shù)據(jù)不用再跨ZooKeeper同步。但就像我前面說(shuō)的學(xué)習(xí)理解ZooKeeper模式的核心概念仍然有價(jià)值因?yàn)楹芏嗌a(chǎn)環(huán)境還沒(méi)有遷到KRaft。5. 環(huán)境搭建中的高頻問(wèn)題與排查思路5.1 網(wǎng)絡(luò)和端口問(wèn)題連不上、超時(shí)的常見(jiàn)原因Kafka環(huán)境搭建遇到最多的就是網(wǎng)絡(luò)問(wèn)題。由于Kafka涉及多端口、多角色任何一個(gè)端口沒(méi)放行、任何一個(gè)IP配置不匹配表現(xiàn)出來(lái)的癥狀都是連不上。我先列一個(gè)端口清單讓你心里有個(gè)數(shù)組件默認(rèn)端口用途ZooKeeper客戶端端口2181Kafka連接ZooKeeperZooKeeper節(jié)點(diǎn)間同步端口2888集群節(jié)點(diǎn)間數(shù)據(jù)同步ZooKeeper選舉端口3888Leader選舉通信Kafka Broker端口9092客戶端生產(chǎn)和消費(fèi)消息Kafka Controller端口KRaft9093Controller節(jié)點(diǎn)間通信排查網(wǎng)絡(luò)問(wèn)題的通用思路是先確認(rèn)進(jìn)程監(jiān)聽(tīng)在哪個(gè)端口再確認(rèn)目標(biāo)機(jī)器的端口能不能通最后再去查應(yīng)用日志。第一步在Broker節(jié)點(diǎn)上執(zhí)行ss -tlnp | grep 9092確認(rèn)Kafka確實(shí)監(jiān)聽(tīng)在9092。如果這里顯示監(jiān)聽(tīng)的IP是127.0.0.1而不是0.0.0.0或你的內(nèi)網(wǎng)IP那說(shuō)明listeners配置有問(wèn)題。第二步在客戶端機(jī)器上執(zhí)行telnet或nc測(cè)試到Broker的9092端口是否可通。比如telnet 192.168.1.10 9092如果通了會(huì)顯示Connected to...。如果超時(shí)要么是防火墻擋了要么是安全組規(guī)則沒(méi)放行要么是網(wǎng)絡(luò)不通。第三步查看Kafka的server.log。這個(gè)日志文件在logs目錄下里面會(huì)記錄Broker的啟動(dòng)過(guò)程、連接的客戶端地址、異常信息。很多網(wǎng)絡(luò)問(wèn)題都能從日志里找到線索比如Connection refused代表目標(biāo)端口沒(méi)監(jiān)聽(tīng)Connection timed out代表網(wǎng)絡(luò)層就不通。還有一種很隱蔽的情況Broker配置正常、端口也通但客戶端還是報(bào)TimeoutException。這種情況大概率是advertised.listeners配置成了127.0.0.1或localhost客戶端從集群元數(shù)據(jù)里拿到的是Broker自己的回環(huán)地址自然無(wú)法連接。我在本地用Docker跑Kafka時(shí)就經(jīng)常遇到因?yàn)槿萜鲀?nèi)的localhost跟宿主機(jī)的localhost不是同一個(gè)概念。5.2 磁盤(pán)和內(nèi)存問(wèn)題爆盤(pán)、OOM、垃圾回收異常Kafka對(duì)磁盤(pán)和內(nèi)存的要求是出了名的高很多環(huán)境搭起來(lái)能用但一有流量就掛多半是這兩塊配置沒(méi)做好。先說(shuō)磁盤(pán)。Kafka是順序?qū)懴⒌倪@個(gè)特性決定了它對(duì)磁盤(pán)的訴求是順序?qū)懶阅芤枚皇请S機(jī)讀性能要多強(qiáng)。所以服務(wù)器選型時(shí)機(jī)械硬盤(pán)不是不能用但一定要確認(rèn)RAID策略和磁盤(pán)轉(zhuǎn)速SSD當(dāng)然是更好的選擇尤其是NVMe盤(pán)。另外Kafka的數(shù)據(jù)目錄千萬(wàn)不要放系統(tǒng)盤(pán)也不要跟ZooKeeper或數(shù)據(jù)庫(kù)的數(shù)據(jù)目錄混在一起。爆盤(pán)問(wèn)題是最常見(jiàn)的。Kafka會(huì)在磁盤(pán)剩余空間低于一定閾值時(shí)報(bào)警默認(rèn)的閾值是低于500MB進(jìn)入只讀模式低于200MB甚至無(wú)法啟動(dòng)。這個(gè)參數(shù)是log.flush.offset.checkpoint.interval.ms之后的保護(hù)機(jī)制實(shí)際上不同版本的閾值定義不太一樣。我的經(jīng)驗(yàn)是給Kafka單獨(dú)規(guī)劃一塊盤(pán)容量按單日消息量乘以保留天數(shù)再乘以副本數(shù)來(lái)估算比如每天100GB流量、保留3天、2副本那至少要600GB空間再留30%的余量就是800GB左右。再說(shuō)內(nèi)存。Kafka本身是Java應(yīng)用JVM堆內(nèi)存大小通過(guò)bin/kafka-server-start.sh里的KAFKA_HEAP_OPTS來(lái)控制默認(rèn)是1GB-Xmx1G -Xms1G。開(kāi)發(fā)測(cè)試環(huán)境1GB綽綽有余但生產(chǎn)環(huán)境一般建議8GB到16GB起步。這里有個(gè)容易忽視的點(diǎn)Kafka的頁(yè)緩存Page Cache是操作系統(tǒng)層面的不占JVM堆內(nèi)存所以不要一味地調(diào)大JVM堆要給操作系統(tǒng)留足夠的內(nèi)存來(lái)緩存消息數(shù)據(jù)。我見(jiàn)過(guò)有人把JVM堆設(shè)成32GB結(jié)果系統(tǒng)內(nèi)存全被JVM吃掉了頁(yè)緩存幾乎為0Kafka的吞吐量反而下降因?yàn)榇罅孔x操作要走磁盤(pán)而不是走內(nèi)存緩存。如果Kafka進(jìn)程在啟動(dòng)后不久就報(bào)OutOfMemoryError優(yōu)先檢查KAFKA_HEAP_OPTS是不是太小。如果是GC時(shí)間過(guò)長(zhǎng)導(dǎo)致的暫??梢韵瓤碐C日志判斷是堆太小還是有大對(duì)象的分配問(wèn)題。實(shí)用的做法是先保持默認(rèn)的G1收集器不變把堆內(nèi)存調(diào)到合適范圍然后觀察一段時(shí)間不要急著換GC算法。5.3 高并發(fā)環(huán)境下的JVM參數(shù)優(yōu)化聊到環(huán)境搭建很多人以為配置完就完事了實(shí)際上Broker的JVM參數(shù)對(duì)高并發(fā)場(chǎng)景的影響非常大。我在壓測(cè)Kafka環(huán)境時(shí)習(xí)慣在啟動(dòng)前就把JVM的GC日志打開(kāi)這樣就算后面出問(wèn)題也有據(jù)可查。默認(rèn)的bin/kafka-server-start.sh腳本里JVM參數(shù)是通過(guò)KAFKA_HEAP_OPTS和KAFKA_JVM_PERFORMANCE_OPTS兩個(gè)環(huán)境變量注入的。KAFKA_JVM_PERFORMANCE_OPTS默認(rèn)內(nèi)容大概是這樣-server -XX:UseG1GC -XX:MaxGCPauseMillis20 -XX:InitiatingHeapOccupancyPercent35 -XX:DisableExplicitGCG1GC是默認(rèn)的收集器MaxGCPauseMillis控制目標(biāo)停頓時(shí)間InitiatingHeapOccupancyPercent控制G1并發(fā)標(biāo)記的觸發(fā)閾值。這些參數(shù)不需要你去改但建議你理解它們的作用。如果遇到GC停頓時(shí)間飆升除了調(diào)參更要先找代碼層面有沒(méi)有問(wèn)題比如消費(fèi)者客戶端頻繁創(chuàng)建新的Consumer實(shí)例、生產(chǎn)者端批量參數(shù)沒(méi)設(shè)置好都會(huì)導(dǎo)致Broker端負(fù)載異常。一個(gè)非常多人忽略的參數(shù)是num.network.threads和num.io.threadsnum.network.threads默認(rèn)是3負(fù)責(zé)處理網(wǎng)絡(luò)請(qǐng)求。num.io.threads默認(rèn)是8負(fù)責(zé)處理實(shí)際的讀寫(xiě)請(qǐng)求。當(dāng)你的生產(chǎn)者、消費(fèi)者連接數(shù)很多或者請(qǐng)求量很大的時(shí)候這兩個(gè)線程數(shù)可能會(huì)出現(xiàn)瓶頸。我在壓測(cè)環(huán)境里一般會(huì)先調(diào)到num.network.threads8、num.io.threads16再根據(jù)監(jiān)控逐步調(diào)整。不過(guò)提醒一句調(diào)線程數(shù)不是越多越好線程多了會(huì)帶來(lái)線程切換的額外開(kāi)銷反而降低性能。5.4 安全認(rèn)證配置的基礎(chǔ)注意事項(xiàng)如果搭建的Kafka環(huán)境要暴露到不受信任的網(wǎng)絡(luò)或者公司內(nèi)部有安全合規(guī)要求那SSL認(rèn)證和SASL認(rèn)證就是繞不開(kāi)的話題。Kafka的安全配置涉及兩個(gè)鏈路客戶端到Broker、Broker到ZooKeeper如果是ZooKeeper模式。配置不當(dāng)會(huì)導(dǎo)致一個(gè)詭異的現(xiàn)象客戶端能用telnet連上9092端口但一執(zhí)行消費(fèi)命令就報(bào)認(rèn)證失敗。配置SSL時(shí)核心步驟是生成服務(wù)端證書(shū)和客戶端證書(shū)然后分別配置到server.properties的ssl.keystore.location和ssl.truststore.location客戶端則在連接參數(shù)里配置ssl.truststore.location。這一步的坑在于證書(shū)的CNCommon Name必須和Broker的主機(jī)名匹配否則SSL握手時(shí)會(huì)因?yàn)橹鳈C(jī)名校驗(yàn)失敗而拒絕連接。如果是IP直連方式的客戶端需要在證書(shū)里配置subjectAlternativeName的IP否則一樣會(huì)失敗。如果你只是想簡(jiǎn)單驗(yàn)證SASL配置可以用Kafka自帶的配置模板config目錄下有kafka_server_jaas.conf等文件可以直接參考。不過(guò)請(qǐng)記住一個(gè)原則安全認(rèn)證先在內(nèi)網(wǎng)測(cè)試環(huán)境驗(yàn)證完整流程再上生產(chǎn)不要在生產(chǎn)環(huán)境直接開(kāi)啟認(rèn)證后才發(fā)現(xiàn)客戶端全部連不上。6. 可視化工具與后續(xù)學(xué)習(xí)建議6.1 常用監(jiān)控工具從命令行到可視化的過(guò)渡環(huán)境搭好之后純命令行操作效率太低了尤其是多個(gè)主題、多個(gè)消費(fèi)者組的情況下用眼睛看命令輸出根本盯不過(guò)來(lái)。我通常會(huì)搭一個(gè)可視化的監(jiān)控界面把集群狀態(tài)、Topic列表、消費(fèi)者Lag這些關(guān)鍵指標(biāo)集中展示出來(lái)。常見(jiàn)的方案有三種Kafka UI舊稱UI for Apache Kafka界面清爽支持多集群管理能直接查看Topic、分區(qū)、消費(fèi)者組、消息內(nèi)容對(duì)新手很友好是我首推的學(xué)習(xí)工具。Kafdrop輕量級(jí)Spring Boot項(xiàng)目一條命令就能啟動(dòng)但功能相對(duì)簡(jiǎn)單適合快速查看消息。Prometheus Grafana這是生產(chǎn)環(huán)境的主流監(jiān)控方案通過(guò)JMX Exporter暴露Kafka指標(biāo)然后在Grafana上配置看板。對(duì)新手前期不推薦因?yàn)閷W(xué)習(xí)曲線比較陡但你遲早要接觸它。如果你只是想快速看消息內(nèi)容Kafdrop的啟動(dòng)非常簡(jiǎn)單一條jar命令就行java -jar kafdrop.jar --kafka.brokerConnect192.168.1.10:9092如果你希望功能更全Kafka UI會(huì)是一個(gè)更完整的選擇。它支持通過(guò)配置文件來(lái)管理多個(gè)Kafka集群還能展示某個(gè)消費(fèi)者組的消費(fèi)進(jìn)度和Lag。但注意Kafka UI本身是Java應(yīng)用部署時(shí)同樣要注意JDK版本兼容。6.2 從環(huán)境搭建到生產(chǎn)級(jí)安裝要補(bǔ)的功課環(huán)境搭建只是第一步從能跑到生產(chǎn)可用中間差的功課還有很多。我總結(jié)幾個(gè)優(yōu)先級(jí)比較高的事項(xiàng)第一參數(shù)基線化。把搭建環(huán)境的參數(shù)記錄下來(lái)打包成標(biāo)準(zhǔn)配置模板不要再每次手動(dòng)改。Kafka的參數(shù)實(shí)在太多今天改了一個(gè)明天忘了是誰(shuí)改的后面排查問(wèn)題全靠猜。用配置文件管理工具把server.properties作為代碼管理記錄變更歷史這會(huì)省下無(wú)數(shù)時(shí)間。第二日志和監(jiān)控的打通。Kafka本身的運(yùn)行日志需要采集Broker的指標(biāo)需要暴露。生產(chǎn)環(huán)境至少要保證進(jìn)程級(jí)別的存活監(jiān)控、磁盤(pán)使用率監(jiān)控、Topic級(jí)別的消息積壓監(jiān)控。任何一個(gè)維度缺失線上問(wèn)題都會(huì)難定位。第三消息發(fā)送的可靠性驗(yàn)證。環(huán)境搭好之后不要只測(cè)一條消息能不能通。要測(cè)異常場(chǎng)景殺掉一個(gè)Broker看消息會(huì)不會(huì)丟、消費(fèi)者能不能繼續(xù)消費(fèi)把磁盤(pán)空間占滿看Kafka的表現(xiàn)是什么重啟一個(gè)節(jié)點(diǎn)看分區(qū)副本能不能自動(dòng)恢復(fù)。這些測(cè)試沒(méi)有一個(gè)會(huì)白做它們才是環(huán)境真正可用的證明。7. 寫(xiě)在最后的幾點(diǎn)經(jīng)驗(yàn)Kafka環(huán)境搭建這件事看起來(lái)就是個(gè)安裝配置流程但在實(shí)際操作中真正花時(shí)間的往往不是安裝本身而是對(duì)Kafka運(yùn)行機(jī)制的理解和對(duì)各種邊界情況的處理。我陸陸續(xù)續(xù)搭過(guò)開(kāi)發(fā)環(huán)境、測(cè)試環(huán)境和生產(chǎn)環(huán)境每一次都會(huì)踩到不一樣的坑但有幾個(gè)經(jīng)驗(yàn)是通用的第一啟動(dòng)順序永遠(yuǎn)不要亂。ZooKeeper模式先啟ZooKeeper再啟KafkaKRaft模式雖然不需要ZooKeeper但每個(gè)節(jié)點(diǎn)要保證controller角色先就緒再啟動(dòng)broker角色。順序錯(cuò)了表面上不會(huì)有立刻的崩潰但后臺(tái)的錯(cuò)誤日志會(huì)持續(xù)報(bào)。第二版本統(tǒng)一是一個(gè)硬性原則。Kafka、ZooKeeper、JDK、客戶端庫(kù)、可視化工具的版本盡量在同一個(gè)兼容矩陣?yán)镞x。不要小看版本差異我在測(cè)試環(huán)境遇到過(guò)因?yàn)榭蛻舳藥?kù)版本太低連不上3.x版本的Broker報(bào)的是UnsupportedVersionException根本讓人摸不著頭腦。第三日志和監(jiān)控從第一天就做好。哪怕只是單機(jī)測(cè)試環(huán)境也把Kafka的日志級(jí)別調(diào)整好把關(guān)鍵目錄的空間監(jiān)控加好。等到出了大問(wèn)題再來(lái)加監(jiān)控往往已經(jīng)晚了。如果你正在搭Kafka環(huán)境我的建議是先按這篇的步驟把單機(jī)環(huán)境跑通再把三節(jié)點(diǎn)集群跑通最后再考慮安全認(rèn)證和監(jiān)控完善。不要一上來(lái)就追求完美架構(gòu)先把基礎(chǔ)鏈路跑起來(lái)你才能真正理解Kafka的每一個(gè)組件在干什么。