ZooKeeper 的数据模型系统是一个类文件系统结构,每个节点称之为 ZNode,具体代码实现类是 DataNode。既不是文件夹,也不是文件,但是既具有文件夹的能力,也具有文件的能力。
3.4.x 及以下版本,ZNode 分为按照 临时/ 持久、 带序列编号/ 不带序列编号 分为 4 种:
zk.getData(znodePath, watcher); // 关注节点的数据变化
zk.exists(znodePath, watcher); // 关注节点的存在与否的状态变化
zk.getChildren(znodePath, watcher); // 关注节点的子节点个数变化
zk.setData(); // 更改节点数据,触发监听
zk.create(); // 创建节点
zk.delete(); // 删除节点
NodeCreated // 节点被创建
NodeDeleted // 节点被删除
NodeDataChanged // 节点数据发生改变
NodeChildrenChanged // 节点的子节点个数发生改变
Zookeeper 作为分布式协调服务,最常见应用场景是分布式锁和集群管理。例如 HDFS NameNode 高可用模式,YARN ResourceManager 高可用模式,HBase RegionServer 的管理与元数据存储,Flink 主节点组件 ResourceManager、JobManager、Dispatcher 的高可用模式等等。
序列化的 API 主要在 zookeeper-jute 子项目中。
class ZNode implements Record{
int id;
String name;
// 反序列化
void deserialize(InputArchive archive, String tag){
archive.readBytes();
archive.readInt();
}
// 序列化
void serialize(OutputArchive archive, String tag)
}
1、数据模型:DataTree + DataNode
2、持久化机制:FileTxnSnalLog = TxnLog + SnapLog
3、zk数据库:ZKDataBase = DataTree + FileTxnSnapLog
补充1:
1、每个节点上都保存了整个系统的所有数据 ( leader 存储了数据,所有的 follower 节点都是 leader 的副本节点)。
2、每个节点上的都把数据放在磁盘一份,放在内存一份。
补充2:
1、DataNode znode 系统中的一个节点的抽象。
2、DataTree znode 系统的完整抽象。
3、ZKDataBase 负责管理 DataTree,处理最基本的增删改查的动作,执行 DataTree 的相关快照和恢复的操作。
第一组:主要是用来操作日志的(如果客户端往 ZooKeeper 中写入一条数据,则记录一条日志)
TxnLog,接口,读取事务性日志的接口
FileTxnLog,实现 TxnLog 接口,添加了访问该事务性日志的 API
第二组:拍摄快照(当内存数据持久化到磁盘)
Snapshot,接口类型,持久层快照接口
FileSnap,实现 Snapshot 接口,负责存储、序列化、反序列化、访问快照
第三组;两个成员变量:TxnLog 和 SnapShot
FileTxnSnapLog,封装了 TxnLog 和 SnapShot
第四组:工具类
Util,工具类,提供持久化所需的API
class ZKDataBase{
protected DataTree dataTree;
protected FileTxnSnapLog snapLog;
}
class DataTree{
//根节点
private static final String rootZookeeper = "/";
// 所有节点的 路径 和 节点抽象的 映射
private final NodeHashMap nodes = new NodeHashMapImpl(digestCalculator){
private final ConcurrentHashMap<String, DataNode> nodes;
}
}
public class DataNode implements Record {
byte[] data;
private Set<String> children = null;
}
class FileTxnSnapLog{
TxnLog txnLog;
SnapShot snapLog;
}
// 实现类:
interface TxnLog{
void rollLog() throws IOException;
boolean append(TxnHeader hdr, Record r) throws IOException;
boolean truncate(long zxid) throws IOException;
void commit() throws IOException;
}
// 实现类:
interface SnapShot{
long deserialize(DataTree dt, Map<Long, Integer> sessions) throws IOException;
void serialize(DataTree dt, Map<Long, Integer> sessions, File name, boolean fsync) throws IOException;
}
建立连接时,只允许服务器ID较大者去连服务器ID较小者,小ID服务器去连大ID服务器会被拒绝。
1、集群启动脚本:zkServer.sh start
2、集群启动的启动类的代码执行:QuorumPeerMain.main()
3、冷启动数据恢复,从磁盘恢复数据到内存:zkDatabase.loadDatabase()
4、选举:startLeaderElection() + QuorumPeer.lookForLeader()
5、同步:follower.followLeader() + observer.observerLeader()
入口:QuorumPeerMain 的 main 方法
// 入口方法
QuorumPeer.loadDataBase(){
zkDb.loadDataBase(){
// 冷启动的时候,从磁盘恢复数据到内存
snapLog.restore(dataTree,...){
// 第一件事:从快照恢复
snapLog.deserialize(dt, sessions){
deserialize(dt, sessions, ia){
SerializeUtils.deserializeSnapshot(dt, ia, sessions){
dt.deserialize(ia, "tree");
}
}
}
// 第二件事:从操作日志恢复
fastForwardFromEdits(dt, sessions, listener){
while(true) {
// 恢复执行一条事务
processTransaction(hdr, dt, sessions, itr.getTxn()){
// 恢复执行一条事务
dt.processTxn(hdr, txn){
// 创建一个znode
createNode 或者 deleteNode
}
}
}
}
}
}
}
(1)所有的节点(有选举权和被选举权),一上线,就是 LOOKING 状态,当选举结束了之后,有选举权中的角色会变量另外两种:Leader, Follower,相应的状态变为 LEADING、FOLLOWING。
(2)需要发送选票,选票是 Vote 对象,广播到所有节点。事实上,关于选票和投票的类型有四种:
Vote 选票
Notificatioin 接收到的投票
Message 放入投票队列的选票
ToSend 待发送的选票
还有一个特殊的中间对象:ByteBuffer —— NIO 的一个 API
(3)当每个 zookeeper 服务器启动好了之后,第一件事就是发起投票,如果是第一次发起投票都是去找 leader,如果发现有其他 zookeeper 返回给我 leader 的信息,那么选举结束。
(4)在进行选票发送的时候,每个 zookeeper 都会为其他的 zookeeper 服务节点生成一个对应的 SendWorker 和一个 ArrayBlockingQueue,ArrayBlockingQueue 存放待发送的选票,SendWorker 从队列中,获取选票执行发送。还有一个线程叫做: ReceiveWorker 真正完整从其他节点接收发送过来的投票。
整个图总结一下,分成两个部分:
创建 QuorumCnxManager
创建了 recvQueue 队列
创建了 queueSendMap
创建了 senderWorkerMap
创建了 Listener 线程
创建 FastLeaderElection
创建了 sendqueue 队列
创建了 recvqueue 队列
创建了 WorkerSender 线程
创建了 WorkerReceiver 线程
终于启动了选举:入口方法 FastLeaderElection.lookForLeader();
选举客户端入口方法:FastLeaderElection.lookForLeader() 中会调用 QuorumCnxManager 的 connectAll 方法,进而对每个节点调用 connectOne 方法。
选举服务端入口方法:QuorumCnxManager 内部类 ListenerHandler 的 ServerSocket.accept() 方法处理连接请求,校验 sid 是否满足大于本节点 sid,满足则创建对应的 SendWorker、RecvWorker、CircularBlockingQueue 用于发送和接受选票。
QuorumPeer.run(){
while(true){
switch(getPeerState()) {
case LOOKING:
// 选举入口 makeLEStrategy() = FastLeaderElection
// 默认每间隔 2 秒 (tickTime) 执行一轮投票
setCurrentVote(makeLEStrategy().lookForLeader());
break;
case OBSERVING:
// 选举结束,observer 跟 leader 进行状态同步
setObserver(makeObserver(logFactory));
observer.observeLeader();
break;
case FOLLOWING:
// 选举结束,没有成为 leader 的服务器成为 follower,保持和 leader 的同步
setFollower(makeFollower(logFactory));
follower.followLeader();
break;
case LEADING:
// 选举结束,其中一个 participant 成为 leader,进入领导状态
setLeader(makeLeader(logFactory));
leader.lead();
break;
}
}
}
ELECTION:开始选举,当前 Server 进入找 Leader 状态:Vote currentVote = lookForLeader()
DISCOVERY:当选举得出了结果,开始进入发现认同阶段,当超过半数 Follower 认同该 Leader,意味着选举真正结束
SYNCHRONIZATION:经过确认,有超过半数节点都追随了刚推举出来的 Leader 节点
BROADCAST:当有超过半数的 Follower 完成了和 Leader 的状态同步之后进入消息广播状态,正常对外提供服务
右图详细架构如下:
(1)快照同步:Leader 直接把最新的快照文件(这个快照文件必然包含了 Leader 的所有数据)直接通过网络发送给 Follower。
(2)差异化同步:在确认同步方式的时候,如果得到的结果不是快照同步,则同时把要同步的数据,变成(Proposal + Commit 消息放到 队列中),队列中的数据的形态:
DIFF + Proposal + Commit + Proposal + Commit + Proposal + Commit + Proposal + Commit + NEWLEADER
当 Follower 接收到 NEWLEADER 消息的时候,意味着 Follower 已经接收到了需要同步的所有数据。
(1)Follower 发送 FOLLOWERINFO 消息给 Leader,信息中包含 Follower 的 AcceptedEpoch。
(2)Leader 在接收到 Follower 的 FOLLOWERINFO 消息的时候,返回一个 LEADERINFO 消息给 Follower,信息中包含 Leader 的 AcceptedEpoch。
(3)Follower 给 Leader 返回一个 ACKEPOCH 消息,表示已经接收到 Leader 的 AcceptedEpoch 了。Leader 需要等待有超过半数的 Follower 发送回 ACKEPOCH 消息;有超过半数节点在追随相同的 Leader 节点,则选举结束,开始进入同步阶段。
(4)Leader 根据 Follower 发送过来的 epoch 信息给 Follower 计算同步方式,同步方式有可能是 DIFF, SNAP, TRUNC 的其中之一。计算得到的同步方式消息放入到 LearnerHandler 的 queuedPackets 队列中,跟后面计算出来的待同步的分布式事务日志一起执行发送。同步方式的计算逻辑位于 LearnerHandler 的 syncFollower 方法中。
(5)如果同步方式是 DIFF,则获取到需要同步的分布式事务的 PROPOSAL 和 COMMIT 日志,也放入 LearnerHandler 的 queuedPackets 队列中,如果
同步方式是 SNAP,则先写入一个 SNAP 消息给 Follower,然后把快照文件发送过 Follower 进行快照同步
(6)通过 startSendingPackets() 方法启动一个匿名线程执行 LearnerHandler 的 queuedPackets 队列中的数据包发送。
(7)当该同步的数据(queuedPackets 队列中的 PROPOSAL 和 COMMIT 日志,或者 SNAP 方式的快照文件)都发送完毕之后,Leader 给 Follower 发送一个 NEWLEADER 消息表示所有待同步数据已经发送完毕。
(8)Follower 在接收到 Leader 发送过来的 NEWLEADER 消息,就必然得知,要和 Leader 进行同步的日志数据,都已经发送过来了,自己也都执行成功了,则可以给 Leader 发送过一个 ACK 反馈
(9)Leader 需要等待集群中,有超过半数的节点发送 ACK 反馈过来,如此,集群启动成功,Leader 给发送过了 ACK 的 Follower Server 发送一个 UPTODATE 的消息表示集群已经启动成功。Leader 和 Follower 都启动各自的一些必备基础服务可以开始对外提供服务了。
(10)Follower 接收到 Leader 的 UPTODATE 消息,即表示集群启动正常,Follower 可以正常对外提供服务,Follower 再给 Leader 返回一个 ACK。
(1)Leader 和 Follower 之间是有心跳的。如果维持心跳的节点数不超过集群半数节点了,则集群不能正常对外提供服务了。全部进入 LOOKING 状态。
(2)Leader 通过 syncFollower() 方法来计算和 Follower 的同步方式。关于什么情况分析得到什么同步方式的细节需要了解清楚。
(3)Leader 和 Follower 在集群正常启动成功之后,需要启动一些基础服务,比如 SessionTracker 和 RequestProcessor 等。
大体分为三个大部分:
基本上分为四个大步骤:
在 Leader 的 lead() 方法的最后,也就是 Leader 完成了和集群过半 Follower 的同步之后,就会调用 startZkServer() 来启动必要的服务,主要包括:
Leader.lead(){
startZkServer(){
zk.startup(){
// ZooKeeperServer 启动
super.startup(){
startupWithServerState(State.RUNNING);
}
// ZK Container ZNode 定时清除任务
if(containerManager != null) {
containerManager.start();
}
}
}
}
注:Leader 中聚合了 ZooKeeperServer 的子类 LeaderZooKeeperServer。ZooKeeperServer 内部枚举类 State 共有 4 个实例:INITIAL, RUNNING, SHUTDOWN, ERROR。
Follower 也是一样的。在完成了和 Leader 的状态同步之后,也就是接收到 Leader 发送过来的 NEWLEADER 消息的时候,就会首先拍摄快照,然后调用 zk.startupWithoutServing() 来启动 Follower 必要的一些基础服务,包括:
Learner.syncWithLeader(long newLeaderZxid){
// 创建 SessionTracker
zk.createSessionTracker();
// 启动一些各种服务
zk.startupWithoutServing(){
startupWithServerState(State.INITIAL);
}
}
ZooKeeperServer.startupWithServerState(State state){
// 创建和启动 SessionTracker
if(sessionTracker == null) {
createSessionTracker();
}
startSessionTracker();
// 初始化 RequestProcessor
setupRequestProcessors();
// 其他各项基础服务
startRequestThrottler();
registerJMX();
startJvmPauseMonitor();
registerMetrics();
// TODO_MA 注释: 更新状态为 RUNNING
setState(state);
// 解除其他线程的阻塞
notifyAll();
}
其中,最为重要的两件事有两件:
在 Leader 启动的时候,Leader 会创建 LeaderSessionTracker,在 Follower 启动的时候,内部会创建一个 LearnerSessionTracker。SessionTracker 的内部都有 globalSessionTracker 和 localSessionTracker 之分,但是无论如何,都是通过 SessionTrackerImpl 和 ExpiryQueue 来完成 Session 管理的。
Leader 的 setupRequestProcessors() 方法的核心逻辑:
Leader RequestProcessor 详解:
不管是 follower 还是 leader, 不管是读请求,还是写请求, RP 处理链的入口,都是 firstProcessor。
Follower 的 setupRequestProcessors() 方法的核心逻辑:
Follower RequestProcessor 详解:
public enum States {
CONNECTING,
ASSOCIATING,
CONNECTED,
CONNECTEDREADONLY,
CLOSED,
AUTH_FAILED,
NOT_CONNECTED;
}
NIOServerCnxnFacotry 内部首先启动(一个 AcceptorThread, 多个 SelectorThread,一个线程池(WorkerThread))。每次接收到一个客户端链接请求,在服务端会生成一个 ServerCnxn 的组件,这个对象的内部就是封装了一个 SocketChannel,专门对某个 client 执行服务。
在 ZooKeeper 的客户端 ClientCnxn 初始化的时候,是由内部的 SendThread 发起连接请求给服务端建立连接,然后服务端的会给当前的客户端生成一个 ServerCnxn,一个客户端就会有一个对应的 ServerCnxn,相当于 ServerCnxn 和 ClientCnxn 是一一对应的关系。而且当服务端 生成好了 ServerCnxn 之后,还会给当前这个连接创建一个 Session,通过 Session 来管理这个链接。
默认 selector 线程个数:处理器个数/ 2 开根号 跟1 求最大值
默认 worker 线程个数:处理器个数 * 2
默认 worker 线程超时时间:5s
启动过程:
关于 ZooKeeper 的会话创建流程:
watcher => 模板方法模式
requestProcessors =>职责链模式(netty 的 pipeline 也用到了类似的思想)
zookeeper => ZKDataBase 用到了 外观模式(能力增强)、组合模式(子节点管理)