前言
在前面的小节中,我们介绍了ZK的Session的基本原理,其中有一个属性是这么描述的:
TickTime:下次会话超时时间点,默认 2000 毫秒。可在 zoo.cfg 配置文件中配置,便于 server 端对 session 会话实行分桶策略管理。
这里对Session会话实行分桶策略管理是什么意思呐?这就是本节要重点来介绍的。
一、分桶策略概述
1.1 什么是分桶策略
Zookeeper的session管理主要是通过SessionTracker来负责,其采用了分桶策略进行管理。
分桶策略是指,将空闲超时时间相近的会话放到同一个桶中来进行管理,以减少管理的复杂度。在检查超时时,只需要检查桶中剩下的会话即可,因为没有超时的会话已经被移出了桶,而桶中存在的会话就是超时的会话。
zk 对于会话空闲的超时管理并非是精确的管理,即并非是一超时马上就执行相关的超时操作。
1.2 什么是分桶策略辅助理解
通过分桶策略的定义,我们可以看出分桶策略中有两个桶,这两个桶在代码层面的表现形式就是两个Map数据类型,那么我们就会想Map的key是什么?接着往下看,我们慢慢就会揭开TA神奇的面纱。
二、分桶策略原理
2.1 分桶策略图示解说
那在每个红点所在的时间点都要运行一次‘session过期任务’,而且一次任务的执行,只使得一个session过期。这样效率不高。而且如果session比较多的话,那“session过期任务”的执行会占用很大的负载。
ZooKeeper为了方便管理session超时时间,使用分桶策略,使用如下计算公式,来计算session的超时时间:
long currentTime = Time.currentElapsedTime();
long sessionTimeout = 10000;
long expireTime_0 = currentTime + sessionTimeout;
long expireTime = (expireTime_0 / expirationInterval + 1) * expirationInterval;
此计算公式,会根据expirationInterval(默认2000毫秒)把时间分成相同时间段,让处于同一时间段的session的超时时间移动到此时间段结束的时间点上。
如上例子,这三个session的超时时间都变为ExpirationTime2。也就是在ExpirationTime1到ExpirationTime2之间的session,超时时间都设置为ExpirationTime2,这样当currentTime到达ExpirationTime2的时刻,执行一次session过期任务,就把这三个session都给设置为过期了。
这样处理会更加方便。
2.2 分桶类ExpiryQueue整体工作流程图
2.2.1 ExpiryQueue作用
ZooKeeper服务端管理客户端会话超时使用到ExpiryQueue,用来管理Session超时的会话。
2.2.2 ExpiryQueue类图
该类中主要包含了以下变量:
(1)nextExpirationTime(下一个过期的时间点) ;
(2)expirationInterval(过期时间间隔) ;
(3)elemMap(Session对象集合,key是session对象,类型是SessionImpl,value为过期时间);
(4)expiryMap(过期的Session对象集合,key为过期时间,value为session)
该类中主要方法:
(1)构造方法初始化nextExpirationTime
(2)update增加或更新session的过期时间
(3)remove清除过期session
(4)getWaitTime判断当前时间是否已经超过了nextExpirationTime,超过返回0,没有超过返回nextExpirationTime-now,zookeeper中通过不停的轮询这个方法来判断是否清除过期session
(5)poll拉取过期session进行清除
2.2 分桶类ExpiryQueue数据结构理解
ExpiryQueue根据expirationInterval将时间分段,将每段区间的时间放入对应的一个集合进行管理。如图二所示,时间段在1503556830000-1503556860000中的数据将会放到1503556860000对应的集合中,1503556860000-1503556890000中的数据将会放到1503556890000的集合中,以此类推。
在ExpiryQueue的数据结构中,图中的集合由ConcurrentHashMap<Long, Set<E>>进行管理,其中的Key值为到期时间。
数据分段使用公式为:(当前时间(毫秒)/ expirationInterval + 1)* expirationInterval。该公式表示将当前时间按照expirationInterval间隔算份数,算完后再加一个份额,最后再乘以expirationInterval间隔,就得出了下一个到期时间。
三、分桶策略源码解析
我们来看看源码的实现。
3.1 SessionTrackerImpl#touchSession
分桶策略的实现,主要是 SessionTrackerImpl#touchSession 方法,通俗说这个方法是给session续期的。我们先来阅读此方法的源码:
此代码的逻辑:
(1)调用touchSession(sessionId , timeout)方法,先从 sessionsById 这个map中通过 sessionId 取出session1(下面的update会此session1传入);
(2)然后调用updateSessionExpiry(s,timeout)进行续期。
我们跟进updateSessionExpiry(s,timeout):
从这里我可以看出来,session续期最终是交给了sessionExpiryQueue来进行管理,我们通过定义可以看到是ExpiryQueue<SessionImpl>,也是就是实现类是ExpiryQueue。
3.2 ExpiryQueue
我们看下ExpiryQueue的update是怎么续期的。
在此之前,我们先来看下ExpiryQuqueue类的情况:
(1)nextExpirationTime(下一个过期的时间点) ;
(2)expirationInterval(过期时间间隔) ;
(3)elemMap(Session对象集合,key是session对象,类型是SessionImpl,value为过期时间);
(4)expiryMap(过期的Session对象集合,key为过期时间,value为session)
3.3 ExpiryQueue#update
对于ExpiryQueue有了基本的认知之后,我们就可以来看看update这个方法是怎么续期的了:
此代码逻辑说明:
(1)eleMap.get(elem):这个的E代表的是SessionImpl ,这个通过上面的说到的定义中ExpiryQueue<SessionImpl>可以看出来;所以这个就是通过会话对象获取到上一期的过期时间点。
(2)通过roundToNextInterval(time)计算下一个过期时间点:
(3)把session1 从expirationTime1 移动到 expirationTime2:(代码上就是先从expirationTime1的sessionSet中移除session1;然后把session1 添加到expirationTime2的sessionSet中。)
3.3 session过期任务线程
过期任务主要是由SessionTrackerImpl的run来进行处理的:
在 SessionTrackerImpl 中的session超时任务中,只是把 session 的isClosing 变量设置为true。
只把session标识位isClosing是不够的,如果此session对应的client创建了临时节点,还需要把临时节点删掉呢。所以要调用 expirerexpire(s); 处理session过期逻辑的:
org.apache.zookeeper.server.ZooKeeperServer#createSessionTracker
可以找到ZooKeeperServer的expire(Session):
此方法处理session过期逻辑:调用的是 ZooKeeperServer#close 方法,close方法中又调用了 ZooKeeperServer#submitRequest提交请求 方法,其中 操作类型为OpCode.closeSession。
也就是 ZooKeeperServer 处理session 过期,是提交了一个操作类型为 为OpCode.closeSession的request,
3.4 session什么时候被移出掉呢?
session标识位isClosing = true后,是在哪里把session从sessionSet中remove掉的呢?
以为 调用 expirer.expire(s); 就是为了把session从 sessionSet中remove掉。
所以的请求处理器,都只是把session 的isClosing赋值为true。
其实,在 touchSession()方法中有一个前提:就是session不能是isClosing 状态的,否则就直接返回:
四、小结
这个可能有点复杂,大家可以根据源码自己进行分析的。
要知道的核心就是:
(1)分桶策略的实现就是两个HashMap的容器来进行实现的。
(2)核心的两个方法就是SessionTrackerImpl的touchSession()和run()方法。