注:内容很长,如有需要,请耐心阅读。还有分享计划。
Zookeeper 介绍
ZooKeeper是以Fast Paxos算法为基础的,Paxos 算法存在活锁的问题,即当有多个proposer交错提交时,有可能互相排斥导致没有一个proposer能提交成功,而Fast Paxos作了一些优化,通过选举产生一个leader (领导者),只有leader才能提交proposer,具体算法可见Fast Paxos。因此,要想弄懂ZooKeeper首先得对Fast Paxos有所了解。
ZooKeeper的基本运转流程:
1、选举Leader。
2、同步数据。
3、选举Leader过程中算法有很多,但要达到的选举标准是一致的。
4、Leader要具有最高的执行ID,类似root权限。
5、集群中大多数的机器得到响应并接受选出的Leader。
- 工作机制
一致性协议
- CAP
CA - 单点集群,满足一致性,可用性,通常在可拓展性上不太强大
CP - 满足一致性,分区容错性的系统,通常性能不是特别的高
AP - 满足可用性,分区容错性,通过对数据一致性要求低一些。
所以,分布式系统考虑到集群的拓展,只能选择CP 或者 AP 。
zookeeper 保证CP
服务注册功能对可用性的要求高于一致性。
但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点回重新进行leader选举,问题在于选举leader的时间太长,30~120s且选举期间整个zk集群都是不可用的。
Eureka 保证AP
在设计时就先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。
- Paxos
是能够基于一大堆完全不可靠的网络条件下却能可靠确定地实现共识一致性的算法。
也就是说:它允许一组不一定可靠的处理器(服务器)在某些条件得到满足情况下就能达成确定的安全的共识,如果条件不能满足也确保这组处理器(服务器)保持一致。
Paxos 算法适用的几种情况:一台机器中多个进程/线程达成数据一致;分布式文件系统或者分布式数据库中多客户端并发读写数据;分布式存储中多个副本响应读写请求的一致性。
- ZAB协议(Zookeeper)
ZAB 协议全称:Zookeeper Atomic Broadcast(Zookeeper 原子广播协议)。
ZAB协议主要包含两部分内容:
1、崩溃的数据恢复
2、消息的原子广播
当整个集群正在启动时,或者当leader节点出现网络中断、崩溃等情况时,ZAB协议就会进入恢复模式并选举产生新的leader。
当leader服务器选举出来后,并且集群中有过半的机器和该leader节点完成数据同步后,ZAB协议就会退出恢复模式。
当集群中已经有过半的Follower节点完成了和Leader状态同步以后,那么整个集群就进入了消息广播模式。
在Leader节点正常工作时,启动一台新的服务器加入到集群,那这个服务器会直接进入数据恢复模式,和Leader节点进行数据同步。同步完成后即可正常对外提供非事务请求的处理。
同步指的是数据同步,用来保证集群中过半的机器能够和leader服务器的数据状态保持一致。
⑴消息的原子广播
是一个简化版的2PC提交过程。
Leader接收到消息请求后,将消息赋予一个全局唯一的64位自增id,叫: ZXID 。 ZXID由两部分组成:低32位表示计数器(counter)和高32位的纪元号(epoch)。epoch为当前leader在成为leader的时候生成的,且保证会比前一个leader的epoch大(大1) 。
Leader为每个Follower准备了一个FIFO队列(通过TCP协议来实现,以保证执行事务有序这一个特点)将带有ZXID的消息作为一个提案(proposal)分发给所有的 Follower。
当Follower接收到proposal,先把proposal写到磁盘,写入成功以后再向Leader回复一个ack。
当Leader接收到合法数量(超过半数节点)的ack后,Leader就会向这些Follower发送commit命令,同时会在本地执行该消息。
当Follower收到消息的commit命令以后,会提交该消息。
⑵崩溃的数据恢复
在ZAB协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的Leader。并且要保证:不能多数据、不能丢数据。
ZAB协议需要满足上面两种情况,就必须要设计一个Leader选举算法,能够确保已经被Leader提交的事务proposal能够提交、同时丢弃已经被跳过的事务proposal。
恢复模式大致可以分为四个阶段
1、选举
当Leader崩溃后,集群进入选举阶段,开始选举出潜在的新Leader(一般为集群中拥有最大ZXID的节点,因为这样就可以保证这个新选举出来的Leader一定具有已经提交的提案,提案被commit之前必须有超过半数的Follower ack,即必须有超过半数节点的服务器的事务日志上有该提案的proposal,因此只要有合法数量的节点正常工作,就必然有一个节点保存了所有被commit消息的proposal状态)。
2、发现
进入发现阶段,Follower与潜在的新Leader进行沟通,如果发现超过法定人数的Follower同意,则潜在的新Leader将epoch加1,进入新的纪元(epoch)。新的Leader产生。
3、同步
集群间进行数据同步,保证集群中各个节点的事务一致。
4、广播
集群恢复到广播模式,开始接受客户端的写请求。
当Leader产生某个proposal之后但在发出消息之前宕机,即只有老Leader自己有这个proposal,当老的leader重启后(此时左右Follower),新的Leader必须保证老的Leader必须丢弃这个proposal.(新的Leader会通知上线后的老Leader截断其epoch对应的最后一个commit的位置)。
基础知识
- 集群角色
Zookeeper集群中主要有三种角色:Leader、Follower、Observer。Leader负责接收客户端的写请求,并将该请求以事务的方式提交给Follower执行。Follower负责接收读请求、参与Leader的选举和过半写成功策略。Observer也负责接收读请求,不需要参与Leader的选举和过半写成功,该角色设计的主要目标是用来在不影响写性能的前提下扩展Zookeeper的读性能。
- 会话
会话指的是客户端和Zookeeper集群建立的连接,假设与客户端相连的服务器宕机,在没有超过sessionTimeout参数设置的前提下能够重新连接上另一台服务器,则之前的会话有效。
- 数据节点
这里的数据节点除了机器节点之外,指的还是Zookeeper中的ZNode,ZNode以文件目录的形式进行组织。主要有两种形式:持久节点和临时节点。持久节点一旦创建除非手动移除否则不会删除,临时节点和会话的生命周期有关,会话启动创建的临时节点会在会话断开时自动删除。Zookeeper还可以为节点设置SEQUENTIAL属性,被设置了该属性的节点在创建时会自动在节点名后面加一个数字,该数字是由父节点维护的。
zookeeper数据模型的结构与unix文件系统类似,整体上可以看作是一棵树,每个节点称作一个znode
znode的数据模型:
zookeeper的Stat结构体
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]
[zk: localhost:2181(CONNECTED) 2] create /leo v1.0
Created /leo
[zk: localhost:2181(CONNECTED) 3] ls /
[leo, zookeeper]
[zk: localhost:2181(CONNECTED) 4] get /leo
v1.0
[zk: localhost:2181(CONNECTED) 6] get -s /leo
v1.0
# 引起这个znode创建的zxid,创建节点的事物的zxid
# 每次修改zookeeper状态都会收到一个zxid形式的时间戳,也就是zookeeper事物id
# 如果zxid1 < zxid2,那么zxid1在zxid2之前发生
cZxid = 0x6
ctime = Mon Sep 16 14:43:30 CST 2019 #被创建时间
mZxid = 0x6 #znode最后更新的zxid
mtime = Mon Sep 16 14:43:30 CST 2019 #znode最后更新的时间
pZxid = 0x6 #最后更新的子节点zxid
cversion = 0 #znode子节点变化号,znode子节点修改次数
dataVersion = 0 #znode数据变化号
aclVersion = 0 #znode访问控制列表的变化号
ephemeralOwner = 0x0 #如果是临时节点,这个是znode拥有者的session id,如果不是临时节点则是0
dataLength = 4 # znode数据长度 length("v1.0")
numChildren = 0 # znode子节点个数
[zk: localhost:2181(CONNECTED) 7] get -w /leo
v1.0
Zookeeper应用场景
- 数据发布与订阅
发布与订阅模型,即所谓的配置中心,顾名思义就是发布者将数据发布到ZK节点上,供订阅者动态获取数据,实现配置信息的集中式管理和动态更新。例如全局的配置信息,服务式服务框架的服务地址列表等就非常适合使用。
应用中用到的一些配置信息放到ZK上进行集中管理。
这类场景通常是这样:应用在启动的时候会主动来获取一次配置,同时,在节点上注册一个Watcher,这样一来,以后每次配置有更新的时候,都会实时通知到订阅的客户端,从来达到获取最新配置信息的目的。 分布式搜索服务中,索引的元信息和服务器集群机器的节点状态存放在ZK的一些指定节点,供各个客户端订阅使用。
- 负载均衡
指软负载均衡。在分布式环境中,为了保证高可用性,通常同一个应用或同一个服务的提供方都会部署多份,达到对等服务。而消费者就须要在这些对等的服务器中选择一个来执行相关的业务逻辑,其中比较典型的是消息中间件中的生产者,消费者负载均衡。
消息中间件中发布者和订阅者的负载均衡,linkedin开源的KafkaMQ和阿里开源的metaq都是通过zookeeper来做到生产者、消费者的负载均衡。
以metaq为例: 生产者负载均衡:metaq发送消息的时候,生产者在发送消息的时候必须选择一台broker上的一个分区来发送消息,因此metaq在运行过程中,会把所有broker和对应的分区信息全部注册到ZK指定节点上,默认的策略是一个依次轮询的过程,生产者在通过ZK获取分区列表之后,会按照brokerId和partition的顺序排列组织成一个有序的分区列表,发送的时候按照从头到尾循环往复的方式选择一个分区来发送消息。
消费负载均衡:
在消费过程中,一个消费者会消费一个或多个分区中的消息,但是一个分区只会由一个消费者来消费。MetaQ的消费策略是:
每个分区针对同一个group只挂载一个消费者。 如果同一个group的消费者数目大于分区数目,则多出来的消费者将不参与消费。
* 如果同一个group的消费者数目小于分区数目,则有部分消费者需要额外承担消费任务。
在某个消费者故障或者重启等情况下,其他消费者会感知到这一变化(通过 zookeeper watch消费者列表),然后重新进行负载均衡,保证所有的分区都有消费者进行消费。
- 命名服务
是分布式系统中比较常见的一类场景。
在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。被命名的实体通常可以是集群中的机器,提供的服务地址,远程对象等等——这些我们都可以统称他们为名字(Name)。其中较为常见的就是一些分布式服务框架中的服务地址列表。通过调用ZK提供的创建节点的API,能够很容易创建一个全局唯一的path,这个path就可以作为一个名称。
阿里巴巴集团开源的分布式服务框架Dubbo中使用ZooKeeper来作为其命名服务,维护全局的服务地址列表。
在Dubbo实现中: 服务提供者在启动的时候,向ZK上的指定节点:
/dubbo/${serviceName}/providers目录下写入自己的URL地址,这个操作就完成了服务的发布。 服务消费者启动的时候,订阅/dubbo/${serviceName}/providers目录下的提供者URL地址, 并向/dubbo/${serviceName} /consumers目录下写入自己的URL地址。
注意,所有向ZK上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。
- 分布式通知/协调
ZooKeeper中特有watcher注册与异步通知机制,能够很好的实现分布式环境下不同系统之间的通知与协调,实现对数据变更的实时处理。使用方法通常是不同系统都对ZK上同一个znode进行注册,监听znode的变化(包括znode本身内容及子节点的),其中一个系统update了znode,那么另一个系统能够收到通知,并作出相应处理
另一种心跳检测机制:检测系统和被检测系统之间并不直接关联起来,而是通过zk上某个节点关联,大大减少系统耦合。 另一种系统调度模式:某系统有控制台和推送系统两部分组成,控制台的职责是控制推送系统进行相应的推送工作。管理人员在控制台作的一些操作,实际上是修改了ZK上某些节点的状态,而ZK就把这些变化通知给他们注册Watcher的客户端,即推送系统,于是,作出相应的推送任务。
* 另一种工作汇报模式:一些类似于任务分发系统,子任务启动后,到zk来注册一个临时节点,并且定时将自己的进度进行汇报(将进度写回这个临时节点),这样任务管理者就能够实时知道任务进度。
总之,使用zookeeper来进行分布式通知和协调能够大大降低系统之间的耦合
- 集群管理与Master选举
集群机器监控:这通常用于那种对集群中机器状态,机器在线率有较高要求的场景,能够快速对集群中机器变化作出响应。这样的场景中,往往有一个监控系统,实时检测集群机器是否存活。过去的做法通常是:监控系统通过某种手段(比如ping)定时检测每个机器,或者每个机器自己定时向监控系统汇报“我还活着”。 这种做法可行,但是存在两个比较明显的问题:
1. 集群中机器有变动的时候,牵连修改的东西比较多。
2. 有一定的延时。
利用ZooKeeper有两个特性,就可以实时另一种集群机器存活性监控系统:
1. 客户端在节点 x 上注册一个Watcher,那么如果 x?的子节点变化了,会通知该客户端。
2. 创建EPHEMERAL类型的节点,一旦客户端和服务器的会话结束或过期,那么该节点就会消失。
- 分布式锁
分布式锁,这个主要得益于ZooKeeper为我们保证了数据的强一致性。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
所谓保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把zk上的一个znode看作是一把锁,通过create znode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。 控制时序,就是所有视图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。做法和上面基本类似,只是这里 /distribute_lock 已经预先存在,客户端在它下面创建临时有序节点(这个可以通过节点的属性控制:CreateMode.EPHEMERAL_SEQUENTIAL来指定)。Zk的父节点(/distribute_lock)维持一份sequence,保证子节点创建的时序性,从而也形成了每个客户端的全局时序。
- 分布式队列
队列方面,简单地讲有两种,一种是常规的先进先出队列,另一种是要等到队列成员聚齐之后的才统一按序执行。对于第一种先进先出队列,和分布式锁服务中的控制时序场景基本原理一致,这里不再赘述。
第二种队列其实是在FIFO队列的基础上作了一个增强。通常可以在 /queue 这个znode下预先建立一个/queue/num 节点,并且赋值为n(或者直接给/queue赋值n),表示队列大小,之后每次有队列成员加入后,就判断下是否已经到达队列大小,决定是否可以开始执行了。这种用法的典型场景是,分布式环境中,一个大任务Task A,需要在很多子任务完成(或条件就绪)情况下才能进行。这个时候,凡是其中一个子任务完成(就绪),那么就去 /taskList 下建立自己的临时时序节点(CreateMode.EPHEMERAL_SEQUENTIAL),当 /taskList 发现自己下面的子节点满足指定个数,就可以进行下一步按序进行处理了。
Zookeeper监听器原理
- 要有一个 main() 线程;
- 在 main() 线程中创建 Zookeeper 客户端,这时就会创建两个线程,一个负责 网络连接通信(connet) , 一个负责监听(listener) .
- 通过 connet 线程将注册的监听事件发送给 Zookeeper.
- 在 Zookeeper 的注册监听器列表中将注册的监听事件添加到列表中 .
- Zookeeper 监听到有数据或路径变化,就会将这个消息发送到 listener 线程 .
- listener 线程内部调用了 process() 方法 .
Zookeeper安装方法
01 单机
- 下载并解压zookeeper
# cd /usr/local
# wget http://mirror.bit.edu.cn/apache/zookeeper/stable/zookeeper-3.4.12.tar.gz
# tar -zxvf zookeeper-3.4.12.tar.gz
# cd zookeeper-3.4.12
- 重命名配置文件zoo_sample.cfg
# cp conf/zoo_sample.cfg conf.cfg
- 启动zookeeper
# bin/zkServer.sh start
- 检测是否成功启动,用zookeeper客户端连接下服务端
检测是否成功启动,用zookeeper客户端连接下服务端
02 集群
三台机器上,分别建立 zk目录
机器1 zookeeper 目录为:zk
机器2 zookeeper 目录为:zk2
机器3 zookeeper 目录为:zk3
三台机器ip分别为
xx.xx.xx.1
xx.xx.xx.2
xx.xx.xx.3
- 主机名到IP地址的映射配置
直接修改/etc/hosts文件,设置主机zoo-1映射到x.x.x.1,设置主机zoo-2映射到x.x.x.2,设置主机zoo-3映射到x.x.x.3
vim /etc/hosts
x.x.x.1 zoo-1
x.x.x.2 zoo-2
x.x.x.3 zoo-3
- 修改zoo.cfg配置
vi zoo.cfg
#增加配置
server.1=zoo-1:2888:3888
server.2=zoo-2:2888:3888
server.3=zoo-3:2888:3888
- 新建myid文件并写入集群标识
在dataDir目录中,创建一个名为myid的文件,并写入机器对应的数字值,比如我们是在zk的机器上,就将该值设置为1,即集群中sever.1=zoo-1:2888:3888中server.后对应的数字。这是zookeeper用来识别是那一台集群机器的标识。
echo 1 > /opt/zk/zk/data/myid
echo 2 > /opt/zk/zk2/data/myid
echo 3 > /opt/zk/zk3/data/myid
- 查看集群状态配置结束
分别启动机器1,机器2,机器3.
集群会自动选举出一台服务器作为leader,其余服务器为follower,可以在每台服务器上使用命令查看服务器是什么类型的。
zkServer.sh status
下篇分享:
Zookeeper kafka redis 整合及环境搭建
閱讀更多 天天面試題 的文章