ZooKeeper系列之(十二):服务端实现机制

血红的双手。 提交于 2019-11-29 08:10:22

服务端有3种运行方式:leader,follower,observer。leader是领导者,一个ZooKeeper集群同一时刻最多只能有一个leader。follower是跟随者,可以有多个跟随者。Observer是观察者,也可以有多个观察者。

集群刚开始的时候没有leader,这时所有的follower会发起选举过程,选举出唯一一个leader。只有选举出了leader之后集群才能正式工作,被选举出的机器的角色则从follower转换成leader,其余的机器则将自己的leader地址更改为新选举出的leader地址。observer不参与选举过程。

正常的集群对外提供统一服务接口,不管leader、follower、还是observer都可以提供对外服务,对于客户端来说他们是没有区别的。

服务端功能可以分成两大类:选举相关的和服务相关的,我们先看服务相关的。

服务相关的专门用于响应ZooKeeper客户端的请求,类名称为ZooKeeperServer。它又分成3个子类,名称为:LeaderZooKeeperServer,FollowerZooKeeperServer,ObserverZooKeeperServer。顾名思义它们分别在Leader,Follower,Observer时使用。

当LeaderZooKeeperServer收到客户端写事务请求时,先将请求交给Leader类来发起整个集群的写事务流程,这时候Leader类给所有Follower发送PROPOSAL,要求Follower将写请求持久化到本地磁盘以防丢失,Follower持久化后回复ACK信息给Leader,当Leader收集到超过半数的ACK回复时,给Follower发送COMMIT包提交事务执行,最终Leader和Follower都会执行该事务。当然最后是由LeaderZooKeeperServer返回结果给客户端的。

当FollowerZooKeeperServer收到客户端写事务请求时,会调用Follower类发送REQUEST消息给Leader。Leader再给所有Follower发送PROPOSAL发起写事务流程,后面的流程和Leader模式下类似。

对于写操作来说,都是要交给leader然后再开始后续流程的。而对于读操作来说,由于集群提供了分布式一致性存储,就由本地服务端提供服务,而不用区别服务端的角色。

1、怎么启动

服务端的启动是从QuorumPeerMain开始的,它又干了什么事呢?

QuorumPeerMain读取配置信息然后启动QuorumPeer,配置信息由QuorumPeerConfig类负责读取。然后在ZooKeeperServerMain方法中启动ZooKeeperServer并等待关闭事件,ZooKeeperServer又启动ServerCnxn负责和客户端的网络通信。

2、ZooKeeperServer

ZooKeeperServer是服务端基类,它使用NettyServerCnxn作为底层Socket通信机制,NettyServerCnxn收到客户端请求之后会自动调用ZooKeeperServer的prorcessPacket方法来执行客户端命令,并发送响应包给客户端。

ZooKeeper服务端定义了3种子类:LeaderZooKeeperServer、FollowerZooKeeperServer和ObserverZooKeeperServer。它们分别用于Leader,Follower和Observer。

ZooKeeper客户端连接服务端时,可能连接的是Leader,也可能连接的是Follower或者Observer。连接到不同类型的服务端它的命令请求流程是不太一样的,特别是写事务请求。

如果连接的是Leader,则Leader会将写请求生成Proposal包广播到Follower,通知Follower有写事务要执行,这时Follower先将写事务请求保存到磁盘,然后给Leader发送ACK确认。Leader在超过半数确认的情况下才会真正开始执行写命令,写命令完成后Leader再给Follower发送COMMIT命令,通知Follower写操作已完成,这时Follower也会接着调用CommitProcessor并且最后调用FinalRequestProcessor执行同样的写命令,全部完成后Leader和Follower仍然保持相同的ZooKeeper事务,保持集群数据一致性。

读操作则不需要这么复杂,客户端不管连接的是Leader还是Follower,它发送的读命令都会直接执行并返回结果给客户端,因此读操作的并发性能要高很多。

ZooKeeper客户端和服务端之间数据交互流程示意图如下:

 

它们之间采用Netty框架作为底层Socket连接,NettyServerFactory是服务端连接管理池,为每个客户端创建一个NettyServer,NettyServer接收到客户端命令请求之后会调用ZooKeeperServer的processPacket方法处理该客户端请求。

