What is ZooKeeper
ZooKeeper是一個(gè)分布式的分布式應(yīng)用程序協(xié)調(diào)服務(wù)。簡(jiǎn)單地來說,就是用于協(xié)調(diào)管理多個(gè)分布式應(yīng)用程序的一個(gè)工具,扮演著一個(gè)第三方管理者的角色。
問題背景分析
假設(shè)現(xiàn)在有10個(gè)應(yīng)用程序(App#0 - App#9),運(yùn)行在由10臺(tái)服務(wù)器(Server#0 - Server#9)組成的集群上(假設(shè)平均分配,每臺(tái)服務(wù)器上運(yùn)行一個(gè)程序)。此時(shí)由于某個(gè)熱門線上活動(dòng)的開始(如搶票or低價(jià)秒殺等),突然間有數(shù)以百萬計(jì)的用戶訪問服務(wù)器上的資源,等待服務(wù)器處理并應(yīng)答(如下圖所示)。

很不幸10臺(tái)服務(wù)器中有K臺(tái)受不住負(fù)載壓力,導(dǎo)致服務(wù)器崩潰。在這種情況下,如果客戶端無法感知服務(wù)器的狀態(tài)(在線/離線),部分向已經(jīng)崩潰的服務(wù)器發(fā)送請(qǐng)求的客戶端將會(huì)有長(zhǎng)時(shí)間無法獲得應(yīng)答,它們只能一直重復(fù)地向已經(jīng)崩潰的服務(wù)器地址重發(fā)請(qǐng)求,無法切換至另外(10-K)臺(tái)完好的服務(wù)器進(jìn)行交互。

其實(shí)在這種場(chǎng)景下,如果客戶端能夠及時(shí)地感知到集群中哪些節(jié)點(diǎn)已經(jīng)崩潰,哪些節(jié)點(diǎn)仍然完好,是可以切換至完好的節(jié)點(diǎn)并向其發(fā)送請(qǐng)求的。理論上只要集群中仍有1個(gè)節(jié)點(diǎn)是完好的,它即能向客戶端提供服務(wù)。
所以整個(gè)問題的癥結(jié)就在于,如何讓客戶端感知到服務(wù)器上下線狀態(tài),以便切換請(qǐng)求發(fā)送的地址。

重新參考ZooKeeper的功能描述,ZooKeeper可以用來協(xié)調(diào)管理多個(gè)分布式應(yīng)用程序,那其實(shí)可以用于管理我們的分布式機(jī)器集群。如上圖所示,在用戶和服務(wù)器集群中間可設(shè)置ZooKeeper層,讓ZooKeeper實(shí)時(shí)感知每一個(gè)節(jié)點(diǎn)的狀態(tài),然后客戶端并不直接向具體節(jié)點(diǎn)發(fā)起請(qǐng)求,而應(yīng)先向ZooKeeper詢問當(dāng)前仍然存活的服務(wù)器節(jié)點(diǎn),然后再從中挑選一個(gè)負(fù)載較低的服務(wù)器節(jié)點(diǎn)進(jìn)行交互。由于ZooKeeper本身的高可用性(本身也可拓展為分布式架構(gòu)),所以就能大大地提高整個(gè)系統(tǒng)的可用性。
ZooKeeper數(shù)據(jù)結(jié)構(gòu)
ZooKeeper數(shù)據(jù)結(jié)構(gòu)采用了樹狀結(jié)構(gòu)(在文件系統(tǒng)中被廣泛使用),且不是簡(jiǎn)單的二叉樹,而是多叉樹。在ZooKeeper的樹結(jié)構(gòu)中,每一個(gè)節(jié)點(diǎn)被稱為znode,可通過控制臺(tái)命令或者Java的SDK對(duì)內(nèi)部數(shù)據(jù)進(jìn)行管理。
znode的類型有2*2=4種,分別是:
- PERSISTENT
- PERSISTENT_SEQUENTIAL
- EPHEMERAL
- EPHEMERAL_SEQUENTIAL
其中PERSISTENT和EPHEMERAL的區(qū)別正如其名,在無外力影響下PERSISTENT節(jié)點(diǎn)不會(huì)被改變和刪除,而EPHEMERAL節(jié)點(diǎn)在創(chuàng)建節(jié)點(diǎn)的session結(jié)束后會(huì)自動(dòng)從樹中刪除。至于SEQUENTIAL與非SEQUENTIAL則影響了節(jié)點(diǎn)id自增,SEQUENTIAL節(jié)點(diǎn)的id會(huì)自動(dòng)遵循父節(jié)點(diǎn)下的自增規(guī)則進(jìn)行命名。

如圖所示,在本問題中我們可以把一臺(tái)服務(wù)器看作樹中的一個(gè)節(jié)點(diǎn),我們可以利用EPHEMERAL節(jié)點(diǎn)的這一特性進(jìn)行服務(wù)器狀態(tài)的監(jiān)聽。服務(wù)器上線時(shí)創(chuàng)建與zk之間的session并向zk注冊(cè)節(jié)點(diǎn),只要服務(wù)器不崩潰,session便不會(huì)結(jié)束,即EPHEMERAL節(jié)點(diǎn)會(huì)一直存在,可被客戶端感知;當(dāng)服務(wù)器崩潰時(shí),其與zk之間保持的session自然也會(huì)結(jié)束,EPHEMERAL節(jié)點(diǎn)會(huì)自動(dòng)被刪除,客戶端查詢服務(wù)器列表時(shí)絕對(duì)無法獲得已刪除的節(jié)點(diǎn)信息。
Demo程序
- Server.java (服務(wù)器端代碼)
package my.bigdata.zk;
import org.apache.zookeeper.*;
public class Server {
private static final String HOST_ADDRESS = "localhost:2181";
private static final int DEFAULT_TIMEOUT = 2000;
private static final String DEFAULT_SERVER_PARENT = "/servers";
private ZooKeeper zkConnect = null;
/**
* 連接至ZooKeeper
* @throws Exception
*/
public void connect() throws Exception{
zkConnect = new ZooKeeper(HOST_ADDRESS, DEFAULT_TIMEOUT, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println("Type:" + watchedEvent.getType()
+ " Path:" + watchedEvent.getPath());
}
});
}
/**
* 向ZooKeeper注冊(cè)本服務(wù)器節(jié)點(diǎn)
* @param data 服務(wù)器信息
* @throws Exception
*/
public void register(String data) throws Exception{
String create = zkConnect.create(DEFAULT_SERVER_PARENT + "/server",
data.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL); // 注冊(cè)成ephemeral節(jié)點(diǎn)以便自動(dòng)在zk上注銷
System.out.println(create + " is registered!");
}
/**
* 通過sleep模擬服務(wù)器在線
*/
public void sleep() {
try {
Thread.sleep(20000);
} catch (Exception e) {
System.out.println(e.toString());
}
}
public static void main(String[] args) throws Exception {
//連接至zk
Server server = new Server();
server.connect();
//向zk注冊(cè)服務(wù)器信息
String data = args[0];
server.register(data);
server.sleep();
}
}
服務(wù)器端的重點(diǎn)在于,程序啟動(dòng)時(shí)向ZooKeeper的指定節(jié)點(diǎn)下注冊(cè)服務(wù)器信息,相當(dāng)于通知ZooKeeper這個(gè)第三方:“服務(wù)器已上線”。其次,注冊(cè)的節(jié)點(diǎn)類型必須是ephemeral節(jié)點(diǎn),為了實(shí)現(xiàn)節(jié)點(diǎn)id自增(auto-increment)還可以使用ephemeral_sequential節(jié)點(diǎn)。
- Client.java (客戶端代碼)
package my.bigdata.zk;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Client {
private static final String HOST_ADDRESS = "localhost:2181";
private static final int DEFAULT_TIMEOUT = 2000;
private static final String DEFAULT_SERVER_PARENT = "/servers";
private ZooKeeper zkConnect = null;
private List<String> availableServers;
/**
* 連接至ZooKeeper
* @throws Exception
*/
public void connect() throws Exception {
zkConnect = new ZooKeeper(HOST_ADDRESS, DEFAULT_TIMEOUT, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
try {
updateServerCondition(); // 重復(fù)注冊(cè)
} catch (Exception e) {
System.out.println(e.toString());
}
}
});
}
/**
* 向zk查詢服務(wù)器情況, 并update本地服務(wù)器列表
* @throws Exception
*/
public void updateServerCondition() throws Exception {
List<String> children = zkConnect.getChildren(DEFAULT_SERVER_PARENT, true);
List<String> servers = new ArrayList<>();
for(String child : children) {
byte[] data = zkConnect.getData(DEFAULT_SERVER_PARENT + "/" + child,
false,
null);
servers.add(new String(data));
}
availableServers = servers;
System.out.println(Arrays.toString(servers.toArray(new String[0])));
}
/**
* 通過sleep讓客戶端持續(xù)運(yùn)行,模擬"監(jiān)聽"
*/
public void sleep() throws Exception{
System.out.println("client is working");
Thread.sleep(Long.MAX_VALUE);
}
public static void main(String[] args) throws Exception {
// 連接zk
Client client = new Client();
client.connect();
// 獲取servers節(jié)點(diǎn)信息(并監(jiān)聽),從中獲取服務(wù)器信息列表
client.updateServerCondition();
client.sleep();
}
}
客戶端的重點(diǎn)在于,它不斷地向ZooKeeper某個(gè)特定節(jié)點(diǎn)(此處是servers節(jié)點(diǎn))注冊(cè)了一個(gè)Watcher,那么一旦該節(jié)點(diǎn)下的結(jié)構(gòu)發(fā)生改變,ZooKeeper會(huì)向注冊(cè)了Watcher的客戶端發(fā)送“狀態(tài)變化”的消息,那么客戶端即可動(dòng)態(tài)地從ZooKeeper中獲取最新的服務(wù)器節(jié)點(diǎn)信息,甚至無需“主動(dòng)”詢問。
當(dāng)然,ZooKeeper的應(yīng)用場(chǎng)景還有很多,考慮到它本身也可拓展為一個(gè)分布式應(yīng)用,在這種高可用性保證下它簡(jiǎn)直就是多個(gè)分布式應(yīng)用的萬能管家和協(xié)調(diào)者??。