Zookeeper会话

会话(Session)是 ZooKeeper中最重要的概念之一,客户端与服务端之间的任何交互操作都与会话息息相关,这其中就包括临时节点的生命周期、客户端请求的顺序执行以及Watcher通知机制等。

ZooKeeper客户端与服务端之间一次会话创建的大体过程。以Java语言为例,简单地说, ZooKeeper的连接与会话就是客户端通过实例化ZooKeeper对象来实现客户端与服务器创建并保持TCP连接的过程。

1、会话状态

在 ZooKeeper客户端与服务端成功完成连接创建后,就建立了一个会话。 ZooKeeper会话在整个运行期间的生命周期中,会在不同的会话状态之间进行切换,这些状态一般可以分为 CONNECTING、CONNECTED、 RECONNECTING、 RECONNECTED和CLOSE。
等。

如果客户端需要与服务端创建一个会话,那么客户端必须提供个使用字符串表示的服务器地址列表: “host1:port1,host2:port2,host3:port3”。例如,"192.168.0.1:2181”或是“192.168.0.1:2181,192.1680.2:2181,192.1680.3:2181”。一旦客户端开始创建 ZooKeeper对象,那么客户端状态就会变成 CONNECTING,同时客户端开始从上述服务器地址列表中逐个选取IP地址来尝试进行网络连接,直到成功连接上服务器,然后将客户端状态变更为 CONNECTED。

通常情况下,伴随着网络闪断或是其他原因,客户端与服务器之间的连接会出现断开情况。一旦碰到这种情况, ZooKeeper客户端会自动进行重连操作,同时客户端的状态再次变为 CONNECTING,直到重新连接上 ZooKeeper服务器后,客户端状态又会再次转变成 CONNECTED。因此,通常情况下,在 ZooKeeper运行期间,客户端的状态总是介于 CONNECTING和 CONNECTED两者之一。

另外,如果出现诸如会话超时、权限检査失败或是客户端主动退出程序等情况,那么客户端的状态就会直接变更为 CLOSE。下图展示了 ZooKeeper客户端会话状态的变更情况。

2、会话创建

2.1、Session

Session是 ZooKeeper中的会话实体,代表了一个客户端会话。其包含以下4个基本属性。

  • sessionID:会话ID,用来唯一标识一个会话,每次客户端创建新会话的时候,ZooKeeper都会为其分配一个全局唯一的 sessionID。
  • TimeOut:会话超时时间。客户端在构造 ZooKeeper实例的时候,会配置一个sessiontimeout参数用于指定会话的超时时间。ZooKeeper客户端向服务器发送这个超时时间后,服务器会根据自己的超时时间限制最终确定会话的超时时间。
  • TickTime:下次会话超时时间点。为了便于 ZooKeeper对会话实行“分桶策略”管理,同时也是为了高效低耗地实现会话的超时检查与清理, ZooKeeper会为每个会话标记一个下次会话超时时间点。 TickTime是一个13位的long型数据,其值接近于当前时间加上 Time Out,但不完全相等。关于 TickTime的计算方式,将在“分桶策略”部分做详细讲解。
  • isClosing:该属性用于标记一个会话是否已经被关闭。通常当服务端检测到一个会话已经超时失效的时候,会将该会话的 isClosing属性标记为“已关闭”,这样就能确保不再处理来自该会话的新请求了。

2.2、sessionsID

Zookeeper为了保证请求会话的全局唯一性,在SessionTracker初始化时,调用initializeNextSession方法生成一个sessionID,之后在Zookeeper运行过程中,会在该sessionID的基础上为每个会话进行分配,初始化算法如下。

public static long initializeNextSession(long id) {
    long nextSid = 0;
    // 无符号右移8位使为了避免左移24后,再右移8位出现负数而无法通过高8位确定sid值
    nextSid = (System.currentTimeMillis() << 24) >>> 8;
    nextSid = nextSid | (id << 56);
    return nextSid;
}

在 initializeNextsession方法中,出现了一个id变量,该变量就是当前ZooKeeper服务器的SID值。SID就是当时配置在myid文件中的值,该值通常是一个整数。

2.3、SessionTracker

SessionTracker是 ZooKeeper服务端的会话管理器,负责会话的创建、管理和清理等工作。可以说,整个会话的生命周期都离不开SessionTracker的管理。每一个会话在SessionTracker内部都保留了三份,具体如下。

  • sessionsById:这是一个 HashMap<Long, SessionImpl>类型的数据结构用于根据 sessionID来管理 Session实体。
  • sessionsWithTimeout:这是一个 ConcurrentHashMap<Long, Integer>类型的数据结构,用于根据 sessionId来管理会话的超时时间。该数据结构和ZooKeeper内存数据库相连通,会被定期持久化到快照文件中去。
  • sessionSets:这是一个 HashMap<Long, Sessionset>类型的数据结构,用于根据下次会话超时时间点来归档会话,便于进行会话管理和超时检查。在下文“分桶策略”会话管理的介绍中,我们还会对该数据结构进行详细讲解。

