ZooKeeper是一个分布式协调服务,提供了高度可靠且具有高性能的分布式应用协调服务。 Zookeeper = 文件系统 + 通知机制
Zookeeper 的核心是原子广播,这个机制保证了各个 Server 之间的同步。实现这个机制的协议叫做 ZAB 协议。 Zab 协议有两种模式,它们 分别是恢复模式(选主)和广播模式(同步) Zab 协议 的全称是 Zookeeper Atomic Broadcast** (Zookeeper 原子广播)。Zookeeper 是通过 Zab 协议来保证分布式事务的最终一致性。Zab 协议要求每个 Leader 都要经历三个阶段:发现,同步,广播。 当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和 leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。 为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加 上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一 个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。 epoch:可以理解为皇帝的年号,当新的皇帝leader产生后,将有一个新的epoch年号。
(1)因为 zookeeper 中只要有半数以上的机器正常工作,那么整个集群对外就是可用的。比如说如果有 2 个 zookeeper,那么只要 1 个死了 zookeeper 就不能用了,因为 1没有过半,那么 zookeeper 的死亡容忍度为 0,同理,如果有 3 个 zookeeper,如果死了 1个,还剩2个 正常,还是过半的,所以 zookeeper 的死亡容忍度为 1,我之前算过 4 个5 个 6 个等情况下的死亡容忍度,发现了一个规律,2n 和 2n-1 的容忍度是一样的,所以为了节约资源,就选择奇数台。 (2)防止因为集群脑裂造成集群用不了。比如有 4 个节点,脑裂为 2 个小集群,都为 2 个节点,这时候,不能满足半数以上的机器正常工作,因此集群就不可用了,那么当有 5 个节点的时候,脑裂为 2 个小集群,分别为2和 3,这时候 3 这个小集群仍然可以选举出 leader,因此集群还是可用的。
不是,是一次性的,使用一次就会失效。无论是服务端还是客户端,一旦一个 Watcher 被触发, Zookeeper 都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。 为什么不是永久的,举个例子,如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,这太消耗性能了。在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可。
Observer应用场景:
部署的规则:集群最少需要机器数:3,集群规则为2N+1台,N>0
ZAB 协议是 ZooKeeper 自己定义的协议,全名 ZooKeeper 原子广播协议。 ZAB 协议有两种模式:Leader 节点崩溃了如何恢复和消息如何广播到所有节点。 整个 ZooKeeper 集群没有 Leader 节点的时候,属于崩溃的情况。比如集群启动刚刚启动,这时节点们互相不认识。比如运作 Leader 节点宕机了,又或者网络问题,其他节点 Ping 不通 Leader 节点了。这时就需要 ZAB 中的节点崩溃协议,所有节点进入选举模式,选举出新的 Leader。整个选举过程就是通过广播来实现的。选举成功后,一切都需要以 Leader 的数据为准,那么就需要进行数据同步了。
相同点: (1)两者都存在一个类似于 Leader 进程的角色,由其负责协调多个 Follower 进程的运行 (2)Leader 进程都会等待超过半数的 Follower 做出正确的反馈后,才会将一个提案进行提交 (3)ZAB 协议中,每个 Proposal 中都包含一个 epoch 值来代表当前的 Leader周期,Paxos 中名字为 Ballot 不同点: ZAB 用来构建高可用的分布式数据主备系统(Zookeeper),Paxos 是用来构建分布式一致性状态机系统。 ZAB是在Paxos的基础上改进和演变过来的
节点宕机 Zookeeper本身也是集群,推荐配置不少于3个服务器。Zookeeper自身也要保证当一个节点宕机时,其他节点会继续提供服务。如果是一个Follower宕机,还有2台服务器提供访问,因为Zookeeper上的数据是有多个副本的,数据并不会丢失;如果是一个Leader宕机,Zookeeper会选举出新的Leader。ZK集群的机制是只要超过半数的节点正常,集群就能正常提供服务。只有在ZK节点挂得太多,只剩一半或不到一半节点能工作,集群才失效。所以3个节点的cluster可以挂掉1个节点(leader可以得到2票>1.5)2个节点的cluster就不能挂掉任何1个节点了(leader可以得到1票<=1)
Znode 数据节点代码如下
public class DataNode implements Record {
byte data[];
Long acl;
public StatPersisted stat;
private Set<String> children = null;
}
Znode 包含了存储数据、访问权限、子节点引用、节点状态信息
为了保证高吞吐和低延迟,以及数据一致性,znode 只适合存储非常小数据,不能超过 1M,最好都小于 1K
ZXID 生成规则如下:
ZXID 有两部分组成:
ZXID 低 32 位计数器,所以同一任期内,ZXID 连续,每个结点又都保存着自身最新生效 ZXID,通过对比新提案 ZXID 与自身最新 ZXID 否相差“1”,来保证事务严格按照顺序生效
服务器具有四种状态,分别:LOOKING、FOLLOWING、LEADING、OBSERVING。
ZooKeeper 集群部署图
ZooKeeper 集群一主多从结构:
ZooKeeper 如何保证主从节点数据一致性?
我们知道集群主从部署结构,要保证主从节点一致性问题,无非就两个主要问题:
Zookeeper 采用ZAB 协议(Zookeeper Atomic Broadcast,Zookeeper 原子广播协议)来保证主从节点数据一致性, ZAB 协议支持崩溃恢复和消息广播两种模式,很好解决了这两个问题:
Leader 服务器挂了,所有集群中服务器进入 LOOKING 状态,首先,它们会选举产生新 Leader 服务器;接着,新 Leader 服务器与集群中Follower服务进行数据同步,当集群中超过半数机器与该 Leader 服务器完成数据同步之后,退出恢复模式进入消息广播模式。Leader 服务器开始接收客户端事务请求生成事务 Proposal 进行事务请求处理。
ZooKeeper作为分布式协调服务,它的职责是保证数据(注:配置数据,状态数据)在其管辖下的所有服务之间保持同步、一致。所以,我们可以认为Zookeeper是一个CP的分布式系统。所以他会牺牲可用性,也就是在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果。
这个 CP体现在以下几个情况下:1、如果集群中的存活节点数低于总结点数的一半,那么整个集群将无法接受新的写请求。2、在 ZK 的 master 选举过程中,在新的Master被选举出来之前,整个集群也无法接受新的写请求。
而且, 作为ZooKeeper的核心实现算法 Zab,就是解决了分布式系统下数据如何在多个服务之间保持同步问题的。
如果 ZooKeeper下所有节点都断开了,或者集群中出现了网络分割的故障(注:由于交换机故障导致交换机底下的子网间不能互访);那么ZooKeeper 会将它们都从自己管理范围中剔除出去,外界就不能访问到这些节点了,即便这些节点本身是“健康”的,可以正常提供服务的;所以导致到达这些节点的服务请求被丢失了。
但是,请一定要注意,这里面的一致性,它确实是强一致性,但是,Zookeeper保证的是强一致模型中的顺序一致性而不是线性一致性。
脑裂是在分布式系统中经常出现的问题之一,它指的是由于网络或节点故障等原因,导致一个分布式系统被分为多个独立的子系统,每个子系统独立运行,无法相互通信,同时认为自己是整个系统的主节点,这就会导致整个系统失去一致性和可用性。 Zookeeper集群中的脑裂出现的原因通常有以下2种情况:
针对Zookeeper集群中的脑裂问题,可以采取以下几种方式进行恢复脑裂:
如何避免避免脑裂的发生呢?
基于zookeeper临时有序节点可以实现的分布式锁:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
来看下Zookeeper能不能解决以下问题。
● 锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。● 非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。● 不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。● 单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。
可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
try {
return interProcessMutex.acquire(timeout, unit);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
public boolean unlock() {
try {
interProcessMutex.release();
} catch (Throwable e) {
log.error(e.getMessage(), e);
} finally {
executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
}
return true;
}
Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。
使用ZK实现的分布式锁好像完全符合了我们对一个分布式锁的所有期望。但是,其实并不是,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上。
其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的情况,由于网络抖动,客户端和ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。(所以,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)
使用Zookeeper实现分布式锁的优缺点:
优点:有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。
缺点:性能上不如使用缓存实现分布式锁。需要对ZK的原理有所了解。
依赖了 ZAB 协议,ZAB 协议借鉴了 Paxos 算法,是专门为 ZooKeeper 设计的支持崩溃恢复的原子广播协议。Paxos 算法中采用多个Proposer 会存在竞争 Acceptor 的问题,ZooKeeper 设计为只有一个 Leader 负责处理外部的写事务请求,然后 Leader 将数据同步到其他 Follower 节点。即,ZooKeeper 只有一个 Leader 可以发起提议。 ZAB 协议包括两种基本的模式:消息广播(正常)、崩溃恢复(异常) 这两个模式是相辅相成的,消息广播模式就是 Zookeeper 不出现任何问题,并且正常工作的模式,崩溃恢复看字面意思就是当 Zookeeper 出现故障时用于恢复的
消息广播
崩溃恢复
zk的负载均衡是可以调控,nginx只是能调权重,其他需要可控的都需要自己写插件;但是nginx的吞吐量比zk大很多,应该说按业务选择用哪种方式。