Zookeeper源码分析笔记[2]-服务端源码分析

Zookeeper源码分析笔记[2]-服务端源码分析

  相比于客户端,服务端的整体更加的复杂,本文将从网络连接开始一步一步分析Zk的服务端功能,只分析单机版,主要包括:

  • 服务端初始化整体流程
  • 目录树数据初始化
  • 网络连接管理
  • 会话管理
  • 请求处理
  • 服务端通知机制实现

  内容有点小多,现在开始

1、服务端初始化整体流程

服务端初始化流程图
在这里插入图片描述

  小旁白里说到了目录树,其实Zk的核心存储结构,会在目录树初始化的小结中介绍

2、目录树数据初始化

  Zk内部的核心数据存储结构是一棵目录树,叫ZkDataBase,通过客户端创建的节点数据都存储在里面,该数据结构内比较重要的两个成员是:

1)、DataTree,其实际是一个Map,Map的key是字符串,value是DataNode,展开之后的结构是一个目录树结构,DataNode内部存储的数据有:节点的数据、权限、事务Id、父DataNode、子节点名称集合等信息【命令行get /aaa获得的数据就是存储在这个类】,zk中有三种节点,分别是

  • 永久性节点(和会话无关,创建之后一直存在)
  • 临时节点(和某个会话绑定,会话消失后,节点消失)
  • 序列节点(允许客户端创建名称相同的节点,zk会为原始路径拼接上唯一性标志节点唯一,序列节点必须和前面两个类型结合使用)

2)、sessionsWithTimeouts,会话信息,也是一个Map,key是会话Id,value是超时时间【zk会为每个连接分配一个唯一性的会话Id,标识一个连接或者说是客户端,在分布式环境下,如果某个客户端连接的服务器发生故障下线,客户端重连时会带着之前分配的会话Id,该会话Id信息在所有节点都有保存,所以即使连接到其他服务器节点,也不会被当成是新的连接,超时除外】

  Zk内部将请求分成了两类,事务性操作和非事务性操作,基本上只要对目录树有变更的操作都属于事务性操作,查询动作时非事务性操作,为了保证消息的一致性(可以搜索一下分布式一致性算法Paxos,Zk的Zab协议和它差不多),每个事务性操作都有一个唯一的事务操作ID,事务ID是长整形的自增变量,每处理一个操作请求【不分是否事务性操作】,该数值+1

  和大部分带有数据存储功能的系统一样,为了提升访问速度,目录树都是存储在内存中,但是内存掉电易失,很不可靠,所以都会涉及将数据持久化到外部磁盘存储,和HDFS的持久化机制类似,Zk也采用了快照+操作日志的方式,其内部会隔一段时间生成一个目录树的快照存储到磁盘,并将所有的事务性操作记录到log,在恢复时只需要将快照加载到内存,再回放增量的log即可,在持久化目录树和存储操作日志时,所生成的文件名都是:固定的文件名前缀+当前最大操作Id编号,所以一般情况下文件名越大代表文件越新,对于快照文件,每个文件还会生成一个CRC校验码,防止文件内容错误

  既然有数据的持久化,那就有数据的恢复,如果Zk中途宕机,再次恢复时就需要从持久化的文件中将目录树恢复,其恢复的主流程图如下:
在这里插入图片描述

3、网络连接管理

  服务端默认的网络管理类是NIOServerCnxnFactory,当然这个类是可配置的,也有基于Netty的实现,但是核心的业务逻辑其实是一样的,NIOServerCnxnFactory使用的就是原生的Java NIO,自己构建业务逻辑处理PipeLine,本文分析的就是NIOServerCnxnFactory,看了很多个开源框架,其网络处理的逻辑都差不多,都是有一个死循环线程,然后不断使用多路复用器Selector从内核获取IO时间集合,再遍历处理,NIOServerCnxnFactory不好的一点就是吧所有的连接都注册到一个selector上,会使得NIOServerCnxnFactory负载很高,Netty则是采用了Reactor的模型,具体等到分析Netty源码的时候才写吧

  上面说到,会有一个死循环线程,NIOServerCnxnFactory本身就实现了Runnable接口,就可以是一个线程,其主体流程如下:
在这里插入图片描述

  多余的废话就不多说了,看图吧

