zookeeper
zookeeper被廣泛用於分佈式服務中,如集羣,kafka等。但基本的api卻很簡單。通過客戶端調用簡單的zookeeper的api實現數據更新、節點檢測、權限控制、異步操作、事件監聽等,最終實現分佈式需要。
這裏簡單介紹一下客戶端api。
首先,啓動zookeeper
zkServer.sh start
開放2181端口,供外部調用
firewall-cmd --zone=public --add-port=2181/tcp --permanent
重啓防火牆
systemctl restart firewalld.service
重載配置文件讓它生效
firewall-cmd --reload
這樣就可以根據IP和端口訪問zookeeper服務器了。
啓動客戶端,就可以連接到服務器了
zkCli.sh -server 127.0.0.1:2181
zookeeper的數據結構其實就是一棵樹,每個數據節點都是一個 ZNode。znode一共4種類型:持久的,臨時的,持久有序的,臨時有序的。
提供的操作也就是操作這棵樹。輸入 h 後回車,看下zkCli.sh提供的命令
使用客戶端可以創建會話、創建節點、刪除節點、獲取子節點、獲取節點內容等。
命令行客戶端
命令行客戶端一般用於調試、運維,實際服務不要用它。
執行 ls / 發現,/路徑下已經有一個節點 /zookeeper 了
創建節點
create -e /master "hello"
這裏 -e 是標明創建臨時節點。還有-s,標明創建順序節點(會在在節點名稱後自動加上一串數字,每次創建時這串數字自增)
/master 爲節點路徑,"hello" 爲節點的數據
注意,zookeeper根節點是 / 。因此所有節點路徑都要帶上 / 。
查看子節點列表
ls2 /master
獲取節點
get /master
獲取節點狀態
stat /master
這裏說下節點的一些屬性:
屬性 |
含義 |
cZxid |
節點被創建時的事務id |
ctime |
當前節點的創建時間 |
mZxid |
節點最後一次修被改時的事務id |
mtime |
節點數據的修改時間 |
pZxid |
當前節點的子節點列表最後一次被修改時的事務id |
cversion |
子節點的版本號 |
dataVersion |
節點數據的版本號 |
aclVersion |
節點訪問權限的版本號 |
ephemeralOwner |
創建該臨時節點的會話sessionId,若是持久節點則爲0 |
dataLength |
節點的數據長度 |
numChildren |
子節點個數 |
註冊監聽
stat /master true
注意:stat 命令加true參數是爲當前客戶端註冊監聽。並且註冊監視點,是一次性下的!
修改數據
set /master "5678"
給節點設置限制
set quota -n|-b val path
這裏-n 代表限制節點的數據長度,-b 代表限制子節點個數,val是對應的值。注意,zookeeper節點超過限制後,會告警不會報錯。
原生API
zookeeper用java寫的。也提供了簡單的api供與服務器交互。
public class MyMain {
static final CountDownLatch connectedSemaphore = new CountDownLatch(1);
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
// 採用Watcher實現,不過Watcher監聽是一次性的
ZooKeeper zk = new ZooKeeper("172.16.59.154:2181", 5000, new WatcherListener(connectedSemaphore));
System.out.println("ZooKeeper Connecting");
// /進行阻塞,直到連接成功
connectedSemaphore.await();
System.out.println("ZooKeeper Connected is " + zk.getState().isConnected());
// 獲取子節點列表
List<String> children = zk.getChildren("/", true);
for (String ch : children) {
Stat stat = new Stat();
byte[] rt = zk.getData("/" + ch, false, stat);
System.out.println("Path : /" + ch + " get : " + rt.toString() + " stat " + stat.toString());
}
// 創建節點,節點的狀態會存入stat中
Stat stat = new Stat();
String p = zk.create("/test", " 123".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL, stat);
System.out.println("Zk created :" + p);
// 檢測節點的存在,不存在返回null
Stat existStat = zk.exists("/test", false);
System.out.println("exist node ? :" + (existStat != null ? "Yes" : "No"));
// 修改節點數據
Stat rtStat = zk.setData("/test", "456".getBytes(), stat.getVersion());
System.out.println("Zk setData");
// 查詢節點數據
byte[] r = zk.getData("/test", false, stat);
System.out.println("Zk getData " + new String(r));
// 刪除節點
zk.delete("/test", stat.getVersion());
// 關閉會話
zk.close();
System.out.println("The end");
}
}
客戶端對象的構建需要一個監聽對象
public class WatcherListener implements Watcher {
private CountDownLatch connectedSemaphore;
public void process(WatchedEvent event) {
// 注意,這裏原生event只包含節點狀態、event類型、節點路徑信息。不包含節點數據。
if (Event.KeeperState.SyncConnected == event.getState()) {
if (Event.EventType.None == event.getType()) {
//如果建立連接成功,則發送信號量,讓後續阻塞程序向下執行
connectedSemaphore.countDown();
System.out.println("ZooKeeper 建立連接");
}
} else {
System.out.println("Path:" + event.getPath() + ", event :" + event.getType().name());
}
}
public WatcherListener(CountDownLatch countDownLatch) {
this.connectedSemaphore = countDownLatch;
}
}
通過原生api構建客戶端對象,操作節點。注意:原生api的事件監聽跟命令行客戶端一樣,都是一次性的。
zkClient
ZkClient是由Datameer的工程師開發的開源客戶端,對Zookeeper的原生API進行了包裝,實現了超時重連、Watcher反覆註冊等功能。
public class MyMain {
public static void main(String[] args) {
ZkClient zkClient = new ZkClient("172.16.59.154:2181", 1000 * 6, 10000, new SerializableSerializer());
System.out.println("connected success");
User user = new User();
user.setName("kaka");
// 創建節點
String path = zkClient.create("/hust", user, CreateMode.PERSISTENT);
zkClient.addAuthInfo("", null);
// TODO
System.out.println("created hust " + path);
user.setName("zw");
// 臨時節點下不能創建子節點
zkClient.create("/hust/zw", user, CreateMode.EPHEMERAL);
boolean existZw = zkClient.exists("/hust/zw");
System.out.println("created zw " + path + " : " + existZw);
User findResult = zkClient.readData("/hust");
System.out.println(findResult.toString());
Stat stat = new Stat();
User resultWithStat = zkClient.readData("/hust", stat);
System.out.println(resultWithStat.toString());
System.out.println(stat.toString());
List<String> childrens = zkClient.getChildren("/hust");
for (String children : childrens) {
System.out.println(children);
User ch = zkClient.readData("/hust/" + children);
System.out.println("each children : " + ch.getName());
}
// 迭代刪除子節點
boolean deleteResult = zkClient.deleteRecursive("/hust");
System.out.println("Delete /hust result : " + deleteResult);
zkClient.subscribeChildChanges("/hust", new MinChildListener());
System.out.println("The end");
}
private static class MinChildListener implements IZkChildListener {
public void handleChildChange(String s, List<String> list) throws Exception {
}
}
}
zkClient可以省去反覆註冊監聽,而且它需要一個序列化器,也就是說我們直接把java對象傳遞給zookeeper存儲。
還有IZkDataListener,IZkStateListener 可供選擇監聽。
curator客戶端
Curator是Netflix公司開源的一套zookeeper客戶端框架,解決了很多Zookeeper客戶端非常底層的細節開發工作,包括連接重連、反覆註冊Watcher和NodeExistsException異常等等。一句話,是一個非常優秀的java包。
Curator包含了幾個包:
curator-framework:對zookeeper的底層api的一些封裝
curator-client:提供一些客戶端的操作,例如重試策略等
curator-recipes:封裝了一些高級特性,如:Cache事件監聽、選舉、分佈式鎖、分佈式計數器、分佈式Barrier等
簡單看例子
引入依賴,注意兼容性問題。否則報java.lang.NoClassDefFoundError: org/apache/zookeeper/MultiTransactionRecord
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
// 注意這裏 3.6.0 暫時不支持 curator的4.3.0
compile "org.apache.zookeeper:zookeeper:3.5.0"
compile "org.apache.curator:curator-framework:4.3.0"
compile "org.apache.curator:curator-client:4.3.0"
compile "org.apache.curator:curator-recipes:4.3.0"
}
例子
public class MinMain {
private static CountDownLatch count = new CountDownLatch(1);
public static void main(String[] args) throws Exception {
// 重試策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("172.16.59.154:2181").sessionTimeoutMs(5000).connectionTimeoutMs(5000).retryPolicy(retryPolicy)
.namespace("base")
.build();
client.start();// 調用start後纔會去連接
// 創建數據
client.create().creatingParentContainersIfNeeded()// 遞歸創建所需父節點
.withMode(CreateMode.PERSISTENT) // 創建類型爲持久節點
.forPath("/nodeA", "init".getBytes()); // 目錄及內容
Stat stat = new Stat();
// 讀取數據
byte[] bytes = client.getData().storingStatIn(stat).forPath("/nodeA");
System.out.println("Get data " + new String(bytes) + " stat " + stat.toString());
ExecutorService pool = Executors.newFixedThreadPool(2);
/**
* 監聽數據節點的變化情況
*/
final NodeCache nodeCache = new NodeCache(client, "/nodeA", false);
nodeCache.start(true);
nodeCache.getListenable().addListener(
new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
System.out.println("Node data is changed, new data: " + new String(nodeCache.getCurrentData().getData()));
}
},
pool
);
// 修改數據
client.setData()
.withVersion(stat.getVersion()) // 指定版本修改
.forPath("/nodeA", "data".getBytes());
List<String> children = client.getChildren().forPath("/"); // 獲取子節點的路徑
for (String child : children) {
System.out.println("path : " + "/base/" + child);
}
/**
* 監聽子節點的變化情況
*/
final PathChildrenCache childrenCache = new PathChildrenCache(client, "/nodeA", true);
childrenCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
childrenCache.getListenable().addListener(
new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
switch (event.getType()) {
case CHILD_ADDED:
System.out.println("CHILD_ADDED for: " + event.getData().getPath());
break;
case CHILD_REMOVED:
System.out.println("CHILD_REMOVED for: " + event.getData().getPath());
break;
case CHILD_UPDATED:
System.out.println("CHILD_UPDATED for: " + event.getData().getPath());
break;
default:
System.out.println("連接異常" + event.getType());
break;
}
MinMain.count.countDown();
}
},
pool
);
// 事務
Collection<CuratorTransactionResult> transactionResultCollection = client.inTransaction()
.create().withMode(CreateMode.EPHEMERAL).forPath("/nodeA/one", "sssss".getBytes())
.and()
.create().withMode(CreateMode.EPHEMERAL).forPath("/nodeA/two", "sss".getBytes())
.and()
.commit();
for (CuratorTransactionResult result : transactionResultCollection) {
System.out.println("TransactionResult " + result);
}
// 節點檢測,返回結果不空則存在
Stat exist = client.checkExists() // 檢查是否存在
.forPath("/nodeA");
System.out.println("Exist znode /nodeA " + (exist != null ? "Yes" : "No"));
// 異步調用:在forPath 前調用 inBackground 並傳入參數即可
Executor executor = Executors.newFixedThreadPool(2);
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.inBackground((curatorFramework, curatorEvent) -> {
System.out.println(String.format("異步調用 eventType:%s,ResultCode:%s", curatorEvent.getType(), curatorEvent.getResultCode()));
MinMain.count.countDown();
}, executor)
.forPath("/path");
MinMain.count.await();
// 刪除
client.delete()
.guaranteed() // 強制保證刪除
.deletingChildrenIfNeeded() // 遞歸刪除子節點
.withVersion(exist.getVersion()) // 指定刪除的版本號
.forPath("/nodeA");
client.close();
System.out.println("The end");
}
}
功能非常強大,而且是 Fluent編程風格,非常優雅。對應的event和操作也很全面
CuratorEventType |
對應method |
CREATE |
CuratorFramework#create() |
DELETE |
CuratorFramework#delete() |
EXISTS |
CuratorFramework#checkExists() |
GET_DATA |
CuratorFramework#getData() |
SET_DATA |
CuratorFramework#setData() |
CHILDREN |
CuratorFramework#getChildren() |
SYNC |
CuratorFramework#sync(String, Object) |
GET_ACL |
CuratorFramework#getACL() |
SET_ACL |
CuratorFramework#setACL() |
TRANSACTION |
CuratorFramework#transaction() |
GET_CONFIG |
CuratorFramework#getConfig() |
RECONFIG |
CuratorFramework#reconfig() |
WATCHED |
Watchable#usingWatcher(Watcher) Watchable#watched() |
REMOVE_WATCHES |
CuratorFramework#watches() |
CLOSING |
客戶端關閉 |
curator recipes
這裏單獨拎出來說。zookeeper只提供基本的api,然後給出一些解決方案。但是沒有實現它。簡單來說提供了材料(原生api)、菜單(doc裏的方案場景),就是沒有提供菜譜(方案)。
引用官方文檔:
Curator implements all of the recipes listed on the ZooKeeper recipes doc (except two phase commit). NOTE: Most Curator recipes will autocreate parent nodes of paths given to the recipe as CreateMode.CONTAINER.
意思:Curator實現了除ZooKeeper二次提交外的所有的技巧(recipes)。Curator Recipes可以實現自動化創建節點路徑的父節點。
也就是說zookeeper列舉的分佈式中的一些方案,它都實現了。
採用cache來封裝了對事件的監聽,包括監聽節點,監聽子節點等。這裏簡單列舉,就不舉例說明了。
cache |
說明 |
NodeCache |
主要用來監聽節點本身的變化,當節點的狀態發生變更後,回調NodeCachaListener |
PathChildrenCache |
主要用來監聽子節點 |
TreeCache |
即能監聽節點也能監聽節點的子節點變更 |
注意:這裏是對節點和子節點的監聽,二級節點變化不會被監聽。
Elections(選舉)
Locks(鎖)
Barriers(阻塞)
Counters(計數器)
Caches(緩存)
Nodes(節點)
Queues(隊列)
這些功能待續