2.1 processPacket

当ZooKeeperServer的底层NettyServerCnxn接收到客户端的Request时,该方法被NettyServerCnzx的主接收线程调用。ZooKeeperServer然后开始处理Request。

该方法的主要代码:

public void processPacket(ServerCnxn cnxn, ByteBuffer incomingBuffer) throws IOException {
    // We have the request, now process and setup for next
    InputStream bais = new ByteBufferInputStream(incomingBuffer);
    BinaryInputArchive bia = BinaryInputArchive.getArchive(bais);
    RequestHeader h = new RequestHeader();
h.deserialize(bia, "header");
Request si = new Request(cnxn, cnxn.getSessionId(), h.getXid(),
                  h.getType(), incomingBuffer, cnxn.getAuthInfo());
    si.setOwner(ServerCnxn.me);
    setLocalSessionFlag(si);
submitRequest(si);
}

最后一行的submitRequest方法调用firstProcessor开始处理Request,firstProcessor是处理请求包的入口方法,在Leader/Follower等不同的模式下其firstProcessor是不同的。主要代码如下:

public void submitRequest(Request si) {
    if (firstProcessor == null) {
        touch(si.cnxn);
        boolean validpacket = Request.isValid(si.type);
        if (validpacket) {
            firstProcessor.processRequest(si);
            if (si.cnxn != null) {
                incInProcess();
            }
         } 
    }
}

2.2 processTxn

处理客户端的写操作请求,通过ZKDatabase类实现具体逻辑。

该方法由FinalRequestProcessor调用,客户端读操作不在这里实现,而是放在FinalRequestProcessor中直接代码实现。当然读操作最终也是通过ZkDatabase的相关方法实现具体逻辑的。

处理写操作请求,写操作涉及到集群状态同步,和读操作有很大的区别,写操作提供了统一的入口方法processTxn来处理。

该方法的主要代码如下:

private ProcessTxnResult processTxn(Request request,TxnHeader hdr, Record txn) {
    ProcessTxnResult rc;
    int opCode = request != null ? request.type : hdr.getType();
    long sessionId = request != null ? request.sessionId : hdr.getClientId();
    if (hdr != null) {
        rc = getZKDatabase().processTxn(hdr, txn);
    } else {
        rc = new ProcessTxnResult();
    }
    if (opCode == OpCode.createSession) {
        if (hdr != null && txn instanceof CreateSessionTxn) {
           CreateSessionTxn cst = (CreateSessionTxn) txn;
           sessionTracker.addGlobalSession(sessionId, cst.getTimeOut());
        } else if (request != null && request.isLocalSession()) {
           request.request.rewind();
           int timeout = request.request.getInt();
           request.request.rewind();
           sessionTracker.addSession(request.sessionId, timeout);
        } else {
           LOG.warn("*****>>>>> Got " + txn.getClass() + " " + txn.toString());
        }
    } else if (opCode == OpCode.closeSession) {
      sessionTracker.removeSession(sessionId);
    }
    return rc;
}

2.3 NettyServerCnxn

NettyServerCnxn是ZooKeeper客户端和服务端底层通信的接口,采用Netty组件作为Socket接口。客户端提交的命令会通过NettyServerCnxn到达服务端,然后触发NettyServerCnxn中的receiveMessage方法,该方法再调用ZooKeeperServer的processPacket方法处理接收到的请求包。其中代码片段如下:

public void receiveMessage(ChannelBuffer message) {
     if (initialized) {
           zks.processPacket(this, bb);
           if (zks.shouldThrottle(outstandingCount.incrementAndGet())) {
                 disableRecvNoWait();
           }
      } else {
           zks.processConnectRequest(this, bb);
           initialized = true;
      }
 }

当客户端第一次连接到服务端时,会调用ZooKeeperServer的processConnectRequest方法处理;其他情况下会调用ZooKeeperServer的processPacket方法处理客户端请求。具体的处理逻辑可参见ZooKeeperServer及其子类(LeaderZooKeeperServer、FollowerZooKeeperServer、ObserverZooKeeperServer)的相关代码。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!