前言
zookeeper 相信大家都不陌生,很多分布式中间件都利用 zk 来提供分布式一致性协调的特性。
dubbo 官方推荐使用 zk 作为注册中心,zk 也是 hadoop 和 Hbase 的重要组件。
其他知名的开源中间件中也都出现了 zk 的身影。
有很多童鞋认识 zk 很久了,知道其基本理念,知道如何使用。
但当面试时问到集群 zk 之间的选举和数据同步机制时,就陷入了盲区。
其实很多的分布式中间件的选举和同步,都和 zk 有异曲同工之妙。
这篇文章我就来重点聊下关于 zk 集群之间的选举和同步机制。
点击下方卡片,关注「码哥字节」
ZK 集群的部署
首先我们来看下半数运行机制:
集群至少需要三台服务器,且官方文档强烈建议使用奇数个服务器,因为 zookeeper 是通过判断大多数节点的的存活来判断整个服务集群是否可用的。
比如 3 个节点,它的一半是 1.5,向上取整就是 2。
挂掉了 2 个就是表示整个集群挂掉。
而用偶数 4 个的话,挂掉 2 个也表示不是大部分存活,因此也会挂掉。
所以用 4 台服务器的话 ,从使用资源上来说,并不划算。
配置语法:
代码语言:javascript复制server.<节点ID>=<IP>:<数据同步端口>:<选举端口>
- 节点 ID:为 1 到 125 之间的数字,写到对应服务节点的{dataDir}/myid 文件中。
- IP 地址:节点的远程 IP 地址,可以相同,生产环境建议用不同的机器,否则无法达到容错的目的。
- 数据同步端口:主从同步时数据复制端口。
- 选举端口:主从节点选举端口。
假设现在有 3 个 zookeeper 节点,我们要为其编写 config 配置,也要编写 3 份,分别放在不同的服务器上,其配置如下:
代码语言:javascript复制initLimit=10
syncLimit=5
dataDir=/data/zk_data
clientPort=2181
# 集群配置
server.1=192.168.1.1:2888:3888
server.2=192.168.1.2:2888:3888
server.3=192.168.1.3:2888:3888
其中 dataDir 参数指定的是一个目录,用来存放 zk 的数据,里面有个文件 myid,3 台机器上 myid 文件里面分别存放 1,2,3,对应各自节点 ID。
配置好 3 个配置文件后,分别启动,这样我们一个 3 个节点的集群 zookeeper 就搭建好了。
代码语言:javascript复制./bin/zkServer.sh start conf/zoo.cfg
ZK 集群中的角色
zookeeper 集群中公共有三种角色,分别是leader
,follower
,observer
。
角色 | 描述 |
---|---|
leader | 主节点,又名领导者。用于写入数据,通过选举产生,如果宕机将会选举新的主节点。 |
follower | 子节点,又名追随者,用于实现数据的读取,同时他也是主节点的备选节点,并拥有投票权。 |
observer | 次级子节点,又名观察者。 |
用于读取数据,与 follower 区别在于没有投票权,不能被选为主节点。
并且在计算集群可用状态时不会将 observer 计算入内。|
关于 observer 的配置:
只要在集群配置中加上 observer 后缀即可,示例如下:
代码语言:javascript复制server.3=127.0.0.1:2883:3883:observer
其中 leader 只有一个,剩下的都是 follower 和 observer,但是我们一般生产上不会配置 observer,因为 observer 并没有选举权,可以理解为 observer 是一个临时工,不是正式员工,没法获得晋升。
除此之外,它和 follower 的功能是一样的。
什么时候需要用到 observer 呢,因为 zk 一般读的请求会大于写。当整个集群压力过大时,我们可以增加几个临时工 observer 来获得性能的提升。
在不需要的时候的时候,可以随时撤掉 observer。
zk 进行连接时,一般我们都会把 zk 所有的节点都配置上去,用逗号分隔。
其实连接集群中的任意一个或者多个都是可以的。
只是如果只连一个节点,当这个节点宕机的时候,我们就断开了连接。
所以还是推荐配置多个节点进行连接。
如何查看 ZK 集群中的角色
我们可以利用以下命令来查看 zk 集群中的角色
代码语言:javascript复制./bin/zkServer.sh status conf/zoo.cfg
我在自己机器上搭建了 3 个节点的伪集群(共用一台机器),配置文件分别命名为 zoo1.cfg,zoo2.cfg,zoo3.cfg。使用以上命令查看的结果为:
可以看到,其中节点 2 为 leader,其他的为 follower。但是如果你按照 zoo1.cfg,zoo2.cfg,zoo3.cfg 的顺序启动,无论你启动多少遍,节点 2 总是 leader,而这时如果把节点 2 关掉,进行查看角色,发现节点 3 成了 leader。
以上这些现象都和 zookeeper 的选举机制有关
ZK 集群的选举机制
我们就拿 3 个节点的 zk 作一个简单选举的说明
zk 会进行多轮的投票,直到某一个节点的票数大于或等于半数以上,在 3 个节点中,总共会进行 2 轮的投票:
- 第一轮,每个节点启动时投票给自己,那这样 zk1,zk2,zk3 各有一票。
- 第二轮,每个节点投票给大于自己 myid,那这样 zk2 启动时又获得一票。加上自己给自己投的那一票。总共有 2 票。2 票大于了当前节点总数的半数,所以投票终止。zk2 当选 leader。
有的童鞋会问,zk3 呢,因为 zk2 已经当选了,投票终止了。
所以 zk2 也不会投票给 zk3 了。
当然这是一个比较简单版的选举,其实真正的选举还要比较 zxid,这个后面会讲到。
zk 选举什么时候会被触发呢?
一是启动时会被触发,二是 leader 宕机时会被触发。
上面的例子中,如果节点 2 宕机,根据规则,那获得 leader 的就应该是 zk3 了。
ZK 集群的数据同步机制
zookeeper 的数据同步是为了保证每个节点的数据一致性,大致分为 2 个流程:
- 一个是正常的客户端数据提交流程;
- 二是集群中某个节点宕机后数据恢复流程。
正常客户端数据提交流程
客户端写入数据提交流程大致为:leader 接受到客户端的写请求,然后同步给各个子节点:
但是有童鞋就产生疑惑了,客户端一般连接的是所有节点,客户端并不知道哪个是 leader 呀。
的确,客户端会和所有的节点建立链接,并且发起写入请求是挨个遍历节点进行的,比如第一次是节点 1,第二次是节点 2。
以此类推。
如果客户端正好链接的节点的角色是 leader,那就按照上面的流程走。
那如果链接的节点不是 leader,是 follower 呢,则有以下流程:
如果 Client 选择链接的节点是 Follower 的话,这个 Follower 会把请求转给当前 Leader,然后 Leader 会走蓝色的线把请求广播给所有的 Follower,每个节点同步完数据后会走绿色的线告诉 Leader 数据已经同步完成(但是还未提交)。
当 Leader 收到半数以上的节点 ACK 确认消息后,那么 Leader 就认为这个数据可以提交了,会广播给所有的 Follower 节点,所有的节点就可以提交数据。
整个同步工作就结束了。
那我们再来说说节点宕机后的数据同步流程
当 zookeeper 集群中的 Leader 宕机后,会触发新的选举,选举期间,整个集群是没法对外提供服务的。
直到选出新的 Leader 之后,才能重新提供服务。
我们重新回到 3 个节点的例子,zk1,zk2,zk3,其中 z2 为 Leader,z1,z3 为 Follower,假设 zk2 宕机后,触发了重新选举,按照选举规则,z3 当选 Leader。
这时整个集群只整下 z1 和 z3,如果这时整个集群又创建了一个节点数据,接着 z2 重启。这时 z2 的数据肯定比 z1 和 z3 要旧,那这时该如何同步数据呢。
zookeeper 是通过 ZXID 事务 ID 来确认的,ZXID 是一个长度为 64 位的数字,其中低 32 位是按照数字来递增,任何数据的变更都会导致低 32 位数字简单加 1。
高 32 位是 leader 周期编号,每当选举出一个新的 Leader 时,新的 Leader 就从本地事务日志中取出 ZXID,然后解析出高 32 位的周期编号,进行加 1,再将低 32 位的全部设置为 0。
这样就保证了每次选举新的 Leader 后,保证了 ZXID 的唯一性而且是保证递增的。
查看某个数据节点的 ZXID 的命令为:
代码语言:javascript复制先进入zk client命令行
./bin/zkCli.sh -server 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
stat加上数据节点名称
stat /test
执行结果为:
可以看到,有 3 个 ZXID,这 3 个 ZXID 各自代表:
cZxid:该节点创建时的事务 ID
mZxid:该节点最近一次更新时的事务 ID
pZxid:该节点的子节点的最新一次创建/更新/删除的事务 ID
查看节点最新 ZXID 的命令为:
代码语言:javascript复制echo stat|nc 127.0.0.1 2181
这个命令需提前在cfg文件后追加:4lw.commands.whitelist=*,然后重启
这里的 ZXID 就是当前节点最后一次事务的 ID。
如果整个集群数据为一致的,那么所有节点的 ZXID 应该一样。所以 zookeeper 就通过这个有序的 ZXID 来确保各个节点之间的数据的一致性,带着之前的问题,如果 Leader 宕机之后,再重启后,会去和目前的 Leader 去比较最新的 ZXID,如果节点的 ZXID 比最新 Leader 里的 ZXID 要小,那么就会去同步数据。
再看 ZK 中的选举
我们带着 ZXID 的概念再来看 ZK 中的选举机制。
假设还是有一个 3 个节点的集群,zk2 为 Leader,这时候如果 zk2 挂了。zk3 当选 Leader,zk1 为 Follower。这时候如果更新集群中的一个数据。
然后把 zk1 和 zk3 都关闭。然后挨个再重启 zk1,zk2,zk3。这时候启动后,zk2 还能当选为 Leader 吗?
其实这个问题,换句话说就是:在挨个启动 zk 节点的时候,zk1 和 zk3 的数据为最新,而 zk2 的数据不是最新的,按照之前的选举规则的话,zk2 是否能顺利当选 Leader?
答案为否,最后当选的为 zk1。
这是为什么呢。
因为 zk2 的最新 ZXID 已经不是最新了,zk 的选举过程会优先考虑 ZXID 大的节点。这时 ZXID 最大的有 zk1 和 zk3,选举只会在这 2 个节点中产生,根据之前说的选举规则。在第一轮投票的时候,zk1 只要获得 1 票,就能达到半数了,就能顺利当选为 Leader 了。