一、ZK簡介
在大數(shù)據(jù)技術(shù)體系內(nèi),很多技術(shù)框架都是用動物的名字命名的,比如Hadoop(大象)、Hive(蜜蜂)、Pig(小豬)。大數(shù)據(jù)服務(wù)通常都是分布式的,多個節(jié)點之間角色不同,在特殊情況下角色還會發(fā)生轉(zhuǎn)換,比如HDFS中的NameNode和SidebyNameNode,YARN中的ResourceManager和Standby ResourceManager,Spark中的Mastser和 Standby Master等等,這些角色(節(jié)點)在發(fā)生故障的時候,如何確保集群能正常工作是一個很重要的問題,Zookeeper的出現(xiàn)就是為了解決這個問題的。
Zookeeper是Apache的開源項目,按照官網(wǎng)的描述來定義:Zookeeper是一個支持分布式部署的,開源的,分布式應(yīng)用程序協(xié)調(diào)服務(wù),主要提供集群管理、分布式鎖、注冊中心和配置中心的功能。
Zookeeper本質(zhì)上是提供一個樹形目錄服務(wù),類似于Linux操作系統(tǒng)的目錄樹文件系統(tǒng),使得其下節(jié)點有一個層次化的結(jié)構(gòu)。其中的每一個節(jié)點都被成為ZNode,每個節(jié)點都可以擁有自己的子節(jié)點,也可以在節(jié)點本身上存儲少量(1MB)的數(shù)據(jù)信息。節(jié)點的分類如下:
- 持久化節(jié)點,在ZK重啟后仍然存在的節(jié)點;
- 臨時節(jié)點,存在內(nèi)存中的具有有效期的節(jié)點,有效期過后或者重啟后就不存在了;
- 持久化順序節(jié)點;順序節(jié)點的作用在分布式鎖的時候會用到;
- 臨時順序節(jié)點;

