ZooKeeper中的会话机制

在本文中将对zk的会话机制进行总结

相关的类

  • SessionTracker
  • SessionTrackerImpl

会话状态

常见的几种会话状态如下:

  • CONNECTING,正在连接
  • CONNECTED, 已连接
  • RECONNECTING,正在重连
  • RECONNECTED,已重连
  • CLOSE,会话关闭

连接建立的初始化阶段,客户端的状态会变成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主要由以下三个属性组成:

  • sessionId,这是一个64位的long型整数,代表一个唯一的会话。每次客户端创建会话的时候,ZooKeeper都会为其分配一个全局唯一的一个sessionId。
  • timeout,会话的超时时间。客户端在构造ZooKeeper实例的时候,会为本次会话配置一个会话的超时时间。客户端在向ZooKeeper服务器发送这个超时时间后,服务端会根据自己的配置最终确定本次会话的超时时间。
  • isClosing,这是一个标志位,表示本次会话是否已经关闭。在服务端的“会话超时检查”线程在检查到该会话已经失效的时候,会第一时间将这个标志位置为true,只要这个标志位为true,那么服务端就不会在处理该会话的请求了。

会话初始化

在讲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文件中的。

初始化的步骤如下:

  • 生成系统当前时间的时间戳,64位的long型整数
  • 将时间戳左移24位,在无符号右移8位
  • 经过上一步,该时间戳的高8位全部为0,低56位不为0
  • 接着,将机器编号左移56位,那么机器编号的高8位不为0,低56位全为0
  • 最后,将上面得到的机器编号和时间戳进行或运算
为什么要这样做运算?

时间戳经过这样的运算之后,高8位全部位0,和机器编号的高8位进行或运算之后,其结果完全取决于机器编号的高8位;同理,低56位由时间戳决定。因此,可以看出,sessionId其实是由机器编号+时间戳唯一决定的。可以保证在单机环境下的唯一性。

之所以右移8位的时候采用无符号,是因为防止前面左移24位的时候,可能出现负数的情况,因此为了消除产生的负数的影响,采用无符号的右移。

会话激活

在ZooKeeper的设计过程中,只要客户端有请求发送到服务端,那么服务端就会触发一次会话激活。以下情况会发生的会话激活:

  • 客户端向服务端发送读写请求
  • 如果客户端在sessionTimeout / 3的时间内都没有与服务端有过交互,那么客户端会主动的向服务端发送ping请求(心跳检测),服务端收到请求之后,会触发一次会话激活。

与会话激活相关的方法和类由:

  • org.apache.zookeeper.server.ExpiryQueue
  • SessionTrackerImpl中的touchSession方法

首先我们来看看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代码中可以看出:

  • 分桶策略中的buckets其实就是一个set集合,每个超时时间对应一个会话的集合
  • 维护另外一个map,管理单个session对应的超时时间
  • 相应的计算超时时间的方法

在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;
    }

执行流程如下:

  • 根据sessionId获取到对应的会话实体
  • 判断该会话是否已经关闭,如果是的话,那么就不需要激活,直接返回
  • 从ExpiryQueue中的elemMap获取本次会话以前的超时时间prevExpiryTime
  • 计算新的超时时间,计算逻辑为roundToNextInterval方法
  • 根据新的超时时间,将session实体放入到新的超时时间对应的expiryMap,并且设置新的elemMap
  • 将session实体从以前的expiryMap中删除,并且更新对应的elemMap

会话超时检查

会话超时检查是由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;
    }

你可能感兴趣的:(ZooKeeper)