深入理解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。采用这种设计,有什么好处呢?这个问题比较复杂,后面会单独写文章分析。

相关推荐
灰色孤星A1 天前
Kafka学习笔记(三)Kafka分区和副本机制、自定义分区、消费者指定分区
zookeeper·kafka·kafka分区机制·kafka副本机制·kafka自定义分区
漫无目的行走的月亮3 天前
Ubuntu下安装Zookeeper集群
linux·ubuntu·zookeeper
程序那点事儿4 天前
zookeeper 服务搭建(集群)
linux·分布式·zookeeper
楼下创了电瓶车5 天前
ubuntu20.04系统安装zookeeper简单教程
分布式·zookeeper·云原生
AAA 建材批发王哥(天道酬勤)5 天前
ZooKeeper
java·zookeeper
lzhlizihang5 天前
Hadoop集群的高可用(HA):NameNode和resourcemanager高可用的搭建
hadoop·zookeeper·高可用·ha
懒鸟一枚6 天前
Zookeeper下载、安装配置
分布式·zookeeper
程序那点事儿7 天前
zookeeper 服务搭建(单机)
分布式·zookeeper·云原生·java-zookeeper
花千树-0108 天前
深度对比:etcd、Consul、Zookeeper 和 Nacos 作为注册中心和配置中心的优势与劣势
微服务·zookeeper·服务发现·etcd·consul
花千树-0108 天前
Dubbo 如何使用 Zookeeper 作为注册中心:原理、优势与实现详解
分布式·zookeeper·dubbo