聊一聊 Zookeeper 客戶端之 Curator

glmapper發表於2019-04-13

原文連結:ZooKeeper 客戶端之 Curator

ZooKeeper 是一個分散式的、開放原始碼的分散式應用程式協調服務,是 Google 的 Chubby 一個開源的實現。它是叢集的管理者,監視著叢集中各個節點的狀態,根據節點提交的反饋進行下一步合理操作。最終,將簡單易用的介面和效能高效、功能穩定的系統提供給使用者。

Curator 是 Netflix 公司開源的一套 Zookeeper 客戶端框架,解決了很多 Zookeeper 客戶端非常底層的細節開發工作,包括連線重連、反覆註冊 Watcher 和 NodeExistsException 異常等等。Curator 包含了幾個包:

  • curator-framework:對 Zookeeper 的底層 api 的一些封裝
  • curator-client:提供一些客戶端的操作,例如重試策略等
  • curator-recipes:封裝了一些高階特性,如:Cache 事件監聽、選舉、分散式鎖、分散式計數器、分散式Barrier 等

Curator 和 zookeeper 的版本問題

目前 Curator 有 2.x.x 和 3.x.x 兩個系列的版本,支援不同版本的 Zookeeper。其中Curator 2.x.x 相容 Zookeeper的 3.4.x 和 3.5.x。而 Curator 3.x.x 只相容 Zookeeper 3.5.x,並且提供了一些諸如動態重新配置、watch刪除等新特性。

Curator 2.x.x - compatible with both ZooKeeper 3.4.x and ZooKeeper 3.5.x
Curator 3.x.x - compatible only with ZooKeeper 3.5.x and includes support for new
複製程式碼

如果跨版本會有相容性問題,很有可能導致節點操作失敗,當時在使用的時候就踩了這個坑,拋瞭如下的異常:

