vlambda博客
学习文章列表

从0到1详解ZooKeeper的应用场景及架构原理

  • 背景

  • ZooKeeper

  • 深入ZooKeeper一致性协议原理

    • ZooKeeper服务端角色

    •  一致性协议-ZAB

    • ZAB协议读写流程

    • ZooKeeper Leader选举算法

    • ZooKeeper 集群启动选举流程图解

    • commit过的数据不丢失

  • 参考文献 && 鸣谢



01

背景

1 后台系统由集中式发展为分布式

随着计算机系统的规模越来越大,业务量的迅速提升和互联网的爆炸式增长,集中式系统采用大型主机单机部署带来了一系列问题:系统大而复杂、难于维护、发生单点故障引起雪崩、扩展性差等。这些都使业务面临巨大的压力和严重的风险,为了解决集中式系统架构面临的痛点,分布式系统架构逐步走上舞台。分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统,可以很好的解决系统扩容、可用性以及降低成本。

2 分布式系统架构引入的新问题

“天下没有免费的午餐”,分布式系统架构带来了优点的同时,也提出了一系列的挑战: 

(1)由于多节点甚至多地部署,节点之间的数据一致性如何保证? 

(2)在并发场景下如何保证任务只被执行一次?

(3)一个节点挂掉不能提供服务时如何被集群知晓并由其他节点接替任务? 

(4)存在资源共享时,资源的安全性和互斥性如何保证? 

以上列举了分布式系统中面临的一些挑战,需要一个协调机制来解决分布式集群中的问题,使得开发者更专注于应用本身的逻辑而不是关注分布式系统处理。

3 分布式协调组件

为解决分布式系统中面临的这些问题,开发者们通过工程实践创造了很多非常优秀的分布式系统协调组件,这些组件可以在分布式环境下,保证分布式系统的数据一致性和容错性等。其中为我们熟知的有:ZooKeeper、ETCD、Consul等。ZooKeeper作为Apache的顶级开源项目,基于Google Chubby 开源实现,在 Hadoop,Hbase,Kafka 等技术中充当核心组件的角色。虽然历史悠久,但就像陈酿一样,其设计思想和实现不论何时还是值得仔细学习和品味。


02


ZooKeeper

1 ZooKeeper是什么

从理论概念角度解释:ZooKeeper是一个分布式的,开源的分布式应用程序协调服务,它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

从数据读写角度解释:ZooKeeper 是一个分布式的开源协调服务,用于分布式系统。ZooKeeper允许你读取、写入数据和发现数据更新。数据按层次结构组织在文件系统中,并复制到ensemble(一个 ZooKeeper 服务器的集合)中所有的ZooKeeper服务器。对数据的所有操作都是原子的和顺序一致的。ZooKeeper通过Zab一致性协议在 ensemble 的所有服务器之间复制一个状态机来确保这个特性。

2 ZooKeeper的安装与使用

“纸上得来终觉浅,绝知此事要躬行”,学习一个新的组件,我们先通过安装使用,对配置、API等有一个直观的认识,也为后面动手实现一些功能部署好开发环境基础

2.1 ZooKeeper下载与安装

注意:本文均在Linux环境下指导演示) 

(1)JDK安装:ZooKeeper使用JAVA语言开发,使用前需要先安装JDK(读者自行安装),安装JDK后可在终端命令行中使用java -version命令查看版本。JDK安装教程:https://www.timberkito.com/?p=12


(2)ZooKeeper下载:https://zookeeper.apache.org/releases.html 

在下载页面分为最新的Release版本和最近的稳定Release版本,这里生产环境使用推荐稳定版本,点击下载并上传apache-zookeeper-3.7.0-bin.tar.gz到Linux服务器

从0到1详解ZooKeeper的应用场景及架构原理


(3)ZooKeeper安装:

ZooKeeper安装分为集群安装和单机安装,生产环境一般为集群安装。此处作为演示,使用一台服务器来做模拟集群,也称伪集群安装(通过三个不同的文件夹zk1/zk2/zk3,模拟真实环境中的三台服务器实例)

1.本篇中我们将要在本地开发机上安装三个zk实例(可以认为在生产集群模式中,这是三台不同的服务器),其安装位置分别如下

/Users/newboy/ZooKeeper/zk1/Users/newboy/ZooKeeper/zk2/Users/newboy/ZooKeeper/zk3

    2.将上文中下载的ZooKeeper安装包apache-zookeeper-3.7.0-bin.tar.gz上传到第一个实例zk1文件夹下,并使用如下命令进行解压:

