Zookeeper的数据模型
节点特性
临时节点(生命周期
持久化节点
有序节点(递增的序列号)
有序节点的使用场景 有序节点: 全局ID、分布式锁、分布式队列
先有父节点,再有子节点
临时节点下不能存在子节点
同级节点下,节点名字必须是唯一
弱一致性模型
2pc协议( 原子性 )
过半提交
QuorumPeerMain 是zookeeper集群的启动入口类,是用来加载配置启动QuorumPeer线程的
ZooKeeperServerMain 是zookeeper单机的启动入口类
Leader角色 Leader服务器是整个zookeeper集群的核心,主要的工作任务有两项
1. 事物请求的唯一调度和处理者,保证集群事物处理的顺序性
2. 集群内部各服务器的调度者
Follower角色 Follower角色的主要职责是
1. 处理客户端非事物请求、转发事物请求给leader服务器
2. 参与事物请求Proposal的投票(需要半数以上服务器通过才能通知leader commit数据; Leader发起的提案,要求Follower投票)
3. 参与Leader选举的投票
Observer角色 Observer是zookeeper3.3开始引入的一个全新的服务器角色,从字面来理解,该角色充当了观察者的角色。
观察zookeeper集群中的最新状态变化并将这些状态变化同步到observer服务器上。
Observer的工作原理与follower角色基本一致,而它和follower角色唯一的不同在于observer不参与任何形式的投票,包括事物请求Proposal的投票和leader选举的投票。
简单来说,observer服务器只提供非事物请求服务,通常在于不影响集群事物处理能力的前提下提升集群非事物处理的能力
Leader选举 leader选举存在与两个阶段中,
一个是服务器启动时的leader选举。
另一个是运行过程中leader节点宕机导致的leader选举 ;
了解几个重要的参数
服务器ID(myid) 比如有三台服务器,编号分别是1,2,3。编号越大在选择算法中的权重越大。
zxid 事务id 值越大说明数据越新,在选举算法中的权重也越大
逻辑时钟(epoch – logicalclock) 或者叫投票的次数,同一轮投票过程中的逻辑时钟值是相同的。
每投完一次票这个数据就会增加,然后与接收到的其它服务器返回的投票信息中的数值相比,根据不同的值做出不同的判断选举状态
LOOKING,竞选状态。
FOLLOWING,随从状态,同步leader状态,参与投票。
OBSERVING,观察状态,同步leader状态,不参与投票。
LEADING,领导者状态。
投票机制
QuorumPeerMain 启动类
初始化
启动2181的端口(独立的业务服务),监听客户端请求(zkClient)
启动(2888、 3888)这个端口的监听
初始化leader选举(----)
开启leader选举
加载磁盘的数据
MetricsProvider 指标数据
QuorumPeerConfig 加载配置(zoo.cfg)
QuorumPeer 集群节点的信息
ServerCnxnFactory.configure 默认为 NIOServerCnxnFactory ServerCnxnFactory.createFactory()
AcceptThread 用于处理接收客户端的请求
SelectorThread 用来处理selector的读写请求 根据CPU个数除以2 取算数平方根
quorumPeer.start()
loadDataBase(); //加载数据
startServerCnxnFactory(); //这里来启动 2181 的服务监听. ServerSocketChannel
startLeaderElection(); //开启leader选举
this.electionAlg = createElectionAlgorithm(electionType); 根据electionType 来创建选举算法
QuorumCnxManager 管理集群选举和投票相关的操作
createElectionAlgorithm
QuorumCnxManager.Listener listener = qcm.listener; 监听集群中的票据
QuorumCnxManager.Listener.run -> ListenerHandler.run -> acceptConnections
client = serverSocket.accept(); 接收请求 最后通过参数 quorumSaslAuthEnabled 判断是异步处理还是同步处理
receiveConnectionAsync(client); -> connectionExecutor.execute 进入 QuorumConnectionReceiverThread.run
receiveConnection(client); -> handleConnection
殊途同归 最终调用 receiveConnection -> handleConnection 生成两个线程 去进行集群节点的通信
SendWorker sw = new SendWorker(sock, sid); 从 queueSendMap 中拿到集群节点的信息
RecvWorker rw = new RecvWorker(sock, din, sid, sw); 把通讯返回的节点信息 放入 recvQueue 中
FastLeaderElection fle = new FastLeaderElection(this, qcm); 初始化了FastLeaderElection
this.messenger = new Messenger(manager); 初始化两个线程
1:this.ws = new WorkerSender(manager); 对应 sendqueue
WorkerSender.run -> process(m); -> manager.toSend(m.sid, requestBuffer);
最终进入到 QuorumCnxManager.toSend -> connectOne(sid);
connectOne -> initiateConnectionAsync -> -> connectionExecutor.execute
进入到 QuorumConnectionReqThread.run 方法 -> initiateConnection -> startConnection(sock, sid);
DataOutputStream 输出流写出数据
2:this.wr = new WorkerReceiver(manager); 从 recvqueue 中取出通讯返回来的节点信息
把节点信息 放入 queueSendMap 中 让 SendWorker 拿节点信息进行投票选举的通信
super.start(); //启动线程。进入 QuorumPeer.run 方法
run 方法 while循环
looking -> setCurrentVote(makeLEStrategy().lookForLeader()); 得到vote是一个leader的vote.->当前节点一定会在选举算法中,得到leader之后,重设设置一个状态
lookForLeader -> updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());//myid, zxid , epoch 把自己的投票的proposal设置成自己的信息
sendNotifications(); //发送通知
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) 只要当前节点的状态是LOOKING,就不断的循环
其他节点会同步leader节点的epoch,然后发送过来进行比较,也就是epoch趋于一致,比较myid和zxid,最后完成投票
watch机制
客户端
1: ClientCnxn 初始化 在 ZooKeeper 类构造方法
2: exists(getChild,addWatch等事件)注册watcher监听
3:ClientCnxn.submitRequest
调用queuePacket,把请求数据添加到队列 outgoingQueue 队列(LinkedBlockingDeque<ClientCnxn.Packet> outgoingQueue)
通过packet.wait使得当前线程一直阻塞,直到请求完成
4: SendThread.run 在Zookeeper这个对象初始化的时候,启动了一个 SendThread,这个线程会从 outgoingQueue 中获取任务,然后发送到服务端处理
最后执行 clientCnxnSocket.doTransport
5:ClientCnxnSocketNIO.doTransport 调用协议层进行数据传输。
读写请求调用 doIO(pendingQueue, cnxn)
6: ClientCnxnSocketNIO.doIO
找到可以发送的 packet 如果Packet的byteBuffer没有创建,那么就创建
outgoingQueue 从待发送队列中移除 packet
把信息放入 SocketChannel 进行通讯
7: 客户端收到请求后的处理 sendThread.readResponse
客户端接收请求的处理是在ClientCnxnSocketNIO的doIO中,之前客户端发起请求是写,现在客户端收到请求,则是一个读操作,也就是当客户端收到服务端的数据时会触发一下代码的执行。
其中很关键的是sendThread.readResponse(incomingBuffer);来接收服务端的请求。
这个方法里面主要的流程如下首先读取header,如果其xid == -2,表明是一个ping的response,return
如果xid是 -4 ,表明是一个AuthPacket的response return
如果xid是-1,表明是一个notification,此时要继续读取并构造一个enent,通过EventThread.queueEvent发送,return
其它情况下:从pendingQueue拿出一个Packet,校验后更新packet信息对于exists请求,返回的xid=1,则进入到其他情况来处理
SendThread.readResponse 调用 finishPacket
8: finishPacket 通过前面客户端和服务端的交互,可以确定服务端已经成功保存了watcher这个事件,那么受到服务端的确认之后,客户端会把这个watcher保存到本地的事件中。
所以,finishPacket主要功能是把从 Packet 中取出对应的 Watcher 并注册到 ZKWatchManager 中去
watchRegistration.register
Map<String, Set<Watcher>> watches = getWatches(rc); 把path对应的watcher本地回调保存到一个集合中。
ExistsWatchRegistration.getWatches
DataWatchRegistration.getWatches
ChildWatchRegistration.getWatches
watchers.add(watcher); //把watcher保存到watches集合,此时的watcher对应的就是在exists方法中传入的匿名内部类
ZkWatchManager ZkWatchManager 是客户端这边用来保存本地节点对应的 watcher 回调的管理类,提供了三种不同的事件管理机制。
eventThread.queuePacket
processEvent 进入到 实现 Watcher 接口的实现类 表明节点发生了变化
服务端
1: zookeeper启动的时候,通过下面的代码构建了一个ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory(); 监听 2181 端口
并且,在QuorumPeer.start()->startServerCnxnFactory()->cnxnFactory.start(); 中,启动了一个 acceptThread 线程,这个线程从名字上看,应该是用来处理客户端的来请求
2: AcceptThread.run 在run方法中,调用了select()方法
select方法中,会通过复路器Selector,去进行select操作,获取就绪的连接。其中select这个方法中主要做的事情是遍历所有的就绪连接,进行连接的判断调用 doAccept 方法进行处理
3: doAccept 真正读取客户端的请求
轮询,将当前连接分配给选择器线程 SelectorThread
selectorThread.addAcceptedConnection 把当前连接再丢给SelectorThread来处理 添加到接收队列 acceptedQueue,后续会为该连接注册读写事件
4: SelectorThread.run
1: select(); 处理多路复用
从selector中获取就绪的连接,针对读写事件,调用 handleIO 方法进行处理。
2: processAcceptedConnections(); 处理连接请求
3: processInterestOpsUpdateRequests(); 注册一个更新请求
5: SelectorThread.run -> select -> handleIO -> WorkerService.schedule -> ScheduledWorkRequest.run -> NIOServerCnxnFactory.IOWorkRequest.doWork
通过N个异步化处理过程,最终进入到 ZookeeperServer.processPacket
调用链路: WorkerService.schedule -> ScheduledWorkRequest.run -> IOWorkRequest.doWork->NIOServerCnxn.doIO->readPayload->readRequest->processPacket
6: ZookeeperServer.processPacket
根据数据包的类型来处理不同的数据包,对于读写请求
7: ZookeeperServer.submitRequest 将请求添加到 RequestThrottler(限流器)中去处理,它是一个线程,而 submitRequest 方法实际就是把任务添加到阻塞队列。
8: RequestThrottler.submitRequest
RequestThrottler 本身就是一个线程 进入到 run 方法 在 RequestThrottler 的 run 方法中,会从阻塞队列中取出任务进行处理。
9:ZookeeperServer.submitRequestNow
firstProcessor 这个是一个责任链模式 firstProcessor 的初始化是在 ZookeeperServer.setupRequestProcessor 中完成的
firstProcessor = PrepRequestProcessor(SyncRequestProcessor(FinalRequestProcessor))
10: PredRequestProcessor.processRequest
通过上面的调用链关系以后,firstProcessor.processRequest(si); 会调用到 PrepRequestProcessor 在这个处理器中,又把请求对象提交到了阻塞队列中
PredRequestProcessor 是一个线程,在构建处理器链的时候,就已经启动了这个线程,所以直接进入到 run 方法中。
PreRequestProcessor 一般是放在处理链的起始部分的,它对请求做一些预处理
1. 检查Session 、2. 检查要操作的节点及其父节点是否存在 、3. 检查客户端是否有权限
pRequest 方法比较长,主要逻辑就是根据不同的请求类型实现不同的操作。
SyncRequestProcessor 是一个线程,在构建处理器链的时候,就已经启动了这个线程,所以直接进入到 run 方法中。
这个 processor 负责把写request持久化到本地磁盘,为了提高写磁盘的效率,这里使用的是缓冲写,但是会周期性(1000个request)的调用flush操作,flush之后request已经确保写到磁盘了
同时他还要维护本机的txnlog和snapshot
每隔snapCount/2个request会重新生成一个snapshot并滚动一次txnlog,同时为了避免所有的zookeeper server在同一个时间生成snapshot和滚动日志,
这里会再加上一个随机数,snapCount的默认值是10w个request
FinalRequestProcessor 是一个线程,在构建处理器链的时候,就已经启动了这个线程,所以直接进入到 run 方法中。
这个是最终的一个处理器,主要负责把已经commit的写操作应用到本机,对于读操作则从本机中读取数据并返回给client
ProcessTxnResult rc = zks.processTxn(request); 修改内存中的数据.(DataTree)
processTxnInDB() -> getZKDatabase().processTxn() -> DataTree.processTxn
最终在 FinalRequestProcessor.run 找到一个很关键的代码,判断请求的getWatch是否存在,如果存在,则传递cnxn(servercnxn) //对于exists请求,需要监听data变化事件,添加watcher
exists -> Stat stat = zks.getZKDatabase().statNode(path, existsRequest.getWatch() ? cnxn : null);
statNode statNode应该会做两个事情 1:获取指定节点的元数据 、 2:保存针对该节点的事件监听
DataNode n = nodes.get(path) 根据path获取节点数据
dataWatches.addWatch(path, watcher) 如果watcher不为空,则将当前的watcher和path进行绑定
WatchManager.addWatch 通过WatchManager来保存指定节点的事件监听,WatchManager维护了两个集合。 watchTable 、watch2Paths
private final Map<String, Set<Watcher>> watchTable = new HashMap<>(); 表示从节点路径到 watcher 集合的映射
private final Map<Watcher, Set<String>> watch2Paths = new HashMap<>(); 表示从 watcher 到所有节点路径集合的映射
设置watch的模式
watch 有三种类型,
默认为一次性的
一种是PERSISTENT、 前者是持久化订阅
一种是PERSISTENT_RECURSIVE、STANDARD,后者是持久化递归订阅,所谓递归订阅就是针对监听的节点的子节点的变化都会触发监听,
watcherModeManager.setWatcherMode(watcher, path, watcherMode);
返回处理结果 在 FinalRequestProcessor 的 processRequest 方法中,将处理结果rsp返回给客户端