Zookeeper单机模式启动与接收数据模型源码解析
DataTree : 节点的存储形式
DataNode
服务端架构
-
当服务端接收到命令做的事:
- 解析配置文件:先持久化命令,即事务日志、快照
- 再更新到DataTree
-
那我们启动服务端的时候,就会调用启动类:org.apache.zookeeper.server.quorum.QuorumPeerMain
会执行initializeAndRun()方法:
- 解析配置
将配置文件的内容初始化QuorumPeerConfig类 - 开启一个定时器,根据配置清空多余的日志和快照文件
通过new 一个Timer定时任务,每个一段时间就去删快照,只保留指定的个数(默认是3) - 调用ZooKeeperServerMain.main(args)开启真正的启动流程
ZooKeeperServerMain.main(args)
- initializeAndRun
- 解析配置文件初始化到ServerConfig
- runFromConfig:单机环境下启动Zookeeper
runFromConfig 单机环境下启动Zookeeper
- 初始化FileTxnSnapLog对象:快照和日志操作工具类
- 初始化JvmPauseMonitor
- 初始化ZooKeeperServer
- 启动AdminServer
- 绑定端口,并做准备工作
- 创建NIOServerCnxnFactory对象
- 开启ServerSocketChannel,并绑定客户端端口
- startup() 启动
- 初始化WorkerService 线程池
- 启动多个SelectorThread 负责接收读写就绪事件
- 启动AcceptThread 负责接收连接事件
- 根据快照和日志初始化ZKDatabase
- 启动Session跟踪器SessionTracker
- 初始化RequestProcessor Chain
- 启动请求限流器RequestThrottler
- 启动容器节点定时器
- shutdownLatch.await()
主线程执行完上面的逻辑就会阻塞在这里,等到zkServer被shutdown时,才会继续向下执行
绑定端口
runFromConfig 中的以下部分就是关于绑定端口的:
-
创建NIOServerCnxnFactory对象
服务端每接收到客户端的一个连接就会生成一个ServerCnxn对象 ,Zookeeper用nio或netty进行客户端与服务端的绑定。
选择哪个方式就会拿哪个方式的工厂,默认是用nio,所以就默认创建一个NIOServerCnxnFactory。 -
开启ServerSocketChannel,并绑定客户端端口,设置最大客户端连接限制数
-
startup()
这里就是创建了什么工厂,就调用哪个工厂的startup方法,我们默认用的是NIOServerCnxnFactory
- start() 线程池
- 初始化WorkerService 线程池
- 启动SelectorThread 负责接收读写就绪事件
- 启动AcceptThread 负责接收连接事件
- zks.startdata() 初始化数据
- 初始化ZKDatabase,加载数据
- 还会把session加载出来,放到sessionWithTimeouts中
- zks.startup()
- 创建sessionTracker
- 初始化RequestProcessor Chain
- 创建requestThrottler
- 注册jmx
- 修改为RUNNING状态
- notifyAll()
- start() 线程池
接收数据模型:SelectorThread和AcceptThread
- 首先创建一个线程池
- 启动多个SelectorThread
有多个SelectorThread,并且一个SelectorThread会负责多个SocketChannel - 启动一个AcceptThread,并将SocketChannel分配给SelectorThread
AcceptThread的工作流程
AcceptThread是负责接收Socket连接的
- AcceptThread 每接收到一个socket连接就会得到一个SocketChannel对象
- 然后通过轮询的方式把SocketChannel分配给某个SelectorThread,分配方式是把SocketChannel对象放到SelectorThread的队列acceptQueue中
通过offer() 添加到阻塞队列acceptQueue中 - 唤醒Selector,让SelectorThread线程负责处理SocketChannel里的数据
SelectorThread的工作流程
SelectorThread就是把SocketChannel对象包装一下,给线程池处理
服务端有多个SelectorThread线程,每个线程负责处理多个SocketChannel的读写就绪事件,SelectorThread线程会不断的从acceptedQueue队列中获取SocketChannel对象并注册读事件到SelectorThread线程中的Selector对象上,同时,SelectorThread线程也会不断的从Selector对象上获取就绪事件。
- 循环进行如下操作
- 遍历就绪事件,得到读就绪事件或写就绪事件就进行处理,即这里就在处理客户端的命令
具体处理流程:把key包装成IOWorkRequest,再封装成scheduledWorkRequest,扔到之前最开始创建的线程池中去execute。程池中的线程执行时,调用的就是IOWorkRequest的doWork方法,而doWork方法中会调用到NIOServerCnxn的doIO方法。这里就是真正的处理读写事件 - 从acceptQueue(是一个BlockingQueue)中拿SocketChannel对象
- 注册读事件
- 并创建NIOServerCnxn对象(Socket连接的上下文)
- 遍历就绪事件,得到读就绪事件或写就绪事件就进行处理,即这里就在处理客户端的命令
RequestProcessor
链式的后置处理器一共有三个:
- PrepRequestProcessor
- SyncRequestProcessor
- FinalRequestProcessor
NIOServerCnxn doIO 处理数据的逻辑
NIOServerCnxn对象才是真正执行就绪事件的逻辑实现。整个模型相当于,每接收到一个就绪事件,NIOServerCnxn对象就会去处理该事件。
NIOServerCnxn做的工作就是把客户端发来的数据放到incomingBuffer中。
客户端发来的命令会被打包成Packet,其中前4个字节为这个Packet的长度
- 先读数据包的前四个字节得到数据包的长度,存在incomingBuffer中
首先incomingBuffer是4个字节,这样就将Packet的前四个字节读到了incomingBuffer,也就是这时incomingBuffer存的是 数据包的长度 - readLength() 会将数据包的长度len读出来,给incomingBuffer分配len字节的空间
- readPayload() 用incomingBuffer读取len个字节的数据放到incomingBuffer中
这里是真正读数据的过程- 如果没有初始化好,readConnectRequest() 连接初始化,读ConnectRequest对象
客户端发送连接请求后还会给服务端发送ConnectRequest,其中包括了SessionTimeOut。服务端需要读一下ConnectRequest内容,才算初始化完成。 其中会判断服务端的SessionTimeOut和传来的SessionTimeOut哪个小就用哪个。
并构造connectResponse对象,其中会传SessionTimeOut,返回给客户端。 - 如果是初始化好的,就readRequest() 处理命令请求,进行增删改查
调用zkServer. 进行增删改查
- 如果没有初始化好,readConnectRequest() 连接初始化,读ConnectRequest对象