tar -xzvf apache-zookeeper-3.7.0-bin.tar.gz

    3.解压完成后在zk1文件夹下创建data和log目录,分别用于存储当前zk实例数据和日志

mkdir data logs


    此时zk1文件夹目录结构如下所示:
从0到1详解ZooKeeper的应用场景及架构原理


    4.创建myid文件 在zk1的data目录下, 创建myid文件,此文件记录节点id,每个zookeeper节点都需要一个myid文件来记录节点在集群中的id,此文件中只能有一个数字,这里zk1实例myid中写入一个1
echo "1" >> /Users/newboy/ZooKeeper/zk1/data/myid // 实例zk1的myid赋值为1echo "2" >> /Users/newboy/ZooKeeper/zk2/data/myid // 实例zk2的myid赋值为2echo "3" >> /Users/newboy/ZooKeeper/zk3/data/myid // 实例zk3的myid赋值为3

     5.进入zk1文件夹下apache-zookeeper-3.7.0-bin/conf/目录,将配置文件zoo_sample.cfg重命名为zoo.cfg,打开zoo.cfg进行配置,具体配置如下:

tickTime=2000 # 单位时间,其他时间都是以这个倍数来表示initLimit=10  # 节点初始化时间,10倍单位时间(即十倍tickTime)syncLimit=5   # 心跳最大延迟周期dataDir=/Users/newboy/ZooKeeper/zk1/data   # 该实例对应的数据目录(上文步骤3创建)dataLogDir=/Users/newboy/ZooKeeper/zk1/logs # 该实例对应的日志目录(上文步骤3创建)clientPort=2181                   # 端口(每个实例不同)server.1=127.0.0.1:8881:7771    server.2=127.0.0.1:8882:7772server.3=127.0.0.1:8883:7773

  集群配置中模版为server.id=host:port:port

    id是上面myid文件中配置的id;ip是节点的ip,第一个port是节点之间通信的端口,第二个port用于选举leader节点(在真正的集群模式下,不同服务器可以共用同一个port,这里单机上演示为了避免端口冲突,选择不同的端口)

    6.zk2和zk3的实例配置与zk1类似,为了方便我们可以直接拷贝zk1的配置到zk2和zk3文件夹,然后修改各自的zoo.cfg和data目录下的myid即可 拷贝命令:

cp -R zk1 zk2cp -R zk1 zk3

zk2对应的zoo.cfg:

tickTime=2000 # 单位时间,其他时间都是以这个倍数来表示initLimit=10  # 节点初始化时间,10倍单位时间(即十倍tickTime)syncLimit=5   # 心跳最大延迟周期dataDir=/Users/newboy/ZooKeeper/zk2/data    # 该实例对应的数据目录(上文步骤3创建)dataLogDir=/Users/newboy/ZooKeeper/zk2/logs # 该实例对应的日志目录(上文步骤3创建)clientPort=2182 # 端口(每个实例不同)// 集群配置server.1=127.0.0.1:8881:7771 # server.id=host:port:portserver.2=127.0.0.1:8882:7772 # server.id=host:port:portserver.3=127.0.0.1:8883:7773 # server.id=host:port:port

zk3对应的zoo.cfg:

tickTime=2000 # 单位时间,其他时间都是以这个倍数来表示initLimit=10  # 节点初始化时间,10倍单位时间(即十倍tickTime)syncLimit=5   # 心跳最大延迟周期dataDir=/Users/newboy/ZooKeeper/zk3/data    # 该实例对应的数据目录(上文步骤3创建)dataLogDir=/Users/newboy/ZooKeeper/zk3/logs # 该实例对应的日志目录(上文步骤3创建)clientPort=2183 # 端口(每个实例不同)// 集群配置server.1=127.0.0.1:8881:7771 # server.id=host:port:portserver.2=127.0.0.1:8882:7772 # server.id=host:port:portserver.3=127.0.0.1:8883:7773 # server.id=host:port:port

至此zk伪集群模式的安装配置已经完成,整体目录结构纵览如下:

.├── zk1│ ├── data│ │ └── myid│ ├── logs│ └── apache-zookeeper-3.7.0-bin├── zk2│ ├── data│ │ └── myid│ ├── logs│ └── apache-zookeeper-3.7.0-bin└── zk3│ ├── data│ │ └── myid│ ├── logs│ └── apache-zookeeper-3.7.0-bin


(4)ZooKeeper实例启动及使用客户端交互:

      1.启动刚刚创建的三个zk实例

      启动zk1实例,命令行运行下面命令:
// 启动命令/Users/newboy/ZooKeeper/zk1/apache-zookeeper-3.7.0-bin/bin/zkServer.sh start // 启动结果ZooKeeper JMX enabled by defaultUsing config: /Users/newboy/ZooKeeper/zk1/apache-zookeeper-3.7.0-bin/bin/../conf/zoo.cfgStarting zookeeper ... STARTED

     同样启动zk2和zk3实例,命令行运行下面命令:

// 启动zk2命令/Users/newboy/ZooKeeper/zk2/apache-zookeeper-3.7.0-bin/bin/zkServer.sh start// 启动zk3命令/Users/newboy/ZooKeeper/zk3/apache-zookeeper-3.7.0-bin/bin/zkServer.sh start

      2.连接实例 所有实例全部启动过后,选择任一实例进行连接,这里选择实例zk2,命令行输入如下命令:

/Users/newboy/ZooKeeper/zk2/apache-zookeeper-3.7.0-bin/bin/zkCli.sh -server 127.0.0.1:2182

      3.创建节点 连接之后,可以在当前实例上创建节点,类似于创建一个kv值或者文件夹(ZK的命令和可选参数读者可以自行查看用户手册)

// 创建节点 create表示创建命令,/zk-demo为节点名称 123为节点值[zk: 127.0.0.1:2181(CONNECTED) 1] create /zk-demo 123Created /zk-demo// 获取节点值 get表示获取 /zk-demo为需要获取的节点名称[zk: 127.0.0.1:2181(CONNECTED) 2] get /zk-demo123

      4.在其他实例上获取zk2实例创建的节点 由于zk会将节点写入的值同步到集群中每个节点,从而保证数据的一致性,那么其他节点理论上也可以访问到刚刚zk2创建的值。下面连接zk1来验证下:

// 连接zk1/Users/newboy/ZooKeeper/zk1/apache-zookeeper-3.7.0-bin/bin/zkCli.sh -server 127.0.0.1:2181// 获取zk2上创建的节点/zk-demo[zk: 127.0.0.1:2183(CONNECTED) 0] get /zk-demo123

可以看到,我们成功地在实例zk1上获取到了实例zk2创建的节点,说明数据写入zk2后,在各个节点间同步并实现了一致,zk的下载、安装和基本命令操作也就讲完了。

3 ZooKeeper能做什么

前文中,我们了解了ZooKeeper出现的背景,它是分布式系统中非常重要的中间件,分布式应用程序可以基于ZooKeeper实现:

数据的发布和订阅服务注册与发现分布式配置中心命名服务分布式锁Master选举负载均衡分布式队列     可以看到ZooKeeper可以实现非常多的功能,之所以能够实现各种不同的能力,源于ZooKeeper底层的数据结构和数据模型

4 ZooKeeper的数据结构和数据模型

4.1 Znode数据节点

ZooKeeper的数据节点可以视为树状结构,树中的各节点被称为Znode(即ZooKeeper node),一个Znode可以有多个子节点,ZooKeeper中的所有存储的数据是由 znode组成,并以 key/value 形式存储数据。整体结构类似于 linux 文件系统的模式以树形结构存储,其中根路径以 / 开头

从0到1详解ZooKeeper的应用场景及架构原理


如上图所示,在根目录下我们创建Dog和Cat两个不同的数据节点,Cat节点下有TomCat这个数据存储节点,整个ZooKeeper的树形存储结构就是这样的Znode构成,并存储在内存中。命令行下使用ZooKeeper客户端工具创建节点的过程如下:首先连接一个zk实例:

// 连接zk1/Users/newboy/ZooKeeper/zk1/apache-zookeeper-3.7.0-bin/bin/zkCli.sh -server 127.0.0.1:2181

创建节点:

[zk: 127.0.0.1:2181(CONNECTED) 5] create /DogCreated /Dog[zk: 127.0.0.1:2181(CONNECTED) 6] create /CatCreated /Cat[zk: 127.0.0.1:2181(CONNECTED) 7] create /Cat/TomCatCreated /Cat/TomCat

使用ls命令查看各个目录下的节点数据:

[zk: 127.0.0.1:2181(CONNECTED) 8] ls /[Cat, Dog, zk-demo, zookeeper][zk: 127.0.0.1:2181(CONNECTED) 10] ls /Cat[TomCat]

Znode节点类似于Unix文件系统,但也有自己的特性:

(1)Znode兼具文件和目录特点 既像文件一样维护着数据、信息、时间戳等数据,又像目录一样可以作为路径标识的一部分,并可以具有子Znode。用户对Znode具有增、删、改、查等操作 

