ZooKeeper(三)

2021-07-16 15:29:00 浏览数 (1)

zookeeper数据变更通知--watcher

上图我们看到watcher机制有三部分组成,客户端,zookeeper,watchmanager,客户端把向zookeeper注册的同时,灰板watcher存储到客户端的watcherManager中,当zookeeper服务器端触发watcher事件后,向客户端发送通知,客户端从watchManager中取出对应Wacher对象来执行回调逻辑

客户端注册watcher

我们可以使用多种方式注册watcher,如下

代码语言:javascript复制
//1.构造zookeeper时候添加默认的watcher,作为整个回话的默认watcher,会一直保存在客户端
//ZKWatchManager的defaultWatcher中
public Zookeeper(String conectSring,int sessionTimeout,Watcher watcher)
public byte[] getData(String path,boolean watch,Stat,stat)
public byte[] getData(final String path,Watcher watcher,Stat stat)
//还有就是getChildren,exist这写接口,但是原理都是一样
//因此我们下面说的都是按照getData进行说明

具体的逻辑如下

  • 其中WatcherRegistration对象是暂时保存数据节点路径和watcher的对应关系
  • 最后将展示保存的watcher对象转交给ZKWatcherManager,并保存到dataWatchers中,dataWatchers是一个Map<String,Set<Watcher>>类型的数据结构,用于保存数据节点路径和Watcher对象进行一一映射
  • 如果每一次客户端注册的wather对象都发送到服务端,内存和性能优惠有问题,因此向服务端发送请求的时候,并不是把watcherRegistration对象完全的序列化到底层字节数据去,因此就不会进行网络传输了。

服务端处理Watcher

当服务端接受到客户端的请求之后,会判断是否进行注册watcher,例如getData,出入ServerCnxn和和数据节点路径传入getData,这个ServerCnxn可以看做一个watcher,最终被存在WatchManager中watchTable和watch2Paths中.

WatchManager是zookeeper服务端watcher的管理者,其内部有管理的watchTable和watch2Paths两个存储结构,

  • watchTable是从数据节点路径的粒度来托管Watcher
  • watcher2Paths是从Watcher的粒度来控制事件触发需要触发的数据节点

同时watchManager还负责Watcher事件触发,并移除那些已经被触发的Watcher.

Watcher触发

上面已经了解标记了watcher注册请求,zookeeper会将其对应的ServerCnxn存储到WatchManager中,现在我们看看服务端是如何触发watcher的。

基本逻辑如下

  1. 封装wachedEvent 首先将通知状态(keeperState),事件类型(EventType)以及节点路径(Path)分装一个watchedEvent对象
  2. 查询watcher 根据数据节点的路径从watcherTable去除对应watcher,如果没有找到watcher,说明没有任何客户端在该数据节点上注册过watcher,直接退出,如果找到这个watcher,将其去除,同时会直接从watchTable和watch2Paths中将其删除,从这里我们可以看出wacher在服务端是一次性的
  3. 调用process方法触发watcher 这一步直接拿到第二步的watcher的process方法,其实这watcher即使之前注册的ServerCnxn,而在这process的主要逻辑如下
    1. 请求红标记-1,表明当前是一个通知
    2. 将watchedEvent包装成watcherEvent,以便网络传出序列化
    3. 向客户端发送该通知

我们看到真正的逻辑不是服务端进行的,而是有客户端连接的ServerCnxn对象实现对客户端WatchedEvent传递,真正的客户端watcher回调与业务员逻辑执行都在客户端,

客户端回到watcher

上面我们已经知道如何触发了watcher,并且知道最终服务端会通过使用ServerCnxn对应的TCP连接向客户端发送一个WatcherEvent事件,

对于一个服务端的响应,客户端都是有sendThread.readResponse方法进行统一处理,具体逻辑如下

  1. 分序列化 zookeeper客户端接到请求后,首先会将字节流转换成watcherEvent对象
  2. 处理chrootPath 如何客户端设置了chrootPath属性,那么需要对服务端传过来的完整节点路径进行chrootPath处理,生成一个相对节点路径,例如客户端设置了chrootPath为/app1,服务端传过来的响应包含的节点路径为/app/locks,经过chrootPath处理后,变成相对路径/locks
  3. 还原watchedEvent 回调方法process方法,参数定义是watchedEvent,因此这里将watcherEvent对象转换成watchedEvent
  4. 回调watcher 最后将watchedEvent对象交给EventThread线程,在下一个轮询周期中进行watcher回调

