一、概述
Zookeeper是Apache旗下一个开源的分布式协调服务,它所提供的服务非常简单,客户端可以在其树状文件系统中创建任意节点,并且可以监听节点以及其子节点的变化。分布式应用可以基于Zookeeper提供的这些功能,实现诸如:注册中心、配置管理、集群管理、分布式锁、统一命名服务等功能。
Zookeeper这个名字怎么来的呢?
Zookeeper最早是源于雅虎研究院的一个研究小组,当时在雅虎内部很多大型系统都需要依赖一个类似的系统来进行分布式协调,但是这些系统往往都存在分布式单点问题。所以,雅虎的开发者就试图开发一个无单点问题的分布式协调框架,以便让开发者将精力更多的集中在处理业务逻辑上。
当初在雅虎公司内部很多的项目都是采用动物的名称来进行命名的,因此雅虎的工程师希望给这个项目也取一个动物的名字。时任研究院的首席科学家Raghu Ramakrishnan开玩笑地说:“再这样下去,我们这儿就要变成动物园了”。此话一出大家纷纷就表示就叫动物园管理员吧,因为各个以动物命名的分布式组件放在一起,雅虎的整个分布式系统看上去就像一个大型的动物园。而Zookeeper正好是用于进行分布式系统之间的协调,于是Zookeeper这个名字也就应运而生。
二、底层实现
Zookeeper底层是由文件系统 + 监听机制构成。
1、文件系统
Zookeeper的底层是一个文件系统,具有一个路径为/的根节点,其下可以有任意个子节点。其结构类似于一棵树,树上的每个节点被称之为Znode,Znode中可以存储大小在1MB以内的数据。
Znode类型
-
PERSISTENT
持久化目录节点
在客户端与zookeeper断开连接后,客户端创建的节点依旧存在 -
PERSISTENT_SEQUENTIAL
持久化顺序编号目录节点
在客户端与zookeeper断开连接后,客户端创建的节点依旧存在,并且zookeeper会给该节点进行顺序编号 -
EPHEMERAL
临时目录节点
在客户端与zookeeper断开连接后,客户端创建的节点被自动清除 -
EPHEMERAL_SEQUENTIAL
临时顺序编号目录节点
在客户端与zookeeper断开连接后,客户端创建的节点被自动清除,并且zookeeper会给该节点进行顺序编号
2、监听机制
客户端可以监听关心的znode,当znode发生变化(数据改变、被删除、子节点新增/删除),zookeeper会通知客户端。
监听步骤
- 客户端创建Zookeeper客户端,这时候会创建两个线程,一个负责网络通讯、一个负责监听。
- 通过connect线程,将监听事件发送给Zookeeper服务端。
- Zookeeper将这个监听事件放入到监听器列表中。
- Zookeeper监听到该节点有数据、节点变化,就会将这个消息发送给客户端。
- 客户端的监听线程接收到这个消息,并调用process()方法。
监听事件
- 监听节点被创建
- 监听节点被删除
- 监听节点其子节点发生变化
- 监听节点的节点内容发生变化
特别注意
客户端发起的对于节点的监听,只会被触发一次。如果需要多次触发,则可以在监听触发方法中再次发起对于该节点的监听。
三、应用场景
配置中心
1、将公共配置存放到Zookeeper的节点中。
2、客户端程序可以连接到Zookeeper,并对其配置节点中的内容进行读取或修改,下面是具体的流程图:
分布式锁
在程序开发中,对于单个进程中的共享资源访问可以通过synchronized关键字或Java.util.concurrent包下的Lock接口提供的lock方法来实现互斥操作。但是对于跨进程、跨主机的工项资源就无可奈何了。这时候我们就可以通过Zookeeper来实现分布式锁。
实现流程
1、首先在Zookeeper中创建一个/distributed_lock
持久化节点
2、在/distributed_lock
节点下创建自己的临时顺序节点,比如/distributed_lock/task_00000000006
3、获取/distributed_lock
节点下所有的子节点,并排序
4、判断自己创建的节点是否是最小的节点
5、如果是,则获取到锁,执行自己的业务逻辑,完成后删除这个临时节点
6、如果不是最小值,则监听上一个节点的数据变化,并阻塞自己
7、当上一个节点被删除时,再判断当前节点是否是最小的节点,如果是则进入步骤5,如果不是则进入步骤6
统一命名服务
统一命名服务主要是根据指定名称来获取资源、服务的地址。利用Znode的特点以及监听机制,可以将具体的访问ip、路径等信息存储在Znode中,当传入指定名称后,可以去Zookeeper获取对应的节点,从中拿出节点中存储的具体ip、路径,再进行资源获取、服务调用。
通过
Zookeeper还被用于统一集群管理、服务器动态上下线、软负载均衡等等,这里就不再一一赘述了。
四、集群
1、集群构成
角色 | 描述 | 功能 |
---|---|---|
Leader | 领导者,是zookeeper集群中的核心 | 负责投票的发起与决议,更新系统状态 |
Follower | 追随者 | 接收客户端请求并返回结果,在选举Leader过程中参与投票 |
Observer | 观察者 | 可以接收客户端连接,将写请求转发给Leader节点,但是Observer不参与投票过程,只是单纯的同步Leader的状态。Observer的目的是为了扩展系统,提高读取速度。 |
2、集群特点
- 一个领导者(Leader)多个跟随者(Follower)组成的集群。
- 高可用性,集群中只要有半数以上的节点存活,就能够正常对外提供服务。
- 强一致性,每个节点都存有一份相同的数据副本,Client无论连接那台Server,获取到的数据都是一致的。
- 顺序性,更新请求顺序进行,来自一个Client的更新请求按其发送顺序依次执行。
- 原子性,一次数据更新操作要么成功要么失败。
- 实时性,在一定时间范围内,Client能读取到最新的数据。
3、选举Leader
Zookeeper中的Leader作为整个集群的服务调度者,必然要确保其高可用性,因此如何在启动时或Leader故障后选举新的Leader便至关重要。Leader的选举要牵涉到以下两个概念:节点状态、事务id。
1. 节点状态
- LOOKING:寻找Leader状态,处于该状态需要进入选举流程
- LEADING:领导者状态,处于该状态的节点说明是角色已经是Leader
- FOLLOWING:跟随者状态,表示Leader已经选举出来,当前节点角色是follower
- OBSERVER:观察者状态,表明当前节点角色是observer
2. 事务id
Zookeeper中,每次造成服务器状态变化的操作被称之为事务操作,比如节点的创建、删除、节点内容变更等等。
对于每个事务操作,Zookeeper都会为其分配一个全局唯一的事务id(即ZXID),通常是一个64位的数字。每个ZXID都对应着一次事务操作,通过ZXID的顺序我们便能得知Zookeeper执行这些事务操作的全局顺序。
3. 那么Leader是如何被选举出来的呢?
我们假设有3台Zookeeper服务器组成的集群,分别是Server1、Server2、Server3。
场景1:初始化启动时选举
- Server1最先启动,发出将自己作为Leader的投票请求,请求中包含选举服务器的myid和ZXID,我们假设为(1,0)。 由于没有其他服务器响应其投票请求,因此陷入LOOKING状态。
- Server2随后启动,Server发出将自己作为Leader的投票(2,0)。此时Server1与Server2都互相收到了对方的投票,将收到的投票与自己的投票进行对比。首先对比ZXID,发现两者的ZXID都相同,在对比myid,Server2的myid要大于Server1的myid,因此Server2胜出。
- Server1更新自己的投票为(2, 0),并将投票重新发送给Server2。
- 统计投票,每次投票后,服务器都会统计投票信息,判断是否已经有过半的机器接收到相同的投票,对于Server1、Server2而言,都统计出了集群中已经有2台机器接收了(2,0)的投票,因此Server2被选举为Leader。
- 一旦Leader被确认,每个服务器都需要更新自己的状态,Server1更新为LEADING,Server2更新为FOLLOWING。
- Server3最后启动,发现已经有Leader存在,因此不在进行投票,直接将状态更新为FOLLOWING。
场景2:运行期间重新选举
- Server2作为Leader,突然宕机
- Server1、Server3将自己服务器状态更新为LOOKING,然后开始Leader选举。
- 每个Server都会发起投票请求,并且都是优先投自己作为Leader。在运行期间,每台机器的ZXID必然不同,假设Server1的ZXID是100、Server3的ZXID是101,因此Server1发起的投票便是(1,100),Server3发起的投票便是(3,101)。
- Server1、Server3接收到来自对方的投票,Server1将Server3的投票与自己的投票进行比较,发现Server3的ZXID:101是要大于自己投票的ZXID:100。因此Server3胜出。
- Server1更新自己的投票信息为(3,101),并重新发送给Server3。
- 统计投票,最终Server3的投票数超过了一半,最终Server3的状态更新为LEADING,Server1更新为FOLLOWING。