从源码了解zookeeper的会话机制

会话也就是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,使用无符号右移保证了最高位不受符号位的影响

参考文章

Zookeeper源码篇8-会话管理源码分析(分桶策略) - 掘金 (juejin.cn)

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值