ZAB协议是ZooKeeper内部用于保证集群数据一致性的核心机制,它把一次写请求转换为事务提案,通过原子广播让所有跟随者节点按相同顺序提交。要在本地观察这一过程,物理部署三台机器成本较高,而Docker容器可以在一台主机上快速创建多个相互隔离的ZooKeeper节点。本文将基于Docker搭建三节点ZooKeeper集群,并结合选举日志、四字命令和故障注入,解析ZAB协议在容器环境中的实际表现。

一、ZAB协议的角色划分与Docker部署前提
ZAB协议的全称是ZooKeeper Atomic Broadcast,它解决的是分布式系统中多个副本之间如何保持一致的问题。ZooKeeper集群通常由一个Leader和多个Follower组成,写请求必须发送给Leader,Leader生成事务提案并分配全局递增的zxid,然后通过两阶段提交的变体向Follower广播。Follower收到提案后写入本地事务日志,并返回确认,Leader在收到超过半数节点的确认后提交该事务,这就是ZAB协议的基本流程。Observer可以参与读请求,但不参与投票,适合横向扩展读能力。
用Docker模拟这套环境时,需要保证每个容器有独立的主机名和网络标识,因为ZooKeeper节点之间通过2888端口做Leader与Follower通信,3888端口用于选举,2181端口面向客户端。容器默认的bridge网络下虽然可以通过IP互通,但重启后IP可能变化,因此最好创建一个自定义网络,让容器通过服务名互相解析。同时每个节点的dataDir下必须有一个myid文件,内容是一个唯一的数字,与配置中的server.N对应,否则节点无法加入集群。
除了网络和myid,ZAB协议还依赖事务日志和快照目录。Docker容器天生的无状态特性意味着如果直接使用容器可写层,容器删除后数据全部丢失,这对观察崩溃恢复和重新加入集群非常不利。因此建议在创建容器时挂载两个卷,一个用于事务日志,一个用于数据快照。这样即使某个节点被停止或删除,重新启动后仍能根据磁盘上的数据恢复状态,便于模拟真实的故障场景。
二、用Docker Compose编排三节点集群
下面给出一份简化的docker-compose.yml,它定义了三个ZooKeeper节点,分别命名为zk1、zk2和zk3,并使用同一个自定义网络。每个容器都挂载本地目录,确保myid文件和数据文件能够持久化。
version: "3.8"
services:
zk1:
image: zookeeper:3.8
hostname: zk1
container_name: zk1
networks:
- zk-net
ports:
- "2181:2181"
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=zk1:2888:3888;2181 server.2=zk2:2888:3888;2181 server.3=zk3:2888:3888;2181
volumes:
- ./zk1/data:/data
- ./zk1/datalog:/datalog
zk2:
image: zookeeper:3.8
hostname: zk2
container_name: zk2
networks:
- zk-net
ports:
- "2182:2181"
environment:
ZOO_MY_ID: 2
ZOO_SERVERS: server.1=zk1:2888:3888;2181 server.2=zk2:2888:3888;2181 server.3=zk3:2888:3888;2181
volumes:
- ./zk2/data:/data
- ./zk2/datalog:/datalog
zk3:
image: zookeeper:3.8
hostname: zk3
container_name: zk3
networks:
- zk-net
ports:
- "2183:2181"
environment:
ZOO_MY_ID: 3
ZOO_SERVERS: server.1=zk1:2888:3888;2181 server.2=zk2:2888:3888;2181 server.3=zk3:2888:3888;2181
volumes:
- ./zk3/data:/data
- ./zk3/datalog:/datalog
networks:
zk-net:
driver: bridge
这份配置的核心是环境变量 ZOO_MY_ID 和 ZOO_SERVERS。前者会由官方镜像的启动脚本写入dataDir下的myid文件,后者会被转换成zoo.cfg中的server列表。server项的格式为server.id=host:port1:port2,其中port1用于Follower连接Leader,port2用于选举通信。三个节点的server列表必须完全一致,否则集群无法形成。
如果不想使用Compose,也可以用docker run命令逐个启动,但要手动创建网络和卷,并设置相同的ZOO_SERVERS。使用Compose的好处在于服务名可以直接被解析,例如zk1会被解析为对应容器的IP,这正好满足ZAB协议对节点地址稳定的要求。需要注意的是,官方ZooKeeper镜像会根据ZOO_SERVERS自动配置,但如果使用自定义zoo.cfg,则需要手动挂载配置文件,并保证dataDir与dataLogDir分别指向不同目录。
完成配置后执行docker compose up -d,三个容器会同时启动。初始阶段没有Leader,所有节点都进入LOOKING状态,并通过3888端口互相发送选举消息。ZAB协议的选举算法会优先比较zxid,zxid较大的节点持有更新的数据,更有可能成为Leader;如果zxid相同则比较myid,myid较大的节点胜出。因此在这个三节点环境中,通常zk3会最先成为Leader,因为它的myid是3,但这并非绝对,真实结果还受启动时间和日志状态影响。
三、观察选举、事务广播与故障恢复
启动完成后,可以先通过四字命令检查每个节点的角色。四字命令是ZooKeeper内置的监控接口,默认在2181端口上监听。echo stat | nc localhost 2181 可以查看节点状态,其中Mode字段会显示leader或follower。针对映射到主机的端口,分别检查2181、2182和2183。也可以直接进入容器执行 zkServer.sh status。如果配置正确,可以看到一个leader和两个follower。
# 查看zk1角色 echo stat | nc 127.0.0.1 2181 # 查看zk2角色 echo stat | nc 127.0.0.1 2182 # 查看zk3角色 echo stat | nc 127.0.0.1 2183
另一种观察方式是查看容器日志。在docker compose启动后的前几秒,日志中会打印出大量LOOKING、FOLLOWING和LEADING状态信息。如果某个节点成为Leader,日志中会出现LEADING字样;Follower则会打印FOLLOWING并显示正在连接Leader。这些日志直接反映了ZAB协议选举阶段的交互过程。使用 docker logs zk1 或 docker logs zk2 即可看到。
接下来验证事务广播。写请求只能由Leader处理,但客户端可以连接任意节点。当连接到一个Follower时,该Follower会将写请求转发给Leader,Leader生成事务并向所有Follower广播。可以打开一个客户端创建持久节点:
docker exec -it zk1 zkCli.sh -server zk2:2181 create /docker-zab "hello" ls / get /docker-zab
执行成功后切换到zk3查看是否能读取到该节点。由于ZAB协议保证只要超过半数节点提交,读操作就能看到最新数据,因此连接到任意节点都应该返回相同内容。如果尝试连接到一个暂时不可用的节点,客户端会触发重新连接,这也是ZooKeeper会话机制的一部分。
要模拟Leader故障,可以先确认当前Leader是哪个容器,然后使用docker stop将其停止。此时剩余的Follower会检测到Leader失去心跳,重新进入LOOKING状态并发起新一轮选举。由于剩余节点的zxid已经包含刚才创建的事务,选举会很快完成,新的Leader被选出。使用docker start重启旧节点后,它会以Follower身份重新加入集群,并从新Leader同步缺失的事务。观察日志中的NEWLEADER和SYNC阶段可以更深入理解崩溃恢复过程。
四、容器网络与持久化对ZAB稳定性的影响
ZAB协议在选举和广播时大量依赖节点之间的TCP长连接。Docker默认的bridge网络提供NAT和端口映射,但如果容器之间通过宿主机端口互访,可能会出现地址混乱。比如zk1配置的server地址是zk1:2888:3888,但外部访问必须经过宿主机映射端口,这与集群内部通信路径不一致,容易导致选举失败。因此应该让所有ZooKeeper容器加入同一个自定义网络,并且不要为2888和3888做不必要的端口映射,除非确实需要从宿主机直连。官方镜像在自定义网络下能够正确使用服务名进行内部通信。
另一个常见问题是容器重启后IP变化。如果使用默认bridge网络,容器停止再启动可能获得不同IP,而myid对应的server列表不会自动更新,节点会找不到其他成员。自定义网络可以为容器固定IP,或者配合hostname和服务名解析避免IP变化。更稳妥的做法是使用固定的网络别名,并保持容器名和hostname一致。
持久化同样关键。ZooKeeper将已提交的事务追加到事务日志,并定期生成快照。如果容器数据卷没有挂载,节点重启后会丢失所有状态,甚至可能因为myid文件丢失而无法启动。官方镜像默认将数据放在/data,事务日志放在/datalog,但只有挂载到宿主机目录才能持久化。在测试崩溃恢复时,建议为每个节点单独挂载数据目录,不要共享同一个卷,否则会导致myid冲突和文件锁问题。
最后还需要关注心跳和超时参数。ZAB协议通过tickTime定义基本时间单位,initLimit和syncLimit分别控制Follower连接Leader时的初始同步时间和心跳超时时间。在Docker环境中,尤其是资源受限的笔记本上,容器可能因为CPU调度延迟而无法及时响应心跳,导致误判节点失效。此时可以适当增大tickTime或syncLimit,但会降低故障检测的实时性,需要根据实际环境权衡。
DockerZAB协议ZooKeeper集群修改时间:2026-10-04 19:38:00