(2)Znode具有原子性操作 读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据 

(3)Znode存储数据大小有限制 每个Znode的数据大小至多1M,但是常规使用中应该远小于此值 

(4)Znode通过路径引用 如同Unix中的文件路径。路径必须是绝对的,因此他们必须由斜杠字符来开头。除此以外,他们必须是唯一的,也就是说每一个路径只有一个表示,因此这些路径不能改变

4.2 Znode节点类型

Znode有两种,分别为临时节点和永久节点,节点的类型在创建时即被确定,并且不能改变 。

(1)临时节点:该节点的生命周期依赖于创建它们的会话。一旦会话结束,临时节点将被自动删除,当然可以也可以手动删除。临时节点不允许拥有子节点。

(2)永久节点:该节点的生命周期不依赖于会话,并且只有在客户端显式执行删除操作的时候,才能被删除 。

Znode还有一个序列化的特性,如果创建的时候指定的话,该Znode的名字后面会自动追加一个递增的序列号。序列号对于此节点的父节点来说是唯一的,这样便会记录每个子节点创建的先后顺序 因此组合之后,Znode有四种节点类型:PERSISTENT:永久节点 

EPHEMERAL:临时节点 

PERSISTENT_SEQUENTIAL:永久顺序节点 

EPHEMERAL_SEQUENTIAL:临时顺序节点 


为了对节点类型有更清楚的认识,在命令行下来模拟创建一个临时节点:

(1)首先连接zk1实例:

// 连接zk1/Users/newboy/ZooKeeper/zk1/apache-zookeeper-3.7.0-bin/bin/zkCli.sh -server 127.0.0.1:2181

(2)创建一个临时节点:

// -e 表示该节点为临时节点[zk: 127.0.0.1:2181(CONNECTED) 12] create -e /Dog/Puppy 123Created /Dog/Puppy

(3)连接zk2实例,查看该临时节点是否同步:

// 连接zk2/Users/newboy/ZooKeeper/zk2/apache-zookeeper-3.7.0-bin/bin/zkCli.sh -server 127.0.0.1:2182// 查询/Dog/Puppy节点值[zk: 127.0.0.1:2182(CONNECTED) 2] get /Dog/Puppy123

(4)断开zk1实例的会话

[zk: 127.0.0.1:2181(CONNECTED) 16] quitWATCHER::WatchedEvent state:Closed type:None path:null2022-03-15 15:39:55,807 [myid:] - INFO [main:ZooKeeper@1232] - Session: 0x1000c3279ae0000 closed2022-03-15 15:39:55,807 [myid:] - INFO [main-EventThread:ClientCnxn$EventThread@570] - EventThread shut down for session: 0x1000c3279ae00002022-03-15 15:39:55,810 [myid:] - ERROR [main:ServiceUtils@42] - Exiting JVM with code 0

(5)在zk2上查看该节点

[zk: 127.0.0.1:2182(CONNECTED) 3] get /Dog/Puppyorg.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode = NoNode for /Dog/Puppy

可以看到/Dog/Puppy临时节点随着zk1实例会话的退出消失了,这就是临时节点的特性,zk1创建的临时节点会随着zk1实例连接的退出而消失,永久节点则只能通过delete /Dog(节点名)删除才会消失

4.3 ZooKeeper的Znode Watcher机制

ZooKeeper可以用来做数据的发布和订阅,一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能够让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使它们能够做出相应的处理。在ZooKeeper中,引入了Watcher机制实现这种分布式的通知功能 ZooKeeper允许ZK客户端向服务端注册一个Watcher监听,当服务端的一些指定事件触发了这个Watcher,那么就会向指定客户端发送一个事件通知。例如ZK客户端监听临时节点/Cat,当该临时节点消失时,则会由服务端触发调用客户端WatchManager,客户端从WatchManager中取出对应的Watcher对象来进行处理逻辑

从0到1详解ZooKeeper的应用场景及架构原理


(1)客户端首先将Watcher注册到服务端,同时将Watcher对象保存到客户端的Watch管理器中。

(2)当ZooKeeper服务端监听的数据状态发生变化时,服务端会主动通知客户端 (3)接着客户端的Watch管理器会触发相关Watcher来回调相应处理逻辑,从而完成整体的数据发布/订阅流程

4.4 经典案例:基于Znode临时顺序节点+Watcher机制实现公平分布式锁

1、临时顺序节点:在介绍Znode节点时,我们提到过Znode节点有“临时节点”这个类型,它会随着客户端连接的断开而消失,同时节点类型可以选择顺序性,组合起来就是“临时顺序节点”,如下图所示:

