Zookeeper-连接和会话的过期清理策略(ExpiryQueue)

背景

Zookeeper作为一个服务器,需要管理与客户端的连接和会话,如接受连接,网络IO,和过期清理等,此文章就介绍下Zookeeper的过期策略.

注:目前网络上的资料介绍的都是会话的过期策略,在最新版Zookeeper中,将过期策略的代码抽象为ExpiryQueue类,除了会话,连接也用此类进行过期处理,但为了行文方便,下文中一律使用会话进行介绍
注:在下文中使用过期时间表示会话或连接何时过期,对应的英文为expiration time,用unix时间戳表示.超时时间表示会话或连接的超时时间,对应的英文为timeout,如5000ms

简单的会话清理策略

在介绍分桶策略之前,我先介绍一种比较简略的会话清理策略.

数据结构

使用3个TreeMap作为数据结构,如下:

  1. timeoutMap:其key是session,value是该会话的timeout(用于保存会话的timeout)
  2. elementMap:其key是session,value是该回话的过期时间(expiration time)(过期时间都是在当前时间之后的)
  3. expiryMap:其key是过期时间,value是原定在该过期时间过期的session

更新会话过期时间

客户端与服务端通信过程中更新上述3个数据结构,流程如下:

  1. 会话初始化时将其加入到timeoutMap,保存该会话的timeout
  2. 每当服务端与该会话进行通信时,更新会话的过期时间,将其过期时间修改为currentTime+timeout,包括2个步骤
    1. 通过elementMap获取该session的prevExpiryTime,根据prevExpiryTime在expiryMap删除其对应的session
    2. 修改elementMap,将session对应的过期时间修改为currentTime+timeout

清理过期会话

后台启动一个清理线程清理过期的session,线程中无限循环,执行工作如下:

  1. 执行定时的wait()操作,等待到当前时间为expiryMap中最小的key(使用TreeMap的原因),清理该key对应的session.

可能有同学对清理线程中执行的操作不是非常理解,由于expireMap的key是过期时间,当前时间到了过期时间,对应的session就要被清除.若在此之前,session与服务端进行了通信,就会更新expiryMap,删除该session原本的过期时间,该session就不会被清理线程清理.

优缺点

缺点:

  1. 若有多个session在同一时间过期,则exipryMap中只保存了最后一个调用put(time,session)方法的session的信息
    解决方案:将exipryMap的value修改为Set<Session>
  2. 若存在大量会话,则清理线程会在短时间内执行大量的wait()操作,虽然不知道会有什么不足,但总感觉有问题,比如系统能在短时间内调度大量的wait()操作吗?

优点:

  1. 实时性高

分桶策略

为了解决上述策略的缺点2,就引入了分桶策略,其将过期时间在某个时间段内的会话将看做一组,在每组最后的时间点,即下图中expiration time n,批量清理此组的过期会话.此时间段的长度可适当调整,若过大,则效率较高,实时性不足;若过小,则效率较低,实时性高.
这里写图片描述

具体实现

改进

ExpiryQueue的实现思路与上文基本相同,其改进如下:

  1. expiryMap的value修改为Set
  2. expiryMap使用ConCurrentHashMap,使用nextExpirationTime存放下一次进行会话超时检查的时间
  3. 没有使用timeoutMap存放会话的超时时间,每次更新会话过期时间时将timeout当做参数调用update(E elem, int timeout)
  4. 使用泛型,被清理的元素可以是SessionImpl,也可以是NIOServerCnxn
  5. 增加了expirationInterval属性,用于存放分桶策略中的两次超时时间的间隔(也即分组时间段长度)

update

    /**
     * 为队列中的元素添加或更新过期时间,将超时四舍五入到此队列使用的到期时间间隔。
     * <p>
     * Adds or updates expiration time for element in queue, rounding the
     * timeout to the expiry interval bucketed used by this queue.
     *
     * @param elem    element to add/update
     * @param timeout timout in milliseconds
     * @return time at which the element is now set to expire if
     * changed, or null if unchanged
     */
    public Long update(E elem, int timeout) {
        Long prevExpiryTime = elemMap.get(elem);
        long now = Time.currentElapsedTime();
        //2.计算该会话新的的过期时间
        Long newExpiryTime = roundToNextInterval(now + timeout);

        if (newExpiryTime.equals(prevExpiryTime)) {
            // No change, so nothing to update
            return null;
        }

        // First add the elem to the new expiry time bucket in expiryMap.
        //3.将会话迁移到新bucket上
        Set<E> set = expiryMap.get(newExpiryTime);
        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.
        //4.从旧bucket上删除该回话
        //获取该会话旧的过期时间,根据旧的过期时间在expiryMap获取在该过期时间过期的session集合,从该集合中删除该会话
        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;
    }

poll

    /**
     * 将即将超时的元素集合从{@link #expiryMap}中删除,此方法需要通过检查{@link #getWaitTime()}来频繁地调用,
     * 否则将会有大量已超时的元素堆积在{@link #expiryMap}中
     * <p>
     * Remove the next expired set of elements from expireMap. This method needs
     * to be called frequently enough by checking getWaitTime(), otherwise there
     * will be a backlog of empty sets queued up in expiryMap.
     *
     * @return next set of expired elements, or an empty set if none are
     * ready
     */
    public Set<E> poll() {
        long now = Time.currentElapsedTime();
        long expirationTime = nextExpirationTime.get();
        if (now < expirationTime) {
            return Collections.emptySet();
        }

        Set<E> set = null;
        long newExpirationTime = expirationTime + expirationInterval;
        if (nextExpirationTime.compareAndSet(
                expirationTime, newExpirationTime)) {
            set = expiryMap.remove(expirationTime);
        }
        if (set == null) {
            return Collections.emptySet();
        }
        return set;
    }

remove

    /**
     * Removes element from the queue.
     *
     * @param elem element to remove
     * @return time at which the element was set to expire, or null if
     * it wasn't present
     */
    public Long remove(E elem) {
        Long expiryTime = elemMap.remove(elem);
        if (expiryTime != null) {
            Set<E> set = expiryMap.get(expiryTime);
            if (set != null) {
                set.remove(elem);
                // We don't need to worry about removing empty sets,
                // they'll eventually be removed when they expire.
            }
        }
        return expiryTime;
    }

连接和会话

虽然连接和会话都可通过ExpiryQueue进行清理,但其相关配置不同

方式expirationInterval配置默认值
连接使用ticktime作为expirationInterval值3000ms
会话通过系统属性zookeeper.nio.sessionlessCnxnTimeout配置10000ms

总结

  1. 本文介绍了Zookeeper中NIOServerCnxnSessionImpl的过期策略,使用分桶策略将会话分组,在保证实时性的同时提高了效率
  2. 为何在存在会话过期清理的情况下,还需对连接进行过期清理?
    个人认为会话连接还是两个层次的概念,会话过期清理是一定需要实现的,而连接的过期清理应该属于对网络IO的一种优化,在NIOServerCnxnFactory使用ExpiryQueue进行会话过期清理,但NettyServerCnxnFactory便没有显式的使用ExpiryQueue

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值