Zookeeper-会话创建流程

初始化阶段:

  1. 初始化Zookeeper对象。调用Zookeeper的构造方法来实例化一个Zookeeper,在初始化过程中,会创建一个客户端Watcher管理器:ClientWatchManager
  2. 设置会话默认Watcher。如果在构造方法中传入一个Watcher对象,那么客户端会将这个对象作为默认Watcher保存在ClientWatchManager
  3. 构造Zookeeper服务器地址列表管理器:HostProvider。在构造方法中传入的服务器地址,客户端会将其存放在服务器地址列表管理器HostProvider中。
  4. 创建并初始化客户端网络连接器:ClientCnxnZookeeper客户端首先会创建一个网络连接器ClientCnxn。用来管理客户端与服务器的网络交互。另外,客户端在创建ClientCnxn的同时,还会初始化客户端两个核心队列outgoingQueuependingQueue,分别作为客户端的请求发送队列和服务器端响应的等待队列。
  5. 初始化SendThreadEventThread。客户端会创建两个核心网络线程SendThreadEventThread,前者用于管理客户端和服务端之间的所有网络I/O,后者则用于进行客户端的事件处理。同时,客户端还会将ClientCnxnSocket分配给SendThread作为底层网络I/O处理器,并初始化EventThread的待处理事件队列waitingEvents,用于存放所有等待被客户端处理的事情。

      //初始化ZooKeeper  

     

     ZooKeeper  zk = new ZooKeeper("127.0.0.1:2181", 5000, new Zookeeper_Watcher ());     

    public ZooKeeper(String connectString, intsessionTimeout, Watcher watcher,

            boolean canBeReadOnly)

        throws IOException

    {

        LOG.info("Initiating client connection, connectString=" + connectString

                + " sessionTimeout=" + sessionTimeout + " watcher=" + watcher);

        //设置默认的watcher

        watchManager.defaultWatcher = watcher;

//分析传入的IP地址,并存放ServerAddress

        ConnectStringParser connectStringParser = new ConnectStringParser(

                connectString);

//构造Zookeeper服务地址列表管理器、底层是随机分配一个ZK服务地址。基于环形地址列表队列(可以去研究)

        HostProvider hostProvider = new StaticHostProvider(

                connectStringParser.getServerAddresses());

//创建并初始化客户端网络连接器:ClientCnxn;

        cnxn = newClientCnxn(connectStringParser.getChrootPath(),

                hostProvider, sessionTimeout, this, watchManager,

                getClientCnxnSocket(), canBeReadOnly);

        cnxn.start();

    }

 

 

public ClientCnxn(String chrootPath, HostProvider hostProvider, int sessionTimeout, ZooKeeper zooKeeper,

            ClientWatchManager watcher, ClientCnxnSocket clientCnxnSocket,

            long sessionId, byte[] sessionPasswd, boolean canBeReadOnly) {

        this.zooKeeper = zooKeeper;

        this.watcher = watcher;

        this.sessionId = sessionId;

        this.sessionPasswd = sessionPasswd;

        this.sessionTimeout = sessionTimeout;

        this.hostProvider = hostProvider;

        this.chrootPath = chrootPath;

 

        connectTimeout = sessionTimeout / hostProvider.size();

        readTimeout = sessionTimeout * 2 / 3;

        readOnly = canBeReadOnly;

        //初始化SendThread和EventThread

        sendThread = new SendThread(clientCnxnSocket);

        eventThread = new EventThread();

 

    }

