1. Zookeeper技术内幕
1.1. 客户端
1.1.1. 服务器地址列表
Zookeeper构造方法中传入的地址,使用逗号分隔的多个IP地址和端口的字符串,
192.168.0.1:2181,192.168.0.2:2181,192.168.0.3:2181
Zookeeper客户端在连接服务器的过程中,是如何从这个服务器列表中选择服务器机器的呢?是按序访问,还是随机访问呢?
Zookeeper客户端内部在接收到这个服务器地址列表后,会将其首先放入一个ConnectStringParser对象中封装起来。ConnectStringParser是一个服务器地址列表的解析器,该类的基本结构如下:
public final class ConnectStringParser {
/*
* 根目录
* 如:connectString=100.73.17.29:2181,100.73.17.30:2181,100.73.17.31:2181/checksystem/faceaudit
* chrootPath = /checksystem/faceaudit
* */
private final String chrootPath;
/*存储所有的zookeeper服务器地址*/
private final ArrayList<InetSocketAddress> serverAddresses
= new ArrayList<InetSocketAddress>();
}
ConnectStringParser做两个主要的处理,解析chrootPath和解析服务器地址列表。
Chroot:客户端隔离命名空间
在3.2.0之后版本的zookeeper中,添加了“Chroot”特性,该特性允许每个客户端为自己设置已给命名空间。如果一个zookeeper客户端设置了Chroot,那么该客户端对服务器的任何操作,都将会被限制在自己的命名空间下。
客户端可以通过在connectString中添加后缀的方式来设置Chroot,如下所示:
192.168.0.1:2181,192.168.0.2:2181,192.168.0.3:2181/apps/X
将这样一个connectString传入客户端的ConnectStringParser后就能够解析出Chroot并保存在chrootPath属性中。
解析服务器地址
针对ConnectStringParser.serverAddresses集合中哪些没有被解析的服务器地址,staticHostProvider首先会对这些地址逐个进行解析,然后再放入serverAddresses结合中去。同时使用Collections工具类的shuffle方法来将这个服务器地址列表进行随机的打算。
获取可用的服务器地址
通过调用staticHostProvider的next()方法,能够从staticHostProvider中获取一个可用的服务器地址。这个next()方法并非简单地从serverAddresses中一次获取一个服务器地址,而是现将随机打散后的服务器地址列表拼装成一个环形的循环队列,如下图,注意这个随机过程是一次性的,也就是说,之后的使用过程中一直是按照这样的顺利来获取服务器地址的。
举个例子来说,假如客户端传入这样一个地址列表:“host1,host2,host3,host4,host5”。经过一轮随机打散后,可能的一种顺序变成了“host2,host4,host1,host5,host1”,并且形成了上图的循环队列。此外HostProvider还会为该环形队列创建两个游标:currentIndex俄lastIndex。currentIndex标识环形队列中当前遍历到的那个元素位置,lastIndex则表示当前正在使用的服务器地址位置。初始化的时候,curre和lastIndex的值都为-1。
在每次尝试获取一个服务器地址的时候,都会首先将currentIndex游标向前移动1位,如果发现游标移动超过了整个地址列表的长度,那么就重置为0,回到开始的位置重新开始,这样一来,就实现了循环队列。当然对于那些服务器地址列表提供的比较少的场景,staticHostProvider中做了一个小技巧,就是如果发现当前游标的位置和上次已经使用过的地址位置一样,即当currentIndex和lastIndex游标值相同时,就进行spinDelay毫秒时间的等待。
总的来时,staticHostProvider就是不断从上图所示的环形地址列表队列中去获取已给地址,整个过程非常类似于“Round Robin”的调度策略。
对于HostProvider的几个设想
staticHostProvider只是zookeeper官方提供的对于地址列表管理器的默认实现方式,也是最通用和最简单的一种实现方式。读者如果有些要的话,满足接口要求的前提下,可以实现自己的服务器地址列表管理器。
1.配置文件方式
实现从配置文件中加载服务器地址列表。
2.动态变更的地址列表管理器
从DNS或一个配置管理中心上解析出zookeeper服务器地址列表。
3.实现同机房优先策略
在目前大规模的分布式系统设计中,多机房的情况下,考虑引入“同机房优先”的策略。所谓的“同机房优先”是指服务的消费者优先小飞同一个机房中提供的服务。举个例子来说,一个服务F在杭州机房和北京机房中都有部署,那么对于杭州机房中的服务消费者,会优先调用杭州机房中的服务,对于北京机房的客户端也一样。
对于zookeeper继群来说,为了达到容灾要求,通常会将集群中的机器分开部署在多个机房中,这样就会面临网络延迟问题。对于这种情况,就可以实现一个能够优先和同机房zookeeper服务器创建会话的HostProvider。
1.1.2. 请求发送与响应接收
请求发送
在正常情况下(即客户端与服务端之间的TCP连接正常且会话有效的情况下),会从 outgoingQueue队列中提取出一个可发送的Packet对象,同时生成一个客户端请求序号xid并将其设置到Packet请求头中去,然后将其序列化后进行发送。
请求发送完毕后,会立即将该Packet保存到pendingQueue队列中,以便等待服务端响应返回后进行相应的处理,如上图。
响应接收
客户端获取到来自服务端的完整响应数据后,根据不同的客户端请求类型,会进行不同的处理。
l 如果检测到当前客户端还尚未进行初始化,那么说明当前客户端与服务端之间正在进行会话创建,那么就直接将接收到的ByteBuffer(incomingBuffer)序列化成 ConnectResponse 对象。
l 如果当前客户端已经处在正常的会话周期,并且接收到的服务端响应是一个事件, 那么ZooKeeper客户端会将接收到的ByteBuffer (incomingBuffer)序列化成WatcherEvent对象,并将该事件放入待处理队列中。
l 如果是一个常规的请求响应(指的是Create, GetData和Exist等操作请求),那么会从pendingQueue队列中取出一个Packet来进行相应的处理。ZooKeeper 客户端首先会通过检验服务端响应中包含的XID值来确保请求处理的顺序性,然后再将接收到的ByteBuffer (incomingBuffer)序列化成相应的Response对象。
SendThread
SendThread是客户端ClientCnxn内部一个核心的I/O调度线程,用于管理客户端和服务端之间的所有网络I/O操作。在ZooKeeper客户端的实际运行过程中,一方面, SendThread维护了客户端与服务端之间的会话生命周期,其通过在—定的周期频率内向服务端发送一个PING包来实现心跳检测。同时,在会话周期内,如果客户端与服务端之间出现TCP连接断开的怙况,那么就会自动且透明化地完成重连操作。
另一方面,SendThread管理了客户端所有的请求发送和响应接收操作,其将上层客户端API操作转换成相应的请求协议并发送到服务端,并完成对同步调用的返回和异步调用的回调。同时,SendThread还负责将来自服务端的事件传递给EventThread去处理。
EventThread
EventThread是客户端ClientCnxn内部的另一个核心线程,负责客户端的事件处理,并触发客户端注册的Watcher监听。EventThread中有一个waitingEvents队列,用于临时存放那些需要被触发的Object,包括那些客户端注册的Watcher和异步接口中注册的回调器AsyncCallback。同时,EventThread会不断地从waitingEvents这个队列中取出Object, 识别出具体类型(Watcher或者AsyncCallback),并分别调用process和processResult接口方法来实现对事件的触发和回调。