ZooKeeper 4:会话处理与其源码分析

prev:ZooKeeper 3 Watch机制与其源码分析


ZooKeeper是一个 服务器-客户端结构的应用,客户端与服务器之间需要建立一个连接,而这个连接就是一个会话。客户端与服务端的交互操作中都离不开会话的相关的操作。

会话的数据结构

维护一个会话需要一些信息,主要由4部分构成:

  • sessionId 会话 ID
    会话的唯一标识。由服务器生成,以64位数字表示,并分配给客户端
  • timeout
    会话的超时时间。会话的超时时间就是指一次会话从发起后到被服务器关闭的时长。
  • isClosing
    用于标记一个会话是否已经被关闭。如果服务器检查到一个会话已经因为超时等原因失效时, ZooKeeper 会在该会话的 isClosing 属性值标记为关闭,再之后就不对该会话进行操作了。
  • owner

(ZooKeeper 3.8 org.apache.zookeeper.server SessionTrackerImpl.java 56行)

会话的状态

在这里插入图片描述
在 ZooKeeper 服务的运行过程中,会话会经历不同的状态变化。而这些状态包括:正在连接(CONNECTING)、已经连接(CONNECTIED)、正在重新连接(RECONNECTING)、已经重新连接(RECONNECTED)、会话关闭(CLOSE)等。

服务端会话处理

整体流程

在这里插入图片描述
图源:糖爸的架构师之路

会话创建

服务端会话的管理主要是SessionTrackerImpl进行的(org.apache.zookeeper.server SessionTrackerImpl.java)
会话会被维护在两个map中:

/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper.server
* SessionTrackerImpl.java
* 51行
*/
public class SessionTrackerImpl extends ZooKeeperCriticalThread implements SessionTracker {
	// 保存id到session的映射
	protected final ConcurrentHashMap<Long, SessionImpl> sessionsById = new ConcurrentHashMap<Long, SessionImpl>();
	// 存放过期队列,使用bucket来维护会话,每一个bucket对应一个某时间范围内过期的会话
	private final ExpiryQueue<SessionImpl> sessionExpiryQueue;
	// 保存session到超时时间的映射
    protected final ConcurrentMap<Long, Integer> sessionsWithTimeout;
}

在SessionTrackerImpl的构造函数中,可以看到用initializeNextSession()方法来生成一个会话ID,该会话ID会作为一个唯一的标识符:

	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper.server
	* SessionTrackerImpl.java
	* 110行
	*/
    public SessionTrackerImpl(SessionExpirer expirer, ConcurrentMap<Long, Integer> sessionsWithTimeout, int tickTime, long serverId, ZooKeeperServerListener listener) {
        super("SessionTracker", listener);
        this.expirer = expirer;
        this.sessionExpiryQueue = new ExpiryQueue<SessionImpl>(tickTime);
        this.sessionsWithTimeout = sessionsWithTimeout;
        this.nextSessionId.set(initializeNextSessionId(serverId));
        for (Entry<Long, Integer> e : sessionsWithTimeout.entrySet()) {
            trackSession(e.getKey(), e.getValue());
        }
        EphemeralType.validateServerId(serverId);
    }

SessionTracker会为该会话分配一个sessionID,并将其注册到sessionsById和sessionsWithTimeout中去,同时进行会话的激活。

会话管理——分桶策略

分桶策略指的是将超时时间相近的会话放到同一个桶中来进行管理,以减少管理的复杂度。
本文的第一个代码块中,可以看到维护了一个过期队列:

private final ExpiryQueue<SessionImpl> sessionExpiryQueue;

这个队列的类型是ExpiryQueue,其内部使用以下数据结构来维护过期:

	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper.server
	* ExpiryQueue.java
	* 37行
	*/
	// 维护Session对象与其过期时间的映射关系,key是Session,value是过期时间
    private final ConcurrentHashMap<E, Long> elemMap = new ConcurrentHashMap<E, Long>();
    /**
     * 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.
     */
     // 维护过期时间和在该时间过期的session,key是过期时间,value是session集合
    private final ConcurrentHashMap<Long, Set<E>> expiryMap = new ConcurrentHashMap<Long, Set<E>>();
    // 下一个过期的时间点
    private final AtomicLong nextExpirationTime = new AtomicLong();
    // 过期时间间隔,默认2000,单位毫秒
    private final int expirationInterval;

对于一个新创建的会话而言,其会话创建完毕后,就会为其预期计算过期时间,即:
E x p i r a t i o n T i m e = C u r r e n t T i m e + S e s s i o n T i m e o u t ExpirationTime = CurrentTime + SessionTimeout ExpirationTime=CurrentTime+SessionTimeout
因为是预期过期时间,所以并不一定就在这个时间过期,因为ZooKeeper的Leader服务器在运行期间会每隔Expirationlnterval毫秒的时间,定期地进行会话超时检查。
因此,会话过期的时间实际上是:

	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper.server
	* ExpiryQueue.java
	* 53行
	* 计算会话实际的过期时间
	*/
	private long roundToNextInterval(long time) {
        return (time / expirationInterval + 1) * expirationInterval;
    }