会话创建阶段

  1. 启动SendThreadEventThreadSendThread首先会判断当前客户端的状态,进行一系列请理性工作,为客户端发送会话创建请求做准备。
  2. 获取一个服务器地址。在开始创建TCP之前,SendThread首先需要获取一个Zookeeper服务器的目标地址, 这通常是从HostProvider中随机获取出一个地址,然后委托给ClientCnxnSocket去创建与Zookeeper服务器之间的TCP连接。
  3. 创建TCP连接。获取一个服务器地址后,ClientCnxnSocket负责和服务器创建一个TCP长连接。

   ClientCnxnSocketNetty实现了ClientCnxnSocket的抽象方法,它负责连接到server,读取/写入网络流量,并作为网络数据层和更高packet层的中间层。其生命周期如下:

    loop:

        - try:

       - - !isConnected()

        - - - connect()

        - - doTransport()

        - catch:

        - - cleanup()

     close()

  1. 构造ConnectRequest请求。
  2. TCP连接创建完毕后,可能有的读者会认为,这样是否就说明已经和Zookeeper服务器完成连接了呢?其实不然,上面的步骤只是纯粹地从网络TCP层完成了客户端与服务端之间的Socket连接,但远未完成Zookeeper客户端的会话创建。
  3. SendThread会负责根据当前客户端的实际设置,构造出一个ConnectRequest请求,该请求代表了客户端试图与服务端创建一个会话。同时,Zookeeper客户端还会进一步将该请求包装成网络I/O层的Packet对象,放入发送队列outgoingQueue中去。
  4. 发送请求。当客户端请求准备完毕后,就可以开始向服务端发送请求了。ClientCnxnSocket负责从outgoingQueue中取出一个待发送的Packet对象,将其序列化成ByteBuffer后,向服务端进行发送。

    clientCnxnSocket.doTransport:

    @Override

    voiddoTransport(int waitTimeOut, List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue,

                     ClientCnxn cnxn)

            throws IOException, InterruptedException {

        selector.select(waitTimeOut);

        Set<SelectionKey> selected;

        synchronized (this) {

            selected = selector.selectedKeys();

        }

        // Everything below and until we get back to the select is

        // non blocking, so time is effectively a constant. That is

        // Why we just have to do this once, here

        updateNow();

        for (SelectionKey k : selected) {

            SocketChannel sc = ((SocketChannel) k.channel());

            if ((k.readyOps() & SelectionKey.OP_CONNECT) != 0) {

                if (sc.finishConnect()) {

                    updateLastSendAndHeard();

                  //构造出一个ConnectRequest请求,该请求代表了客户端试图与服务端创建一个会话。同时,Zookeeper客户端还会进一步将该请求包装成网络I/O层的Packet对象,放入发送队列outgoingQueue中去。

                          sendThread.primeConnection();

                }

            } elseif ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {

                doIO(pendingQueue, outgoingQueue, cnxn);   //处理读写操作。

            }

        }

        if (sendThread.getZkState().isConnected()) {

            synchronized(outgoingQueue) {

                if (findSendablePacket(outgoingQueue,

                        cnxn.sendThread.clientTunneledAuthenticationInProgress()) != null) {

                    enableWrite();

                }

            }

        }

        selected.clear();

    }

响应处理阶段

  1. 接受服务器端响应。ClientCnxnSocket接受到服务端响应后,会首先判断当前的客户端状态是否是已初始化,如果尚未完成初始化,那么就认为该响应一定是会话创建请求的响应,直接交由readConnectResult方法来处理该响应。
  2. 处理ResponseClientCnxnSocket会对接受到的服务端响应进行反序列化,得到ConnectResponse对象,并从中获取到Zookeeper服务端分配的会话SessionId[SessionID怎么分配的了?]
  3. 连接成功。连接成功后,一方面需要通知SendThread线程,进一步对客户端进行会话参数的设置,包括readTimeoutconnectTimeout等,并更新客户端状态,另一方面,需要通知地址管理器HostProvider当前成功连接的服务器地址。
  4. 生成时间:SyncConnected-None。为了能够让上层应用感知到会话的成功创建,SendThread会生成一个事件SyncConnected-None,代表客户端与服务器会话创建成功,并将该事件传递给EventThread线程。
  5. 查询WatcherEventThread线程收到事件后,会从ClientWatchManager管理器中查询出对应的Watcher,针对SyncConnected-None事件,那么就直接找出存储的默认Watcher,然后将其放到EventThreadwatingEvents队列中去。
  6. 处理事件。EventThread不断的从watingEvents队列中取出待处理的Watcher对象,然后直接调用该对象的process接口方法,以达到触发Watcher的目的。