从0到1详解ZooKeeper的应用场景及架构原理

在根目录“/”下创建分布式锁“/Lock”节点目录,/Lock节点本身可以是永久节点,用于存放客户端抢占创建的临时顺序节点。此时假设有两个ZK客户端A和B同时调用Create函数,在"/Lock"节点下创建临时顺序节点,A比B网络延时更小,先创建,ZK分配节点名称为"/Lock/Seq0001", B晚于A创建成功,ZK分配节点名为"/Lock/Seq0002",ZK负责维护这个递增的顺序节点名。

2、分布式锁实现的具体流程 :

(1)如下图,客户端A、B同时在"/Lock"节点下创建临时顺序子节点,可以理解为同时抢占分布式锁,A先于B创建成功,此时分配的节点为“/Lock/seq-0000001”,由于A创建成功,并且临时顺序节点的顺序值序号最小,代表它是最先获取到该锁,此时加锁成功

从0到1详解ZooKeeper的应用场景及架构原理


(2)如下图,(红色虚线)客户端B晚于A创建临时顺序节点,此时ZK分配的节点顺序值为“/Lock/seq-0000002”,B创建成功之后,它的顺序值大于A的顺序值,不是最小顺序值,此时说明A已经抢占到分布式锁,这个时候B就使用Watcher监听机制,监听次小于自己的临时顺序节点A的状态变化

从0到1详解ZooKeeper的应用场景及架构原理


(3)如下图,当A客户端因宕机或者完成处理逻辑而断开链接时,A创建的临时顺序节点会随之消失,此时由于客户端B已经监听了A临时顺序节点的状态变化,当消失事件发生时,Watcher监听器逻辑会回调客户端B,B重新开始获取锁。

注意此时不是B再次创建节点,而是获取"/Lock"下的临时顺序节点,发现自己的顺序值最小,那么就加锁成功。

从0到1详解ZooKeeper的应用场景及架构原理


如果有C、D甚至更多的客户端同时抢占,原理都是一致的,他们会依次排队,监听自己之前(节点顺序值次小于自己)的节点,等待他们的状态发生变化时,再去重新获取锁 这里使用临时顺序节点和Watcher机制实现了一个公平分布式锁,还有很多其他用法,如只使用临时节点实现非公平分布式锁,篇幅所限,读者可以自行探索。


03


深入ZooKeeper一致性协议原理

从0到1详解ZooKeeper的应用场景及架构原理

上图是ZooKeeper的整体架构,ZooKeeper Service是服务端集群,也是整个组件的核心,客户端的读写请求都是它来处理。ZK下载安装章节模拟的zk1/zk2/zk3就可以认为是一个ZK服务端集群,我们在zk2中写入的节点值,在zk1和zk3实例中也能读到这个节点值,zk2会话退出后临时节点在其他服务器上也同样消失了,ZK服务端是通过什么机制实现数据在各个节点之间的同步,从而保证一致性?当有节点出现故障时又是如何保证正常提供对外服务?这就涉及到ZooKeeper的核心-分布式一致性原理。

1 ZooKeeper服务端角色


从0到1详解ZooKeeper的应用场景及架构原理

Leader 一个ZooKeeper集群同一时间只会有一个实际工作的Leader,它会发起并维护与各Follwer及Observer间的心跳。所有的写操作必须要通过Leader完成再由Leader将写操作广播给其它服务器。Follower 一个ZooKeeper集群可能同时存在多个Follower,它会响应Leader的心跳。Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理,并且负责在Leader处理写请求时对请求进行投票。Observer 角色与Follower类似,但是无投票权。 1、早期的 ZooKeeper 集群服务运行过程中,只有Leader服务器和Follow服务器 2、随着集群规模扩大,follower变多,ZK在创建节点和选主等事务性请求时,需要一半以上节点AC,所以导致性能下降写入操作越来越耗时,follower之间通信越来越耗时 3、为了解决这个问题,就引入了观察者,可以处理读,但是不参与投票。既保证了集群的扩展性,又避免过多服务器参与投票导致的集群处理请求能力下降

2 一致性协议-ZAB

ZooKeeper为了保证集群中各个节点读写数据的一致性和可用性,设计并实现了ZAB协议,ZAB全称是ZooKeeper Atomic Broadcast,也就是ZooKeeper原子广播协议。这种协议支持崩溃恢复,并基于主从模式,同一时刻只有一个Leader,所有的写操作都由Leader节点主导完成,而读操作可通过任意节点完成,因此ZooKeeper读性能远好于写性能,更适合读多写少的场景。一旦Leader节点宕机,ZAB协议的崩溃恢复机制能自动从Follower节点中重新选出一个合适的替代者,即新的Leader,该过程即为领导选举。领导选举过程,是ZAB协议中最为重要和复杂的过程

