1.概述
前面分析了集群选举,集群同步,现在分析集群形成后主节点,从节点面向外部提供接入后,如何处理客户端请求。
分别分析:
(1). 主节点如何处理连接建立,连接断开,客户端请求。
(2). 从节点如何处理连接建立,连接断开,客户端请求。
2.主节点
2.1.主节点开启对外集群服务
// 对外服务需要这个zxid,用于为每个请求设置zxid
lastCommitted = zk.getZxid();
leaderStartTime = Time.currentElapsedTime();
zk.startup();
// 更新选票里的集群epoch
self.updateElectionVote(getEpoch());
// 数据实体也有zxid,为数据实体上最后迭代的请求的zxid
zk.getZKDatabase().setlastProcessedZxid(zk.getZxid());
上述zk.startup将执行:
private void startupWithServerState(State state) {
if (sessionTracker == null) {
createSessionTracker();
}
startSessionTracker();
setupRequestProcessors();
startRequestThrottler();
setState(state);
localSessionEnabled = sessionTracker.isLocalSessionsEnabled();
notifyAll();
}
具体到LeaderZookeeperServer
:
public void createSessionTracker() {
sessionTracker = new LeaderSessionTracker(this, getZKDatabase().getSessionWithTimeOuts(),
tickTime, self.getId(), self.areLocalSessionsEnabled(), getZooKeeperServerListener());
}
protected void startSessionTracker() {
upgradeableSessionTracker = (UpgradeableSessionTracker) sessionTracker;
upgradeableSessionTracker.start();
}
protected void setupRequestProcessors() {
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
RequestProcessor toBeAppliedProcessor = new Leader.ToBeAppliedRequestProcessor(finalProcessor, getLeader());
commitProcessor = new CommitProcessor(toBeAppliedProcessor, Long.toString(getServerId()),
false, getZooKeeperServerListener());
commitProcessor.start();
ProposalRequestProcessor proposalProcessor = new ProposalRequestProcessor(this, commitProcessor);
proposalProcessor.initialize();
prepRequestProcessor = new PrepRequestProcessor(this, proposalProcessor);
prepRequestProcessor.start();
firstProcessor = new LeaderRequestProcessor(this, prepRequestProcessor);
setupContainerManager();
}
上述形成的主节点请求处理流水线为:
(1). LeaderRequestProcessor
(2). PrepRequestProcessor
(3). ProposalRequestProcessor
(4). CommitProcessor
(5). Leader.ToBeAppliedRequestProcessor
(6). FinalRequestProcessor
2.2.如何处理连接建立
回到基于连接池建立被动连接,收到收到注册包场景。此时触发ZooKeeperServer
的processConnectRequest
。
BinaryInputArchive bia = BinaryInputArchive.getArchive(new ByteBufferInputStream(incomingBuffer));
ConnectRequest connReq = new ConnectRequest();
connReq.deserialize(bia, "connect");
long sessionId = connReq.getSessionId();
int tokensNeeded = 1;
int sessionTimeout = connReq.getTimeOut();
byte[] passwd = connReq.getPasswd();
// 连接级超时机制
cnxn.setSessionTimeout(sessionTimeout);
cnxn.disableRecv();
2.2.1.建立新会话
若请求中sessionId
为0
,则需为此连接接入新建一个会话:
if (passwd == null) {
passwd = new byte[0];
}
long sessionId = sessionTracker.createSession(timeout);
Random r = new Random(sessionId ^ superSecret);
r.nextBytes(passwd);
ByteBuffer to = ByteBuffer.allocate(4);
to.putInt(timeout);
cnxn.setSessionId(sessionId);
Request si = new Request(cnxn, sessionId, 0, OpCode.createSession, to, null);
submitRequest(si);
return sessionId;
通过向自身构造请求来创建新会话。
我们结合主节点请求处理流水线来分析上述请求的处理过程:
(1). LeaderRequestProcessor
中处理
这一级仅在支持本地会话时,对来自本地会话的特定类型请求额外新增一个升级请求。默认不支持本地会话。此时这级直接将请求提交给下一级。
(2). PrepRequestProcessor
中处理
参考单机部分分析。这里做的是为其构建事务头,事务体。为其分配一个递增的zxid。开始追踪此会话。提交给下一级。
(3). ProposalRequestProcessor
中处理
if (request instanceof LearnerSyncRequest) {
zks.getLeader().processSync((LearnerSyncRequest) request);
} else {
if (shouldForwardToNextProcessor(request)) {
nextProcessor.processRequest(request);// 进入下一级
}
if (request.getHdr() != null) {// 事务头非空的请求
try {
zks.getLeader().propose(request);// 主节点提议
} catch (XidRolloverException e) {
throw new RequestProcessorException(e.getMessage(), e);
}
syncProcessor.processRequest(request);//
}
}
这里做了这些事情:
a. 把此请求提交给下一级
b. 对此请求执行leader
的propose
c. 将此请求提交给syncProcessor
。
(4). CommitProcessor
中处理
这一级的作用是,对主节点收到的来自客户端的请求,先排队缓存。
集群下主节点对收到的每个包含事务头的请求,需要经过提议,收集超过多数确认,才能使得请求进入 提交阶段。
这个CommitProcessor
的作用就是先将前面到达的请求缓存起来,然后执行事件循环,不断检测是否有请求被提交。
检测到请求被提交后,再对提交的请求进行处理(提交给下一级),并相应的修改自身维护的缓存结构。
(5). Leader.ToBeAppliedRequestProcessor
这一级的作用是,直接将请求提交给下一级处理,并且在主节点的toBeApplied
集合中寻找是否存在此请求,若存在,则移除。
(6). FinalRequestProcessor
参考前述单机模式请求处理。
针对createSession
请求,这里执行:
a. 向会话最终提交会话及其超时信息
CreateSessionTxn cst = (CreateSessionTxn) txn;
sessionTracker.commitSession(sessionId, cst.getTimeOut());
b. 更新数据实体的lastProcessedZxid
。
c. 回复客户端
ConnectResponse rsp = new ConnectResponse(0, valid ? cnxn.getSessionTimeout() : 0,
valid ? cnxn.getSessionId() : 0, valid ? generatePasswd(cnxn.getSessionId()) : new byte[16]);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BinaryOutputArchive bos = BinaryOutputArchive.getArchive(baos);
bos.writeInt(-1, "len");
rsp.serialize(bos, "connect");
baos.close();
ByteBuffer bb = ByteBuffer.wrap(baos.toByteArray());
bb.putInt(bb.remaining() - 4).rewind();
cnxn.sendBuffer(bb);
if (valid) {
cnxn.enableRecv();
} else {
cnxn.sendBuffer(ServerCnxnFactory.closeConn);
}
上述分析中我们略去了请求被提议及提交请求的产生过程。下面我们分析这两个过程:
try {
zks.getLeader().propose(request);// 主节点提议
} catch (XidRolloverException e) {
throw new RequestProcessorException(e.getMessage(), e);
}
syncProcessor.processRequest(request);
请求的提议通过zks.getLeader().propose(request);
实现。
该实现过程为:
// 序列化
byte[] data = SerializeUtils.serializeRequest(request);
proposalStats.setLastBufferSize(data.length);
// 构建提议包
QuorumPacket pp = new QuorumPacket(Leader.PROPOSAL, request.zxid, data, null);
Proposal p = new Proposal();
p.packet = pp;// 包
p.request = request;// 原始请求
synchronized (this) {
p.addQuorumVerifier(self.getQuorumVerifier());// 此请求所在的集群信息
lastProposed = p.packet.getZxid();
// zxid-Proposal放入map
outstandingProposals.put(lastProposed, p);
sendPacket(pp);// 向所有可以正常通信的从节点发送此包
}
我们再分析syncProcessor.processRequest(request);
实现:
上述syncProcessor
构建过程为:
AckRequestProcessor ackProcessor = new AckRequestProcessor(zks.getLeader());
syncProcessor = new SyncRequestProcessor(zks, ackProcessor);
对SyncRequestProcessor
参考单机部分描述。简单来说在本阶段会先缓存发过来的请求,将其序列化处理后写入文件流,达到刷新条件后,将文件流刷新到磁盘文件,再批量将刷新处理后的各个请求提交给下一阶段去处理。
这里下一阶段为 AckRequestProcessor
。
此阶段执行动作为:
leader.processAck(self.getId(), request.zxid, null);
主节点的processAck
处理为:
Proposal p = outstandingProposals.get(zxid);
boolean hasCommitted = tryToCommit(p, zxid, followerAddr);
tryToCommit
执行为:
if (outstandingProposals.containsKey(zxid - 1)) {
return false;
}
if (!p.hasAllQuorums()) {
return false;
}
outstandingProposals.remove(zxid);
if (p.request != null) {
toBeApplied.add(p);
}
commit(zxid);
zk.commitProcessor.commit(p.request);
if (pendingSyncs.containsKey(zxid)) {
for (LearnerSyncRequest r : pendingSyncs.remove(zxid)) {
sendSync(r);
}
}
上述会对尝试提交的请求进行检测,只有此请求收到了半数以上确认,且满足有序要求。才提交。
提交的zxid
会从outstandingProposals
移除,加入toBeApplied
。
// commit(zxid)用于将zxid可以提交的情况发给所有连到自己的从节点
public void commit(long zxid) {
synchronized (this) {
lastCommitted = zxid;
}
QuorumPacket qp = new QuorumPacket(Leader.COMMIT, zxid, null, null);
sendPacket(qp);
}
将zxid
可以提交通知给连到自己的从节点后,再执行zk.commitProcessor.commit(p.request);
使主节点自身处理此请求的提交后续流程。正是在这里将此请求加入commitProcessor
的committedRequests
集合。通过前面分析可知,CommitProcessor
中做的就是不断检测其committedRequests
是否存在内容,存在则取出请求进行后续处理。
上述分析中我们略去了从节点对收到的Leader.PROPOSAL
类型包,及后续对收到的Leader.COMMIT
类型包的处理。
前面说从节点完成集群同步后执行如下循环:
QuorumPacket qp = new QuorumPacket();
while (this.isRunning()) {
readPacket(qp);
processPacket(qp);
}
从节点对Leader.PROPOSAL
处理策略为:
TxnLogEntry logEntry = SerializeUtils.deserializeTxn(qp.getData());
TxnHeader hdr = logEntry.getHeader();
Record txn = logEntry.getTxn();
TxnDigest digest = logEntry.getDigest();
lastQueued = hdr.getZxid();
fzk.logRequest(hdr, txn, digest);// 记录日志
主要是fzk.logRequest
,对此部分详细分析放在从节点请求处理部分。这里简要描述为:从节点将受到的请求序列化到文件流,待符合刷新要求时,批量刷新一批到磁盘,再对刷新到磁盘的Leader.PROPOSAL
类型包,向主节点发送关于此zxid
的Leader.ACK
包。
主节点方面收到来自从节点的Leader.ACK
包处理策略:
learnerMaster.processAck(this.sid, qp.getZxid(), sock.getLocalSocketAddress());
前述我们已经分析过。这里将使得主节点针对此zxid
执行tryToCommit
尝试去提交此zxid
。
至于从节点收到Leader.COMMIT
类型包的处理,可简要描述为将其放入从节点的`commitProcessor的committedRequests集合。而放入此集合的请求会被从节点后续请求流水线继续处理,直到依据请求修改其数据实体。
2.2.2.复用老会话
validateSession(cnxn, sessionId);
if (serverCnxnFactory != null) {
// ServerCnxnFactory层面解除sessionId与被动连接关联
// 对被动连接执行close。
// 被动连接的close流程:
// a.从隶属的被动连接池执行移除自身处理。
// b.从隶属的服务端执行移除自身处理。
// c.取消被动连接的事件监控。
// d.套接字关闭。
serverCnxnFactory.closeSession(sessionId, ServerCnxn.DisconnectReason.CLIENT_RECONNECT);
}
cnxn.setSessionId(sessionId);
// 参考前述单机分析。
// 这里验证。并回复。
reopenSession(cnxn, sessionId, passwd, sessionTimeout);
回复逻辑:
ConnectResponse rsp = new ConnectResponse(0,
valid ? cnxn.getSessionTimeout() : 0, valid ? cnxn.getSessionId() : 0,
valid ? generatePasswd(cnxn.getSessionId()) : new byte[16]);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BinaryOutputArchive bos = BinaryOutputArchive.getArchive(baos);
bos.writeInt(-1, "len");
rsp.serialize(bos, "connect");
baos.close();
ByteBuffer bb = ByteBuffer.wrap(baos.toByteArray());
bb.putInt(bb.remaining() - 4).rewind();
cnxn.sendBuffer(bb);
if (valid) {
cnxn.enableRecv();
} else {
cnxn.sendBuffer(ServerCnxnFactory.closeConn);
}
这样我们便完成了主节点方面对接入的外部连接的注册包的处理流程。
2.3.如何处理创建新节点
(1). 通过被动连接对象收取完整数据包
Request si = new Request(cnxn, cnxn.getSessionId(), h.getXid(), h.getType(), incomingBuffer, cnxn.getAuthInfo());
si.setOwner(ServerCnxn.me);
submitRequest(si);
参考上述,来自客户端的请求会构造一个Request对象提交给请求处理流水线.
(2). LeaderRequestProcessor
中处理
这一级仅在支持本地会话时,对来自本地会话的特定类型请求额外新增一个升级请求。默认不支持本地会话。此时这级直接将请求提交给下一级。
(3). PrepRequestProcessor
中处理
参考单机部分分析。
为请求分配TxnHeader
头,OpCode.create
类型请求会得到一个新的zxid
。
继续对请求执行反向序列化得到CreateRequest
实例对象。
这一步会依据数据实体及中间态对请求合理性进行检测。
在判断请求会失败时,失败时会设置请求对象,使其包含失败信息。
request.getHdr().setType(OpCode.error);
request.setTxn(new ErrorTxn(e.code().intValue()));// 错误请求
request.setException(e);
在判断请求可以成功执行时,会相应调整中间态,为请求分配Txn
,这里是CreateTxn
。
此后递交给下一级处理。
(3). ProposalRequestProcessor
中处理
if (request instanceof LearnerSyncRequest) {
zks.getLeader().processSync((LearnerSyncRequest) request);
} else {
if (shouldForwardToNextProcessor(request)) {
nextProcessor.processRequest(request);// 进入下一级
}
if (request.getHdr() != null) {// 事务头非空的请求
try {
zks.getLeader().propose(request);// 主节点提议
} catch (XidRolloverException e) {
throw new RequestProcessorException(e.getMessage(), e);
}
syncProcessor.processRequest(request);//
}
}
这里做了这些事情:
a. 把此请求提交给下一级
b. 对此请求执行leader
的propose
c. 将此请求提交给syncProcessor
。
(4). CommitProcessor
中处理
这一级的作用是,对主节点收到的来自客户端的请求,先排队缓存。
集群下主节点对收到的每个包含事务头的请求,需要经过提议,收集超过多数确认,才能使得请求进入 提交阶段。
这个CommitProcessor
的作用就是先将前面到达的请求缓存起来,然后执行事件循环,不断检测是否有请求被提交。
检测到请求被提交后,再对提交的请求进行处理(提交给下一级),并相应的修改自身维护的缓存结构。
(5). Leader.ToBeAppliedRequestProcessor
这一级的作用是,直接将请求提交给下一级处理,并且在主节点的toBeApplied
集合中寻找是否存在此请求,若存在,则移除。
(6). FinalRequestProcessor
参考前述单机模式请求处理。
a.针对数据实体执行请求处理
若请求被允许执行,则:
reateTxn createTxn = (CreateTxn) txn;
rc.path = createTxn.getPath();
createNode(createTxn.getPath(), createTxn.getData(),
createTxn.getAcl(), createTxn.getEphemeral() ? header.getClientId() : 0, createTxn.getParentCVersion(), header.getZxid(), header.getTime(), null);
这样将修改数据实体以反映请求操作。
更新数据实体的lastProcessedZxid。
a.针对中间态
若请求被允许执行,则
进行相应调整,以免除不必要的中间态信息存储。
b.回复阶段
针对请求出错的情况,构造回复包
err = e.code();
ReplyHeader hdr = new ReplyHeader(request.cxid, lastZxid, err.intValue());
responseSize = cnxn.sendResponse(hdr, rsp, "response");
针对请求允许的情况,构造回复包
lastOp = "CREA";
rsp = new CreateResponse(rc.path);
err = Code.get(rc.err);
responseSize = cnxn.sendResponse(hdr, rsp, "response");
2.4.如何处理连接断开
可类似进行不再展开
3.从节点
3.1.从节点对外开启集群服务
从节点开启集群服务后,进入如下循环:
QuorumPacket qp = new QuorumPacket();
while (this.isRunning()) {
readPacket(qp);
processPacket(qp);
}
上述一个不断接收来自主节点的包并对其处理的过程.
为了理解从节点如何处理来自主节点的包,处理连到其的客户端的包.有必要先分析下从节点的请求处理流水线.
3.2.从节点的请求处理流水线
从节点的请求处理流水线有两个:
FollowerRequestProcessor->CommitProcessor->FinalRequestProcessor
SyncRequestProcessor->SendAckRequestProcessor
一个服务于对连到其的客户端请求的处理.一个服务于对来自主节点的数据包的处理.
3.3.处理来自主节点的数据包
以处理来自主节点的Leader.PROPOSAL为例.
TxnLogEntry logEntry = SerializeUtils.deserializeTxn(qp.getData());
TxnHeader hdr = logEntry.getHeader();
Record txn = logEntry.getTxn();
TxnDigest digest = logEntry.getDigest();
lastQueued = hdr.getZxid();
fzk.logRequest(hdr, txn, digest);// 记录日志
其中logRequest为:
public void logRequest(TxnHeader hdr, Record txn, TxnDigest digest) {
Request request = new Request(hdr.getClientId(), hdr.getCxid(), hdr.getType(), hdr, txn, hdr.getZxid());
request.setTxnDigest(digest);
if ((request.zxid & 0xffffffffL) != 0) {
pendingTxns.add(request);
}
syncProcessor.processRequest(request);
}
(1). SyncRequestProcessor中的请求处理
可参考单机部分.这里将缓存收到的请求,累计到一定数量或时间后,会将序列化后的内容刷新到磁盘文件,再将请求提交给下一级流水线处理.
(2). SendAckRequestProcessor
这一级对请求的处理为:
public void processRequest(Request si) {
QuorumPacket qp = new QuorumPacket(Leader.ACK, si.getHdr().getZxid(), null, null);
learner.writePacket(qp, false);
}
即这一级会向主节点发回一个关于此zxid的Leader.ACK.
前面主节点的分析中我们知道,主节点每次收到一个Leader.ACK包,会尝试对包内zxid执行一次提交.若提交成功,则后续从节点会收到
Leader.COMMIT类型的包.
从节点对Leader.COMMIT类型包处理流程为:
(1). 对包内zxid在执行提交
fzk.commit(qp.getZxid());
(2).commit为:
public void commit(long zxid) {
long firstElementZxid = pendingTxns.element().zxid;
if (firstElementZxid != zxid) {
LOG.error("Committing zxid 0x" + Long.toHexString(zxid) + " but next pending txn 0x" + Long.toHexString(firstElementZxid));
ServiceUtils.requestSystemExit(ExitCode.UNMATCHED_TXN_COMMIT.getValue());
}
Request request = pendingTxns.remove();
request.logLatency(ServerMetrics.getMetrics().COMMIT_PROPAGATION_LATENCY);
commitProcessor.commit(request);
}
抛除检测逻辑,上述主要是依据zxid,取出之前缓存的请求.然后提交给CommitProcessor.
(3). CommitProcessor处理
参考之前主节点中分析.
这个CommitProcessor
的作用就是先将前面到达的请求缓存起来,然后执行事件循环,不断检测是否有请求被提交。
检测到请求被提交后,再对提交的请求进行处理(提交给下一级),并相应的修改自身维护的缓存结构。
(4). FinalRequestProcessor
参考单机部分.主要是将请求落地到自身的数据实体上.只有请求来源是连到自身的客户端,才需执行回复逻辑.
3.2.从节点处理接入其的客户端的请求
(1). FollowerRequestProcessor阶段
a.收到的请求提交给下一级
b.若请求涉及修改数据实体,或是全局的会话创建和关闭则需执行zks.getFollower().request(request);
c.上述request:
void request(Request request) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream oa = new DataOutputStream(baos);
oa.writeLong(request.sessionId);
oa.writeInt(request.cxid);
oa.writeInt(request.type);
if (request.request != null) {
request.request.rewind();
int len = request.request.remaining();
byte[] b = new byte[len];
request.request.get(b);
request.request.rewind();
oa.write(b);
}
oa.close();
QuorumPacket qp = new QuorumPacket(Leader.REQUEST, -1, baos.toByteArray(), request.authInfo);
writePacket(qp, true);
}
即会给主节点发送Leader.REQUEST消息,其中包含包的信息.
(2). CommitProcessor处理
参考前面描述.
(3). FinalRequestProcessor
参考前面描述.
这里主要再分析下主节点对收到的来自从节点的Leader.REQUEST的处理:
bb = ByteBuffer.wrap(qp.getData());
sessionId = bb.getLong();
cxid = bb.getInt();
type = bb.getInt();
bb = bb.slice();
Request si;
if (type == OpCode.sync) {
si = new LearnerSyncRequest(this, sessionId, cxid, type, bb, qp.getAuthinfo());
} else {
si = new Request(null, sessionId, cxid, type, bb, qp.getAuthinfo());
}
si.setOwner(this);// 请求来自哪里
learnerMaster.submitLearnerRequest(si);// 给主的流水线提交请求
上述会反向序列化获得请求信息,并将请求提交给主节点的流水线.首先接到此请求的是PrepRequestProcessor.
后续分析过程参考前述.
简而言之,从节点收到来自客户端的消息,先提交给自身的CommitProcessor.
自身的CommitProcessor会持续等待,直到从节点从主节点收到关于此请求的Leader.COMMIT,从节点将其放入提交集合.该请求才会在从节点这边继续处理下去.也只有在从节点这边的FinalRequestProcessor阶段会判断此请求来自接入自身的客户端,所以,会向客户端回复.
主节点在这个过程会收到来自从节点关于此包的Leader.REQUEST,然后在自身的流水线处理中,一次对其提议,收集确认,并最终提交.提交后主节点和各个收到Leader.COMMIT的从节点依次在自身流水线中继续处理此请求,直到走完请求处理流水线.但只有初始收到此请求的成员会在最后执行客户端回复逻辑.