Curator(ZoooKeeper客戶端)使用詳解

小姐姐味道發表於2019-07-09

更多精彩文章。

《微服務不是全部,只是特定領域的子集》

《“分庫分表" ?選型和流程要慎重,否則會失控》

這麼多監控元件,總有一款適合你

《Linux生產環境上,最常用的一套“vim“技巧》

《使用Netty,我們到底在開發些什麼?》

最有用系列:

《Linux生產環境上,最常用的一套“vim“技巧》

《Linux生產環境上,最常用的一套“Sed“技巧》

《Linux生產環境上,最常用的一套“AWK“技巧》

歡迎Linux和java後端的同學關注公眾號。

注:該文件主要是基於官方文件的說明,具體可檢視:curator.apache.org/index.html

Curator是netflix公司開源的一套zookeeper客戶端,目前是Apache的頂級專案。與Zookeeper提供的原生客戶端相比,Curator的抽象層次更高,簡化了Zookeeper客戶端的開發量。Curator解決了很多zookeeper客戶端非常底層的細節開發工作,包括連線重連、反覆註冊wathcer和NodeExistsException 異常等。

Curator由一系列的模組構成,對於一般開發者而言,常用的是curator-framework和curator-recipes,下面對此依次介紹。

1.maven依賴問題

最新版本的curator 4.0支援zookeeper 3.4.x和3.5,但是需要注意curator傳遞進來的依賴,需要和實際伺服器端使用的版本相符,以我們目前使用的zookeeper 3.4.6為例

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>4.0.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.0.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.6</version>
</dependency>
複製程式碼

2.curator-framework

public static CuratorFramework getClient() {
    return CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2181")
            .retryPolicy(new ExponentialBackoffRetry(1000, 3))
            .connectionTimeoutMs(15 * 1000) //連線超時時間,預設15秒
            .sessionTimeoutMs(60 * 1000) //會話超時時間,預設60秒
            .namespace("arch") //設定名稱空間
            .build();
}
 
public static void create(final CuratorFramework client, final String path, final byte[] payload) throws Exception {
    client.create().creatingParentsIfNeeded().forPath(path, payload);
}
 
public static void createEphemeral(final CuratorFramework client, final String path, final byte[] payload) throws Exception {
    client.create().withMode(CreateMode.EPHEMERAL).forPath(path, payload);
}
 
public static String createEphemeralSequential(final CuratorFramework client, final String path, final byte[] payload) throws Exception {
    return client.create().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, payload);
}
 
public static void setData(final CuratorFramework client, final String path, final byte[] payload) throws Exception {
    client.setData().forPath(path, payload);
}
 
public static void delete(final CuratorFramework client, final String path) throws Exception {
    client.delete().deletingChildrenIfNeeded().forPath(path);
}
 
public static void guaranteedDelete(final CuratorFramework client, final String path) throws Exception {
    client.delete().guaranteed().forPath(path);
}
 
public static String getData(final CuratorFramework client, final String path) throws Exception {
    return new String(client.getData().forPath(path));
}
 
public static List<String> getChildren(final CuratorFramework client, final String path) throws Exception {
    return client.getChildren().forPath(path);
}
複製程式碼

3.curator-recipes

curator-recipes 提供了一些zk的典型使用場景的參考,主要介紹一下開發中常用的元件。

事件監聽

zookeeper原生支援通過註冊watcher來進行事件監聽,但是其使用不是特別方便,需要開發人員自己反覆註冊watcher,比較繁瑣。Curator引入Cache來實現對zookeeper服務端事務的監聽。Cache是Curator中對事件監聽的包裝,其對事件的監聽其實可以近似看作是一個本地快取檢視和遠端Zookeeper檢視的對比過程。同時Curator能夠自動為開發人員處理反覆註冊監聽,從而大大簡化原生api開發的繁瑣過程。

1)Node Cache

