0


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

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

一、心得分享

如何阅读ZooKeeper源码?从哪里开始阅读?最近把

ZooKeeper

源码看了个大概,有一些心得想和大家分享和探讨:

1、寻找迷宫入口

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

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

2、画流程图

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

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

Leader

Learner

之间流转的。

3、任务分解

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

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

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

4、思维导图

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

二、源码基本结构

ZooKeeper

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

1、客户端源码

ZooKeeper客户端源码

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

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、服务端源码

ZooKeeper服务端源码

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

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

的职责链处理。

请求处理流程骨架.drawio

(4)Leader选举

在配置文件中,有几行这样格式的配置:

server.A=B:C:D 
  • A是一个数字,表示每个zk实例的myid文件中的编号,即SID
  • B是ip地址,每个zk实例所在机器ip。
  • C是集群中 LeaderLearner通信的端口。
  • D是集群中用于Leader选举同步票据的端口。

首先创建一个或者一组线程用于监听投票端口,然后创建一个快速选举

Leader

算法

FastLeaderElection

,并启动两个线程

WorkerSender

WorkerReceiver

分别用于选票发送和选票接收。

在交换选票前,服务节点间互相建立连接,为避免连接重复建立,只有

SID

较大的服务器才可以主动向其他服务器发起建立连接请求。建立连接后,会为每个连接创建两个线程

SendWorker

RecvWorker

分别用于网络底层的IO事件处理。

FastLeaderElection#lookForLeader

Leader

选举的核心实现,包括将选票广播给所有其他服务,处理其他服务同步过来的选票,选票PK,最终选出

Leader

,完成选票。

Leader选举骨架.drawio

(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

处理器。

请求处理链.drawio

(8)会话管理

客户端与服务端建立连接后,紧接着必须建立会话,之后所有通信都要在会话有效的基础上进行。会话建立也是事务请求,

sessionID

的创建和会话超时时间协商由当前服务实例完成,但是会话管理包括会话超时检查、清理、激活等都必须交由

Leader

负责。

客户端发向服务端的请求,无论是正常请求还是心跳都会重新激活会话,即重置会话超时时间。而

Learner

没有激活会话的权限,只有在

Leader

Learner

发送心跳,

Learner

响应心跳时,将需要激活的会话发给

Leader

,由

Leader

激活会话。

会话激活

(9)watcher注册与触发

watcher

注册是非事务请求特有的。客户端并不会将

watcher

的详细信息发送给服务器,而是只发送一个是否注册

watcher

的布尔值。

服务器在处理请求时检测到请求体里的

watch=true

,就在内存数据库里注册一个

watcher

;数据发生变更,就取出该节点上注册的所有

watcher

,进行触发,触发的动作由服务端传递给客户端;客户端也保存了节点和

watcher

的关系,客户端从内存中取出该节点的所有

watcher

,一个个触发,触发的过程中判断是发生了什么事件,如节点创建、节点内容变更、节点删除等。

watcher注册和触发骨架.drawio

(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

zookeeper源码稳定版
源码导入IDEA即可。

org.apache.zookeeper.proto

org.apache.zookeeper.data

等包下的类会出现异常:

image-20220307003449704

这是因为这些包的源码不是现成的,需要通过编译

Jute

模块自动生成。生成的代码路径如下:

image-20220307003838873

也可以一劳永逸,直接编译

root

项目,这样就会编译所有模块了。

image-20220307011927954

image-20220307012029947

2、本地运行

root

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

(1)伪集群搭建准备

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

image-20220307012456309

分别创建三个

Application

Program arguments

指定配置文件路径,

Main class

有两种,一种是单体模式

ZooKeeperServerMain

,一种是集群模式

QuorumPeerMain

,这里选择

QuorumPeerMain

image-20220307012731053

(2)运行

分别启动zoo-1、zoo-2、zoo-3。可能会出现某些类找不到的情况:

image-20220307013536917

这是因为

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

目录才会生效。

image-20220307014433999

如此这般就可以运行了:

image-20220307015158541

image-20220307015221707

image-20220307015238572

在本地运行源码的好处就是可以debug,debug对于阅读源码,理解一些流程非常有帮助。

ZooKeeper源码注释:https://github.com/stefanxfy/ZooKeeperLearning

如若文章有错误理解,欢迎批评指正,同时非常期待你的评论、点赞和收藏。

如果想了解更多优质文章,和我更密切的学习交流,请关注如下同名公众号【徐同学呀】,期待你的加入。


本文转载自: https://blog.csdn.net/weixin_36586120/article/details/123321261
版权归原作者 徐同学呀 所有, 如有侵权,请联系我们删除。

“ZooKeeper源码阅读心得分享+源码基本结构+源码环境搭建”的评论:

还没有评论