4、会话管理

  每个客户端连接在服务端都是一个会话【Session】,Zk中的临时节点都是和会话绑定,会话消失之后,其绑定的临时节点都要被删除,删除之后生成消息通知给其他的客户端,所以会话管理算是比较重要的一部分内容

  正式介绍会话的管理流程之前,先大致过一下会话管理的实现类,默认实现类是SessionTrackerImpl,其内部主要有以下几个成员变量

  • 1)、sessionsById,存储sessionId和实际Session的映射关系
  • 2)、sessionSets,存储该时刻过期的session集合
  • 3)、sessionsWithTimeout,存储sessionId和过期时间的映射关系
  • 4)、nextSessionId,唯一的会话Id,用于标识每个会话,自增的长整型变量
  • 5)、nextExpirationTime下一次过期时间
  • 6)、expirationInterval扫描间隔,默认是3s

  SessionTrackerImpl会负责会话的创建和销毁,同时其自身为一个线程,这个线程类似一个定时任务,用于清理过期的会话,扫描间隔就是expirationInterval

  灌水灌完了,开始正式剖析【BB】

4-1 会话创建流程

  前面介绍客户端源码的时候说过,当客户端与服务端创建连接之后,会马上给服务端发送一各连接请求包,所以服务端收到的该客户端的第一个请求数据包就是ConnectRequest请求,收到这个请求之后,就会进入会话创建流程,整体的会话创建流程如下:
在这里插入图片描述

4-2 Run方法解析

  在本小结最开始的时候就说了,这个会话管理的实现类本身就是一个线程,线程的任务就是定时清理过期的会话

  首先,在Zk中会话的清理和会话的失效失效时间有关系,会话的失效时间是客户端传过来的,例如某个会话的失效时间是10s,意味着如果服务端上一次接收到该会话的请求距离当前系统时间超过10s,则该会话需要被清理删除,这个定时线程采用的算法有点类似时间轮,使用一个Map存储有可能过期的会话,Map的Key是时间,Value是Session集合,如果线程被唤醒执行清理动作,会根据当时的时间直接从Map中取出该Session集合,如果存在数据,则执行清理操作,这个Map就是sessionSets

  举个小栗子,假设现在的系统时间是0,线程扫描间隔expirationInterval是3s,此时有3个会话,过期时间分别是是s1:2s,s2:10s,s3:12s,则sessionSets的数据如下:

{
  {3,{s1}},
  {12,{s2}},
  {15,{s3}}
}

  该集合表示,如果没有收到新的请求,s1将在3s后被线程回收,s2在12s后被线程回收,s3在15秒后被线程回收

  说了这么多,是时候上图了呀,Run方法的执行流程如下:
在这里插入图片描述

4-3 如何防止会话被清理

  既然有会话清理线程,那么如果客户端还在,就肯定会不断将会话过期时间迁移,保证不被线程扫描到,说到底就是更新sessionSets,还是用上面的例子,如果不做任何操作,随着时间轴的推移,这三个会话肯定会被清理,如果不想被清理,就需要将会话沿着时间轴移动,假设在系统时间为1s的时候收到了s1的请求,那么sessionSets的数据会被更新成如下格式:

{
  {6((1+2)/3+1)*3,{s1}},
  {12,{s2}},
  {15,{s3}}
}

  s1的超时时刻被移到6s了,那么当时间到达3s的时候s1就不会被清理,那么什么时候会触发这个动作?答案是服务端只要接到客户端的请求,无论是什么请求,都会更新对应会话的超时时间,心跳也算

  sessionSets更新流程如下:
在这里插入图片描述

5、请求数据处理

  Zk的服务端请求处理是一个PipeLine结构,其结构如下图所示:
在这里插入图片描述

  请求会沿着处理链路从前到后传播,部分Processor本身是一个独立线程,这些Processor之间通过消息队列衔接,也就是前面一个Peocessor将消息处理完后会丢给后一个Processor的队列,而不是直接调用后一个Processor的方法,这一点和Netty的Pipeline不太一样

  详细分析一下各个ProcessSor的处理逻辑

5-1 从请求的接收说起

  服务端接收到客户端发送的数据包后,会从数据包头部获取RequestHeader,得到请求的类型,不同的请求会有不同的处理方式,一般性的请求最终都会通过ZooKeeperServer.submitRequest()方法将请求交由逻辑处理链的第一个节点处理,请求的接收与提交流程图如下:
在这里插入图片描述

  从图中可以看出,请求已经被提交到preRequestProcessor的submittedRequests队列</font

5-2 preRequestProcessor的处理流程

  在最开始的时候就介绍过,每个Processor都是一个线程,且拥有一个消息队列,所以这里只要分析其run方法,run方法的主流程如下:
在这里插入图片描述

图中在处理时,会根据请求是否是事务请求走不同的处理逻辑,非事务请求基本上就直接发往下一个Processor了,如果是事务性请求,还需要对请求进行一步封装,这里以创建节点为例进行说明

所有的事务请求的封装类都是pRequest2Txn函数,Create请求的整体执行流程如下:
在这里插入图片描述