EventThread处理时间通知

上面我们知道服务端的watcher时间通知,最终交给了EventThread线程处理,EventThread是zookeeper客户端专门处理服务端通知事件的线程

客户端是被出事件类型EventType后,从相应的Watcher存储中移除对应watcher,同样客户端watcher机制是一次性的,一旦触发就会失效

获得所有的watcher之后,放到waitingEvents这个队列中,waitingEvents是一个待处理watcher的队列,EventThread的run方法不断的对队列进行处理。

EventThrad线程每次都会从waiting Events队列中取出一个watcher,并进行串行同步处理,注意此时处理的watcher才是客户端真正注册的watcher,调用器process方法就可以实现watcher的回调了。

不难发现zookeeper的watcher具有以下几个特性

  • 一次性 无论服务端或客户端,一旦一个watcher被触发,zookeeper都将其从相应的存储中移除,这样就可以减轻服务端的压力
  • 串行执行 客户端回调过程是一个重新同步的过程,这为了我们保证顺序,注意千万不要为了一个wacher处理逻辑影响了整个客户端的watcher回调
  • 轻量 watchedEvent是zookeeper整个watcher通知机制的最小通知单元,这个数据结构仅包含三部分内容,通知状态,时间类型,节点路径,也就是说watcher非常简单,只会告诉客户端发生了事件,而不会说事件的内容, 另外客户端向服务端注册watcher的时候,并不会把客户端真实的watcher对象传递服务端,仅仅只是在客户端请求中使用boolean类型属性进行了标记,同时服务端也仅仅只是保存了当前连接的ServerCnxn对象。

ACL机制

ACL权限控制,scheme:id:perm,来标识

权限模式,scheme,授权的策略

授权对象,id,授权的对象

权限,permssion,授权的权限

特点

  • zookeeper的权限控制是基于每一个znode节点,需要对每个节点设置权限
  • 每个权限支持设置多种权限控制方案和多个权限
  • 子节点不会继承父节点的权限,客户端无权访问某节点,但可能可以访问他的子节点

scheme授权的策略

  • world,默认方式,相当于全能访问
  • auth,代表已经真正通过的用户,cli中可以通过addauth digest user:pwd 来添加当前上下文中的授权用户
  • digest,即用户名:密码认证,也是业务最常用的,用username:password字符串SHA加密生成,然后在进行base64编码,最后生成的字符串作为ACL的授权对象即id,
  • ip,使用客户端主机作为ACL的id,
  • super,和digest一样

授权对象和权限模式的关系

权限

分为五种,增删改查管理,简称crwda

  • create,c可以创建子节点
  • delete,d可以删除子节点
  • read,r可以读取及诶单数据以及显示子节点列表
  • write,w可以甚至节点数据
  • admin,a可以设置节点访问控制列表权限
代码语言:javascript复制
getACL <path> 读取ACL权限
setACL <path> <acl> 设置ACL权限
addauth addauth <schemen> <auth> 添加认证用户
代码语言:javascript复制
create /test vale 创建节点
setACL /test world:anyone:acd 修改为所有人可以acd
getACL /test 获取权限
setACL /test ip:127.0.0.1:acd 修改id具有权限
addauth digest user:123456 增加授权用户,明文用户名和密码
setACL /test auth:user:cdwra 授予权限
setACL /test digest:user:6DY5WhzOfGsWQ1XFuIyzxkpwdPo=:crwda 授权

一次会话的创建过程

