- 只有Leader支持寫,只能由Leader發起提案proposal , 所以Zookeeper的寫能力是無法擴展的。
- leader只要接收到過半follower對提案的
ACK
,就發起COMMIT
。
那些沒有對提案ACK
(也就是提案處理失敗)的follower 怎麼處理了?
首先,follower 正常情況下肯定能對提案進行ACK
。
如果無法處理提案的follower,會重新進入LOOKING
,進行下一輪的選主 + 恢復階段的數據同步(主要是這個來保證數據一致性)。
下文以3.4.9版本來分析(3.4.8的實現裏SyncRequestProcessor是會退出程序System.exit)
當follower在處理提案proposal 時發生異常,有2種情況:
-
SyncRequestProcessor.run()
異常了。
這裏會設置 ZooKeeperServer.state = ERROR
, 那 Follower.followLeader()
在末尾處的while循環邏輯判定不再滿足,會break;在QuorumPeer.run()
邏輯裏,會驅動它進入LOOKING
。
// SyncRequestProcessor.run()
... } catch (Throwable t) {
handleException(this.getName(), t); // kServer.setState(State.ERROR)
running = false;
}
LOG.info("SyncRequestProcessor exited!");
-
SyncRequestProcessor
執行完成後SendAckRequestProcessor
需要ACK
回leader, 異常了 。
直接關閉Socket連接,Follower.followLeader()
也因異常跳出執行,重新進入LOOKING
public void processRequest(Request si) {
if(si.type != OpCode.sync){
QuorumPacket qp = new QuorumPacket(Leader.ACK, si.hdr.getZxid(), null,
null);
try {
learner.writePacket(qp, false);
} catch (IOException e) {
LOG.warn("Closing connection to leader, exception during packet send", e);
try {
if (!learner.sock.isClosed()) {
learner.sock.close(); // 異常直接關閉sokcet了
}
} catch (IOException e1) {
// Nothing to do, we are shutting things down, so an exception here is irrelevant
LOG.debug("Ignoring error closing the connection", e1);
}
}
}
}
一個請求的全過程
- 客戶端org.apache.zookeeper.ZooKeeper 在實例化時創建了 ClientCnxnSocketNIO;
- 服務端org.apache.zookeeper.server.ZooKeeperServerMain在啓動時默認創建的是NIOServerCnxnFactory,對應的是NIOServerCnxn 。
可以通過指定 "zookeeper.serverCnxnFactory" 來選擇使用Netty版本: NettyServerCnxn。
NIOServerCnxnFactory.run() 的邏輯中,在發生連接事件SelectionKey.OP_ACCEPT時,
- 維護了相同 InetAddress下保持的ClientCnxn計數,判定相同訪問ip的連接不能超過配置值maxClientCnxns(默認60)。
- 創建了專屬這個ClientCnxn的NIOServerCnxn。
- ClientCnxnSocketNIO.doIO : 請求從客戶端發出
- NIOServerCnxn.doIO :從SocketChannel讀入數據到緩衝區
- ZooKeeperServer.processPacket : 進入到ZooKeeperServer處理
- ZooKeeperServer.submitRequest: 找到 firstProcessor 來鏈式處理
不同的角色對於ZooKeeperServer有不同的實現。
ZooKeeperServer
在 Zookeeper 的 Leader & Follower 在選主和恢復階段的流程簡析 裏有提及 :在選主後的數據同步過程完成後會恢復對外服務。
這裏會執行ZooKeeperServer.startup() -> .setupRequestProcessors()
來初始化Server端的處理鏈Processor。
// LeaderZooKeeperServer
protected void setupRequestProcessors() {
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
RequestProcessor toBeAppliedProcessor = new Leader.ToBeAppliedRequestProcessor(
finalProcessor, getLeader().toBeApplied);
commitProcessor = new CommitProcessor(toBeAppliedProcessor,
Long.toString(getServerId()), false,
getZooKeeperServerListener());
commitProcessor.start();
ProposalRequestProcessor proposalProcessor = new ProposalRequestProcessor(this,
commitProcessor);
proposalProcessor.initialize();
firstProcessor = new PrepRequestProcessor(this, proposalProcessor);
((PrepRequestProcessor)firstProcessor).start();
}
// FollowerZooKeeperServer
protected void setupRequestProcessors() {
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
commitProcessor = new CommitProcessor(finalProcessor,
Long.toString(getServerId()), true,
getZooKeeperServerListener());
commitProcessor.start();
firstProcessor = new FollowerRequestProcessor(this, commitProcessor);
((FollowerRequestProcessor) firstProcessor).start();
syncProcessor = new SyncRequestProcessor(this,
new SendAckRequestProcessor((Learner)getFollower()));
syncProcessor.start();
}
FollowerZooKeeperServer.submitRequest
如果外部請求是涉及到數據變動的,則需要將請求轉給Leader。
它的流程是 FollowerRequestProcessor -> CommitProcessor -> FinalRequestProcessor
FollowerRequestProcessor.run():
-
執行方法 CommitProcessor.processRequest: 將這個原始的Request請求緩存到CommitProcessor裏的“queuedRequests”隊列裏;隨後被CommitProcessor.run()處理成“nextPending” :表示下一個準備要commit的數據。它是帶上了“ServerCnxn”信息的。
- 判定如果是 create、delete、setData等涉及到數據更改的操作,將數據轉發給leader,指令是
REQUEST
。
Leader對於每個follower有一個 LearnerHandler.run() 循環處理邏輯。
在末尾處的邏輯裏, 會判定收到follower的 REQUEST
請求時執行自己的LeaderZooKeeperServer.submitRequest ,只是這Request裏沒有“NIOServerCnxn”信息, 而由Leader直接從客戶端收到的請求是有的!
對於Leader分發下的
PROPOSAL
、COMMIT
指令的處理在: Follower.followLeader() -> .processPacket
PROPOSAL
: FollowerZooKeeperServer.logRequest- 依leader發送來的數據創建了一個Request對象,緩存在“pendingTxns”隊列裏, 標識待提交的提案。
- SyncRequestProcessor 將提案寫入到ZKDatabase的日誌文件FileTxnSnapLog裏 -> SendAckRequestProcessor 回
ACK
給leader.
-
COMMIT
:- FollowerZooKeeperServer.commit: 判定“pendingTxns”隊列裏第一個待提交的Request.zxid 就是本次需要提交的zxid, 則執行CommitProcessor.commit將Request扔到CommitProcessor 的 “committedRequests”待提交隊列。 否則
System.exit(12);
直接程序退出! -
CommitProcessor -> FinalRequestProcessor: 更新DateTree, 響應客戶端結果。
- FollowerZooKeeperServer.commit: 判定“pendingTxns”隊列裏第一個待提交的Request.zxid 就是本次需要提交的zxid, 則執行CommitProcessor.commit將Request扔到CommitProcessor 的 “committedRequests”待提交隊列。 否則
在 CommitProcessor.run()
的實現裏 :
當“nextPending” 和 "committedRequests" 裏的這個Request指代的是同一個客戶端發出的同一個數據時,“nextPending”的這個帶ServerCnxn內容的Request對象將被FinalRequestProcessor處理。
LeaderZooKeeperServer.submitRequest
它的流程是: PrepRequestProcessor -> ProposalRequestProcessor -> CommitProcessor -> Leader.ToBeAppliedRequestProcessor -> FinalRequestProcessor
- PrepRequestProcessor:根據不同的類型創建不同的請求對象,做一些前置校驗:ack、session、path等。
- 接着是 ProposalRequestProcessor :執行 Leader.propose 發起提案;
PROPOSAL
指令會寫到對應每個follower的LearnerHandler發送隊列裏, 異步線程會推送給follower。
LearnerHandler.run() 裏,在收到過半follower回的 ACK
後: Leader.processAck
- 發出
COMMIT
指令給每一個follower的LearnerHandler - CommitProcessor -> ToBeAppliedRequestProcessor -> FinalRequestProcessor
FinalRequestProcessor.processRequest
DataTree.processTxn
將request的數據應用到ZKDatabase的DateTree上-
ZKDatabase.addCommittedProposal
將request添入committedLog中 , 更新的maxCommittedLog = request.zxid - 如果請求信息裏有 ServerCnxn 內容 , 才response客戶端!
所以這裏leader是不會直接返回響應follower轉發過來的客戶端請求的。