public static void nodeCache() throws Exception {
    final String path = "/nodeCache";
    final CuratorFramework client = getClient();
    client.start();
 
    delete(client, path);
    create(client, path, "cache".getBytes());
 
    final NodeCache nodeCache = new NodeCache(client, path);
    nodeCache.start(true);
    nodeCache.getListenable()
            .addListener(() -> System.out.println("node data change, new data is " + new String(nodeCache.getCurrentData().getData())));
 
    setData(client, path, "cache1".getBytes());
    setData(client, path, "cache2".getBytes());
 
    Thread.sleep(1000);
 
    client.close();
}
複製程式碼

NodeCache可以監聽指定的節點,註冊監聽器後,節點的變化會通知相應的監聽器

2)Path Cache

Path Cache 用來監聽ZNode的子節點事件,包括added、updateed、removed,Path Cache會同步子節點的狀態,產生的事件會傳遞給註冊的PathChildrenCacheListener。

public static void pathChildrenCache() throws Exception {
        final String path = "/pathChildrenCache";
        final CuratorFramework client = getClient();
        client.start();
 
        final PathChildrenCache cache = new PathChildrenCache(client, path, true);
        cache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
 
        cache.getListenable().addListener((client1, event) -> {
            switch (event.getType()) {
                case CHILD_ADDED:
                    System.out.println("CHILD_ADDED:" + event.getData().getPath());
                    break;
                case CHILD_REMOVED:
                    System.out.println("CHILD_REMOVED:" + event.getData().getPath());
                    break;
                case CHILD_UPDATED:
                    System.out.println("CHILD_UPDATED:" + event.getData().getPath());
                    break;
                case CONNECTION_LOST:
                    System.out.println("CONNECTION_LOST:" + event.getData().getPath());
                    break;
                case CONNECTION_RECONNECTED:
                    System.out.println("CONNECTION_RECONNECTED:" + event.getData().getPath());
                    break;
                case CONNECTION_SUSPENDED:
                    System.out.println("CONNECTION_SUSPENDED:" + event.getData().getPath());
                    break;
                case INITIALIZED:
                    System.out.println("INITIALIZED:" + event.getData().getPath());
                    break;
                default:
                    break;
            }
        });
 
//        client.create().withMode(CreateMode.PERSISTENT).forPath(path);
        Thread.sleep(1000);
 
        client.create().withMode(CreateMode.PERSISTENT).forPath(path + "/c1");
        Thread.sleep(1000);
 
        client.delete().forPath(path + "/c1");
        Thread.sleep(1000);
 
        client.delete().forPath(path); //監聽節點本身的變化不會通知
        Thread.sleep(1000);
 
        client.close();
    }
複製程式碼

3)Tree Cache

Path Cache和Node Cache的“合體”,監視路徑下的建立、更新、刪除事件,並快取路徑下所有孩子結點的資料。

public static void treeCache() throws Exception {
    final String path = "/treeChildrenCache";
    final CuratorFramework client = getClient();
    client.start();
 
    final TreeCache cache = new TreeCache(client, path);
    cache.start();
 
    cache.getListenable().addListener((client1, event) -> {
        switch (event.getType()){
            case NODE_ADDED:
                System.out.println("NODE_ADDED:" + event.getData().getPath());
                break;
            case NODE_REMOVED:
                System.out.println("NODE_REMOVED:" + event.getData().getPath());
                break;
            case NODE_UPDATED:
                System.out.println("NODE_UPDATED:" + event.getData().getPath());
                break;
            case CONNECTION_LOST:
                System.out.println("CONNECTION_LOST:" + event.getData().getPath());
                break;
            case CONNECTION_RECONNECTED:
                System.out.println("CONNECTION_RECONNECTED:" + event.getData().getPath());
                break;
            case CONNECTION_SUSPENDED:
                System.out.println("CONNECTION_SUSPENDED:" + event.getData().getPath());
                break;
            case INITIALIZED:
                System.out.println("INITIALIZED:" + event.getData().getPath());
                break;
            default:
                break;
        }
    });
 
    client.create().withMode(CreateMode.PERSISTENT).forPath(path);
    Thread.sleep(1000);
 
    client.create().withMode(CreateMode.PERSISTENT).forPath(path + "/c1");
    Thread.sleep(1000);
 
    setData(client, path, "test".getBytes());
    Thread.sleep(1000);
 
    client.delete().forPath(path + "/c1");
    Thread.sleep(1000);
 
    client.delete().forPath(path);
    Thread.sleep(1000);
 
    client.close();
}
複製程式碼