臨時節(jié)點常被用于心跳監(jiān)控,其生命周期依賴創(chuàng)建它們的會話,一旦會話結(jié)束或者連接超時,那么臨時節(jié)點就是失效被刪除,當(dāng)然也可以在會話有效的時候主動刪除。臨時節(jié)點是不能擁有子節(jié)點的。
二、ZK環(huán)境搭建
首先我們在阿里云上購買一臺ECS云服務(wù)器,選擇1C1G Linux CentOS7.5 X64的系統(tǒng),然后通過本地的SSH工具連接上去。
Zookeeper需要依賴Java運(yùn)行環(huán)境,所以,我們要確保目標(biāo)服務(wù)器上已經(jīng)安裝好JDK。假設(shè)下載的JDK位于/soft目錄下。
[root@iZuf6fkfr4guj30qo8g4woZ ~]# cd /soft/
[root@iZuf6fkfr4guj30qo8g4woZ soft]# tar -zxvf jre-8u351-linux-x64.tar.gz
......
[root@iZuf6fkfr4guj30qo8g4woZ soft]# vim /etc/profile
# 在最下面增加如下內(nèi)容
export JAVA_HOME=/soft/jre1.8.0_351
export PATH=$PATH:$JAVA_HOME/bin
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
[root@iZuf6fkfr4guj30qo8g4woZ soft]# source /etc/profile
[root@iZuf6fkfr4guj30qo8g4woZ soft]# java -version
java version "1.8.0_351"
Java(TM) SE Runtime Environment (build 1.8.0_351-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.351-b10, mixed mode)
2.1 服務(wù)端的搭建和使用
然后我們從官網(wǎng)下載ZK,這里假設(shè)下載的ZK位于/soft目錄下,然后進(jìn)行ZK的解壓縮和配置。同時在該目錄下創(chuàng)建一個數(shù)據(jù)文件夾,用來存放ZK的數(shù)據(jù)。
[root@iZuf6fkfr4guj30qo8g4woZ soft]# tar -zxvf apache-zookeeper-3.5.6-bin.tar.gz
......
# 創(chuàng)建ZK存放數(shù)據(jù)的目錄
[root@iZuf6fkfr4guj30qo8g4woZ soft]# mkdir zkdata
[root@iZuf6fkfr4guj30qo8g4woZ soft]# cd apache-zookeeper-3.5.6-bin/conf/
# 根據(jù)示例配置復(fù)制一份正式生效的ZK配置
[root@iZuf6fkfr4guj30qo8g4woZ conf]# cp zoo_sample.cfg zoo.cfg
[root@iZuf6fkfr4guj30qo8g4woZ conf]# vim zoo.cfg
#修改ZK存放數(shù)據(jù)的目錄
dataDir=/soft/zkdata
以上我們就完成了ZK服務(wù)端的配置,就可以開始啟動了。
[root@iZuf6fkfr4guj30qo8g4woZ conf]# cd /soft/apache-zookeeper-3.5.6-bin/bin
[root@iZuf6fkfr4guj30qo8g4woZ bin]# ./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
ZK服務(wù)端的常見操作命令如下:
啟動命令,
./zkServer.sh start停止命令,
./zkServer.sh stop重啟命令,
./zkServer.sh restart-
查看狀態(tài)命令,
./zkServer.sh status[root@iZuf6fkfr4guj30qo8g4woZ bin]# ./zkServer.sh status ZooKeeper JMX enabled by default Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg Client port found: 2181. Client address: localhost. Mode: standalone
PS:下面客戶端的連接需要保證服務(wù)器開通了2181端口給外部,否則訪問失敗。
2.2 客戶端的搭建和使用
同樣的,我們需要在目標(biāo)客戶端服務(wù)器上下載和解壓縮下載到的Zookeeper壓縮包,如果客戶端和服務(wù)端是一起的,那就直接在/opt/zooKeeper/apache-zooKeeper-3.5.6-bin/bin/目錄下啟動客戶端就行了。常見的客戶端命令如下:
- 連接ZK服務(wù)器,
./zkCli.sh -server ip:port,本機(jī)的話就是localhost:2181; - 斷開連接,
quit; - 創(chuàng)建節(jié)點,
create /path value; - 設(shè)置節(jié)點中的數(shù)據(jù)值,
set /path value; - 獲取節(jié)點值,
get /path; - 顯示指定目錄下的節(jié)點,
ls /path; - 刪除單個節(jié)點,
delete /path,當(dāng)前節(jié)點有子節(jié)點時會刪除失敗; - 刪除帶有子節(jié)點的節(jié)點,
deleteall /path; - 獲取幫助,
help; - 創(chuàng)建臨時節(jié)點,
create -e /path value; - 創(chuàng)建順序節(jié)點,
create -s /path value; - 查詢節(jié)點詳細(xì)信息,
ls -s /path;
三、使用JavaAPI Curator
Curator是Apache提供的一個操作ZK服務(wù)器的Java客戶端庫,其大大簡化了對ZK服務(wù)器節(jié)點和數(shù)據(jù)的操作,我們將在下面來介紹下其基本的使用方法。首先我們從start.spring.io網(wǎng)站上生成一個最基本的SpringBoot工程,然后在其中引入Curator的依賴。
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.0</version>
</dependency>
3.1 基本操作
在如下的代碼中,我們使用Curator來操作Zookeeper實現(xiàn)了開啟連接、創(chuàng)建節(jié)點、查詢節(jié)點、設(shè)置節(jié)點數(shù)據(jù)、刪除節(jié)點的操作。
@Slf4j
public class CuratorBaseTest {
private CuratorFramework curatorClient;
@Before
public void testConnect(){
/**
* 重試策略,最多嘗試10次,每次間隔3秒
*/
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
// 使用建造者模式創(chuàng)建ZK-Curator客戶端
curatorClient = CuratorFrameworkFactory.builder()
.connectString("x.x.x.x:2181")
.sessionTimeoutMs(60*1000)
.connectionTimeoutMs(15*1000)
.retryPolicy(retryPolicy)
.namespace("zhangxun")
.build();
curatorClient.start();
}
/**
* 創(chuàng)建節(jié)點
* 如果創(chuàng)建節(jié)點,沒有指定數(shù)據(jù),則默認(rèn)將當(dāng)前客戶端的ip作為數(shù)據(jù)存儲
*/
@Test
public void testCreateNode() throws Exception {
String path1 = curatorClient.create().forPath("/node1");
// /node1
log.info("{}", path1);
String path2 = curatorClient.create().forPath("/node2", "node2".getBytes(StandardCharsets.UTF_8));
// /node2
log.info("{}", path2);
// 節(jié)點的默認(rèn)類型為持久型,這里指定為臨時節(jié)點
String path3 = curatorClient.create().withMode(CreateMode.EPHEMERAL).forPath("/node3");
// /node3
log.info("{}", path3);
// 如果父節(jié)點不存在,則創(chuàng)建父節(jié)點
String path4 = curatorClient.create().creatingParentsIfNeeded().forPath("/node4/branch1");
// /node4/branch1
log.info("{}", path4);
}
/**
* [zk: localhost:2181(CONNECTED) 7] ls -R /zhangxun
* /zhangxun
* /zhangxun/node1
* /zhangxun/node2
* /zhangxun/node4
* /zhangxun/node4/branch1
*/
/**
* 查詢節(jié)點
*/
@Test
public void testGetNode() throws Exception{
// 查看節(jié)點中的數(shù)據(jù)
byte[] data = curatorClient.getData().forPath("/node1");
// x.x.x.x 默認(rèn)的IP地址
log.info("{}", new String(data));
// 查看某個路徑下面的所有子節(jié)點
List<String> subNodes = curatorClient.getChildren().forPath("/node4");
for(String node : subNodes){
// branch1
log.info("{}", node);
}
// 查看某個節(jié)點的詳細(xì)信息,ls -s
Stat nodeStatus = new Stat();
// 0,0,0,0,0,0,0,0,0,0,0
log.info("{}", nodeStatus);
curatorClient.getData().storingStatIn(nodeStatus).forPath("/node2");
// 7,7,1677836202355,1677836202355,0,0,0,0,5,0,7
log.info("{}", nodeStatus);
}
/**
* 設(shè)置節(jié)點數(shù)據(jù)
*/
@Test
public void testSetNode() throws Exception {
curatorClient.setData().forPath("/node1", "node1".getBytes(StandardCharsets.UTF_8));
byte[] data = curatorClient.getData().forPath("/node1");
// node1
log.info("{}", new String(data));
}
/**
* 刪除節(jié)點
*/
@Test
public void testDeleteNode() throws Exception {
// 刪除指定節(jié)點,且該節(jié)點沒有子節(jié)點
curatorClient.delete().forPath("/node1");
// 刪除帶有子節(jié)點的指定節(jié)點,其子節(jié)點一并刪除
curatorClient.delete().deletingChildrenIfNeeded().forPath("/node4");
// 帶重試機(jī)制的保證刪除方法
curatorClient.delete().guaranteed().forPath("node2");
curatorClient.delete().guaranteed().inBackground(new BackgroundCallback() {
@Override
public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
log.info("節(jié)點刪除成功:{}", event);
}
}).forPath("/node3");
}
@After
public void close(){
if (curatorClient != null) {
curatorClient.close();
}
}
}
3.2 監(jiān)聽事件
ZK允許用戶在指定的節(jié)點或其子節(jié)點上注冊監(jiān)聽器,當(dāng)這些節(jié)點上發(fā)生變更時,ZK會將事件通知給所有注冊的客戶端,實現(xiàn)發(fā)布訂閱的功能,這也是ZK實現(xiàn)分布式應(yīng)用協(xié)調(diào)服務(wù)功能的重要特性。ZK提供了三種類型的監(jiān)聽器:
- NodeCache,只是監(jiān)聽某個特定的節(jié)點;
- PathChildrenCache,監(jiān)聽某個節(jié)點的所有子節(jié)點;
- TreeCache,監(jiān)聽某個節(jié)點及其所有子節(jié)點;
監(jiān)聽事件需要Curator另一個組件的支持,因此需要在pom中引入該依賴。
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.0</version>
</dependency>
如下是一個使用Curator實現(xiàn)對ZK某個節(jié)點或其子節(jié)點進(jìn)行監(jiān)聽的例子:
@Slf4j
public class CuratorWatcherTest {
private CuratorFramework curatorClient;
@Before
public void testConnect(){
/**
* 重試策略,最多嘗試10次,每次間隔3秒
*/
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
// 使用建造者模式創(chuàng)建ZK-Curator客戶端
curatorClient = CuratorFrameworkFactory.builder()
.connectString("47.100.139.15:2181")
.sessionTimeoutMs(60*1000)
.connectionTimeoutMs(15*1000)
.retryPolicy(retryPolicy)
.namespace("zhangxun")
.build();
curatorClient.start();
}
/**
* 給指定的節(jié)點注冊監(jiān)聽器
*/
@Test
public void testWatcher1() throws Exception {
// 創(chuàng)建需要監(jiān)聽節(jié)點的NodeCache對象
NodeCache nodeCache = new NodeCache(curatorClient, "/node1");
// 注冊監(jiān)聽
nodeCache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
log.info("監(jiān)聽的節(jié)點發(fā)生了變化");
byte[] data = nodeCache.getCurrentData().getData();
log.info("節(jié)點變化后的內(nèi)容為:{}", new String(data));
}
});
nodeCache.start(true);
// 等待60秒,手動去更改/node1上的數(shù)據(jù),觸發(fā)如上回調(diào)函數(shù)
Thread.sleep(60*1000);
}
/**
* [zk: localhost:2181(CONNECTED) 9] set /zhangxun/node1 123
*
* 17:58:04.817 [main-EventThread] INFO com.example.zkcurator.CuratorWatcherTest - 監(jiān)聽的節(jié)點發(fā)生了變化
* 17:58:04.817 [main-EventThread] INFO com.example.zkcurator.CuratorWatcherTest - 節(jié)點變化后的內(nèi)容為:123
*/
/**
* 給指定的節(jié)點的所有子節(jié)點注冊監(jiān)聽器
*/
@Test
public void testWatcher2() throws Exception {
PathChildrenCache pathChildrenCache = new PathChildrenCache(curatorClient, "/node4", true);
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
log.info("監(jiān)聽的子節(jié)點發(fā)生了變化:{}", pathChildrenCacheEvent);
PathChildrenCacheEvent.Type type = pathChildrenCacheEvent.getType();
byte[] data = pathChildrenCacheEvent.getData().getData();
log.info("監(jiān)聽的子節(jié)點發(fā)生了{(lán)}類型的變更,變更后的內(nèi)容為:{}", type, new String(data));
}
});
pathChildrenCache.start();
// 等待60秒,手動去更改/node4的任意一個子節(jié)點上的數(shù)據(jù),觸發(fā)如上回調(diào)函數(shù)
Thread.sleep(60*1000);
}
/**
* 給指定的節(jié)點及其所有子節(jié)點注冊監(jiān)聽器
*/
@Test
public void testWatcher3() throws Exception {
TreeCache treeCache = new TreeCache(curatorClient, "/node4");
treeCache.getListenable().addListener(new TreeCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception {
log.info("監(jiān)聽的子節(jié)點發(fā)生了變化:{}", treeCacheEvent);
TreeCacheEvent.Type type = treeCacheEvent.getType();
byte[] data = treeCacheEvent.getData().getData();
log.info("監(jiān)聽的節(jié)點或其子節(jié)點發(fā)生了{(lán)}類型的變更,變更后的內(nèi)容為:{}", type, new String(data));
}
});
treeCache.start();
// 等待60秒,手動去更改/node4節(jié)點或者其任意一個子節(jié)點上的數(shù)據(jù),觸發(fā)如上回調(diào)函數(shù)
Thread.sleep(60*1000);
}
@After
public void close(){
if (curatorClient != null) {
curatorClient.close();
}
}
}
那么ZK是如何利用監(jiān)聽機(jī)制和臨時節(jié)點來實現(xiàn)分布式應(yīng)用協(xié)調(diào)服務(wù)功能的呢?
假設(shè)客戶端集群HDFS或者Kafaka,它們都連上了ZK服務(wù)器,各個節(jié)點都注冊了臨時節(jié)點在ZK服務(wù)器上,同時對其它節(jié)點注冊了監(jiān)聽機(jī)制。這樣如果集群中有服務(wù)器節(jié)點上下線,其它節(jié)點都能及時收到變更通知,從而再利用ZK集群進(jìn)行選舉,保證了客戶端自身集群的穩(wěn)定和可用。
3.3 分布式鎖實現(xiàn)
分布式鎖不同于線程鎖,線程鎖是為了解決在一個Java進(jìn)程內(nèi),多個線程并發(fā)訪問共享資源時同步的問題,而分布式鎖則是解決集群環(huán)境下,多個Java進(jìn)程間訪問分布式共享資源的同步問題。
同樣的,要實現(xiàn)分布式鎖也需要引入curator-recipes依賴。下面是一個分布式場景下(用多線程來模擬分布式場景)搶購書籍的例子,你也可以不使用多線程,而是啟動多個應(yīng)用進(jìn)程來模擬。
@Slf4j
public class BookSale implements Runnable{
/**
* 假設(shè)書本售賣總量為10本,模擬分布式搶購資源
*/
private int count = 10;
/**
* 定義分布式鎖變量
*/
private InterProcessLock lock;
/**
* 在構(gòu)造函數(shù)中進(jìn)行ZK連接的獲取及分布式鎖的初始化
*/
public BookSale(){
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
CuratorFramework curatorClient = CuratorFrameworkFactory.builder()
.connectString("47.100.139.15:2181")
.sessionTimeoutMs(60*1000)
.connectionTimeoutMs(15*1000)
.retryPolicy(retryPolicy)
.build();
curatorClient.start();
lock = new InterProcessMutex(curatorClient, "/lock");
}
@Override
public void run() {
while (true) {
// 無限循環(huán)進(jìn)行搶購
try {
// 第一步先上鎖確保了下面操作的同步執(zhí)行,當(dāng)獲取不到鎖時最多等待3秒
lock.acquire(3, TimeUnit.SECONDS);
if(count <= 0){
log.info("{}-您來晚了,所有書本都被搶完!", Thread.currentThread());
return;
}
count--;
log.info("{}-恭喜您搶到了1本,還剩{}本,繼續(xù)加油!", Thread.currentThread(), count);
} catch (Exception e) {
log.warn("{}-本次搶購失敗,還剩{}本,繼續(xù)加油: {}", Thread.currentThread() ,count, e);
} finally {
try {
lock.release();
} catch (Exception e) {
log.error("釋放鎖失敗:{}", e);
}
}
}
}
/**
* Thread[Thread-3,5,main]-恭喜您搶到了1本,還剩9本,繼續(xù)加油!
* Thread[Thread-1,5,main]-恭喜您搶到了1本,還剩8本,繼續(xù)加油!
* Thread[Thread-2,5,main]-恭喜您搶到了1本,還剩7本,繼續(xù)加油!
* Thread[Thread-3,5,main]-恭喜您搶到了1本,還剩6本,繼續(xù)加油!
* Thread[Thread-1,5,main]-恭喜您搶到了1本,還剩5本,繼續(xù)加油!
* Thread[Thread-2,5,main]-恭喜您搶到了1本,還剩4本,繼續(xù)加油!
* Thread[Thread-3,5,main]-恭喜您搶到了1本,還剩3本,繼續(xù)加油!
* Thread[Thread-1,5,main]-恭喜您搶到了1本,還剩2本,繼續(xù)加油!
* Thread[Thread-2,5,main]-恭喜您搶到了1本,還剩1本,繼續(xù)加油!
* Thread[Thread-3,5,main]-恭喜您搶到了1本,還剩0本,繼續(xù)加油!
* Thread[Thread-1,5,main]-您來晚了,所有書本都被搶完!
* Thread[Thread-2,5,main]-您來晚了,所有書本都被搶完!
* Thread[Thread-3,5,main]-您來晚了,所有書本都被搶完!
*/
}
public class BookMain {
public static void main(String[] args) {
BookSale bookSale = new BookSale();
/**
* 模擬三個獨(dú)立的人同時搶購書籍
*/
Thread tom = new Thread(bookSale);
Thread jack = new Thread(bookSale);
Thread lucy = new Thread(bookSale);
tom.start();
jack.start();
lucy.start();
}
}
如上案例中,使用curator來實現(xiàn)分布式鎖非常的簡單,那它是如何做到的呢,背后的工作機(jī)制又是怎樣的呢?
- 1.客戶端在獲取鎖時,會在指定目錄下創(chuàng)建臨時順序節(jié)點;
- 2.然后獲取該目錄下所有子節(jié)點,判斷自己創(chuàng)建的子節(jié)點是否是最小的,如果是就代表獲取到了鎖,執(zhí)行業(yè)務(wù)邏輯,結(jié)束時刪除自己創(chuàng)建的節(jié)點;
- 3.如果發(fā)現(xiàn)自己創(chuàng)建的子節(jié)點不是最小的,那代表當(dāng)前沒有獲取到鎖,同時注冊監(jiān)聽器到最小的那個子節(jié)點上,監(jiān)聽其刪除事件;
- 4.當(dāng)最小的那個子節(jié)點被刪除時,代表有人釋放了鎖,重復(fù)如上步驟2;
在如上步驟中,客戶端創(chuàng)建的節(jié)點有兩個特點:
- 臨時的,確??蛻舳嗽诰W(wǎng)絡(luò)失?。〞捠В┣闆r下,不會長時間占用鎖,使得其他客戶端可以繼續(xù)競爭鎖;
- 順序的,保證一次只有一個客戶端能獲取到鎖;
當(dāng)然,在學(xué)習(xí)線程鎖的時候,我們有講過鎖的種類,Curator中實現(xiàn)的分布式鎖也有多種,供了解。
- InterProcessSemaphoreMutex,分布式非可重入排它鎖;意思是在任何時刻不會有兩個客戶端同時持有鎖,該客戶端在擁有鎖的同時,也不能多次獲取鎖;
- InterProcessMutex,分布式可重入排它鎖;意思是在任何時刻不會有兩個客戶端同時持有鎖,和JDK的ReentrantLock類似, 意味著該客戶端在擁有鎖的同時,可以多次獲取鎖,不會被阻塞;
- InterProcessReadWriteLock,分布式可重入讀寫鎖;參考JDK中的讀寫鎖;
- InterProcessSemaphoreV2,共享信號量;參考JDK中的信號量;
- InterProcessMultiLock,多共享分布式鎖,多個鎖作為一個鎖,可以同時在多個資源上加鎖。一個維護(hù)多個鎖對象的容器。當(dāng)調(diào)用 acquire()時,獲取容器中所有的鎖對象,請求失敗時,釋放所有鎖對象。同樣調(diào)用release()也會釋放所有的鎖;
四、ZK集群搭建和使用
4.1 集群搭建
ZK中的角色總共分為三種:
- 領(lǐng)導(dǎo)者,Leader,負(fù)責(zé)處理事務(wù)類的請求,是集群內(nèi)部各個服務(wù)器的調(diào)度者,一個集群只能有一個Leader;
- 跟隨著,F(xiàn)ollower,負(fù)責(zé)處理非事務(wù)類的請求,收到事務(wù)類請求時會轉(zhuǎn)發(fā)給Leader進(jìn)行處理;同時參與集群Leader的選舉投票,有可能成為新的Leader;
- 觀察者,Observer,負(fù)責(zé)處理非事務(wù)類的請求,不參加投票,不是必須的;

