ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建

Posted 徐同学呀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建相关的知识,希望对你有一定的参考价值。



首发CSDN:徐同学呀,原创不易,转载请注明源链接。我是徐同学,用心输出高质量文章,希望对你有所帮助。


ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_源码心得

一、心得分享

如何阅读ZooKeeper源码?从哪里开始阅读?最近把​​ZooKeeper​​源码看了个大概,有一些心得想和大家分享和探讨:

1、寻找迷宫入口

ZooKeeper源码的脉络就像一个迷宫,要想玩这个迷宫游戏,必须找到迷宫的入口。有两条入口可供选择:

  • 从服务端的启动流程开始看起,可以了解配置文件​​zoo.cfg​​解析过程和配置项在源码中的应用,以及Leader选举流程等。服务端源码比较复杂,在了解服务端启动和Leader选举的过程中,又涉及很多其他知识点,包括内存数据库DataTree的原理,日志机制(事务日志和快照日志),数据恢复与同步等。最接近核心,也最难,容易劝退或者举步维艰。
  • 从客户端向服务端建立连接开始看起,可以了解客户端是如何建立连接、发送请求和处理响应等,相对于服务端,客户端源码要简单很多。从客户端开始突破,要顺利些。

2、画流程图

看源码一定要画流程图。源码走向是错综复杂,每个流程、每个走向都画好流程图或者时序图,有助于原理理解。

客户端源码只有两个线程还好说,服务端源码有很多线程,直接绕晕。比如请求处理,就分为事务请求和非事务请求,事务请求又需要经过两阶段提交,不画流程图,根本梳理不清事务请求是如何在​​Leader​​​和​​Learner​​之间流转的。

3、任务分解

任务拆分,化繁为简,化整为零,是大家都懂的道理,但是如何拆分并不是一件易事。

Zookeeper源码有很多大知识点,攻克大知识点很花时间,有时候会因为太难,而一拖再拖,举步维艰。将大知识点拆分为一个个小知识点,一步步攻克。拆分的过程不是一步到位,不要纠结于如何拆分,而是先拆起来,进行的过程中不断拆分,不知不觉一个大的,难的知识点就被攻克了。

这里推荐一个任务管理的工具TAPD,非常之好用:

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_zookeeper服务端源码_02

4、思维导图

看完源码,总结是非常重要的。将一个知识点扩展成一个思维导图,每一个分支都是最精华的总结,这样会更加印象深刻。

二、源码基本结构

​ZooKeeper​​源码分为客户端源码和服务端源码。

1、客户端源码

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_zookeeper客户端源码_03

客户端源码从一行初始化代码开始:

String connectString = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
ZooKeeper zooKeeper = new ZooKeeper(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、服务端源码

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_zookeeper客户端源码_04

服务端源码较为复杂,突破口在启动流程上。在服务端启动的过程中,涉及到的知识点:

  • 配置文件解析和配置项在源码中应用。
  • 读取日志文件恢复内存数据库。
  • 监听和接收客户端连接。
  • 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​​的职责链处理。

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_源码心得_05

(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​​,完成选票。

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_zookeeper源码运行_06

(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​​处理器。

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_zookeeper客户端源码_07

(8)会话管理

客户端与服务端建立连接后,紧接着必须建立会话,之后所有通信都要在会话有效的基础上进行。会话建立也是事务请求,​​sessionID​​​的创建和会话超时时间协商由当前服务实例完成,但是会话管理包括会话超时检查、清理、激活等都必须交由​​Leader​​负责。

客户端发向服务端的请求,无论是正常请求还是心跳都会重新激活会话,即重置会话超时时间。而​​Learner​​​没有激活会话的权限,只有在​​Leader​​​向​​Learner​​​发送心跳,​​Learner​​​响应心跳时,将需要激活的会话发给​​Leader​​​,由​​Leader​​激活会话。

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_zookeeper源码运行_08

(9)watcher注册与触发

​watcher​​​ 注册是非事务请求特有的。客户端并不会将 ​​watcher​​​ 的详细信息发送给服务器,而是只发送一个是否注册​​watcher​​ 的布尔值。

服务器在处理请求时检测到请求体里的​​watch=true​​​,就在内存数据库里注册一个​​watcher​​​;数据发生变更,就取出该节点上注册的所有​​watcher​​​,进行触发,触发的动作由服务端传递给客户端;客户端也保存了节点和​​watcher​​​的关系,客户端从内存中取出该节点的所有​​watcher​​,一个个触发,触发的过程中判断是发生了什么事件,如节点创建、节点内容变更、节点删除等。

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_zookeeper源码运行_09

(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 git@github.com:apache/zookeeper.git

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_源码心得_10

源码导入IDEA即可。​​org.apache.zookeeper.proto​​和​​org.apache.zookeeper.data​​等包下的类会出现异常:

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_客户端_11

这是因为这些包的源码不是现成的,需要通过编译​​Jute​​模块自动生成。生成的代码路径如下:

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_zookeeper客户端源码_12

也可以一劳永逸,直接编译​​root​​项目,这样就会编译所有模块了。

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_zookeeper客户端源码_13

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_zookeeper源码运行_14

2、本地运行

​root​​项目编译成功后,就可以像搭建伪集群一样本地运行源码了。

(1)伪集群搭建准备

如果不知道伪集群搭建需要准备哪些东西,请参考​​《分布式系统的基石之ZooKeeper——基本原理+场景应用+集群搭建(最强万字入门指南)》​​。

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建_zookeeper服务端源码_15

分别创建三个​​Application​​​,​​Program arguments​​​ 指定配置文件路径,​​Main class​​​有两种,一种是单体模式​​ZooKeeperServerMain​​​,一种是集群模式​​QuorumPeerMain​​​,这里选择​​QuorumPeerMain​​。

ZooKe<p以上是关于ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建的主要内容,如果未能解决你的问题,请参考以下文章

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建

ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建

Zookeeper源码阅读(十八) 选举之快速选举算法FastLeaderElection

Zookeeper源码阅读 ACL基础

Zookeeper源码阅读(十四) 单机Server