会话也就是session。
zookeeper的接口sessionImpl定义了三个属性
sessionId session的唯一标识符
timeout session的过期时间
isClosing session的关闭状态
服务端使用SessionTracker来管理session
他将session按照过期的时间点放入不同的桶,这样每次只要访问一个桶就可以清除过期session了。所有可能过期的session都在一个桶里。因此zookeeper的session并不是过期则丢弃的,是定时进行清理。
分桶的算法
ExpirationTime=CurrentTime+SessionTimeout
BucketTime=(ExpirationTime/ExpirationInterval+1)*ExpirationInterval
CurrentTime
:当前时间(这是时间轴上的时间)SessionTimeout
:会话超时时间(这是一个时间范围)ExpirationTime
:当前会话下一次超时的时间点(这是时间轴上的时间)ExpirationInterval
:桶的大小(这是一个时间范围)BucketTime
:代表的是当前会话下次超时的时间点所在的桶来自https://juejin.cn/post/7100150142904303624
服务端启动后会创建一个单例的zooKeeperServer实例,并启动SessionTracker
private void startupWithServerState(State state) {
if (sessionTracker == null) {
createSessionTracker();
}
startSessionTracker();
}
SessionTracker是一个线程,实际调用了,run方法
long waitTime = sessionExpiryQueue.getWaitTime();
if (waitTime > 0) {
Thread.sleep(waitTime);
continue;
}
会话的过期
SessionTracker先计算离下一次结算会话还有多久时间,如果还没到时间就sleep需要等待的秒数
for (SessionImpl s : sessionExpiryQueue.poll()) {
//统计过期的个数
ServerMetrics.getMetrics().STALE_SESSIONS_EXPIRED.add(1);
//设置会话关闭状态
setSessionClosing(s.sessionId);
//关闭会话
expirer.expire(s);
}
然后取出sessionExpiryQueue中的元素,统计个数,设置状态并关闭
private final ConcurrentHashMap<E, Long> elemMap = new ConcurrentHashMap<>();
/**
* The maximum number of buckets is equal to max timeout/expirationInterval,
* so the expirationInterval should not be too small compared to the
* max timeout that this expiry queue needs to maintain.
*/
private final ConcurrentHashMap<Long, Set<E>> expiryMap = new ConcurrentHashMap<>();
private final AtomicLong nextExpirationTime = new AtomicLong();
private final int expirationInterval;
ExpiryQueue是zookeeper中的数据结构,维护了一个元素map用来存放会话,key是sessionId,value是还剩多久过期。expiryMap是会话桶集合,key是桶号,value是桶内元素,nextExpirationTime用来和expirationInterval一起计算下次的过期时刻。
会话的续期
会话的管理实际上就是对ExpiryQueue的操作。
client即使没有操作也会向server发送心跳防止会话过期,server一旦接受到了session的信息就会调用ExpiryQueue的update方法来更新
public Long update(E elem, int timeout) {
//取出之前在哪一个桶
Long prevExpiryTime = elemMap.get(elem);
long now = Time.currentElapsedTime();
//计算现在是在哪一个桶
Long newExpiryTime = roundToNextInterval(now + timeout);
//如果是同一个桶 do nothing
if (newExpiryTime.equals(prevExpiryTime)) {
// No change, so nothing to update
return null;
}
Set<E> set = expiryMap.get(newExpiryTime);
//如果改桶还没有初始化,就新建一个set,放入expiryMap,注意线程安全
if (set == null) {
// Construct a ConcurrentHashSet using a ConcurrentHashMap
set = Collections.newSetFromMap(new ConcurrentHashMap<>());
// Put the new set in the map, but only if another thread
// hasn't beaten us to it
//防止多次放入污染数据
Set<E> existingSet = expiryMap.putIfAbsent(newExpiryTime, set);
if (existingSet != null) {
set = existingSet;
}
}
// 放入桶
set.add(elem);
// Map the elem to the new expiry time. If a different previous
// mapping was present, clean up the previous expiry bucket.
prevExpiryTime = elemMap.put(elem, newExpiryTime);
// 从旧桶中移除
if (prevExpiryTime != null && !newExpiryTime.equals(prevExpiryTime)) {
Set<E> prevSet = expiryMap.get(prevExpiryTime);
if (prevSet != null) {
prevSet.remove(elem);
}
}
return newExpiryTime;
}
每当有新的会话加入就会调用trackSession来加入ExpiryQueue中
会话的新建
public synchronized boolean trackSession(long id, int sessionTimeout) {
boolean added = false;
// 是否已经track了
SessionImpl session = sessionsById.get(id);
if (session == null) {
session = new SessionImpl(id, sessionTimeout);
}
// 防止重复放入
SessionImpl existedSession = sessionsById.putIfAbsent(id, session);
if (existedSession != null) {
session = existedSession;
} else {
added = true;
LOG.debug("Adding session 0x{}", Long.toHexString(id));
}
if (LOG.isTraceEnabled()) {
String actionStr = added ? "Adding" : "Existing";
ZooTrace.logTraceMessage(
LOG,
ZooTrace.SESSION_TRACE_MASK,
"SessionTrackerImpl --- " + actionStr
+ " session 0x" + Long.toHexString(id) + " " + sessionTimeout);
}
updateSessionExpiry(session, sessionTimeout);
return added;
}
这样会话管理的新建,过期,续期都看到了
分桶算法的源码
private long roundToNextInterval(long time) {
return (time / expirationInterval + 1) * expirationInterval;
}
这个代码决定了该会话被分在哪一个桶里
SessionId的构建
额外聊聊sessionId的构建方法
public static long initializeNextSessionId(long id) {
long nextSid;
nextSid = (Time.currentElapsedTime() << 24) >>> 8;
nextSid = nextSid | (id << 56);
if (nextSid == EphemeralType.CONTAINER_EPHEMERAL_OWNER) {
++nextSid; // this is an unlikely edge case, but check it just in case
}
return nextSid;
}
sessionId取决于myid和建立连接的时间
最高一哥字节是myid,接下来的5个字节来自时间戳,最低两个字节为0,使用无符号右移保证了最高位不受符号位的影响