2.4、创建连接

服务端对于客户端的“会话创建”请求的处理,大体可以分为四大步骤,分别是处理ConnectRequest请求、会话创建、处理器链路处理和会话响应。在 ZooKeeper服务端,首先将会由NI0 Servercnxn来负责接收来自客户端的“会话创建”请求,并反序列化出ConnectRequest请求,然后根据 ZooKeeper服务端的配置完成会话超时时间的协商。随后, Session tracker将会为该会话分配一个 sessionID,并将其注册到sessionsById和 sessionsWithTimeout中去,同时进行会话的激活。之后,该“会话请求”还会在 ZooKeeper服务端的各个请求处理器之间进行顺序流转,最终完成会话的创建。

3、会话管理

本节我们将开始讲解 ZooKeeper服务端是如何管理这些会话的。

3.1、分桶策略

ZooKeeper的会话管理主要是由SessionTracker负责的,其采用了一种特殊的会话管理方式,我们称之为“分桶策略”。所谓分桶策略,是指将类似的会话放在同一区块中进行管理,以便于 ZooKeeper对会话进行不同区块的隔离处理以及同一区块的统一处理,如图所示。

Zookeeper将所有的会话都分配在不同的区块一种,分配的原则是每个会话的下次超时时间点(ExpirationTime)。ExpirationTime指该会话最近一次可能超时的时间点。同时,Zookeeper Leader服务器在运行过程中会定时地进行会话超时检查,时间间隔是ExpirationInterval,默认为tickTime的值,ExpirationTime的计算时间如下

ExpirationTime = CurrentTime + SessionTimeOut

其中 CurrentTime指当前时间,单位是毫秒; SessionTimeout指该会话设置的超时时间,单位也是毫秒。那么,上图中横坐标所标识的时间,是否就是通过上述公式计算出来的呢?答案是否定的,在 ZooKeeper的实际实现中,还做了一个处理。 ZooKeeper的
Leader服务器在运行期间会定时地进行会话超时检査,其时间间隔是 ExpirationInterval,单位是毫秒,默认值是 tickTime的值,即默认情况下,每隔2000毫秒进行一次会话超时检查。为了方便对多个会话同时进行超时检查,完整的 ExpirationTime的计算方式如下:

 ExpirationTime = ((CurrentTime + SessionTimeOut) / ExpirationInterval + 1) * ExpirationInterval

也就是说,上图中横坐标的 ExpirationTime值总是 ExpirationInterva的整数倍数。

3.2、会话激活

为了保持客户端会话的有效性,在 ZooKeeper的运行过程中,客户端会在会话超时时间过期范围内向服务端发送PING请求来保持会话的有效性,我们俗称“心跳检测”。同时,服务端需要不断地接收来自客户端的这个心跳检测,并且需要重新激活对应的客户端会话,我们将这个重新激活的过程称为 TouchSession。会话激活的过程,不仅能够使服务端检测到对应客户端的存活性,同时也能让客户端自己保持连接状态。其主要流程如图所示。

1.检验该会话是否已经被关闭
Leader会检査该会话是否已经被关闭,如果该会话已经被关闭,那么不再继续激活该会话。
2.计算该会话新的超时时间 ExpirationTimeNew
如果该会话尚未关闭,那么就开始激活会话。首先需要计算出该会话下一次超时时间点,使用的就是上面提到的计算公式
3.定位该会话当前的区块
获取该会话老的超时时间 ExpirationTime Old,并根据该超时时间来定位到其所在的区块。
4.迁移会话
将该会话从老的区块中取出,放入 Expiration Time New对应的新区块中,如图所示。

通过以上4步,就基本完成会话激活的过程。在上面的会话激活过程中,我们可以看到,只要客户端发来心跳检测,那么服务端就会进行一次会话激活。心跳检测由客户端主动发起,以PING请求的形式向服务端发送。但实际上,在 ZooKeeper服务端的设计中,只要客户端有请求发送到服务端,那么就会触发一次会话激活。因此,总的来讲,大体会出现以下两种情况下的会话激活。

  • 只要客户端向服务端发送请求,包括读或写请求,那么就会触发一次会话激活。
  • 如果客户端发现在 session Timeout/3时间内尚未和服务器进行过任何通信,即没有向服务端发送任何请求,那么就会主动发起一个PING请求,服务端收到该请求后,就会触发上述第一种情况下的会话激活。

