ZooKeeper会话Session (秒懂+图解+史上最全)

文章很长,而且持续更新,建议收藏起来,慢慢读! Java 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源:


什么是Zookeeper的会话机制

那对于ZK的服务端来说,如何维护管理这些会话,就是本文要聊的内容啦~

我们在服务器启动Zookeeper的时候能得知,ZK服务端对外默认端口是2181。而客户端连接到服务端上,其本质其实就是一个TCP连接(长连接) ,当连接正式建立起来的时候,就开起来该次会话的生命周期了。有了会话之后,后续的请求发送,回应,心跳检测等机制都是基于会话来实现的。

为什么会有会话机制Session

ZooKeeper的架构图

首先我们看下ZooKeeper的架构图,client跟ZooKeeper集群中的某一台server保持连接,发送读/写请求,读请求直接由当前连接的server处理,写请求由于是事务请求,由当前server转发给leader进行处理。同时,client还能接收来自server端的watcher通知。

而所有的这些交互,都是基于client和ZooKeeper的server之间的TCP长连接,也称之为Session会话。ZooKeeper对外的服务端口默认是2181,客户端启动时,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够通过心跳检测和服务器保持有效的会话,也能够向ZooKeeper服务器发送请求并接受响应,同时还能通过该连接接收来自服务器的Watch事件通知。Session的SessionTimeout值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在SessionTimeout规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。

说点题外话,长连接、短连接、数据库连接池:

短连接 :连接->传输数据->关闭连接
也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接。

长连接:连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。
长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。

网络中不同节点使用TCP协议通过SOCKET进行通信,首先需要3次握手建立连接,数据传输,4次握手断开连接,因此如果频繁的创建、关闭,是很耗费系统资源的,就像短连接那样;使用长连接貌似弥补了短连接的缺点,但是,如果并发量过大,会有大量的长连接,同样会耗费大量系统资源,因此具体选用长连接还是短连接,是要根据具体的场景来选择。

ZooKeeper中一个client只会跟一个server进行交互(除非与当前server连接失败,会切换到下个server),不管这种交互有多频繁,只需要一个TCP长连接就足以应对,因选择一个TCP长连接,不失为一种最好的方案。

数据库连接池:我们在使用JDBC进行数据库连接的时候,其实是建立了一个数据库连接池,它本身是一种短连接+长连接的方案,我们通过JDBC的3个关键配置来说明下:

参数名称 参数说明 默认值 备注
minPoolSize 连接池中保留的最小连接数 5 长连接
maxPoolSize 连接池中保留的最大连接数 15 短连接
maxIdleTime 最大空闲时间,如果超出空闲时间未使用,连接被收回

超过最小连接数后创建的连接,在最大空闲时间后如果未使用,是会被回收的,因此可以被理解为短连接。但是保留的最小连接数,即使未被使用也会一直存在,等待被使用,因此可以理解为长连接。

好了,扯了这么远,我们还是回到ZooKeeper是如何通过TCP长连接来管理它的Session会话的吧。

Session相关的基本概念

当连接建立的时候,Session就已经建立起来,与这个过程相关的有三个重要的值:

  • SessionID:会话的唯一标识,由ZK来分配
  • TimeOut:会话超时时间。在客户端与服务端连接的期间,如果因为某些原因断开了连接(如网络中断等等),该次会话以及其相关的临时节点不会被马上删除,而是等待TimeOut耗尽之后,若客户端没有重连上来,那本次会话才会失效,相关的一些临时节点也会被删除
  • Expiration Time:TimeOut是一个相对时间,而Expiration Time则是在时间轴上的一个绝对过期时间。

顺便贴一下SessionId生成的源码,SessionId的生成和两个东西相关联,一个是时间戳,一个是机器id

/**其中id是机器id**/
public static long initializeNextSession(long id) {
        long nextSid = 0;
        nextSid = (System.currentTimeMillis() << 24) >>> 8;
        nextSid =  nextSid | (id <<56);
        return nextSid;
}

分桶机制

