常见的几种会话状态如下:
连接建立的初始化阶段,客户端的状态会变成CONNECTING,同时客户端会从服务器地址列表中随机获取一个ip地址尝试进行网络连接,知道成功建立连接,这时候,客户端的状态就会变成CONNECTED。但是,在通常情况下,由于网络的不可靠性,时常会伴随这网络中断的出现,这时候,客户端和服务端之间会出现断开连接的情况,一旦出现这种情况,客户端会尝试去重新连接服务端,这时,客户端的状态会再一次变成CONNECTING,直到重新连接上服务器后,客户端的状态又会再次变成CONNECTED。
因此,在通常情况下,客户端的会话状态始终在CONNECTED和CONNECTING之间变化。
出现CLOSE的情况:
会话session是ZooKeeper中的会话实体,代表了一个客户端的会话。其定义在org.apache.zookeeper.server.SessionTracker.Session中。接口的定义如下:
public interface SessionTracker {
public static interface Session {
long getSessionId();
int getTimeout();
boolean isClosing();
}
}
其实现类为org.apache.zookeeper.server.SessionTrackerImpl.SessionImpl,代码如下:
public static class SessionImpl implements Session {
final long sessionId;
final int timeout;
boolean isClosing;
Object owner;
public long getSessionId() { return sessionId; }
public int getTimeout() { return timeout; }
public boolean isClosing() { return isClosing; }
}
从上述代码中可以看出,Session主要由以下三个属性组成:
在讲session的初始化之前,首先先看看SessionTrackerImpl的实现
public class SessionTrackerImpl extends ZooKeeperCriticalThread implements SessionTracker
{
protected final ConcurrentHashMap sessionsById =
new ConcurrentHashMap();
private final ConcurrentMap sessionsWithTimeout;
private final AtomicLong nextSessionId = new AtomicLong();
//构造函数
public SessionTrackerImpl(SessionExpirer expirer,
ConcurrentMap sessionsWithTimeout, int tickTime,
long serverId, ZooKeeperServerListener listener)
{
super("SessionTracker", listener);
this.expirer = expirer;
this.sessionExpiryQueue = new ExpiryQueue(tickTime);
this.sessionsWithTimeout = sessionsWithTimeout;
//初始化sessionId
this.nextSessionId.set(initializeNextSession(serverId));
for (Entry e : sessionsWithTimeout.entrySet()) {
addSession(e.getKey(), e.getValue());
}
EphemeralType.validateServerId(serverId);
}
}
从上面的构造函数中可以看出,对session进行初始化,是由方法initializeNextSession来完成的。那么,下面我们就来看看该方法的具体实现细节。
public static long initializeNextSession(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;
}
从上面的初始化方法中可以看出,该方法的入参是一个long的整数id,该id表示的是服务端的机器编号,这个参数是在部署ZooKeeper服务器的时候,配置在myid文件中的。
初始化的步骤如下:
时间戳经过这样的运算之后,高8位全部位0,和机器编号的高8位进行或运算之后,其结果完全取决于机器编号的高8位;同理,低56位由时间戳决定。因此,可以看出,sessionId其实是由机器编号+时间戳唯一决定的。可以保证在单机环境下的唯一性。
在ZooKeeper的设计过程中,只要客户端有请求发送到服务端,那么服务端就会触发一次会话激活。以下情况会发生的会话激活:
与会话激活相关的方法和类由:
首先我们来看看org.apache.zookeeper.server.ExpiryQueue
//记录的是: Session -> 超时时间
private final ConcurrentHashMap elemMap =
new ConcurrentHashMap();
//记录的是: 下一个超时时间 -> session的集合
private final ConcurrentHashMap> expiryMap =
new ConcurrentHashMap>();
private final AtomicLong nextExpirationTime = new AtomicLong();
private final int expirationInterval;
// 服务端计算超时时间的方法,expirationInterval默认等于tickTime,2000ms
private long roundToNextInterval(long time) {
return (time / expirationInterval + 1) * expirationInterval;
}
从上面的的ExpiryQueue代码中可以看出:
在SessionTrackerImpl中对touchSession方法描述:
synchronized public boolean touchSession(long sessionId, int timeout) {
SessionImpl s = sessionsById.get(sessionId);
if (s == null) {
logTraceTouchInvalidSession(sessionId, timeout);
return false;
}
if (s.isClosing()) {
logTraceTouchClosingSession(sessionId, timeout);
return false;
}
//会话激活的主要执行逻辑
updateSessionExpiry(s, timeout);
return true;
}
执行流程如下:
会话超时检查是由SessionTracker负责的。代码如下:
public void run() {
try {
while (running) {
//sessionExpiryQueue是一个ExpiryQueue对象
long waitTime = sessionExpiryQueue.getWaitTime();
if (waitTime > 0) {
Thread.sleep(waitTime);
continue;
}
// sessionExpiryQueue.poll()是获取expiryMap中超时的会话
for (SessionImpl s : sessionExpiryQueue.poll()) {
setSessionClosing(s.sessionId);
expirer.expire(s);
}
}
} catch (InterruptedException e) {
handleException(this.getName(), e);
}
LOG.info("SessionTrackerImpl exited loop!");
}
整个代码的核心逻辑在:
for (SessionImpl s : sessionExpiryQueue.poll()) {
setSessionClosing(s.sessionId); //将会话的isClosing标志位置为true
expirer.expire(s); //会话清理过程, 在后面的文章中会详细介绍该过程的细节
}
超时检查的策略:逐个检查会话bucket中剩下的,超过超时时间的会话。代码如下
public Set poll() {
long now = Time.currentElapsedTime();
long expirationTime = nextExpirationTime.get();
if (now < expirationTime) {
return Collections.emptySet();
}
Set set = null;
long newExpirationTime = expirationTime + expirationInterval;
if (nextExpirationTime.compareAndSet(
expirationTime, newExpirationTime)) {
set = expiryMap.remove(expirationTime);
}
if (set == null) {
return Collections.emptySet();
}
return set;
}