不废话,直接说结论:Kafka在Linux下的启动,说难不难,但说简单也绝不简单。我自己第一次在服务器上把Kafka跑起来时,就栽在了一个很蠢的细节上——照着Windows教程想也没想就去找kafka-server-start.bat,结果当然是找不到。后来又经历了几次"窗口启动又秒退"、"jps看不到进程"、"日志刷了一屏不知道到底成功没有"的折磨之后,我干脆把整套启动流程写成了一个带start/stop/status的脚本。这篇就把完整脚本和启动链路里最容易踩的坑全部整理出来,给同样从小白阶段起步的朋友,照着做就行。
1. 启动前检查这三样,能避开九成报错
Kafka启动失败,绝大多数不是Kafka本身的问题,而是启动之前的环境就没准备好。我每次在新机器上部署Kafka,动手敲启动命令前都会按下面的顺序过一遍检查项,总共就三样:JDK、安装包、配置文件。
1.1 JDK和jps:先确认Java环境真的能用
Kafka是JVM应用,没有Java环境什么都白搭。检查JDK不要只看java -version能打印版本号就算完,还要确认jps这个工具能用。jps来自JDK而不是JRE,如果你装的是精简版JRE,java -version没问题但jps会报"command not found"。而后面的启动和进程检查都要依赖jps或ps。
java -version jps -l版本方面需要注意:Kafka 2.x版本用JDK 8完全没问题;Kafka 3.0之后的版本建议用JDK 11或17。尤其是Kafka 3.4以后的某些版本,在JDK 8环境下运行会提示不支持。我自己的习惯是服务器上统一装JDK 11,兼容性最稳。如果机器上已经装了多个JDK版本,但默认版本不对,可以通过update-alternatives --config java切换,或者直接把JAVA_HOME和PATH写进/etc/profile。
1.2 Kafka安装包选型和解压位置
Kafka官网下载页面会看到类似kafka_2.13-3.6.0.tgz这样的包名,前面的2.13是编译Kafka时使用的Scala版本,后面的3.6.0才是Kafka本身的版本。我建议优先选最新的稳定版本号,Scala版本选哪种影响不大,只要不是特别老的项目,2.13或2.12都行。下载之后解压,推荐放到/usr/local/kafka或/opt/kafka这类统一目录,并设置一个软链,方便以后升级版本。
tar -xzf kafka_2.13-3.6.0.tgz mv kafka_2.13-3.6.0 /usr/local/kafka这里顺带说一个解压的坑:如果你拿到的是zip包,并且在Windows上解压过一次再传到Linux,经常会出现文件名乱码的问题。用unzip -O GBK xxx.zip能解决一部分乱码情况,但最稳妥的办法还是直接用Linux上重新解压tar.gz包,Kafka官方本来就推荐用tgz格式,不会有这类问题。
解压之后,Kafka的所有启动脚本都在bin/目录下,Windows下的.bat文件在bin/windows/目录下,Linux下真正要用的是不带扩展名的.sh脚本。这一点很多新手会懵,下面第3章会再展开。
1.3 server.properties里必须亲手改的两个地方
Kafka的配置文件在config/server.properties,默认配置里有几个关键参数是启动前必须确认的,否则启动后会出现各种"看起来正常但连不上"的诡异问题。
第一是broker.id,默认是0。单机部署不需要改,但如果你要搭多节点集群,每个节点这个值必须唯一。
第二是listeners,这是最容易被忽略的设置。如果保持默认值,Kafka会以PLAINTEXT://:9092这样的格式监听本机所有网卡的9092端口。如果你只是在本机测试,这个默认值没问题;但如果你要在别的机器上访问这台Kafka,就需要显式设置,否则会碰到"生产和消费都能跑通,但远程客户端连不上"的情况。
broker.id=0 listeners=PLAINTEXT://0.0.0.0:9092 log.dirs=/data/kafka-logs zookeeper.connect=localhost:2181第三是log.dirs,这是Kafka存放消息数据的位置,默认在/tmp/kafka-logs。必须改掉,因为/tmp目录在Linux重启后会被清空,一旦清了,Kafka所有数据都没了。我习惯改成独立的/data/kafka-logs,并提前创建好目录、赋予当前用户写权限。
mkdir -p /data/kafka-logs如果目录不存在或没有写权限,Kafka启动会直接报错退出,所以这一步提前做掉能少踩一个坑。
2. 启动链路拆解:先ZooKeeper后Kafka,版本差异别搞混
Kafka 2.x以及3.0到3.4之间常见的部署模式,都依赖ZooKeeper来保存集群元数据。虽然Kafka 3.0开始引入了KRaft模式、可以去掉ZooKeeper,但实际工作中接触到的老集群、老教程、大多数公司的生产环境,仍然以ZooKeeper模式为主。所以这篇里我按最常见的模式来讲,先启动ZooKeeper,再启动Kafka。
2.1 标准启动命令与成功标志
启动ZooKeeper的命令在Kafka的bin目录下:
cd /usr/local/kafka bin/zookeeper-server-start.sh config/zookeeper.propertiesZooKeeper默认监听2181端口,看到输出里有binding to port 0.0.0.0/0.0.0.0:2181基本就说明起来了。这个进程启动之后会一直占用当前终端,所以实操中通常用nohup ... &或者后面给的脚本来后台运行。
ZooKeeper起来之后,再启动Kafka:
bin/kafka-server-start.sh config/server.propertiesKafka默认监听9092端口。如果配置没问题,启动日志里会看到一条典型的成功标志。不同版本写法略有差异,常见的有:
[KafkaServer id=0] started (kafka.server.KafkaServer)或者新版:
[KafkaServer id=0] Finished starting brokers and controllers看到这类信息,基本就可以确定Kafka启动成功了。但启动成功不意味着Kafka立刻就能对外服务,尤其是刚启动完那一两秒,Kafka还在初始化日志恢复等内部流程,如果你立刻跑生产消费命令,偶尔会报LeaderNotAvailable或BrokerNotAvailable,等几秒再试通常就正常了。
2.2 如果能看懂启动日志,排查会快很多
新手看到Kafka启动时刷出一大屏日志往往就慌了,其实只要关注几个关键点就行。第一是确认INFO级别日志里有没有ERROR或FATAL级别的异常堆栈。第二是确认最后那几行有没有出现started、Started、Finished这类词。第三是如果你改了端口,确认日志里实际监听端口和配置一致。
还有一个常见疑问:为什么启动Kafka时日志明明刷了一大片INFO却没有结束?因为Kafka本来就是常驻服务,正常启动后日志停在"started"附近,进程不会退出,这不是卡住了。判断是否卡住,最直接的方法是再开一个终端执行jps -l,能看到QuorumPeerMain(ZooKeeper)和Kafka两个进程,就说明都在运行。
jps -l # 输出示例 # 12345 kafka.Kafka # 12346 quorum.QuorumPeerMain有些发行版默认没装jps,那就用ps -ef | grep kafka也一样。
2.3 Windows教程的"后遗症":bat和sh的混用问题
经常在技术群里看到有人发这种求助:"我按照教程输入了kafka-server-start.bat,为什么提示找不到命令?"原因很简单,.bat是Windows批处理文件,Linux下根本没有这个可执行文件。Kafka发行包里带了bin/windows/目录,里面才是给Windows用的脚本。
在Linux下你应该找的是:
bin/kafka-server-start.sh bin/kafka-topics.sh bin/kafka-console-producer.sh bin/kafka-console-consumer.sh bin/kafka-consumer-groups.sh实际敲命令时,bin/kafka-server-start.sh后面必须跟一个配置文件参数,否则会报"Missing required argument"。这也是新手常踩的坑——拿着教程只复制了脚本名,忘了后面的config/server.properties。理解了这条启动链路,下面就可以直接上脚本了。
3. Kafka启动脚本:可直接复制,支持start、stop、status
把Kafka启动过程脚本化,最大的价值不只是省事,而是让它变得可控。手工启动时,ZooKeeper和Kafka分别占两个终端,关掉终端进程就没了,你没法方便地停止、重启、查状态。用脚本统一管理PID和日志,这些问题一次解决。
3.1 脚本完整代码
下面这个脚本我在多台机器上用过,兼容Kafka 2.x和3.x的ZooKeeper模式,你只需要改开头的KAFKA_HOME路径即可。
#!/usr/bin/env bash # Kafka启停脚本,适用于 Kafka 2.x / 3.x + ZooKeeper 模式 # 用法: ./kafka-control.sh {start|stop|status|restart} set -u KAFKA_HOME="${KAFKA_HOME:-/usr/local/kafka}" ZK_CONFIG="$KAFKA_HOME/config/zookeeper.properties" KAFKA_CONFIG="$KAFKA_HOME/config/server.properties" LOG_DIR="$KAFKA_HOME/logs" PID_DIR="${PID_DIR:-/tmp}" is_running() { local pid_file="$1" if [[ -f "$pid_file" ]]; then local pid pid=$(cat "$pid_file") if kill -0 "$pid" 2>/dev/null; then return 0 fi rm -f "$pid_file" fi return 1 } write_pid() { local pid_file="$1" local pid="$2" echo "$pid" > "$pid_file" } start_one() { local label="$1" local pid_file="$PID_DIR/${label}.pid" local log_file="$LOG_DIR/${label}.log" shift 1 if is_running "$pid_file"; then echo "[$label] 已在运行,pid=$(cat "$pid_file")" return 0 fi mkdir -p "$LOG_DIR" nohup "$@" > "$log_file" 2>&1 & write_pid "$pid_file" $! sleep 2 if is_running "$pid_file"; then echo "[$label] 启动成功,pid=$!,日志: $log_file" else echo "[$label] 启动失败,请查看日志: $log_file" exit 1 fi } stop_one() { local label="$1" local pid_file="$PID_DIR/${label}.pid" if is_running "$pid_file"; then local pid pid=$(cat "$pid_file") kill "$pid" echo "[$label] 停止信号已发送,pid=$pid" else echo "[$label] 未在运行" fi } case "${1:-}" in start) echo "=== 启动 ZooKeeper ===" start_one zookeeper "$KAFKA_HOME/bin/zookeeper-server-start.sh" "$ZK_CONFIG" echo "=== 等待 ZooKeeper 就绪 ===" sleep 5 echo "=== 启动 Kafka ===" start_one kafka "$KAFKA_HOME/bin/kafka-server-start.sh" "$KAFKA_CONFIG" ;; stop) echo "=== 停止 Kafka ===" stop_one kafka echo "=== 停止 ZooKeeper ===" stop_one zookeeper ;; status) if is_running "$PID_DIR/kafka.pid"; then echo "Kafka: 运行中 (pid=$(cat "$PID_DIR/kafka.pid"))" else echo "Kafka: 未运行" fi if is_running "$PID_DIR/zookeeper.pid"; then echo "ZooKeeper: 运行中 (pid=$(cat "$PID_DIR/zookeeper.pid"))" else echo "ZooKeeper: 未运行" fi ;; restart) "$0" stop sleep 3 "$0" start ;; *) echo "用法: $0 {start|stop|status|restart}" exit 1 ;; esac使用方法:
chmod +x kafka-control.sh ./kafka-control.sh start ./kafka-control.sh status ./kafka-control.sh restart ./kafka-control.sh stop3.2 逐段解读:脚本里每个变量和函数都是干什么的
很多人拿到脚本就直接用,但一旦出问题就抓瞎,所以我建议还是理解一下每个块的作用。
开头的set -u表示变量如果没定义就报错,而不是悄悄用空值,这是脚本安全的基本习惯。KAFKA_HOME、ZK_CONFIG、KAFKA_CONFIG、LOG_DIR、PID_DIR这几个变量是脚本的基础配置,改成适合自己环境的路径即可。我用${PID_DIR:-/tmp}这种写法,意思是如果外面没有设置PID_DIR环境变量,就默认用/tmp,这样你可以在不修改脚本的情况下改PID目录。
is_running函数是整个脚本的地基。它的逻辑是:读PID文件里面的进程号,然后用kill -0测试这个进程是否存在。kill -0不会真的发送信号,只是探测进程可不可以被信号访问,能访问就说明进程活着,不能访问就说明进程已经死了。如果进程死了,顺手把残留的PID文件删掉,避免下次误判。用这个函数,start时可以防止重复启动,stop时也可以判断是否需要发送停止信号。
start_one函数接收三个参数:服务名(zookeeper或kafka)、启动脚本路径、配置文件路径。先判断是否已在运行,在运行就提前返回;然后用nohup把进程放到后台,把标准输出和错误输出全部重定向到日志文件,同时把新进程的PID写入PID文件。这里的sleep 2不是为了炫技,是因为有些环境JVM启动需要一两秒,马上探测PID可能还没写入,会导致误判。nohup的必要性,在于它能让进程在你关掉终端、退出SSH会话后继续运行,否则你断开连接Kafka就跟着挂了。
stop_one函数就简单多了,先判断进程是否存活,存活就发送kill信号。这里有个细节要注意:kill默认发送的是TERM信号,这属于优雅退出信号。Kafka和ZooKeeper收到TERM后,会先做数据刷盘、清理资源等收尾工作,然后再退出。所以停服时看到进程没有立刻消失,不要急,那是在正常退出。如果确实卡住很久,再考虑用kill -9强制结束。
最后的case分支就是命令分发逻辑。start分支里我在启动Kafka之前先sleep 5,这个时间要重点说一下。Kafka启动时要去ZooKeeper注册broker数据和各种元信息,如果ZooKeeper还没完全就绪就去启动Kafka,会碰到连接拒绝或者元数据初始化异常,表现就是Kafka进程启动失败,或者启动日志里一堆Connection refused。如果你机器性能较差,5秒不够,可以适当调到10秒。
3.3 将这个脚本放进PATH并使用
脚本写好后,每次使用都敲./kafka-control.sh虽然不麻烦,但既然要长期用,我建议把它放到/usr/local/bin下,这样在任何目录都能直接用命令名调用:
sudo cp kafka-control.sh /usr/local/bin/kafka-control sudo chmod +x /usr/local/bin/kafka-control之后你就可以直接执行:
kafka-control start kafka-control status如果你是那种同时管理多套Kafka环境的运维,可以用环境变量KAFKA_HOME来切换不同安装目录,脚本里已经支持了,不需要维护多个副本:
KAFKA_HOME=/usr/local/kafka-3.6.0 kafka-control start4. 启动失败的完整排查链路
脚本不是万能的,如果启动失败,它只能告诉你"启动失败,请查看日志"。所以我把实际过程中最常见的几类失败场景整理成一条完整排查链路,按顺序走下去,大部分问题都能定位。
4.1 卡住不动的处理:检查ZooKeeper是否真正就绪
表现:执行kafka-control start之后,ZooKeeper提示启动成功,但Kafka部分迟迟不输出"启动成功",最后还提示失败。
首先确认ZooKeeper是不是真的能接受请求。看日志是最直接的:
tail -n 50 /usr/local/kafka/logs/zookeeper.log如果日志里有binding to port 0.0.0.0/0.0.0.0:2181,说明ZooKeeper至少已经绑定了端口。但绑定端口不代表完全就绪,你可以用netstat或ss再看一下:
ss -lntp | grep 2181如果确认ZooKeeper没问题,再看Kafka日志:
tail -n 100 /usr/local/kafka/logs/kafka.log比较常见的报错是:
org.apache.zookeeper.KeeperException$ConnectionLossException这通常说明Kafka启动时连不上ZooKeeper。检查server.properties里的zookeeper.connect是不是配错了地址或端口。如果你改过ZooKeeper端口,这里必须同步修改。还有一种容易被忽略的情况:server.properties里zookeeper.connect=localhost:2181,但Kafka进程因为某些原因解析不了localhost。这种在容器或特殊网络环境下遇到得多,最省事的办法是把localhost改成127.0.0.1,或者直接改成机器内网IP。
还有个新手必踩的坑:主机名解析。Kafka或ZooKeeper启动时有时会尝试根据主机名做DNS解析,如果机器没有配置合理的/etc/hosts,会出现启动卡住很久然后超时。建议看看/etc/hostname和/etc/hosts,确保本机主机名能够解析到本机IP。
4.2 端口被占用和权限不足:两个极易被忽视的元凶
端口被占用时的报错通常是:
java.net.BindException: Address already in use这时候不要急着猜是谁占用的,直接查:
ss -lntp | grep 9092如果发现有其他进程占了9092,要么停掉它,要么改Kafka的listeners端口。同理,ZooKeeper的2181被占也会出现类似问题。实际工作中我还遇到过一种情况:上一批Kafka进程没被完全杀掉,端口被残留进程占着,看着像"新启动失败",其实是老进程还活着。这时候清理僵尸进程比盲目重启更有用。
权限不足的情况,启动日志里通常会看到:
Exception in thread "main" java.io.IOException: No space left on device注意,No space left on device不只是磁盘满了才会出现,也可能是inode耗尽。用df -h看磁盘剩余,用df -i看inode剩余。Kafka如果一直写日志,磁盘满的概率还是不小的。另外日志目录和数据目录的写权限也要确认,我之前在根目录部署时,图省事没给log.dirs指向的目录授予正确权限,结果Kafka每次启动都在初始化时失败,报错却很隐晦,找了半天才发现是权限问题。
4.3 内存不足和JVM参数调优
默认情况下,Kafka的启动脚本会设置堆内存大小,通常在bin/kafka-server-start.sh里能看到:
KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"如果你机器本身内存不大,比如只有1G内存,启动时可能直接报:
Java HotSpot(TM) 64-Bit Server VM warning: INFO: os::commit_memory ...或者干脆Killed,进程被OOM Killer干掉了。这时可以修改kafka-server-start.sh,把堆内存调小。我一般这样改:
export KAFKA_HEAP_OPTS="-Xmx512M -Xms512M"注意,修改时要先看脚本里KAFKA_HEAP_OPTS是在哪里被设置的。如果在脚本里写死了,你export外部变量可能覆盖不了,需要实际编辑对应脚本。ZooKeeper的JVM参数也有类似限制,但通常默认值更保守,问题不大。
还有一个容易被忽略的内存坑:Kafka消息本身存储在操作系统的页缓存(Page Cache)里,而不是完全依赖JVM堆。所以如果你给JVM堆设了1G,额外还需要一些内存来承载页缓存压力。实测中,512M堆加上系统本身的开销,在2G内存的云主机上跑单机Kafka是可行的,但要注意别把系统内存全部交给JVM。
5. 启动成功后,用这几条命令检验Kafka真的能用
很多人启动Kafka后看到日志里有"started"就以为万事大吉,但"进程活着"和"功能可用"是两码事。我习惯启动后立刻用一条链路做验证:创建topic、起一个生产者、起一个消费者,把消息跑通。
5.1 验证Topic创建和基础消息收发
如果没有自动创建topic,可以先手动创建一个,建议先建一个1分区1副本的测试topic:
bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic test-topic --partitions 1 --replication-factor 1提示:如果用的是Kafka 2.x版本,也可以用
--zookeeper localhost:2181,但新版本已经推荐用--bootstrap-server localhost:9092。如果你看到"zookeeper is not a recognized option"之类的报错,说明你的Kafka版本已经移除了ZooKeeper相关参数。
创建成功后,再启动一个生产者:
bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test-topic这个命令启动后会进入交互模式,光标停在空白行等你输入内容。输入几行文本后,Ctrl+C退出。你可以看到,这个命令启动后会一直运行,不会自动退出,这个问题我下面会单独讲。
然后启动一个消费者,从最早的消息开始读:
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test-topic --from-beginning如果配置没问题,刚才在生产者那边输入的内容会一条条打出来。到这里,Kafka的生产消费链路就验证通过,可以放心使用。
5.2 查看topic数据和消费组lag
除了基础收发,日常使用中还有几个高频排查命令。查看topic列表和详情:
bin/kafka-topics.sh --bootstrap-server localhost:9092 --list bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic test-topic查看某个消费组的消费延迟(lag):
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group your-consumer-group --describe消费组和lag是Kafka面试和日常运维的高频概念。简单理解,lag就是生产者已经写入的消息数量减去消费者已经处理掉的消息数量,lag数值越高说明消费者落后越多。网上很多人问"Kafka能重复消费吗",这背后其实是offset提交机制的问题:消费者处理完一批消息后要提交offset,但如果消费者在提交前就挂掉了,重启后会根据上次提交的offset重新消费,就出现了重复消费。排查这类问题时,kafka-consumer-groups.sh --describe是必用的工具。
注意:很多教程里查询消费组会用
--zookeeper参数,但新版Kafka同样建议使用--bootstrap-server。如果你用的是比较老的Kafka版本,--zookeeper才能查询到offset,所以这个命令在不同版本之间行为差异较大,使用时先确认你的版本。
5.3 可选的图形化工具:一条Docker命令拉起UI
如果觉得命令行不方便,想用图形界面看Kafka里的数据,这里推荐一个很轻量的方案,用Docker跑一个Kafka UI服务:
docker run -d --name kafka-ui -p 8080:8080 \ -e KAFKA_CLUSTERS_0_NAME=local \ -e KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=localhost:9092 \ -e KAFKA_CLUSTERS_0_ZOOKEEPER=localhost:2181 \ provectuslabs/kafka-ui:latest然后浏览器访问http://服务器IP:8080,就能看到broker信息、topic列表、消息内容。这个工具对排查问题帮助很大,尤其是当你想快速确认一条消息有没有进入某个topic的时候,不用在命令行里反复切命令。前提是Kafka的listeners地址能被这个UI容器访问到,如果网络不通可能要调整KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS的地址。
最后再说一个我在实际操作中的体会:Kafka启动这个事,一旦你用脚本把它管起来,后面就再也不想裸敲命令了。因为裸敲命令的时候,你永远不知道哪个进程还在背后运行,哪个PID已经失效,而脚本把start/stop/status/restart都收敛成一条命令,状态一目了然。建议你把这篇里的脚本存好,换机器部署Kafka时直接拿过来改掉路径就能用,能省下不少重复折腾的时间。