技术库 > 网站架构

ZooKeeper 会话Session

技术库:tec.5lulu.com

1 概述

from:tec.5lulu.com

会话(Session)是ZooKeeper中最重要的概念之一,客户端与服务端之间的任何交互操作都与会话息息相关,这其中就包括临时节点的生命周期、客户端请求的顺序执行以及Watcher通知机制等。
在Java中,ZooKeeper的连接与会话就是客户端通过实例化 ZooKeeper对象来实现客户端与服务器创建并保持TCP连接的过程。

2 会话状态

在ZooKeeper客户端与服务端成功完成连接创建后,就建立了一个会话。ZooKeeper会话在整个运行期间的生命周期中,会在不同的会话状态之间进行切换,这些状态一般可以分为五种,如下:

  • CONNECTING
  • CONNECTED
  • RECONNECTING
  • RECONNECTED
  • CLOSE

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

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

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

下面是一个简要的会话状态转移图。
ZooKeeper 会话Session,by 5lulu.com

上图中几个状态的说明如下:

  • not_connected:最开始,未和服务器进行通讯的时候
  • connecting:客户端正在开始和zookeeper集群服务中的某一台服务器进行连接
  • associating:客户端已经和某个服务器建立tcp连接,在等待服务器根据客户端上传的会话信息进行"匹配"
  • connected:客户端已和服务器联系上,此时可以和服务器发送读写请求
  • expired:会话已经超时不可用,客户端此时应关闭本会话,自杀或者重新发起连接

在ZooKeeper会话的整个生命周期中,其状态可能在connected和connecting之间做多次切换。使用客户端库的业务代码需要注意这两种情况,并避免在connecting 的时候对zookeeper进行读写。

3 会话创建

Session

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

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

sessionID

sessionID用来唯一标识一个会话,因此ZooKeeper必须保证sessionID的全局唯一性。在每次客户端向服务端发起“会话创建”请求时,服务端都会为其分配一个sessionID,现在我们就来看看sessionID究竟是如何生成的。

在SessionTracker初始化的时候,会调用initializeNextSession方法来生成一个初始化的sessionID,之后在ZooKeeper的正常运行过程中,会在该sessionID的基础上为每个会话进行分配,其初始化算法如下: 

    public static long initializeNextSession(long id) { 
        long nextSid = 0; 
        nextSid = (System.currentTimeMillis() << 24) >>> 8; 
        nextSid = nextSid | (id << 56); 
        return nextSid; 
    } 


SessionTracker

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

  • sessionsByld
    这是一个HashMap<Long, Sessionlmpl>类型的数据结构,用于根据sessionID来管理Session实体。
  • sessionsWithTimeout
    这是一个ConcurrentHashMaps<Long, Integer>类型的数据结构,用于根据sessionID来管理会话的超时时间。该数据结构和ZooKeeper内存数据库相连通,会被定期持久化到快照文件中去。
  • sessionSets
    这是一个HashMap<Long, SessionSet>类型的数据结构,用于根据下次会话超时时间点来归档会话,便于进行会话管理和超时检査。

创建连接

服务端对于客户端的“会话创建”请求的处理,大体可以分为四大步骤,如下:

  • 处理ConnectRequest请求
  • 会话创建
  • 处理器链路处理
  • 会话响应

在ZooKeeper服务端,首先将会由NIOServerCnxn来负责接收来自客户端的“会话创建”请求,并反序
列化出ConnectRequest请求,然后根据ZooKeeper服务端的配置完成会话超时时间的协商。随后,SessionTracker将会为该会话分配一个sessionID,并将其注册到sessionsByld和sessionsWithTimeout中去,同时进行会话的激活。之后,该“会话请求”还会在ZooKeeper服务端的各个请求处理器之间进行顺序流转,最终完成会话的创建。

4 会话管理

ZooKeeper服务端是如何管理这些会话的?

分桶策略

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

