分布式基础概念-分布式服务之ZK[1]

2023-11-03 15:55:16 浏览数 (2)

ZK的初始化选举和崩溃选举过程

相关概念
  • zxId:事务id,
  • sId:节点id

先对比zxId,再对比sId,先投自己,选票内容(zxId,sId),遇强改投

  • 投票箱:每个节点在本地维护自己和其他节点的投票信息,改投时需要更新信息,并广播

节点状态:

  • LOOKING,竞选状态。
  • FOLLOWING,随从状态,同步leader状态,参与投票。
  • OBSERVING,观察状态,同步leader状态,不参与投票。
  • LEADING,领导者状态
初始化

假设没有数据,以5个节点为例

  • 节点1启动,此时只有一台服务器启动,它发出去的请求没有任何响应,所以它的选举状态一直是LOOKING状态
  • 节点2启动,它与节点1进行通信,互相交换自己的选举结果,由于两者都没有历史数据,所以serverId值较大的服务器2胜出,但是由于没有达到半数以上,所以服务器1,2还是继续保持LOOKING状态
  • 节点3启动,与1、2节点通信交互数据,服务器3成为服务器1,2,3中的leader,此时有三台服务器选举了3,所以3成为leader
  • 节点4启动,理论上服务器4应该是服务器1,2,3,4中最大的,但是由于前面已经有半数以上的服务器选举了服务器3,所以它只能切换为follower
  • 节点5启动,同4一样
崩溃选举
  • 变更状态,leader故障后,follower进入looking状态
  • 各节点投票,先投自己(zxId,sId),再广播投票,
  • 接收到投票,对比zxId和sId,如果本节点小、则将票改为接收的投票信息,并记录投票信息,重新广播。否则本节点大、则可不做处理
  • 统计本地投票信息,超过半数,则切换为leading状态并广播

ZK的数据模型

ZK的数据模型是一种树形结构,具有一个固定的根节点(/),可以在根节点下创建子节点,并在子节点下继续创建下一级节点。每一层级用/隔开,且只能用绝对路径(get/work/task1)的方式查询ZK节点,而不能用相对路径。

持久节点
  • 将节点创建为持久节点,该数据节点会一直存储在ZK服务器上,即使创建该节点的客户端与服务端的会话关闭了,该节点依然不会被删除,除非显式调用delete函数进行删除操作。
临时节点
  • 如果将节点创建为临时节点,那么该节点数据不会一直存储在ZK服务器上。当创建该临时节点的客户端会话因超时或发生异常而关闭时,该节点也相应在ZK服务器上被删除。也可以主动调用delete删除。
有序节点
  • 有序节点并不算一种单独种类的节点,而是在持久节点和临时节点的基础上,增加一个节点有序的性质。创建有序节点的时候,ZK服务器会自动使用一个单调递增的数字作为后缀,追加到创建的节点后边。例如一个客户端创建了一个路径为works/task-的有序节点,那么ZooKeeper将会生成一个序号并追加到该节点的路径后,最后该节点的路径为works/task-1。
节点内容

一个二进制数组(byte data[]),用来存储节点的数据、ACL访问控制、子节点数据(因为 临时节点不允许有子节点,所以其子节点字段为null),记录自身状态信息的stat。

stat 节点路径可以查看状态信息 czxid:创建节点的事务id mzxid:最后一次被更新的事务id pzxid:子节点最后一次被修改的事务id ctime:创建时间 mtime:最后更新时间 version:版本号、表示的是对节点数据内容,子节点信息或ACL信息的修改次数可以避免并发更新问题,使用之前获取的版本进行CAS操作更新 cversion:子节点版本号 aversion:acl的版本号 ephemeralOwner:创建节点的sessionId,如果是持久节点、值为0 dataLenght:数据内容长度 numChildren:子节点个数

ZK的watch机制实现原理

newZooKeeper(StringconnectString,intsessionTimeout,Watcherwatcher)这个Watcher将作为整个ZooKeeper会话期间的上下文,一直被保存在客户端ZKWatchManager的defaultWatcher

也可以动态添加watcher:getData(),exists,getChildren。

分布式环境下的观察者模式:通过客服端和服务端分别创建有观察者的信息列表。客户端调用相应接口时,首先将对应的Watch事件放到本地的ZKWatchManager中进行管理。服务端在接收到客户端的请求后根据请求类型判断是否含有Watch事件,并将对应事件放到WatchManager中进行管理。在事件触发的时候服务端通过节点的路径信息查询相应的Watch事件通知给客户端,客户端在接收到通知后,首先查询本地的ZKWatchManager获得对应的Watch信息处理回调操作。这种设计不但实现了一个分布式环境下的观察者模式,而且通过将客户端和服务端各自处理Watch事件所需要的额外信息分别保存在两端,减少彼此通信的内容。大大提升了服务的处理性能

