Zookeeper-会话创建流程

来源:互联网 发布:java web 测试工具 编辑:程序博客网 时间:2024/05/16 06:13

初始化阶段:

  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
原创粉丝点击