1. 程式人生 > >Zookeeper C客戶端解析

Zookeeper C客戶端解析

Zookeeper客戶端主要有以下幾個核心元件組成:

Zookeeper控制代碼:儲存預設Watcher、Zookeeper伺服器地址列表等;
Adaptor:包含兩個執行緒:I/O執行緒和Completion執行緒,其中,I/O執行緒負責客戶端與服務端之間的I/O通訊;Completion線
程負責處理非同步介面的Completion回撥。

1、客戶端初始化
Zookeeper客戶端的初始化其實就是Zookeeper控制代碼初始化的過程,C API如下:

ZOOAPI zhandle_t *zookeeper_init(const char *host, watcher_fn fn, int recv_timeout, const clientid_t * clientid, void *context, int flags);

客戶端初始化過程大體分為以下3個步驟:

設定預設Watcher;
設定Zookeeper伺服器地址列表;
建立網路連線物件:Adaptor;

如果Zookeeper初始化API中Watcher回撥函式賦值了,則Zookeeper會將此回撥函式儲存在Zookeeper控制代碼中,作為客戶端在會話期間的預設Watcher回撥函式。
2、一次會話建立過程
客戶端一次會話建立的過程分為三部分:Zookeeper控制代碼初始化、會話建立和處理服務端應答。
2.1、Zookeeper控制代碼初始化
Zookeeper控制代碼初始化步驟如下:
1.建立Zookeeper控制代碼
呼叫calloc介面分配Zookeeper控制代碼;
2.設定會話預設Watcher
如果Zookeeper控制代碼初始化API中傳入了Watcher回撥函式,則將回調函式儲存在控制代碼的watcher欄位中;否則,將null_watcher_fn作為預設Watcher回撥函式。
3.構造Zookeeper伺服器地址列表
① 解析Zookeeper控制代碼初始化API傳入的host引數,獲得伺服器列表,並儲存到Zookeeper控制代碼hostname欄位中。
② 呼叫介面getaddrs()構造Zookeeper伺服器地址列表,並儲存到Zookeeper控制代碼addrs欄位中。
4.建立並初始化客戶端網路連線物件
① 首先,Zookeeper客戶端建立一個網路聯結器,即adaptor_threads物件,用來管理客戶端與服務端的網路互動。
② 其次,初始化用於客戶端與服務端網路IO交換的描述符,即adaptor_threads物件的fd欄位。
③ 最後,將網路連線物件儲存到Zookeeper控制代碼adaptor_priv欄位中。
5.建立I/O執行緒和Completion執行緒
呼叫介面start_threads()建立客戶端的兩個核心網路執行緒:I/O執行緒和Completion執行緒。I/O執行緒用於管理客戶端與服務端之間的所有網路IO,Completion執行緒用於進行客戶端的事件處理。
2.2、會話建立


會話建立主要是由介面zookeeper_interest()完成的。
6.啟動I/O執行緒和Completion執行緒
I/O執行緒首先判斷當前客戶端的狀態(Zookeeper控制代碼的state欄位),進行一系列清理工作,為客戶端傳送“會話建立”請求做準備。
7.獲取一個伺服器地址
在建立TCP連線之前,首先選取一個Zookeeper伺服器的地址。Zookeeper控制代碼結構體中connect_index欄位表示會話建立時連線的Zookeeper伺服器地址在列表中的索引。
Zookeeper伺服器地址選取方法:在建立客戶端會話時,以Zookeeper控制代碼的connect_index欄位為索引從伺服器地址列表中獲取Zookeeper伺服器的地址,然後建立TCP連線。在建立Zookeeper控制代碼時,此欄位初始化為0;在會話建立失敗、鑑權失敗等客戶端與服務端互動異常時,此欄位加1(在handle_error()介面中執行加1操作)。
8.建立TCP連線
在選取伺服器地址後,I/O執行緒負責建立客戶端與服務端的TCP長連線。
9.構造Connect請求
在TCP長連線建立完成後,呼叫介面prime_connection()傳送Connect請求,首先構造Connect請求,並且呼叫介面serialize_prime_connect()將Connect請求序列化為網路I/O層的Packet物件。
10.傳送Connect請求
呼叫介面zookeeper_send()將序列化後的Connect請求傳送給服務端,如果傳送成功,則將客戶端會話狀態修改為ASSOCIATING。
2.3、處理服務端應答

11.接收服務端應答
客戶端在傳送完Connect請求後,利用select機制監控建立會話時生成的Socket描述符,等待接收服務端應答。
12.處理Response
呼叫介面zookeeper_process()分發處理服務端應答。
13.連線成功
將客戶端會話狀態標記為ZOO_CONNECTED_STATE,表示會話連線成功,同時進行客戶端會話引數設定,包括:recv_timeout和client_id等。
參見介面check_events()。
最後傳送會話鑑權請求。
14.處理SESSION事件
呼叫介面PROCESS_SESSION_EVENT處理Session事件。
首先,構造ZOO_SESSION_EVENT事件。
其次,呼叫介面collectWatchers()查詢Watcher,並將ZOO_SESSION_EVENT事件加入到會話的準備執行的Completion佇列(completions_to_process)。
最後,Completion執行緒呼叫介面process_completions()處理事件:先反序列化事件,再查詢Watcher連結串列,找到待處理的Watcher物件,最後執行Watcher的回撥函式。
2.4、總結
至此,Zookeeper客戶端完整的一次會話建立過程全部完成。
在會話建立過程中函式呼叫關係如圖1所示:
在這裡插入圖片描述
圖1 Zookeeper客戶端一次會話建立過程

3、伺服器地址列表
在使用Zookeeper初始化API時,傳入的Zookeeper伺服器地址列表,即host引數,通常是一個使用逗號分隔的多個IP地址和埠的字串,比如:

192.168.0.1:2181, 192.168.0.2:2181,192.168.0.3:2181

Zookeeper客戶端在接收到伺服器地址列表後,首先將其放入Zookeeper控制代碼的hostname欄位中,然後再呼叫介面getaddrs()解析chroot路徑,並儲存伺服器地址列表到欄位addrs中。
3.1、Chroot:客戶端隔離名稱空間
在3.2.0及其之後版本的Zookeeper中,添加了“Chroot”特性,該特性允許客戶端為自己設定一個名稱空間。如果客戶端設定了Chroot,則客戶端對伺服器的任何操作都會被限制在自己的名稱空間下。

假如客戶端將其Chroot設定為/apps/X,則對於此客戶端來說,它發給服務端的所有請求中的節點路徑都是一個相對路徑——以/apps/X為根節點。比如,客戶端建立節點/test_chroot,則它實際在服務端建立的節點是/apps/X/test_chroot。

客戶端可以通過在host引數中新增字尾的方式來設定Chroot,如下所示:

192.168.0.1:2181,192.168.0.2:2181,192.168.0.3:2181/apps/X

Zookeeper_init()會將Chroot解析出來,並儲存到Zookeeper控制代碼的chroot欄位中。
3.2、伺服器地址列表
Zookeeper控制代碼中有以下幾個欄位與伺服器地址有關係,分別用在伺服器地址解析和建立TCP連線時的伺服器地址選取。
在這裡插入圖片描述
伺服器地址列表(欄位addrs)的大小是不固定的、可擴充套件的,在伺服器地址解析的過程中,如果伺服器地址列表已滿,則呼叫介面realloc()將伺服器地址列表的空間擴大(每次擴大16個伺服器地址的長度)。
4、Adaptor物件:網路I/O
Adaptor是Zookeeper C客戶端的核心模組,負責客戶端與服務端之間的網路連線並進行一系列的通訊,其結構為struct adaptor_threads。詳細描述如下:

struct adaptor_threads {
     pthread_t io;
     pthread_t completion;
     int threadsToWait;         // barrier
     pthread_cond_t cond;       // barrier's conditional
     pthread_mutex_t lock;      // ... and a lock
     pthread_mutex_t zh_lock;   // critical section lock
#ifdef WIN32
     SOCKET self_pipe[2];
#else
     /* Pipe描述符, 用於通知IO執行緒將zookeeper控制代碼中to_send佇列中的請求傳送給服務端
      * self_pipe[0]用於IO執行緒接收請求傳送訊號;
      * self_pipe[1]用於向IO執行緒傳送請求傳送訊號;
      */
     int self_pipe[2];
#endif
};

4.1、幾個重要的佇列
Zookeeper客戶端有幾個比較重要的佇列:sent_requests、to_send、to_process、completion_to_process。

to_send佇列:待發送的客戶端請求佇列,用於儲存準備好的待發送到服務端的請求。
sent_requests佇列:客戶端請求佇列,用於儲存客戶端請求的Completion和Watcher回撥。
to_process佇列:服務端響應佇列,用於儲存客戶端接收到的服務端響應。
completion_to_process佇列:服務端響應佇列,用於響應的Completion和Watcher回撥。

4.2、網路I/O互動
Adaptor物件的主要核心是IO執行緒,負責客戶端請求的傳送和服務端響應的接收。Zookeeper控制代碼中的欄位fd是客戶端與服務端通訊使用的Socket描述符。
4.2.1、請求傳送
客戶端傳送請求時,首先將構建請求並對其進行序列化,然後將請求加入to_send佇列和sent_requests佇列,最後通過管道通知IO執行緒傳送客戶端請求。
IO執行緒是按照FIFO順序處理to_send佇列中的請求的。Zookeeper API介面將客戶端請求加入佇列之後,向管道(Adaptor物件內建的pipe)內寫入一個字元,而IO執行緒檢測到管道內讀取到資料後,會從to_send佇列中取出客戶端請求傳送給服務端。
請求傳送完成後,客戶端會將to_send佇列中的請求刪除,保留sent_requests佇列中的請求,以便等待服務端響應返回後進行相應處理,如圖2所示。
在這裡插入圖片描述
圖2 請求傳送和響應接收示意圖
4.2.2、響應接收
客戶端接收到服務端的完整響應後,根據不同的客戶端請求型別,進行不同的處理。

如果客戶端尚未完成會話建立,則將接收到的Buffer序列化成Connect Response物件。
如果客戶端處於正常的會話,並且服務端響應是一個事件,則客戶端將Buffer序列化為Watcher Event物件,並將該事件放入待處理佇列中。
如果是一個常規請求的響應(比如:create、getData等),則會從sent_requests佇列中取出客戶端請求進行相應的處理。

4.2.3、IO執行緒
IO執行緒是客戶端的一個核心的I/O排程執行緒,用於管理客戶端和服務端之間的網路I/O操作。
在執行過程中,它一方面維護客戶端和服務端之間的會話生命週期,通過在一定週期頻率內向服務端傳送PING請求來實現心跳檢測。同時,在會話週期內,如果客戶端與服務端之間出現TCP連線斷開,則它能自動且透明的完成重連操作。
另一方面,IO執行緒管理了客戶端所有的請求傳送和響應接收操作,它將客戶端API操作轉換成相應的請求協議傳送到服務端,並完成對同步呼叫的返回和非同步呼叫的回撥。
同時,IO執行緒還負責將服務端的事件傳遞給Completion執行緒處理。
4.2.4、Completion執行緒
Completion執行緒是客戶端的另一個核心執行緒,負責客戶端事件的處理、非同步呼叫的Completion回撥的觸發,並觸發客戶端註冊的Watcher監聽。