在上图4中,我们可以看到,ZooKeeper将所有的会话都分配在了不同的区块之中,分配的原则是每个会话的“下次超时时间点”(ExpirationTime)。ExpirationTime是指该会话最近一次可能超时的时间点,对于一个新创建的会话而言,其会话创建完毕后,ZooKeeper就会为其计算ExpirationTime,计算方式如下: 

    ExpirationTime = CurrentTime + SessionTimeout 


其中:

  • CinTentTime指当前时间,单位是毫秒;
  • SessionTimeout指该会话设置的超时时间,单位也是毫秒。

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

    ExpirationTime_ = CurrentTime + SessionTimeout 
    ExpirationTime = (ExpirationTime_/Expirationlnterval +1) x Expirationlnterval 


也就是说,上图中横坐标的ExpirationTime值总是Expirationlnterval的整数倍数。
举个实际例子,假设当前时间的毫秒表示是1370907000000,客户端会话设置的超时时间是 15000 毫秒,ZooKeeper服务器设置的tickTime为2000毫秒,那么Expirationlnterval的值同样为2000毫秒,于是我们可以计算该会话的ExpirationTime值为1370907016000,计算过程如下: 

    ExpirationTime_ = 1370907000000 + 15000 = 1370907015000 
    ExpirationTime =( 1370907015000 / 2000 + 1) x 2000 = 1370907016000 


会话激活

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

步骤如下:

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

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

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

会话超时检查

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

如果一个会话被激活,那么ZooKeeper会将其从上一个会话桶迁移到下一个会话桶中。因此,超时检査线程的任务就是定时检査出这个会话桶中所有剩下的未被迁移的会话。

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

5 会话清理

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

  1. 标记会话状态为“已关闭”。
    由于整个会话清理过程需要一段的时间,因此为了保证在此期间不再处理来自该客户端的新请求,SessionTracker会首先将该会话的isClosing属性标记为true。这样,即使在会话清理期间接收到该客户端的新请求,也无法继续处理了。
  2. 发起“会话关闭”请求。
    为了使对该会话的关闭操作在整个服务端集群中都生效,ZooKeeper使用了提交“会话关闭”请求的方式,并立即交付给PrepRequestProcessor处理器进行
    处理。
  3. 收集需要清理的临时节点
    在ZooKeeper中,一旦某个会话失效后,那么和该会话相关的临时(EPHEMERAL)节点都需要被一并清除掉。因此,在清理临时节点之前,首先需要将服务器上所有和该会话相关的临时节点都整理出来。
    在ZooKeeper的内存数据库中,为每个会话都单独保存了一份由该会话维护的所有临时节点集合,因此在会话清理阶段,只需要根据当前即将关闭的会话的
    sessionID从内存数据库中获取到这份临时节点列表即可。
    但是,在实际应用场景中,情况并没有那么简单,有如下的细节需要处理:在
    ZooKeeper处理会话关闭请求之前,正好有以下两类请求到达了服务端并正在处理中。

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

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

对于这两类请求,其共同点都是事务处理尚未完成,因此还没有应用到内存数据库中,所以上述获取到的临时节点列表在遇上这两类事务请求的时候,会存在不一致的情况。
假定我们当前获取的临时节点列表是ephemerals,那么针对第一类请求,我们需要将所有这些请求对应的数据节点路径从ephemerals中移除,以避免重复删除。针对第二类请求,我们需要将所有这些请求对应的数据节点路径添加到ephemerals中去,以删除这些即将会被创建但是尚未保存到内存数据库中去的临时节点。

  1. 添加“节点删除”事务变更。
    完成该会话相关的临时节点收集后,ZoolCeeper会逐个将这些临时节点转换成“节点删除”请求,并放人事务变更队列outstandingChanges中去。
  2. 删除临时节点。
    在上面的步骤中,我们已经收集了所有需要删除的临时节点,并创建了对应的“节点删除”请求,FinalRequestProcessor处理器会触发内存数据库,删除该会话对应的所有临时节点。
  3. 移除会话。
    完成节点删除后,需要将会话从SessionTracker中移除。主要就是从上面提到的三个数据结构(sessionsByld、sessionsWithTimeout 和 sessionSets)中将该会话移除掉。
  4. 关闭 NIOServerCnxn。
    最后,从NIOServerCnxnFactory找到该会话对应的NIOServerCnxn,将其
    关闭。

