咱们在这一篇文章中大概了解了三高、架构、四个问题、相应解决方案…,会发现分布式系统架构有一个最核心的问题就是如何解决分布式环境一致性。围绕这个问题,分布式一致性问题的工业解决方案——开源框架 ZooKeeper脱颖而出。【zookeeper有挂的时候,但是挂了可以很快就恢复,快速恢复Leader就体现了zookeeper的可靠性
,就因为这哥们恢复的很快,所以经常用来做分布式协调组件】
首先,整理笔记之前,特此感谢一下Zookeeper的官方文档,因为里面好多内容都是和官方文档离不开的。
PART1:Zookeeper概念篇
ZooKeeper 是 Hadoop 生态系统的一员
】,是一个树形目录服务
,数据模型和Unix的文件系统目录树很类似,拥有一个层次化结构
。
但是Unix的目录下是不能直接存数据的,数据必须要写在文件中,然后目录结构中存文件;而Zookeeper中是可以直接存数据的。咱们可以向这个节点中写入数据,也可以在节点下面创建子节点。
一个通用的无单点问题的分布式协调框架
,并以一系列简单易用的接口提供给用户使用。每一个节点都被称为ZNode
,每个节点上都会保存自己的数据和父子节点信息
ZooKeeper 数据模型采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二级制序列。并且。每个节点还可以拥有 N 个子节点,最上层是根节点以“/”来代表
。
名称是由斜杠(“ /”)分隔的一系列路径元素
。ZooKeeper每个数据节点在 ZooKeeper 中被称为 znode,ZooKeeper每个被称为znode的数据节点是 ZooKeeper 中数据的最小单元
。ZooKeeper 命名空间中的每个 znode 均由一个唯一的路径标识。每个 znode 都有一个父对象
,其路径是 znode 的前缀,元素少一个;此规则的例外是 root(“ /”),它没有父项。此外,与标准文件系统完全一样,如果 znode 有子节点,则无法删除它
。ZooKeeper 与标准文件系统之间的主要区别在于
,每个 znode 都可以具有与之关联的数据
(每个文件也可以是目录,反之亦然),并且 znode 限于它们可以拥有的数据量。ZooKeeper 旨在存储协调数据:状态信息,配置,位置信息等。这种元信息通常以千字节(如果不是字节)来度量。ZooKeeper 具有1M的内置完整性检查,以防止将其用作大型数据存储
,但是通常,它用于存储小得多的数据。 ZooKeeper 不适合保存大量数据
。节点可以拥有子节点,同时也允许少量(1MB)数据存储在该节点之下
。
ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,所以不要放比较大的数据在 znode 上,ZooKeeper 给出的上限是每个结点的数据大小最大是 1M
。ZooKeeper 将数据保存在内存中,性能是非常棒的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态
。(“读”多于“写”是协调服务的典型场景)。存储数据(data)
、访问权限(acl)、子节点引用(child)、节点状态信息(stat)【对应于每个 znode,ZooKeeper 都会为其维护一个叫作 Stat 的数据结构,Stat 中记录了这个 znode 的三个相关的版本:】
CREATE 和 DELETE 这两种权限都是针对 子节点 的权限控制
。】CREATE 和 DELETE 这两种权限都是针对 子节点 的权限控制
。】四种类型
:
PERSISTENT持久节点
:一旦创建就一直存在即使 ZooKeeper 集群宕机也存在
,直到将其删除。
EPHEMERAL:临时节点
客户端断开连接后,相当于会话消失则节点消失 ,ZooKeeper 会自动删除临时节点
。
Session 可以看作是 ZooKeeper 服务器与客户端的之间的一个 TCP 长连接
通过心跳检测
与ZooKeeper服务器保持有效的会话
发送请求并接受响应
接收来自服务器的 Watcher 事件通知
。只要在sessionTimeout规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效
。】在为客户端创建会话之前,服务端首先会为每个客户端都分配一个 sessionID
。由于 sessionID是 ZooKeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID 的,因此,无论是哪台服务器为客户端分配的 sessionID,都务必保证sessionID全局唯一
临时节点只能做叶子节点 ,不能创建子节点
。PERSISTENT_SEQUENTIAL:持久顺序节点
顺序节点(sequential node),每次创建顺序节点时,ZooKeeper 都会在路径后面自动添加上10位的数字,从1开始,最大是2147483647 (2^32-1
)】。比如 /node1/app0000000001 、/node1/app0000000002 。EPHEMERAL_SEQUENTIAL临时顺序节点
:-es。
顺序节点(sequential node),每次创建顺序节点时,ZooKeeper 都会在路径后面自动添加上10位的数字,从1开始,最大是2147483647 (2^32-1
)】。同一客户端发起的事务请求
,最终将 会严格地按照顺序
被应用到 ZooKeeper 中去。
要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用
。无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的
。
更改的结果就会被持久化
,直到被下一次更改覆盖。【可靠性这块redis是比不上zookeeper】
zookeeper有挂的时候,但是挂了可以很快就恢复,快速恢复Leader就体现了zookeeper的可靠性
有变动则回调函数
我们可以让多个客户端创建一个指定的节点 ,创建成功的就是 master
。但是,如果这个 master 挂了怎么办???所以我们可以利用 临时节点、节点状态 和 watcher 来实现选主的功能,临时节点主要用来选举,节点状态和watcher 可以用来判断 master 的活性和进行重新选举
因为强一致性会影响高可用性
),会按照最终一致性去和各个从节点进行数据同步。所以zookeeper还有一个特点就是在特定范围内数据会一致而不是实时性的一致zookeeper 天然支持的 watcher 和 临时节点能很好的实现这些需求
。我们 可以为每条机器创建临时节点,并监控其父节点,如果子节点列表有变动(我们可能创建删除了临时节点),那么我们可以使用在其父节点绑定的 watcher 进行状态监控和回调
。让 服务提供者 在 zookeeper 中创建一个临时节点并且将自己的 ip、port、调用方式 写入节点,当 服务消费者 需要进行调用的时候会 通过注册中心找到相应的服务的地址列表(IP端口什么的) ,并缓存到本地(方便以后调用)
,当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务
。当服务提供者的某台服务器宕机或下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然你可以让消费者进行节点监听)。可以通过 ZooKeeper 的顺序节点生成全局唯一 ID
。
通过 Watcher 机制 可以很方便地实现数据发布/订阅
。当你将数据发布到 ZooKeeper 被 Watcher 机制监听的节点上,其他机器可通过监听 ZooKeeper 上节点的变化来实现配置的动态更新
。通过创建唯一节点获得分布式锁,当获得锁的一方执行完相关代码或者是挂掉之后就释放锁
】原来为了防止并发访问产生的线程安全问题,可以用synchronized或者lock单独加锁,但是在分布式中你A服务加的锁对B服务肯定是没作用的
(有可能就没在一个电脑上,怎么可能有作用?【分布式指的是很多的节点在不同的物理位置,他们之间要相互通信传递数据】),所以搞个分布式锁,通过创建唯一节点获得分布式锁,当获得锁的一方执行完相关代码或者是挂掉之后就释放锁,其他方才能获得锁继续执行
。资源在谁那,分布式锁就加到哪里(别加到代理中介那里去了)
【分布式锁的实现方式有很多种,比如 Redis 、数据库 、zookeeper 等】
在我们进行单机应用开发,涉及并发同步的时候,我们往往采用synchronized或者Lock的方式来解决多线程间的代码同步问题
,这时多线程的运行都是在同一个JVM之下, 没有任何问题。但当我们的应用是分布式集群工作的情况下,属于多JVM下的工作环境,跨JVM之间已经无法通过多线程的锁解决同步问题
。那么就需要一种更加高级的锁机制, 处理种跨机器的进程之间的数据同步问题
一这就是分布式锁。其实咱们单机中实现锁基本上思路也是差不多,你该tryLock还是得tryLock,你该释放还得释放呀,通知呀、防止死锁等肯定是重要的几个点
:
watch前一个人,最小的获得锁,第二个watch第一个,第三个watch第二个...。一旦最小的释放锁,zookeeper只给第二个发事件,回调,此时的资源消耗是最小的
分布式的情况下实现分布式锁。可以利用临时节点的创建来实现
。 创建节点的唯一性,我们可以让多个客户端同时创建一个临时节点,创建成功的就说明获取到了锁 。然后没有获取到锁的客户端也像上面选主的非主节点创建一个 watcher 进行节点状态的监听,如果这个互斥锁被释放了(可能获取锁的客户端宕机了,或者那个客户端主动释放了锁)可以调用回调函数重新获得锁。zk 中不需要向 redis 那样考虑锁得不到释放的问题了,因为当客户端挂了,节点也挂了,锁也释放了
。核心思想是当客户端要获取锁,则创建节点,使用完锁,则删除该节点。
临时顺序
节点。
只有删除了才能保证这个锁被释放。搞成临时的哪怕宕机了他也会自动删除
)找到序号最小的那个就是锁
),那么就认为该客户端获取到了锁。使用完锁后,将该节点删除。此时客户端需要找到比自己小的那个节点
,同时对其注册事件监听器,监听删除事件。最好是以集群形态来部署 ZooKeeper
,这样 只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的
。通常 3 台服务器就可以构成一个 ZooKeeper 集群了
。主服务器提供写服务
,其他的 Slave 服务器从服务器通过异步复制的方式
获取 Master 服务器最新
的数据提供读服务
。ZooKeeper 分为服务器端(Server) 和客户端(Client)
,客户端可以连接到整个 ZooKeeper 服务的任意
服务器上(除非 leaderServes 参数被显式设置,leader 不允许接受客户端连接),客户端使用并维护一个 TCP 连接,通过这个连接发送请求、接受响应、获取观察的事件以及发送信息。
只要大多数服务器可用,ZooKeeper 服务就可用
;也就是有Leader时算是可用状态
大于
宕掉的个数的话整个 ZooKeeper 才依然可用。假如我们的集群中有 n 台 ZooKeeper 服务器,那么也就是剩下的服务数必须大于 n/2,也就是说 最大坏掉的机器数必须小于n/2
。先说一下结论,2n 和 2n-1 的容忍度是一样的,比如假如我们有 3 台,那么最大允许宕掉 1 台 ZooKeeper 服务器,如果我们有 4 台的的时候也同样只允许宕掉 1 台。 假如我们有 5 台,那么最大允许宕掉 2 台 ZooKeeper 服务器,如果我们有 6 台的的时候也同样只允许宕掉 2 台。综上,何必增加那一个不必要的 ZooKeeper 呢,所以就是最大坏掉的机器数必须小于n/2
在 ZooKeeper 中没有选择传统的 Master/Slave 概念,选择这种传统的主从模式只有两个角色呀
】:Leader、Follower 和 Observer(将 server 分为三种是为了避免太多的从节点参与过半写的过程
,导致影响性能,这样 Zookeeper 只要使用一个几台机器的小集群就可以实现高性能了,如果要横向扩展的话,只需要增加 Observer 节点即可
。)Leader 既可以为客户端提供写服务又能提供读服务,集群中唯一的写请求处理者
。】,一个更新操作成功的标志是当且仅当大多数 Server 在内存中成功修改数据。每个 Server 在内存中存储了一份数据
】,写数据【Leader 既可以为客户端提供写服务又能提供读服务
】
leader节点提供读写操作,而Follow和Observer只提供读操作。这不就是读写分离zookeeper嘛
】
只有一个主节点的集群,不管是redis还是zookeeper,都要考虑单点故障等问题,从而会影响高可用问题
。leader挂了之后,跟redis相似,但是redis过半时不知道哪些节点过半,而zookeeper可以知道哪些台节点过半了【配置文件中记录了都有哪些节点】
Follower 和 Observer 都只能提供读服务
】,在选主过程中参与投票
为客户端提供读服务【能够接收客户端的请求,如果是读请求则可以自己处理,如果是写请求则要转发给 Leader】,如果是写服务则转发给 Leader
。Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能。
将写请求转发给leader节点
,但是不参与投票过程,只同步leader状态
,主要存在目的就是为了提高读取效率【在不影响写性能的情况下提升集群的读性能。此角色于 ZooKeeper3.3 系列新增的角色。】
【Follower 和 Observer 都只能提供读服务
】
当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,就会进入 Leader 选举过程。ZooKeeper 集群中的所有机器启动时通过一个 Leader 选举过程来选定一台称为 “Leader” 的机器
】在Leader选举的过程中,如果某台ZooKeeper获得了超过半数的选票
,则此ZooKeeper就可以成为Leader了。
Leader election(选举阶段)
:节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。Discovery(发现阶段)
:在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。Synchronization(同步阶段)
:同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后 准 leader 才会成为真正的 leader。Broadcast(广播阶段)
:到了这个阶段,ZooKeeper 集群才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。集群初始化阶段
,只有两台以以上的 ZK 启动才会发生leader选举
,选举过程如下:
将自己
作为 Leader 服务器来进行投票,每次投票会包含所推举的服务器的(myid, ZXID),此时 ZK1 的投票为(1, 0),ZK2 的投票为(2, 0),然后各自将这个投票发给集群中其他机器。
只要任何人投票,都会触发那个准leader发起自己的投票,然后其他节点就开始推选准leader
首先判断该投票的有效性
,如检查是否是本轮投票、是否来自 LOOKING 状态的服务器。
进行比较
,规则如下:【如果zxid相同,再比较myid,如果过半的话【如果不过半的话就会进入不可用状态,对外停止服务,保证不能向外提供脏数据】,通过通信选一个myid最大的作为Leader】
】
ZXID 比较大的服务器优先作为 Leader
。
如果 ZXID 相同,那么就比较 myid。myid 较大的服务器作为Leader服务器
。
判断是否已经有过半机器接受到相同的投票信息
,对于 ZK1、ZK2 而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出 ZK2 作为Leader。一旦确定了 Leader,每个服务器就会更新自己的状态
,如果是Follower,那么就变更为 FOLLOWING,如果是 Leader,就变更为 LEADING。当新的 Zookeeper 节点 ZK3 启动时,发现已经有 Leader 了,不再选举,直接将直接的状态从 LOOKING 改为 FOLLOWING。过半机制防止脑裂
:
子集群各自选主导致“脑裂”
的情况。若没有过半机制,当网络恢复的时候会发现有 2 个 leader。仿佛是 1 个大脑(leader)分散成了 2 个大脑,这就发生了脑裂现象。脑裂期间 2 个大脑都可能对外提供了服务,这将会带来数据一致性等问题
。ZooKeeper 的过半机制导致不可能产生 2 个 leader,因为少于等于一半是不可能产生 leader 的,这就使得不论机房的机器如何分配都不可能发生脑裂
。Zookeeper 的数据一致性是依靠ZAB协议完成的
【一篇关于ZAB的好文章】
ZooKeeper 并没有完全采用 Paxos 算法 ,而是使用ZAB(ZooKeeper Atomic Broadcast) 原子广播协议作为其保证数据一致性的核心算法
。另外,在 ZooKeeper 的官方文档中也指出,ZAB(ZooKeeper Atomic Broadcast) 原子广播协议并不像 Paxos 算法那样,是一种通用的分布式一致性算法,ZAB(ZooKeeper Atomic Broadcast) 原子广播协议是一种特别为 Zookeeper 设计的崩溃可恢复的原子消息广播算法
第一步先写日志,给所有节点发提议,发起者等待过半的回复;保证数据操作的可靠性
保持集群中各个副本之间的数据一致性
。
崩溃恢复和消息广播
。【在 ZAB 协议中对 zkServer(即三个角色的总称:ZAB 中三个主要的角色,Leader 领导者、Follower跟随者、Observer观察者 。) 还有两种模式的定义,分别是 消息广播 和 崩溃恢复】
选举产生新的 Leader 服务器
。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步【所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致。】之后
,ZAB 协议就会退出恢复模式
。剩下未同步完成的机器会继续同步,直到同步完成并加入集群后该节点的服务才可用
此时还在选举阶段所以整个集群处于 Looking 状态
。定义ZAB 协议是如何处理写请求的
已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步
,那么整个服务框架就可以进人消息广播模式了
。当一台同样遵守 ZAB 协议的服务器启动后加人到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播
,那么新加人的服务器就会自觉地进人数据恢复模式
:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。ZooKeeper 设计成只允许唯一的一个 Leader 服务器来进行事务请求的处理
。Leader 服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议;而如果集群中的其他机器接收到客户端的事务请求,那么这些非 Leader 服务器会首先将这个事务请求转发给 Leader 服务器
。
全局单调递增的事务ID ZXID【定义这个的原因也是为了顺序性,每个 proposal 在 Leader 中生成后需要 通过其 ZXID 来进行排序 ,才能得到处理。】 ,它是一个64位long型,其中高32位表示 epoch 年代,低32位表示事务id
。
PART2:Zookeeper实践篇
//通过运行get命令验证数据是否与znode关联
[zkshell: 12] get /zk_test
my_data
cZxid = 5 //节点被创建的事务ID
ctime = Fri Jun 05 13:57:06 PDT 2009 //创建时间
mZxid = 5 //最后一次被更新的事务ID
mtime = Fri Jun 05 13:57:06 PDT 2009 //修改时间
pZxid = 5 //子节点列表最后一次被更新的事务ID
cversion = 0 //子节点的版本号
dataVersion = 0 //数据版本号
aclVersion = 0 //权限版本号
ephemeralOwner = 0 //用于临时节点,代表临时节点的事务ID,如果为持久节点则为0
dataLength = 7 //节点存储的数据的长度
numChildren = 0 //当前节点的子节点个数
psvm(){//形参中有watcher就说明你可以跟下面这个形式一样new Watcher(){写自己的回调逻辑就行},相当于把某个节点给watch住了
Zookeeper zk = new Zookeeper("zookeeper集群的连接列表对应的字符串,用逗号隔开","session的超时时间,如3s",new Watcher(){
//写watch的回调方法,依赖事件回调的,被回调的是这个process方法
public void process(WatchedEvent event){
//传回事件
Event.KeeperState state = event.getState();
Event.KeeperType type = event.getType();
String path = event.getPath();
switch(state){
case
...
}
switch(type){
case Node:
break;
case NodeCreated:
break;
case NodeDeleted:
break;
case NodeDataChanged:
break;
case NodeChildrenChanged:
break;
}
}
});
Zookeeper.States state = zk.getState();
switch(state){
case CONNECTING:
...
break;
...
}
//有两种create方法重载,同步阻塞和异步模型
zk.create(...);
zk.getData(...);//有四个重载形式
}
//1.第一种方式
CuratorFrameworkFactory.newClient();
//2.第二种方式
CuratorFrameworkFactory.builder();
Stat status = new Stat();
client.getData().storingStatIn(status).forPath("/app1");
int version = status.getVersion();//查询出来的Version
client.setData().withVersion(version).forPath( path:"/app1" ,"haha".getBytes());
Watcher事件监听
:ZooKeeper允许用户在指定节点上注册一些Watcher,
并且在一些特定事件触发的时候, ZooKeeper服务端会将事件通知到感兴趣的客户端上去
,该机制是ZooKeeper实现分布式协调服务的重要特性。如果用zookeeper的watch后,每个客户端连接到zookeeper时都会产生一个session来代表这个客户端, session就是在描述我这个用户是谁。然后依托这个session产生zookeeper的临时节点 ,如果我zookeeper在则session就一直在,如果我zookeeper中的节点挂掉了则session就会消失,此时就会产生一个事件event,会回调之前watch的那个节点目录
。
比如心跳3秒钟,那么两个客户端之间(或者客户端与服务端)就会有3秒的时间间隔,我刚一跳你挂了,我只能等3秒之后才能继续跳。而人家zookeeper的watch,如果你挂了,人家session只用几毫秒,session没了就产生事件,然后回调
ZooKeeper中引入了Watcher机制来实现了发布/订阅功能能,能够让多个订阅者同时监听某一个对象, 当一个对象自身状态变化时,会通知所有订阅者
。但是其使用并不是特别方便需要开发人员自己反复注册Watcher,比较繁琐。所以Curator引入了Cache来实现对ZooKeeper服务端事件的监听
。redis不支持,因此如果用redis则需要客户端启动多个线程进行订阅监听,对服务器有一压力!
触发
之后,Zookeeper就会将它从存储中移除
,如果还要继续监听这个节点,就需要我们在客户端的监听回调中,再次对节点的监听watch事件设置为True。否则客户端只能接收到一次该节点的变更通知Zookeeper只能保证最终的一致性
,而无法保证强一致性。ZooKeeper中引入了Watcher机制来实现了发布/订阅功能能,能够让多个订阅者同时监听某一个对象, 当一个对象自身状态变化时,会通知所有订阅者
”。如果回归到咱们最原始的Web 实时消息推送,该怎么实现呢?【消息推送一般又分为 Web 端消息推送和移动端消息推送,思路就是只要触发某个事件(主动分享了资源或者后台主动推送消息)
。】
长轮询是对上边短轮询的一种改进版本,在尽可能减少对服务器资源浪费的同时,保证消息的相对实时性
。长轮询在中间件中应用的很广泛,比如 Nacos 和 Apollo 配置中心,消息队列 Kafka、RocketMQ 中都有用到长轮询。
是一种在 TCP 连接上进行全双工通信的协议,建立客户端和服务器之间的通信渠道。浏览器和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输
。
其实除了可以用WebSocket这种耳熟能详的机制外,还有一种服务器发送事件(Server-Sent Events),简称 SSE
。这是一种服务器端到客户端(浏览器)的单向消息推送。
MQTT (Message Queue Telemetry Transport)是一种基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,通过订阅相应的主题来获取消息,是物联网(Internet of Thing)中的一个标准传输协议。该协议将消息的发布者(publisher)与订阅者(subscriber)进行分离,因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的 MQ 有点类似
。巨人的肩膀:
moon聊技术
B站黑马视频
Zookeeper官方文档
javaGuide老师关于消息推送的文章,很赞
《从 Paxos 到 ZooKeeper 分布式一致性原理与实践》