在使用ZooKeeper构造方法时,用户传入的ZooKeeper服务器地址列表,即connectString参数,通常是这样一个使用英文状态逗号分隔的多个IP地址和端口的字符串:
- 192.168.0.1:2181,192.168.0.1:2181,192.168.0.1:2181
从这个地址串中我们可以看出,ZooKeeper客户端允许我们将服务器的所有地址都配置在一个字符串上,于是一个问题就来了:ZooKeeper客户端在连接服务器的过程中,是如何从这个服务器列表中选择服务器机器的呢?是按序访问,还是随机访问呢?
ZooKeeper客户端内部在接收到这个服务器地址列表后,会将其首先放入一个ConnectStringParser对象中封装起来。ConnectStringParser是一个服务器地址列表的解析器,该类的基本结构如下:
ConnectStringParser解析器将会对传入的connectString做两个主要处理:解析chrootPath;保存服务器地址列表。
Chroot:客户端隔离命名空间
在3.2.0及其之后版本的ZooKeeper中,添加了“Chroot”特性,该特性允许每个客户端为自己设置一个命名空间(Namespace)。如果一个ZooKeeper客户端设置了Chroot,那么该客户端对服务器的任何操作,都将会被限制在其自己的命名空间下。
举个例子来说,如果我们希望为应用X分配/apps/X下的所有子节点,那么该应用可以将其所有ZooKeeper客户端的Chroot设置为/apps/X的。一旦设置了Chroot之后,那么对这个客户端来说,所有的节点路径都以/apps/X为根节点,他和ZooKeeper发起的所有请求中相关的节点路径,都将是一个相对路径——相对于/apps/X的路径。例如通过ZooKeeper客户端API创建节点/test_chroot,那么实际上在服务端被创建的节点是/apps/X/test_chroot。通过设置Chroot,那么实际上在服务端被创建的节点是/apps/X/test_chroot。通过设置Chroot,我们能够将一个客户端因够用与ZooKeeper服务端的一棵子树相对应,在那些多个因够用共用一个ZooKeeper集群的场景下,这对于实现不同应用之间的相互隔离非常有帮助。
客户端可以通过在connecString中添加后缀的方式来设置Chroot,如下所示:
- 192.168.0.1:2181,192.168.0.1:2181,192.168.0.1:2181/apps/X
将这样一个connectString传入客户端的ConnectStringParser后就能够解析出Chroot并保存在chrootPath属性中。
HostProvider:地址列表管理器
在ConnectStringParser解析器中会对服务器地址做一个简单的处理,并将服务器地址和相应的端口封装成一个InetSocketAddress对象,以ArrayList形式保存在ConnectStringParser.serverAddress属性中。然后,经过处理的地址列表会被进一步封装到StaticHostProvider类中。
在讲解StaticHostProvider之前,我们首先来看其对应的接口:HostProvider。HostProvider类定义了一个客户端的服务器地址管理器:
其各接口方法的定义说明如下表所示。
接口方法 说明 int size() 该方法用于返回当前服务器地址列表的个数 InetSocketAddress next(long spinDelay) 该方法用于返回一个服务器地址InetSocketAddress,以便客户端进行服务器连接 void onConnected() 这时一个回调方法,如果客户端与服务器成功创建连接,就通过调用这个方法来通知HostProvider
ZooKeeper规定,任何对于该接口的实现必须满足以下3点,这里简称为“HostProvider三要素”。
- next()方法必须要有合法的返回值。
ZooKeeper规定,凡是对该方法的调用,必须要返回一个合法的InetSocketAddress对象。也就是说,不能返回null或其他不合法的InetSocketAddress。
- next()方法必须返回已解析的InetSocketAddress对象。
在上面我们已经提到,服务器的地址列表已经被保存在ConnectStringParser.serverAddresses中,但是需要注意的一点是,此时里面存放的都是没有被解析的InetSocketAddress。在进一步传递到HostProvider后,HostProvider需要负责来这个InetSocketAddress列表进行解析,不一定是在next()方法中来解析,但是无论如何,最终在next()方法中返回的必须是已被解析的InetSocketAddress对象。
- size()方法不能返回0。
ZooKeeper规定了该方法不能返回0,也就是说,HostProvider中必须至少有一个服务器地址。
StaticHostProvider
接下来我们看看ZooKeeper客户端中对HostProvider的默认实现:StaticHostProvider,其数据结构如下图所示。
解析服务器地址
针对ConnectStringParser.serverAddresses集合中那些没有被解析的服务器地址,StaticHostProvider首先会对这些地址逐个进行解析,然后再放入serverAddresses集合中去。同时,使用Collections工具类的shuffle方法来将这个服务器地址列表进行随机的打散。
获取可用的服务器地址
通过调用StaticHostProvider的next()方法,能够从StaticHostProvider中获取一个可用的服务器地址。这个next()方法并非简单的从serverAddress中依次获取一个服务器地址,而是先将随机打散后的服务器地址列表拼装成一个环形循环队列,如下图所示。注意,这个随机过程是一次性的,也就是说,之后的使用过程中一直是按照这样的顺序来获取服务器地址的。
举个例子来说,假如客户端传入这样一个地址列表:“host1,host2,host3,host4,host5”。经过一轮随机打散后,可能的一种顺序变成了“host2,host4,host1,host5,host3”,并且形成了上图所示的循环队列。此外,HostProvider还会为该循环队列创建两个游标:currentIndex和lastIndex。currentIndex表示循环队列中当前遍历到的那个元素位置,lastIndex则表示当前正在使用的服务器地址位置。初始化的时候,currentIndex和lastIndex的值都为-1。
在每次尝试获取一个服务器地址的时候,都会首先将currentIndex游标向前移动1位,如果发现游标移动超过了整个地址列表的长度,那么就重置为0,回到开始的位置重新开始,这样一来,就实现了循环队列。当然,对于那些服务器地址列表提供得比较少的场景,StaticHostProvider中做了一个小技巧,就是如果发现当前游标的位置和上次已经使用过的地址位置一样,即当currentIndex和lastIndex游标值相同时,就进行spinDelay毫秒时间的等待。
总的来说,StaticHostProvider就是不断地从上图所示的环形地址列表队列中去获取一个地址,整个过程非常类似于“Round Robin”的调度策略。
对HostProvider的几个设想
StaticHostProvider只是ZooKeeper官方提供的对于地址列表管理器的默认实现方式,也是最通用和最简单的一种实现方式。读者如果有需要的话,完全可以在满足上面提到的“HostProvider三要素”的前提下,实现自己的服务器地址列表管理器。
配置文件方式
在ZooKeeper默认的实现方式中,是通过在构造方法中传入服务器地址列表的方式来实现地址列表的设置,但其实通常开发人员更习惯于将例如IP地址这样的配置信息保存在一个单独的配置文件中统一管理起来。针对这样的需求,我们可以自己实现一个HostProvider,通过在应用启动的时候加载这个配置文件来实现对服务器地址列表的获取。
动态变更的地址列表管理器
在ZooKeeper的使用过程中,我们会碰到这样的问题:ZooKeeper服务器集群的整体迁移或个别机器的变更,会导致大批客户端应用也跟着一起进行变更。出现这个尴尬局面的本质原因是因为我们将一些可能会动态变更的IP地址写死在程序中了。因此,实现动态变更的地址列表管理器,对于提升ZooKeeper客户端用户使用体验非常重要。
为了解决这个问题,最简单的一种方式就是实现这样一个HostProvider:地址列表管理器能够定时从DNS或一个配置管理中心上解析出ZooKeeper服务器地址列表,如果这个地址列表变更了,那么就同时更新到serverAddresses集合中去,这样在下次需要获取服务器地址(即调用next()方法)的时候,就自然而然使用了新的服务器地址,随着时间推移,慢慢的就能够在保证客户端透明的情况下实现ZooKeeper服务器机器的变更。
实现同机房优先策略
随着业务增长,系统规模不断扩大,我们对于服务器机房的需求也日益旺盛。同时,随着系统稳定性和系统容灾等问题越来越被重视,很多互联网公司会出现多个机房,甚至是异地机房。多机房,在提高系统稳定性和容灾能力的同时,也给我们带来了一个新的困扰:如何解决不同机房之间的延时。我们以目前主流的采用光电波传输的网络带宽架构(光纤中光速大约为20万公里每秒,千兆带宽)为例,对于杭州和北京之间相隔1500公里的两个机房计算其网络延时:
(1500*2)/(20*10000)=15(毫秒)
需要注意的是,这个15毫秒仅仅是一个理论上的最小值,在实际的情况中,我们的网络线路并不能实现直线铺设,同时信号的干扰、光电信号的转换以及自身的容错修复对网络通信都会有不小的影响,导致了在实际情况中,两个机房之间可能达到30~40毫秒,甚至更大的延时。
所以在目前大规模的分布式系统设计中,我们开始考虑引入“同机房优先”的策略。所谓的“同机房优先”是指服务的消费者优先消费同一个机房中提供的服务。举个例子来说,一个服务F在杭州机房和北京机房中都有部署,那么对于杭州机房中的服务消费者,会优先调用杭州机房中的服务,对于北京机房的客户端也一样。
对于ZooKeeper集群来说,为了达到容灾要求,通常会将集群中的机器分开部署在多个机房中,因此同样面临上述网络延时问题。对于这种情况,就可以实现一个能够优先和同机房ZooKeeper服务器创建的HostProvider。