6 重连

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

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

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

  • CONNECTION_LOSS (连接断开)
  • SESSION—EXPIRED (会话过期)

那么该如何正确处理 CONNECTION_LOSS 和 SESSION_EXPIRED 呢?

连接断开:CONNECTION_LOSS

有时会因为网络闪断导致客户端与服务器断开连接,或是因为客户端当前连接的服务器出现问题导致连接断开,我们统称这类问题为“客户端与服务器连接断开”现象,即CONNECTION_LOSS。在这种情况下,ZooKeeper客户端会自动从地址列表中重新逐个选取新的地址并尝试进行重新连接,直到最终成功连接上服务器。
举个例子,假设某应用在使用ZooKeeper客户端进行setData操作的时候,正好出现了CONNECTION_LOSS现象,那么客户端会立即接收到事件None-Disconnected通知,同时会抛出异常:org.apache.zookeeper.KeeperException$ConnectionLossExcep tion
在这种情况下,我们的应需要做的事情就是捕获住Connection LossException,然后等待ZooKeeper的客户端自动完成重连。一旦客户端成功连接上一台ZooKeeper机器后,那么客户端就会收到事件None-SyncConnected通知,之后就可以重试刚刚出错的setData操作。

会话失效:SESSI〇N_EXPIRED

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

会话转移:SESSION_MOVED

会话转移是指客户端会话从一台服务器机器转移到了另一台服务器机器上。正如上文中提到,假设客户端和服务器S1之间的连接断开后,如果通过尝试重连后,成功连接上了新的服务器S2并且延续了有效会话,那么就可以说会话从S1转移到了S2上。

会话转移现象其实在ZooKeeper中一直存在,但是在3.2.0版本之前,会话转移的概念并没有被明确地提出来,于是就会出现如下所述的异常场景。

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

当然,上面这个问题非常罕见,只有在C1和S1之间的网路非常慢的情况下才会发生,一旦发生这个问题,将会产生非常严重的后果。

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

7 ZooKeepr会话常见问题

ZooKeeper会话失效,怎么办?

ZooKeeper client在与所有server断开连接后(有可能是各种原因),client会收到disconnted消息。当ZooKeeper server恢复后,ZooKeeper client会自动与server连接上,但是此时会话已失效,client收到session expired消息。前一个会话的所有数据均丢失。接下来你要怎么做,得看你的程序用途:

  • 如果只是读写,没有主备机切换情况(就是当一台主机一台备机,当主机挂机时,ZooKeeper通知备机成为主机),那么,重新new一个会话,将原来session的树状结构重新建立起来就行了。
  • 如果是主备机切换情况,那就不能简单的建立树状结构,因为这时我们是不知道到底主机是挂掉了,还是session expired了。就只能把它当作真的是主机挂机来处理。
  • ZooKeeper所有集群均不可用情况是比较少见的。但是session expired需要引起重视起来。一般情况下,集群中一两台机器的挂机和启动,我们都不用关心,ZooKeeper client可以帮我们自动 处理这些问题。

连接断开

