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的。
基本逻辑如下
- 封装wachedEvent 首先将通知状态(keeperState),事件类型(EventType)以及节点路径(Path)分装一个watchedEvent对象
- 查询watcher 根据数据节点的路径从watcherTable去除对应watcher,如果没有找到watcher,说明没有任何客户端在该数据节点上注册过watcher,直接退出,如果找到这个watcher,将其去除,同时会直接从watchTable和watch2Paths中将其删除,从这里我们可以看出wacher在服务端是一次性的
- 调用process方法触发watcher
这一步直接拿到第二步的watcher的process方法,其实这watcher即使之前注册的ServerCnxn,而在这process的主要逻辑如下
- 请求红标记-1,表明当前是一个通知
- 将watchedEvent包装成watcherEvent,以便网络传出序列化
- 向客户端发送该通知
我们看到真正的逻辑不是服务端进行的,而是有客户端连接的ServerCnxn对象实现对客户端WatchedEvent传递,真正的客户端watcher回调与业务员逻辑执行都在客户端,
客户端回到watcher
上面我们已经知道如何触发了watcher,并且知道最终服务端会通过使用ServerCnxn对应的TCP连接向客户端发送一个WatcherEvent事件,
对于一个服务端的响应,客户端都是有sendThread.readResponse方法进行统一处理,具体逻辑如下
- 分序列化 zookeeper客户端接到请求后,首先会将字节流转换成watcherEvent对象
- 处理chrootPath 如何客户端设置了chrootPath属性,那么需要对服务端传过来的完整节点路径进行chrootPath处理,生成一个相对节点路径,例如客户端设置了chrootPath为/app1,服务端传过来的响应包含的节点路径为/app/locks,经过chrootPath处理后,变成相对路径/locks
- 还原watchedEvent 回调方法process方法,参数定义是watchedEvent,因此这里将watcherEvent对象转换成watchedEvent
- 回调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可以设置节点访问控制列表权限
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 授权
一次会话的创建过程
初始化阶段
- 初始化zookeeper对象 通过调用zookeeper的构造方法实例化一个zookeeper对象,同时创建一个客户端的watcher管理器:ClientWatchManaget
- 设置会话默认Watcher 如果在构造方法中传入一个watcher对象,那么客户端会将这个对象作为默认watcher保存在ClientWatchManager中
- 构造zookeeper服务器地址列表管理器,hostProvider 客户端会将其存放到服务器地址列表管理器HostProvider
- 创建并初始化客户端网络连接器:ClientCnxn 创建网络连接器ClientCnxn,用来管理客户端与服务端的网络交互,同时初始化客户端两个核心队列outgoingQueue和pengingQueue,分别作为客户端的请求发送队列和服务端响应的等待队列,客户端还会同时创建ClientCnxnSocket处理器
- 初始化SendThread和EventThread sendThread用于管理客户端和服务端的网络IO,后者用于进行客户端的事件处理,客户端还会将ClentCnxnSocket分配给sendThread作为底层网络IO处理器,并初始化EventThread的待处理事件队列waitingEvents,用于存放所有等待被客户端处理的事件
会话创建阶段
- 启动sendThred和EventThread sendThread首先会判断当前客户端的状态,进行一系列清理性工作,为客户端发送会话创建请求准备
- 获取服务器地址 sendThread首先需要获取一个zookeeper服务器的目标地址,通常是从HostProvider中随机获取一个地址,然后委托给ClientCnxnSocket去创建与zokeeper服务之间的TCP连接
- 创建TCP连接 获取一个地址之后,ClientCnxnSocket负责和服务器创建一个TCP长连接
- 构造ConnectRequest请求 上面步骤仅仅是完成了客户端与服务端之间的scoket连接,但远未完成zookeeper客户端的回话创建, sendThread会根据实际请求,构造一个connectRequest请求,代表了客户端与服务器创建一个回话,同时zookeeper客户端还会进一步将giant请求包装成网络IO层的packet对象,放入请求队列outgingQueue中去
- 发送请求 客户端准备完成之后,就可以向服务端发送请求了,ClientcnxnSocket负责从outgoingQueue中取出一个待发送的Packet对象,将其序列化成bytebuffer后,请服务器进行发送
响应处理阶段
- 接受服务端响应 clientcnxnSocker接受到服务daunt的响应后,首先判断客户端是否完成初始化如果没有,就认为此响应是回话创建请求的响应,直接交由readConnectResult方法来处理该响应
- 处理response ClientCnxnSocker会对接受到的服务端响应进行反序列化,得到ConnectResponse对象,并从中获取到Zookeeper服务端分配的回话sessionId,
- 连接成功 连接成功后,一方面需要通知sendThred线程,进一步对客户端参数设置,包括readTimeout和connectTimeout等,并更新客户端状态,另一方面,需要通知地址管理器HostProvider当前成功连接服务器地址
- 生成事件,SyncConnected-None 为了能让上层感知到会话的成功创建,sendThread生成一个时间SynConnected-None,代表客户端和服务端会话创建成功,并将该事件传递给EventThread线程
- 查询watcher EventThread收到事件后,会从ClientWatchManager管理器中查询对应watcher,针对SyncConnected-None事件,直接把存储的watcher,然后将其放到EventThread的waitingEvents队列中去
- 处理事件 EventThread不断从waitingEvents队列中取出待处理的watcher对象,然后直接调用该对象的process接口的方法,已达到触发watcher目的
单机服务器启动过程
预启动
- 统一有QuorumPeerMain作为启动类 无论是单机版还是集群版启动zookeeper服务器,在zkserver.cmd,zkServer.sh,两个脚本,都是配置了启动入口类为org.apache.zookeeper.server.quorum.QurumPeerMain
- 解析配置文件 其实就是对zoo.cfg文件的解析,比如tickTime,dataDir和clientPort等参数
- 启动创建历史文件清理器DatadirCleanupManager 3.4.0版本开始,zookeeper增加了自动清理历史数据的机制,包括对事务日志和快照数据文件进行定时清理
- 判断是否是单机还是集群 如果是单机,就会委托给zookeeperServerMain进行启动处理
- 再次解析配置文件zoo.cfg
- 创建服务器实例zookeeperServer zookeeperserver是单机版zookeeper服务器最为核心的实例类,zookeeper服务器首先会进行服务器实例化创建,接下来就是对服务器实例的初始化工作,包括连接器,内存数据库,和请求处理器等组件初始化
初始化
- 创建服务器统计器ServerStats serverStats是zookeeper服务器运行时的统计器,包括了基本的运行时信息,响应包次数,请求包次数,最大延迟,最小延迟,总延迟,客户端请求总次数
- 创建zookeeper数据管理器FileTxnSnapLog FilerTxnSnapLog是zookeeper上层服务器和底层数据库的对接层,提供了一系列操作数据文件的接口,包括事务日志文件,和快照数据文件.
- 设置服务器tickTime和会话超时时间限制、
- 创建serverCnxnFactory 通过系统属性zookeeper.serverCnxnFactory来指定使用zookeeper自己实现的NIO还是使用Netty框架作为zookeeper服务器网路连接工厂
- 初始化ServerCnxnFactory zookeeper首先初始化一个Thread,作为整个ServerCnxnFactory的主线程,然后再初始化NIO服务器
- 启动serverCnxnFactory主线程 虽然这里zookeeper的NIO服务器已经对外开放端口,客户端能够访问到zookeeper的客户端服务端口2181,但是此时zookeeper服务器是无法正常处理客户端请求的
- 恢复本地数据 每次启动,都会从本地快照数据文件和事务日志文件中进行数据恢复
- 创建并启动会话管理器 创建会话管理器SessionTracker,主要负责服务端的会话管理,
- 初始化zookeeper的请求处理连 zookeeper请求处理方式是典型的责任链模式的实现,在zookeeper服务器上,会有多个请求处理器一次处理一个客户端请求,服务端启动的时候,会将这些请求处理器串联形成一个请求处理链,
- 注册JMX服务 zookeeper将服务器运行的一些信息以JMX方式暴露给外部
- 注册zookeeper服务器实例 前面serverCnxnFactory主线程启动,但是同时我们提到无法处理客户端请求,原因是此时网络层尚不能访问zookeeper服务器实例,在进过后续的初始化,zookeeper服务器实例化完毕,只要注册给ServerCnxnFactory即可,之后就可以对外正常提供服务了
集群zookeeper服务器启动流程
预启动
- 统一有QurumPeerMain最为启动类
- 解析配置文件
- 创建并启动历史文件清理器DataDirCleanManager
- 判断是否是集群模式还是单机模式
初始化
- 创建serverCnxnFactory
- 初始化ServerCnxnFatory
- 创建zookeeper数据管理器FileTxnSnapLog
- 创建QuorumPeer实例 Quorum是集群模式下特有的对象,是zookeeper服务器实例的托管者,从集群层面看,Quorumpeer代表了zookeeper集群的一台机器,当运行期间,QuorumPeer会不断检测当前服务器实例的运行状态,同时根据情况发起Leader选举
- 创建内存数据库ZKDatabase ZKDatabase是zookeeper的多有会话记录以及DataTree和事务日志的存储
- 初始化QuorumPeer Quorumpeer是zookeeperServer的托管者,因此需要将一些核心组件注册到QuorumPeer中去,包括FileTxnSnapLog,serverCnxnFactory,和zkdatabase,包括服务器地址,leader选举算法,和会话超时限制等
- 恢复本地数据
- 启动serverCnxnFaxtory主线程
Leader选举
- 初始化leader选举 zookeeper会根据自身的SID,LastLoggedZxid(最新zxid),服务器epoch,来生成以一个初始化投票,即初始化过程,都会投自己一票 默认有三种选举算法,分别是leaderElection,AuthFastLeaderElection和FastLeaderElection,通过配置文件配置,但是在3.4.0开始废弃了前两种算法,只支持FastLeaderElection选举算法 初始化阶段,zk会首先创建leader选举所需的网络Io层QuorumCnxManager,同时启动对Leader选举端口监听,等待集群中其他机器创建连接
- 注册JMX服务
- 检测当前服务状态
- leader选举 关于leader算法,简而言之,就是这个阶段那个机器的数据最新(即zxid最大),越可能成为leader,如果相同,SID最大成为服务器Leader
Leader和Follower启动期交互过程
上面已经选举出了Leader,且每个机器确定了自己的角色,即Leader和Follower,下面我们看看leader和Follower交互的流程
- 创建leader服务器和Follower服务器 每个服务器根据自己的角色,开始进入主流程
- leader服务器启动Follower接受器leaderCnxAccept leaderCnxAcceptor接收器用来接受非leader服务器的连接请求
- follower服务器开始和leader建立连接
- leader服务器创建LearnerHandler leader接收到其他机器连接创建请求后,会常见一个leaderHander,他代表了leader和follower的之间的链接,服务leader和Follower之间的数据同步和消息通信
- 向leader注册 follower服务器和leader连接后,follower就会向leader进行注册,即将自己的信息发送给leader服务器
- leader解析Follower信息,计算新的epoch
- 发送Leader状态 计算出新的epoch之后,leader将信息以一个LEADERINFO消息形式发送个Follower,同时等待follower响应
- Follower发送ack消息 Follower接收到LEADERINFO信息后,会解析出epoch和ZXID,然后向Leader反馈一个ACKEPOCH响应
- 数据同步
- 启动Leader和follwer服务器
Leader和Follower启动
- 创建并启动会话管理器
- 初始化zookeeper的请求处理链
- 注册JMX服务
- 启动完毕
Leader选举详解
- 服务器启动时期的Leader选举
- 服务运行期间的Leader选举
服务器启动时期的leader选举
我们以3台服务器说明问题,假设server1(myid=1),server2(myid=2),server3(myid=3),在服务器集群初始化的时候,他是无法完成leader选举的,当第二台服务器启动后,此时两台可以进行互相通信,每台机器都尝试成为leader,于是进入了leader选举流程
- 每个server发出一个投票 每个server都会投给自己(myid,zxid),即server1的投票为(1,0),server2的投票(2,0),然后将这个投票发给集群中其他多有机器
- 接受来自各个服务器的投票 集群中每个服务器接受到投票后,首先判断是否有效,包括是否是本轮投票是否来自LOOKING状态的服务器
- 处理投票
在接受到投票之后,然后拿自己的投票和接受到别人的投票进行PK,规则如下
- 优先检查ZXID,ZXID比较大的服务器优先作为leader
- ZXID相等,比较myid,myid比较大的服务器作为leader
现在看到server1的投票是(1,0),接受到的投票是(2,0),首先会对比两者ZXID,因为都是0,因此比较myid,显然server2的myid大于server1,因此server1更改自己的投票为(2.0),然后重新发送出去,而对于server2不需要更改投票,只是再一次的向集群中所有机器发出上一次投票信息即可
- 统计投票 每次投票后,服务器统计投票结果,判断是否已经有过半机器接受到相同的投票信心,对server1和server2服务器说,统计出已经有两台服务器接受到了(2,0),此时就会选举server2为leader
- 改变服务器状态 一旦确定了leader,每个服务器更新自己的状态,如果是follower,那么就是变更为FOLLOWING,如果是leaeder,就变更为LEADING
服务器运行期间的leader选举
当集群正常运行中,leader服务器宕机了,此时整个集群将暂时无法对外服务,而是进入新一轮的Leader选举,比如3台机器,分别是server1,server2,server3,其中server2是leader,但是宕机了,这个时候开始leader选举
- 变更状态 当leader挂了之后,余下非Observer服务器将经自己的服务器状态变更为LOOKING,然后进入leader选举流程
- 每个server会发出一个投票 假设server1为的ZXID为123,而server3为ZXID为122,在一轮投票中,server1和server3都会投自己,即分别产生投票(1,123)和(3,122),然后将这个投票发给了集群中所有机器
- 接受来自各个服务器的投票
- 处理投票 由于server1的ZXID为123,server3的ZXID为122,显然server1为leader
- 统计投票
- 改变服务器装填