ZooKeeper源碼學習筆記(1)--client端解析

前言

ZooKeeper是一個相對簡單的分布式協調服務,通過閱讀源碼我們能夠更進一步的清楚分布式的原理。

環境

ZooKeeper 3.4.9

入口函數

bin/zkCli.sh中,我們看到client端的真實入口其實是一個org.apache.zookeeper.ZooKeeperMain的Java類

"$JAVA" "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" \
     -cp "$CLASSPATH" $CLIENT_JVMFLAGS $JVMFLAGS \
     org.apache.zookeeper.ZooKeeperMain "$@"

通過源碼走讀,看到在ZooKeeperMain中主要由兩部分構成

connectToZK(cl.getOption("server"));

while ((line = (String)readLine.invoke(console, getPrompt())) != null) {
  executeLine(line);
}
  1. 構造一個ZooKeeper對象,同ZooKeeperServer進行建立通信連接
  2. 通過反射調用jline.ConsoleReader類,對終端輸入進行讀取,然后通過解析單行命令,調用ZooKeeper接口。

如上所述,client端其實是對 zookeeper.jar 的簡單封裝,在構造出一個ZooKeeper對象后,通過解析用戶輸入,調用 ZooKeeper 接口和 Server 進行交互。

ZooKeeper 類

剛才我們看到 client 端同 ZooKeeper Server 之間的交互其實是通過 ZooKeeper 對象進行的,接下來我們詳細深入到 ZooKeeper 類中,看看其和服務端的交互邏輯。

public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,
            boolean canBeReadOnly)
        throws IOException 
{
  ConnectStringParser connectStringParser = new ConnectStringParser(connectString);
  HostProvider hostProvider = new StaticHostProvider( connectStringParser.getServerAddresses());
  cnxn = new ClientCnxn(connectStringParser.getChrootPath(),
  hostProvider, sessionTimeout, this, watchManager, getClientCnxnSocket(), canBeReadOnly);
  cnxn.start();
}

在 ZooKeeper的構造方法中,可以看到 ZooKeeper 中使用 Server 的服務器地址構建了一個 ClientCnxn 類,在這個類中,系統新建了兩個線程

sendThread = new SendThread(clientCnxnSocket);
eventThread = new EventThread();

其中,SendThread 負責將ZooKeeper的請求信息封裝成一個Packet,發送給 Server ,并維持同Server的心跳,EventThread負責解析通過通過SendThread得到的Response,之后發送給Watcher::processEvent進行詳細的事件處理。

Client 時序圖

如上圖所示,Client中在終端輸入指令后,會被封裝成一個Request請求,通過submitRequest,進一步被封裝成Packet包,提交給SendThread處理。

SendThread通過doTransportPacket發送給Server,并通過readResponse獲取結果,解析成一個Event,再將Event加入EventThread的隊列中等待執行。

EventThread通過processEvent消費隊列中的Event事件。

SendThread

SendThread 的主要作用除了將Packet包發送給Server之外,還負責維持Client和Server之間的心跳,確保 session 存活。

現在讓我們從源碼出發,看看SendThread究竟是如何運行的。

SendThread是一個線程類,因此我們進入其run()方法,看看他的啟動流程。

while (state.isAlive()) {
  if (!clientCnxnSocket.isConnected()) {
    // 啟動和server的socket鏈接
    startConnect();
  }
  // 根據上次的連接時間,判斷是否超時
  if (state.isConnected()) {
    to = readTimeout - clientCnxnSocket.getIdleRecv();
  } else {
    to = connectTimeout - clientCnxnSocket.getIdleRecv();
  }
  if (to <= 0) {
    throw new SessionTimeoutException(warnInfo);
  }
  // 發送心跳包
  if (state.isConnected()) {
    if (timeToNextPing <= 0 || clientCnxnSocket.getIdleSend() > MAX_SEND_PING_INTERVAL) {
      sendPing();
      clientCnxnSocket.updateLastSend();
    }
  }
  // 將指令信息發送給 Server
  clientCnxnSocket.doTransport(to, pendingQueue, outgoingQueue, ClientCnxn.this);
}

從上面的代碼中,可以看出SendThread的主要任務如下:

  1. 創建同 Server 之間的 socket 鏈接
  2. 判斷鏈接是否超時
  3. 定時發送心跳任務
  4. 將ZooKeeper指令發送給Server

與 Server 的長鏈接

ZooKeeper通過獲取ZOOKEEPER_CLIENT_CNXN_SOCKET變量構造了一個ClientCnxnSocket對象,默認情況下是ClientCnxnSocketNIO

String clientCnxnSocketName = System
                .getProperty(ZOOKEEPER_CLIENT_CNXN_SOCKET);
if (clientCnxnSocketName == null) {
  clientCnxnSocketName = ClientCnxnSocketNIO.class.getName();
}

ClientCnxnSocketNIO::connect中我們可以看到這里同Server之間創建了一個socket鏈接。

SocketChannel sock = createSock();
registerAndConnect(sock, addr);