ClientCnxnSocketNIO:

     
  /**

     * @return true if a packet was received

     * @throws InterruptedException

     * @throws IOException

     */

    voiddoIO(List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue, ClientCnxn cnxn)

      throws InterruptedException, IOException {

        SocketChannel sock = (SocketChannel) sockKey.channel();

        if (sock == null) {

            thrownew IOException("Socket is null!");

        }

        if (sockKey.isReadable()) {

            int rc = sock.read(incomingBuffer);

            if (rc < 0) {

                thrownew EndOfStreamException(

                        "Unable to read additional data from server sessionid 0x"

                                + Long.toHexString(sessionId)

                                + ", likely server has closed socket");

            }

            if (!incomingBuffer.hasRemaining()) {

                incomingBuffer.flip();

                if (incomingBuffer == lenBuffer) {

                    recvCount++;

                    readLength();

                } elseif (!initialized) {

                    readConnectResult(); //处理Response

                    enableRead();

                    if (findSendablePacket(outgoingQueue,

                            cnxn.sendThread.clientTunneledAuthenticationInProgress()) != null) {

                        // Since SASL authentication has completed (if client is configured to do so),

                        // outgoing packets waiting in the outgoingQueue can now be sent.

                        enableWrite();

                    }

                    lenBuffer.clear();

                    incomingBuffer = lenBuffer;

                    updateLastHeard();

                    initialized = true;

                } else {

                    sendThread.readResponse(incomingBuffer);

                    lenBuffer.clear();

                    incomingBuffer = lenBuffer;

                    updateLastHeard();

                }

            }

        }

        if (sockKey.isWritable()) {

            synchronized(outgoingQueue) {

                Packet p = findSendablePacket(outgoingQueue,

                        cnxn.sendThread.clientTunneledAuthenticationInProgress());

 

                if (p != null) {

                    updateLastSend();

                    // If we already started writing p, p.bb will already exist

                    if (p.bb == null) {

                        if ((p.requestHeader != null) &&

                                (p.requestHeader.getType() != OpCode.ping) &&

                                (p.requestHeader.getType() != OpCode.auth)) {

                            p.requestHeader.setXid(cnxn.getXid());

                        }

                        p.createBB();

                    }

                    sock.write(p.bb);

                    if (!p.bb.hasRemaining()) {

                        sentCount++;

                        outgoingQueue.removeFirstOccurrence(p);

                        if (p.requestHeader != null

                                && p.requestHeader.getType() != OpCode.ping

                                && p.requestHeader.getType() != OpCode.auth) {

                            synchronized (pendingQueue) {

                                pendingQueue.add(p);

                            }

                        }

                    }

                }

                if (outgoingQueue.isEmpty()) {

                    // No more packets to send: turn off write interest flag.

                    // Will be turned on later by a later call to enableWrite(),

                    // from within ZooKeeperSaslClient (if client is configured

                    // to attempt SASL authentication), or in either doIO() or

                    // in doTransport() if not.

                    disableWrite();

                } elseif (!initialized && p != null && !p.bb.hasRemaining()) {

                    // On initial connection, write the complete connect request

                    // packet, but then disable further writes until after

                    // receiving a successful connection response.  If the

                    // session is expired, then the server sends the expiration

                    // response and immediately closes its end of the socket.  If

                    // the client is simultaneously writing on its end, then the

                    // TCP stack may choose to abort with RST, in which case the

                    // client would never receive the session expired event.  See

                    // http://docs.oracle.com/javase/6/docs/technotes/guides/net/articles/connection_release.html

                    disableWrite();

                } else {

                    // Just in case

                    enableWrite();

                }

            }

        }

    }

doIO:

响应接收:

   1、如果检测到当前客户端还尚未初始化,那么说明当前客户端与服务端之间正在进行会话创建,那么直接将接收到的ByteBuffer(imcomingBuffer)序列户成ConnectResponse对象。

    2、如果当前客户端已经处于正常的会话周期,并且接收的服务响应的是一个事件,那么Zookeeper客户端会将接收到的ByteBuffer(imcomingBuffer)序列化成WatcherEvent对象,并将该对象放入待处理队列中,。

    3.如果是一个常规的请求响应(指定是CreateGetDataExist等操作请求),那么会从PendingQuene队列中取出一个Packet来进行相应的处理。Zookeeper客户端首先会检查服务端响应中包含的XID值来确保请求处理的顺序性,然后将接收到的ByteBuffer(imcomingBuffer)序列化成相应的Response对象。

最后会在finshPacket方法处理Watcher注册逻辑。


   注:outgoingQueuependingQuene

      ClientCnxn,两个比较核心的队列outgoingQueuependingQueneoutgoingQueue队列是一个请求发送队列,专门用于存储那些需要发送到服务端的Packet对象集合。pendingQuene队列是为了存储那些已经从客户端发送到服务器的,但是需要等待服务器响应的Packet集合。