客户端实现过程
  • 标记该会话是一个带有Watch事件的请求
  • 通过DataWatchRegistration类来保存watcher事件和节点的对应关系
  • 客户端向服务器发送请求,将请求封装成一个Packet对象,并添加到一个等待发送队列outgoingQueue中调用负责处理队列outgoingQueue的SendThread线程类中的readResponse方法接收服务端的回调,并在最后执行finishPacket()方法将Watch注册到ZKWatchManager,sendThread通过发送path路径和watcher为true,到server注册watch事件

ZKWatchManager保存了Map<String,Set> dataWatchers、Map<String,Set> existsWatchers、Map<String,Set> childrenWatchers三个集合,客户端会在dataWatchers中会添加一个key为path路径的本地事件

服务端实现过程
  • 解析收到的请求是否带有Watch注册事件,通过FinalRequestProcessor类中的processRequest函数实现的。当getDataRequest.getWatch()值为True时,表明该请求需要进行Watch监控注册。
  • 将对应的Watch事件存储到WatchManager,通过zks.getZKDatabase().getData函数实现, WatchManger该类中有HashMap<String,HashSet> watchTable,key为path,Watcher是一个客户端网络连接封装,当节点变化时会通知对应的连接(连接通过心跳保持)
服务端触发过程
  • 调用WatchManager中的方法触发数据变更事件
  • 封装了一个具有会话状态、事件类型、数据节点3种属性的WatchedEvent对象。之后查询该节点注册的Watch事件,如果为空说明该节点没有注册过Watch事件。如果存在Watch事件则添加到定义的Wathcers集合中,并在WatchManager管理中删除。最后,通过调用process方法向客户端发送通知
客户端回调过程
  • 使用SendThread.readResponse()方法来统一处理服务端的相应
  • 将收到的字节流反序列化转换成WatcherEvent对象。调用eventThread.queueEvent()方法将接收到的事件交给EventThread线程进行处理
  • 从ZKWatchManager中查询注册过的客户端Watch信息。查询到后,会将其从ZKWatchManager的管理中删除。因此客户端的Watcher机制是一次性的,触发后就会被删除
  • 将查询到的Watcher存储到waitingEvents队列中,调用EventThread类中的run方法循环取出在waitingEvents队列中等待的Watcher事件进行处理

ZK分布式锁实现原理

  • 上来直接创建一个锁节点下的一个接一个的临时顺序节点
  • 如果自己不是第一个节点,就对自己上一个节点加监听器
  • 只要上一个节点释放锁,自己就排到前面去了,相当于是一个排队机制。

而且用临时顺序节点,如果某个客户端创建临时顺序节点之后,自己宕机了,zk感知到那个客户端宕机,会自动删除对应的临时顺序节点,相当于自动释放锁,或者是自动取消自己的排队。解决了惊群效应

Zookeeper的典型应用场景

通过对Zookeeper中丰富的数据节点进行交叉使用,配合Watcher事件通知机制,可以非常方便的构建一系列分布式应用中会涉及的核心功能,如:

  1. 数据发布/订阅:配置中心
  2. 负载均衡:提供服务者列表
  3. 命名服务:提供服务名到服务地址的映射
  4. 分布式协调/通知:watch机制和临时节点,获取各节点的任务进度,通过修改节点发出通知
  5. 集群管理:是否有机器退出和加入、选举master
  6. 分布式锁
  7. 分布式队列

第一类,在约定目录下创建临时目录节点,监听节点数目是否是要求的数目。

第二类,和分布式锁服务中的控制时序场景基本原理一致,入列有编号,出列按编号。在特定的目录下创建PERSISTENT_SEQUENTIAL节点,创建成功时Watcher通知等待的队列,队列删除序列号最小的节点用以消费。此场景下Zookeeper的znode用于消息存储,znode存储的数据就是消息队列中的消息内容,SEQUENTIAL序列号就是消息的编号,按序取出即可。由于创建的节点是持久化的,所以不必担心队列消息的丢失问题。

如有问题,欢迎加微信交流:w714771310,备注- 技术交流 。或关注微信公众号【码上遇见你】。


0 人点赞