连接断开(CONNECTIONLOSS)一般发生在网络的闪断或是客户端所连接的服务器挂机的时候,这种情况下,ZooKeeper客户端自己会首先感知到这个异常,具体逻辑是在如下方法中触发的: 

    void org.apache.zookeeper.ClientCnxn.SendThread.run(){  
    …… 
    …… 
        } catch (Throwable e) { 
            if (closing) { 
                if (LOG.isDebugEnabled()) { 
                    // closing so this is expected 
                    LOG.debug("An exception was thrown while closing send thread for session 0x" 
                            + Long.toHexString(getSessionId()) 
                            + " : " + e.getMessage()); 
                } 
                break; 
            } else { 
                // this is ugly, you have a better way speak up 
                if (e instanceof SessionExpiredException) { 
                    LOG.info(e.getMessage() + ", closing socket connection"); 
                } else if (e instanceof SessionTimeoutException) { 
                    LOG.info(e.getMessage() + RETRY_CONN_MSG); 
                } else if (e instanceof EndOfStreamException) { 
                    LOG.info(e.getMessage() + RETRY_CONN_MSG); 
                } else if (e instanceof RWServerFoundException) { 
                    LOG.info(e.getMessage()); 
                } else { 
        …… 
        …… 
    } 


一种场景是Server服务器挂了,这个时候,ZK客户端首选会捕获异常,如下:
ZooKeeper 会话Session,by 5lulu.com

捕获异常后,ZK客户端会打印类似于如下日志: 

    EndOfStreamException: Unable to read additional data from server sessionid 0x13ab17ad9ec000b, likely server has closed socket 


然后做一些socket连接的善后工作。接下去是客户端重新选择一个Server Ip尝试连接,这里主要就是从地址列表中获取一个新的Server地址进行连接。逻辑如下: 

    private void startConnect() throws IOException { 
        state = States.CONNECTING; 
        InetSocketAddress addr; 
        if (rwServerAddress != null) { 
            addr = rwServerAddress; 
            rwServerAddress = null; 
        } else { 
            addr = hostProvider.next(1000); 
        } 
        LOG.info("Opening socket connection to server " + addr); 
        setName(getName().replaceAll("(.*)", 
                "(" + addr.getHostName() + ":" + addr.getPort() + ")")); 
        try { 
            zooKeeperSaslClient = new ZooKeeperSaslClient("zookeeper/"+addr.getHostName()); 
        } catch (LoginException e) { 
            LOG.warn("SASL authentication failed: " + e + " Will continue connection to Zookeeper server without " 
                    + "SASL authentication, if Zookeeper server allows it."); 
            eventThread.queueEvent(new WatchedEvent( 
                    Watcher.Event.EventType.None, 
                    Watcher.Event.KeeperState.AuthFailed, null)); 
        }  
        clientCnxnSocket.connect(addr); 
    } 


程序运行过程中,整个过程日志打印大致如下: 

    2016-11-31 09:09:57,379 - INFO  [main-SendThread(test.zookeeper.connection_string2:2181):zookeeper.ClientCnxn$SendThread@1053] - Unable to read additional data from server sessionid 0x23ab45c87df0000, likely server has closed socket, closing socket connection and attempting reconnect 
    收到事件通知:Disconnected 
    获取数据成功,path:/leader 
    2016-11-31 09:09:58,293 - INFO  [main-SendThread-zookeeper.ClientCnxn$SendThread@933] - Opening socket connection to server /1.2.1.1:2181 
    2016-11-31 09:09:58,294 - WARN  [main-SendThread-client.ZooKeeperSaslClient@123] - SecurityException: java.lang.SecurityException: Unable to locate a login configuration occurred when trying to find JAAS configuration.  
    2016-11-31 09:09:58,295 - INFO  [main-SendThread-client.ZooKeeperSaslClient@125] - Client will not SASL-authenticate because the default JAAS configuration section 'Client' could not be found. If you are not using SASL, you may ignore this. On the other hand, if you expected SASL to work, please fix your JAAS configuration. 
    2016-11-31 09:09:58,296 - INFO  [main-SendThread-zookeeper.ClientCnxn$SendThread@846] - Socket connection established to test.zookeeper.connection_string/1.2.1.1:2181, initiating session  
    2016-11-31 09:09:58,299 - INFO  [main-SendThread-zookeeper.ClientCnxn$SendThread@1175] - Session establishment complete on server test.zookeeper.connection_string/1.2.1.1:2181, sessionid = 0x23ab45c87df0000, negotiated timeout = 10000 
    收到事件通知:SyncConnected 


所以,现在对于“连接断开”这个过程就一目了然了,核心流程如下:
ZK客户端捕获“连接断开”异常 ——> 获取一个新的ZK地址 ——> 尝试连接

在这个流程中,我们可以发现,整个过程不需要开发者额外的程序介入,都是ZK客户端自己会进行的,并且,使用的会话ID都是同一个,所以结论就是:发生CONNECTIONLOSS的情况,应用不需要做什么事情,等待ZK客户端建立新的连接即可。

会话超时

SESSION EXPIRED,这个通常是ZK客户端与服务器的连接断了,试图连接上新的ZK机器,但是这个过程如果耗时过长,超过了SESSION_TIMEOUT后还没有成功连接上服务器,那么服务器认为这个Session已经结束了(服务器无法确认是因为其它异常原因还是客户端主动结束会话),由于在ZK中,很多数据和状态都是和会话绑定的,一旦会话失效,那么ZK就开始清除和这个会话有关的信息,包括这个会话创建的临时节点和注册的所有Watcher。在这之后,由于网络恢复后,客户端可能会重新连接上服务器,但是很不幸,服务器会告诉客户端一个异常:SESSION EXPIRED(会话过期)。此时客户端的状态变成CLOSED状态,应用要做的事情就是的看自己应用的复杂程序了,要重新实例zookeeper对象,然后重新操作所有临时数据(包括临时节点和注册Watcher),总之,会话超时在ZK使用过程中是真实存在的。

所以这里也简单总结下,一旦发生会话超时,那么存储在ZK上的所有临时数据与注册的订阅者都会被移除,此时需要重新创建一个ZooKeeper客户端实例,需要自己编码做一些额外的处理。

会话时间

在实例化一个ZK客户端的时候,需要设置一个会话的超时时间。这里需要注意的一点是,客户端并不是可以随意设置这个会话超时时间,在ZK服务器端对会话超时时间是有限制的,主要是minSessionTimeoutmaxSessionTimeout这两个参数设置的。

Session超时时间限制,如果客户端设置的超时时间不在这个范围,那么会被强制设置为最大或最小时间。 默认的Session超时时间是在2 * tickTime ~ 20 * tickTime

所以,如果应用对于这个会话超时时间有特殊的需求的话,一定要和ZK管理员沟通好,确认好服务端是否设置了对会话时间的限制。

8 Zookeeper会话故障案例

故障场景

在一个ZooKeeper集群中,当前leader是A,B和C都是备机。假如A和ZooKeeper集群之间的网络出现了异常,A会收到一个连接状态被持久化为Disconnected的event,但是ZooKeeper Server并没有在这时移除A注册的临时节点,所以理论上A还是leader直至session timeout,session timeout后ZooKeeper会将A注册的临时节点移除掉,然后通知B和C选出新的leader,显而易见,B因为序列号小会成为新的leader。但是问题来了,session timeout的时候A的客户端并没有接收到任何notification,换句话说,它依然会认为自己是leader,这个时候就出现了这样的场景,A认为自己是leader,而B同样会认为自己是leader,即同时出现两个leader对外提供服务的情况。这很显然是不合理的,但如何深入地理解并解决这个问题呢?

解决这个问题需要理解下面三个子问题:

    理解Zookeeper中Session的含义以及Connection Loss和Session Expired的关系
    理解Zookeeper中Session为什么由Server维护,而不由Client维护
    理解作为leader的A在整个流程中应该如何转变自己的角色,来避免脑裂


Zookeeper中Session的含义以及Connection Loss和Session Expired的关系

Session是指当Client创建一个同Server的连接时产生的会话。连接Connected之后Session状态就开启,Zookeeper服务器和Client采用长连接方式(Client会不停地向Server发送心跳)保证session在不出现网络问题、服务器宕机或Client宕机情况下可以一直存在。因此,在正常情况下,session会一直有效,并且ZK集群上所有机器都会保存这个Session信息。

在Zookeeper中,很多数据和状态都是和会话绑定的,一旦会话失效,那么Zookeeper就开始清除和这个会话有关的信息,包括这个会话创建的临时节点和注册的所有Watcher。

一旦网络连接因为某种原因断开或者Zookeeper集群发生宕机,Zookeeper Client会马上捕获到这个异常,封装为一个ConnectionLoss的事件,然后启动自动重连机制在地址列表中选择新的地址进行重连。重连会有三种结果:

  • 在session timeout时间内重连成功,client会重新收到一个syncconnected的event,并将连接重新持久化为connected状态
  • 超过session timeout时间段后重连成功,client会收到一个expired的event,并将连接持久化为closed状态
  • 一直重连不上,client将不会收到任何event

很显然,无论重连成功与否,在session timeout那个重要的时间点,Zookeeper Client是接收不到任何Zookeeper Server清理临时节点的信息的。这也就导致Zookeeper会通知了B和C节点A已经不再是Leader,A自身却没有接收到这样的信息,依旧对外提供服务,进而产生脑裂的问题。

Zookeeper中Session为什么由Server维护,而不由Client维护

为什么Zookeeper Client不维护Session信息,如果这样做了,Zookeeper Client就会在Session Timeout时得到相应的通知。

设想有这么一种真实场景,某个连接的Session Timeout是15s,Zookeeper集群因为未知原因发生宕机,5min之后集群恢复成功。在Session Timeout时,Zookeeper Client确实可以知道Session失效,然后做降主操作,但是Zookeeper Server却不知道Session已经失效,也就不会通知其他节点选出新的Leader,此时整个系统实际上处于没有Leader的状态。即使5min之后重连成功,因为旧Session对应的临时节点没有被清理且序号最小,Zookeeper依然会认为Leader是该临时节点,而实际上该临时节点对应不到任何的Zookeeper Client,所以这种情况下系统依然选不出Leader。

可见,如果由Client维护Session,在某些场景下(网络异常或者集群宕机时间超过Session Timeout),由于逻辑问题根本选不出Leader。因此这种方案是不可行的。

那能不能从应用层面避免脑裂问题呢?带着问题进入下个问题

避免脑裂问题:作为leader的A在整个流程中应该如何转变自己的角色

因为Zookeeper本身的设计使得这种场景下没有一个完美的解决方案,可以考虑采用退化的方案进行处理。

A在接收到DisConnected事件后就降主,不对外提供服务。然后等待接下来的发生的事情,首先可能发生的事件是在Session Timeout时间段内重连成功得到SyncConnected事件,这时A可以重新升级为主,对外提供服务。如果这段时间内没有重连成功,Zookeeper Server在Session Timeout时会将A注册的临时节点移除,并通知B和C,A已经停止对外服务了,需要选出新的leader。因为A自己已经降主了,所以在选出新leader后也不会出现多主现象。如果A在Session Timeout时间段外重连又成功了,那此时肯定新的leader已经选出来了,A需要重新注册作为新的备机候选。

可以使用如下的流程图解释这个过程:
ZooKeeper 会话Session,by 5lulu.com
这种应用层面的方案在一定程度上解决了脑裂问题,但是会出现一段时间系统无Leader的情况,持续时间最长为Session超时时间。

ZooKeeper 会话Session


本文链接 http://tec.5lulu.com/detail/105djn2i5r6w98sfa.html

我来评分 :6.1
0

转载注明:转自5lulu技术库

本站遵循:署名-非商业性使用-禁止演绎 3.0 共享协议

www.5lulu.com