举个例子,例如我们使用默认的过期检查间隔时间2000毫秒,假设目前时间为0,Session A的过期时间为1000,Session B的过期时间为2000ms,Session C的过期时间为3000,Session D的过期时间为3200,那么:

  • Session A和Session B都会落在2000ms这个时间(expiryMap的key=2000对应的Set里面)
  • Session C和Session D都会落在4000ms这个时间(expiryMap的key=4000对应的Set里面)

会话探活(激活)

为了客户端这边防止会话过期,要保持客户端会话的有效性,也就是说,需要一种探活机制来实现客户端服务器之间的会话保持。ZooKeeper的探活机制是在客户端和服务器之间采用心跳机制:

  • 客户端会在会话超时时间过期范围内向服务端发送PING请求来保持会话的有效性
  • 服务端需要不断地接收来自客户端的这个心跳检测,并且需要重新激活对应的客户端会话

这个重新激活的过程称为TouchSession,它的具体的实现在SessionTrackerImpl.touch()中:

	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper.server
	* SessionTrackerImpl.java
	* 179行
	* Session激活
	*/
    public synchronized boolean touchSession(long sessionId, int timeout) {
    	// 从map里面获取id对应的Session
        SessionImpl s = sessionsById.get(sessionId);
		// 查不到Session或Session过期
        if (s == null) {
            logTraceTouchInvalidSession(sessionId, timeout);
            return false;
        }
        if (s.isClosing()) {
            logTraceTouchClosingSession(sessionId, timeout);
            return false;
        }
		// 更新Session过期时间,具体实现如下
        updateSessionExpiry(s, timeout);
        return true;
    }
    /**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper.server
	* SessionTrackerImpl.java
	* 196行
	* Session过期时间更新到sessionExpiryQueue
	*/
    private void updateSessionExpiry(SessionImpl s, int timeout) {
        logTraceTouchSession(s.sessionId, timeout, "");
        sessionExpiryQueue.update(s, timeout);
    }

sessionExpiryQueue的update逻辑如下:

    /**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper.server
	* ExpiryQueue.java
	* 84行
	* 向ExpiryQueue中更新或新增数据
	*/
	public Long update(E elem, int timeout) {
		// 获取上次会话过期时间
        Long prevExpiryTime = elemMap.get(elem);
        long now = Time.currentElapsedTime();
        // 计算新的会话过期时间
        Long newExpiryTime = roundToNextInterval(now + timeout);
		// 如果过期时间不变,直接返回
        if (newExpiryTime.equals(prevExpiryTime)) {
            // No change, so nothing to update
            return null;
        }
		// 从expiryMap中获取相应时间过期的所有会话,如2000ms的所有过期会话
        // First add the elem to the new expiry time bucket in expiryMap.
        Set<E> set = expiryMap.get(newExpiryTime);
        // 没找到就创建一个新桶
        if (set == null) {
            // Construct a ConcurrentHashSet using a ConcurrentHashMap
            set = Collections.newSetFromMap(new ConcurrentHashMap<E, Boolean>());
            // 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;
            }
        }
        // 把Session添加到桶里
        set.add(elem);

        // Map the elem to the new expiry time. If a different previous
        // mapping was present, clean up the previous expiry bucket.
        // 过期时间维护到emeMap中
        prevExpiryTime = elemMap.put(elem, newExpiryTime);
        if (prevExpiryTime != null && !newExpiryTime.equals(prevExpiryTime)) {
            Set<E> prevSet = expiryMap.get(prevExpiryTime);
            // 如果Session还在旧桶,删掉
            if (prevSet != null) {
                prevSet.remove(elem);
            }
        }
        return newExpiryTime;
    }

超时处理

如果会话在过了预期过期时间后,仍然没有被激活,就需要将超时的Session移除。
在SessionTracker中有一个单独的线程,进行会话超时检查。由于采用了分桶的逻辑,超时检查就很简单了,到了相应时间后,没有被移除的会话都是需要执行超时处理逻辑的。

	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper.server
	* SessionTrackerImpl.java
	* 158行
	* 起一个线程处理过期Session
	*/
    @Override
    public void run() {
        try {
            while (running) {
            	// 获取最近的过期时间的时长,如果大于0就sleep()
                long waitTime = sessionExpiryQueue.getWaitTime();
                if (waitTime > 0) {
                    Thread.sleep(waitTime);
                    continue;
                }
				// 取出要过期的会话
                for (SessionImpl s : sessionExpiryQueue.poll()) {
                    ServerMetrics.getMetrics().STALE_SESSIONS_EXPIRED.add(1);
                    // isClosing置为true
                    setSessionClosing(s.sessionId);
                    // 具体过期处理逻辑
                    expirer.expire(s);
                }
            }
        } catch (InterruptedException e) {
            handleException(this.getName(), e);
        }
        LOG.info("SessionTrackerImpl exited loop!");
    }

这里具体的过期处理逻辑是发送一个关闭请求:

	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper.server
	* ZooKeeperServer.java
	* 604行
	* 过期释放的具体执行:发送Session关闭
	*/
	private void close(long sessionId) {
        Request si = new Request(null, sessionId, 0, OpCode.closeSession, null, null);
        submitRequest(si);
    }

参考

聊聊Zookeeper的会话(中篇)
09 创建会话:避开日常开发的那些“坑”
ZooKeeper Programmer’s Guide
ZooKeeper会话的创建过程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值