目录
1、介绍
1.1、CAP 理论
1.2、BASE 理论
1.3、一致性协议ZAB
1、介绍
2、角色
3、ZXID和myid
4、 历史队列
5、协议模式
6、崩溃恢复模式
7、脑裂问题
2、zookeeper
2.1、开源项目
2.2、功能
2.3、选举机制
3、数据模型
3.1、介绍
3.2、znode分类
4、监听通知机制
4.1、介绍
4.2、流程
5、会话(Session)
6、zookeeper分布式锁
6.1、获取锁
6.2、释放锁
6.3、对比
前言
Zookeeper 是一个开源的分布式协调服务框架,由 Apache 基金会维护。它主要用于管理大规模分布式系统中的配置数据、命名、同步和提供组服务等。
Zookeeper 的设计目的是为了简化分布式应用的开发,确保在跨多个服务器或节点中共享数据的一致性和可靠性。
Zookeeper 是 Dubbo 中的核心组件,主要用作服务的注册和发现。关于更多dubbo知识:可参考:深入探讨dubbo组件的实践-CSDN博客
1、介绍
CAP 理论和 BASE 理论是分布式系统设计中的两个重要概念,帮助开发者理解系统在可用性、一致性和分区容忍性方面的权衡。
1.1、CAP 理论
一个分布式系统必然会存在一个问题:因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)和数据一致性(consistency)中做出权衡 。
这就是著名的 CAP
定理。
1.一致性(Consistency)
所有节点在同一时间看到的数据是一致的。
即在所有副本更新成功后,任何后续的读操作都能得到最新的数据。
2.可用性(Availability)
系统能够在任何时候响应请求,无论是读取还是写入操作。
即使部分节点不可用,系统依然可以提供服务。
3.分区容忍性(Partition Tolerance)
系统在网络分区的情况下,仍然能够正常工作。
即使某些节点之间的通信失败,系统仍能继续运行。
CAP 理论的关键点:
在网络分区发生时,你必须在一致性和可用性之间做出选择。通常,强调可用性的系统在网络分区情况下可能返回旧数据,而强调一致性的系统可能会拒绝服务请求以保持数据一致性。
综述:
根据 CAP 理论,分布式系统最多只能满足两个特性,不能同时满足三个特性。举例来说,如果一个系统需要保证一致性和分区容忍性,则在部分节点宕机时,可能无法提供服务(可用性下降)。
1.2、BASE 理论
定义:BASE 是一种用于描述分布式系统的理论,特别是相对于柔性一致性的设计理念。
BASE 是以下三个特性的缩写:
1.基本可用(Basically Available)
系统确保在一定条件下始终可用。
尽管可能存在部分数据的不一致,但系统会在一定时间内提供服务。
2.软状态(Soft state)
系统的状态可能在某个时间点不一致。
由于数据的更新延迟,系统允许一定的时间内数据保持不一致,而不是强制要求所有节点在每次操作后都达到一致。
3.最终一致性(Eventually consistent)
系统将保证最终在没有新的更新的情况下,所有节点的数据会在某个时间点达到一致性。
也就是说,经过一段时间后,所有的副本都将会一致。
关于cap理论和base理论的区别,如下图所示:
1.3、一致性协议ZAB
1、介绍
ZooKeeper
在解决分布式数据一致性问题时并没有直接使用 Paxos
,而是专门定制了一致性协议叫做 ZAB(ZooKeeper Automic Broadcast)
原子广播协议,该协议能够很好地支持 崩溃恢复 。
如下所示:
2、角色
ZAB 中三个主要的角色,Leader 领导者、Follower跟随者、Observer观察者 。
1.Leader
集群中 唯一的写请求处理者 ,能够发起投票(投票也是为了进行写请求)。
2.Follower
能够接收客户端的请求,如果是读请求则可以自己处理,如果是写请求则要转发给 Leader
。在选举过程中会参与投票,有选举权和被选举权 。
3.Observer
就是没有选举权和被选举权的 Follower
。
ZAB协议两种模式:消息广播模式和崩溃恢复模式。
3、ZXID和myid
1、zxid
ZooKeeper 采用全局递增的事务 id 来标识,所有 proposal(提议)在被提出的时候加上了ZooKeeper Transaction Id 。
ZXID是64位的Long类型,这是保证事务的顺序一致性的关键。
ZXID中高32位表示纪元epoch,低32位表示事务标识xid。
你可以认为zxid越大说明存储数据越新,如下图所示:
高32位(epoch) | 低32位(事务id) |
每个leader都会具有不同的epoch值,表示一个纪元/朝代,用来标识 leader周期。每个新的选举开启时都会生成一个新的epoch,从1开始,每次选出新的Leader,epoch递增1,并会将该值更新到所有的zkServer的zxid的epoch。
xid是一个依次递增的事务编号。数值越大说明数据越新,可以简单理解为递增的事务id。
每次epoch变化,都将低32位的序号重置,这样保证了zxid的全局递增性。
2、myid
每个ZooKeeper服务器,都需要在数据文件夹下创建一个名为myid的文件,该文件包含整个ZooKeeper集群唯一的id(整数)。例如,某ZooKeeper集群包含三台服务器,hostname分别为zoo1、zoo2和zoo3,其myid分别为1、2和3,则在配置文件中其id与hostname必须一一对应,如下所示。在该配置文件中,server.
后面的数据即为myid
tex
server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888
4、 历史队列
每一个follower节点都会有一个先进先出(FIFO)的队列用来存放收到的事务请求,保证执行事务的顺序。所以:
- 可靠提交由ZAB的事务一致性协议保证
- 全局有序由TCP协议保证
- 因果有序由follower的历史队列(history queue)保证
5、协议模式
ZAB协议两种模式:消息广播模式和崩溃恢复模式。
如下图所示:
1、广播模式
如下图所示:
- leader从客户端收到一个写请求
- leader生成一个新的事务并为这个事务生成一个唯一的ZXID
- leader将这个事务发送给所有的follows节点,将带有 zxid 的消息作为一个提案(proposal)分发给所有 follower。
- follower节点将收到的事务请求加入到历史队列(history queue)中,当 follower 接收到 proposal,先将 proposal 写到硬盘,写硬盘成功后再向 leader 回一个 ACK
- 当leader收到大多数follower(超过一半)的ack消息,leader会向follower发送commit请求(leader自身也要提交这个事务)
- 当follower收到commit请求时,会判断该事务的ZXID是不是比历史队列中的任何事务的ZXID都小,如果是则提交事务,如果不是则等待比它更小的事务的commit(保证顺序性)
- Leader将处理结果返回给客户端
关于过半写成功策略:
Leader节点接收到写请求后,这个Leader会将写请求广播给各个Server,各个Server会将该写请求加入历史队列,并向Leader发送ACK信息,当Leader收到一半以上的ACK消息后,说明该写操作可以执行。
Leader会向各个server发送commit消息,各个server收到消息后执行commit操作。
这里要注意以下几点:
1.Leader并不需要得到Observer的ACK,即Observer无投票权。
2.Leader不需要得到所有Follower的ACK,只要收到过半的ACK即可,同时Leader本身对自己有一个ACK
3.Observer虽然无投票权,但仍须同步Leader的数据从而在处理读请求时可以返回尽可能新的数据
而对于读请求,Leader/Follower/Observer都可直接处理读请求,从本地内存中读取数据并返回给客户端即可。由于处理读请求不需要各个服务器之间的交互,因此Follower/Observer越多,整体可处理的读请求量越大,也即读性能越好。
6、崩溃恢复模式
恢复模式大致可以分为四个阶段:选举、发现、同步、广播。
如下图所示:
1、选举阶段(Leader election):
当leader崩溃后,集群进入选举阶段(下面会将如何选举Leader),开始选举出潜在的准 leader,然后进入下一个阶段。
2、发现阶段(Discovery):
用于在从节点中发现最新的ZXID和事务日志。准Leader接收所有Follower发来各自的最新epoch值。
Leader从中选出最大的epoch,基于此值加1,生成新的epoch分发给各个Follower。
各个Follower收到全新的epoch后,返回ACK给Leader,带上各自最大的ZXID和历史提议日志。Leader选出最大的ZXID,并更新自身历史日志,此时Leader就用拥有了最新的提议历史。(注意:每次epoch变化时,ZXID的第32位从0开始计数)。
3、同步阶段(Synchronization):
主要是利用 leader 前一阶段获得的最新提议历史,同步给集群中所有的Follower。只有当超过半数Follower同步成功,这个准Leader才能成为正式的Leader。
这之后,follower 只会接收 zxid 比自己的 lastZxid 大的提议。
4、广播阶段(Broadcast):
集群恢复到广播模式,开始接受客户端的写请求。
7、脑裂问题
通俗的说,就是比如当你的 cluster 里面有两个节点,它们都知道在这个 cluster 里需要选举出一个 master。但是如果它们之间的通信出了问题,那么两个结点都会觉得现在没有 master,所以每个都把自己选举成 master,于是 cluster 里面就会有两个 master。
ZAB为解决脑裂问题,要求集群内的节点数量为2N+1, 当网络分裂后,始终有一个集群的节点数量过半数,而另一个集群节点数量小于N+1(即小于半数), 因为选主需要过半数节点同意,所以任何情况下集群中都不可能出现大于一个leader的情况。
因此,有了过半机制,对于一个Zookeeper集群,要么没有Leader,要没只有1个Leader,这样就避免了脑裂问题。
2、zookeeper
从设计模式角度来看,zk是一个基于观察者设计模式的框架,它负责管理跟存储大家都关心的数据,然后接受观察者的注册,数据反生变化zk会通知在zk上注册的观察者做出反应。
如下图所示:
如上所示,zookeeper有以下几个特点:
1、集群:
Zookeeper是一个领导者(Leader),多个跟随者(Follower)组成的集群。
2、高可用性:
集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。
3、全局数据一致:
每个Server保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的。
4、更新请求顺序进行:
来自同一个Client的更新请求按其发送顺序依次执行。
5、数据更新原子性:
一次数据更新要么成功,要么失败。
6、实时性:
在一定时间范围内,Client能读到最新数据。
总结
Zookeeper是一个分布式协调系统,满足CP性,跟SpringCloud中的Eureka满足AP不一样。
2.1、开源项目
许多著名的开源项目用到了 ZooKeeper,比如:
- Kafka : ZooKeeper 主要为 Kafka 提供 Broker 和 Topic 的注册以及多个 Partition 的负载均衡等功能。
- Hbase : ZooKeeper 为 Hbase 提供确保整个集群只有一个 Master 以及保存和提供 regionserver 状态信息(是否在线)等功能。
- Hadoop : ZooKeeper 为 Namenode 提供高可用支持。
- Dubbo:阿里巴巴集团开源的分布式服务框架,它使用 ZooKeeper 来作为其命名服务,维护全局的服务地址列表。
2.2、功能
1.1、配置管理
Zookeeper 可以集中管理分布式应用程序的配置数据,提供快速且一致的配置服务。
1.2、服务注册与发现
Zookeeper 允许服务提供者在启动时向 Zookeeper 注册自身,消费者可以通过 Zookeeper 查询可用服务。
1.3、命名服务
Zookeeper 提供了一个分布式命名服务,允许应用组件如服务、任务、配置等进行命名和查找。
1.4、分布式同步
Zookeeper 可以帮助多个节点之间实现同步和协调,避免由于竞争条件导致的数据不一致。
1.5、集群管理
Zookeeper 可以用于管理集群中的节点状态,支持心跳检测等功能,保证集群的稳定性和可靠性。
2.3、选举机制
Leader
选举可以分为两个不同的阶段,第一个是 Leader
宕机需要重新选举,第二则是系统的 Leader
初始化选举。
下面是zkserver的几种状态:
- LOOKING 不确定Leader状态。该状态下的服务器认为当前集群中没有Leader,会发起Leader选举。
- FOLLOWING 跟随者状态。表明当前服务器角色是Follower,并且它知道Leader是谁。
- LEADING 领导者状态。表明当前服务器角色是Leader,它会维护与Follower间的心跳。
- OBSERVING 观察者状态。表明当前服务器角色是Observer,与Folower唯一的不同在于不参与选举,也不参与集群写操作时的投票。
如下图所示:
关于选举leader的过程示意图如下所示:
总结:
会先根据server的投票内容(myid,zxid)进行投票,进行选举时候,zxid比较,再比较myid。较大的获选,后续加入的server直接追随已有的leader节点。
在选举过程中,server会进行looking--->following--->leading或者其他状态改变。
举例:
假设集群三台服务器,Leader(server2)
挂掉了,只剩下server1和server3。 server1
给自己投票为(1,99),然后广播给其他 server
,server3
首先也会给自己投票(3,95),然后也广播给其他 server
。
server1
和 server3
此时会收到彼此的投票信息,和一开始选举一样,他们也会比较自己的投票和收到的投票(zxid
大的优先,如果相同那么就 myid
大的优先)。
这个时候 server1
收到了 server3
的投票发现没自己的合适故不变,server3
收到 server1
的投票结果后发现比自己的合适于是更改投票为(1,99)然后广播出去,最后 server1
收到了发现自己的投票已经超过半数就把自己设为 Leader
,server3
也随之变为 Follower
。
3、数据模型
3.1、介绍
如下图所示:
采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二级制序列。并且,每个节点还可以拥有 N 个子节点,最上层是根节点以
/
来代表。每个数据节点被称为 znode,它是 ZooKeeper 中数据的最小单元。并且,每个 znode 都一个唯一的路径标识。
由于ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为1M。
在zookeeper客户端使用get
命令可以查看znode的内容和状态信息:
bash
[zk: localhost:2181(CONNECTED) 2] get /zk01
updateed02
cZxid = 0x600000023
ctime = Mon Mar 01 21:20:26 CST 2021
mZxid = 0xb0000000d
mtime = Fri Mar 05 17:15:53 CST 2021
pZxid = 0xb00000018
cversion = 5
dataVersion = 7
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 10
numChildren = 3
下面每个 znode 状态信息究竟代表的如下图所示:
3.2、znode分类
有四种类型:
持久化目录节点 persistent:
客户端与zookeeper断开连接后,该节点依旧存在。
持久化顺序编号目录节点 persistent_sequential:
客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号。
临时目录节点 ephemeral:
客户端与zookeeper断开连接后,该节点被删除。
临时顺序编号目录节点 ephemeral_sequential:
客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号。
4、监听通知机制
4.1、介绍
当客户端在Zookeeper上某个节点绑定监听事件后,如果该事件被触发,Zookeeper会通过回调函数的方式通知客户端,但是客户端只会收到一次通知。
如果后续这个节点再次发生变化,那么之前设置 Watcher 的客户端不会再次收到消息(Watcher是一次性的操作),可以通过循环监听去达到永久监听效果。
4.2、流程
ZooKeeper 的 Watcher 机制,总的来说可以分为三个过程:
- 客户端注册 Watcher,注册 watcher 有 3 种方式,getData、exists、getChildren。
- 服务器处理 Watcher 。
- 客户端回调 Watcher 客户端。
监听通知机制的流程如下:
下面是一个简单的 Java 程序,演示如何使用 Zookeeper 创建一个 Znode,并对该 Znode 添加一个监听器,以便响应节点的数据变化。
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
public class ZookeeperWatchExample implements Watcher {
private static final String ZNODE_PATH = "/my_node";
private ZooKeeper zookeeper;
public ZookeeperWatchExample() throws IOException {
// 连接到 Zookeeper 服务
this.zookeeper = new ZooKeeper("localhost:2181", 3000, this);
}
public void createNode() throws KeeperException, InterruptedException {
// 检查节点是否存在
Stat stat = zookeeper.exists(ZNODE_PATH, false);
if (stat == null) {
// 创建节点,并设置初始数据
zookeeper.create(ZNODE_PATH, "Initial data".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
}
public void watchNode() throws KeeperException, InterruptedException {
// 设置监听器,监控节点的变化
zookeeper.getData(ZNODE_PATH, true, null);
}
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDataChanged) {
System.out.println("Node data has changed!");
try {
// 重新添加监听,以保持持续监控
byte[] newData = zookeeper.getData(ZNODE_PATH, true, null);
System.out.println("New data: " + new String(newData));
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
try {
ZookeeperWatchExample example = new ZookeeperWatchExample();
example.createNode();
example.watchNode();
// 模拟在其他地方更新节点(在实际使用中,这可以是其他程序或线程)
Thread.sleep(5000);
example.zookeeper.setData(ZNODE_PATH, "Updated data".getBytes(), -1);
// 保持程序运行,等待事件
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
process
方法是由 Zookeeper 自动调用的。当你注册一个Watcher
时,Zookeeper 将监视与特定 Znode 相关的事件(例如节点数据变化、节点创建和删除等)。
当这些事件发生时,Zookeeper 会自动触发相应的 Watcher
实现中的 process
方法。
总结:
基于 Zookeeper上创建的节点,可以对这些节点绑定监听事件,比如可以监听节点数据变更、节点删除、子节点状态变更等事件,通过这个事件机制,可以基于 Zookeeper 实现分布式锁、集群管理等多种功能。
5、会话(Session)
客户端与 Zookeeper 服务器之间的连接被称为会话。会话由跟踪的心跳确保活性,并维护 Znode 的持久性与临时性。
持续更新
6、zookeeper分布式锁
分布式锁是雅虎研究员设计Zookeeper的初衷。利用Zookeeper的临时顺序节点,可以轻松实现分布式锁。
如下图所示:
6.1、获取锁
首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。
之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。
这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。
Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。
于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。
这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。
Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。
于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。
这样一来,Client1得到了锁,Client2监听了Lock1,Client3监听了Lock2。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock所依赖的AQS(AbstractQueuedSynchronizer)。
6.2、释放锁
释放锁分为两种情况:
1.任务完成,客户端显示释放
当任务完成时,Client1会显示调用删除节点Lock1的指令。
2.任务执行过程中,客户端崩溃
获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。
由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。
同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。
最终,Client3成功得到了锁。
6.3、对比
总结:
通过将服务提供者的信息注册到 Zookeeper,Dubbo 能够动态地找到和调用所需服务,使得整个服务架构更具弹性和扩展性。
Zookeeper 的高可用性和一致性,有助于增强分布式系统的稳定性和可靠性。
参考文章:
1、史上最全的Zookeeper原理详解(万字长文)_zk原理-CSDN博客