深入理解zookeeper——客户端发送请求流程

在如今这样的分布式系统和云计算的时代,构建可靠、高性能的分布式服务是各个领域不可缺失的一部分。在这个领域里,zookeeper作为java中非常知名的中间件,一直以来都扮演着一个关键的角色。

接下来,我决定写一系列关于zookeeper源码分析的文章,对zookeeper的源码进行深度解读。

为什么要进行ZooKeeper源码分析?

1、深刻理解分布式系统基础理论:ZooKeeper是分布式系统领域的一个经典实现,通过深度源码解析,我们可以更好地理解分布式系统中的关键概念和原理,为设计和构建分布式系统打下坚实基础。

2、学习设计模式和优秀编程实践:ZooKeeper的源码体现了许多设计模式和良好的编程实践。通过学习这些,我们可以汲取宝贵的经验,提高我们自己的编程水平。

3、解密ZooKeeper的神秘面纱:ZooKeeper的内部实现一直以来都是分布式领域的热门话题。通过源码分析,我们将揭示它的神秘面纱,理解它是如何实现高可用性、一致性和可靠性的。

从客户端发送报文开始

java 复制代码
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
     //1. 创建一个Zookeeper客户端
     ZooKeeper zookeeper = new ZooKeeper("localhost:2181", 2000, null);
     //2. 创建一个持久节点
     zookeeper.create("/hello", "编程易行".getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
     Stat stat = new Stat();
     //3. 获取写入节点的数据
     byte[] data = zookeeper.getData("/hello", false, stat);
     System.out.println(new String(data, StandardCharsets.UTF_8));
}

上面是一个非常简单的hello world,就是创建一个Zookeeper客户端,调用create创建节点。然后,在调用getData打印我们之前写入的数据。

我们源码分析就从这里开始。(zookeeper的配置管理,也是个很有意思的话题,后面有机会可以分析下。)

1. 从Zookeeper.create 开始

一切的开始,都是Zookeeper.create

代码非常简洁,其实就是:

1、构造Request报文

2、调用ClientCnxn#submitRequest 发送报文,并等待结果

什么是submit一个报文?

如果是个java,我们想要往一个socket写数据,我们会怎么写?

我们会写 Socket.getOutputStream().write(byte数组),如果我们用的Netty网络库,我们可能会写Channel.writeAndFlush(对象)

这里zookeeper发送报文的方法,名字叫submit,本身其实也说明了它的原理 并不是简单的write 一下就完了。

我们接着往下看

1、先来看类名,叫做 ClientCnxn 这是zookeeper对于客户端连接的一个抽象。Zookeeper客户端与服务端的交互,都是通过ClientCnxn 这一入口。

2、还记得么,在create方法里,我们构造了一个CreateRequest 对象,并且把这个对象以参数形式,传入 submitRequest 方法中。在submitRequest里,我们又把CreateRequest 以参数形式传入queuePacket 方法里

发送报文 = 插入队列?

代码也非常简单

1、把我们的RequestResponse 包装成一个个的Packet

2、把Packet添加到一个队列里

非常简单,后面的一系列文章中,我们能看到zookeeper的源码设计里,大量的应用了队列。

写入队列中的报文是什么时候发出去的?

客户端调用create肯定不只是想写个队列就完了,终极目标肯定还是要发送数据到服务端。因此,一定会有另一个线程,去读这个队列的数据。这个线程,就是SendThread

1、ClientCnxnSocket的一些初始化

2、如果Tcp连接是断开的

2.1)找到要连接的zookeeper节点地址 我们使用zookeeper时,传入的connectString可能不止有一个节点的地址,比如 "10.0.0.1:2181,10.0.0.2:2181,10.0.0.3:2181" 这一步其实就是挑选我们要连接的zookeeper的地址。具体算法其实就是把地址打乱,不停地next。也就是说,如果某台zookeeper节点挂了,下一个循环,会尝试去连另一个zookeeper节点

2.2)发送Connect报文

3、判断是否超时,其实就是一段时间没收到zookeeper的报文就超时了,认为zookeeper已经挂了,这里会抛异常

4、调用网络库发送报文 这里就是调用网络库,获取outgoingQueue 队首的报文,通过socket发送出去

整个流程有点复杂,可以对照着流程图理解

小结create发送报文

所以,整个流程大概是这样。我们调用create时,会往outgoingQueue队列写入一个Packet。然后,会有一个SendThread线程,它会不停地取出队首报文,然后调用网络库(Netty或者NIO)往socket写入数据,实现往zookeeper服务端发送报文。

收到报文流程是怎么样?