選舉

curator提供了兩種方式,分別是Leader Latch和Leader Election。

1)Leader Latch

隨機從候選著中選出一臺作為leader,選中之後除非呼叫close()釋放leadship,否則其他的後選擇無法成為leader

public class LeaderLatchTest {
 
    private static final String PATH = "/demo/leader";
 
    public static void main(String[] args) {
        List<LeaderLatch> latchList = new ArrayList<>();
        List<CuratorFramework> clients = new ArrayList<>();
        try {
            for (int i = 0; i < 10; i++) {
                CuratorFramework client = getClient();
                client.start();
                clients.add(client);
 
                final LeaderLatch leaderLatch = new LeaderLatch(client, PATH, "client#" + i);
                leaderLatch.addListener(new LeaderLatchListener() {
                    @Override
                    public void isLeader() {
                        System.out.println(leaderLatch.getId() + ":I am leader. I am doing jobs!");
                    }
 
                    @Override
                    public void notLeader() {
                        System.out.println(leaderLatch.getId() + ":I am not leader. I will do nothing!");
                    }
                });
                latchList.add(leaderLatch);
                leaderLatch.start();
            }
            Thread.sleep(1000 * 60);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            for (CuratorFramework client : clients) {
                CloseableUtils.closeQuietly(client);
            }
 
            for (LeaderLatch leaderLatch : latchList) {
                CloseableUtils.closeQuietly(leaderLatch);
            }
        }
    }
 
    public static CuratorFramework getClient() {
        return CuratorFrameworkFactory.builder()
                .connectString("127.0.0.1:2181")
                .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                .connectionTimeoutMs(15 * 1000) //連線超時時間,預設15秒
                .sessionTimeoutMs(60 * 1000) //會話超時時間,預設60秒
                .namespace("arch") //設定名稱空間
                .build();
    }
 
}
複製程式碼

2)Leader Election

通過LeaderSelectorListener可以對領導權進行控制, 在適當的時候釋放領導權,這樣每個節點都有可能獲得領導權。 而LeaderLatch則一直持有leadership, 除非呼叫close方法,否則它不會釋放領導權。

public class LeaderSelectorTest {
    private static final String PATH = "/demo/leader";
 
    public static void main(String[] args) {
        List<LeaderSelector> selectors = new ArrayList<>();
        List<CuratorFramework> clients = new ArrayList<>();
        try {
            for (int i = 0; i < 10; i++) {
                CuratorFramework client = getClient();
                client.start();
                clients.add(client);
 
                final String name = "client#" + i;
                LeaderSelector leaderSelector = new LeaderSelector(client, PATH, new LeaderSelectorListenerAdapter() {
                    @Override
                    public void takeLeadership(CuratorFramework client) throws Exception {
                        System.out.println(name + ":I am leader.");
                        Thread.sleep(2000);
                    }
                });
 
                leaderSelector.autoRequeue();
                leaderSelector.start();
                selectors.add(leaderSelector);
            }
            Thread.sleep(Integer.MAX_VALUE);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            for (CuratorFramework client : clients) {
                CloseableUtils.closeQuietly(client);
            }
 
            for (LeaderSelector selector : selectors) {
                CloseableUtils.closeQuietly(selector);
            }
 
        }
    }
 
    public static CuratorFramework getClient() {
        return CuratorFrameworkFactory.builder()
                .connectString("127.0.0.1:2181")
                .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                .connectionTimeoutMs(15 * 1000) //連線超時時間,預設15秒
                .sessionTimeoutMs(60 * 1000) //會話超時時間,預設60秒
                .namespace("arch") //設定名稱空間
                .build();
    }
 
}
複製程式碼

分散式鎖

1)可重入鎖Shared Reentrant Lock

Shared意味著鎖是全域性可見的, 客戶端都可以請求鎖。 Reentrant和JDK的ReentrantLock類似, 意味著同一個客戶端在擁有鎖的同時,可以多次獲取,不會被阻塞。 它是由類InterProcessMutex來實現。 它的建構函式為:

public InterProcessMutex(CuratorFramework client, String path)
複製程式碼

通過acquire獲得鎖,並提供超時機制:

/**
* Acquire the mutex - blocking until it's available. Note: the same thread can call acquire
* re-entrantly. Each call to acquire must be balanced by a call to release()
*/
public void acquire();
 
/**
* Acquire the mutex - blocks until it's available or the given time expires. Note: the same thread can
* call acquire re-entrantly. Each call to acquire that returns true must be balanced by a call to release()
* Parameters:
* time - time to wait
* unit - time unit
* Returns:
* true if the mutex was acquired, false if not
*/
public boolean acquire(long time, TimeUnit unit);
複製程式碼

通過release()方法釋放鎖。 InterProcessMutex 例項可以重用。 Revoking ZooKeeper recipes wiki定義了可協商的撤銷機制。 為了撤銷mutex, 呼叫下面的方法:

/**
* 將鎖設為可撤銷的. 當別的程式或執行緒想讓你釋放鎖時Listener會被呼叫。
* Parameters:
* listener - the listener
*/
public void makeRevocable(RevocationListener<T> listener)
複製程式碼

2)不可重入鎖Shared Lock

使用InterProcessSemaphoreMutex,呼叫方法類似,區別在於該鎖是不可重入的,在同一個執行緒中不可重入

3)可重入讀寫鎖Shared Reentrant Read Write Lock

類似JDK的ReentrantReadWriteLock. 一個讀寫鎖管理一對相關的鎖。 一個負責讀操作,另外一個負責寫操作。 讀操作在寫鎖沒被使用時可同時由多個程式使用,而寫鎖使用時不允許讀 (阻塞)。 此鎖是可重入的。一個擁有寫鎖的執行緒可重入讀鎖,但是讀鎖卻不能進入寫鎖。 這也意味著寫鎖可以降級成讀鎖, 比如請求寫鎖 —>讀鎖 —->釋放寫鎖。 從讀鎖升級成寫鎖是不成的。 主要由兩個類實現:

InterProcessReadWriteLock
InterProcessLock
複製程式碼

4)訊號量Shared Semaphore

一個計數的訊號量類似JDK的Semaphore。 JDK中Semaphore維護的一組許可(permits),而Cubator中稱之為租約(Lease)。注意,所有的例項必須使用相同的numberOfLeases值。 呼叫acquire會返回一個租約物件。 客戶端必須在finally中close這些租約物件,否則這些租約會丟失掉。 但是, 但是,如果客戶端session由於某種原因比如crash丟掉, 那麼這些客戶端持有的租約會自動close, 這樣其它客戶端可以繼續使用這些租約。 租約還可以通過下面的方式返還:

public void returnAll(Collection<Lease> leases)
public void returnLease(Lease lease)
複製程式碼

注意一次你可以請求多個租約,如果Semaphore當前的租約不夠,則請求執行緒會被阻塞。 同時還提供了超時的過載方法:

public Lease acquire()
public Collection<Lease> acquire(int qty)
public Lease acquire(long time, TimeUnit unit)
public Collection<Lease> acquire(int qty, long time, TimeUnit unit)
複製程式碼

主要類有:

InterProcessSemaphoreV2
Lease
SharedCountReader
複製程式碼

5)多鎖物件Multi Shared Lock

Multi Shared Lock是一個鎖的容器。 當呼叫acquire, 所有的鎖都會被acquire,如果請求失敗,所有的鎖都會被release。 同樣呼叫release時所有的鎖都被release(失敗被忽略)。 基本上,它就是組鎖的代表,在它上面的請求釋放操作都會傳遞給它包含的所有的鎖。 主要涉及兩個類:

InterProcessMultiLock
InterProcessLock
複製程式碼

它的建構函式需要包含的鎖的集合,或者一組ZooKeeper的path。

public InterProcessMultiLock(List<InterProcessLock> locks)
public InterProcessMultiLock(CuratorFramework client, List<String> paths)
複製程式碼

