zookeeper
| 介紹
開源的分佈式協調服務,雅虎創建,基於google chubby。
可以解決的問題
- 數據的發佈/訂閱(配置中心:disconf)
- 負載均衡(dubbo利用了zookeeper機制實現負載均衡)
- 命名服務
- master選舉(kafka、hadoop、hbase)
- 分佈式隊列
- 分佈式鎖。
特性
-
順序一致性
-
原子性
-
可靠性
-
實時性
一旦一個事務被成功應用,客戶端就能夠立即從服務器端讀取到事務變更後的最新數據狀態;(zookeeper僅僅保證在一定時間內,近實時)
文件系統
數據模型和文件系統類似,每一個節點爲稱znode
,zk中的最小數據單元,每個node上可以保存數據和掛載子節點。構成一個層次化的屬性結構。
[外鏈圖片轉存失敗(img-jI6h3nEF-1564466530214)(.assets/20141108213344_45.png)]
節點類型
- 持久化節點
- 持久化有序節點
- 臨時節點
臨時節點的生命期和會話週期相同
- 臨時有序節點
存儲數據大小:不要超過1M
命令
create [-s] [-e] path data
get path [watch]
set path data [version]
version表示鎖的概念,樂觀鎖,數據庫裏面有一個version字段去控制數據的版本號
delete path [version]
必須從子節點開始刪除,不會立即生效,有會話重試機制,過一段時間纔會有
stat信息
名稱 | 說明 |
---|---|
cversion | 子節點的版本號 |
dataVersion | 數據的版本號 |
aclVersion | 表示acl的版本號,修改節點權限 |
czxid | 節點被創建時的事務ID |
mzxid | 節點最後一次被更新的事務ID |
pzxid | 當前節點下的子節點最後一次被修改時的事務ID |
ctime | |
mtime | |
ephemeralOwner | 創建臨時節點時,會有一個sessionId |
dataLength | 數據長度 |
Watcher特性
分佈式數據發佈/訂閱。zk允許客戶端向服務器註冊一個watcher監聽,當服務器端的節點觸發指定事件(數據改變、刪除、子目錄節點增加刪除等
)的時候會觸發watcher,服務端會向客戶端發送一個事件通知。
watcher的通知是一次性的,一量觸發一次通知後,該watcher就失效。
ACL
提供控制節點訪問權限的功能,用於有效的保證zk中數據的安全性,避免誤操作而導致出現重大事故。
| 集羣
角色類型
角色 | 說明 |
---|---|
Leader | 接收所有Follower的提案請求並統一協調發起投票,負責與所有的Follower進行內部的數據交換(同步) |
Follower | 直接爲客戶端服務並參與提案的投票,同時與Leader進行數據交換(同步) |
Observer | 直接爲客戶端服務但並不參與提案投票,同時也與Leader進行數據交換(同步) |
follower不接收寫請求
| zk一致性協議 - zab工作原理
leader選舉
三種選主算法:leaderElection/AuthFastLeaderElection/FastLeaderElection
(默認)
FastLeaderElection
- serverid: 在配置server集羣時,給定服務器的標識id(myid)
- zxid: 64位Long類型,高32位(Epoch,選舉輪數)表示當前屬於那個leader統治,低32位遞增的事務id號,zxid值越大,表示數據越新
- server的狀態:Looking,Following,Observering,Leading
選舉流程
- 狀態設置爲LOOKING,初始化內部投票Vote(id, zxid),將其廣播到其它節點;首次投票都是自己作爲Leader;然後循環等待其它節點的投票信息;
- 每收到一個Vote,都和自己的Vote數據PK,規則爲ZXID大的優先,相等時給ID大的投票。若外部投票獲勝,將該選票覆蓋自己的Vote後再次廣播出去;同時統計是否有過半的贊同者與自己的投票數據一致,無則繼續等待Vote,有則需要判斷Leader是否在贊同者之中,在則退出循環,選舉結束,根據選舉結果及各自角色切換狀態。
每一次啓動時初始化爲Looking
[外鏈圖片轉存失敗(img-oj3BwLk1-1564466530215)(.assets/20181129114824253-1564408184881.png)]
假設這些服務器從id1-5,依序啓動:
因爲一共5臺服務器,只有超過半數以上,即最少啓動3臺服務器,集羣才能正常工作。
(1)服務器1啓動,發起一次選舉。
服務器1投自己一票。此時服務器1票數一票,不夠半數以上(3票),選舉無法完成;
服務器1狀態保持爲LOOKING;
(2)服務器2啓動,再發起一次選舉。
服務器1和2分別投自己一票,此時服務器1發現服務器2的id比自己大,更改選票投給服務器2;
此時服務器1票數0票,服務器2票數2票,不夠半數以上(3票),選舉無法完成;
服務器1,2狀態保持LOOKING;
(3)服務器3啓動,發起一次選舉。
與上面過程一樣,服務器1和2先投自己一票,然後因爲服務器3id最大,兩者更改選票投給爲服務器3;
此次投票結果:服務器1爲0票,服務器2爲0票,服務器3爲3票。此時服務器3的票數已經超過半數(3票),服務器3當選Leader。
服務器1,2更改狀態爲FOLLOWING,服務器3更改狀態爲LEADING;
(4)服務器4啓動,發起一次選舉。
此時服務器1,2,3已經不是LOOKING狀態,不會更改選票信息。交換選票信息結果:服務器3爲3票,服務器4爲1票。
此時服務器4服從多數,更改選票信息爲服務器3;
服務器4並更改狀態爲FOLLOWING;
(5)服務器5啓動,同4一樣投票給3,此時服務器3一共5票,服務器5爲0票;
服務器5並更改狀態爲FOLLOWING;
最終Leader是服務器3,狀態爲LEADING;
其餘服務器是Follower,狀態爲FOLLOWING。
選主後的數據同步
二階提交:leader生成提議並廣播給followers,收到半數以上的ACK後,再廣播commit消息,同時將事務操作應用到內存中。follower收到提議後先將事務寫到本地事務日誌,然後反饋ACK,等接到leader的commit消息時,纔會將事務操作應用到內存中。
問題
假設一個事務P1在leader服務器被提交了,並且已經有過半的follower返回了ack。 在leader節點把commit消息發送給folower機器之前leader服務器掛了怎麼辦?
新生成的Leader之前是follower,未收到commit消息,內存中是沒有P1數據,ZAB協議保證選主後,P1是需要應用到集羣中的。即通過選主後的數據同步來彌補。
已被處理的消息不能丟
消息在Leader上Commit了,但是其它Server還沒有收到Commit消息已經掛了,爲了實現已經被處理的消息不能丟,Zab使用了以下策略:
- 選舉擁有zxid最大的節點作爲新的leader;由於所有Proposal需要過半節點ACK,必須有一個節點保存了所有被COMMIT消息的Proposal狀態;
- 新leader將自己事務日誌中的proposal但未commit的消息處理;
- 新laeder與follower建立先進先出隊列,先將自身有而follower沒有的proposal發送給follower,再將這些proposal的commit命令發送給follower,以保證所有的 follower都保存了所有的proposal並已處理。
被丟棄的消息不能再次出現
場景:當leader接收到消息請求生成proposal後就掛了,其它follower並沒有收到此proposal,
重新選了leader後,這條消息是被跳過的。
之前的leader重新啓動成了follower,保留的被跳過的proposal狀態,與整個系統狀態不一致,需要刪除。
Zab通過巧妙的設計zxid來實現這一目的,高32 epoch表示leader選舉的輪數,每選一次,epoch+1,低32位是消息計數器,每接收到一條消息,這個值+1,新leader選舉後這個值重置爲0。這樣,舊的leader掛了後重啓,它不會被選舉爲leader,因爲此時它的zxid肯定小於當前新的leader,當舊的leader作爲follwer接入新leader後,新leader會讓它將所有的擁有舊的epoch號的未被commit的proposal清除
。
zab協議,一定需要保證已經被leader提交的事務也能夠被所有follower提交
zab協議需要保證,在崩潰恢復過程中跳過哪些已經被丟棄的事務
事務操作
二階提交
,針對client的請求,leader服務器會爲其生成對應的事務proposal,並將其發送給其它follower,然後收集各自的選票,最後進行事務提交。
[外鏈圖片轉存失敗(img-rl2lu3Om-1564466530215)(.assets/1523632294541695.png)]
二階提交過程中,移除了中斷邏輯(事務回滾),所有follower要麼正常反饋,要麼拋棄。follower處理proposal後的處理很簡單,寫入事務日誌,然後立馬反饋ACK給leader,即是說如果不是網絡,內存或磁盤問題,follower肯定會定入成功,並正常反饋ACK。Leader收到過半FollowerACK後,會廣播Commit消息給所有learner,並將事務應用到內存;Learner收到commit消息後會將事務應用到內存
,
什麼情況下zab協議會進入崩潰恢復模式
- 當服務器啓動時
- 當leader服務器出現網絡中斷、崩潰或者重啓的情況
- 集羣中已經不存在過半的服務器與該leader保持正常通信
zab協議進入崩潰恢復模式會做什麼
- 當leader出現問題,zab協議進入崩潰恢復模式,並且選舉出新的leader。當新的leader選舉出來以後,如果集羣中已經有過半機器完成了leader服務器的狀態同(數據同步),退出崩潰恢復,進入消息廣播模式
- 當新的機器加入到集羣中的時候,如果已經存在leader服務器,那麼新加入的服務器就會自覺進入數據恢復模式,找到leader進行數據同步
[外鏈圖片轉存失敗(img-JUvIJkpS-1564466530215)(.assets/1564408409704.png)]
| 安裝
單機安裝
http://apache.fayea.com/zookeeper中下載
zkCli.sh -server ip:port
zoo.cfg配置說明
tickTime:時間單位,默認值是2000ms
initLimit:10*2000,leader服務器等待follow啓動並完成同步的時間
syncLimit:5*2000,leader節點和follower節點進行心跳檢測的最大延時時間
dataDir:zk服務器存儲快照文件的目錄
clientPort:客戶端訪問的端口
dataLogDir:表示配置zk事務日誌的存儲路徑,默認在dataDir目錄下
集羣安裝
[外鏈圖片轉存失敗(img-tQ4V5Qtu-1564466530216)(.assets/1564197131345.png)]
第三步:啓動每個zookeeper
如何增加observer節點
zoo.cfg中 增加 peerType=observer
server.1=192.168.11.129:2888:3181
server.2=192.168.11.135:2888:3181
server.3=192.168.11.136:2888:3181:observer
三個端口:
2181: 對Client端提供服務
2888:集羣內機器通訊使用
3888:選舉leader,leader掛掉時使用
使用docker安裝,直接使用zookeeper:latest
版本,最新版本爲3.5.5
。
使用docker-compose啓動zk集羣
version: '3.1'
services:
zoo1:
image: zookeeper
restart: always
container_name: zoo1
ports:
- 2181:2181
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=0.0.0.0:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
networks:
zoo2:
image: zookeeper
restart: always
container_name: zoo2
ports:
- 2182:2181
environment:
ZOO_MY_ID: 2
ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=0.0.0.0:2888:3888;2181 server.3=zoo3:2888:3888;2181
zoo3:
image: zookeeper
restart: always
container_name: zoo3
ports:
- 2183:2181
environment:
ZOO_MY_ID: 3
ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=0.0.0.0:2888:3888;2181
docker-compose up -d
[外鏈圖片轉存失敗(img-zqNFwUNn-1564466530216)(.assets/1563270840302.png)]
分別將本地的2181、2182、2183端口映射到對應容器的2181端口上。
ZOO_MY_ID
:表示zk服務的id
ZOO_SERVERS
:表示zk集羣的主機列表
查看各個節點的狀態
[root@ceos03 zookeeper-svc]# docker exec -it zoo1 zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower
[root@ceos03 zookeeper-svc]# docker exec -it zoo2 zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower
[root@ceos03 zookeeper-svc]# docker exec -it zoo3 zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: leader
| 應用 - 分佈式鎖
鎖分爲兩類,一類是保持獨佔,另一種是控制時序。
獨佔鎖
所有client去創建同一個節點,如/Lock,最終成功創建的那個client擁有了這把鎖,用完成之後再刪除。
控制時序
所有client在/LOCKs下創建臨時有序節點,編號最小的獲得鎖,用完刪除,其它client依次使用,未獲得鎖的監控相臨較小編號節點即可。
控制時序代碼實現
- 導入jar包
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.5</version>
</dependency>
- zk連接
public class ZkClient {
private final static String CONNECTSTRING="192.168.10.13:2181";
private static int sessionTimeout=5000;
//獲取連接
public static ZooKeeper getInstance() throws IOException, InterruptedException {
final CountDownLatch conectStatus=new CountDownLatch(1);
ZooKeeper zooKeeper=new ZooKeeper(CONNECTSTRING, sessionTimeout, new Watcher() {
public void process(WatchedEvent event) {
if(event.getState()== Event.KeeperState.SyncConnected){
conectStatus.countDown();
}
}
});
conectStatus.await();
return zooKeeper;
}
public static int getSessionTimeout() {
return sessionTimeout;
}
}
- 分佈式鎖實現
public class DistributeLock {
private static final String ROOT_LOCKS="/LOCKS";//根節點
private ZooKeeper zooKeeper;
private int sessionTimeout; //會話超時時間
private String lockID; //記錄鎖節點id
private final static byte[] data={1,2}; //節點的數據
private CountDownLatch countDownLatch=new CountDownLatch(1);
public DistributeLock() throws IOException, InterruptedException {
this.zooKeeper=ZookeeperClient.getInstance();
this.sessionTimeout=ZookeeperClient.getSessionTimeout();
}
//獲取鎖的方法
public boolean lock(){
try {
//1. create LOCKS/00000001
lockID=zooKeeper.create(ROOT_LOCKS+"/",data, ZooDefs.Ids.
OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(Thread.currentThread().getName()+"->成功創建了lock節點["+lockID+"], 開始去競爭鎖");
//2. get all node
List<String> childrenNodes=zooKeeper.getChildren(ROOT_LOCKS,true);//獲取根節點下的所有子節點
//3. sort and get minist
SortedSet<String> sortedSet=new TreeSet<String>();
for(String children:childrenNodes){
sortedSet.add(ROOT_LOCKS+"/"+children);
}
String first=sortedSet.first(); //拿到最小的節點
if(lockID.equals(first)){
//表示當前就是最小的節點
System.out.println(Thread.currentThread().getName()+"->成功獲得鎖,lock節點爲:["+lockID+"]");
return true;
}
SortedSet<String> lessThanLockId=sortedSet.headSet(lockID);
// 4. watch close less No. node
if(!lessThanLockId.isEmpty()){
String prevLockID=lessThanLockId.last();//拿到比當前LOCKID這個幾點更小的上一個節點
zooKeeper.exists(prevLockID,new LockWatcher(countDownLatch));
countDownLatch.await(sessionTimeout, TimeUnit.MILLISECONDS);
//上面這段代碼意味着如果會話超時或者節點被刪除(釋放)了
System.out.println(Thread.currentThread().getName()+" 成功獲取鎖:["+lockID+"]");
}
return true;
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
public boolean unlock(){
System.out.println(Thread.currentThread().getName()+"->開始釋放鎖:["+lockID+"]");
try {
zooKeeper.delete(lockID,-1);
System.out.println("節點["+lockID+"]成功被刪除");
return true;
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
return false;
}
}
- main
public static void main(String[] args) {
final CountDownLatch latch=new CountDownLatch(10);
Random random=new Random();
for(int i=0;i<10;i++){
new Thread(()->{
DistributeLock lock=null;
try {
lock=new DistributeLock();
latch.countDown();
latch.await();
lock.lock();
Thread.sleep(random.nextInt(500));
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if(lock!=null){
lock.unlock();
}
}
}).start();
}
}
| 應用 - master選舉
master-slave模式
資源:https://www.cnblogs.com/sky-sql/p/6804467.html
[外鏈圖片轉存失敗(img-6zPbMhnm-1564466530216)(.assets/393620-20160627154234109-1109968833.png)]
zookeeper進行master選舉使用場景是什麼?
分佈式,Master往往用來協調集羣中的其他系統單元,具有對分佈式狀態變更的決定權。如:Master負責處理一些複雜的邏輯,並將結果同步給集羣中其他系統單元。
多個client同時去創建相同的節點,創建成功的即是master;
創建失敗的需要獲取節點上的數據,即具體的master信息;
代碼實現
1. jar依賴
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.11</version>
</dependency>
2. MasterSelector
package com.wjg.master_slave;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkNodeExistsException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class MasterSelector {
private ZkClient zkClient;
private final static String MASTER_PATH = "/master"; //需要爭搶的節點
private IZkDataListener dataListener; //註冊節點內容變化
private UserCenter server; //其他服務器
private UserCenter master; //master節點
private boolean isRunning = false;
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
public MasterSelector(UserCenter server, ZkClient zkClient) {
System.out.println("[" + server + "] 去爭搶master權限");
this.server = server;
this.zkClient = zkClient;
this.dataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
//節點如果被刪除, 發起選主操作,這裏可以處理,判斷之前的master是否爲本機,若是,則立即進行選舉,否則 ,等待5s再進行選舉
if(master.getMc_id() != server.getMc_id())
TimeUnit.SECONDS.sleep(4);
chooseMaster();
}
};
}
public void start() {
//開始選舉
if (!isRunning) {
isRunning = true;
zkClient.subscribeDataChanges(MASTER_PATH, dataListener); //註冊節點事件
chooseMaster();
}
}
public void stop() {
//停止
if (isRunning) {
isRunning = false;
scheduledExecutorService.shutdown();
zkClient.unsubscribeDataChanges(MASTER_PATH, dataListener);
releaseMaster();
}
}
//具體選master的實現邏輯
private void chooseMaster() {
if (!isRunning) {
System.out.println("當前服務沒有啓動");
return;
}
try {
zkClient.createEphemeral(MASTER_PATH, server);
master = server; //把server節點賦值給master
System.out.println(master + "->我現在已經是master,你們要聽我的");
//定時器
//master釋放(master 出現故障),5秒後釋放一次
scheduledExecutorService.schedule(() -> {
releaseMaster();//釋放鎖
}, 2, TimeUnit.SECONDS);
} catch (ZkNodeExistsException e) {
//表示master已經存在
UserCenter userCenter = zkClient.readData(MASTER_PATH, true);
if (userCenter == null) {
System.out.println("啓動操作:");
chooseMaster(); //再次獲取master
} else {
master = userCenter;
}
}
}
private void releaseMaster() {
//釋放鎖(故障模擬過程)
//判斷當前是不是master,只有master才需要釋放
if (checkIsMaster()) {
System.out.println(server+" release master!");
zkClient.delete(MASTER_PATH); //刪除
}
}
private boolean checkIsMaster() {
//判斷當前的server是不是master
UserCenter userCenter = zkClient.readData(MASTER_PATH);
if (userCenter.getMc_name().equals(server.getMc_name())) {
master = userCenter;
return true;
}
return false;
}
}
3. UserCenter
package com.wjg.master_slave;
import java.io.Serializable;
public class UserCenter implements Serializable {
private static final long serialVersionUID = -1776114173857775665L;
private int mc_id; //機器信息
private String mc_name;//機器名稱
public int getMc_id() {
return mc_id;
}
public void setMc_id(int mc_id) {
this.mc_id = mc_id;
}
public String getMc_name() {
return mc_name;
}
public void setMc_name(String mc_name) {
this.mc_name = mc_name;
}
@Override
public String toString() {
return "UserCenter{" +
"mc_id=" + mc_id +
", mc_name='" + mc_name + '\'' +
'}';
}
}
4. main
public class App
{
private final static String CONNECTSTRING="192.168.10.13:2181";
public static void main(String[] args) throws IOException {
List<MasterSelector> selectorLists = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int id = i;
Thread th = new Thread(() -> {
System.out.println(id+" started!");
ZkClient zkClient = new ZkClient(CONNECTSTRING, 5000,
10000,
new SerializableSerializer());
UserCenter userCenter = new UserCenter();
userCenter.setMc_id(id);
userCenter.setMc_name("客戶端:" + id);
MasterSelector selector = new MasterSelector(userCenter, zkClient);
selectorLists.add(selector);
selector.start();//觸發選舉操作
});
th.start();
}
System.out.println("press any key to stop!");
System.in.read();
for (MasterSelector selector : selectorLists) {
selector.stop();
}
}
}
說明:在實際生產環境中,可能會由於插拔網線等導致網絡短時的不穩定,也就是網絡抖動。由於正式生產環境中可能server在zk上註冊的信息是比較多的,而且server的數量也是比較多的,那麼每一次切換主機,每臺server要同步的數據量(比如要獲取誰是master,當前有哪些salve等信息,具體視業務不同而定)也是比較大的。那麼我們希望,這種短時間的網絡抖動最好不要影響我們的系統穩定,也就是最好選出來的master還是原來的機器,那麼就可以避免發現master更換後,各個salve因爲要同步數據等導致的zk數據網絡風暴。所以在搶主的時候,如果之前主機是本機,則立即搶主,否則延遲5s搶主。這樣就給原來主機預留出一定時間讓其在新一輪選主中佔據優勢,從而利於環境穩定。
| 應用 - 分佈隊列
先進先出隊列
- 通過getchildren獲取指定根節點下的所有子節點,子節點就是任務
- 確定自己節點在子節點中的順序
- 如果自己不是最小的節點,那麼監控比自己小的上一個子節點,否則處於等待
- 接收watcher通知,重複流程