文章目錄
引言
Zookeeper引入了Watcher機制來實現分佈式數據的發佈/訂閱功能,使得多個訂閱者可以同時監聽某一個主題對象,當主題對象自身狀態發生改變時,就會通知所有訂閱者。那麼Zookeeper是如何實現Watcher的呢?要了解其中的原理,那必然只能通過分析源碼才能明白。
正文
在分析源碼前,我們首先需要思考幾個問題:
- 如何註冊綁定監聽器?(哪些API可以綁定監聽器)
- 哪些操作可以觸發事件通知?
- 事件類型有哪些?
- Watcher可以被無限次觸發麼?爲什麼要這麼設計?
- 客戶端和服務端如何實現和管理Watcher?
一、如何註冊監聽
zookeeper只能通過以下幾個API註冊監聽器:
- 通過構造器註冊默認監聽事件(在連接成功後會觸發):
CountDownLatch countDownLatch = new CountDownLatch(1);
ZooKeeper zooKeeper = new ZooKeeper("192.168.0.106,192.168.0.108,192.168.0.109",
5000, new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("全局事件");
if (Event.KeeperState.SyncConnected.equals(event.getState())) {
countDownLatch.countDown();
}
}
});
countDownLatch.await();
- 通過exist()/getData()/getChildren()方法綁定事件,這三個方法可以綁定自定義watcher或者傳入true綁定默認的監聽器
// 傳入true則是再次綁定默認的監聽器,所有監聽器只會被觸發一次,除非再次綁定
Stat stat = zooKeeper.exists(NODE, true);
二、如何觸發監聽事件
Watcher註冊好後,我們要如何去觸發呢?也分爲下述兩種情況:
- 構造器綁定的默認監聽器會在連接成功後觸發,通常利用該方式來保證client端在操作節點時zookeeper已經連接成功,即上述實現
- 通過exists()/getData()/getChildren()綁定的事件,會監聽到相應節點的變化事件,即setData()/delete()/create()操作。
三、事件類型有哪些
Zookeeper包含如下事件類型:
EventType | 觸發條件 |
---|---|
NodeCreated | Watcher監聽的對應節點被創建 |
NodeDeleted | Watcher監聽的對應節點被刪除 |
NodeDataChanged | Watcher監聽的對應節點數據內容發生改變 |
NodeChildrenChanged | Watcher監聽的對應節點的子節點發生變更(增、刪、改) |
四、Watcher可以被無限次觸發麼?爲什麼要這麼設計?
通過調用API,我們不難發現,每次註冊綁定的Watcher都只會觸發一次,而不是一直存在;至於爲什麼這麼設計,也不難理解,如果Watcher一直存在,那麼當某些節點更新非常頻繁時,服務端就會不停地通知客戶端,使得服務端壓力非常的大,因此,如此設計是爲了緩解服務端的壓力。而對於需要一直保持監聽的節點我們只需要嵌套註冊監聽器即可。如下:
zooKeeper.exists(NODE, new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println(event.getType() + "->" + event.getPath());
try {
// 綁定的是自定義事件
zooKeeper.exists(event.getPath(), new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println(event.getType() + "->" + event.getPath());
}
});
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
五、Watcher實現原理
對於最後一個問題,我們就需要深入源碼中尋求答案了,那麼源碼如何去看呢?這裏我總結了幾個方法:
- 找准入口,這裏的入口就是Watcher的註冊
- 不要太關注細節,從宏觀角度把握整體流程,看源碼主要學的是設計思想和優秀的命名規範
- 不要過度依賴debug調試,在不清楚整體的流程架構時,調試只會讓我們陷入細節的陷阱中去(在關鍵步驟debug)
- 進入一個很長的實現方法時,先大體瀏覽下方法的實現,找到熟悉的步驟
- 判斷語句決定流程走向,緊跟我們當前的流程,非當前流程的直接跳過不看,避免暈車
- 大膽猜測。比如當一個流程突然斷掉,那極有可能就是一個異步處理或設計模式實現,根據變量名、方法名找到對應處理和接收的地方(優秀的源碼命名都是極爲規範和有規律的,後面我們就能看到Zookeeper在這方面的體現)。
- 最後一個就是一定熟練運用我的IDE快捷鍵和功能,可以節省很多時間。筆者使用的是IDEA,在後面的源碼分析中會分享幾個比較實用的快捷鍵。
下面就開始源碼分析之旅吧!(PS:Zookeeper版本爲3.4.8)
1. 客服端發送請求
a. 初始化客戶端並綁定Watcher
Zookeeper的構造器是註冊的是全局的默認Watcher。
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,
boolean canBeReadOnly)
throws IOException
{
LOG.info("Initiating client connection, connectString=" + connectString
+ " sessionTimeout=" + sessionTimeout + " watcher=" + watcher);
// 構造器傳入的Watcher會註冊爲全局默認的Watcher
watchManager.defaultWatcher = watcher;
ConnectStringParser connectStringParser = new ConnectStringParser(
connectString);
HostProvider hostProvider = new StaticHostProvider(
connectStringParser.getServerAddresses());
// 初始化ClientCnxn,並start啓動SendThread和EventThread兩個線程
cnxn = new ClientCnxn(connectStringParser.getChrootPath(),
hostProvider, sessionTimeout, this, watchManager,
getClientCnxnSocket(), canBeReadOnly);
cnxn.start();
}
public void start() {
sendThread.start();
eventThread.start();
}
上面就是構造器註冊監聽器的過程,需要注意的是getClientCnxnSocket方法,從方法名可以看出應該是獲取客戶端的通信對象:
private static ClientCnxnSocket getClientCnxnSocket() throws IOException {
// 獲取zoo.cfg配置
String clientCnxnSocketName = System
.getProperty(ZOOKEEPER_CLIENT_CNXN_SOCKET);
if (clientCnxnSocketName == null) {
clientCnxnSocketName = ClientCnxnSocketNIO.class.getName();
}
try {
return (ClientCnxnSocket) Class.forName(clientCnxnSocketName)
.newInstance();
} catch (Exception e) {
IOException ioe = new IOException("Couldn't instantiate "
+ clientCnxnSocketName);
ioe.initCause(e);
throw ioe;
}
}
從源碼中也可以看出確實是去初始化客戶端的通信連接對象,Zookeeper有兩個連接對象:ClientCnxnSocketNIO和ClientCnxnSocketNetty,默認是使用ClientCnxnSocketNIO,即默認使用NIO方式通信(PS:後者是3.5.1版本纔出現的,但是這段代碼在早期版本中已經存在,可以看到優秀代碼對於擴展性的考慮和設計)。
b. exists/getData/getChildren綁定Watcher以及發送請求
通過構造器註冊的監聽在連接成功觸發後就移除了(監聽器只會觸發一次,這裏就是監聽連接是否成功),因此後面需要監聽節點變化只能通過exists/getData/getChildren來綁定(defaultWatcher 這個對象還存在),這三個方法註冊監聽的流程都一樣,因此這裏就以exists方法來說明。
// 綁定默認監聽
Stat stat = zooKeeper.exists(NODE, true);
// 實現
public Stat exists(String path, boolean watch) throws KeeperException,
InterruptedException
{
// 這裏就可以看到如果爲true則使用構造器註冊的默認監聽,否則就不監聽節點變化
return exists(path, watch ? watchManager.defaultWatcher : null);
}
public Stat exists(final String path, Watcher watcher)
throws KeeperException, InterruptedException
{
final String clientPath = path;
PathUtils.validatePath(clientPath);
// 這裏實例化了一個ExistsWatcherRegistration,記住這個類,後面會用到
WatchRegistration wcb = null;
if (watcher != null) {
wcb = new ExistsWatchRegistration(watcher, clientPath);
}
final String serverPath = prependChroot(clientPath);
RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.exists);
ExistsRequest request = new ExistsRequest();
request.setPath(serverPath);
request.setWatch(watcher != null);
SetDataResponse response = new SetDataResponse();
// 將實例化後的Header、ExistsRequest、Response以及wcb打包發送,並等待服務端響應
ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
if (r.getErr() != 0) {
if (r.getErr() == KeeperException.Code.NONODE.intValue()) {
return null;
}
throw KeeperException.create(KeeperException.Code.get(r.getErr()),
clientPath);
}
return response.getStat().getCzxid() == -1 ? null : response.getStat();
}
ClientCnxn.submitRequest
public ReplyHeader submitRequest(RequestHeader h, Record request,
Record response, WatchRegistration watchRegistration)
throws InterruptedException {
ReplyHeader r = new ReplyHeader();
// 打包
Packet packet = queuePacket(h, r, request, response, null, null, null,
null, watchRegistration);
// 等待服務端響應
synchronized (packet) {
while (!packet.finished) {
packet.wait();
}
}
return r;
}
Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
Record response, AsyncCallback cb, String clientPath,
String serverPath, Object ctx, WatchRegistration watchRegistration)
{
Packet packet = null;
synchronized (outgoingQueue) {
// 封裝Packet對象後將其加入到outgoingQueue
packet = new Packet(h, r, request, response, watchRegistration);
packet.cb = cb;
packet.ctx = ctx;
packet.clientPath = clientPath;
packet.serverPath = serverPath;
if (!state.isAlive() || closing) {
conLossPacket(packet);
} else {
if (h.getType() == OpCode.closeSession) {
closing = true;
}
outgoingQueue.add(packet);
}
}
sendThread.getClientCnxnSocket().wakeupCnxn();
return packet;
}
流程到這裏就斷了,但是我們注意到將要發送的包放入到了一個類型爲LinkedList的outgoingQueue發送隊列,既然產生了隊列,那麼說明肯定存在其它線程來異步消費該隊列(Zookeeper中大量運用了異步處理,值得我們學習借鑑),還記得構造器中啓動的兩個線程麼(SendThread和EventThread)?
既然是線程,那麼我們不用看其它的,直接找到run方法即可(在SendThread類中按CTRL + F12顯示所有的方法和字段):
while (state.isAlive()) {
try {
// 傳輸邏輯主要在這兒,還記得clientCnxnSocket是在什麼時候實例化的麼?
clientCnxnSocket.doTransport(to, pendingQueue, outgoingQueue, ClientCnxn.this);
}
}
代碼很長,這裏我只截取了關鍵代碼,其中有很多判斷,細看一定會看的很懵逼,因此一定要先瀏覽整段代碼,然後我們會發現clientCnxnSocket.doTransport(to, pendingQueue, outgoingQueue, ClientCnxn.this); 這樣一段代碼,通過方法名我們就能大致就能猜測到主要的傳輸邏輯就在這個方法中,這時可以在大致看看前面的判斷語句中都做了一些什麼事情,主要是看看會不會對傳輸造成大的影響,這裏我們會發現就是判斷是否連接成功,對我們的流程沒有什麼影響,那麼按住啊CTRL + 鼠標左鍵直接進入到doTransport方法中:
abstract void doTransport(int waitTimeOut, List<Packet> pendingQueue,
LinkedList<Packet> outgoingQueue, ClientCnxn cnxn)
throws IOException, InterruptedException;
我們看到是一個抽象的方法,那麼可以直接點擊如下圖中的按鈕或者返回調用處按住CTRL + ALT + 鼠標左鍵進入具體的實現。
因爲筆者使用的版本只有ClientCnxnSocketNIO一種實現方式,因此會直接進入到具體實現方法中,否則在高版本中加入了ClientCnxnSocketNetty後會彈出選擇框(那我們怎麼知道使用的是哪個類呢?還記得構造器中的getClientCnxnSocket方法麼,忘了就回去看看!)。
ClientCnxnSocketNIO.doTranport()
void doTransport(int waitTimeOut, List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue,
ClientCnxn cnxn)
throws IOException, InterruptedException {
selector.select(waitTimeOut);
Set<SelectionKey> selected;
synchronized (this) {
selected = selector.selectedKeys();
}
updateNow();
for (SelectionKey k : selected) {
SocketChannel sc = ((SocketChannel) k.channel());
if ((k.readyOps() & SelectionKey.OP_CONNECT) != 0) {
if (sc.finishConnect()) {
updateLastSendAndHeard();
sendThread.primeConnection();
}
} else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
doIO(pendingQueue, outgoingQueue, cnxn);
}
}
if (sendThread.getZkState().isConnected()) {
synchronized(outgoingQueue) {
if (findSendablePacket(outgoingQueue,
cnxn.sendThread.clientTunneledAuthenticationInProgress()) != null) {
enableWrite();
}
}
}
selected.clear();
}
該方法中主要是基於NIO多路複用機制對連接狀態的判斷,不難發現最主要的邏輯在doIO中:
void doIO(List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue, ClientCnxn cnxn)
throws InterruptedException, IOException {
// 讀取服務端的響應
........
// 向服務端傳送消息
if (sockKey.isWritable()) {
synchronized(outgoingQueue) {
Packet p = findSendablePacket(outgoingQueue,
cnxn.sendThread.clientTunneledAuthenticationInProgress());
if (p != null) {
updateLastSend();
// If we already started writing p, p.bb will already exist
if (p.bb == null) {
if ((p.requestHeader != null) &&
(p.requestHeader.getType() != OpCode.ping) &&
(p.requestHeader.getType() != OpCode.auth)) {
p.requestHeader.setXid(cnxn.getXid());
}
// 序列化Packet
p.createBB();
}
// 向服務端發送Packet
sock.write(p.bb);
// 發送完成後,從發送隊列移除該Packet並將其加入到pendingQueue等待服務器的響應
if (!p.bb.hasRemaining()) {
sentCount++;
outgoingQueue.removeFirstOccurrence(p);
if (p.requestHeader != null
&& p.requestHeader.getType() != OpCode.ping
&& p.requestHeader.getType() != OpCode.auth) {
synchronized (pendingQueue) {
pendingQueue.add(p);
}
}
}
}
}
}
}
該方法中如同方法名就是做IO操作,即發送和接收數據包,那我們現在是向服務端發送數據包,所以只需要看socket.isWritable流程即可。在前面說了這是一個異步消費outgoingQueue的過程,因此需要從發送隊列中挨個取出Packet,並序列化後發送給服務端,同樣,在發送完成後,我們不可能阻塞等待服務端的響應,因此將Packet放入pendingQueue等待隊列,這樣大大提高客戶端處理請求的能力。
這樣,請求就發送完成了,但是我們還需要關注客戶端向服務端都發送了哪些數據,點開createBB就知道了:
public void createBB() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BinaryOutputArchive boa = BinaryOutputArchive.getArchive(baos);
boa.writeInt(-1, "len"); // We'll fill this in later
if (requestHeader != null) {
requestHeader.serialize(boa, "header");
}
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);
}
}
可以看到,僅僅只是序列化了Header和Request,這些信息裏包含了哪些數據還記得嗎?
RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.exists);
ExistsRequest request = new ExistsRequest();
request.setPath(serverPath);
request.setWatch(watcher != null);
在開始的exists方法中可以看到,header中是當前的操作類型,request中是綁定節點信息和是否設置監聽的標識,也就是說這裏並沒有將Watcher傳遞給服務端,只是傳遞了一個flag,告訴服務端“我”要監聽該節點的變化,所以,從這裏我們就能看到客戶端和服務端是分別管理Watcher的,但是客戶端會在何時註冊監聽呢?當然需要等到服務端響應成功後註冊了,不難明白,如果客戶端都還沒有接收到服務端的響應,那怎麼能確保服務端一定是操作成功了呢,所以,客戶端的監聽器一定是在服務端響應後去註冊。
2. 服務端處理請求並響應
a. 讀取請求報文及反序列化
在看分析服務端源碼之前,我們首先需要解決的一個問題,從何入手?客戶端的入口很好找,但是我們和服務端是沒有直接交互的,那該怎麼辦呢?這時候就需要我們大膽聯想猜測了。在客戶端我們是通過ClientCnxnSocketNIO來進行傳輸的,那麼服務端肯定存在對應的一個類來響應請求。代碼都是人寫的,因此肯定在命名上就會有一定的規範和聯繫,優秀的代碼更是如此。所以,可以通過IDEA快捷鍵CTRL + N搜索關鍵字ServerCnxn,然後你就會發現NIOServerCnxn,無需多想,服務端肯定是通過這個類來處理請求的。找到類之後我們還需要猜測處理請求的方法,同樣,在客戶端是通過doTransport和doIO方法來處理請求,那麼服務端應該有對應的方法,或者帶有response關鍵字的方法,打開類的方法結構,果然看到doIO方法:
NIOServerCnxn.doIO()
void doIO(SelectionKey k) throws InterruptedException {
try {
// 讀取輸入流
if (k.isReadable()) {
int rc = sock.read(incomingBuffer);
if (rc < 0) {
throw new EndOfStreamException(
"Unable to read additional data from client sessionid 0x"
+ Long.toHexString(sessionId)
+ ", likely client has closed socket");
}
if (incomingBuffer.remaining() == 0) {
boolean isPayload;
if (incomingBuffer == lenBuffer) { // start of next request
incomingBuffer.flip();
isPayload = readLength(k);
incomingBuffer.clear();
} else {
// continuation
isPayload = true;
}
if (isPayload) { // not the case for 4letterword
// 讀取報文
readPayload();
}
else {
// four letter words take care
// need not do anything else
return;
}
}
}
} catch (CancelledKeyException e) {
LOG.warn("Exception causing close of session 0x"
+ Long.toHexString(sessionId)
+ " due to " + e);
if (LOG.isDebugEnabled()) {
LOG.debug("CancelledKeyException stack trace", e);
}
close();
} catch (CloseRequestException e) {
// expecting close to log session closure
close();
} catch (EndOfStreamException e) {
LOG.warn("caught end of stream exception",e); // tell user why
// expecting close to log session closure
close();
} catch (IOException e) {
LOG.warn("Exception causing close of session 0x"
+ Long.toHexString(sessionId)
+ " due to " + e);
if (LOG.isDebugEnabled()) {
LOG.debug("IOException stack trace", e);
}
close();
}
}
對應客服端,這裏同樣有read和write兩個流程,因爲是接收處理請求,所以這裏肯定是走read流程,因此會進入到readPayload方法:
private void readPayload() throws IOException, InterruptedException {
if (incomingBuffer.remaining() != 0) { // have we read length bytes?
int rc = sock.read(incomingBuffer); // sock is non-blocking, so ok
if (rc < 0) {
throw new EndOfStreamException(
"Unable to read additional data from client sessionid 0x"
+ Long.toHexString(sessionId)
+ ", likely client has closed socket");
}
}
if (incomingBuffer.remaining() == 0) { // have we read length bytes?
packetReceived();
incomingBuffer.flip();
if (!initialized) {
// 連接請求
readConnectRequest();
} else {
// 其它請求
readRequest();
}
lenBuffer.clear();
incomingBuffer = lenBuffer;
}
}
目前我們是一個ExistsRequest,所以會調用readRequest方法:
private void readRequest() throws IOException {
zkServer.processPacket(this, incomingBuffer);
}
很簡單,就是通過server去反序列化Packet並處理。
Zookeeper.processPacket反序列化Header並提交服務端請求
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();
// 反序列化header,還記得客戶端header中存儲的是什麼吧
h.deserialize(bia, "header");
incomingBuffer = incomingBuffer.slice();
// 判斷當前的請求類型是不是auth
if (h.getType() == OpCode.auth) {
LOG.info("got auth packet " + cnxn.getRemoteSocketAddress());
AuthPacket authPacket = new AuthPacket();
ByteBufferInputStream.byteBuffer2Record(incomingBuffer, authPacket);
String scheme = authPacket.getScheme();
AuthenticationProvider ap = ProviderRegistry.getProvider(scheme);
Code authReturn = KeeperException.Code.AUTHFAILED;
if(ap != null) {
try {
authReturn = ap.handleAuthentication(cnxn, authPacket.getAuth());
} catch(RuntimeException e) {
LOG.warn("Caught runtime exception from AuthenticationProvider: " + scheme + " due to " + e);
authReturn = KeeperException.Code.AUTHFAILED;
}
}
if (authReturn!= KeeperException.Code.OK) {
if (ap == null) {
LOG.warn("No authentication provider for scheme: "
+ scheme + " has "
+ ProviderRegistry.listProviders());
} else {
LOG.warn("Authentication failed for scheme: " + scheme);
}
// send a response...
ReplyHeader rh = new ReplyHeader(h.getXid(), 0,
KeeperException.Code.AUTHFAILED.intValue());
cnxn.sendResponse(rh, null, null);
// ... and close connection
cnxn.sendBuffer(ServerCnxnFactory.closeConn);
cnxn.disableRecv();
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Authentication succeeded for scheme: "
+ scheme);
}
LOG.info("auth success " + cnxn.getRemoteSocketAddress());
ReplyHeader rh = new ReplyHeader(h.getXid(), 0,
KeeperException.Code.OK.intValue());
cnxn.sendResponse(rh, null, null);
}
return;
} else {
// 判斷當前請求類型是不是sasl
if (h.getType() == OpCode.sasl) {
Record rsp = processSasl(incomingBuffer,cnxn);
ReplyHeader rh = new ReplyHeader(h.getXid(), 0, KeeperException.Code.OK.intValue());
cnxn.sendResponse(rh,rsp, "response"); // not sure about 3rd arg..what is it?
}
else {
// 最終會進入到該分支,封裝服務端請求對象並提交
Request si = new Request(cnxn, cnxn.getSessionId(), h.getXid(),
h.getType(), incomingBuffer, cnxn.getAuthInfo());
si.setOwner(ServerCnxn.me);
submitRequest(si);
}
}
cnxn.incrOutstandingRequests(h);
}
b. Processor
反序列化完成後會封裝request並提交,而在submitRequest方法中我們可以看到關鍵的一段代碼firstProcessor.processRequest(si);,這個firstProcessor是什麼東西?通過快捷鍵跳轉到具體實現代碼時,提示中會出現很多個Processor實現類,我們該進入哪一個?
這個時候不要慌,要進入哪一個類肯定要看初始化,通過快捷鍵Alt + F7顯示所有使用了該變量的地方:
我們可以看到在當前類中有一個setupRequestProcessors方法,毫無疑問,點過去看就是:
protected void setupRequestProcessors() {
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
RequestProcessor syncProcessor = new SyncRequestProcessor(this,
finalProcessor);
((SyncRequestProcessor)syncProcessor).start();
firstProcessor = new PrepRequestProcessor(this, syncProcessor);
// 看到start,我們應該想到這應該又是通過線程來實現異步處理
((PrepRequestProcessor)firstProcessor).start();
}
可以看到這裏是通過責任鏈模式實現了一個鏈式處理流程,按照順序Request會分別被PrepRequestProcessor、SyncRequestProcessor、FinalRequestProcessor處理,這個時候彆着急去看方法實現做了些什麼事,而是應該先去看看這三個類的註釋,大概瞭解一下這三個類的職能和作用。
- PrepRequestProcessor:請求預處理器,繼承了線程,所有請求首先通過該處理器,它能識別出事務請求,並對其進行一系列預處理(比如版本校驗)。
- SyncRequestProcessor:事務請求同步處理器,繼承了線程。它將事務請求和事務日誌同步記錄到磁盤,並且在同步操作未完成時,不會進入到下一個處理器。同時該處理器在不同的角色中所做的操作不同:
- 當前服務器爲leader:同步事務請求到磁盤
- 當前服務器爲follower:同步事務請求到磁盤,並將請求轉發給leader
- 當前服務器爲observer:observer只會接收到來自於leader的同步請求(在之前的文章有說過,observer不會處理客戶端的事務請求,只有leader執行事務請求成功後會將數據同步到observer),因此,只需要提交事務請求即可,也不需要返回響應給客戶端。
- FinalRequestProcessor:所有處理器鏈最終都會調用的處理器,該處理器未繼承線程,並且是同步處理請求。負責將所有已經提交的事務請求寫入到本機,以及對於讀請求,將本機數據返回給client。
大概瞭解了各處理器的職能後,我們再來分析具體的源碼,首先是PrepRequestProcessor的processRequest方法:
// LinkedBlockingQueue<Request> submittedRequests = new LinkedBlockingQueue<Request>();
public void processRequest(Request request) {
submittedRequests.add(request);
}
很簡單,就是將request加入到隊列中,等待線程異步處理,那麼我直接找到該類的run方法。run方法中調用了pRequest方法處理request,這個方法很長,瀏覽整個方法,發現是根據當前的操作類型來處理request,那我們直接找到exists操作的流程:
@Override
public void run() {
pRequest(request);
}
protected void pRequest(Request request) throws RequestProcessorException {
switch (request.type) {
case OpCode.sync:
case OpCode.exists:
case OpCode.getData:
case OpCode.getACL:
case OpCode.getChildren:
case OpCode.getChildren2:
case OpCode.ping:
case OpCode.setWatches:
zks.sessionTracker.checkSession(request.sessionId,
request.getOwner());
break;
}
// 調用下一個processor的方法
nextProcessor.processRequest(request);
}
可以看到這裏主要是對request進行校驗,詳細的校驗內容跟我們當前主流程關係不大,不用關心。所以直接翻到方法最後看到是調用了nextProcessor.processRequest方法,nextProcessor是什麼,剛剛初始化的時候我們已經看到了,所以直接進入相應類的方法中:
public void processRequest(Request request) {
// request.addRQRec(">sync");
queuedRequests.add(request);
}
同樣的也是放入到了一個隊列中,所以直接看run方法:
// 將要被寫入到磁盤上的request隊列
private final LinkedList<Request> toFlush = new LinkedList<Request>();
public void run() {
while (true) {
Request si = null;
if (toFlush.isEmpty()) {
si = queuedRequests.take();
} else {
si = c.poll();
if (si == null) {
// queuedRequests隊列中沒有request了就提交toFlush隊列中的事務,並掉用下一個processor
flush(toFlush);
continue;
}
}
if (si == requestOfDeath) {
break;
}
if (si != null) {
// 記錄事務日誌成功走這裏
if (zks.getZKDatabase().append(si)) {
logCount++;
if (logCount > (snapCount / 2 + randRoll)) {
randRoll = r.nextInt(snapCount/2);
// roll the log
zks.getZKDatabase().rollLog();
// take a snapshot
if (snapInProcess != null && snapInProcess.isAlive()) {
LOG.warn("Too busy to snap, skipping");
} else {
snapInProcess = new ZooKeeperThread("Snapshot Thread") {
public void run() {
try {
zks.takeSnapshot();
} catch(Exception e) {
LOG.warn("Unexpected exception", e);
}
}
};
snapInProcess.start();
}
logCount = 0;
}
} else if (toFlush.isEmpty()) {
// 事務日誌記錄失敗並且toFlush中沒有request,那麼直接調用下一個processor提高效率
if (nextProcessor != null) {
nextProcessor.processRequest(si);
if (nextProcessor instanceof Flushable) {
((Flushable)nextProcessor).flush();
}
}
continue;
}
// 事務日誌記錄完成後將request放入到toflush
toFlush.add(si);
if (toFlush.size() > 1000) {
flush(toFlush);
}
}
}
}
記錄事務日誌的細節我們不用太過關注,主要了解其處理流程以及設計思想,不要被細節搞暈了。這裏處理完成後,就到了我們最後一個processor(FinalRequestProcessor)中了,代碼很長,直接看關鍵代碼:
Record rsp = null;
ServerCnxn cnxn = request.cnxn;
case OpCode.exists: {
lastOp = "EXIS";
// 反序列化request
ExistsRequest existsRequest = new ExistsRequest();
ByteBufferInputStream.byteBuffer2Record(request.request,
existsRequest);
String path = existsRequest.getPath();
if (path.indexOf('\0') != -1) {
throw new KeeperException.BadArgumentsException();
}
// 這裏非常關鍵,判斷客服端是否設置watcher,如有則服務端也對該path節點
// 添加watcher(cnxn本身就是一個watcher子類)
Stat stat = zks.getZKDatabase().statNode(path, existsRequest
.getWatch() ? cnxn : null);
// 封裝ExistsResponse響應信息
rsp = new ExistsResponse(stat);
break;
}
// 生成header
long lastZxid = zks.getZKDatabase().getDataTreeLastProcessedZxid();
ReplyHeader hdr =
new ReplyHeader(request.cxid, lastZxid, err.intValue());
// 發送響應信息
cnxn.sendResponse(hdr, rsp, "response");
終於找到關鍵性的代碼,exists操作會去添加監聽,下面我們就詳細看看服務端註冊監聽的流程。
c. 服務端註冊監聽器
點擊zks.getZKDatabase().statNode(path, existsRequest.getWatch() ? cnxn : null)方法進入,最終進入DataTree的statNode方法(該對象就是Zookeeper數據結構對象):
// 注意這裏的watcher是由ServerCnxn向上轉型得到的
public Stat statNode(String path, Watcher watcher)
throws KeeperException.NoNodeException {
Stat stat = new Stat();
DataNode n = nodes.get(path);
if (watcher != null) {
// 調用WatcherManager的方法添加監聽
dataWatches.addWatch(path, watcher);
}
if (n == null) {
throw new KeeperException.NoNodeException();
}
synchronized (n) {
n.copyStat(stat);
return stat;
}
}
主要邏輯在addWatch中(還記得客戶端的WatcherManager麼?這裏是服務端管理Watcher的類,再次說明客戶端服務端是分開管理Watcher的):
private final HashMap<String, HashSet<Watcher>> watchTable =
new HashMap<String, HashSet<Watcher>>();
private final HashMap<Watcher, HashSet<String>> watch2Paths =
new HashMap<Watcher, HashSet<String>>();
public synchronized void addWatch(String path, Watcher watcher) {
// 從節點角度管理watcher,一個節點可能會對應多個watcher
HashSet<Watcher> list = watchTable.get(path);
if (list == null) {
// 第一次給該節點添加監聽初始化。這個細節值得學習,內存是昂貴的,
// 從實際角度出發,一般一個節點對應的watcher並不會特別多,因此初始化
// 容量設定4是一個非常好的節省資源和平衡性能的折衷方案
list = new HashSet<Watcher>(4);
watchTable.put(path, list);
}
list.add(watcher);
// 從watcher角度管理path,一個watcher可能會監聽多個path
HashSet<String> paths = watch2Paths.get(watcher);
if (paths == null) {
// 第一次添加初始化
paths = new HashSet<String>();
watch2Paths.put(watcher, paths);
}
paths.add(path);
}
至此,服務端監聽註冊完成,然後就會調用下面的方法發送響應信息給客戶端:
cnxn.sendResponse(hdr, rsp, "response");
3. 客戶端處理服務器響應信息
a. 客戶端讀取響應流
ClientCnxnSocketNIO.doIO
一開始在分析客戶端發送請求時,我們看到是通過ClientCnxnSocketNIO.doIO方法傳輸的,應該還記得當時read讀取流的部分我們是跳過的,這就是讀取服務端的響應信息:
if (sockKey.isReadable()) {
int rc = sock.read(incomingBuffer);
// incomingBuffer已經被讀取完
if (!incomingBuffer.hasRemaining()) {
incomingBuffer.flip();
if (incomingBuffer == lenBuffer) {
recvCount++;
readLength();
} else if (!initialized) {
readConnectResult();
enableRead();
if (findSendablePacket(outgoingQueue,
cnxn.sendThread.clientTunneledAuthenticationInProgress()) != null) {
// Since SASL authentication has completed (if client is configured to do so),
// outgoing packets waiting in the outgoingQueue can now be sent.
enableWrite();
}
lenBuffer.clear();
incomingBuffer = lenBuffer;
updateLastHeard();
initialized = true;
} else {
// 主要邏輯在這兒,通過發送線程去讀取響應信息
sendThread.readResponse(incomingBuffer);
lenBuffer.clear();
incomingBuffer = lenBuffer;
updateLastHeard();
}
}
}
SendThread.readResponse
void readResponse(ByteBuffer incomingBuffer) throws IOException {
ByteBufferInputStream bbis = new ByteBufferInputStream(
incomingBuffer);
BinaryInputArchive bbia = BinaryInputArchive.getArchive(bbis);
ReplyHeader replyHdr = new ReplyHeader();
replyHdr.deserialize(bbia, "header");
// pendingQueue中取出Packet
Packet packet;
synchronized (pendingQueue) {
if (pendingQueue.size() == 0) {
throw new IOException("Nothing in the queue, but got "
+ replyHdr.getXid());
}
packet = pendingQueue.remove();
}
try {
// 將Header設置到Packet
packet.replyHeader.setXid(replyHdr.getXid());
packet.replyHeader.setErr(replyHdr.getErr());
packet.replyHeader.setZxid(replyHdr.getZxid());
if (replyHdr.getZxid() > 0) {
lastZxid = replyHdr.getZxid();
}
// 反序列化response到Packet
if (packet.response != null && replyHdr.getErr() == 0) {
packet.response.deserialize(bbia, "response");
}
} finally {
// 客戶端處理響應信息
finishPacket(packet);
}
}
readResponse中主要做了以下幾件事:
- 反序列化ReplyHeader
- 根據header中的xid判斷當前服務端的響應類型(-2:Ping類型;-4:權限相關的響應;-1:事件通知。這些類型跟我們目前的流程都不相關,可以先忽略)
- 從pendingQueue中取出客戶端之前存放的Packet,並將反序列化後的response以及header設置到Packet
- 調用finishPacket方法完成對數據包的處理(客戶端註冊監聽的邏輯就在該方法中)。
b. 客戶端註冊監聽器
finishPacket
private void finishPacket(Packet p) {
// 註冊監聽器
if (p.watchRegistration != null) {
// watchRegistration是exists方法實例化的ExistsWatchRegistration對象
p.watchRegistration.register(p.replyHeader.getErr());
}
if (p.cb == null) {
// 未設置回調錶示是同步調用接口,不需要異步回調,因此直接喚醒等待響應的Packet線程
synchronized (p) {
p.finished = true;
p.notifyAll();
}
} else {
// 否則放入到EventThread的waitingEvents中
p.finished = true;
eventThread.queuePacket(p);
}
}
這裏我們主要看看監聽器的註冊流程,其它的就是返回客戶端結果。
WatchRegistration註冊流程
public void register(int rc) {
if (shouldAddWatch(rc)) {
// 模板方法模式獲取監聽器
Map<String, Set<Watcher>> watches = getWatches(rc);
synchronized(watches) {
// 根據路徑獲取Watcher集合,如果是第一個監聽器則初始化
Set<Watcher> watchers = watches.get(clientPath);
if (watchers == null) {
watchers = new HashSet<Watcher>();
watches.put(clientPath, watchers);
}
// 將watcher添加到集合中即完成Watcher的註冊
watchers.add(watcher);
}
}
}
因爲一開始我們初始化的是ExistsWatchRegistration對象,所以getWatches是調用的該對象的實例方法並返回存儲Watcher的集合:
protected Map<String, Set<Watcher>> getWatches(int rc) {
// 這裏不用想,肯定是返回的watchManager.existWatches
return rc == 0 ? watchManager.dataWatches : watchManager.existWatches;
}
下面這個三個集合是ZKWatchManager存儲watcher的map集合,分別對應三種註冊監聽的事件:
// getData
private final Map<String, Set<Watcher>> dataWatches =
new HashMap<String, Set<Watcher>>();
// exists
private final Map<String, Set<Watcher>> existWatches =
new HashMap<String, Set<Watcher>>();
// getChildren
private final Map<String, Set<Watcher>> childWatches =
new HashMap<String, Set<Watcher>>();
至此,客戶端註冊監聽的流程就完成了。總而言之,客戶端當通過構造器或者exists/getData/getChildren三個方法註冊監聽時,首先會通知服務端,得到服務端的成功響應時,客戶端再將Watcher註冊存儲起來等待對應事件的觸發。下面我們就來看看事件的觸發機制。
4. 事件觸發流程
事件的註冊是通過非事務型的方法實現,即訂閱服務端節點變化,而節點狀態只能通過事務型方法去改變,因此事件只能通過create/setData/delete觸發(事件綁定時的響應不算),這裏通過setData方法來說明。
a. 服務端響應setData類型操作
PrepRequestProcessor.pRequest
客戶端和服務端交互的過程和前面講的都是樣的,這裏就不再累述了,直接找到PrepRequestProcessor.pRequest方法中的關鍵位置(在上面的流程中是到這個方法後纔會判斷操作的類型):
case OpCode.setData:
SetDataRequest setDataRequest = new SetDataRequest();
pRequest2Txn(request.type, zks.getNextZxid(), request, setDataRequest, true);
break;
封裝了SetDataRequest對象並掉用pRequest2Txn方法,這個方法中也是根據操作類型來進入對應的流程:
case OpCode.setData:
zks.sessionTracker.checkSession(request.sessionId, request.getOwner());
SetDataRequest setDataRequest = (SetDataRequest)record;
if(deserialize)
ByteBufferInputStream.byteBuffer2Record(request.request, setDataRequest);
path = setDataRequest.getPath();
validatePath(path, request.sessionId);
// 根據path獲取node信息,先從outstandingChangesForPath獲取,未獲取到再從服務器中獲取
nodeRecord = getRecordForPath(path);
checkACL(zks, nodeRecord.acl, ZooDefs.Perms.WRITE,
request.authInfo);
version = setDataRequest.getVersion();
int currentVersion = nodeRecord.stat.getVersion();
if (version != -1 && version != currentVersion) {
throw new KeeperException.BadVersionException(path);
}
version = currentVersion + 1;
request.txn = new SetDataTxn(path, setDataRequest.getData(), version);
// 拷貝新的對象
nodeRecord = nodeRecord.duplicate(request.hdr.getZxid());
nodeRecord.stat.setVersion(version);
// 將變化的節點存到outstandingChanges和outstandingChangesForPath中
addChangeRecord(nodeRecord);
break;
FinalRequestProcessor
然後是調用下一個processor處理器,而SyncRequestProcessor中沒有流程分支,不必分析,因此直接進入到FinalRequestProcessor的run方法:
case OpCode.setData: {
lastOp = "SETD";
rsp = new SetDataResponse(rc.stat);
err = Code.get(rc.err);
break;
}
同開始exists操作的流程一樣,setData操作也會有對應的分支,但在這裏面只是封裝了響應對象就發送給客戶端了,沒有其它的邏輯,那說明我們遺漏了什麼,就需要返回網上看看了。我們注意到這裏使用到了rc.stat,直接點過去,我們看到一個熟悉的東西outstandingChanges,這不是剛剛還在PrepRequestProcessor中添加了變化節點信息進去的麼,由於exists操作並沒有操作該變量,所以我們一開始就忽略了這一步,那現在不用多想,我們想要的邏輯肯定是在這個步驟裏面是實現的:
ProcessTxnResult rc = null;
synchronized (zks.outstandingChanges) {
// 確保request的zxid和最近變化的node的zxid是一致的
while (!zks.outstandingChanges.isEmpty()
&& zks.outstandingChanges.get(0).zxid <= request.zxid) {
ChangeRecord cr = zks.outstandingChanges.remove(0);
if (cr.zxid < request.zxid) {
LOG.warn("Zxid outstanding "
+ cr.zxid
+ " is less than current " + request.zxid);
}
if (zks.outstandingChangesForPath.get(cr.path) == cr) {
zks.outstandingChangesForPath.remove(cr.path);
}
}
if (request.hdr != null) {
TxnHeader hdr = request.hdr;
Record txn = request.txn;
// 實際處理事務請求的方法
rc = zks.processTxn(hdr, txn);
}
// do not add non quorum packets to the queue.
if (Request.isQuorum(request.type)) {
zks.getZKDatabase().addCommittedProposal(request);
}
}
DataTree.processTxn
追蹤processTxn方法,最終會進入到DataTree.setData方法中,之前說過,該對象就是服務器的數據結構對象,如果你足夠敏感,那麼在一開始分析setData方法時,是可以直接定位這裏的。
public Stat setData(String path, byte data[], int version, long zxid,
long time) throws KeeperException.NoNodeException {
Stat s = new Stat();
DataNode n = nodes.get(path);
if (n == null) {
throw new KeeperException.NoNodeException();
}
byte lastdata[] = null;
synchronized (n) {
lastdata = n.data;
n.data = data;
n.stat.setMtime(time);
n.stat.setMzxid(zxid);
n.stat.setVersion(version);
n.copyStat(s);
}
// now update if the path is in a quota subtree.
String lastPrefix;
if((lastPrefix = getMaxPrefixWithQuota(path)) != null) {
this.updateBytes(lastPrefix, (data == null ? 0 : data.length)
- (lastdata == null ? 0 : lastdata.length));
}
// 觸發監聽器,類型爲NodeDataChanged
dataWatches.triggerWatch(path, EventType.NodeDataChanged);
return s;
}
b. 服務端監聽器的觸發
WatcherManager.triggerWatch
服務端節點信息改變會就調用WatcherManager的triggerWatch方法去觸發監聽器:
public Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {
// 根據事件類型、連接狀態、節點path創建WatchedEvent
WatchedEvent e = new WatchedEvent(type,
KeeperState.SyncConnected, path);
HashSet<Watcher> watchers;
synchronized (this) {
// 取出並移除path對應的所有watcher
watchers = watchTable.remove(path);
if (watchers == null || watchers.isEmpty()) {
return null;
}
// 移除watcher對應的所有path
for (Watcher w : watchers) {
HashSet<String> paths = watch2Paths.get(w);
if (paths != null) {
paths.remove(path);
}
}
}
for (Watcher w : watchers) {
if (supress != null && supress.contains(w)) {
continue;
}
// 這裏纔是響應客戶端的關鍵
w.process(e);
}
return watchers;
}
w.process(e)
這是Watcher的一個模板方法,是做什麼呢?我們自己實現的監聽器就要實現該方法,即事件觸發時調用和接收通知的方法,但是這裏是服務端,客戶端和服務端是分別管理Watcher的,所以這裏不可能是直接調用我們實現的方法,那應該是調用哪一個類呢?
想不起來就返回去看看服務端註冊Watcher的流程吧,那時是將ServerCnxn對象向上轉型存入到Watcher集合中的,而我們這裏又是通過子類NIOServerCnxn來傳輸的,因此,這裏就是調用該類的process方法:
synchronized public void process(WatchedEvent event) {
// 構建header,注意這裏的xid = -1
ReplyHeader h = new ReplyHeader(-1, -1L, 0);
// 構建WatcherEvent對象
WatcherEvent e = event.getWrapper();
// 將header、event以及tag=notification序列化傳送到客戶端
sendResponse(h, e, "notification");
}
至此,服務端事件通知就完成了,最後就是客戶端如何處理事件通知了。
c. 客戶端監聽器的觸發
SendThread.readResponse
客戶端接收響應信息肯定也是同之前一樣,所以直接定位到SendThread.readResponse中,在之前的客戶端接收服務端響應的流程中我講過在這個方法中會根據當前header的xid判斷進行什麼樣的操作,剛剛我們看到了服務端設置了header.xid=-1,因此,這就是事件觸發要走的流程:
if (replyHdr.getXid() == -1) {
// 反序列化event
WatcherEvent event = new WatcherEvent();
event.deserialize(bbia, "response");
// 構建客戶端WatchedEvent 對象並加入到EventThread的waitingEvents中等待觸發
WatchedEvent we = new WatchedEvent(event);
eventThread.queueEvent( we );
return;
}
EventThread
public void queueEvent(WatchedEvent event) {
if (event.getType() == EventType.None
&& sessionState == event.getState()) {
return;
}
sessionState = event.getState();
// 構建WatcherSetEventPair對象並加入到隊列中
WatcherSetEventPair pair = new WatcherSetEventPair(
watcher.materialize(event.getState(), event.getType(),
event.getPath()),
event);
// queue the pair (watch set & event) for later processing
waitingEvents.add(pair);
}
從這裏我們可以看到事件觸發對象爲WatcherSetEventPair,再看processEvent方法(run方法調用):
private void processEvent(Object event) {
if (event instanceof WatcherSetEventPair) {
WatcherSetEventPair pair = (WatcherSetEventPair) event;
// 這裏的Watcher就是剛剛queueEvent方法中初始化WatcherSetEventPair設置的watcher集合,即我們自定義的Watcher
for (Watcher watcher : pair.watchers) {
try {
// 這裏就是客戶端最終去調用我們自己實現的Watcher的process方法
watcher.process(pair.event);
} catch (Throwable t) {
LOG.error("Error while calling watcher ", t);
}
}
}
}
WatcherSetEventPair中watcher就是我們自定義的Watcher,它們是怎麼關聯上的呢?返回剛剛queueEvent方法實例化這個對象的地方:
WatcherSetEventPair pair = new WatcherSetEventPair(
watcher.materialize(event.getState(), event.getType(),
event.getPath()),
event);
其中第一個參數就是watcher集合,看看materialize這個方法:
public Set<Watcher> materialize(Watcher.Event.KeeperState state,
Watcher.Event.EventType type,
String clientPath)
{
Set<Watcher> result = new HashSet<Watcher>();
switch (type) {
case NodeDataChanged:
case NodeCreated:
synchronized (dataWatches) {
addTo(dataWatches.remove(clientPath), result);
}
synchronized (existWatches) {
addTo(existWatches.remove(clientPath), result);
}
break;
return result;
}
很簡單,就是根據當前的事件類型返回對應的watcher,當前我們是NodeDataChanged事件,因此,返回dataWatches和existWatches中的內容。這兩個集合肯定不陌生吧,就是客戶端註冊監聽時存儲監聽器的集合。
結語
本篇文章到這裏終於結束了,個人在這篇文章上也花費了很多的時間和精力。但確實收穫頗豐:學到了異步處理的思路,代碼架構的設計以及閱讀源碼的技巧等等。Zookeeper的源碼其實是非常易於我們閱讀的,它不像Spring那樣跳來跳去,但仍需我們反覆閱讀,多畫時序圖才能理清楚。
Zookeeper系列文章暫時就到這裏結束了,我也是初學,不可能方方面面都講到,因此也只是針對核心的一些原理和應用進行了總結,想要深入瞭解的推薦看《從Paxos到Zookeeper 分佈式一致性原理與實踐 》。
PS:接下來將進入Dubbo系列,而Dubbo的基礎使用基本概念官網都講得非常清楚了,我也不打算耗費時間在這上面,所以主要是分析Dubbo的核心源碼。