本文作者:HelloGitHub-老荀
Hi,这里是HelloGitHub推出的HelloZooKeeper系列,免费开源、有趣、入门级的ZooKeeper教程,面向有编程基础的新手。
前一篇文章我们介绍了Follower或Observer是如何同Leader同步数据的,以及ACL的介绍、使用和原理。这章我们将正式学习有关session的内容,具体客户端怎么同服务端保持心跳?服务端不同节点之间是如何保持心跳?
一、客户端会话的秘密
会话,即session,这个词语或者说概念很多地方都有用到,在ZK中会话指的是两个不同的机器建立了网络连接后,就可以说他们之间创建了一个会话。ZK的会话是有超时的概念的,当会话超时后,会由服务端主动关闭,当然客户端也可以主动请求服务端想要关闭会话。你可能会问,为什么要搞这个麻烦,直接两边连上一直用不就好了吗?有了会话这个概念就是为了防止,在建立连接后,有些客户端不常使用,早点关闭连接可以节省资源。
1.1鸡太美的一天
我发现我好久没有cue鸡太美了,这次就让他再C位出道一次吧。
我们的鸡太美每天起床后,日常发微博、直播、跳舞、打篮球,很多事务都需要去办事处办理。
所以第一件事情就是去办事处找马果果(现在就假设马果果一个办事处)申请使用办事处(建立连接,创建会话)
而马果果会为鸡太美创建一个ID,就是会话ID,这个ID(我这里假设是)和鸡太美会进行绑定,而鸡太美在申请的同时还需要告诉马果果自己最长的超时时间是多久,我这里假设是毫秒。
而马果果这边会记录下来:
在马果果开张的时候自己本身也有一个会话的检查间隔,就是配置在zoo.cfg中的tickTime选项,我这里假设是毫秒。马果果在开张的时候会计算出一个时间轴,这个时间轴的间隔是固定的,并且不会改变。
然后马果果会通过鸡太美的以及当前的时间戳结合时间轴,计算出一个鸡太美会话超时时间点
然后会记录下来:
记录完,就算鸡太美会话创建成功了。
而马果果这边会遵循这个时间轴的节点定期对会话进行检查,假设现在的时间进行到鸡太美的时间点了
马果果会把在这个时间点的会话全部取出(记得我们上面说过,可以是多个吗?)
然后会根据ID信息找到对应的村民,一个个通知他们会话关闭了。
你可能会问现在因为鸡太美超时时间是,而马果果超时检查是,正好是整数倍,如果超时时间不是整数倍呢?要不说我们的马果果同志好学上进呢,他早就想到啦,所以设计了一个算法,无论村民的超时时间是怎么样,都会向下取整找到马果果设置的检查点。
假设鸡太美的超时间是
再比如鸡太美的超时时间是
所以看到了吧,以马果果的为例,只要小于的都按照0来算,小于的按照来算,小于的按照来算,以此类推,所以只要马果果自己的检查时间间隔确定了之后,无论是哪个村民设置了什么样的超时时间都能被向下取整至最近的统一检查点。这样马果果检查的时候就不会有太大的负担,可以统一对村民的超时时间进行检查。
但是这么做一定会造成客户端的超时时间是有误差的(通常是比设置的要短一点),减少这个误差的方式就是减小马果果的检查间隔,也就是tickTime参数(默认是,已经够用了我觉得)。
而马果果的会话管理不会只有鸡太美一个人,我们来看看有多个村民的会话管理页长什么样吧
可以看到使用了三个哈希表去记录这些映射关系,画到时间轴是这样的
所以当时间进行到的时候,对应三个村民就超时了,2530时另外两个村民就超时了。
这里我还得说下其实会话ID在马果果这边办事处开张后就会根据当前时间戳和myid初始化出一个基数,举个例子可能是类似这种数字,之后每一个村民过来分配会话ID的时候,只是对这个数字不停地加1,所以不会出现乱七八糟无序的数字,图中的数字举例仅仅是我个人的玩梗癖好,和实际情况不符~
但是这样的话,鸡太美岂不是每次毫秒就超时了吗?这当然不可能,因为村民的每一次任意的操作(增删改查)都会刷新该超时时间戳,具体怎么做的呢?我们一起来看下,假设红色箭头是会话刚创建时马果果替鸡太美计算出来的超时时间,假设在绿色箭头时间戳的地方,鸡太美执行了任意操作。
马果果会根据当前时间戳(绿色箭头处)加上鸡太美之前设置的超时时间(),重新计算出新的超时时间:
然后对会话管理页的数据进行修改,我仍然以多个村民的例子讲解
更新前:
更新后:
这个更新的过程可以被称为会话激活。
1.2心跳检测
猿话一下,除了客户端每次的正常操作会刷新超时时间以外,客户端仍然需要一个机制去保持住这个会话,这个机制就是我们平时听到过的心跳检测,原理是每次客户端启动的时候也会设置一个心跳检测的间隔时间,在后台一直会去判断最后一次发送的时间戳和当前时间是否超过了该心跳检测的间隔,如果超过了就会发送一个名为PING的请求,由于刚刚我们说了客户端的任意操作都会刷新该超时时间,PING也不例外,有了这个心跳机制就可以让客户端保持住和服务端的会话状态。而服务端收到PING,除了刷新超时时间会简单的回复一个PING给客户端,而客户端收到服务端的PING会直接丢弃不需要任何其他操作。
我们以Java客户端为例
ZooKeeperclient=newZooKeeper(.0.0.1:,1,null);
假设超时时间设置1毫秒,那么客户端的心跳间隔就是毫秒,计算过程如下
1*2/3/2=//这个公式是代码中的写死逻辑,其实就是/3
所以只要客户端空闲时间超过毫秒,就会发送一个PING给服务端,如果客户端的超时时间设置的非常大的话,比如半小时,那每隔10秒也会强制发送一个PING(这个10秒是Java客户端写死的逻辑)。
客户端和服务端之间的会话先讲到这里,接下来我们聊聊服务端之间的会话。
二、服务端会话的秘密
如果村里是同时有多个办事处的时候(我这里先假设两个),情况就不太一样了。
假设鸡太美第一次连接的时候找到的作为Follower的马小云:
而Follower是不能独自处理非读请求的,所以此次马小云会为鸡太美分配好ID之后,将创建会话操作转发给马果果,这样就好像是鸡太美找到马果果一样,流程和上面是一样的,在会话管理页中记录下来。
而马小云自己也会简单的维护一个会话ID和超时时间的映射关系,以多个村民为例,每次收到请求都会对其进行记录
现在鸡太美是连接的马小云办事处(包括每次心跳发送),但是全局的会话管理数据在马果果这里,这样是怎么维持住会话状态的呢?
这里我们就得先聊聊服务端之间是怎么进行心跳的。
服务端有一个重要的配置tickTime(默认是),还有另一个重要的配置syncLimit(默认是5),我就以这两个默认值来举例:
首先Leader会以(tickTime/2)毫秒的频率去对各个Follower发起PING的请求每次检查Follower返回的PING的超时时间是否超过0(tickTime*syncLimit),超过这个时间没有收到该Follower的ACK响应就关闭和该Follower的socket连接
那Follower收到PING的消息后会回复一个PING给Leader并且会把自己记录的会话映射关系一起发过去
还会立即清空自己本地的映射关系!
然后Leader收到Follower的这个PING响应后,因为之前所有客户端的会话管理数据其实都在Leader这里,所以Leader可以对发过来的会话ID和超时时间进行会话激活,具体方法和之前的例子中是一样的,通过服务端之间的PING,既可以完成服务端之间的心跳检测,又可以对客户端的会话进行激活,又是一次一鱼两吃。
小结一下:
会话是ZK中的重要概念,会话的状态会影响,服务端对客户端请求的处理客户端的每次操作都会延长会话的超时时间,并且客户端会主动发起PING请求来保持住会话,以免在空闲时会话超时被服务端关闭客户端的会话数据是保存在Leader端的,Follower只是在每次操作的时候简单的记录下会话ID和超时时间的映射关系服务端之间的心跳PING是由Leader主动向Follower发起的Follower收到PING后会将自己保存的会话映射数据发送给LeaderLeader收到Follower的PING响应后会对发送过来的会话数据进行激活我们现在已经知道了会话的概念,就可以聊聊临时节点了。
三、临时节点
我们先来看下临时节点的创建代码
client.create(/HelloZooKeeper/niubi,null,ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL);
这次的创建操作和其他的持久节点创建并无区别,需要在小红本上写下记录,而这个记录中有一个字段是ephemeralOwner当节点是持久节点这个字段值是0,但当节点是临时节点时这个字段记录的就是持有该节点的会话ID。
除了在小红本上创建记录以外,由于是临时节点,还需要额外在一个专门的地方也记录一下,假设还是鸡太美创建了3个临时节点:
=[/鸡太美/我真美,/鸡太美/我真帅,/鸡太美/我真秀]
在鸡太美会话超时的时候,可能是会话真超时了(由于有心跳机制,所以这个可能性其实不大),也可能是鸡太美主动关闭的会话。
马果果就会从这个记录临时节点的地方根据鸡太美的会话ID取出对应的临时节点的路径,然后根据路径删除即可,效果和鸡太美主动删除是一样的,这样就达到了,当客户端关闭之后,对应的临时节点会自动清除的特点。这个临时节点的特性就会被用在ZK实现分布式锁的时候,防止了客户端因意外退出没法执行释放锁的逻辑!
四、协议
还有一个东西我一直就没提过,就是ZK的协议。
众所周知,ZK是一个CS架构的应用,有客户端和服务端之分,那既然这样就免不了需要进行网络通信,而且不光是客户端和服务端之间,服务端和服务端之间也需要通信,有了网络通信就离不开协议,但是协议既是最重要的东西,也是最不重要的东西。
最重要是因为,ZK本身就是基于该协议去通信的,无论是客户端还是服务端之间,我之前提到的各种暗号,如:REQUEST、ACK、COMMIT、PING等。都属于协议中的一个字段,用来区分不同的消息。协议构成了整个ZK通信的基础,能够通信了才能完成整个组件的功能。最不重要是因为,除非你想开发ZK的客户端,主动去请求ZK服务端,不然即使你完全不知道协议的具体格式,也不会影响你理解整个ZK的原理,而且协议的介绍非常的枯燥和无用,容易劝退。所以我把这个概念留到了最后才提起,并且我也不打算去讲解ZK中不同请求的协议具体长什么样。这次我就换一个角度简单的介绍下协议。
首先,我介绍的ZK都是Java程序,无论客户端还是服务端,所以协议的本质是规定如何把Java对象转成字节流,方便在网络中传输,以及拿到字节流的那一方,如何再把这个字节流转换回Java对象,这其实就是序列化和反序列化的过程。而为了方便序列化,ZK中定义的各种对象,如XxxRequest、XxxResponse、XxxPacket等,它们的字段类型通常就几种:int、long、String、byte[]、List、boolean以及其他嵌套的类型。
4.1int、long、boolean
对于这三种类型来说最简单,直接用输出流写即可,区别就是一个是4字节,一个是8字节,一个是1字节
4.2String、byte[]
这两种是类似,如果字段为空,则就写入一个-1,不为空就先写一个int表示长度,之后紧跟byte[]表示具体数据即可
4.3嵌套类型、List
碰到List和4.2是一样,如果为空就写-1,不为空就先写List长度,之后遍历List根据泛型(也只可能是上面这几种)决定如何继续写入,嵌套对象的话就把这个写入操作委托给它就行了,因为它的字段也只可能是上面这几种。
4.4小结
ZK的序列化协议采用的紧凑书写的方式,根据不同的字段类型依次写入最终的字节流即可。
五、总结
今天我们介绍了ZK会话相关的知识:会话是什么,客户端和服务端的会话如何保持,服务端和服务端的会话如何保持,以及介绍了临时节点是如何利用会话机制在会话结束后被自动删除的,最后再用很短的篇幅带大家了解了下ZK的协议,不知不觉已经写了九篇了,我决定这一篇是本系列中最后一篇讲解原理的,之后的文章不讲原理介绍下ZK中的一些隐藏功能,还有整理下重要的资料,如配置信息,面试大全,目标是打造收藏向的三篇重磅文章。期待一下吧~