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去处理;

浙公网安备 33010602011771号