首发CSDN:徐同学呀,原创不易,转载请注明源链接。我是徐同学,用心输出高质量文章,希望对你有所帮助。
一、心得分享
如何阅读ZooKeeper源码?从哪里开始阅读?最近把
ZooKeeper
源码看了个大概,有一些心得想和大家分享和探讨:
1、寻找迷宫入口
ZooKeeper源码的脉络就像一个迷宫,要想玩这个迷宫游戏,必须找到迷宫的入口。有两条入口可供选择:
- 从服务端的启动流程开始看起,可以了解配置文件
zoo.cfg
解析过程和配置项在源码中的应用,以及Leader选举流程等。服务端源码比较复杂,在了解服务端启动和Leader选举的过程中,又涉及很多其他知识点,包括内存数据库DataTree的原理,日志机制(事务日志和快照日志),数据恢复与同步等。最接近核心,也最难,容易劝退或者举步维艰。 - 从客户端向服务端建立连接开始看起,可以了解客户端是如何建立连接、发送请求和处理响应等,相对于服务端,客户端源码要简单很多。从客户端开始突破,要顺利些。
2、画流程图
看源码一定要画流程图。源码走向是错综复杂,每个流程、每个走向都画好流程图或者时序图,有助于原理理解。
客户端源码只有两个线程还好说,服务端源码有很多线程,直接绕晕。比如请求处理,就分为事务请求和非事务请求,事务请求又需要经过两阶段提交,不画流程图,根本梳理不清事务请求是如何在
Leader
和
Learner
之间流转的。
3、任务分解
任务拆分,化繁为简,化整为零,是大家都懂的道理,但是如何拆分并不是一件易事。
Zookeeper源码有很多大知识点,攻克大知识点很花时间,有时候会因为太难,而一拖再拖,举步维艰。将大知识点拆分为一个个小知识点,一步步攻克。拆分的过程不是一步到位,不要纠结于如何拆分,而是先拆起来,进行的过程中不断拆分,不知不觉一个大的,难的知识点就被攻克了。
这里推荐一个任务管理的工具TAPD,非常之好用:
4、思维导图
看完源码,总结是非常重要的。将一个知识点扩展成一个思维导图,每一个分支都是最精华的总结,这样会更加印象深刻。
二、源码基本结构
ZooKeeper
源码分为客户端源码和服务端源码。
1、客户端源码
客户端源码从一行初始化代码开始:
String connectString ="127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
ZooKeeper zooKeeper =newZooKeeper(connectString,20000, null);
初始化一个
ZooKeeper
实例,初始化过程会解析
connectString
,并随机挑选一个服务器地址建立长连接。
(1)ClientCnxn客户端连接抽象
ClientCnxn
是对客户端连接的抽象和封装,负责连接管理和
watcher
管理。有两个核心线程:
- 负责与服务端建立连接和通信的
SendThread
线程。 - 负责处理
watcher
远程回调和本地事件回调的EventThread
线程。
在客户端实例
ZooKeeper
初始化时,会初始化并启动
ClientCnxn
,启动
ClientCnxn
就是启动
SendThread
和
EventThread
两个线程。
(2)SendThread
SendThread
线程主要负责与服务端建立长链接,后续的
getData
、
setData
等操作都通过
SendThread
线程与服务端通信。
SendThread
的核心知识点有:
- 向服务端建立连接的过程
- 建立会话的过程
- 心跳机制保证长链接存活
- 读写IO处理
负责底层网络建立连接和I/O处理的是
ClientCnxnSocket
,实现类有
ClientCnxnSocketNIO
和
ClientCnxnSocketNetty
。
(3)EventThread
SendThread
接收到服务端的
watcher
通知后,会交由
EventThread
线程去触发回调。注册
watcher
的功能只有非事务请求(
getData
、
exists
、
getChildren
)才有,而事务请求,如
getData
可以注册本地事件,事务请求响应成功后会触发本地事件回调,这里的回调流程也是在
EventThread
线程中。
(4)getData非事务请求
非事务请求不仅仅有
getData
,但流程都差不多。
getData
可以注册
watcher
,但是如何注册,并且是如何远程向服务端注册?其实注册
watcher
只是向服务端发送一个是否注册
watcher
的布尔值,具体注册什么事件不会在注册时声明,而是在触发时判断。
getData
构建好请求体和响应体,并提交给
SendThread
线程进行底层网络的异步发送,此时
getData
主线程会阻塞等待响应。
(5)setData事务请求
事务请求也并非只有
setData
,还有
create
、
delete
。但是
setData
在客户端响应处理上稍有不同,
create
、
delete
和
getData
一样会阻塞,要等服务端的响应;而
setData
不需要阻塞,但是需要按顺序处理响应。
2、服务端源码
服务端源码较为复杂,突破口在启动流程上。在服务端启动的过程中,涉及到的知识点:
- 配置文件解析和配置项在源码中应用。
- 读取日志文件恢复内存数据库。
- 监听和接收客户端连接。
- Leader选举。
- Leader和Learner之间差异化数据同步。
- … …
(1)配置解析
将配置文件
zoo.cfg
加载为一个
java.util.Properties
对象,然后解析映射到
QuorumPeerConfig
对象中,再将
QuorumPeerConfig
的变量设置给
QuorumPeer
对象,
QuorumPeer
就是
ZAB
协议的具体实现类。
(2)恢复内存数据库
在服务端启动时,需要通过读取日志文件恢复内存数据库。首先读取快照日志文件反序列化出一棵
DataTree
,然后再读取事务日志文件修补增量数据。这只是初步恢复,等Leader选举完成以后,服务节点之间还需要进行差异化数据同步。
(3)监听客户端连接
在配置文件
zoo.cfg
中指定的
clientPort
就是用来监听客户端连接的。客户端连接监听是常规的
Reactor
响应式线程模型。一个
AcceptThread
线程监听连接事件,多个
SelectorThread
轮询封装注册连接,具体网络IO事件处理交给一个线程池。
AcceptThread
线程接收到来自客户端连接后,轮询选择一个
SelectorThread
来处理连接;每一个客户端连接在服务端都被抽象化成一个
ServerCnxn
对象,默认实现类为
NIOServerCnxn
,负责底层网络IO处理;具体的IO读写事件处理抽象成一个
IOWorkRequest
任务对象交给线程池
workerPool
异步处理。
无论是事务请求还是非事务请求从底层网络读取完数据并构建好请求体后,都会提交给一个节流阀线程
RequestThrottler
,
RequestThrottler
控制请求量,并将请求提交给一个包含多个处理器
RequestProcessor
的职责链处理。
(4)Leader选举
在配置文件中,有几行这样格式的配置:
server.A=B:C:D
- A是一个数字,表示每个zk实例的
myid
文件中的编号,即SID
。 - B是ip地址,每个zk实例所在机器ip。
- C是集群中
Leader
和Learner
通信的端口。 - D是集群中用于
Leader
选举同步票据的端口。
首先创建一个或者一组线程用于监听投票端口,然后创建一个快速选举
Leader
算法
FastLeaderElection
,并启动两个线程
WorkerSender
和
WorkerReceiver
分别用于选票发送和选票接收。
在交换选票前,服务节点间互相建立连接,为避免连接重复建立,只有
SID
较大的服务器才可以主动向其他服务器发起建立连接请求。建立连接后,会为每个连接创建两个线程
SendWorker
和
RecvWorker
分别用于网络底层的IO事件处理。
FastLeaderElection#lookForLeader
是
Leader
选举的核心实现,包括将选票广播给所有其他服务,处理其他服务同步过来的选票,选票PK,最终选出
Leader
,完成选票。
(5)数据差异化同步
数据差异化同步发生在
Leader
选举完成之后。
Learner
服务器(
Follower
和
Observer
)需要向
Leader
服务器发起建立连接请求,
Leader
启动
LearnerCnxAcceptor
线程监听
Learner
的连接请求,每一个建立的连接会被抽象成一个
LearnerHandler
对象。
Leader
检测到有过半数的
Follower
(
Observer
不参与过半数决策)建立连接后,就开始校对
Learner
的数据与自己的数据有哪些差异:
- 如果
Learner
少了数据,Leader
就会发送缺少的数据给Learner
; - 如果
Learner
多出数据,Leader
就会让Learner
回滚到指定位置; - 实在差异太大,就全量同步。
(6)事务日志和快照日志
在服务器正常运行的过程,查询数据都是直接从内存数据库中获取,所以响应速度很快,但是为了服务重启后数据还在,才有了将数据持久化到磁盘日志文件中。
每条事务请求都会先落地到事务日志文件,再提交到内存数据库中。经过一定事务请求次数,还会将整个内存数据库持久化成一个快照日志文件。一个快照日志文件和其后生成的事务日志文件共同组成全局数据。
FileTxnLog
是事务日志文件持久化实现类,主要封装对磁盘文件的追加、读取、截断、滚动等操作。
FileSnap
是快照日志文件持久化实现类,主要封装两个操作:将
DataTree
和会话列表序列化到磁盘文件和读取磁盘文件反序列化出
DataTree
和会话列表。
FileTxnSnapLog
是对
FileTxnLog
和
FileSnap
整合,方便调用。
(7)事务请求流程
事务请求和非事务请求都会经过一个职责链处理,不同的是,事务请求需要经过两阶段提交,而非事务请求不需要。
两阶段提交只能由
Leader
发起提案和进行提交操作,所以
Follower
和
Observer
接收到事务请求必须先转发给
Leader
,由
Leader
发起两阶段提交。
服务节点有三种类型
Leader
、
Follower
、
Observer
,所以有三条请求处理的职责链,其中个别处理器相同。
比如三条处理链最后都有一个
FinalRequestProcessor
来处理响应或者将请求应用到内存数据库;
Follower
和
Observer
首个处理器都是将事务请求转发给
Leader
;
Observer
没有投票权,不参与两阶段决策,所以没有响应
Leader
的
ACK
处理器。
(8)会话管理
客户端与服务端建立连接后,紧接着必须建立会话,之后所有通信都要在会话有效的基础上进行。会话建立也是事务请求,
sessionID
的创建和会话超时时间协商由当前服务实例完成,但是会话管理包括会话超时检查、清理、激活等都必须交由
Leader
负责。
客户端发向服务端的请求,无论是正常请求还是心跳都会重新激活会话,即重置会话超时时间。而
Learner
没有激活会话的权限,只有在
Leader
向
Learner
发送心跳,
Learner
响应心跳时,将需要激活的会话发给
Leader
,由
Leader
激活会话。
(9)watcher注册与触发
watcher
注册是非事务请求特有的。客户端并不会将
watcher
的详细信息发送给服务器,而是只发送一个是否注册
watcher
的布尔值。
服务器在处理请求时检测到请求体里的
watch=true
,就在内存数据库里注册一个
watcher
;数据发生变更,就取出该节点上注册的所有
watcher
,进行触发,触发的动作由服务端传递给客户端;客户端也保存了节点和
watcher
的关系,客户端从内存中取出该节点的所有
watcher
,一个个触发,触发的过程中判断是发生了什么事件,如节点创建、节点内容变更、节点删除等。
(10)DataTree内存数据库
DataTree
是内存数据库的具体实现。所谓树形结构其实就是哈希表
NodeHashMap
,key为节点路径,value为节点信息
DataNode
。
DataNode
中保存节点内容、节点持久化版本状态以及孩子节点相对路径(去掉父节点路径)列表。
NodeHashMap
具体实现类为
NodeHashMapImpl
,实则就是对
ConcurrentHashMap
的简单包装。
三、源码环境搭建
1、IDEA导入源码
从
github
下拉
ZooKeeper
源码最新稳定版https://github.com/apache/zookeeper,为了和当时看源码时的版本一致,这里选择
release-3.7.0
:
git clone -b release-3.7.0 [email protected]:apache/zookeeper.git
源码导入IDEA即可。
org.apache.zookeeper.proto
和
org.apache.zookeeper.data
等包下的类会出现异常:
这是因为这些包的源码不是现成的,需要通过编译
Jute
模块自动生成。生成的代码路径如下:
也可以一劳永逸,直接编译
root
项目,这样就会编译所有模块了。
2、本地运行
root
项目编译成功后,就可以像搭建伪集群一样本地运行源码了。
(1)伪集群搭建准备
如果不知道伪集群搭建需要准备哪些东西,请参考《分布式系统的基石之ZooKeeper——基本原理+场景应用+集群搭建(最强万字入门指南)》。
分别创建三个
Application
,
Program arguments
指定配置文件路径,
Main class
有两种,一种是单体模式
ZooKeeperServerMain
,一种是集群模式
QuorumPeerMain
,这里选择
QuorumPeerMain
。
(2)运行
分别启动zoo-1、zoo-2、zoo-3。可能会出现某些类找不到的情况:
这是因为
zookeeper-server
模块的
pom.xml
文件部分依赖的
scope
是
provided
的,只有编译和测试环境中依赖才起作用,想在运行时也起作用,可以将
provided
改为
compile
,或者去掉
scope
,因为默认
scope
是
compile
,编译,运行,测试环境依赖都起作用。
修改完
zookeeper-server
模块的
pom.xml
文件后重新编译就可以了。
如果运行的过程中控制台没有打印日志,首先查看
zookeeper-server/src/main/resources
路径下是否有
log4j.properties
文件,如果没有,就把
conf
目录下的
log4j.properties
复制过来。同时指定
zookeeper-server/src/main/resources
为
Resources
目录才会生效。
如此这般就可以运行了:
在本地运行源码的好处就是可以debug,debug对于阅读源码,理解一些流程非常有帮助。
ZooKeeper源码注释:https://github.com/stefanxfy/ZooKeeperLearning
如若文章有错误理解,欢迎批评指正,同时非常期待你的评论、点赞和收藏。
如果想了解更多优质文章,和我更密切的学习交流,请关注如下同名公众号【徐同学呀】,期待你的加入。
版权归原作者 徐同学呀 所有, 如有侵权,请联系我们删除。