从上面的分析中,我们知道。无论是写入队列,还是SendThread的死循环,和我们调用 create 的线程都不是同一个。换句话说,都是异步的。然而,实际使用时,我们调用create后,立刻去查zookeeper节点数据,一定都是能查到的。Zookeeper是如何把异步"变成"同步呢?

我们再回过头来看 submitRequest 方法,秘诀就在这个packet.wait 里。

也就是说,除了往队列里写入一个报文,zookeeper客户端还会调用这个Packet.wait() 方法,等待packet的通知(就是jdk的Object.wait())方法。

网络库写入socket时,会插入pendingQueue

之前分析SendThread流程时,我们忽略了网络库调用的细节。这里我们把这块补上

这里我们以Netty网络库为例(NIO的类似,但是NIO里有很多NIO编程的细节,相对来说比较复杂,感兴趣的读者可以自己分析下)

可以看到,ClientCnxnSocketNetty 除了调用channel.writeAndFlush() 把报文写入底层socket,还把Packet写入了一个pendingQueue

如图所示,在调用channel.writeAndFlush() 之前,会往pendingQueue 队尾插入一个Packet

网络库收到报文的处理

这是Netty收到报文后的处理。这里有非常经典的粘包拆包的处理,后面有机会分析下。此处我们关心的是收到完整的报文后,Netty会调用SendThread.readResponse()方法

1、首先,收到报文后,会调用PendingQueue.remove() 获取队首元素。这里有两个注意点:

1)队列的特性是先入先出,所以我们先通过Netty写出的报文,在收到报文时也会被最先弹出。

2)Server端返回报文的数据一定和它收到报文的数据一致。举个例子,客户端往服务端发送了 creategetChildrendelete 报文,服务端一定也会按照 creategetChildrendelete 返回,不会出现乱序。

接下来我们看看finishPacket的逻辑

也就是说,收到了服务端的回包后,会调用Packet的notifyAll方法,通知阻塞等待回包的线程。

zookeeper客户端发送报文完整流程

完整的流程如上所示

1、客户端调用 create 等方法时,会往一个 outgoingQueue 队列中,插入一个Packet。随后,通过 Object.wait() 方法,阻塞等待

2、Zookeeper客户端有一个Send线程,它会不停地取出 outgoingQueue 队首元素,然后调用网络库发送报文给zookeeper服务端

3、发送报文给zookeeper服务端前,Send线程会把Packet插入另一个pendingQueue里,这个队列中专门存放等待服务端响应的报文

4、客户端收到服务端的回包后,会取出 pendingQueue 队首袁术,调用packet.notifyAll() 通知阻塞等待的线程

总结

这篇博客分析了zookeeper 客户端发送报文的流程。从中,我们能有以下几点收获:

1、zookeeper发送报文,是单线程发送的。前一个报文发送完后,才会发送后一个报文。

2、zookeeper客户端发送报文的设计,采用了 "生产者消费者模式",一个线程往队列写,一个线程读取队列数据,写入socket。采用这种设计,有什么好处呢?这个问题比较复杂,后面会单独写文章分析。

相关推荐
寒士obj3 天前
分布式组件【ZooKeeper】
微服务·zookeeper
笨蛋少年派3 天前
zookeeper简介
分布式·zookeeper·云原生
007php0076 天前
百度面试题解析:Zookeeper、ArrayList、生产者消费者模型及多线程(二)
java·分布式·zookeeper·云原生·职场和发展·eureka·java-zookeeper
坐吃山猪8 天前
zk02-知识演进
运维·zookeeper·debian
yumgpkpm8 天前
华为鲲鹏 Aarch64 环境下多 Oracle 数据库汇聚操作指南 CMP(类 Cloudera CDP 7.3)
大数据·hive·hadoop·elasticsearch·zookeeper·big data·cloudera
小醉你真好9 天前
16、Docker Compose 安装Kafka(含Zookeeper)
docker·zookeeper·kafka
yumgpkpm10 天前
CMP (类ClouderaCDP7.3(404次编译) )华为鲲鹏Aarch64(ARM)信创环境多个mysql数据库汇聚的操作指南
大数据·hive·hadoop·zookeeper·big data·cloudera
yumgpkpm12 天前
大数据综合管理平台(CMP)(类Cloudera CDP7.3)有哪些核心功能?
hive·hadoop·elasticsearch·zookeeper·big data
回家路上绕了弯15 天前
深入 Zookeeper 数据模型:树形 ZNode 结构的设计与实践
后端·zookeeper
90919322116 天前
SQL关键词标签在数据分析中的应用与实践
zookeeper