在正常的情况下(即客户端与服务端之间的TCP链接正常且会话有效的情况下),会从outgoingQueue队列取出Packet一个可发送的对象,同时生成客户端请求序号XID将其设置到Packet请求头中,然后将其序列化后进行发送。可发送的Packet对象特指是如果检测到客户端与服务端之间正在处理SASL权限的话,那么那些不含请求头的Packet(会话创建请求)是可以发送的,其余的无法发送。请求完毕后,会立即将该Packet保存到pendingQuene队列中,以便等地啊服务端响应后进行相应的处理。

     SendThred:是客户端ClientCnxn内部核心的IO调度线程,用于管理客户端和服务端之间所有网络IO 操作,在Zookeeper客户端的实际运行过程中,一方面,SendThred维护客户端与服务端之间的会话周期,器通过一定的周期频率内向服务器发送一个PING包来实现心跳机制,同时在会话周期内,如果客户端与服务端之间出现TCP连接断开情况,那么会自动且透明化的完成重连操作,,另一方面SendThred管理客户端所有请求的发送和响应接收,其将上层客户端API的操作转换成相应的请求协议发送到服务端,并完成对同步调用的返回和异步调用的回调。同时SendThred还负责将来自服务端的事件传递给EventThred处理.

     EventThred:是客户端ClientCnxn另外一个核心线程,负责客户端的事件处理,并触发客户端注册的Watcher监听。EventThred中有一个waitEvents队列,用于临时存放那些需要被触发的Object。包括那么客户端注册的Wacter和异步接口注册的回调器AsyncCallBack,同时EventThred会不断从waitEvents这个队列取出Object,识别出其具体类型,并分别调用processprodessResult接口方法来事件对事件的触发和回调。

     SessionTrackerImpl: SessionTracker是Zookeeper服务端的会话管理器,负责会话创建、管理、清理工作

 

1、会话概述

ZooKeeper中,客户端和服务端建立连接后,会话随之建立,生成一个全局唯一的会话ID(Session ID)。服务器和客户端之间维持的是一个长连接,在SESSION_TIMEOUT时间内,服务器会确定客户端是否正常连接(客户端会定时向服务器发送heart_beat,服务器重置下次SESSION_TIMEOUT时间)。因此,在正常情况下,Session一直有效,并且ZK集群所有机器上都保存这个Session信息。在出现网络或其它问题情况下(例如客户端所连接的那台ZK机器挂了,或是其它原因的网络闪断),客户端与当前连接的那台服务器之间连接断了,这个时候客户端会主动在地址列表(实例化ZK对象的时候传入构造方法的那个参数connectString)中选择新的地址进行连接。

2、连接断开

好了,上面基本就是服务器与客户端之间维持会话的过程了。在这个过程中,用户可能会看到两类异常CONNECTIONLOSS(连接断开)SESSIONEXPIRED(Session过期)。连接断开(CONNECTIONLOSS)一般发生在网络的闪断或是客户端所连接的服务器挂机的时候,这种情况下,ZooKeeper客户端自己会首先感知到这个异常,具体逻辑是在如下方法中触发的:一种场景是Server服务器挂了,这个时候,ZK客户端首选会捕获异常

所以,现在对于连接断开这个过程就一目了然了,核心流程如下: ZK客户端捕获连接断开异常 ——> 获取一个新的ZK地址 ——> 尝试连接
在这个流程中,我们可以发现,整个过程不需要开发者额外的程序介入,都是ZK客户端自己会进行的,并且,使用的会话ID都是同一个,所以结论就是:发生CONNECTIONLOSS的情况,应用不需要做什么事情,等待ZK客户端建立新的连接即可。

 3、会话超时

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

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

4、会话时间(Session Time

在《ZooKeeper API 使用》一文中已经提到,在实例化一个ZK客户端的时候,需要设置一个会话的超时时间。这里需要注意的一点是,客户端并不是可以随意设置这个会话超时时间,在ZK服务器端对会话超时时间是有限制的,主要是minSessionTimeoutmaxSessionTimeout这两个参数设置的。(详细查看这个文章《ZooKeeper管理员指南》)Session超时时间限制,如果客户端设置的超时时间不在这个范围,那么会被强制设置为最大或最小时间。默认的Session超时时间是在2 * tickTime ~ 20 * tickTime。所以,如果应用对于这个会话超时时间有特殊的需求的话,一定要和ZK管理员沟通好,确认好服务端是否设置了对会话时间的限制。



参考从Paxos到Zookeeper分布式一致性原理与实践书籍。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值