关于ZooKeeper的会话机制Session解读

2023-02-15 12:02:30 会话 机制 解读

一、为什么会有会话机制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)如何管理

2.1)SessionID的初始化

首先了解3个基本概念:

  • sessionID:会话ID,用来唯一标识一个会话,每次客户端创建会话的时候,ZooKeeper都会为其分配一个全局唯一的sessionID
  • TimeOut:会话超时时间,如果客户端与服务器之间因为网络闪断导致断开连接,并在TimeOut时间内未连上其他server,则此次会话失效,此次会话创建的临时节点将被清理
  • ExpirationTime:下次会话超时时间点。ZooKeeper会为每个会话标记一个下次会话超时时间点,便于对会话进行“分桶管理”,同时也是为了搞笑低耗的实现会话的超时检查与清理。其值接近于当前时间+TimeOut,但不完全相等,稍后会介绍。

在每次client向server发起“会话创建”请求时,服务端都会为其分配一个sessionID,现在看下sessionID是如何生成的。

在SessionTrackerImpl初始化的时候,会调用initializeNextSession来生成一个初始化的sessionID,之后在该sessionID的基础上为每个会话进行分配,其初始化算法如下:

//是ZooKeeper服务器的会话管理器,负责会话的创建、管理和清理等工作
public class SessionTrackerImpl extends Thread implements SessionTracker {
   
    {...}

	//参数id为当前服务器的myid
    public static long initializeNextSession(long id) {
        long nextSid = 0;
        //此处采用无符号右移,是为了防止出现负数的情况
        nextSid = (System.currentTimeMillis() << 24) >>> 8;
        nextSid =  nextSid | (id <<56);
        return nextSid;
    }
    
	{...}
}

该逻辑计算后得到的sessionID的前8位确定了所在的机器,后56位使用当前时间的毫秒表示进行随机。

2.2)分桶策略

SessionTrackerImpl通过**“分桶策略”来进行会话的管理,分桶的原则是将每个会话的“下次超时时间点”(ExpirationTime)**相同的会话放在同一区块中进行管理,以便于ZooKeeper对会话进行不同区块的隔离处理,以及同一区块的统一处理,如下图,横坐标是一个个的超时时间点ExpirationTime:

分桶管理

每个会话创建完毕后,ZooKeeper就会为其计算ExpirationTime,计算方式大体如下:

ExpirationTime = CurrentTime(当前时间) + SessionTimeOut(会话超时时间)

但图中标识的ExpirationTime并不是以上公式简单的算出来的时间。因为在ZooKeeper的实际实现中,还做了一个处理。

ZooKeeper的Leader服务器在运行期间会定时的进行会话超时检查,其时间间隔为ExpirationInterval(默认值2000毫秒),每隔2000毫秒进行一次会话超时检查。

为了方便同时对多个会话进行超时检查,完整的ExpirationTime计算方式如下:

ExpirationTime_ = CurrentTime + SessionTimeOut
ExpirationTime = ( ExpirationTime_/ExpirationInterval + 1 ) * ExpirationInterval

注意不要使用小学的乘法分配律把小括号给消化掉,它存在的目的就是为了保证ExpirationTime是ExpirationInterval的整数倍,那为什么要这样做???

提高会话检查的效率。让创建时间临近的会话,分配在一个桶中,实际生产环境中一个服务端会有很多客户端会话,逐个检查过期时间会非常耗时,把它们放在一个桶中批量处理,可以大大提高效率。

比如CurrentTime为1547046000、1547046001这样的会话就会被分配在一个桶中。

其次,Leader每隔ExpirationInterval 毫秒进行会话的清理,而刚好 ExpirationTime 这个时间点是会话的失效时间点,如果发现失效,直接清理掉就OK,避免了检查时未失效,但没过几毫秒又失效了这种情况。

比如,ExpirationTime 是1547046000,如果在1547045998的时刻检查,发现还有效,但过了2ms之后就无效了。而如果会话超时检查和会话超时时间在同一个时间节点的话,就会避免这种情况。

2.3)会话激活

为了保持client会话的有效性,在ZooKeeper运行过程中,client会在会话超时时间过期范围内向server发送PING请求来保持会话的有效性,俗称“心跳检测”。

同时server重新激活client对应的会话,这段逻辑是在SessionTrackerImpltouchSession中实现的。

先看下流程,再看源码

会话激活

再看下源码实现:

//sessionId为发起会话激活的client的sessionId,timeout为会话超时时间
synchronized public boolean touchSession(long sessionId, int timeout) {
        
        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;
    }

好了,我们了解了是会话是如何激活的,那在什么时候会发起激活呢,也就是touchSession这个方法什么时候被触发呢?

分以下两种情况:

  • 只要client向server发送请求,包括读或写请求,就会触发一次激活;
  • 如果client发现在sessionTimeOut / 3 时间内未尚和server进行任何通信,就会主动发起一次PING请求,进而触发激活;

关于会话激活,可以举个非常脑洞的例子:就像你跟房东租房,进行续签一样。合同是一年一年的续签,这是理论情况下,但是中间免不了要跟房东打交道,比如洗衣机坏了,问问房东如何处理,这一问,糟了,从问的这一天开始,重新签一年的合同吧(当然是把之前的租金结算一下);另外一种就是 租期一年 / 3 = 每个季度,主动的跟房东续签下合同(当然也是把之前的租金结算一下)……

租房伤不起啊,个税申报抵消房租,房东还不愿意[此处一个欲哭无泪的表情]

三、过期会话(Session)如何清理

一言蔽之吧,会话过期后,集群中所有server都删除由该会话创建的临时节点(EPHEMERAL)信息

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持。

相关文章