目录
1 简介
- Zookeeper是根据谷歌的论文《The Chubby Lock Service for Loosely Couple Distribute System 》所做的开源实现
- Zookeeper是Apache Hadoop的子组件之一,但是不仅仅支持Hadoop,还支持绝大部分的分布式集群
- Zookeeper是一个分布式的协调服务框架,用于解决分布式环境下的一些常见问题:集群管理、统一命名服务,信息配置管理,分布式锁等等
2 概念
2.1 zookeeper特点
- Zookeeper是一个树状结构(Znode树)
- 树状结构(Znode树)的根节点为 /
- Zookeeper的每一个节点称之为是znode节点
- 所有的znode节点都是从根节点开始计算
- 每一个znode节点都必须存储数据
- 每一个持久的znode节点都可以挂载子节点
- 每一个znode节点的路径都是唯一的。所以基于这一个特点,可以做集群的统一命名服务
- Znode树是维系在内存中的,即每一个znode节点中的数据也是维系在内存中,这样做的目的是方便快速查找
- 不能利用Zookeeper存储海量数据,原因:
- a. Znode树维系在内存中,并且多个Zookeeper存储的是相同的数据造成内存的浪费;
- b. Zookeeper是做分布式的协调服务而不是做存储服务
- Zookeeper提供了持久化机制,持久化的目录由zoo.cfg中的dataDir属性来决定
- Zookeeper会为每一次的事务(增加、删除、更新)提供一个全局的递增的事务id
2.2 zookeeper的角色
领导者(leader),负责进行投票的发起和决议,更新系统状态
» 学习者(learner),包括跟随者(follower)和观察者(observer),follower用于接受客户端请求并想客户端返回结果,在选主过程中参与投票
» Observer可以接受客户端连接,将写请求转发给leader,但observer不参加投票过程,只同步leader的状态,observer的目的是为了扩展系统,提高读取速度
» 客户端(client),请求发起方
3 特性总结
3.1 数据一致性
客户端不论连接到哪个Zookeeper节点上,展示给它都是同一个视图,即查询的数据都是一样的。这是Zookeeper最重要的性能
3.2 原子性
对于事务决议的更新,只能是成功或者失败两种可能,没有中间状态。 要么都更新成功,要么都不更新。即,要么整个集群中所有机器都成功应用了某一事务,要么都没有应用,一定不会出现集群中部分机器应用了该事务,另外一部分没有应用的情况。
3.3 可靠性
一旦Zookeeper服务端成功的应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会一直保留下来,除非有另一个事务又对其进行了改变。
3.4 实时性
Zookeeper保证客户端将在非常短的时间间隔范围内获得服务器的更新信息,或者服务器失效的信息,或者指定监听事件的变化信息。(前提条件是:网络状况良好)
3.5 顺序性
如果在一台服务器上消息a在消息b前发布,则在所有服务器上消息a都将在消息b前被发布。客户端在发起请求时,都会跟一个递增的命令号,根据这个机制,Zookeeper会确保客户端执行的顺序性。底层指的是Zxid。可以通过事务log来看。
3.6 过半性
Zookeeper集群必须有半数以上的机器存活才能正常工作。因为只有满足过半性,才能满足选举机制选出leader。因为只有过半,在做事务决议时,事务才能更新。所以一般来说,zookeeper集群的数量最好是奇数个
4 选举机制
4.1 选举机制
- 第一阶段:数据恢复阶段
每台Zookeeper服务器在启动的时候,都会从本地的数据目录中找到自己所拥有的最大事务id。 - 第二阶段:选举阶段
每一个Zookeeper的服务器都会推荐自己当leade并且提交选举协议:
a. 自己所拥有的最大事务id - Zxid
b. 自己的选举id - myid
c. 逻辑时钟值,作用是确保每一台Zookeeper服务器都会处在同一轮选举中 - 节点状态
a. Looking - 选举状态
b. follower - 追随者
c. leader - 领导者
d. observer - 观察者
4.2 PK原则
- 在选举的时候会先比较两个节点的最大事务id即Zxid。Zxid越大,则说明事务越新。Zxid较大的会胜出
- 如果Zxid一致,则比较选举id,选举id较大的胜出。
- 满足过半性:即超过一半的节点才能成为leader
4.3 过半性
- 过半选举:即只有一个节点胜过一半的节点之后才能成为leader
- 过半服务:即只有Zookeeper集群中超过一半的节点存活才能对外提供服务
- 过半操作:即只有Zookeeper集群中超过一半的节点同意才会提交请求
5 ZAB协议
5.1 概述
- ZAB(Zookeeper Atomic Broadcast)协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的原子广播协议
- ZAB协议是一种特别为ZooKeeper设计的崩溃可恢复的原子消息广播算法。这个算法是一种类2PC算法,在2PC基础上做的改进
- ZAB协议包括两种基本的模式,分别是:
a. 消息原子广播(保证数据一致性)
b. 崩溃恢复(解决2pc算法的单点问题)
5.2 消息原子广播
- 在ZooKeeper中,主要依赖ZAB协议来实现分布式数据一致性,基于该协议,ZooKeeper实现了一种主备模式的系统架构来保持集群中各副本之间数据的一致性,实现分布式数据一致性的这一过程称为消息广播(原子广播)
- ZAB协议的消息广播过程使用的是原子广播协议,类似于一个二阶段提交过程。但是相较于2PC算法,不同的是ZAB协议引入了过半性思想
- ZooKeeper使用一个单一的主进程(leader服务器)来接收并处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程(follower或observer)上去。即:所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为leader服务器,而余下的其他服务器则成为follower服务器或observer
- 具体流程:
a. 针对客户端的事务请求,leader服务器会先将该事务写到本地的log文件中
b. 然后,leader服务器会为这次请求生成对应的事务Proposal并且为这个事务Proposal分配一个全局递增的唯一的事务ID,即Zxid
c. leader服务器会为每一个follower服务器都各自分配一个单独的队列,将需要广播的事务Proposal依次放入队列中,发送给每一个follower
d. 每一个follower在收到队列之后,会从队列中依次取出事务Proposal,写道本地的事务日志中。如果写成功了,则给leader返回一个ACK消息
e. 当leader服务器接收到半数的follower的ACK相应之后,就会广播一个Commit消息给所有的follower以通知其进行事务提交,同时leader自身也进行事务提交
f. leader在收到Commit消息后完成事务提交 - 当然,在这种简化了的二阶段提交模型下,是无法处理Leader服务器崩溃退出而带来的数据不一致问题的,因此在ZAB协议中添加了另一个模式,即采用崩溃恢复模式来解决这个问题
- 整个消息广播协议是基于具有FIFO特性的TCP协议来进行网络通信的,因此能够很容易地保证消息广播过程中消息接收与发送的顺序性
5.3 崩溃恢复
- 当leader服务器出现崩溃、重启等场景,或者因为网络问题导致过半的follower不能与leader服务器保持正常通信的时候,Zookeeper集群就会进入崩溃恢复模式
- 进入崩溃恢复模式后,只要集群中存在过半的服务器能够彼此正常通信,那么就可以选举产生一个新的leader
- 每次新选举的leader会自动分配一个全局递增的编号,即epochid
- 当选举产生了新的leader服务器,同时集群中已经有过半的机器与该leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式。其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和leader服务器的数据状态保持一致
- 当集群中已经有过半的follower服务器完成了和leader服务器的状态同步,那么整个服务框架就可以进入消息广播模式了
- 当一台同样遵守ZAB协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到leader所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中。
6 脑裂
6.1 什么是脑裂?
脑裂(split-brain)就是“大脑分裂”,也就是本来一个“大脑”被拆分了两个或多个“大脑”,我们都知道,如果一个人有多个大脑,并且相互独立的话,那么会导致人体“手舞足蹈”,“不听使唤”。
脑裂通常会出现在集群环境中,比如ElasticSearch、Zookeeper集群,而这些集群环境有一个统一的特点,就是它们有一个大脑,比如ElasticSearch集群中有Master节点,Zookeeper集群中有Leader节点。
本篇文章着重来给大家讲一下Zookeeper中的脑裂问题,以及是如果解决脑裂问题的。
6.2 Zookeeper集群中的脑裂场景
对于一个集群,想要提高这个集群的可用性,通常会采用多机房部署,比如现在有一个由6台zkServer所组成的一个集群,部署在了两个机房:
正常情况下,此集群只会有一个Leader,那么如果机房之间的网络断了之后,两个机房内的zkServer还是可以相互通信的,如果不考虑过半机制,那么就会出现每个机房内部都将选出一个Leader。
这就相当于原本一个集群,被分成了两个集群,出现了两个“大脑”,这就是脑裂。
对于这种情况,我们也可以看出来,原本应该是统一的一个集群对外提供服务的,现在变成了两个集群同时对外提供服务,如果过了一会,断了的网络突然联通了,那么此时就会出现问题了,两个集群刚刚都对外提供服务了,数据该怎么合并,数据冲突怎么解决等等问题。
刚刚在说明脑裂场景时,有一个前提条件就是没有考虑过半机制,所以实际上Zookeeper集群中是不会出现脑裂问题的,而不会出现的原因就跟过半机制有关。
6.3 过半机制
在领导者选举的过程中,如果某台zkServer获得了超过半数的选票,则此zkServer就可以成为Leader了。
过半机制的源码实现其实非常简单:
public class QuorumMaj implements QuorumVerifier {
private static final Logger LOG = LoggerFactory.getLogger(QuorumMaj.class);
int half;
// n表示集群中zkServer的个数(准确的说是参与者的个数,参与者不包括观察者节点)
public QuorumMaj(int n){
this.half = n/2;
}
// 验证是否符合过半机制
public boolean containsQuorum(Set<Long> set){
// half是在构造方法里赋值的
// set.size()表示某台zkServer获得的票数
return (set.size() > half);
}
}
举个简单的例子:如果现在集群中有5台zkServer,那么half=5/2=2,那么也就是说,领导者选举的过程中至少要有三台zkServer投了同一个zkServer,才会符合过半机制,才能选出来一个Leader。
那么有一个问题我们想一下,选举的过程中为什么一定要有一个过半机制验证?因为这样不需要等待所有zkServer都投了同一个zkServer就可以选举出来一个Leader了,这样比较快,所以叫快速领导者选举算法呗。
那么再来想一个问题,过半机制中为什么是大于,而不是大于等于呢?
这就是更脑裂问题有关系了,比如回到上文出现脑裂问题的场景:
当机房中间的网络断掉之后,机房1内的三台服务器会进行领导者选举,但是此时过半机制的条件是set.size() > 3,也就是说至少要4台zkServer才能选出来一个Leader,所以对于机房1来说它不能选出一个Leader,同样机房2也不能选出一个Leader,这种情况下整个集群当机房间的网络断掉后,整个集群将没有Leader。
而如果过半机制的条件是set.size() >= 3,那么机房1和机房2都会选出一个Leader,这样就出现了脑裂。所以我们就知道了,为什么过半机制中是大于,而不是大于等于。就是为了防止脑裂。
如果假设我们现在只有5台机器,也部署在两个机房:
此时过半机制的条件是set.size() > 2,也就是至少要3台服务器才能选出一个Leader,此时机房件的网络断开了,对于机房1来说是没有影响的,Leader依然还是Leader,对于机房2来说是选不出来Leader的,此时整个集群中只有一个Leader。
所以,我们可以总结得出,有了过半机制,对于一个Zookeeper集群,要么没有Leader,要没只有1个Leader,这样就避免了脑裂问题。