zookeeper知識點總結--持續更新中
- Zookeeper有三種執行形式:叢集模式、單機模式、偽叢集模式。
- 若刪除節點存在子節點,那麼無法刪除該節點,必須先刪除子節點,再刪除父節點。
- zookeeper使用分為命令列、javaApi
- zookeeper的三個jar包jar、javadoc.jar、sources.jar,使用maven依賴的只需要如下配置
<dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.6</version> </dependency>
- 建立節點有非同步和同步兩種方式。無論是非同步或者同步,Zookeeper都不支援遞迴呼叫,即無法在父節點不存在的情況下建立一個子節點,如在/zk-ephemeral節點不存在的情況下建立/zk-ephemeral/ch1節點;並且如果一個節點已經存在,那麼建立同名節點時,會丟擲NodeExistsException異常。
- Watcher通知是一次性的,即一旦觸發一次通知後,該Watcher就失效了,因此客戶端需要反覆註冊Watcher,即程式中在process裡面又註冊了Watcher,否則,將無法獲取c3節點的建立而導致子節點變化的事件。
- 在更新資料時,setData方法存在一個version引數,其用於指定節點的資料版本,表明本次更新操作是針對指定的資料版本進行的,但是,在getData方法中,並沒有提供根據指定資料版本來獲取資料的介面,那麼,這裡為何要指定資料更新版本呢,這裡方便理解,可以等效於CAS(compare and swap),對於值V,每次更新之前都會比較其值是否是預期值A,只有符合預期,才會將V原子化地更新到新值B。Zookeeper的setData介面中的version引數可以對應預期值,表明是針對哪個資料版本進行更新,假如一個客戶端試圖進行更新操作,它會攜帶上次獲取到的version值進行更新,而如果這段時間內,Zookeeper伺服器上該節點的資料已經被其他客戶端更新,那麼其資料版本也會相應更新,而客戶端攜帶的version將無法匹配,無法更新成功,因此可以有效地避免分散式更新的併發問題
- ZkClient開源客戶端提供了遞迴建立節點的介面,即其幫助開發者完成父節點的建立,再建立子節點。值得注意的是,在原生態介面中是無法建立成功的(父節點不存在),但是通過ZkClient可以遞迴的先建立父節點,再建立子節點。
- Curator客戶端解決了很多Zookeeper客戶端非常底層的細節開發工作,包括連線重連,反覆註冊Watcher和NodeExistsException異常等,現已成為Apache的頂級專案。
- Curator除了使用一般方法建立會話外,還可以使用fluent風格進行建立。通過使用Fluent風格的介面,開發人員可以進行自由組合來完成各種型別節點的建立。
- Curator除了提供很便利的API,還提供了一些典型的應用場景,開發人員可以使用參考更好的理解如何使用Zookeeper客戶端,所有的都在recipes包中,只需要在pom.xml中新增如下依賴即可
- Master選舉 藉助Zookeeper,開發者可以很方便地實現Master選舉功能,其大體思路如下:選擇一個根節點,如/master_select,多臺機器同時向該節點建立一個子節點/master_select/lock,利用Zookeeper特性,最終只有一臺機器能夠成功建立,成功的那臺機器就是Master。
- 分散式鎖 為了保證資料的一致性,經常在程式的某個執行點需要進行同步控制。以流水號生成場景為例,普通的後臺應用通常採用時間戳方式來生成流水號,但是在使用者量非常大的情況下,可能會出現併發問題。
-
package com.hust.grid.leesf.curator.examples; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.CountDownLatch; public class Recipes_NoLock { public static void main(String[] args) throws Exception { final CountDownLatch down = new CountDownLatch(1); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { public void run() { try { down.await(); } catch (Exception e) { } SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss|SSS"); String orderNo = sdf.format(new Date()); System.err.println("生成的訂單號是 : " + orderNo); } }).start(); } down.countDown(); } }
執行結果:
生成的訂單號是 : 16:29:10|590 生成的訂單號是 : 16:29:10|590 生成的訂單號是 : 16:29:10|591 生成的訂單號是 : 16:29:10|591 生成的訂單號是 : 16:29:10|590 生成的訂單號是 : 16:29:10|590 生成的訂單號是 : 16:29:10|591 生成的訂單號是 : 16:29:10|590 生成的訂單號是 : 16:29:10|592 生成的訂單號是 : 16:29:10|591
結果表示訂單號出現了重複,即普通的方法無法滿足業務需要,因為其未進行正確的同步。可以使用Curator來實現分散式鎖功能。
package com.hust.grid.leesf.curator.examples; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.CountDownLatch; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.framework.recipes.locks.InterProcessMutex; import org.apache.curator.retry.ExponentialBackoffRetry; public class Recipes_Lock { static String lock_path = "/curator_recipes_lock_path"; static CuratorFramework client = CuratorFrameworkFactory.builder().connectString("127.0.0.1:2181") .retryPolicy(new ExponentialBackoffRetry(1000, 3)).build(); public static void main(String[] args) throws Exception { client.start(); final InterProcessMutex lock = new InterProcessMutex(client, lock_path); final CountDownLatch down = new CountDownLatch(1); for (int i = 0; i < 30; i++) { new Thread(new Runnable() { public void run() { try { down.await(); lock.acquire(); } catch (Exception e) { } SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss|SSS"); String orderNo = sdf.format(new Date()); System.out.println("生成的訂單號是 : " + orderNo); try { lock.release(); } catch (Exception e) { } } }).start(); } down.countDown(); } }
執行結果:
生成的訂單號是 : 16:31:50|293 生成的訂單號是 : 16:31:50|319 生成的訂單號是 : 16:31:51|278 生成的訂單號是 : 16:31:51|326 生成的訂單號是 : 16:31:51|402 生成的訂單號是 : 16:31:51|420 生成的訂單號是 : 16:31:51|546 生成的訂單號是 : 16:31:51|602 生成的訂單號是 : 16:31:51|626 生成的訂單號是 : 16:31:51|656 生成的訂單號是 : 16:31:51|675 生成的訂單號是 : 16:31:51|701 生成的訂單號是 : 16:31:51|708 生成的訂單號是 : 16:31:51|732 生成的訂單號是 : 16:31:51|763 生成的訂單號是 : 16:31:51|785 生成的訂單號是 : 16:31:51|805 生成的訂單號是 : 16:31:51|823 生成的訂單號是 : 16:31:51|839 生成的訂單號是 : 16:31:51|853 生成的訂單號是 : 16:31:51|868 生成的訂單號是 : 16:31:51|884 生成的訂單號是 : 16:31:51|897 生成的訂單號是 : 16:31:51|910 生成的訂單號是 : 16:31:51|926 生成的訂單號是 : 16:31:51|939 生成的訂單號是 : 16:31:51|951 生成的訂單號是 : 16:31:51|965 生成的訂單號是 : 16:31:51|972 生成的訂單號是 : 16:31:51|983
結果表明此時已經不存在重複的流水號。
-
命名服務
命名服務是分步實現系統中較為常見的一類場景,分散式系統中,被命名的實體通常可以是叢集中的機器、提供的服務地址或遠端物件等,通過命名服務,客戶端可以根據指定名字來獲取資源的實體、服務地址和提供者的資訊。Zookeeper也可幫助應用系統通過資源引用的方式來實現對資源的定位和使用,廣義上的命名服務的資源定位都不是真正意義上的實體資源,在分散式環境中,上層應用僅僅需要一個全域性唯一的名字。Zookeeper可以實現一套分散式全域性唯一ID的分配機制。
通過呼叫Zookeeper節點建立的API介面就可以建立一個順序節點,並且在API返回值中會返回這個節點的完整名字,利用此特性,可以生成全域性ID,其步驟如下
1. 客戶端根據任務型別,在指定型別的任務下通過呼叫介面建立一個順序節點,如"job-"。
2. 建立完成後,會返回一個完整的節點名,如"job-00000001"。
3. 客戶端拼接type型別和返回值後,就可以作為全域性唯一ID了,如"type2-job-00000001"。
-
分散式鎖
分散式鎖用於控制分散式系統之間同步訪問共享資源的一種方式,可以保證不同系統訪問一個或一組資源時的一致性,主要分為排它鎖和共享鎖。
排它鎖又稱為寫鎖或獨佔鎖,若事務T1對資料物件O1加上了排它鎖,那麼在整個加鎖期間,只允許事務T1對O1進行讀取和更新操作,其他任何事務都不能再對這個資料物件進行任何型別的操作,直到T1釋放了排它鎖。
① 獲取鎖,在需要獲取排它鎖時,所有客戶端通過呼叫介面,在/exclusive_lock節點下建立臨時子節點/exclusive_lock/lock。Zookeeper可以保證只有一個客戶端能夠建立成功,沒有成功的客戶端需要註冊/exclusive_lock節點監聽。
② 釋放鎖,當獲取鎖的客戶端宕機或者正常完成業務邏輯都會導致臨時節點的刪除,此時,所有在/exclusive_lock節點上註冊監聽的客戶端都會收到通知,可以重新發起分散式鎖獲取。
共享鎖又稱為讀鎖,若事務T1對資料物件O1加上共享鎖,那麼當前事務只能對O1進行讀取操作,其他事務也只能對這個資料物件加共享鎖,直到該資料物件上的所有共享鎖都被釋放。
① 獲取鎖,在需要獲取共享鎖時,所有客戶端都會到/shared_lock下面建立一個臨時順序節點,如果是讀請求,那麼就建立例如/shared_lock/host1-R-00000001的節點,如果是寫請求,那麼就建立例如/shared_lock/host2-W-00000002的節點。
② 判斷讀寫順序,不同事務可以同時對一個數據物件進行讀寫操作,而更新操作必須在當前沒有任何事務進行讀寫情況下進行,通過Zookeeper來確定分散式讀寫順序,大致分為四步。
1. 建立完節點後,獲取/shared_lock節點下所有子節點,並對該節點變更註冊監聽。
2. 確定自己的節點序號在所有子節點中的順序。
3. 對於讀請求:若沒有比自己序號小的子節點或所有比自己序號小的子節點都是讀請求,那麼表明自己已經成功獲取到共享鎖,同時開始執行讀取邏輯,若有寫請求,則需要等待。對於寫請求:若自己不是序號最小的子節點,那麼需要等待。
4. 接收到Watcher通知後,重複步驟1。
③ 釋放鎖,其釋放鎖的流程與獨佔鎖一致。
上述共享鎖的實現方案,可以滿足一般分散式叢集競爭鎖的需求,但是如果機器規模擴大會出現一些問題,下面著重分析判斷讀寫順序的步驟3。
針對如上圖所示的情況進行分析
1. host1首先進行讀操作,完成後將節點/shared_lock/host1-R-00000001刪除。
2. 餘下4臺機器均收到這個節點移除的通知,然後重新從/shared_lock節點上獲取一份新的子節點列表。
3. 每臺機器判斷自己的讀寫順序,其中host2檢測到自己序號最小,於是進行寫操作,餘下的機器則繼續等待。
4. 繼續...
可以看到,host1客戶端在移除自己的共享鎖後,Zookeeper傳送了子節點更變Watcher通知給所有機器,然而除了給host2產生影響外,對其他機器沒有任何作用。大量的Watcher通知和子節點列表獲取兩個操作會重複執行,這樣會造成系能鞥影響和網路開銷,更為嚴重的是,如果同一時間有多個節點對應的客戶端完成事務或事務中斷引起節點小時,Zookeeper伺服器就會在短時間內向其他所有客戶端傳送大量的事件通知,這就是所謂的羊群效應。
可以有如下改動來避免羊群效應。
1. 客戶端呼叫create介面常見類似於/shared_lock/[Hostname]-請求型別-序號的臨時順序節點。
2. 客戶端呼叫getChildren介面獲取所有已經建立的子節點列表(不註冊任何Watcher)。
3. 如果無法獲取共享鎖,就呼叫exist介面來對比自己小的節點註冊Watcher。對於讀請求:向比自己序號小的最後一個寫請求節點註冊Watcher監聽。對於寫請求:向比自己序號小的最後一個節點註冊Watcher監聽。
4. 等待Watcher通知,繼續進入步驟2。
此方案改動主要在於:每個鎖競爭者,只需要關注/shared_lock節點下序號比自己小的那個節點是否存在即可。
-
分散式佇列
分散式佇列可以簡單分為先入先出佇列模型和等待佇列元素聚集後統一安排處理執行的Barrier模型。
① FIFO先入先出,先進入佇列的請求操作先完成後,才會開始處理後面的請求。FIFO佇列就類似於全寫的共享模型,所有客戶端都會到/queue_fifo這個節點下建立一個臨時節點,如/queue_fifo/host1-00000001。
建立完節點後,按照如下步驟執行。
1. 通過呼叫getChildren介面來獲取/queue_fifo節點的所有子節點,即獲取佇列中所有的元素。
2. 確定自己的節點序號在所有子節點中的順序。
3. 如果自己的序號不是最小,那麼需要等待,同時向比自己序號小的最後一個節點註冊Watcher監聽。
4. 接收到Watcher通知後,重複步驟1。
② Barrier分散式屏障,最終的合併計算需要基於很多平行計算的子結果來進行,開始時,/queue_barrier節點已經預設存在,並且將結點資料內容賦值為數字n來代表Barrier值,之後,所有客戶端都會到/queue_barrier節點下建立一個臨時節點,例如/queue_barrier/host1。
建立完節點後,按照如下步驟執行。
1. 通過呼叫getData介面獲取/queue_barrier節點的資料內容,如10。
2. 通過呼叫getChildren介面獲取/queue_barrier節點下的所有子節點,同時註冊對子節點變更的Watcher監聽。
3. 統計子節點的個數。
4. 如果子節點個數還不足10個,那麼需要等待。
5. 接受到Wacher通知後,重複步驟3。