Session是由ZK服务端来进行管理的,一个服务端可以为多个客户端服务,也就是说,有多个Session,那这些Session是怎么样被管理的呢?而分桶机制可以说就是其管理的一个手段。ZK服务端会维护着一个个"桶",然后把Session们分配到一个个的桶里面。而这个区分的维度,就是ExpirationTime

img

为什么要如此区分呢?因为ZK的服务端会在运行期间定时地对会话进行超时检测,如果不对Session进行维护的话,那在检测的时候岂不是要遍历所有的Session?这显然不是一个好办法,所以才以超时时间为维度来存放Session,这样在检测的时候,只需要扫描对应的桶就可以了

那这样的话,新的问题就来了:每个Session的超时时间是一个很分散的值,假设有1000个Session,很可能就会有1000个不同的超时时间,进而有1000个桶,这样有啥意义吗?

可以看到,最终得到的ExpirationTime是ExpirationInterval的倍数,而ExpirationInterval就是ZK服务端定时检查过期Session的频率,默认为2000毫秒。所以说,每个Session的ExpirationTime最后都是一个近似值,是ExpirationInterval的倍数,这样的话,ZK在进行扫描的时候,只需要扫描一个桶即可。

另外让过期时间是ExpirationInterval的倍数还有一个好处就是,让检查时间和每个Session的过期时间在一个时间节点上。否则的话就会出现一个问题:ZK检查完毕的1毫秒后,就有一个Session新过期了,这种情况肯定是不好。

Session激活(续约)

在客户端与服务端完成连接之后生成过期时间,这个值并不是一直不变的,而是会随着客户端与服务端的交互来更新。过期时间的更新,当然就伴随着Session在桶上的迁移

为了保持client会话的有效性,在ZooKeeper运行过程中,client会在会话超时时间过期范围内向server发送PING请求来保持会话的有效性,俗称“心跳检测”。同时server重新激活client对应的会话,这段逻辑是在SessionTrackerImpltouchSession中实现的。先看下流程,再看源码:

会话激活
再看下源码实现:

//sessionId为发起会话激活的client的sessionId,timeout为会话超时时间
synchronized public boolean touchSession(long sessionId, int timeout) {
        /*
         * sessionsById的结构为 HashMap<Long, SessionImpl>(),每个sessionid都有一个对应的session实现
         * 这里取出对应的session实现
         */
        SessionImpl s = sessionsById.get(sessionId);
        // Return false, if the session doesn't exists or marked as closing
        if (s == null || s.isClosing()) {
            return false;
        }
        //计算当前会话的下一个失效时间,可以理解为ExpirationTime_New
        long expireTime = roundToInterval(System.currentTimeMillis() + timeout);
        //tickTime是上一次计算的超时时间,可以理解为ExpirationTime_Old
        if (s.tickTime >= expireTime) {
            // Nothing needs to be done
            return true;
        }
        //将ExpirationTime_Old对应的桶中的会话取出,SessionSet 是SessionImpl的集合
        SessionSet set = sessionSets.get(s.tickTime);
        if (set != null) {
        	//将旧桶中的会话移除
            set.sessions.remove(s);
        }
        //更新当前会话的下一次超时时间
        s.tickTime = expireTime;
        //从新桶中取出该会话,无则创建,有则更新
        set = sessionSets.get(s.tickTime);
        if (set == null) {
            set = new SessionSet();
            sessionSets.put(expireTime, set);
        }
        set.sessions.add(s);
        return true;
    }

最简单的一点,客户端每向服务端发送请求,包括读请求和写请求,都会触发一次激活,因为这预示着客户端处于活跃状态

而如果客户端一直没有读写请求,那么它在TimeOut的三分之一时间内没有发送过请求的话,那么客户端会发送一次PING,来触发Session的激活。当然,如果客户端直接断开连接的话,那么TimeOut结束后就会被服务端扫描到然后进行清楚了

参考文献

https://blog.csdn.net/MuErHuoXu/article/details/86218115
https://blog.csdn.net/SCUTJAY/article/details/106889060

原文地址:https://www.cnblogs.com/crazymakercircle/p/15628331.html