一.簡介
Zookeeper是一個分佈式協調服務,就是爲用戶的分佈式應用程序提供協調服務。
Zookeeper本身就是一個分佈式程序(只要有半數以上節點存活,zk就能正常服務)。
Zookeeper所提供的服務涵蓋:主從協調、服務器節點動態上下線、統一配置管理、分佈式共享鎖、統一名稱服務……
雖然說可以提供各種服務,但是zookeeper在底層其實只提供了兩個功能:
管理(存儲,讀取)用戶程序提交的數據;
併爲用戶程序提供數據節點監聽服務;
Zookeeper集羣的角色: Leader 和 follower (Observer)
只要集羣中有半數以上節點存活,集羣就能提供服務
二.結構
特性
1.Zookeeper:一個leader,多個follower組成的集羣
2.全局數據一致:每個server保存一份相同的數據副本,client無論連接到哪個server,數據都是一致的
3.分佈式讀寫,更新請求轉發,由leader實施
4.更新請求順序進行,來自同一個client的更新請求按其發送順序依次執行
5.數據更新原子性,一次數據更新要麼成功,要麼失敗
6.實時性,在一定時間範圍內,client能讀到最新數據
結構
1.層次化的目錄結構,命名符合常規文件系統規範(見下圖)
2.每個節點在zookeeper中叫做znode,並且其有一個唯一的路徑標識
3.節點Znode可以包含數據和子節點(但是EPHEMERAL類型的節點不能有子節點)
4.客戶端應用可以在節點上設置監視器
節點類型
1.Znode有兩種類型:
短暫(ephemeral)(斷開連接自己刪除)
持久(persistent)(斷開連接不刪除)
2.Znode有四種形式的目錄節點(默認是persistent )
PERSISTENT
PERSISTENT_SEQUENTIAL(持久序列/test0000000019 )
EPHEMERAL
EPHEMERAL_SEQUENTIAL
3.創建znode時設置順序標識,znode名稱後會附加一個值,順序號是一個單調遞增的計數器,由父節點維護。
4.在分佈式系統中,順序號可以被用於爲所有的事件進行全局排序,這樣客戶端可以通過順序號推斷事件的順序。
三.原理
zookeeper的選舉機制(集羣paxos)
以一個簡單的例子來說明整個選舉的過程.
假設有五臺服務器組成的zookeeper集羣,它們的id從1-5,同時它們都是最新啓動的,也就是沒有歷史數據,在存放數據量這一點上,都是一樣的.假設這些服務器依序啓動,來看看會發生什麼.
1. 服務器1啓動,此時只有它一臺服務器啓動了,它發出去的報沒有任何響應,所以它的選舉狀態一直是LOOKING狀態。
2.服務器2啓動,它與最開始啓動的服務器1進行通信,互相交換自己的選舉結果,由於兩者都沒有歷史數據,所以id值較大的服務器2勝出,但是由於沒有達到超過半數以上的服務器都同意選舉它(這個例子中的半數以上是3),所以服務器1,2還是繼續保持LOOKING狀態。
3.服務器3啓動,根據前面的理論分析,服務器3成爲服務器1,2,3中的老大,而與上面不同的是,此時有三臺服務器選舉了它,所以它成爲了這次選舉的leader。
4. 服務器4啓動,根據前面的分析,理論上服務器4應該是服務器1,2,3,4中最大的,但是由於前面已經有半數以上的服務器選舉了服務器3,所以它只能接收當小弟的命了。
5.服務器5啓動,同4一樣,當小弟.
非全新集羣的選舉機制(數據恢復)
那麼,初始化的時候,是按照上述的說明進行選舉的,但是當zookeeper運行了一段時間之後,有機器down掉,重新選舉時,選舉過程就相對複雜了。
需要加入數據id、leader id和邏輯時鐘。
數據id:數據新的id就大,數據每次更新都會更新id。
Leader id:就是我們配置的myid中的值,每個機器一個。
邏輯時鐘:這個值從0開始遞增,每次選舉對應一個值,也就是說: 如果在同一次選舉中,那麼這個值應該是一致的 ; 邏輯時鐘值越大,說明這一次選舉leader的進程更新。
選舉的標準就變成:
1.邏輯時鐘小的選舉結果被忽略,重新投票
2.統一邏輯時鐘後,數據id大的勝出
3.數據id相同的情況下,leader id大的勝出
根據這個規則選出leader。
四.示例
Zookeeper的監聽器工作機制
監聽器是一個接口,我們的代碼中可以實現Wather這個接口,實現其中的process方法,方法中即我們自己的業務邏輯
監聽器的註冊是在獲取數據的操作中實現:
getData(path,watch)監聽的事件是:節點數據變化事件
getChildren(path,watch)監聽的事件是:節點下的子節點增減變化事件
服務端
public class DistributedServer {
private static final String host = "localhost:2181";
private static final int sessionTimeout = 2000;
private static final String parentNode = "/servers/";
private ZooKeeper zk = null;
/**
* 創建到zk的客戶端連接
*
* @throws Exception
*/
public void getConnect() throws Exception {
zk = new ZooKeeper(host, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 收到事件通知後的回調函數
System.out.println(event.getType() + "__" + event.getPath());
try {
zk.getChildren("/", true);
} catch (Exception e) {
}
}
});
}
/**
* 向zk集羣註冊服務器信息
* ZooDefs.Ids.OPEN_ACL_UNSAFE 默認匿名權限,權限scheme id:'world,'anyone,權限位:31(adcwr)
* ZooDefs.Ids.READ_ACL_UNSAFE 只讀權限,權限scheme id:'world,'anyone,權限位:1(r)
*
* CreateMode
* 節點類型,類型定義在枚舉CreateMode中:
* (1)PERSISTENT:持久;
* (2)PERSISTENT_SEQUENTIAL:持久順序;
* (3)EPHEMERAL:臨時;
* (4)EPHEMERAL_SEQUENTIAL:臨時順序。
* @param data 創建節點初始化內容
* @throws Exception
*/
public void registerServer(String data) throws Exception {
String create = zk.create(parentNode + "test", data.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(data + " 註冊節點 " + create);
}
/**
* 業務功能
*
* @throws InterruptedException
*/
public void handleBussiness(String data) throws InterruptedException {
System.out.println(data + "開始handleBussiness");
Thread.sleep(Long.MAX_VALUE);
}
public static void main(String[] args) throws Exception {
// 獲取zk連接
DistributedServer server = new DistributedServer();
server.getConnect();
// 利用zk連接註冊服務器信息
server.registerServer("test01");
// 啓動業務功能
server.handleBussiness("test01");
}
}
分佈式鎖
/**
*
*分佈式鎖:幾種實現方式,示例用臨時順序節點實現共享鎖的一般做法
*
* 邏輯
* 1.zk上註冊一個"臨時+序號"的znode,並監聽父節點
* 2.獲取父節點下所有程序子節點,比較序號大小
* 3.序號最小的獲取到"鎖",去訪問資源,訪問完後,刪除自己的節點,釋放鎖,重新註冊一個新的子節點
* 4.其他程序節點會收到事件通知,可以去zk上獲取鎖
*/
public class DistributedClientLock {
// 會話超時
private static final int SESSION_TIMEOUT = 2000;
// zookeeper集羣地址
private String hosts = "localhost:2181";
private String groupNode = "servers";
private String subNode = "test";
private boolean haveLock = false;
private ZooKeeper zk;
/**
* 記錄自己創建的子節點路徑
* volatile 不是線程安全的,具有可見性,在一個子內存操作完後,立即刷新回到主內存。
* 如果不加Volatile,每次調用thisPath,會有副本,修改會有延遲,比如其它線程搶到沒有修改完的數據,就在新的線程繼續執行,造成最後數據值有誤
* 比如:一個線程寫,其它線程去讀的時候,用的Volatile,比如監聽新節點插入。
*/
private volatile String thisPath;
/**
* 連接zookeeper
*/
public void connectZookeeper() throws Exception {
zk = new ZooKeeper(hosts, SESSION_TIMEOUT, new Watcher() {
public void process(WatchedEvent event) {
try {
System.out.println(event.getType()+"____"+event.getPath());
/**
* 判斷事件類型,此處只處理子節點變化事件
* event For “/path” event For “/path/child”
* create(“/path”) EventType.NodeCreated 無
* delete(“/path”) EventType.NodeDeleted 無
* setData(“/path”) EventType.NodeDataChanged 無
* create(“/path/child”) EventType.NodeChildrenChanged(getChild) EventType.NodeCreated
* delete(“/path/child”) EventType.NodeChildrenChanged(getChild) EventType.NodeDeleted
* setData(“/path/child”) 無 EventType.NodeDataChanged
*/
if (event.getType() == Event.EventType.NodeChildrenChanged && event.getPath().equals("/" + groupNode)) {
//獲取子節點,並對父節點進行監聽
List<String> childrenNodes = zk.getChildren("/" + groupNode, true);
String thisNode = thisPath.substring(("/" + groupNode + "/").length());
// 去比較是否自己是最小id
Collections.sort(childrenNodes);
if (childrenNodes.indexOf(thisNode) == 0) {
//訪問共享資源處理業務,並且在處理完成之後刪除鎖
doSomething();
//重新註冊一把新的鎖
thisPath = zk.create("/" + groupNode + "/" + subNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
// 程序一進來就先註冊一把鎖到zk上
thisPath = zk.create("/" + groupNode + "/" + subNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// wait一小會,便於觀察
Thread.sleep(new Random().nextInt(1000));
// 從zk的鎖父目錄下,獲取所有子節點,並且註冊對父節點的監聽
List<String> childrenNodes = zk.getChildren("/" + groupNode, true);
//如果爭搶資源的程序就只有自己,則可以直接去訪問共享資源
if (childrenNodes.size() == 1) {
doSomething();
thisPath = zk.create("/" + groupNode + "/" + subNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
}
}
/**
* 處理業務邏輯,並且在最後釋放鎖
*/
private void doSomething() throws Exception {
try {
System.out.println("鎖: " + thisPath);
Thread.sleep(2000);
} finally {
System.out.println("完成: " + thisPath);
//刪除當前節點
zk.delete(this.thisPath, -1);
}
}
public static void main(String[] args) throws Exception {
DistributedClientLock dl = new DistributedClientLock();
dl.connectZookeeper();
Thread.sleep(Long.MAX_VALUE);
}
}