Zookeeper内部实现分布式数据一致性(客户端)(七)

关于Zookeeper客户端几个注意的地方:

  1. 服务器地址管理器:HostProvider;

    Zookeeper提供了对HostProvider的默认实现(上一篇中这两个类的源码已经列出,这里不再重复);

    在获取一个可用服务器地址的时候,使用StaticHostProvider的next方法。

      这个方法主要在于一个服务器地址列表打散和一个环形列表的思想:对于这个环形列表的实现通过lastIndex和currentIndex两个游标实现。我个人只理解运行过程,但没理解以下几点:

        1.初始化时,lastIndex和currentIndex都为-1。(lastIndex表示当前正在使用的服务器,currentIndex表示当前循环遍历到那个元素位置,而每次获取一个服务器时,currentIndex+1)。

        2.lastIndex和currentIndex游标值相同的时候就进行spinDelay延迟。

      关于StaticHostProvider的next方法实现的详解,很不巧,我没有找到一篇关于讲这个的,大部分人都是将《从Paxos到Zookeeper分布式一致性原理与实践》原话抄过来,

      笔者也是这样,但是笔者是一边看着Zookeeper源码一边看得书。

我现在贴出next方法部分源码,共同解读:

  

 public InetSocketAddress next(long spinDelay) {
        //是否需要延时
        boolean needToSleep = false;
        InetSocketAddress addr;

        synchronized(this) {
            //从这开始到
            if (reconfigMode) {
                addr = nextHostInReconfigMode();
                if (addr != null) {
                    currentIndex = serverAddresses.indexOf(addr);
                    return resolve(addr);
                }
                //tried all servers and couldn't connect
                reconfigMode = false;
                needToSleep = (spinDelay > 0);
            }
            //这一部分的代码我们先不看; 因为没有用到lastIndex;     
            //假设代码走了下面这部分;
           //currentIndex是先++;
            ++currentIndex;
            //环形思想,之前说过,跳过;
            if (currentIndex == serverAddresses.size()) {
                currentIndex = 0;
            }
            //通过currentIndex获取的addr,书中没有提到;            
            addr = serverAddresses.get(currentIndex);
            //needToSleep默认是faslse,如果lastIndex指向的地址和当前current的
            //指向相同,并且spinDelay>0;才为ture;
           //因为last是上一个服务器地址,而实际通过current拿到地址的时候必须判断是否是上一次的地址(重新获取地址的结果,无非就是上一个地址断开等问题无法使用);
            needToSleep = needToSleep || (currentIndex == lastIndex && spinDelay > 0);

            if (lastIndex == -1) { 
                // We don't want to sleep on the first ever connect attempt.
                lastIndex = 0;
            }
        }
        if (needToSleep) {
            try {
                //延时;
                Thread.sleep(spinDelay);
            } catch (InterruptedException e) {
                LOG.warn("Unexpected exception", e);
            }
        }

        return resolve(addr);
}

其实看到这里大致已经明白地址的获取到底是通过那个游标;但是还有问题:lastIndex?没有具体的更改指向?上述代码忽略的那一部分问题还没有解释清楚?

   这里做一个猜想:如果成功通过current游标获取到addr,按照书上的意思(last指向当前使用的服务器),那么应该是将last指向current。

   也就是说一定有:last指向到current的代码;

    这部分先留一个疑问,后面在回来找这行代码;

StaticHostProvider是Zookeeper官方对地址列表管理器的默认实现。我们也可以实现自己的服务器地址管理列表;

  但前提满足以下三个要素:

    1.next方法必须返回合法的地址:不能是null,且要是合法的InetScoketAddress;

    2.next方法必须返回解析的InetSocketAddress对象;

    3.size方法不能返回0,代表服务器列表必须有一个服务器地址;

  自己实现的HostProvider可能会遇到的问题:

    1.Zookeeper服务端的迁移个别机器的变更导致ip改变。

      于是为解决这个问题:地址列表管理器能定时从DNS或一个配置中心解析Zookeeper服务器地址列表,如果这个地址变更了,那么就要同时更新到serverAddresses集合中。

     2.同机房优先策略:

      系统规模扩大,为了跨机房调用服务带来的网络延时,可以使用一个优先和同机房Zookeeper服务器创建会话HostProvider;

 

  2. ClientCnxn:网络IO

    之前已经提到过各一个协议单元Packet,它有ClientCnxn包装,作为请求和响应的载体;

    源码看一下Packet内部:(ClientCnxn的静态内部类);

static class Packet {
       //请求头
        RequestHeader requestHeader;
       //响应头
        ReplyHeader replyHeader;
       //请求体
        Record request;
        //响应体
        Record response;
        //正在传输的序列化内容
        ByteBuffer bb;

        //节点路径
        String clientPath;
     
        String serverPath;

        boolean finished;

        AsyncCallback cb;

        Object ctx;
         //注册的Watcher
        WatchRegistration watchRegistration;

        public boolean readOnly;

        WatchDeregistration watchDeregistration;

之前提到有关于watcher的几个接口,是否每次都需要将相关的参数序列化进行传输?之前也说过这个答案,当然不是。

  Packet的createBB方法负责对Packet对象进行序列化,生成最终可用于底层传输的ByteBuffer对象。在这个过程中。之后将requestHeader,request和readOnly三个属性进行序列化,其余属性都保存在客户端上下文,不会进行传输。(用1,2,3标识了传输的三个参数) 

public void createBB() {
            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                BinaryOutputArchive boa = BinaryOutputArchive.getArchive(baos);
                boa.writeInt(-1, "len"); // We'll fill this in later
               //1.
                if (requestHeader != null) {
                    requestHeader.serialize(boa, "header");
                }
               //2,3.
                if (request instanceof ConnectRequest) {
                    request.serialize(boa, "connect");
                    // append "am-I-allowed-to-be-readonly" flag
                    boa.writeBool(readOnly, "readOnly");
                } else if (request != null) {
                    request.serialize(boa, "request");
                }
                baos.close();
                this.bb = ByteBuffer.wrap(baos.toByteArray());
                this.bb.putInt(this.bb.capacity() - 4);
                this.bb.rewind();
            } catch (IOException e) {
                LOG.warn("Ignoring unexpected exception", e);
            }
        }

 

  3. ClientCnxnSocket:底层Socket通信使用的是默认实现:ClientCnxnSocketNIO,该实现类,使用java原生的NIO接口。

  4. 再次强调一下SendThread线程:

     管理客户端与服务端网络的(I/O): 在会话生命周期,在一定的周期频率内向服务端发送一个PING包来实现心跳检测(主动)。同时在会话周期内,如果客户端与服务端之间出现TCP连接断开的情况,那就会自动且透明化地完成重连操作;并且,SendThread还负责将来自服务端事件传递给EventThread去处理;

    

posted @ 2019-11-19 20:41  艾尔猿  阅读(315)  评论(0)    收藏  举报