超時與心跳

SendThread::run中,可以看到針對鏈接是否建立分別有readTimeoutconnetTimeout 兩種超時時間,一旦發現鏈接超時,則拋出異常,終止 SendThread

在沒有超時的情況下,如果判斷距離上次心跳時間超過了1/2個超時時間,會再次發送心跳數據,避免訪問超時。

發送 ZooKeeper 指令

在時序圖中,我們看到從終端輸入指令后,我們會將其解析成一個Packet 包,等待SendThread進行發送。

ZooKeeper::create為例

RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.create);
CreateRequest request = new CreateRequest();
CreateResponse response = new CreateResponse();
request.setData(data);
request.setFlags(createMode.toFlag());
request.setPath(serverPath);
if (acl != null && acl.size() == 0) {
    throw new KeeperException.InvalidACLException();
}
request.setAcl(acl);
ReplyHeader r = cnxn.submitRequest(h, request, response, null);

在這里create指令,被封裝成了一個 CreateRequest,通過submitRequest被轉成了一個Packet

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;
    // Note that we do not generate the Xid for the packet yet. It is
    // generated later at send-time, by an implementation of ClientCnxnSocket::doIO(),
    // where the packet is actually sent.
    synchronized (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 the client is asking to close the session then
            // mark as closing
            if (h.getType() == OpCode.closeSession) {
                closing = true;
            }
            outgoingQueue.add(packet);
        }
    }
    sendThread.getClientCnxnSocket().wakeupCnxn();
    return packet;
}

submitRequest中,我們進一步看到Request被封裝成一個Packet包,并加入SendThread::outgoingQueue隊列中,等待執行。

Note:在這里我們還看到,ZooKeeper方法中所謂的同步方法其實就是在Packet被提交到SendThread之后,陷入一個while循環,等待處理完成后再跳出的過程

SendThread::runwhile循環中,ZooKeeper通過doTransport將存放在outgoingQueue中的Packet包發送給 Server。

void doIO(List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue, ClientCnxn cnxn) {
    if (sockKey.isReadable()) {
        // 讀取response信息
        sendThread.readResponse(incomingBuffer);
    }
    if (sockKey.isWritable()) {
        Packet p = findSendablePacket(outgoingQueue, cnxn.sendThread.clientTunneledAuthenticationInProgress());
        sock.write(p.bb);
    }
}

doIO發送socket信息之前,先從socket中獲取返回數據,通過readResonse進行處理。

void readResponse(ByteBuffer incomingBuffer) throws IOException {
     ByteBufferInputStream bbis = new ByteBufferInputStream(incomingBuffer);
     BinaryInputArchive bbia = BinaryInputArchive.getArchive(bbis);
     ReplyHeader replyHdr = new ReplyHeader();
     replyHdr.deserialize(bbia, "header");
     if (replyHdr.getXid() == -1) {
        WatcherEvent event = new WatcherEvent();
        event.deserialize(bbia, "response");
        WatchedEvent we = new WatchedEvent(event);
        eventThread.queueEvent( we );
     }
}

readReponse中,通過解析數據,我們可以得到WatchedEvent對象,并將其壓入EventThread的消息隊列,等待分發

EventThread

public void run() {
    while (true) {
        Object event = waitingEvents.take();
        if (event == eventOfDeath) {
            wasKilled = true;
        } else {
            processEvent(event);
        }
}

EventThread中通過processEvent對隊列中的事件進行消費,并分發給不同的Watcher

watch事件注冊和分發

通常在ZooKeeper中,我們會為指定節點添加一個Watcher,用于監聽節點變化情況,以ZooKeeper:exist為例

// the watch contains the un-chroot path
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();
ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);

代碼的大致邏輯和create類似,但是對wathcer做了一層ExistWatchRegistration的包裝,當packet對象完成請求之后,調用register方法,根據不同包裝的WatchRegistration將watch注冊到不同watch列表中,等待回調。

if (p.watchRegistration != null) {
    p.watchRegistration.register(p.replyHeader.getErr());
}

在 ZooKeeper 中一共有三種類型的WatchRegistration,分別對應DataWatchRegistration,ChildWatchRegistration,ExistWatchRegistration。 并在ZKWatchManager類中根據每種類型的WatchRegistration,分別有一張map表負責存放。

private final Map<String, Set<Watcher>> dataWatches =
            new HashMap<String, Set<Watcher>>();
private final Map<String, Set<Watcher>> existWatches =
            new HashMap<String, Set<Watcher>>();
private final Map<String, Set<Watcher>> childWatches =
            new HashMap<String, Set<Watcher>>();

EventThread::processEvent 時,根據event的所屬路徑,從三張map中獲取對應的watch列表進行消息通知及處理。

總結

client 端的源碼分析就到此為止了。

ZooKeeper Client 的源碼很簡單,擁有三個獨立線程分別對命令進行處理,分發和響應操作,在保證各個線程相互獨立的基礎上,盡可能避免了多線程操作中出現鎖的情況。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容