环境:Rocky Linux 9.4 / ZooKeeper 3.8.6 / Kafka 2.13-3.9.2
目标:单机伪集群(3个ZK节点 + 3个Kafka Broker),用于学习分布式消息队列原理
一、环境说明
服务器信息
| 项目 | 信息 |
|---|---|
| 操作系统 | Rocky Linux 9.4 (Blue Onyx) |
| 内网 IP | 172.17.0.7 |
| 公网 IP | 124.221.242.29 |
| ZooKeeper 版本 | 3.8.6 |
| Kafka 版本 | kafka_2.13-3.9.2 |
初始目录结构
登录服务器后 /opt 目录下已有以下文件:
/opt/
├── apache-zookeeper-3.8.6-bin.tar.gz ← ZooKeeper 安装包
├── kafka_2.13-3.9.2.tgz ← Kafka 安装包
├── zookeeper/ ← 已解压的 ZooKeeper
└── kafka/ ← 已解压的 Kafka
二、为什么要搭集群?核心原理
ZooKeeper 集群原理
ZooKeeper 的核心职责是做分布式协调(比如帮 Kafka 选举 Leader、存储元数据)。如果 ZooKeeper 自己是单点,一旦挂掉整个依赖它的系统就全崩了,所以 ZooKeeper 自身也要高可用。
解决方案就是多个 ZK 节点互相备份,称为 ZooKeeper 集群(Ensemble):
┌─────────────────────────────────────┐
│ ZooKeeper 集群
│
│ [ZK1-Leader] ←→ [ZK2] ←→ [ZK3]
│ 数据同步 数据同步
└─────────────────────────────────────┘
↑ 任意一个挂掉,集群照常运行
关键规则:超过半数节点存活才能正常工作。
- 3个节点:挂1个还剩2个 > 1.5
- 2个节点:挂1个只剩1个 = 1.0
这也是为什么集群节点数必须是奇数,最少3个。
伪集群 vs 真实集群
由于只有一台服务器,无法搭建真正的多机集群,采用伪集群方式:
真实多机集群(3台服务器): 伪集群(1台服务器):
服务器A → ZK进程 /opt/zookeeper/zk1/ → ZK进程1(端口2181)
服务器B → ZK进程 /opt/zookeeper/zk2/ → ZK进程2(端口2182)
服务器C → ZK进程 /opt/zookeeper/zk3/ → ZK进程3(端口2183)
每台服务器天然隔离 用文件夹 + 不同端口来隔离
伪集群的本质:用不同目录 + 不同端口,在同一台机器上模拟多台服务器。
三、搭建 ZooKeeper 伪集群
3.1 目录结构规划
/opt/zookeeper/
├── bin/ ← ZK 启动脚本(3个节点共用同一套程序)
├── lib/ ← ZK 的 jar 包
├── conf/zoo.cfg ← 原始默认配置(单机用,集群不用这个)
│
├── zk1/ ← 模拟节点1(相当于第1台服务器)
│ ├── zoo.cfg ← 节点1的配置(clientPort=2181)
│ ├── data/
│ │ └── myid ← 内容: 1
│ └── logs/
│
├── zk2/ ← 模拟节点2
│ ├── zoo.cfg ← 节点2的配置(clientPort=2182)
│ ├── data/
│ │ └── myid ← 内容: 2
│ └── logs/
│
└── zk3/ ← 模拟节点3
├── zoo.cfg ← 节点3的配置(clientPort=2183)
├── data/
│ └── myid ← 内容: 3
└── logs/
3.2 创建目录并写入 myid
# 创建3个节点各自的数据目录和日志目录
mkdir -p /opt/zookeeper/zk1/data /opt/zookeeper/zk1/logs
mkdir -p /opt/zookeeper/zk2/data /opt/zookeeper/zk2/logs
mkdir -p /opt/zookeeper/zk3/data /opt/zookeeper/zk3/logs
# 写入 myid(每个节点的唯一标识)
echo 1 > /opt/zookeeper/zk1/data/myid
echo 2 > /opt/zookeeper/zk2/data/myid
echo 3 > /opt/zookeeper/zk3/data/myid
关于 myid 的说明:
-
文件名
myid是固定的 ,ZooKeeper 启动时会自动去dataDir目录下找这个文件 -
里面的数字是自定义的 ,但必须是正整数,且和配置文件里的
server.X的 X 对应 -
集群内不能重复
myid 文件内容 对应配置文件中的
1 ←→ server.1=172.17.0.7:2888:3888
2 ←→ server.2=172.17.0.7:2889:3889
3 ←→ server.3=172.17.0.7:2890:3890
3.3 创建配置文件
将原始配置复制到三个节点目录,再分别修改:
cp /opt/zookeeper/conf/zoo.cfg /opt/zookeeper/zk1/zoo.cfg
cp /opt/zookeeper/conf/zoo.cfg /opt/zookeeper/zk2/zoo.cfg
cp /opt/zookeeper/conf/zoo.cfg /opt/zookeeper/zk3/zoo.cfg
zk1/zoo.cfg:
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/opt/zookeeper/zk1/data # 节点1数据目录
dataLogDir=/opt/zookeeper/zk1/logs # 节点1日志目录
clientPort=2181 # 客户端连接端口(各节点不同)
# 集群成员列表(三个节点都要写,且完全一样)
server.1=172.17.0.7:2888:3888
server.2=172.17.0.7:2889:3889
server.3=172.17.0.7:2890:3890
zk2/zoo.cfg:(只有3处不同)
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/opt/zookeeper/zk2/data # ← 改成 zk2
dataLogDir=/opt/zookeeper/zk2/logs # ← 改成 zk2
clientPort=2182 # ← 改成 2182
server.1=172.17.0.7:2888:3888
server.2=172.17.0.7:2889:3889
server.3=172.17.0.7:2890:3890
zk3/zoo.cfg:(同理)
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/opt/zookeeper/zk3/data # ← 改成 zk3
dataLogDir=/opt/zookeeper/zk3/logs # ← 改成 zk3
clientPort=2183 # ← 改成 2183
server.1=172.17.0.7:2888:3888
server.2=172.17.0.7:2889:3889
server.3=172.17.0.7:2890:3890
三个节点配置文件差异对比:
| 配置项 | zk1 | zk2 | zk3 |
|---|---|---|---|
| dataDir | zk1/data | zk2/data | zk3/data |
| dataLogDir | zk1/logs | zk2/logs | zk3/logs |
| clientPort | 2181 | 2182 | 2183 |
| server.x | 完全相同 | 完全相同 | 完全相同 |
3.4 端口说明
配置文件中涉及3种端口,含义完全不同:
clientPort=2181 # 客户端连接端口(Kafka 连 ZK 用这个)
server.1=172.17.0.7:2888:3888
# ↑ ↑
# 节点间数据同步 Leader 选举投票
| 端口类型 | 端口号 | 用途 | 谁来连 |
|---|---|---|---|
| clientPort | 2181/2182/2183 | 客户端通信 | Kafka、客户端程序 |
| 数据同步端口 | 2888/2889/2890 | Follower 同步数据给 Leader | ZK 节点之间 |
| 选举端口 | 3888/3889/3890 | Leader 选举投票 | ZK 节点之间 |
3.5 启动 ZooKeeper 集群
zkServer.sh start /opt/zookeeper/zk1/zoo.cfg
zkServer.sh start /opt/zookeeper/zk2/zoo.cfg
zkServer.sh start /opt/zookeeper/zk3/zoo.cfg
注意: 启动时必须指定配置文件路径!
zkServer.sh start不指定路径的话,3个节点会都读默认的conf/zoo.cfg,集群就乱了。
zkServer.sh 常用命令:
| 命令 | 作用 |
|---|---|
start [cfg] |
后台启动 |
stop [cfg] |
停止 |
restart [cfg] |
重启 |
status [cfg] |
查看节点角色(leader/follower) |
start-foreground [cfg] |
前台启动,报错时用于调试 |
3.6 验证 ZooKeeper 集群状态
zkServer.sh status /opt/zookeeper/zk1/zoo.cfg
zkServer.sh status /opt/zookeeper/zk2/zoo.cfg
zkServer.sh status /opt/zookeeper/zk3/zoo.cfg
正常输出(1个 leader + 2个 follower):
Mode: leader
Mode: follower
Mode: follower
3.7 ZooKeeper 启动过程原理
启动 zk1 进程
└─ 读取 zk1/zoo.cfg
└─ 读取 zk1/data/myid → 得知"我是节点1"
└─ 监听 clientPort=2181
└─ 向 server.2、server.3 发起连接,等待集群组建
启动 zk2 进程
└─ 同上,得知"我是节点2"
└─ 监听 clientPort=2182
└─ 和节点1握手成功
启动 zk3 进程
└─ 同上,得知"我是节点3"
└─ 三个节点互相连通 → 开始 Leader 选举
└─ 选出一个 Leader,其余成为 Follower → 集群就绪!
四、搭建 Kafka 伪集群
4.1 目录结构规划
/opt/kafka/
├── bin/ ← Kafka 启动脚本
├── config/
│ └── server.properties ← 原始默认配置(单机用)
│
├── broker1/
│ ├── server.properties ← broker1 配置(port=9092, id=1)
│ └── logs/ ← broker1 数据目录
│
├── broker2/
│ ├── server.properties ← broker2 配置(port=9093, id=2)
│ └── logs/ ← broker2 数据目录
│
└── broker3/
├── server.properties ← broker3 配置(port=9094, id=3)
└── logs/ ← broker3 数据目录
4.2 创建目录并复制配置
# 创建数据目录
mkdir -p /opt/kafka/broker1/logs
mkdir -p /opt/kafka/broker2/logs
mkdir -p /opt/kafka/broker3/logs
# 从默认配置复制
cp /opt/kafka/config/server.properties /opt/kafka/broker1/server.properties
cp /opt/kafka/config/server.properties /opt/kafka/broker2/server.properties
cp /opt/kafka/config/server.properties /opt/kafka/broker3/server.properties
4.3 修改配置文件
broker1/server.properties:
需要修改的项(原文件里有,找到改掉):
broker.id=1
listeners=PLAINTEXT://172.17.0.7:9092
advertised.listeners=PLAINTEXT://172.17.0.7:9092
log.dirs=/opt/kafka/broker1/logs
zookeeper.connect=172.17.0.7:2181,172.17.0.7:2182,172.17.0.7:2183
需要在文件末尾追加的项(原文件没有):
num.partitions=3
default.replication.factor=3
min.insync.replicas=2
offsets.topic.replication.factor=3
broker2/server.properties:(只改 id 和端口)
broker.id=2 # ← 改成 2
listeners=PLAINTEXT://172.17.0.7:9093 # ← 改成 9093
advertised.listeners=PLAINTEXT://172.17.0.7:9093 # ← 改成 9093
log.dirs=/opt/kafka/broker2/logs # ← 改成 broker2
zookeeper.connect=172.17.0.7:2181,172.17.0.7:2182,172.17.0.7:2183
# 末尾追加(同 broker1)
num.partitions=3
default.replication.factor=3
min.insync.replicas=2
offsets.topic.replication.factor=3
broker3/server.properties:
broker.id=3 # ← 改成 3
listeners=PLAINTEXT://172.17.0.7:9094 # ← 改成 9094
advertised.listeners=PLAINTEXT://172.17.0.7:9094 # ← 改成 9094
log.dirs=/opt/kafka/broker3/logs # ← 改成 broker3
zookeeper.connect=172.17.0.7:2181,172.17.0.7:2182,172.17.0.7:2183
# 末尾追加(同 broker1)
num.partitions=3
default.replication.factor=3
min.insync.replicas=2
offsets.topic.replication.factor=3
新增配置项说明:
| 配置项 | 含义 | 为什么设3 |
|---|---|---|
num.partitions |
新建 Topic 默认分区数 | 对应3个broker,负载均衡 |
default.replication.factor |
默认副本数 | 3个broker各存一份,任意挂一个不丢数据 |
min.insync.replicas |
最少同步副本数 | 至少2个副本写成功才算消息可靠 |
offsets.topic.replication.factor |
消费位移 Topic 的副本数 | 同上,保证可靠性 |
三个 broker 配置差异对比:
| 配置项 | broker1 | broker2 | broker3 |
|---|---|---|---|
| broker.id | 1 | 2 | 3 |
| listeners 端口 | 9092 | 9093 | 9094 |
| log.dirs | broker1/logs | broker2/logs | broker3/logs |
| zookeeper.connect | 完全相同 | 完全相同 | 完全相同 |
4.4 启动 Kafka 集群
kafka-server-start.sh -daemon /opt/kafka/broker1/server.properties
sleep 2
kafka-server-start.sh -daemon /opt/kafka/broker2/server.properties
sleep 2
kafka-server-start.sh -daemon /opt/kafka/broker3/server.properties
sleep 3
# 验证进程
jps
正常输出(3个 ZK + 3个 Kafka):
xxxxxx QuorumPeerMain ← ZooKeeper节点1
xxxxxx QuorumPeerMain ← ZooKeeper节点2
xxxxxx QuorumPeerMain ← ZooKeeper节点3
xxxxxx Kafka ← Kafka broker1
xxxxxx Kafka ← Kafka broker2
xxxxxx Kafka ← Kafka broker3
五、遇到的问题与解决过程
问题1:ZooKeeper 已有解压目录,不需要重新解压
现象: 服务器 /opt 下同时存在 .tar.gz 压缩包和已解压的目录。
原因: 之前已经手动解压过一次,目录名直接叫 zookeeper 和 kafka。
解决: 直接使用已解压的目录即可,不需要重复解压。
问题2:原始 conf/zoo.cfg 要不要处理
疑问: /opt/zookeeper/conf/zoo.cfg 是单机默认配置,搭集群要不要删掉或修改它?
结论: 不用管它,因为启动集群时明确指定了配置文件路径:
zkServer.sh start /opt/zookeeper/zk1/zoo.cfg # 指定 zk1 的配置
ZooKeeper 只会读指定的配置文件,不会去读默认的 conf/zoo.cfg,两者互不干扰。
问题3:Kafka 只启动了1个 broker,另外2个没启动成功
现象: 执行三条启动命令后,jps 只看到1个 Kafka 进程:
2563839 Kafka ← 只有1个,应该是3个
排查过程:
# 查看 broker2 和 broker3 的日志目录,发现是空的(连 server.log 都没有)
ls /opt/kafka/broker2/logs # 空
ls /opt/kafka/broker3/logs # 空
日志目录是空的,说明进程启动瞬间就崩溃了,没来得及写日志。
用前台模式启动 broker2 查看报错:
kafka-server-start.sh /opt/kafka/broker2/server.properties
# 不加 -daemon,错误日志直接打印在终端
报错信息:
Stored node id 1 doesn't match previous node id 2
根本原因:
- broker1 第一次成功启动后,Kafka 把
broker.id=1写入了broker1/logs/meta.properties - 之后启动 broker2 时,
log.dirs配置错误,指向了已经被 broker1 用过的目录 - Kafka 发现目录里记录的 id=1,但配置文件写的 id=2,拒绝启动
实质上是:三个 broker 的 log.dirs 配置重复指向了同一个目录。
解决方法:
# 清空所有broker的日志目录(删除残留的 meta.properties)
rm -rf /opt/kafka/broker1/logs/*
rm -rf /opt/kafka/broker2/logs/*
rm -rf /opt/kafka/broker3/logs/*
# 停掉当前运行的 Kafka
kafka-server-stop.sh
sleep 3
# 确认配置文件中 log.dirs 各不相同后,重新启动
kafka-server-start.sh -daemon /opt/kafka/broker1/server.properties
sleep 2
kafka-server-start.sh -daemon /opt/kafka/broker2/server.properties
sleep 2
kafka-server-start.sh -daemon /opt/kafka/broker3/server.properties
经验总结:
只要看到报错
Stored node id X doesn't match previous node id Y, 说明数据目录里残留了旧的 broker.id 记录,与当前配置冲突。 解决方法:清空对应的 logs 目录,重新启动。
meta.properties 文件内容示例:
# 由 Kafka 自动生成,不要手动修改
broker.id=1
version=0
六、集群验证
6.1 查看 ZooKeeper 中注册的 broker
# 连接 ZooKeeper CLI
zkCli.sh -server 172.17.0.7:2181
# 查看已注册的 broker id 列表
ls /brokers/ids
# 正常输出:[1, 2, 3]
# 查看某个 broker 的详细信息
get /brokers/ids/1
6.2 创建测试 Topic
kafka-topics.sh \
--create \
--bootstrap-server 172.17.0.7:9092 \
--topic test-topic \
--partitions 3 \
--replication-factor 3
# 查看 Topic 详情(重点关注 Leader 分布和 ISR 列表)
kafka-topics.sh \
--describe \
--bootstrap-server 172.17.0.7:9092 \
--topic test-topic
正常输出示例:
Topic: test-topic Partition: 0 Leader: 2 Replicas: 2,1,3 Isr: 2,1,3
Topic: test-topic Partition: 1 Leader: 3 Replicas: 3,2,1 Isr: 3,2,1
Topic: test-topic Partition: 2 Leader: 1 Replicas: 1,3,2 Isr: 1,3,2
每个分区都有独立的 Leader,且 ISR 列表包含全部3个 broker,说明集群完全正常。
6.3 生产者消费者测试
# 终端1:启动生产者
kafka-console-producer.sh \
--bootstrap-server 172.17.0.7:9092 \
--topic test-topic
# 终端2:启动消费者
kafka-console-consumer.sh \
--bootstrap-server 172.17.0.7:9092 \
--topic test-topic \
--from-beginning
七、快速启停脚本
启动脚本 /opt/cluster-start.sh
#!/bin/bash
ZK=/opt/zookeeper/bin/zkServer.sh
KF=/opt/kafka/bin/kafka-server-start.sh
echo "=== 启动 ZooKeeper 集群 ==="
zkServer.sh start /opt/zookeeper/zk1/zoo.cfg
zkServer.sh start /opt/zookeeper/zk2/zoo.cfg
zkServer.sh start /opt/zookeeper/zk3/zoo.cfg
sleep 5
echo "=== 启动 Kafka 集群 ==="
kafka-server-start.sh -daemon /opt/kafka/broker1/server.properties
sleep 2
kafka-server-start.sh -daemon /opt/kafka/broker2/server.properties
sleep 2
kafka-server-start.sh -daemon /opt/kafka/broker3/server.properties
sleep 3
echo "=== 集群状态 ==="
jps
停止脚本 /opt/cluster-stop.sh
#!/bin/bash
echo "=== 停止 Kafka 集群 ==="
kafka-server-stop.sh
sleep 3
echo "=== 停止 ZooKeeper 集群 ==="
zkServer.sh stop /opt/zookeeper/zk1/zoo.cfg
zkServer.sh stop /opt/zookeeper/zk2/zoo.cfg
zkServer.sh stop /opt/zookeeper/zk3/zoo.cfg
echo "=== 当前进程 ==="
jps
# 赋予执行权限
chmod +x /opt/cluster-start.sh /opt/cluster-stop.sh
八、常用运维命令速查
ZooKeeper 相关
# 启动/停止/重启/查看状态
zkServer.sh start|stop|restart|status /opt/zookeeper/zkX/zoo.cfg
# 连接 ZK CLI
zkCli.sh -server 172.17.0.7:2181
# ZK CLI 常用操作
ls / # 列出根节点
ls /brokers/ids # 查看 Kafka broker 列表
get /brokers/ids/1 # 查看 broker 详情
get /controller # 查看当前 Kafka Controller
ls /brokers/topics # 查看所有 topic
Kafka 相关
# 启动(后台)/ 停止
kafka-server-start.sh -daemon <配置文件>
kafka-server-stop.sh
# Topic 管理
kafka-topics.sh --create --bootstrap-server 172.17.0.7:9092 --topic <名称> --partitions 3 --replication-factor 3
kafka-topics.sh --list --bootstrap-server 172.17.0.7:9092
kafka-topics.sh --describe --bootstrap-server 172.17.0.7:9092 --topic <名称>
kafka-topics.sh --delete --bootstrap-server 172.17.0.7:9092 --topic <名称>
# 生产者 / 消费者
kafka-console-producer.sh --bootstrap-server 172.17.0.7:9092 --topic <名称>
kafka-console-consumer.sh --bootstrap-server 172.17.0.7:9092 --topic <名称> --from-beginning
# 查看消费者组
kafka-consumer-groups.sh --bootstrap-server 172.17.0.7:9092 --list
kafka-consumer-groups.sh --bootstrap-server 172.17.0.7:9092 --describe --group <组名>
九、下一步学习建议
阶段一:理解核心概念
- Partition(分区):Topic 的并行处理基础,同一分区内消息有序
- Replica(副本):Leader + Follower 副本机制,保证高可用
- ISR(In-Sync Replicas):正在同步的副本集合,ISR 缩减说明有副本落后
- Offset(位移):消费者记录自己消费到哪条消息的标记
阶段二:实验 - 模拟故障转移
# 1. 查看当前 Controller 是哪个 broker
zkCli.sh -server 172.17.0.7:2181 get /controller
# 2. 杀掉这个 broker
kill <对应的Kafka进程PID>
# 3. 立刻再查,观察新 Controller 产生
zkCli.sh -server 172.17.0.7:2181 get /controller
# 4. 查看 Topic 的 Leader 是否重新分配
kafka-topics.sh --describe --bootstrap-server 172.17.0.7:9092 --topic test-topic
阶段三:理解 ZooKeeper 在 Kafka 中的角色
ZooKeeper 存储了 Kafka 的所有关键元数据:
/brokers/ids/ ← broker 注册信息
/brokers/topics/ ← topic 分区信息
/controller ← 当前 Controller broker
/consumers/ ← 消费者组信息(旧版)
/config/topics/ ← topic 配置
/isr_change_notification/ ← ISR 变更通知
最后更新:2026年5月