柵欄barrier

1)DistributedBarrier建構函式中barrierPath引數用來確定一個柵欄,只要barrierPath引數相同(路徑相同)就是同一個柵欄。通常情況下柵欄的使用如下:
1.主導client設定一個柵欄
2.其他客戶端就會呼叫waitOnBarrier()等待柵欄移除,程式處理執行緒阻塞
3.主導client移除柵欄,其他客戶端的處理程式就會同時繼續執行。
DistributedBarrier類的主要方法如下:
setBarrier() - 設定柵欄
waitOnBarrier() - 等待柵欄移除
removeBarrier() - 移除柵欄

2)雙柵欄Double Barrier
雙柵欄允許客戶端在計算的開始和結束時同步。當足夠的程式加入到雙柵欄時,程式開始計算,當計算完成時,離開柵欄。雙柵欄類是DistributedDoubleBarrier DistributedDoubleBarrier類實現了雙柵欄的功能。它的建構函式如下:

// client - the client
// barrierPath - path to use
// memberQty - the number of members in the barrier
public DistributedDoubleBarrier(CuratorFramework client, String barrierPath, int memberQty)
複製程式碼

memberQty是成員數量,當enter方法被呼叫時,成員被阻塞,直到所有的成員都呼叫了enter。當leave方法被呼叫時,它也阻塞呼叫執行緒,直到所有的成員都呼叫了leave。
注意:引數memberQty的值只是一個閾值,而不是一個限制值。當等待柵欄的數量大於或等於這個值柵欄就會開啟!
與柵欄(DistributedBarrier)一樣,雙柵欄的barrierPath引數也是用來確定是否是同一個柵欄的,雙柵欄的使用情況如下:
1.從多個客戶端在同一個路徑上建立雙柵欄(DistributedDoubleBarrier),然後呼叫enter()方法,等待柵欄數量達到memberQty時就可以進入柵欄。
2.柵欄數量達到memberQty,多個客戶端同時停止阻塞繼續執行,直到執行leave()方法,等待memberQty個數量的柵欄同時阻塞到leave()方法中。
3.memberQty個數量的柵欄同時阻塞到leave()方法中,多個客戶端的leave()方法停止阻塞,繼續執行。
DistributedDoubleBarrier類的主要方法如下: enter()、enter(long maxWait, TimeUnit unit) - 等待同時進入柵欄
leave()、leave(long maxWait, TimeUnit unit) - 等待同時離開柵欄
異常處理:DistributedDoubleBarrier會監控連線狀態,當連線斷掉時enter()和leave方法會丟擲異常。

計數器Counters

利用ZooKeeper可以實現一個叢集共享的計數器。 只要使用相同的path就可以得到最新的計數器值, 這是由ZooKeeper的一致性保證的。Curator有兩個計數器, 一個是用int來計數,一個用long來計數。

1)SharedCount

這個類使用int型別來計數。 主要涉及三個類。

* SharedCount
* SharedCountReader
* SharedCountListener
複製程式碼

SharedCount代表計數器, 可以為它增加一個SharedCountListener,當計數器改變時此Listener可以監聽到改變的事件,而SharedCountReader可以讀取到最新的值, 包括字面值和帶版本資訊的值VersionedValue。

2)DistributedAtomicLong

除了計數的範圍比SharedCount大了之外, 它首先嚐試使用樂觀鎖的方式設定計數器, 如果不成功(比如期間計數器已經被其它client更新了), 它使用InterProcessMutex方式來更新計數值。 此計數器有一系列的操作:

  • get(): 獲取當前值
  • increment(): 加一
  • decrement(): 減一
  • add(): 增加特定的值
  • subtract(): 減去特定的值
  • trySet(): 嘗試設定計數值
  • forceSet(): 強制設定計數值

你必須檢查返回結果的succeeded(), 它代表此操作是否成功。 如果操作成功, preValue()代表操作前的值, postValue()代表操作後的值。

End

Curator抽象和簡化了很多複雜的zookeeper操作,推薦替代zkclient包進行開發。

Curator(ZoooKeeper客戶端)使用詳解

相關文章