3.3、会话超时检查

上面我们分别介绍了 ZooKeeper会话的分桶管理策略和会话激活的过程,现在我们再来看看 ZooKeeper是如何进行会话超时检查的。

在 ZooKeeper中,会话超时检查同样是由 Session tracker负责的。 SessionTracker中有个单独的线程专门进行会话超时检查,这里我们将其称为“超时检查线程”,其工作机制的核心思路其实非常简单:逐个依次地对会话桶中剩下的会话进行清理。

在图3.1中,我们可以看到,如果一个会话被激活,那么ZooKeeper会将其从上一个会话桶迁移到下一个会话桶中,例如图中的session. n这个会话,由于触发了会话激活,因此 ZooKeeper会将其从 expirationTime I桶迁移到 expirationTime n桶中去。于是,expirationTime 1中留下的所有会话都是尚未被激活的。因此,超时检查线程的任务就是定时检查出这个会话桶中所有剩下的未被迁移的会话。

那么超时检査线程是如何做到定时检查的呢?这里就和 ZooKeeper会话的分桶策略紧密联系起来了。在会话分桶策略中,我们将 ExpirationInterval的倍数作为时间点来分布会话,因此,超时检查线程只要在这些指定的时间点上进行检查即可,这样既提高了会话
检查的效率,而且由于是批量清理,因此性能非常好—这也是为什么 ZooKeeper要通过分桶策略来管理客户端会话的最主要的原因。因为在实际生产环境中,一个 ZooKeeper集群的客户端会话数可能会非常多,逐个依次检查会话的方式会非常耗费时间。

4、会话清理

当 SessionTracker的会话超时检查线程整理出一些已经过期的会话后,那么就要开始进行会话清理了。会话清理的步骤大致可以分为以下7步。

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

由于整个会话清理过程需要一段的时间,因此为了保证在此期间不再处理来自该客户端的新请求, SessionTracker会首先将该会话的 isClosing属性标记为true。这样,即使在会话清理期间接收到该客户端的新请求,也无法继续处理了

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

为了使对该会话的关闭操作在整个服务端集群中都生效, ZooKeeper使用了提交“会话关闭”请求的方式,并立即交付给PrepRequestprocessor处理器进行处理。

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

在 ZooKeeper中,一旦某个会话失效后,那么和该会话相关的临时(EPHEMERAL)节点都需要被一并清除掉。因此,在清理临时节点之前,首先需要将服务器上所有和该会话相关的临时节点都整理出来。

在 ZooKeeper的内存数据库中,为每个会话都单独保存了一份由该会话维护的所有临时节点集合,因此在会话清理阶段,只需要根据当前即将关闭的会话的sessionID从内存数据库中获取到这份临时节点列表即可。

但是,在实际应用场景中,情况并没有那么简单,有如下的细节需要处理:在ZooKeeper处理会话关闭请求之前,正好有以下两类请求到达了服务端并正在处理中。

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

对于这两类请求,其共同点都是事务处理尚未完成,因此还没有应用到内存数据库中,所以上述获取到的临时节点列表在遇上这两类事务请求的时候,会存在不致的情况。

假定我们当前获取的临时节点列表是 ephemerals,那么针对第一类请求,我们需要将所有这些请求对应的数据节点路径从ephemerals中移除,以避免重复删除。针对第二类请求,我们需要将所有这些请求对应的数据节点路径添加到ephemerals中去,以删除这些即将会被创建但是尚未保存到内存数据库中去的临时节点。

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

完成该会话相关的临时节点收集后, ZooKeeper会逐个将这些临时节点转换成“节点删除”请求,并放入事务变更队列outstandingChanges中去。

5.删除临时节点。

在上面的步骤中,我们已经收集了所有需要删除的临时节点,并创建了对应的“节点删除”请求, FinalrequestProcessor处理器会触发内存数据库,删除该会话对应的所有临时节点。

6.移除会话。

完成节点删除后,需要将会话从 SessionTracker中移除。主要就是从上面提到的三个数据结构(sessionsById、sessionsWithTimeout和sessionSets)中将该会话移除掉。

7.关闭 NIOServerCnxn。

最后,从NIOServerCnxnFactory找到该会话对应的 NIOServerCnxn,将其关闭。

5、重连

当客户端和服务端之间的网络连接断开时, ZooKeeper客户端会自动进行反复的重连,直到最终成功连接上 ZooKeeper集群中的一台机器。在这种情况下,再次连接上服务端的客户端有可能会处于以下两种状态之一。

  • CONNECTED:如果在会话超时时间内重新连接上了 ZooKeeper集群中任意一台机器,那么被视为重连成功。
  • EXPIRED:如果是在会话超时时间以外重新连接上,那么服务端其实已经对该会话进行了会话清理操作,因此再次连接上的会话将被视为非法会话。

