简介
ZooKeeper由雅虎研究院开发,是Google Chubby的开源实现,后来托管到Apache,于2010年11月正式成为Apache的顶级项目。
ZooKeeper是一个经典的分布式数据一致性解决方案,致力于为分布式应用提供一个高性能、高可用,且具有严格顺序访问控制能力的分布式协调服务。
分布式应用程序可以基于ZooKeeper实现数据发布与订阅、负载均衡、命名服务、分布式协调与通知、集群管理、Leader选举、分布式锁、分布式队列等功能。
数据结构
zookeeper数据模型的结构与Unix文件系统很类似,整体上可以看做一颗树,每个节点称为ZNode。zookeeper为了保证高吞吐量和低延迟,把整个树状结构的目录都放在了内存中,由于内存大小的限制,每一个ZNode默认最多能够存储1MB的数据,每个ZNode都可以通过其路径唯一标识。
节点类型
zookeeper的节点类型分为一下两种:
持久(Persistent)
客户端和服务器断开连接后,该节点不删除,直到有删除操作来主动清除这个节点。
持久化目录节点
客户端与zookeeper断开连接后,该节点依然存在
持久化顺序编号目录节点
客户端与zookeeper断开连接后,该节点依然存在,zookeeper给该节点名称进行顺序编号
创建ZNode时设置顺序标识,ZNode名称后会附加一个值,顺序号是一个单调递增的计数器,用于记录每个节点创建的先后顺序,由父节点维护
短暂(Ephemeral)
客户端和服务器断开连接后,该节点自己删除。临时znode在会话退出时会自动删除,所以不能在临时节点上创建子节点。另外临时节点属于某会话,但所有客户端都可以查看、引用它。
需要注意的是,当客户端会话失效后,所产生的临时节点并不是一下子就消失,而是要过段时间才会消失(大概10S左右)。
临时目录节点
客户端和服务器断开连接后,该节点被删除
临时顺序编号目录节点
客户端与zookeeper断开连接后,该节点被删除,zookeeper给该节点名称进行顺序编号
应用场景
- 统一命名服务:在分布式的环境下,通常一个应用被部署在多台机器上,这些IP难以被记住,而zookeeper可以将这些IP进行统一命名一个域名。
- 统一配置管理:在分布式的环境下,配置文件的同步是非常常见的。一般来说,会要求一个集群下的所有节点的配置信息都是一致的,并且进行修改之后,希望能够快速地同步到各个节点上。
- 统一集群管理:在分布式的环境下,实时掌握每个节点的状态是必要的,可以根据节点实时状态做出一些调整。可以将节点信息写入zookeeper上的一个ZNode上,然后监听这个ZNode获取它的实时状态变化。比如:dubbo的注册中心默认就是zookeeper
- 软负载均衡:在zookeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求
特点
- zookeeper的集群由一个领导者(Leader),多个跟随者(Follower)组成
- 集群中只要有半数以上节点存活,zookeeper集群就可以正常工作,所以zookeeper适合安装奇数台服务器
- 全局数据一致性:每个server保存一份相同的数据副本,Client无论连到哪个server,拿到的数据都是一样的
- 更新请求顺序执行,来自同一个Client的更新请求按其发送顺序依次执行
- 数据更新原子性,要么全部更新成功,要么全部更新失败
- 实时性,在一定时间范围内,Client能读到最新数据
选举机制
理解几个概念
- myid:唯一标识自己的ID
- SID:服务器ID,用来唯一标识一台zookeeper集群中的机器,每台机器不能重名
- ZXID:事务ID,ZXID是一个事务ID,用来标识一次服务器状态的变更。在某一时刻,集群中的每台机器的ZXID值不一定完全一直,这和zookeeper服务器对于客户端的更新请求的处理逻辑有关
- Epoch:每个Leader任期的代号。没有Leader时同一轮投票过程中的逻辑时钟值是相同的,每投完一轮这个数据就会增加
选举状态
- LOOKING: 竞选状态
- FOLLOWING: 随从状态,同步 leader 状态,参与投票
- OBSERVING: 观察状态,同步 leader 状态,不参与投票
- LEADING: 领导者状态
什么时候会进行选举?
- zookeeper集群第一次启动的时候
- zookeeper集群第一次选举完毕,但是发生意外导致服务器在运行期间无法和Leader保持连接
第一次启动
假设一个zookeeper中的集群中有5台服务器:
- 第一台服务器启动,会发起一次选举,投给自己一票。发现自己的票数少于机器数的一半,选举无法完成,则服务器1状态保持为Looking。
- 第二台服务器启动,再发起一次选举,服务器1和2均投给自己一票并且会交换自己的选票信息,此时服务器1发现服务器2的myid比自己的大,更改自己的选票,选服务器2,此时票数为服务器1:0,服务器2:2,但是最高的票数少于机器数的一半,选举无法完成,则服务器1和2状态均保持为Looking。
- 第三台服务器启动,再发起一次选举,服务器1和2和3均投给自己一票并且会交换自己的选票信息,此时服务器1和2发现服务器3的myid比自己的大,都更改自己的选票,选服务器3,此时票数为服务器1:0,服务器2:0,服务器3:3,此时最高的票数大于机器数的一半,选举完成,服务器1和2状态更改为Follower,服务器3状态为Leader。
- 第四台服务器启动,再发起一次选举,此时由于服务器1、2和3的状态不是Looking状态,不会更改选票信息。交换选票信息,此时服务器1:0,服务器2:0,服务器3:3,服务器4:1,此时服务器4少数服从多数,更改选票信息为服务器3,更改自己的状态为Follower
- 第四台服务器启动,再发起一次选举,此时由于服务器1、2、3和4的状态不是Looking状态,不会更改选票信息。交换选票信息,此时服务器1:0,服务器2:0,服务器3:4,服务器4:0,服务器5:1;此时服务器5少数服从多数,更改选票信息为服务器3,更改自己的状态为Follower。
非第一次启动
此时发生意外,导致服务器在运行期间无法和Leader保持连接,此时会进行重新选举,会分两种情况:
Leader在集群中存在
机器试图去选举Leader的时候,会被告知当前集群中Leader的信息,对于该机器来说,只需要去和Leader机器进行建立连接,同步状态即可
Leader不在集群中
此时选举的规则顺序为:
- EPOCH大的直接胜出
- EPOCH相同,事务ID大的胜出
- EPOCH相同,事务ID都相同,服务器ID大的胜出
监听器原理
监听器原理详解:
- 首先有一个main()线程
- 在main线程中创建zookeeper客户端,这是就会创建两个线程,一个负责网络连接通信(connect),一个负责监听(listenr)
- 通过connect线程将注册的监听时间发送给zookeeper
- 在zookeeper的注册监听器列表中将注册的监听时间添加到列表中
- zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程
- listener线程内部调用了process()方法
常见的监听:
- 监听节点数据的变化:get path [watch]
- 监听子节点增减的变化:ls path [watch]
注意
zookeeper的监听事件只会监听一次,如需再次监听,需要继续添加监听请求。
写数据原理
向Leader节点发送写数据的请求
步骤:
- 首先客户端向Leader节点发起一个写的请求
- server写完之后,Follower1进行写的同步
- Follower1写完后,向server返回确认自己已经写完
- 此时由于集群中有3台,已经有超过一半的机器写完了,就可以返回给client端,表示已完成写请求
- 然后服务端再让Follower2同步写的数据
- Follower2写完后,向server返回确认自己已经写完
向Follower节点发送写数据的请求
步骤:
- 首先客户端向Follower1节点发起一个写的请求
- 由于Follower节点没有写权限,于是转给有写权限的Leader,有写的请求
- server写完之后,Follower1进行写的同步
- Follower1写完后,向server返回确认自己已经写完
- 此时由于集群中有3台,已经有超过一半的机器写完了,此时返回给Follower1(因为client是向Follower1发起的请求),表示操作已经完成了
- Follower1就可以返回给client端,表示已完成写请求
- 然后服务端再让Follower2同步写的数据
- Follower2写完后,向server返回确认自己已经写完
分布式锁
分布式锁的由来
在以往单机应用中,我们可以通过synchronized或者ReentrantLcok等常用锁进行对资源访问时线程间的互斥。但在分布式系统中,由于系统中的进程、线程在不同的机器上,而synchronized或者ReentrantLcok等常用锁只对属于自己的JVM中的线程有效,这就会导致这些原来单机部署情况下的并发控制锁策略失效,这时候,我们就需要一个方式来解决这种跨机器的进程对同一资源访问时的互斥问题。
什么是分布式锁?
当多个不在同一个系统中的进程,对同一资源进行访问时,就需要分布式锁来对这些进程的访问顺序进行控制。
分布式锁的特点
- 互斥性:不管任何时候,只能有一个客户端获取到锁,不能出现两个客户端同时获取到锁的情况
- 安全性:锁的释放只能被锁获取的客户端执行,不能被其他客户端执行
- 不死锁:锁当前的拥有客户端,如果突然出现问题,不会出现一直持有锁,导致其他客户端获取不到锁的现象
- 可重入:对于同一线程,可以多次重复加锁
zookeeper分布式锁
zookeeper中的临时顺序节点,让zookeeper能够轻松的实现分布式锁。
原理:
- 服务端接收到请求后,在/locks节点(持久节点)下创建一个临时顺序节点
- 判断自己是不是当前/locks节点下顺序最小的点:是,则获取到锁;不是,则监听前一个节点
- 获取到锁之后,进行相应的业务处理;然后将删除节点,释放锁,而在该节点之后的节点将会收到前面的锁释放了的通知,重复第二步的判断
zookeeper分布式锁的优缺点
优点
- zookeeper分布式锁能有效解决分布式问题,不可重入问题,使用起来较为简单
缺点
- zookeeper分布式锁的性能不高。zookeeper实现分布式锁主要通过创建和销毁临时节点来实现的,而创建和销毁临时节点只能通过Leader节点来进行,然后Leader节点再将数据同步到其他Follower上,这样频繁地网络通信会带来非常差的性能
实现分布式锁的方案
目前比较流行的有两种:
- 基于zookeeper的分布式锁,比较适用于高可用,并且并发量不大的场景
- 基于Redis的分布式锁,适用于并发量大、性能要求高、可靠性问题可以通过其他方案去弥补的场景
两种方案没有谁更好之说,只有谁更适合,依据场景来做选择。
Paxos算法
什么是Paxos算法?
paxos算法是一种基于消息传递且具有高度容错性的一致性算法
能解决什么问题?
如何快速正确的在一个分布式系统中对某个数值达成一致,并且保证不论发生什么异常,如机器宕机,网络异常(出现丢包、重复),都不会破坏整个系统的一致性
算法描述
在PAXOS系统中,将所有节点划分成了Proposer(提议者)、Accept(接受者,即有投票权) 、Learner(学习者,弃权的,看戏的,你们说怎么样就怎么样),并且每个节点可以身兼数职。
一个完整的Paxos算法流程分为了三个阶段:
- Prepare准备阶段:
- 一个或者多个Proposer向多个Accept发出Propose请求Promise(承诺),即发起一个提议
- Acceptor针对收到的Propose请求进行Promise(承诺),即投票
- Accept接受阶段
- Proposer收到多数Accept承诺的Promise后,向Acceptor发出Propose请求,即发出票数最高的那个提议
- Acceptor针对收到的Propose请求进行Accept处理
- Leaner学习阶段
- Proposer将形成的决议发送给所有的Learner
各个阶段的详细流程:
- Prepare:Proposer生成全局唯一且递增的Proposeal ID,向所有Acceptor发送Propose请求,这里无需携带任何填案内容,只需要携带Proposeal ID即可。
- Promise:Acceptor收到Propose请求后,做出“两个承诺,一个应答”:
- 不在接收Proposeal ID小于等于当前请求的Propose请求
- 不在接收Proposeal ID小于当前请求的Accept请求
- 不违背以前做出的承诺下,回复已经Accept过的提案中Proposeal ID最大的提案的value,没有则返回空值
- Propose:Proposer收到多数的Accept的Promise应答后,从应答中选择Proposeal ID最大的提案的value,做为本次要发起的提案。如果所有的value都是空值,则可以自己随意决定提案value,然后携带当前Proposeal ID,向所有的Acceptor发起Propose请求案。
- Accept:Acceptor收到Propose请求后,在不违背自己之前做出的承诺下,接受并持久化当前Proposeal ID和提案value
- Learner:Propose收到多数Acceptor的Accept后,决议形成,将形成的决议发送给所有Learner
ZAB协议
什么是ZAB协议
ZAB协议全称为:Zookeeper Atomic Broadcast,Zookeeper原子广播协议。
ZAB协议借鉴了Paxos算法,是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的一致性协议,ZooKeeper 基于ZAB协议实现了一种主从模式的系统架构来保持集群中各个副本之间的数据一致性。
zookeeper设计为只有一个Leader负责处理外部的写事务请求,然后Leader将数据同步到其他Follower节点。
ZAB协议内容
ZAB协议包含两种基本模式:消息广播和崩溃恢复。
消息广播
过程:
- 客户端发起一个写操作请求
- Leader服务器将客户端的请求转化为事务Proposal提案,同时为每个Proposal分配一个全局的ID,即ZXID
- Leader服务器为每个Follower服务器分配一个单独的队列,然后将需要广播的Proposal提案放到队列中,根据FIFO原则进行消息发送
- Follower接收到Proposal后,先将其以事务日志的方式写入本地磁盘,写入成功后向Leader反馈一个ACK响应消息
- Leader接收到超过半数的Follower的ACK响应消息后,会认为消息发送成功了,可以发送Commit消息
- Leader向所有Follower广播Commit消息,同时自身也会完成事务提交。Follower接收到Commit消息后,会将上一条事务提交
- Zookeeper采用ZAB协议的核心,就是只要有一台服务器提交了Proposal,就要确保所有服务器最终都能正确提交Proposal
ZAB协议对事务的请求处理过程类似一个二阶段提交:
- 广播事务阶段
- 广播提交阶段
但是在这个过程中,有可能因为Leader宕机导致数据不一致,如:
- Leader发起一个事务Proposal后就宕机了,Follower都没有Proposal
- Leader收到半数的ACK宕机,没有来得及向Follower发送Commit
崩溃恢复
一旦Leader服务器出现崩溃或者由于网络原因导致Leader服务器失去了与过半Follower的联系,就会进入崩溃恢复模式。
ZAB协议崩溃恢复要求满足以下两个要求:
- 确保已经被Leader提交的提案Proposal,必须最终被所有的Follower服务器提交(即已经提交的Proposal,Follower必须执行)
- 确保丢弃已经被Leader提出的,但是没有被提交的Proposal(即Leader服务器已经收到Client的请求了,但是还没向FIFO队列中发送)
崩溃恢复主要包括了两部分:Leader选举和数据恢复。
根据上面所说的要求,ZAB协议需要保证选举出来的Leader需要满足以下条件:
- 新选举出来的Leader不能包含未提交的Proposal,即新Leader必须都是已经提交了的Proposal的Follower服务器节点
- 新选举的Leader节点中含有最大的ZXID,这样可以避免Leader服务器检查Proposal的提交和丢弃工作,保证在Leader中是最新的事务
数据同步过程:
- 完成Leader选举后,在正式开始工作之前(即接收事务请求,然后提出新的Proposal),Leader服务器会首先确认事务日志中的所有的Proposal是否已经被集群中过半的服务器Commit
- Leader服务器需要确保所有的Follower服务器能够接收到每一条事务的Proposal,并且能够将所有已经提交的事务Proposal应用到内存数据中。等到Follower将所有尚未同步的事务Proposal都从Leader服务器上同步过,并且应用到内存数据中以后,Leader才会把该Follower加入到真正可用的Follower列表中。
CAP定理
什么是CAP定理
- Consistency:数据一致性。在分布式环境中,一致性是指在多个副本之间是否能够保持数据一致性的特性。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统中的数据仍然保持一致
- Availability:可用性。指系统提供的服务一直处于可用的状态,对于用户来说每个操作请求总是能在有限时间内返回结果
- Partition Tolerance:分区容错性。分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性或可用性的服务,除非整个网络环境都发生了故障。例如,有5台机器,现在有2台出故障了,依然还有3台机器能够提供服务。
为什么只能是CP或者AP
CAP定理告诉我们,一个分布式环境只能满足其中的两种,CP或者AP,分区容错性是必须满足的。
我们来举个例子:
比如有个分布式系统,分别部署在两个集群上,两个集群上分别只有A、B机器,此时两台机器网络是通的,数据也保持一致。如果突然间,A和B之间网络通信断开了,有个更改数据请求将D0改成D1,打到A上面,A将数据写到了它集群上面的数据库,此时,A集群上数据库已经将D0改成了D1,而由于和B之间网络通信中断,B此时的数据依然是D0。如果有请求要查这个数据,B是为了保证可用性给出旧数据呢?还是牺牲可用性,待网络通信正常后,给出新数据,保证数据一致性。
zookeeper为什么满足的是CP定理
- 进行Leader选举的时候,集群处于不可用的状态
- 在极端情况下,zookeeper可能会丢弃一些请求,消费者程序需要重新请求才能获取结果,所以无法保证服务的可用性