客戶端在設(shè)置連接ZK集群的時候,可以將集群所有服務(wù)器的IP+端口都配置進(jìn)去,比如Curator就會任意選擇一個ZK服務(wù)器進(jìn)行連接,任何一臺服務(wù)器上的數(shù)據(jù)都是相同的,當(dāng)前連接的服務(wù)器如果宕機(jī)的話,Curator會自動幫我們重連尋找集群中其它可用的服務(wù)器。
我們現(xiàn)在已經(jīng)有了一臺云主機(jī)了,為了配置成為ZK集群,再購買兩臺(需要在同一個局域網(wǎng)內(nèi)),同時按照2.1節(jié)中的步驟配置好java環(huán)境和ZK的配置。需要額外配置的內(nèi)容有如下:
-
在每臺機(jī)器的zk數(shù)據(jù)目錄下,也就是我們創(chuàng)建的zkdata目錄下,創(chuàng)建myid文件,指定各自的服務(wù)器ID,比如1、2、3;
# node1(172.24.38.213) echo 1 > /soft/zkdata/myid # node2(172.24.38.214) echo 2 > /soft/zkdata/myid # node3(172.24.38.212) echo 3 > /soft/zkdata/myid -
修改每臺機(jī)器的zk配置文件,將集群中所有的機(jī)器地址配置在里面;
vim /soft/apache-zookeeper-3.5.6-bin/conf/zoo.cfg # server.id=服務(wù)器IP地址:服務(wù)器之間的通信端口:服務(wù)器之間的選舉投票端口 server.1=172.24.38.213:2881:3881 server.2=172.24.38.214:2881:3881 server.3=172.24.38.212:2881:3881
做好如上配置后,我們就可以開始啟動我們的ZK集群了:
cd /soft/apache-zookeeper-3.5.6-bin/bin
# node1(172.24.38.213)
./zkServer.sh start
# node2(172.24.38.214)
./zkServer.sh start
# node3(172.24.38.212)
./zkServer.sh start
4.2 集群的使用
在剛才的集群中node1是Leader,node2和node3是Follower,我們模擬node1異常服務(wù)終止,那么會發(fā)生什么呢?
# node1(172.24.38.213)
./zkServer.sh stop
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Stopping zookeeper ... STOPPED
# node2(172.24.38.214)
./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower
# node3(172.24.38.212)
./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: leader
可以看到,經(jīng)過ZAB協(xié)議的重新選舉,node3成為了新的Leader,此時如果再停掉任意一個節(jié)點,整個集群將不可用:
# node2(172.24.38.214)
./zkServer.sh stop
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Stopping zookeeper ... STOPPED
# node3(172.24.38.212)
./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Error contacting service. It is probably not running.
這是因為集群中正常節(jié)點的數(shù)量沒有超過總節(jié)點的一半,此時我們重啟node1,會發(fā)現(xiàn)集群會恢復(fù)正常,且產(chǎn)生了新的Leader:
# node1(172.24.38.213)
./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
# node1(172.24.38.213)
./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower
# node3(172.24.38.212)
./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: leader
五、算法原理介紹
文本是ZK的入門介紹,這些算法理論方面的知識了解即可,暫不做展開描述,后面會有專門的介紹。
5.1 拜占庭將軍問題
拜占庭將軍問題 (The Byzantine Generals Problem) - 知乎 (zhihu.com)
拜占庭將軍問題是用來為描述分布式系統(tǒng)一致性問題而情景化的一個著名例子,該問題是分布式領(lǐng)域最為復(fù)雜的問題,通常解決該問題的算法分為兩類:
- 故障容錯算法,解決的是分布式系統(tǒng)中存在故障, 但不存在惡意攻擊的場景下的共識問題。也就是說, 在該場景下可能存在消息丟失, 消息重復(fù), 但不存在消息被篡改或偽造的場景。一般用于局域網(wǎng)場景下的分布式系統(tǒng), 屬于此類的常見算法有Paxos算法, Raft算法, ZAB協(xié)議等;
- 拜占庭容錯算法,可以解決分布式系統(tǒng)中既存在故障, 又存在惡意攻擊場景下的共識問題。 一般用于互聯(lián)網(wǎng)場景下的分布式系統(tǒng), 如在數(shù)字貨幣的區(qū)塊鏈技術(shù)中,屬于此類的常見算法有PBFT算法, PoW算法等。
5.2 Paxos算法
Paxos算法是用來解決分布式系統(tǒng)中信息一致性問題的算法之一,其實現(xiàn)比較復(fù)雜,在實際中很少直接使用。
5.3 ZAB協(xié)議
分布式共識算法 Zab 簡解 - 知乎 (zhihu.com)
ZAB協(xié)議是基于Multi-Paxos改造和優(yōu)化后的算法,Zookeeper集群的選舉過程就是該算法的體現(xiàn);
5.3 Raft協(xié)議
Raft協(xié)議是將Paxos優(yōu)化和簡化之后的算法,Nacos集群的選舉過程就是該算法的體現(xiàn);
5.5 CAP原理
談?wù)劮植际较到y(tǒng)的CAP理論 - 知乎 (zhihu.com)
CAP分別代表一致性、可用性、分區(qū)容錯性,在分布式系統(tǒng)中,P代表的分區(qū)容錯性是必須的,所以一般都是在CP和AP之間做出選擇。Zookeeper因為ZAB協(xié)議的關(guān)系,不能保證可用性,因此屬于CP陣營。
六、注冊中心和配置中心
阿里一面:Zookeeper用作注冊中心的原理,你知道嗎? - 知乎 (zhihu.com)
現(xiàn)在主流的注冊中心有很多,比如SpringCloud Eureka,Nacos,Apollo,Consul等,為什么Zookeeper明明也可以作為注冊中心和配置中心,卻鮮有人用呢?
Zookeeper作為注冊中心的注冊流程大致如下:
- 服務(wù)提供者啟動時,會將其服務(wù)名稱、ip地址等信息注冊到Zookeeper的某個路徑上,相同的服務(wù)使用同一個路徑,不同的服務(wù)有各自的路徑;
- 服務(wù)消費(fèi)者在第一次調(diào)用服務(wù)時,會通過Zookeeper找到相應(yīng)的服務(wù)的IP地址列表,并緩存到本地,以供后續(xù)使用。當(dāng)消費(fèi)者調(diào)用服務(wù)時,不會再去請求注冊中心,而是直接通過負(fù)載均衡算法從IP列表中取一個服務(wù)提供者的服務(wù)器調(diào)用服務(wù)。
- 當(dāng)服務(wù)提供者的某臺服務(wù)器宕機(jī)、下線、上線時,相應(yīng)的ip會從服務(wù)提供者IP列表中移除或者新增,此時,Zookeeper會依靠其自身的Watcher機(jī)制將新的服務(wù)IP地址列表發(fā)送給服務(wù)消費(fèi)者機(jī)器,緩存在消費(fèi)者本機(jī);
- Zookeeper自身具有心跳檢測機(jī)制,會定期向服務(wù)提供者發(fā)送請求,如果長時間沒有回應(yīng),就認(rèn)為該服務(wù)已經(jīng)下線,會將其剔除;
可以看到Zookeeper的工作機(jī)制和主流的注冊中心沒啥大的區(qū)別,其不太適合作為注冊中心的原因是其底層核心算法ZAB協(xié)議導(dǎo)致的,ZAB是CP陣營的算法,雖然提供了強(qiáng)一致性,但是在選舉的過程中無法保證可用性(其重新選舉和數(shù)據(jù)同步將耗費(fèi)一定的時間,在此期間服務(wù)不可用),對于微服務(wù)體系來說是不可接收的。
同理,Zookeeper作為配置中心也存在同樣的問題,因此不是不能用,而是不太適合,有其它更好的方案可供選擇。