初始化阶段

  1. 初始化zookeeper对象 通过调用zookeeper的构造方法实例化一个zookeeper对象,同时创建一个客户端的watcher管理器:ClientWatchManaget
  2. 设置会话默认Watcher 如果在构造方法中传入一个watcher对象,那么客户端会将这个对象作为默认watcher保存在ClientWatchManager中
  3. 构造zookeeper服务器地址列表管理器,hostProvider 客户端会将其存放到服务器地址列表管理器HostProvider
  4. 创建并初始化客户端网络连接器:ClientCnxn 创建网络连接器ClientCnxn,用来管理客户端与服务端的网络交互,同时初始化客户端两个核心队列outgoingQueue和pengingQueue,分别作为客户端的请求发送队列和服务端响应的等待队列,客户端还会同时创建ClientCnxnSocket处理器
  5. 初始化SendThread和EventThread sendThread用于管理客户端和服务端的网络IO,后者用于进行客户端的事件处理,客户端还会将ClentCnxnSocket分配给sendThread作为底层网络IO处理器,并初始化EventThread的待处理事件队列waitingEvents,用于存放所有等待被客户端处理的事件

会话创建阶段

  1. 启动sendThred和EventThread sendThread首先会判断当前客户端的状态,进行一系列清理性工作,为客户端发送会话创建请求准备
  2. 获取服务器地址 sendThread首先需要获取一个zookeeper服务器的目标地址,通常是从HostProvider中随机获取一个地址,然后委托给ClientCnxnSocket去创建与zokeeper服务之间的TCP连接
  3. 创建TCP连接 获取一个地址之后,ClientCnxnSocket负责和服务器创建一个TCP长连接
  4. 构造ConnectRequest请求 上面步骤仅仅是完成了客户端与服务端之间的scoket连接,但远未完成zookeeper客户端的回话创建, sendThread会根据实际请求,构造一个connectRequest请求,代表了客户端与服务器创建一个回话,同时zookeeper客户端还会进一步将giant请求包装成网络IO层的packet对象,放入请求队列outgingQueue中去
  5. 发送请求 客户端准备完成之后,就可以向服务端发送请求了,ClientcnxnSocket负责从outgoingQueue中取出一个待发送的Packet对象,将其序列化成bytebuffer后,请服务器进行发送

响应处理阶段

  1. 接受服务端响应 clientcnxnSocker接受到服务daunt的响应后,首先判断客户端是否完成初始化如果没有,就认为此响应是回话创建请求的响应,直接交由readConnectResult方法来处理该响应
  2. 处理response ClientCnxnSocker会对接受到的服务端响应进行反序列化,得到ConnectResponse对象,并从中获取到Zookeeper服务端分配的回话sessionId,
  3. 连接成功 连接成功后,一方面需要通知sendThred线程,进一步对客户端参数设置,包括readTimeout和connectTimeout等,并更新客户端状态,另一方面,需要通知地址管理器HostProvider当前成功连接服务器地址
  4. 生成事件,SyncConnected-None 为了能让上层感知到会话的成功创建,sendThread生成一个时间SynConnected-None,代表客户端和服务端会话创建成功,并将该事件传递给EventThread线程
  5. 查询watcher EventThread收到事件后,会从ClientWatchManager管理器中查询对应watcher,针对SyncConnected-None事件,直接把存储的watcher,然后将其放到EventThread的waitingEvents队列中去
  6. 处理事件 EventThread不断从waitingEvents队列中取出待处理的watcher对象,然后直接调用该对象的process接口的方法,已达到触发watcher目的

单机服务器启动过程

预启动

  1. 统一有QuorumPeerMain作为启动类 无论是单机版还是集群版启动zookeeper服务器,在zkserver.cmd,zkServer.sh,两个脚本,都是配置了启动入口类为org.apache.zookeeper.server.quorum.QurumPeerMain
  2. 解析配置文件 其实就是对zoo.cfg文件的解析,比如tickTime,dataDir和clientPort等参数
  3. 启动创建历史文件清理器DatadirCleanupManager 3.4.0版本开始,zookeeper增加了自动清理历史数据的机制,包括对事务日志和快照数据文件进行定时清理
  4. 判断是否是单机还是集群 如果是单机,就会委托给zookeeperServerMain进行启动处理
  5. 再次解析配置文件zoo.cfg
  6. 创建服务器实例zookeeperServer zookeeperserver是单机版zookeeper服务器最为核心的实例类,zookeeper服务器首先会进行服务器实例化创建,接下来就是对服务器实例的初始化工作,包括连接器,内存数据库,和请求处理器等组件初始化