其余事务性操作的代码如有兴趣可以自行分析,无论是事务性操作还是非事务性操作,最终都会添加到下一个Processor【syncRequestProcessor】的queuedRequests队列中

5-3 syncRequestProcessor的处理流程

  首先这个Processor也是个线程,所以也只需要分析它的run方法,这个Processor的主要作用就是持久化数据,**回到目录树数据初始化中介绍的,为了提升性能,目录树都是在内存中,但是需要将其持久化到磁盘保证不丢,**这个Processor的作用就是持久化数据到磁盘,前面说了Zk采用的是快照+增量日志的方式持久化数据,其核心就在这个代码里,整体的程序流程图如下【图片编号:image-20211224170924581.png】:
在这里插入图片描述

总结一下这个Processor做的几件事情:

  • 将请求添加到增量事务日志,事务日志达到一定大小后会滚动生成新的,文件名生成规则前面有介绍;
  • 当日志记录数量达到一定的阈值后,启动新的线程将ZkDataBase持久化到磁盘,也就是存储了目录树的快照;
  • 将请求加到Toflush队列,在合适的实际通过flush方法传递给后一个Processor

通过上面的描述其实是有几个问题需要思考的:

  • 事务性操作是先持久化到日志之后,再加到Toflush队列,也就是说是如果持久化不成功,Zk的目录树就不会更新,而且由于即使持久化成功了,也不是马上将操作发给后一个Processor,但是数据已经持久化,此时即使宕机,数据也不会丢,虽然数据不丢,但是性能会比较低,也就是不支持高并发,这也是为什么Kafka0.8之后将消费者的消费偏移从存储在zk转移到kafka自身默认主题的一个原因
  • 还有一个问题需要结合目录树快照的存储和目录树数据初始化一起看,在生成目录树快照时,取的是最大操作ID,假设此时最大的操作Id是10,但是在生成快照的时候其实并没有停止接收数据【因为快照线程是新的】,是不是有可能会有操作是11的记录也会被写入快照,但是此时快照文件的后缀是10,在后面重新加载数据时从这个快照文件获得的最大操作Id是10,其余的要从增量事务日志中回放,也就是要从log文件中回放id大于10的记录,但是此时其实快照里面已经有操作ID是11的记录了,且已经在目录树中存在如果在从增量日志回放一次操作11,为什么重复操作不报错呢?会想一下,如果我们在命令行创建相同的节点肯定直接就报错了【顺序节点除外】,为了解决这个问题,专门调试了代码,发现其实主要是因为回放操作是直接调用了DataTree相关的操作,在这一层面如果创建相同的节点只是会创建失败,并不会向外抛出异常,而正常的请求需要通过逻辑处理链,在第一个节点PreRequestProcessor就会拦截这些操作,抛出异常

通过Toflush队列的缓冲,可以保证如果此时请求很多,会尽量保证数据先持久化,再更新目录树,Toflush的刷新是通过flush方法完成,其主要流程如下:
在这里插入图片描述

通过这个方法,消息会到finalRequestProcessor,finalRequestProcessor不是线程,只是一个普通类,所以不需要队列衔接,由其直接调用它的方法传入请求操作

5-4 finalRequestProcessor的处理流程

  前面也介绍了,finalRequestProcessor不是线程,只是普通的处理类,作用是更新目录树和返回响应,主方法是processRequest()方法,整体流程是:
在这里插入图片描述

  经过上面几步处理,数据已经正常更新到目录树,如有响应,也会通过网络发回给客户端

6、服务端通知机制的实现

  一直都说通知机制是Zk的生命力所在,所以需要详细剖析一下这部分的源码,和客户端类似,所有服务端的通知都是会被通知管理器Watcher管理

  服务端Watcher分为两个阶段,通知的注册和事件通知

6-1 通知注册(以监测节点是否存在为例)

  正常的客户端请求中,如果客户端需要事件通知,在请求体中会有watcher对象,在提交的请求中当然不是直接将watcher对象发送给服务端,毕竟服务端你要这东西没用,客户端的消息体中有个watch变量,当请求中有watcher对象时,会将watch变量设置为true,在处理请求时,如果检测到该变量为true,服务端就会注册Watcher,注册的是谁?客户端连接处理类继承了watcher接口,所以注册是的是连接对象,也就是谁需要就注册数据,整体流程图如下:
在这里插入图片描述

6-2 消息通知(以删除节点为例)

  程序最终执行入口是:dataTree.deleteNode(),执行流程图:
在这里插入图片描述

  通知触发后就会被移除,也就通知只是一次有效

7、图片地址

图片的gitee地址:https://gitee.com/source-code-note/graph/tree/master/zookeeper

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值