ZooKeeper(五)技术内幕-会话

ZooKeeper(五)技术内幕

四. 会话

临时节点的生命周期、客户端请求顺序执行以及Watcher通知机制等都和会话息息相关。

4.1 会话状态

客户端与服务端完成连接的创建后,就建立了一个会话。会话的生命周期中会有多种状态:

  • CONNECTING:客户端开始创建ZK对象时进入此状态,同时从服务器地址列表逐个选取IP地址来尝试进行网络连接。
  • CONNECTED:当成功连接上服务器时更新为此状态。
  • RECONNECTING/CONNECTING:网络问题等会使连接断开,ZK客户端会自动进行重连操作。
  • RECONNECTED/CONNECTED:运行期间,客户端状态总是会介于CONNECTING与CONNECTED之间。
  • CLOSE:会话超时、权限检查失败或客户端主动退出。

4.2 会话创建

Session基本属性:

  • sessionID:会话ID,全局唯一标识一个会话。
  • TimeOut:会话超时时间,客户端向服务端发送此超时时间,服务器根据自己超时时间限制最终确定会话的超时时间。
  • TickTime:下次会话超时时间点,便于ZK对会话进行“分桶策略”管理,也为了高效低耗的实现会话的超时检查和清理。13位的long型数据,接近于当时间加上TimeOut。
  • isClosing:标记会话是否已经关闭,确保服务端不再处理来自该会话的请求。

生成SessionID,高8位确定了所在机器,后56位为当前时间来进行随机:

1
2
3
4
5
6
7
8
9
10
public static long initializeNextSession(long id) {
long nextSid = 0;
// 获取当前毫秒格式时间,左移24位,低24位用0补齐,再右移8位,高8位用0补齐
// 左移24位将高位的1移出避免了负数出现(特殊的时间节点如2022年0408日仍会得到负数),所以3.4.6版本修复为 >>> 8
nextSid = (System.currentTimeMillis() << 24) >> 8;
// 再添加机器标识,当前ZK服务器的SID值,左移56位
// 将两者|操作得到一个单机唯一的序列号
nextSid = nextSid | (id << 56);
return nextSid;
}

SessionTracker是会话的管理器,负责会话创建、管理和清理等工作,每个会话在其中都保留3份:

  • sessionsById:根据sessionID来管理session实体,HashMap<Long, SessionImpl> 数据结构。
  • sessionsWithTimeout:根据sessionID来管理session超时时间,HashMap<Long, Integer> 数据结构。其与ZK内存数据库相连,会被定期持久化到快照文件。
  • sessionSets:根据下次会话超时时间来管理归档session,HashMap<Long, SessionSet> 数据结构。便于进行会话管理和超时检查。

创建会话连接:

  • 处理ConnectRequest请求:由NIOServerCnxn负责接收客户端的会话创建请求,反序列化出ConnectRequest请求,根据ZK服务端的配置完成会话超时时间的协商。
  • 会话创建:SessionTracker为会话分配一个SessionID,将其注册到sessionsById和sessionsWithTimeout,同时进行会话的激活。会话请求还会在ZK服务端各请求处理器直接进行顺序流转。
  • 处理器链路处理
  • 会话响应

4.3 会话管理

(1)分桶策略

分桶策略指将类似的会话放在同一区块中进行管理,以便于ZK对会话进行不同区块的隔离处理以及同一区块的统一处理:

分配的原则是每个会话的下次超时时间点(ExpirationTime)最近一次可能超时的时间点,计算方式:ExpirationTime = CurrentTime + SessionTimeout ,当前时间+超时时间。

ZK的Leader服务器在运行期间会定时进行会话超时检查,间隔是ExpirationInterval,默认值为TickTime(2000毫秒),完整的公式:

1
2
3
4
5
ExpirationTime_ = CurrentTime + SessionTimeout
ExpirationTime = (ExpirationTime_ / ExpirationInterval + 1) * ExpirationInterval
// 假设当前时间为1370907000000毫秒,客户端设置超时时间为15000毫秒,服务器TickTime为2000毫秒
ExpirationTime_ = 1370907000000 + 15000 = 1370907015000
ExpirationTime = (1370907015000 / 2000 + 1) = 1370907016000

(2)会话激活

客户端在会话运行期间向服务端发送PING请求来保持会话有效,服务端要不断接收这类心跳检测,并重新激活对应客户端会话,即TouchSession,一次心跳检测对应一次会话激活:

  1. 检验会话是否已关闭。
  2. 计算改会话新的超时时间。
  3. 定位改会话当前的区块。根据会话老的超时时间定位其所在区块。
  4. 迁移会话。放入新的超时时间对应区块。