KeeperErrorCode = Unimplemented for /***
複製程式碼

Curator API

這裡就不對比與原生 API 的區別了,Curator 的 API 直接通過 org.apache.curator.framework.CuratorFramework 介面來看,並結合相應的案例進行使用,以備後用。

為了可以直觀的看到 Zookeeper 的節點資訊,可以考慮弄一個 zk 的管控介面,常見的有 zkui 和 zkweb。

zkui:github.com/DeemOpen/zk…

zkweb:github.com/zhitom/zkwe…

我用的 zkweb ,雖然介面上看起來沒有 zkui 精簡,但是在層次展示和一些細節上感覺比 zkui 好一點

環境準備

之前寫的一個在 Linux 上安裝部署 Zookeeper 的筆記,其他作業系統請自行谷歌教程吧。

本文案例工程已經同步到了 github,傳送門

PS : 目前還沒有看過Curator的具體原始碼,所以不會涉及到任何原始碼解析、實現原理的東西;本篇主要是實際使用時的一些記錄,以備後用。如果文中錯誤之處,希望各位指出。

Curator 客戶端的初始化和初始化時機

在實際的工程中,Zookeeper 客戶端的初始化會在程式啟動期間完成。

初始化時機

在 Spring 或者 SpringBoot 工程中最常見的就是繫結到容器啟動的生命週期或者應用啟動的生命週期中:

  • 監聽 ContextRefreshedEvent 事件,在容器重新整理完成之後初始化 Zookeeper
  • 監聽 ApplicationReadyEvent/ApplicationStartedEvent 事件,初始化 Zookeeper 客戶端

除了上面的方式之外,還有一種常見的是繫結到 bean 的生命週期中

  • 實現 InitializingBean 介面 ,在 afterPropertiesSet 中完成 Zookeeper 客戶端初始化

關於 SpringBoot中的事件機制可以參考之前寫過的一篇文章:SpringBoot-SpringBoot中的事件機制

Curator 初始化

這裡使用 InitializingBean 的這種方式,程式碼如下:

public class ZookeeperCuratorClient implements InitializingBean {
    private CuratorFramework curatorClient;
    @Value("${glmapper.zookeeper.address:localhost:2181}")
    private String           connectString;
    @Value("${glmapper.zookeeper.baseSleepTimeMs:1000}")
    private int              baseSleepTimeMs;
    @Value("${glmapper.zookeeper.maxRetries:3}")
    private int              maxRetries;
    @Value("${glmapper.zookeeper.sessionTimeoutMs:6000}")
    private int              sessionTimeoutMs;
    @Value("${glmapper.zookeeper.connectionTimeoutMs:6000}")
    private int              connectionTimeoutMs;
  
    @Override
    public void afterPropertiesSet() throws Exception {
        // custom policy
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries);
        // to build curatorClient
        curatorClient = CuratorFrameworkFactory.builder().connectString(connectString)
                .sessionTimeoutMs(sessionTimeoutMs).connectionTimeoutMs(connectionTimeoutMs)
                .retryPolicy(retryPolicy).build();
        curatorClient.start();
    }

    public CuratorFramework getCuratorClient() {
        return curatorClient;
    }
}
複製程式碼

glmapper.zookeeper.xxx 是本例中需要在配置檔案中配置的 zookeeper 的一些引數,引數解釋如下:

  • baseSleepTimeMs:重試之間等待的初始時間
  • maxRetries:最大重試次數
  • connectString:要連線的伺服器列表
  • sessionTimeoutMs:session 超時時間
  • connectionTimeoutMs:連線超時時間

另外,Curator 客戶端初始化時還需要指定重試策略,RetryPolicy 介面是 Curator 中重試連線(當zookeeper失去連線時使用)策略的頂級介面,其類繼承體系如下圖所示:

聊一聊 Zookeeper 客戶端之 Curator

  • RetryOneTime:只重連一次
  • RetryNTime:指定重連的次數N
  • RetryUtilElapsed:指定最大重連超時時間和重連時間間隔,間歇性重連直到超時或者連結成功
  • ExponentialBackoffRetry:基於 "backoff"方式重連,和 RetryUtilElapsed 的區別是重連的時間間隔是動態的。
  • BoundedExponentialBackoffRetry: 同 ExponentialBackoffRetry的區別是增加了最大重試次數的控制

除上述之外,在一些場景中,需要對不同的業務進行隔離,這種情況下,可以通過設定 namespace 來解決,namespace 實際上就是指定zookeeper的根路徑,設定之後,後面的所有操作都會基於該根目錄。

Curator 基礎 API 使用

檢查節點是否存在

checkExists 方法返回的是一個 ExistsBuilder 構造器,這個構建器將返回一個 Stat 物件,就像呼叫了 org.apache.zookeeper.ZooKeeper.exists()一樣。null 表示它不存在,而實際的 Stat 物件表示存在。

public void checkNodeExist(String path) throws Exception {
    Stat stat = curatorClient.checkExists().forPath(path);
    if (stat != null){
        throw new RuntimeException("path = "+path +" has bean exist.");
    }
}
複製程式碼

建議在實際的應用中,操作節點時對所需操作的節點進行 checkExists。

新增節點

  • 非遞迴方式建立節點

    curatorClient.create().forPath("/glmapper");
    curatorClient.create().forPath("/glmapper/test");
    複製程式碼

    先建立/glmapper,然後再在/glmapper 下面建立 /test ,如果直接使用 /glmapper/test 沒有先建立 /glmapper 時,會丟擲異常:

    org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode = NoNode for /glmapper/test
    複製程式碼

    如果需要在建立節點時指定節點中資料,則可以這樣:

    curatorClient.create().forPath("/glmapper","data".getBytes());
    複製程式碼

    指定節點型別(EPHEMERAL 臨時節點)

    curatorClient.create().withMode(CreateMode.EPHEMERAL).forPath("/glmapper","data".getBytes());
    複製程式碼
  • 遞迴方式建立節點

    遞迴方式建立節點有兩個方法,creatingParentsIfNeeded 和 creatingParentContainersIfNeeded。在新版本的 zookeeper 這兩個遞迴建立方法會有區別; creatingParentContainersIfNeeded() 以容器模式遞迴建立節點,如果舊版本 zookeeper,此方法等於creatingParentsIfNeeded()。

    在非遞迴方式情況下,如果直接建立 /glmapper/test 會報錯,那麼在遞迴的方式下則是可以的

    curatorClient.create().creatingParentContainersIfNeeded().forPath("/glmapper/test");
    複製程式碼

    在遞迴呼叫中,如果不指定 CreateMode,則預設PERSISTENT,如果指定為臨時節點,則最終節點會是臨時節點,父節點仍舊是PERSISTENT

刪除節點

  • 非遞迴刪除節點

    curatorClient.delete().forPath("/glmapper/test");
    複製程式碼

    指定具體版本

    curatorClient.delete().withVersion(-1).forPath("/glmapper/test");
    複製程式碼

    使用 guaranteed 方式刪除,guaranteed 會保證在session有效的情況下,後臺持續進行該節點的刪除操作,直到刪除掉

    curatorClient.delete().guaranteed().withVersion(-1).forPath("/glmapper/test");
    複製程式碼
  • 遞迴刪除當前節點及其子節點

    curatorClient.delete().deletingChildrenIfNeeded().forPath("/glmapper/test");
    複製程式碼

獲取節點資料

獲取節點資料

byte[] data = curatorClient.getData().forPath("/glmapper/test");
複製程式碼

根據配置的壓縮提供程式對資料進行解壓縮處理

byte[] data = curatorClient.getData().decompressed().forPath("/glmapper/test");
複製程式碼

讀取資料並獲得Stat資訊

Stat stat = new Stat();
byte[] data = curatorClient.getData().storingStatIn(stat).forPath("/glmapper/test");
複製程式碼

更新節點資料

設定指定值

curatorClient.setData().forPath("/glmapper/test","newData".getBytes());
複製程式碼

設定資料並使用配置的壓縮提供程式壓縮資料

curatorClient.setData().compressed().forPath("/glmapper/test","newData".getBytes());
複製程式碼

設定資料,並指定版本

curatorClient.setData().withVersion(-1).forPath("/glmapper/test","newData".getBytes());
複製程式碼

獲取子列表

List<String> childrenList = curatorClient.getChildren().forPath("/glmapper");
複製程式碼

事件

Curator 也對 Zookeeper 典型場景之事件監聽進行封裝,這部分能力實在 curator-recipes 包下的。

事件型別

在使用不同的方法時會有不同的事件發生

public enum CuratorEventType
{
    //Corresponds to {@link CuratorFramework#create()}
    CREATE,
    //Corresponds to {@link CuratorFramework#delete()}
    DELETE,
		//Corresponds to {@link CuratorFramework#checkExists()}
    EXISTS,
		//Corresponds to {@link CuratorFramework#getData()}
    GET_DATA,
		//Corresponds to {@link CuratorFramework#setData()}
    SET_DATA,
		//Corresponds to {@link CuratorFramework#getChildren()}
    CHILDREN,
		//Corresponds to {@link CuratorFramework#sync(String, Object)}
    SYNC,
		//Corresponds to {@link CuratorFramework#getACL()}
    GET_ACL,
		//Corresponds to {@link CuratorFramework#setACL()}
    SET_ACL,
		//Corresponds to {@link Watchable#usingWatcher(Watcher)} or {@link Watchable#watched()}
    WATCHED,
		//Event sent when client is being closed
    CLOSING
}
複製程式碼

事件監聽

一次性監聽方式:Watcher

利用 Watcher 來對節點進行監聽操作,可以典型業務場景需要使用可考慮,但一般情況不推薦使用。

 byte[] data = curatorClient.getData().usingWatcher(new Watcher() {
     @Override
     public void process(WatchedEvent watchedEvent) {
       System.out.println("監聽器 watchedEvent:" + watchedEvent);
     }
 }).forPath("/glmapper/test");
System.out.println("監聽節點內容:" + new String(data));
// 第一次變更節點資料
curatorClient.setData().forPath("/glmapper/test","newData".getBytes());
// 第二次變更節點資料
curatorClient.setData().forPath("/glmapper/test","newChangedData".getBytes());
複製程式碼

上面這段程式碼對 /glmapper/test 節點註冊了一個 Watcher 監聽事件,並且返回當前節點的內容。後面進行兩次資料變更,實際上第二次變更時,監聽已經失效,無法再次獲得節點變動事件了。測試中控制檯輸出的資訊如下:

監聽節點內容:data
watchedEvent:WatchedEvent state:SyncConnected type:NodeDataChanged path:/glmapper/test
複製程式碼

CuratorListener 方式

CuratorListener 監聽,此監聽主要針對 background 通知和錯誤通知。使用此監聽器之後,呼叫inBackground 方法會非同步獲得監聽,對於節點的建立或修改則不會觸發監聽事件。

CuratorListener listener = new CuratorListener(){
    @Override
    public void eventReceived(CuratorFramework client, CuratorEvent event) throws Exception {
      System.out.println("event : " + event);
    }
 };
// 繫結監聽器
curatorClient.getCuratorListenable().addListener(listener);
// 非同步獲取節點資料
curatorClient.getData().inBackground().forPath("/glmapper/test");
// 更新節點資料
curatorClient.setData().forPath("/glmapper/test","newData".getBytes());
複製程式碼

測試中控制檯輸出的資訊如下:

event : CuratorEventImpl{type=GET_DATA, resultCode=0, path='/glmapper/test', name='null', children=null, context=null, stat=5867,5867,1555140974671,1555140974671,0,0,0,0,4,0,5867
, data=[100, 97, 116, 97], watchedEvent=null, aclList=null}
複製程式碼

這裡只觸發了一次監聽回撥,就是 getData 。

Curator 引入的 Cache 事件監聽機制

Curator 引入了 Cache 來實現對 Zookeeper 服務端事件監聽,Cache 事件監聽可以理解為一個本地快取檢視與遠端 Zookeeper 檢視的對比過程。Cache 提供了反覆註冊的功能。Cache 分為兩類註冊型別:節點監聽和子節點監聽。

  • NodeCache

    監聽資料節點本身的變化。對節點的監聽需要配合回撥函式來進行處理接收到監聽事件之後的業務處理。NodeCache 通過 NodeCacheListener 來完成後續處理。

    String path = "/glmapper/test";
    final NodeCache nodeCache = new NodeCache(curatorClient,path);
    //如果設定為true則在首次啟動時就會快取節點內容到Cache中。 nodeCache.start(true);
    nodeCache.start();
    nodeCache.getListenable().addListener(new NodeCacheListener() {
    @Override
    public void nodeChanged() throws Exception {
    System.out.println("觸發監聽回撥,當前節點資料為:" + new String(nodeCache.getCurrentData().getData()));
    }
    });
    curatorClient.setData().forPath(path,"1".getBytes());
    curatorClient.setData().forPath(path,"2".getBytes());
    curatorClient.setData().forPath(path,"3".getBytes());
    curatorClient.setData().forPath(path,"4".getBytes());
    curatorClient.setData().forPath(path,"5".getBytes());
    curatorClient.setData().forPath(path,"6".getBytes());
    複製程式碼

    注意:在測試過程中,nodeCache.start(),NodeCache 在先後多次修改監聽節點的內容時,出現了丟失事件現象,在用例執行的5次中,僅一次監聽到了全部事件;如果 nodeCache.start(true),NodeCache 在先後多次修改監聽節點的內容時,不會出現丟失現象。

    NodeCache不僅可以監聽節點內容變化,還可以監聽指定節點是否存在。如果原本節點不存在,那麼Cache就會在節點被建立時觸發監聽事件,如果該節點被刪除,就無法再觸發監聽事件。

  • PathChildrenCache

    PathChildrenCache 不會對二級子節點進行監聽,只會對子節點進行監聽。

    String path = "/glmapper";
    PathChildrenCache pathChildrenCache = new PathChildrenCache(curatorClient,path,true);
    // 如果設定為true則在首次啟動時就會快取節點內容到Cache中。 nodeCache.start(true);
    pathChildrenCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
    pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
      @Override
      public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent event) throws Exception {
        System.out.println("-----------------------------");
        System.out.println("event:"  + event.getType());
        if (event.getData()!=null){
          System.out.println("path:" + event.getData().getPath());
        }
        System.out.println("-----------------------------");
      }
    });
    zookeeperCuratorClient.createNode("/glmapper/test","data".getBytes(),CreateMode.PERSISTENT);
    Thread.sleep(1000);
    curatorClient.setData().forPath("/glmapper/test","1".getBytes());
    Thread.sleep(1000);
    curatorClient.setData().forPath("/glmapper/test","2".getBytes());
    Thread.sleep(1000);
    zookeeperCuratorClient.createNode("/glmapper/test/second","data".getBytes(),CreateMode.PERSISTENT);
    Thread.sleep(1000);
    curatorClient.setData().forPath("/glmapper/test/second","1".getBytes());
    Thread.sleep(1000);
    curatorClient.setData().forPath("/glmapper/test/second","2".getBytes());
    Thread.sleep(1000);
    複製程式碼

    注意:在測試過程中發現,如果連續兩個操作之間不進行一定時間的間隔,會導致無法監聽到下一次事件。因此只會監聽子節點,所以對二級子節點 /second 下面的操作是監聽不到的。測試中控制檯輸出的資訊如下:

    -----------------------------
    event:CHILD_ADDED
    path:/glmapper/test
    -----------------------------
    -----------------------------
    event:INITIALIZED
    -----------------------------
    -----------------------------
    event:CHILD_UPDATED
    path:/glmapper/test
    -----------------------------
    -----------------------------
    event:CHILD_UPDATED
    path:/glmapper/test
    -----------------------------
    複製程式碼
  • TreeCache

    TreeCache 使用一個內部類TreeNode來維護這個一個樹結構。並將這個樹結構與ZK節點進行了對映。所以TreeCache 可以監聽當前節點下所有節點的事件。

    String path = "/glmapper";
    TreeCache treeCache = new TreeCache(curatorClient,path);
    treeCache.getListenable().addListener((client,event)-> {
        System.out.println("-----------------------------");
        System.out.println("event:"  + event.getType());
        if (event.getData()!=null){
          System.out.println("path:" + event.getData().getPath());
        }
        System.out.println("-----------------------------");
    });
    treeCache.start();
    zookeeperCuratorClient.createNode("/glmapper/test","data".getBytes(),CreateMode.PERSISTENT);
    Thread.sleep(1000);
    curatorClient.setData().forPath("/glmapper/test","1".getBytes());
    Thread.sleep(1000);
    curatorClient.setData().forPath("/glmapper/test","2".getBytes());
    Thread.sleep(1000);
    zookeeperCuratorClient.createNode("/glmapper/test/second","data".getBytes(),CreateMode.PERSISTENT);
    Thread.sleep(1000);
    curatorClient.setData().forPath("/glmapper/test/second","1".getBytes());
    Thread.sleep(1000);
    curatorClient.setData().forPath("/glmapper/test/second","2".getBytes());
    Thread.sleep(1000);
    複製程式碼

    測試中控制檯輸出的資訊如下:

    -----------------------------
    event:NODE_ADDED
    path:/glmapper
    -----------------------------
    -----------------------------
    event:NODE_ADDED
    path:/glmapper/test
    -----------------------------
    -----------------------------
    event:NODE_UPDATED
    path:/glmapper/test
    -----------------------------
    -----------------------------
    event:NODE_UPDATED
    path:/glmapper/test
    -----------------------------
    -----------------------------
    event:NODE_ADDED
    path:/glmapper/test/second
    -----------------------------
    -----------------------------
    event:NODE_UPDATED
    path:/glmapper/test/second
    -----------------------------
    -----------------------------
    event:NODE_UPDATED
    path:/glmapper/test/second
    -----------------------------
    複製程式碼

事務操作

CuratorFramework 的例項包含 inTransaction( ) 介面方法,呼叫此方法開啟一個 ZooKeeper 事務。 可以複合create、 setData、 check、and/or delete 等操作然後呼叫 commit() 作為一個原子操作提交。

// 開啟事務  
CuratorTransaction curatorTransaction = curatorClient.inTransaction();
Collection<CuratorTransactionResult> commit = 
  // 操作1 
curatorTransaction.create().withMode(CreateMode.EPHEMERAL).forPath("/glmapper/transaction")
  .and()
  // 操作2 
  .delete().forPath("/glmapper/test")
  .and()
  // 操作3
  .setData().forPath("/glmapper/transaction", "data".getBytes())
  .and()
  // 提交事務
  .commit();
Iterator<CuratorTransactionResult> iterator = commit.iterator();
while (iterator.hasNext()){
  CuratorTransactionResult next = iterator.next();
  System.out.println(next.getForPath());
  System.out.println(next.getResultPath());
  System.out.println(next.getType());
}
複製程式碼

這裡debug看了下Collection資訊,皮膚如下:

聊一聊 Zookeeper 客戶端之 Curator

非同步操作

前面提到的增刪改查都是同步的,但是 Curator 也提供了非同步介面,引入了 BackgroundCallback 介面用於處理非同步介面呼叫之後服務端返回的結果資訊。BackgroundCallback 介面中一個重要的回撥值為 CuratorEvent,裡面包含事件型別、響應嗎和節點的詳細資訊。

在使用上也是非常簡單的,只需要帶上 inBackground() 就行,如下:

 curatorClient.getData().inBackground().forPath("/glmapper/test");
複製程式碼

通過檢視 inBackground 方法定義可以看到,inBackground 支援自定義執行緒池來處理返回結果之後的業務邏輯。

public T inBackground(BackgroundCallback callback, Executor executor);
複製程式碼

這裡就不貼程式碼了。

小結

本文主要圍繞 Curator 的基本 API 進行了學習記錄,對於原理及原始碼部分沒有涉及。這部分如果有時間在慢慢研究吧。另外像分散式鎖、分散式自增序列等實現停留在理論階段,沒有實踐,不敢妄論,用到再碼吧。

參考

相關文章