初始化

  1. 创建服务器统计器ServerStats serverStats是zookeeper服务器运行时的统计器,包括了基本的运行时信息,响应包次数,请求包次数,最大延迟,最小延迟,总延迟,客户端请求总次数
  2. 创建zookeeper数据管理器FileTxnSnapLog FilerTxnSnapLog是zookeeper上层服务器和底层数据库的对接层,提供了一系列操作数据文件的接口,包括事务日志文件,和快照数据文件.
  3. 设置服务器tickTime和会话超时时间限制、
  4. 创建serverCnxnFactory 通过系统属性zookeeper.serverCnxnFactory来指定使用zookeeper自己实现的NIO还是使用Netty框架作为zookeeper服务器网路连接工厂
  5. 初始化ServerCnxnFactory zookeeper首先初始化一个Thread,作为整个ServerCnxnFactory的主线程,然后再初始化NIO服务器
  6. 启动serverCnxnFactory主线程 虽然这里zookeeper的NIO服务器已经对外开放端口,客户端能够访问到zookeeper的客户端服务端口2181,但是此时zookeeper服务器是无法正常处理客户端请求的
  7. 恢复本地数据 每次启动,都会从本地快照数据文件和事务日志文件中进行数据恢复
  8. 创建并启动会话管理器 创建会话管理器SessionTracker,主要负责服务端的会话管理,
  9. 初始化zookeeper的请求处理连 zookeeper请求处理方式是典型的责任链模式的实现,在zookeeper服务器上,会有多个请求处理器一次处理一个客户端请求,服务端启动的时候,会将这些请求处理器串联形成一个请求处理链,
  1. 注册JMX服务 zookeeper将服务器运行的一些信息以JMX方式暴露给外部
  2. 注册zookeeper服务器实例 前面serverCnxnFactory主线程启动,但是同时我们提到无法处理客户端请求,原因是此时网络层尚不能访问zookeeper服务器实例,在进过后续的初始化,zookeeper服务器实例化完毕,只要注册给ServerCnxnFactory即可,之后就可以对外正常提供服务了

集群zookeeper服务器启动流程

预启动

  1. 统一有QurumPeerMain最为启动类
  2. 解析配置文件
  3. 创建并启动历史文件清理器DataDirCleanManager
  4. 判断是否是集群模式还是单机模式

初始化

  1. 创建serverCnxnFactory
  2. 初始化ServerCnxnFatory
  3. 创建zookeeper数据管理器FileTxnSnapLog
  4. 创建QuorumPeer实例 Quorum是集群模式下特有的对象,是zookeeper服务器实例的托管者,从集群层面看,Quorumpeer代表了zookeeper集群的一台机器,当运行期间,QuorumPeer会不断检测当前服务器实例的运行状态,同时根据情况发起Leader选举
  5. 创建内存数据库ZKDatabase ZKDatabase是zookeeper的多有会话记录以及DataTree和事务日志的存储
  6. 初始化QuorumPeer Quorumpeer是zookeeperServer的托管者,因此需要将一些核心组件注册到QuorumPeer中去,包括FileTxnSnapLog,serverCnxnFactory,和zkdatabase,包括服务器地址,leader选举算法,和会话超时限制等
  7. 恢复本地数据
  8. 启动serverCnxnFaxtory主线程

Leader选举

  1. 初始化leader选举 zookeeper会根据自身的SID,LastLoggedZxid(最新zxid),服务器epoch,来生成以一个初始化投票,即初始化过程,都会投自己一票 默认有三种选举算法,分别是leaderElection,AuthFastLeaderElection和FastLeaderElection,通过配置文件配置,但是在3.4.0开始废弃了前两种算法,只支持FastLeaderElection选举算法 初始化阶段,zk会首先创建leader选举所需的网络Io层QuorumCnxManager,同时启动对Leader选举端口监听,等待集群中其他机器创建连接
  2. 注册JMX服务
  3. 检测当前服务状态
  4. leader选举 关于leader算法,简而言之,就是这个阶段那个机器的数据最新(即zxid最大),越可能成为leader,如果相同,SID最大成为服务器Leader

