Zookeeper源码分析笔记[1]-客户端源码分析

  ZooKeeper是一个分布式应用程序协调服务,提供的功能包括:配置维护、域名服务、分布式同步、组服务等,举个例子,在分布式应用中,为了高可用,经常会使用主备架构,当主机挂了之后,自动切换到备机,这就需要有一个服务能感知主机下线【Zookeeper中的临时节点】,并通知备机将自己升级为主机【Zookeeper的通知机制】,在主机和备机之间进行协调

  Zk自身也是一个分布式服务,其也有高可用问题,不同与Hdfs、Yarn等分布式框架,Zk的主节点是自己选出来的,也就是对应其快速选举算法,快速选举能保证集群内当主节点宕机后,能从剩余Follower节点【Observer节点没有投票和参与选主的权利】选出一个新主接管集群,为了用好这个技术,闲暇时间专门阅读了Zk的源Zookeeper源码分析笔记系列文章主要对Zk运行中的重要流程进行介绍,本文使用的Zk版本是3.4.5,部分图有点大,如果看不清可以,可到gitee下载一下,地址在文章末尾😂

1、初始化流程

  客户端初始化一般都是用如下的语法:

new ZooKeeper(zkAddr,100000,watch)

以上代码中

  • zkAddr是zk的服务器地址和默认目录节点【如有】,例如A:2181,B:2181/lock
  • 100000是连接的超时时间
  • watch是通知类,此时是默认的通知类,在连接成功后会触发内部方法

初始化流程图
在这里插入图片描述

为了保证下面文章的顺滑,需要先说一下连接管理器类内部的两个消息队列:outgoingQueue和pendingQueue,虽然叫队列,其实就是个链表实现,客户端所有的请求【事务请求和非事务请求,事务请求包括增删改,非事务请求主要就是查】封装之后都会先进入outgoingQueue,每个请求都有自己的唯一自增序号,请求发送出去后,该请求会被存储到连接管理器的pendingQueue,服务端在接收到客户端的请求后,会将该序号添加到响应体返回给客户端,客户端接收到服务端的响应后,会依次从pendingQueue取出数据比对,如果出现乱序直接抛出异常,例如发送的顺序是1,2,3,则接收的顺序也必须是1,2,3

2、请求发送流程

  在发送请求时可以选择同步和异步两种方式,同步的方式必须等到服务端返回响应后函数才会返回,异步的方式是调用创建方法时需要注册一个回调函数,程序不会阻塞等待而是立即返回,待收到服务端节点创建成功的响应后会回调之前注册的回调函数,无论是哪种方式,调用方法后都是将请求写到OutgoingQueue,队列中的数据会在合适的时机由SendThread线程发送到服务端

2-1 同步方式请求发送流程图

在这里插入图片描述

2-2 异步方式请求发送流程图

在这里插入图片描述

3、数据发送线程SendThread流程解析

  SendThread的主要作用就是和服务端建立连接,并负责将客户端的请求发送到服务端,网络主要使用的是Java的NIO,无NIO基础的童鞋自己网上找点帖子补一补吧,核心组件就是多路复用器Selector,Linux上主要调用的底层函数就是epoll,相比于C和C++,Java已经封装的有点好了

3-1 线程run方法整体流程

  只要是个线程,其启动之后最终调用的就输run方法,SendThread的run方法的整体流程如下,图中红色部分是重要的主流程,会在后续内容详细分析,这个图稍微有那么一点点大【图片编号:image-20211224105628279.png】
在这里插入图片描述

3-2 连接服务器

  客户端启动之后肯定没和服务端建立联系,所以要想向服务端发送请求,第一步就是建立网络连接,建立网络连接的流程如下:在这里插入图片描述

3-3 网络IO处理的主函数doTransport主流程

  客户端连接建立好之后的下一步就是将请求发送给服务端,无论是发送请求还是接收响应,最终都是由SendThread的doTransport负责,其主要流程如下【图片编号:image-20211224110657675.png】:
在这里插入图片描述

  对Java NIO熟悉的童鞋应该对这个代码很熟的吧,主要就是通过Selector的select方法返回有IO事件的SelectionKey,然后遍历处理,我们这主要关系读操作和写操作

3-4 网络写操作

  又是一张大图【图片编号:image-20211224111443970.png】
在这里插入图片描述

具体内容都在图里了😂

3-5 网络读操作

  Tcp网络通信中都会存在粘包和拆包的问题,只要使用TCP传输,都需要解决这个问题,Zk解决粘包拆包的问题使用的方案和很多主流框架大致一样,使用两个buffer,代码中称为incomingBuffer和lenbuffer,这两个buffer都是用来接收数据,但是作用有所不同,lenbuffer的长度固定为4个字节【Int类型】,结合上面的数据格式可以看出,这4字节就是数据包的长度,接收到长度后会开辟对应长度的空间,并将其赋值给incomingBuffer用于存储实际从服务端接收的数据包,只有incomingBuffer存满了,才说明一个完整的数据包接收完成,初始情况下incomingBuffer和lenbuffer指向同一内存区域,后续会根据其是否指向同一内存区域来区分是读取数据包长度还是可以开始读取数据,读操作的整体流程图:
在这里插入图片描述

客户端需要从网络读取的数据主要有两大类

  • 客户端连接的响应数据,里面存储了客户端的唯一ID
  • 正常的响应数据
3-5-1 客户端连接响应数据

  客户端和服务端创建连接后会马上发送连接请求给服务端,主要目的是为了获取客户端唯一标识,也就是一直说的会话ID,有这个会话ID,Zk服务端就能动态感知客户端的上下线

那么是不是有个问题,客户端要这个东西干啥?

想象一下这个情况,如果客户端连接到Zk的某台服务器后,这台服务器挂了!!!网络就得重连,但是客户端还是那个客户端,总不能因为服务端自己挂了,就不认识这些已连接的会话了吧,所以客户端需要自己存储一下这个会话ID,在重连时封装到请求发送到服务端,服务端看到这个ID后,才知道这个客户端不是新的,也就不会删除其创建的一系列临时节点

整体流程图如下:
在这里插入图片描述

3-5-2 正常的响应数据

  正常响应数据的类型比较多,主要是通过响应的响应头标识,我们在这主要分析通知消息和请求响应,其中请求响应最终会调用FinishPacket函数处理,无论哪种想用最终都会将处理结果放到EventThread的WaitingEvents队列中,响应的整体处理流程图如下:
在这里插入图片描述

上图的通知消息处理流程中解释了为啥Zk的通知只有一次有效

FinishPacket函数的处理流程如下:
在这里插入图片描述

概括一下主要功能就是:

  • 如果发送请求时需要接收后续服务端的通知消息,则将消息处理对象注册到内部的通知管理器
  • 调用回调方法【异步】或者唤醒数据发送线程【同步】
4、事件处理线程EventThread流程解析

  上面说到服务端发送的响应或者通知消息最终都会放到EventThread的WaitingEvents队列中,线程的run方法会持续从队列中取出事件进行处理,整体的处理流程如下:
在这里插入图片描述

5、图片地址

图片的gitee地址:https://gitee.com/source-code-note/graph/tree/master/zookeeper

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