在ZooKeeper中,客户端与服务端之间维持的是一个长连接,在 sessionTimeout时间内,服务端会不断地检测该客户端是否还处于正常连接——服务端会将客户端的每次操作视为一次有效的心跳检测来反复地进行会话激活。因此,在正常情况下,客户端会话是一直有效的。然而,当客户端与服务端之间的连接断开后,用户在客户端可能主要会看到两类异常:CONNECTION_LOSS(连接断开)和 SESSION_EXPIRED(会话过期)。那么该如何正确处理 CONNECTION_LOSS和 SESSION_EXPIRED呢?

连接断开: CONNECTION_LOSS

有时会因为网络闪断导致客户端与服务器断开连接,或是因为客户端当前连接的服务器出现问题导致连接断开,我们统称这类问题为“客户端与服务器连接断开”现象,即CONNECTION_LOSS。在这种情况下, ZooKeeper客户端会自动从地址列表中重新逐个选取新的地址并尝试进行重新连接,直到最终成功连接上服务器。

举个例子,假设某应用在使用 ZooKeeper客户端进行 setData操作的时候,正好出现了CONNECTION_LOSS现象,那么客户端会立即接收到事件 None-Disconnected通知,同时会抛出异常:org.apache,zookeeper.KeeperException$ConnectionLossException。
在这种情况下,我们的应用需要做的事情就是捕获住 ConnectionLossException,然后等待ZooKeeper的客户端自动完成重连。一旦客户端成功连接上一台 ZooKeeper机器后,那么客户端就会收到事件None-SyncConnected通知,之后就可以重试刚刚出错的setData操作。

会话失效:SESSION_EXPIRED

SESSION_EXPIRED是指会话过期,通常发生在 CONNECTION_LOSS期间。客户端和服务器连接断开之后,由于重连期间耗时过长,超过了会话超时时间(session timeout)限制后还没有成功连接上服务器,那么服务器认为这个会话已经结束了,就会开始进行会话清理。但是另一方面,该客户端本身不知道会话已经失效,并且其客户端状态还是DISCONNECTED。之后,如果客户端重新连接上了服务器,那么很不幸,服务器会告诉客户端该会话已经失效(SESSION EXPIRED)。在这种情况下,用户就需要重新实例化一个 ZooKeeper对象,并且看应用的复杂情况,重新恢复临时数据。

会话转移:SESSION_MOVED

会话转移是指客户端会话从一台服务器机器转移到了另一台服务器机器上。正如上文中提到,假设客户端和服务器S1之间的连接断开后,如果通过尝试重连后,成功连接上了新的服务器S2并且延续了有效会话,那么就可以说会话从Sl转移到了S2上。
会话转移现象其实在 ZooKeeper中一直存在,但是在3.2.0版本之前,会话转移的概念并没有被明确地提出来,于是就会出现如下所述的异常场景:

假设我们的 ZooKeeper服务器集群有三台机器:S1、S2和S3。在开始的时候,客户端C1与服务器S1建立连接且维持着正常的会话,某一个时刻,C1向服务器发送了一个请求R1: setData('/$7_4_4/ session_moved’,1)。但是在请求发送到服务器之前,客户端和服务器恰好发生了连接断开,并且在很短的时间内重新连接上了新的 ZooKeeper服务器S2。之后,C1又向服务器S2发送了一个请求R2
setData(‘/$7_4_4/ session_ moved’,2)。这个时候,S2能够正确地处理请求R2,但是很不幸的事情发生了,请求R1也最终到达了服务器S1,于是,S1同样处理了请求R1,于是,对于客户端C1来说,它的第2次请求R2就被请求R1覆盖了。

因此,在3.2.0版本之后, ZooKeeper明确提出了会话转移的概念,同时封装了SessionMovedException异常。之后,在处理客户端请求的时候,会首先检查会话的所有者( Owner):如果客户端请求的会话Owner不是当前服务器的话,那么就会直接抛出 SessionMovedException异常。当然,由于客户端已经和这个服务器断开了连接,因此无法收到这个异常的响应。只有多个客户端使用相同的sessionId/sessionPasswd创建会话时,才会收到这样的异常。因为一旦有一个客户端会话创建成功,那么 ZooKeeper服务器就会认为该 sessionid对应的那个会话已经发生了转移,于是,等到第二个客户端连接上服务器后,就被认为是“会话转移”的情况了。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值