发生会话激活的两种情况:

  • 客户端发送请求,包括读写,触发一次会话激活。
  • 客户端发现在sessionTimeout/3时间内未和服务器进行通信,主动发起一次PING请求,服务端收到后触发一次会话激活。

(3)会话超时检查

SessionTracker中有一个单独的线程专门进行会话超时检查,其工作机制的核心思路非常简单,逐个依次对会话桶中剩下的会话进行清理。

会话分桶策略中将ExpirationInterval的倍数作为时间点来分布会话,超时检查线程只要在这些指定的时间点进行检查即可,这样可以批量清理有很好的性能。

4.4 会话清理

SessionTracker的超时检查出已经过期的会话后,开始进行会话清理:

  1. 标记会话状态为“已关闭”

    清理过程需要一段时间,首先要将会话的isClosing设置为true,在此期间不再处理该客户端的请求。

  2. 发起“会话关闭”请求

    提交此请求使会话关闭在整个服务器集群中生效,立即交付给PrepRequestProcessor处理器。

  3. 收集需要清理的临时节点

    会话失效后,相关的临时节点都要一并清除掉,所以要先整理出和会话相关的临时节点。

    ZK内存数据库中每个会话都单独保存了一份由该会话维护的临时节点集合,只需根据sessionID取到这份列表即可。

    实际应用中,ZK处理会话关闭请求前可能有两类请求到达了服务端并正在处理:

    • 节点删除请求,删除的目标节点正好是集合中的一个。
    • 临时节点创建请求,创建的的目标节点正好是集合中的一个。

    共同点是事务尚未完成,还未应用到内存数据库,所以获取临时节点列表时可能会有不一致的情况。前者需要从获取的列表中也移除,后者要将所有请求对应的路径节点加到集合中,以删除这些将要创建的节点。

  4. 添加“节点删除”事务变更

    完成临时节点收集后,ZK逐个将临时节点转换为节点删除请求,并放入事务变更队列outstandingChanges中去。

  5. 删除临时节点

    FinalRequestProcessor处理器会触发内存数据库,删除该会话对应的所有临时节点。

  6. 移除会话

    完成节点删除后,要将会话从SessionTracker中移除。主要是从三个数据结构sessionsById、sessionsWithTimeout和sessionSets中移除。

  7. 关闭NIOServerCnxn

    从NIOServerCnxnFactory中找到并关闭NIOServerCnxn。

4.5 会话重连

客户端与服务端网络连接断开时,ZK客户端会自动进行反复的重连,直到最终成功连接到一台服务器,再次连上的客户端可能处于两个状态之一:

  • CONNECTED:会话超时时间内重新连接上,就是重连成功。
  • EXPIRED:超时时间以外则服务端已进行了会话清理操作,此时连上的会话是非法会话。

当客户端与服务端连接断开后,客户端可能会看到两类异常:

  • CONNECTION_LOSS(连接断开):立即收到事件None-Disconnected通知,同时抛出异常 ConnectionLossException 。ZK客户端自动从地址列表重新逐个选取新的地址并尝试重新连接。重连后收到事件None-SyncConnected通知,此时可以重试出错的操作。
  • SESSION_EXPIRED(会话过期):重连时间过长,超过了会话超时时间,服务器认定这个会话已经结束并执行会话清理,但客户端并不知道会话已经失效,其客户端状态还是DISCONNECTED。之后客户端可能会重新连上服务器,并被告知会话已失效,用户要重新实例化一个ZK对象,并决定是否和如何恢复临时数据。

SESSION_MOVED(会话转移):客户端与服务器1断开连接后,与服务器2连接上并延续了有效会话。3.2.0版本之前没有此概念,会有异常情况,向前一个服务器发送的请求被处理后覆盖了向后一个服务器的相同请求。3.2.0版本后提出了会话转移的概念,并封装了SessionMovedException异常。之后处理客户端请求使,首先检查会话的所有者是否是当前服务器,不是就抛出此异常,但因为客户端已和服务器断开了连接,所以无法收到这个异常的响应。只有多个客户端使用相同的sessionId/sessionPasswd创建会话时才会收到,一旦一个客户端会话创建成功,服务器就认为该sessionId对应的会话已发生转移,于是等到第二个客户端连上此服务器后就被认为是会话转移的情况。


参考:

🔗 《从Paxos到Zookeeper-分布式一致性原理与实践》