一、Zookeeper出现背景
加入存在一个服务器集群,出现了以下几种情况:
20台机器同时工作时,有一台机器down掉了,其他机器怎么进行接管计算任务,否则有些用户的业务不会被处理,造成用户服务终断;
随着用户数量增加,添加机器是可以解决计算的瓶颈,但需要重启所有计算节点,如果需要,那么将会造成整个系统的不可用;
用户数量增加或者减少,计算节点中的机器会出现有的机器资源使用率繁忙,有的却空闲,因为计算节点不知道彼此的运行负载状态;
怎么去通知每个节点彼此的负载状态,怎么保证通知每个计算节点方式的可靠性和实时性。
zookeeper就是为了解决上述的四种情况而诞生的,主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
二、Zookeeper基本架构
Zookeeper的基本架构就是文件系统+通知机制。
1.数据模型
数据模型图如下:
Zookeeper的数据模型有点像各操作系统的文件系统,每个节点我们称之为Znode,每个Znode可以存储数据,并且支持自由的增加删除节点,每个节点最多支持存储1M数据。
Zookeeper访问Znode节点具有原子性,要么读取到所有的数据,要么读取操作失败;同样的,当zk尝试修改Znode节点数据,要么替换所有的数据成功,要么失败。
Zookeeper中的路径必须是绝对路径,即每条路径必须从一个斜杠开始。所有路径必须是规范的,即每条路径只有唯一的一种表示方式,不支持路径解析。/zookeeper是一个保留词,不能用作一个路径组件。Zookeeper使用/zookeeper来保存管理信息。
其中Znode分别有四种类型:
1、 PERSISTENT-持久化目录节点:客户端与zookeeper断开连接后,该节点依旧存在;
2、 PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点:客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号;
3、 EPHEMERAL-临时目录节点:客户端与zookeeper断开连接后,该节点被删除;
4、 EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点:客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号;
每个节点都有状态信息,状态基本属性如下:
状态属性 |
说明 |
cZxid |
Created ZXID表示该数据节点被创建时的事务ID |
ctime |
Created Time表示节点被创建的时间 |
mZxid |
Modified ZXID 表示该节点最后一次被更新时的事务ID |
mtime |
Modified Time表示节点最后一次被更新的时间 |
pZxid |
表示该节点的子节点列表最后一次被修改时的事务ID。只有子节点列表变更了才会变更pZxid,子节点内容变更不会影响pZxid |
cversion |
子节点的版本号 |
dataVersion |
数据节点版本号 |
aclVersion |
节点的ACL版本号 |
ephemeralOwner |
创建该临时节点的会话的SessionID。如果节点是持久节点,这个属性为0 |
dataLength |
数据内容的长度 |
numChildren |
当前节点的子节点个数 |
2.系统模型
系统模型图如下:
模型目的:
1、 最终一致性:client不论连接到哪个Server,展示给它都是同一个视图,这是zookeeper最重要的性能;
2、 可靠性:具有简单、健壮、良好的性能,如果消息m被到一台服务器接受,那么它将被所有的服务器接受;
3、 实时性:Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据;
4、 等待无关(wait-free):慢的或者失效的client不得干预快速的client的请求,使得每个client都能有效的等待;
5、 原子性:更新只能成功或者失败,没有中间状态;
6、 顺序性:包括全局有序和偏序两种:全局有序是指如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布;偏序是指如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面;
三、Zookeeper提供的功能
既然我们前面知道了zk的架构和组成,那么我们可以使用这些来干嘛呢?
1.命名服务(NameService)
因为zk的数据模型是树形的,因此一个path可以确定一个节点,每个节点都是独一无二的,因此可以提供命名服务。在分布式系统中,通过使用命名服务来生成全局唯一的ID标识,客户端应用能够根据指定唯一ID来获取资源或服务的地址,提供者等信息。被命名的实体通常可以是集群中的机器,提供的服务,远程对象等等——这些我们都可以统称他们为ID(即唯一标识)。其中较为常见的就是一些分布式服务框架中的服务地址列表,通过在ZooKeepr里创建顺序节点,能够很容易创建一个全局唯一的路径,这个路径就可以作为唯一ID去通过zk访问。
结构如下图:
2.配置管理(Configuration)
每个程序都需要配置,如果程序分散部署在多台机器上,要逐个改变配置就变得困难。如果把这些配置全部放到zookeeper上去,保存在 Zookeeper 的某个目录节点中,所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到Zookeeper的通知,从Zookeeper获取新的配置信息应用到系统中即可。
结构如下图:
3.集群管理(GroupMembers)
zk的集群管理无在乎两点:1、是否有机器退出和加入;2、选举master。
对于第一点,所有机器约定在父目录GroupMembers下创建临时目录节点,然后监听父目录节点的子节点变化消息。一旦有机器挂掉,该机器与 zookeeper的连接断开,其所创建的临时目录节点被删除,所有其他机器都收到通知:某个目录被删除。于是,所有机器都知道该节点被删除了。新机器加入也是类似流程,所有机器收到通知:新目录加入。所有节点便知道有新节点加入。
对于第二点,所有机器创建临时顺序编号目录节点,每次选取编号最小的机器作为master。当然,实际上是要经过算法来选举的,不会那么草率便决定master。
结构如下图:
4.分布式锁
有了zookeeper的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
对于第一类,我们将zookeeper上的一个znode看作是一把锁,通过createznode的方式来实现。所有客户端都去创建/distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。厕所有言:来也冲冲,去也冲冲,用完删除掉自己创建的distribute_lock节点就释放出锁。
对于第二类,/distribute_lock已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选master一样,编号最小的获得锁,用完删除,依次方便。
虽然我们可以用ZK做这些事,但就像前面说过的那样,ZK的基本架构还是文件系统+通知机制。实现这些功能只是多创建了几个节点以及往这些节点写入了不同的数据而已。思维不要被这几类大致的应用框住,ZK可以实现的功能还有很多。
四、基本概念及工作原理
1.角色
zk的角色有如下几种:
1、 领导者(leader):领导者负责进行投票的发起和决议,更新系统状态;
2、 学习者(learner):学习者又分为两种:;
1、 跟随者(follower):用于接收客户请求并向客户端返回结果,在选主过程中参与投票;
2、 观察者(observer):可以接收客户端连接,将写请求转发给leader节点但observer不参与投票过程,只同步leader状态observer的目的是为了扩展系统,提高读取速度;
3、 客户端(client):请求发起方;
模型图如下:
2.工作原理
zk的核心即原子广播,这个机制保证了各个server之间的同步。实现这个机制的协议是Zab协议,其有两种模式:
1、 恢复模式(选主模式):当服务启动或者在领导者崩溃后,Zab将进入恢复模式,大多数server和leader完成了状态同步后结束恢复模式;
2、 广播模式(同步模式):当领导者被选举出来后,进入广播模式完成状态同步;
为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上 了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个 新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。
每个Server在工作过程中有三种状态:
- LOOKING:当前Server不知道leader是谁,正在搜寻;
- LEADING:当前Server即为选举出来的leader;
- FOLLOWING:leader已经选举出来,当前Server与之同步。
2.1.选主流程
当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的Server都恢复到一个正确的状态。Zk的选举算法有两种:一种是基于basic paxos实现的,另外一种是基于fast paxos算法实现的。系统默认的选举算法为fast paxos。其都需要半数通过。
先介绍basic paxos流程:
1、 选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server;
2、 选举线程首先向所有Server发起一次询问(包括自己);
3、 选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致),然后获取对方的id(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中;
4、 收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server;
5、 线程将当前zxid最大的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得n/2+1的Server票数,设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置自己的状态,否则,继续这个过程,直到leader被选举出来;
fast paxos流程是在选举过程和basic paxos大致流程差不多,只是在选举过程中,某Server首先向所有Server提议自己要成为leader,当其它Server收到提议以后,解决epoch和zxid的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,最后一定能选举出Leader。
举一个例子:
1、 A提案说,我要选自己,B你同意吗?C你同意吗?B说,我同意选A;C说,我同意选A(注意,这里超过半数了,其实在现实世界选举已经成功了但是计算机世界是很严格,另外要理解算法,要继续模拟下去);
2、 接着B提案说,我要选自己,A你同意吗;A说,我已经超半数同意当选,你的提案无效;C说,A已经超半数同意当选,B提案无效;
3、 接着C提案说,我要选自己,A你同意吗;A说,我已经超半数同意当选,你的提案无效;B说,A已经超半数同意当选,C的提案无效;
4、 选举已经产生了Leader,后面的都是follower,只能服从Leader的命令而且这里还有个小细节,就是其实谁先启动谁当头;
既然zk的要求是必须要半数通过才可以选举出leader,因此我们模拟一下server数量:
- 3台机群:3/2=1,因此过半必须大于1,得票数至少需要有2票,集群最多允许挂掉一台;
- 4台机群:4/2=2,因此过半必须大于2,得票数至少需要有3票,集群最多允许挂掉一台;
- 5台机群:5/2=2,因此过半必须大于2,得票数至少需要有3票,集群最多允许挂掉两台;
- 6台机群:6/2=3,因此过半必须大于3,得票数至少需要有4票,集群最多允许挂掉两台。
- ......
根据上面的情况一直往下类推的话,集群抗灾性价比最高的一定是奇数,因为奇数集群比多一台的偶数集群抗灾性能一致。
2.2.同步流程
当通过选主流程投票出leader后,zk将会进入同步流程:
1、 leader等待server连接;
2、 Follower连接leader,将最大的zxid发送给leader;
3、 Leader根据follower的zxid确定同步点;
4、 完成同步后通知follower已经成为following状态;
5、 Follower收到uptodate消息后,又可以重新接受client的请求进行服务了;
2.3.主要功能
leader主要有三个功能:
1、 恢复数据;
2、 维持与Learner的心跳,接收Learner请求并判断Learner的请求消息类型;
3、 Learner的消息类型主要有PING消息、REQUEST消息、ACK消息、REVALIDATE消息,根据不同的消息类型,进行不同的处理;
leader的消息类型有以下四种:
1、 PING消息是指Learner的心跳信息;
2、 REQUEST消息是Follower发送的提议信息,包括写请求及同步请求;
3、 ACK消息是Follower的对提议的回复,超过半数的Follower通过,则commit该提议;
4、 REVALIDATE消息是用来延长SESSION有效时间;
follower主要有四个功能:
1、 向Leader发送请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息);
2、 接收Leader消息并进行处理;
3、 接收Client的请求,如果为写请求,发送给Leader进行投票;
4、 返回Client结果;
follower的消息类型有以下六种:
1、 PING消息:心跳消息;
2、 PROPOSAL消息:Leader发起的提案,要求Follower投票;
3、 COMMIT消息:服务器端最新一次提案的信息;
4、 UPTODATE消息:表明同步完成;
5、 REVALIDATE消息:根据Leader的REVALIDATE结果,关闭待revalidate的session还是允许其接受消息;
6、 SYNC消息:返回SYNC结果到客户端,这个消息最初由客户端发起,用来强制得到最新的更新;
2.4.client和follower之间的通信
为了使客户端具有较高的吞吐量,Client与Follower之间采用NIO的通信方式。当client需要与Zookeeper service打交道时,首先读取配置文件确定集群内的所有server列表,按照一定的load balance算法选取一个Follower作为一个通信目标。这样client和Follower之间就有了一条由NIO模式构成的通信通道。这条通道会一直保持到client关闭session或者因为client或Follower任一方因某种原因异常中断通信连接。正常情况下, client与Follower在没有请求发起的时候都有心跳检测。