3. ZAB协议读写流程

3.1 ZAB写流程

3.1.1 写Leader


从0到1详解ZooKeeper的应用场景及架构原理

由上图可见,通过Leader进行写操作,主要分为五步:

1.客户端向Leader发起写请求2.Leader将写请求以Proposal的形式发给所有Follower并等待ACK3.Follower收到Leader的Proposal后返回ACK4.Leader得到过半数的ACK(Leader对自己默认有一个ACK)后向所有的Follower和Observer发送Commmit5.Leader将处理结果返回给客户端

注意 Leader并不需要得到Observer的ACK,即Observer无投票权 Leader不需要得到所有Follower的ACK,只要收到过半的ACK即可,同时Leader本身对自己有一个ACK。上图中有4个Follower,只需其中两个返回ACK即可,因为(2+1) / (4+1) > 1/2 Observer虽然无投票权,但仍须同步Leader的数据从而在处理读请求时可以返回尽可能新的数据

3.1.2 写Follower

从0到1详解ZooKeeper的应用场景及架构原理

从上图可见:

Follower可接受写请求,但不能直接处理,而需要将写请求转发给Leader处理Observer与Follower写流程相同除了多了一步请求转发,其它流程与直接写Leader无任何区别

3.2 ZAB读流程

从0到1详解ZooKeeper的应用场景及架构原理

Leader/Follower/Observer都可直接处理读请求,从本地内存中读取数据并返回给客户端即可。由于处理读请求不需要服务器之间的交互,Follower/Observer越多,整体可处理的读请求量越大,也即读性能越好。 ZooKeeper官方文档数据,Client数量1000时,读写性能比10:1

4 ZooKeeper Leader选举算法

4.1 选举算法

ZooKeeper中默认的并建议使用的Leader选举算法是:基于TCP的FastLeaderElection,其他选举算法被废弃。集群模式下zoo.cfg配置文件中有参数可配选举算法:

参数名 说明
electionAlg 默认为3。(No Java system property)
配置zk的选举算法。“0”为基于原始的UDP的LeaderElection,“1”为基于UDP和无认证的的FastLeaderElection,“2”为基于UDP和认证的FastLeaderElection,“3”为基于TCP的FastLeaderElection。
目前,0、1和2的选举算法的实现已经弃用,并有意从下个版本中移除,故而该参数应该用处不大了。


4.2 FastLeaderElection选举参数解析

(1)选举算法参数myid:每个ZooKeeper服务器,都需要在数据文件夹下创建一个名为myid的文件,该文件包含整个ZooKeeper集群唯一的ID(整数)。例如,我们第二章中部署的zk1/zk2/zk3三个实例,其myid分别为1、2和3,在配置文件中其ID与hostname必须一一对应,如下所示。在该配置文件中,server.后面的id即为myid。该参数在选举时如果无法通过其他判断条件选择Leader,那么将该ID的大小来确定优先级。

// 集群配置server.1=127.0.0.1:8881:7771 # server.id=host:port:portserver.2=127.0.0.1:8882:7772 # server.id=host:port:portserver.3=127.0.0.1:8883:7773 # server.id=host:port:port

zxid:用于标识一次更新操作的ID。为了保证顺序性,该zxid必须单调递增,因此ZooKeeper使用一个64位的数来表示,高32位是Leader的epoch,从1开始,每次选出新的Leader,epoch加一。低32位为该epoch内的序号,每次有写操作低32位加一,每次epoch变化,都将低32位的序号重置。这样保证了zxid的全局递增性。之前看到过有博主使用中国古代的年号来解释这个字段,非常形象:万历十五年,万历是epoch,十五年是序号选票数据结构:每个服务器在进行选举时,发送的选票包含如下关键信息:

struct Vote { logicClock # 逻辑时钟,表示该服务器发起的第多少轮投票 state # 当前服务器的状态 (LOOKING-不确定Leader状态 FOLLOWING-跟随者状态 LEADING-领导者状态 OBSERVING-观察者状态) self_myid # 当前服务器的myid self_zxid # 当前服务器上所保存的数据的最大zxid vote_myid # 被推举的服务器的myid vote_zxid # 被推举的服务器上所保存的数据的最大zxid}

节点服务器状态:每个服务器所处的状态时下面状态中的一种:    LOOKING 不确定Leader状态。该状态下的服务器认为当前集群中没有Leader,会发起Leader选举。  FOLLOWING 跟随者状态。表明当前服务器角色是Follower,并且它知道Leader是谁。    LEADING 领导者状态。表明当前服务器角色是Leader,它会维护与Follower间的心跳。    OBSERVING 观察者状态。表明当前服务器角色是Observer,与Folower唯一的不同在于不参与选举,也不参与集群写操作时的投票。

4.3 选举投票流程

从0到1详解ZooKeeper的应用场景及架构原理


每个服务器的一次选举流程:

1.自增选举轮次:即logicClock加一,ZooKeeper规定所有有效的投票都必须在同一轮次中。每个服务器在开始新一轮投票时,会先对自己维护的logicClock进行自增操作。2.初始化选票:每个服务器在开始进行新一轮的投票之前,会将自己的投票箱清空,然后初始化自己的选票。在初始化阶段,每台服务器都会将自己推选为Leader,也就是将票都投给自己。例如:服务器1、2、3都投票给自己(1->1), (2->2),(3->3)3.发送初始化选票:每个服务器通过广播将初始化投给自己的票广播出去,让其他服务器接收4.接收外部投票:服务器会尝试从其它服务器获取投票,并记入自己的投票箱内。如果无法获取任何外部投票,则会确认自己是否与集群中其它服务器保持着有效连接。如果是,则再次发送自己的投票;如果否,则马上与之建立连接5.判断选举轮次:收到外部投票后,首先会根据投票信息中所包含的logicClock来进行不同处理 (1)如果大于当前服务的选票中的选举次数,那么则会更新当前服务的logicClock,并且清空所有收到的选票,再次拿选票和外部投票进行选票的比较,确定是否真的要更改自身的选票,然后重新发送选票信息 (2)如果外部选票的选举次数小于当前服务实例的选举次数,那么直接无视掉这个选票信息,并且继续发送自身的选票出去 (3)如果外部选票和自身服务实例的选举次数一致,那么就需要进入选票之间的比较操作6.选票PK:选票PK是基于(self_myid, self_zxid)与(vote_myid, vote_zxid)的对比 (1)外部投票的logicClock大于自己的logicClock,则将自己的logicClock及自己的选票的logicClock变更为收到的logicClock (2)若logicClock一致,则对比二者的vote_zxid,若外部投票的vote_zxid比较大,则将自己的票中的vote_zxid与vote_myid更新为收到的票中的vote_zxid与vote_myid并广播出去,另外将收到的票及自己更新后的票放入自己的票箱。如果票箱内已存在(self_myid, self_zxid)相同的选票,则直接覆盖 (3)若二者vote_zxid一致,则比较二者的vote_myid,若外部投票的vote_myid比较大,则将自己的票中的vote_myid更新为收到的票中的vote_myid并广播出去,另外将收到的票及自己更新后的票放入自己的票箱7.统计选票:如果已经确定有过半服务器认可了自己的投票(可能是更新后的投票),则终止投票。否则继续接收其它服务器的投票8.更新服务器状态:投票终止后,服务器开始更新自身状态。若过半的票投给了自己,则将自己的服务器状态更新为LEADING,否则将自己的状态更新为FOLLOWING

同时还需要注意的一点是,即使选票超过半数了,选出Leader服务实例了,也不是立刻结束,而是等待200ms,确保没有丢失其他服务的更优的选票

5 ZooKeeper 集群启动选举流程图解

5.1 集群启动领导选举

1、各自推选自己:ZooKeeper集群刚启动时,所有服务器的logicClock都为1,zxid都为0。各服务器初始化后,先把第一票投给自己并将它存入自己的票箱,同时广播给其他服务器。此时各自的票箱中只有自己投给自己的一票。如下图所示

从0到1详解ZooKeeper的应用场景及架构原理


2、更新选票:第一步中各个服务器先投票给自己,并把投给自己的结果广播给集群中的其他服务器,这一步其他服务器接收到广播后开始更新选票操作(如果对此规则不熟悉,可以对照4.3 选举投票流程小节),以Server1为例流程如下:

(1)Server1收到Server2和Server3的广播选票后,由于logicClock和zxid都相等,此时就比较myid 

(2)Server1收到的两张选票中Server3的myid最大,此时Server1判断应该遵从Server3的投票决定,将自己的票改投给Server3。接下来Server1先清空自己的票箱(票箱中有第一步中投给自己的选票),然后将自己的新投票(1->3)和接收到的Server3的(3->3)投票一起存入自己的票箱,再把自己的新投票决定(1->3)广播出去,此时Server1的票箱中有两票:(1->3),(3->3) 

(3)同理,Server2收到Server3的选票后也将自己的选票更新为(2->3)并存入票箱然后广播。此时Server2票箱内的选票为(2->3),(3->3) 

(4)Server3根据上述规则,无须更新选票,自身的票箱内选票仍为(3->3) 

(5)Server1与Server2重新投给Server3的选票广播出去后,由于三个服务器最新选票都相同,最后三者的票箱内都包含三张投给服务器3的选票

从0到1详解ZooKeeper的应用场景及架构原理


3、根据选票确定角色:

根据上述选票,三个服务器一致认为此时Server3应该是Leader。

因此Server1和Server2都进入FOLLOWING状态,而Server3进入LEADING状态。之后Leader发起并维护与Follower间的心跳

从0到1详解ZooKeeper的应用场景及架构原理

5.2 Follower重启选举

本节讨论Follower节点发生故障重启或网络产生分区恢复后如何进行选举 1、Follower重启投票给自己:Follower重启,或者发生网络分区后找不到Leader,会进入LOOKING状态并发起新的一轮投票

从0到1详解ZooKeeper的应用场景及架构原理


2、发现已有Leader后成为Follower:

Server3收到Server1的投票后,将自己的状态LEADING以及选票返回给Server1。

Server2收到Server1的投票后,将自己的状态FOLLOWING及选票返回给Server1。

此时Server1知道Server3是Leader,并且通过Server2与Server3的选票可以确定Server3确实得到了超过半数的选票。

因此服务器1进入FOLLOWING状

从0到1详解ZooKeeper的应用场景及架构原理

5.3 Leader宕机重启选举

1、Follower发起新投票:Leader(Server3)宕机后,Follower(Server1和2)发现Leader不工作了,因此进入LOOKING状态并发起新的一轮投票,并且都将票投给自己,同时将投票结果广播给对方

从0到1详解ZooKeeper的应用场景及架构原理


2、更新选票:

(1)Server1和2根据外部投票确定是否要更新自身的选票,这里跟之前的选票PK流程一样,比较的优先级为:

从0到1详解ZooKeeper的应用场景及架构原理


3、重新选出Leader:

从0到1详解ZooKeeper的应用场景及架构原理


4、旧Leader恢复发起选举:

之前宕机的旧Leader恢复正常后,进入LOOKING状态并发起新一轮领导选举,并将选票投给自己。

此时服务器1会将自己的LEADING状态及选票返回给服务器3,而服务器2将自己的FOLLOWING状态及选票返回给服务器3

从0到1详解ZooKeeper的应用场景及架构原理


5、旧Leader成为Follower:

服务器3了解到Leader为服务器1,且根据选票了解到服务器1确实得到过半服务器的选票,因此自己进入FOLLOWING状态

6 commit过的数据不丢失

ZK的数据写入都是通过Leader,一条数据写入过程中,ZK服务集群中只有超过一半的服务器返回给Leader ACK后,Leader服务器才会Commit这条消息,同步到每一个节点。已经被过Leader commit,也就是被过半节点同步过的消息,在Leader宕机之后,重新选举出Leader这个消息也不会丢失。但是未被commit也就是未被过半节点复制到的消息则会丢失。

四、参考文献 && 鸣谢

内部文章:

  • 你一定看得懂学得会记得住的paxos、raft相关知识点梳理

  • ZooKeeper原理剖析

  • 十分钟带你理解ZooKeeper核心概念

  • 分布式共识算法之Paxos详解

外部文章:

https://forthe77.github.io/2019/03/25/one-zookeeper-deploy/ https://blog.nowcoder.net/n/16f13a7d72b2496c8ff4da080f777a5a https://blog.csdn.net/Weixiaohuai/article/details/112788171 https://www.cnblogs.com/IcanFixIt/p/7818592.html https://www.runoob.com/w3cnote/zookeeper-znode-data-model.html https://www.cnblogs.com/reycg-blog/p/10208585.html https://dbaplus.cn/news-141-1875-1.html https://juejin.cn/post/6907151199141625870 https://lotabout.me/2019/QQA-What-is-Sequential-Consistency/  https://developer.aliyun.com/article/768655 https://www.cnblogs.com/aspirant/p/8994227.html https://blog.xiaohansong.com/lamport-logic-clock.html http://icyfenix.cn/distribution/consensus/ https://dbaplus.cn/news-141-2053-1.html https://www.infoq.cn/article/dvaaj71f4fbqsxmgvdce https://segmentfault.com/a/1190000039760185