Leader和Follower启动期交互过程

上面已经选举出了Leader,且每个机器确定了自己的角色,即Leader和Follower,下面我们看看leader和Follower交互的流程

  1. 创建leader服务器和Follower服务器 每个服务器根据自己的角色,开始进入主流程
  2. leader服务器启动Follower接受器leaderCnxAccept leaderCnxAcceptor接收器用来接受非leader服务器的连接请求
  3. follower服务器开始和leader建立连接
  4. leader服务器创建LearnerHandler leader接收到其他机器连接创建请求后,会常见一个leaderHander,他代表了leader和follower的之间的链接,服务leader和Follower之间的数据同步和消息通信
  5. 向leader注册 follower服务器和leader连接后,follower就会向leader进行注册,即将自己的信息发送给leader服务器
  6. leader解析Follower信息,计算新的epoch
  7. 发送Leader状态 计算出新的epoch之后,leader将信息以一个LEADERINFO消息形式发送个Follower,同时等待follower响应
  8. Follower发送ack消息 Follower接收到LEADERINFO信息后,会解析出epoch和ZXID,然后向Leader反馈一个ACKEPOCH响应
  9. 数据同步
  10. 启动Leader和follwer服务器

Leader和Follower启动

  1. 创建并启动会话管理器
  2. 初始化zookeeper的请求处理链
  3. 注册JMX服务
  4. 启动完毕

Leader选举详解

  • 服务器启动时期的Leader选举
  • 服务运行期间的Leader选举

服务器启动时期的leader选举

我们以3台服务器说明问题,假设server1(myid=1),server2(myid=2),server3(myid=3),在服务器集群初始化的时候,他是无法完成leader选举的,当第二台服务器启动后,此时两台可以进行互相通信,每台机器都尝试成为leader,于是进入了leader选举流程

  1. 每个server发出一个投票 每个server都会投给自己(myid,zxid),即server1的投票为(1,0),server2的投票(2,0),然后将这个投票发给集群中其他多有机器
  2. 接受来自各个服务器的投票 集群中每个服务器接受到投票后,首先判断是否有效,包括是否是本轮投票是否来自LOOKING状态的服务器
  3. 处理投票 在接受到投票之后,然后拿自己的投票和接受到别人的投票进行PK,规则如下
    1. 优先检查ZXID,ZXID比较大的服务器优先作为leader
    2. ZXID相等,比较myid,myid比较大的服务器作为leader

现在看到server1的投票是(1,0),接受到的投票是(2,0),首先会对比两者ZXID,因为都是0,因此比较myid,显然server2的myid大于server1,因此server1更改自己的投票为(2.0),然后重新发送出去,而对于server2不需要更改投票,只是再一次的向集群中所有机器发出上一次投票信息即可

  1. 统计投票 每次投票后,服务器统计投票结果,判断是否已经有过半机器接受到相同的投票信心,对server1和server2服务器说,统计出已经有两台服务器接受到了(2,0),此时就会选举server2为leader
  2. 改变服务器状态 一旦确定了leader,每个服务器更新自己的状态,如果是follower,那么就是变更为FOLLOWING,如果是leaeder,就变更为LEADING

服务器运行期间的leader选举

当集群正常运行中,leader服务器宕机了,此时整个集群将暂时无法对外服务,而是进入新一轮的Leader选举,比如3台机器,分别是server1,server2,server3,其中server2是leader,但是宕机了,这个时候开始leader选举

  1. 变更状态 当leader挂了之后,余下非Observer服务器将经自己的服务器状态变更为LOOKING,然后进入leader选举流程
  2. 每个server会发出一个投票 假设server1为的ZXID为123,而server3为ZXID为122,在一轮投票中,server1和server3都会投自己,即分别产生投票(1,123)和(3,122),然后将这个投票发给了集群中所有机器
  3. 接受来自各个服务器的投票
  4. 处理投票 由于server1的ZXID为123,server3的ZXID为122,显然server1为leader
  5. 统计投票
  6. 改变服务器装填

0 人点赞