zookeeper學習總結
- zookeeper是一個服務於分佈式應用程序的中間件協調服務
單體架構到微服務架構
- 單體架構下,一個完整的流程比如"購物",可以在單體架構中完成整個流程。
- 分佈式架構下,服務進行細化,拆分,涉及到服務之間的通信。
SpringBoot+RestTemplate
- SpringBoot提供RestTemplate支持遠程通信,其簡化了http服務的通信
- 使用RestTemplate,只需要使用者提供URL
- RestTemplate本質上是對現有技術進行了封裝,如JDK、Apache HttpClient、OkHttp等
分佈式一致性問題
- 在一個分佈式系統中,有多個節點,每個節點都會提出一個請求,
- 但是所有節點中只有一個請求會被通過
- 被通過的請求是所有節點達成一致的結果
- 所謂一致性:提出的所有請求中能夠選出最終一個確定的請求,並且選出之後,所有節點都要知道
分佈式鎖服務
- zookeeper是作爲分佈式鎖服務的一設計
- 註冊中心是zookeeper是其能夠實現的功能之一
zookeeper集羣部署
- 進入zk安裝的conf目錄下,複製zoo.sample.cfg文件爲zoo.cfg文件,並配置dirData目錄存儲相關數據
- 在配置的dirData路徑下創建myid文件,文件內容自定義,爲每一臺zk服務器的標識
- 在conf目錄下的zoo.cfg配置文件下配置集羣
- zk啓動的時候會根據myid的內容與配置文件中的配置相匹配
- 集羣啓動後,會進行leader選舉,選出leader節點
#2181:訪問zookeeper的端口 3888:重新選舉後leadeer的端口
server.1=192.168.1.1:2181:3888
server.2=192.168.1.1:2181:3888
server.3=192.168.1.1:2181:3888
#解包
tar -zxvf z..
#啓動ZK服務
./zkServer.sh start
#查看zk服務狀態
./zkServer.sh status
#停止zk服務
./zkServer.sh stop
#重啓zk服務
./zkServer.sh restart
#連接zk
sh zkCli.sh
問題記錄
- 記錄創建zookeeper集羣時遇到的坑
- 本地虛擬機同時啓動三臺總有一臺無法運行——暫未解決
- zk啓動之後查看啓動狀態和日誌發現,節點之間未建立連接
- 通過修改服務器上的hosts文件,去掉ip解決
zookeeper集羣角色
- 在zookeeper集羣中一共有三種角色:Leader、follower、Observer
- leader節點是被選舉出的主節點,這裏稱爲leader選舉
- follow節點參與leader選舉,選舉失敗爲follower節點
- Observer爲觀察者節點,不參與選舉
zookeeper數據結構
- zk的數據結構類似於標準的文件系統,其內部維護了一系列的節點稱爲zNode
- 每個節點上都可以保存數據,數據形式是以key-value的形式存儲的
- 節點上都可以保存數據以及掛載子節點,構成層次化的樹形結構
節點類型
- 持久化節點:PERSISTENT 創建完成後一直存在zk服務器上,直到主動刪除
- 持久有序節點:PERSISTENT_SEQUENTIAL 會爲其子節點維護一個順序
- 臨時節點:EPHEMERAL 生命週期與會話綁定在一起,當客戶端失效,自動清理
- 臨時有序節點:在臨時節點的基礎上多了一個有序性
zookeeper會話
-
初始化或連接未建立的狀態是connecting
-
建立連接的狀態時connected
-
關閉連接的狀態是closed
zookeeper節點狀態信息
- zk的每個節點除了存儲數據內容之外,還存儲了節點本身的狀態信息,通過get命令可獲得詳細內容
狀態屬性 | 解釋說明 |
---|---|
czxid | Create ZXID 表示該節點被創建時的事務ID |
mzxid | Modify ZXID 表示該節點最後一次被更新時的事務ID |
ctime | Create Time 表示節點被創建的時間 |
mtime | Modify Time 節點最後一次被更新的時間 |
version | 數據節點的版本號 |
cversion | 子節點的版本號 |
aversion | 節點的ACL版本號 |
ephemeralOwner | 創建臨時節點的會話sessionID若爲持久化節點,該屬性值爲0 |
dataLength | 數據內容的長度 |
numChildren | 當前節點的子節點個數 |
pzxid | 表示當前節點的子節點列表最後一次被修改時的事務ID(這裏特指子節點列表變更) |
zk-分佈式數據的原子性
- zk爲數據節點引入了版本的概念,每個數據節點對應三類版本信息
- 對數據節點的任何更新操作都會引起版本號的變化
- 通過版本實現分佈式數據的原子性類似於悲觀鎖和樂觀鎖的概念
- 悲觀鎖、樂觀鎖概念回顧
- 悲觀鎖是數據庫中一種併發控制策略,當一個事務A正在對數據進行處理
- 數據將會處於鎖定狀態,這期間其它事務無法對數據進行操作
- 樂觀鎖,多個事務在處理過程中不受影響,在同時對一個數據進行修改時
- 在更新請求提交之前,會對數據是否衝突進行檢測,若發生衝突則提交被拒絕
- zk就是通過version來實現樂觀鎖機制的——”寫入校驗“
watcher
- zk提供了分佈式數據的發佈/訂閱功能
- zk允許客戶端向服務端註冊一個watcher監聽,當服務端一些事件觸發了watcher
- 服務端向客戶端發送一個事件通知
- watcher通知是一次性的,一旦觸發一次通知後,watcher就會失效,可以通過循環註冊實現持續監聽
zk重試策略
- 在和zk服務器建立連接的時候,有提供三種連接策略 retryPolicy
RetryOneTime | 僅僅重試一次 |
---|---|
RetryUntilElapsed | 一直重試直到規定時間結束 |
RetryNTimes | 指定最大重試次數 |
zk節點權限控制
- zk提供ACL權限控制機制(Access Control List)來保證數據的安全性
權限模式 | 授權對象 |
---|---|
IP | IP地址或IP段,被賦予權限的用戶可在指定IP或IP段進行操作 |
Digest | 最常用控制模式,設置的時候需要DigestAuthenticationProvider.generateDigest() SHA- 加 密和 base64 編碼 |
World | 最開放的控制模式,數據訪問權限對所用用戶開放 |
Surper | 超級用戶,可對節點進行任何操作 |
zk——watcher機制
- zk提供watcher監聽機制,可對節點數據變更、刪除、子節點狀態變更等事件進行監聽
- 通過監聽機制,可以基於zookeeper實現分佈式鎖、集羣管理等功能
zookeeper事件 | 事件含義 |
---|---|
EventType.NodeCreated | 當節點被創建,事件觸發 |
EventType.NodeChildrenChanged | 監聽當前節點的直接子節點 |
EventType.NodeDataChanged | 節點數據發生變更,事件觸發 |
EventType.NodeDeleted | 節點刪除,事件觸發 |
EventType.None | 客戶端連接狀態變更,事件觸發 |
-
watcher監聽是一次性的,可通過循環註冊實現永久監聽
-
Curator是對zookeeper的封裝,提供了三種watcher來監聽節點的變化
-
PathChildCache:監視一個路徑下目標子結點的創建、刪除、更新
-
NodeCache:監視當前結點的創建、更新、刪除,並將結點的數據緩存在本地
-
TreeCache:PathChildCache 和 NodeCache 的“合體”,監視路徑下的創建、更新、刪除事件,
並緩存路徑下所有孩子結點的數據
-
zk實現分佈式鎖
- 基於Curator實現分佈式鎖
- 在分佈式架構下,涉及到多個進程訪問同一個共享資源的情況
- 這個時候沒有辦法使用synchronized和lock之類的鎖實現數據安全
- zookeeper實現分佈式鎖
- zk的同級節點具有唯一性,可利用此特性實現獨佔鎖
- 即多個進程在zk的指定節點下創建一個名稱相同的節點,只有一個進程能夠創建成功,認爲其獲取了鎖
- 創建失敗的節點可通過zookeeper的watcher機制來監聽子節點的變化,監聽到刪除事件,觸發所有進程去競爭鎖
- 通過有序節點實現分佈式鎖,可避免大量進程同時去競爭鎖,每個節點只需要監聽比自己小的節點,避免了資源浪費
- curator基於zk提供了分佈式鎖的基本使用
Curator 實現分佈式鎖的基本原理
- 利用path創建臨時有序節點,實現公平鎖核心,1代表了能獲得分佈式鎖的數量,即爲互斥鎖
- 由LockInternals對象執行分佈式鎖的申請和釋放
- internaLock(-1, null):不限時等待 internaLock:限時等待
private boolean internalLock(long time, TimeUnit unit) throws Exception
{
Thread currentThread = Thread.currentThread();
// 這裏給定的 LockData 實例只能由一個線程操作
LockData lockData = threadData.get(currentThread);
if ( lockData != null )
{
// 實現可重入,lockData映射表存放線程的鎖信息
lockData.lockCount.incrementAndGet();
return true;
}
/// 映射表 中沒有對應的鎖信息, ,通過 attempLock獲得鎖
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath != null )
{
// 獲取鎖成功記錄 鎖信息到映射表 即 thread
// private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
return false;
}
- lockCount:分佈式鎖重入次數
- attempLock 嘗試獲取鎖,並返回鎖對應的 Zookeeper 臨時順序節點的路徑
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
{
final long startMillis = System.currentTimeMillis();
// 無限時等待 millisToWait 爲null
final Long millisToWait = (unit != null) ? unit.toMillis(time) : null;
final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
int retryCount = 0;
// 臨時節點路徑
String ourPath = null;
// 是否已經持有分佈式鎖
boolean hasTheLock = false;
// 是否已經完成獲取鎖操作
boolean isDone = false;
while ( !isDone )
{
isDone = true;
try
{
// 在 zk 中創建臨時順序節點
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
// 循環等待來激活分佈式鎖,實現鎖的公平性
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
}
catch ( KeeperException.NoNodeException e )
{
// gets thrown by StandardLockInternalsDriver when it can't find the lock node
// this can happen when the session expires, etc. So, if the retry allows, just try it all again
if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) )
{
isDone = false;
}
else
{
throw e;
}
}
}
if ( hasTheLock )
{
// 成功獲得分佈式鎖,返回臨時順序節點的路徑上層將其封裝成鎖信息,記錄在映射表,方便鎖重入
return ourPath;
}
return null;
}
- 創建臨時順序節點即鎖節點,createsTheLock
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception
{
String ourPath;
// lockNodeBytes爲空的話,採用默認(IP地址)創建節點
if ( lockNodeBytes != null )
{
ourPath=client.create()
.creatingParentContainersIfNeeded()
.withProtection()
// 保證臨時節點的有序性
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(path, lockNodeBytes);
}
else
{
ourPath = client.create()
.creatingParentContainersIfNeeded()
.withProtection()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(path);
}
return ourPath;
}
- internalLockLoop 判斷是否已經持有分佈式鎖:循環等待激活,實現鎖的公平性
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception
{
// 是否已經持有鎖
boolean haveTheLock = false;
// 是否刪除子節點
boolean doDelete = false;
try
{
if ( revocable.get() != null )
{
client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
}
while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock )
{
// 獲取排序後的子節點列表
List<String> children = getSortedChildren();
// +1 to include the slash 獲取自己創建的臨時子節點的名稱
String sequenceNodeName = ourPath.substring(basePath.length() + 1);
PredicateResults predicateResults = driver
.getsTheLock(client, children, sequenceNodeName, maxLeases);
if ( predicateResults.getsTheLock() )
{
// 獲得了鎖,中斷循環
haveTheLock = true;
}
else
{
// 沒有獲得鎖,,監聽上一臨時順序節點
String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
synchronized(this)
{
try
{
// use getData() instead of exists() to avoid leaving unneeded watchers which is a type of resource leak // 使用 getData() 避免資源泄露,exists會創建不必要的監聽器
// 這裏 如果監聽到上一順序節點被刪除,,則會獲得鎖
client.getData().usingWatcher(watcher).forPath(previousSequencePath);
if ( millisToWait != null )
{
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
if ( millisToWait <= 0 )
{
// 獲取鎖超時 刪除屬於當前進程的臨時順序節點
doDelete = true; // timed out - delete our node
break;
}
wait(millisToWait);
}
else
{
wait();
}
}
catch ( KeeperException.NoNodeException e )
{
// it has been deleted (i.e. lock released). Try to acquire again
}
}
}
}
}
catch ( Exception e )
{
ThreadUtils.checkInterrupted(e);
doDelete = true;
throw e;
}
finally
{
if ( doDelete )
{
deleteOurPath(ourPath);
}
}
return haveTheLock;
}
- getsTheLock獲得鎖的邏輯
- 獲取臨時有序節點在排序後的列表中的索引
- 校驗創建的臨時有序節點是否有效
- 鎖公平性核心邏輯
- 已經瞭解到maxLeases爲1,即當ourIndex爲零的時候,線程才能獲得鎖
- zk臨時有序節點特性,保證了跨多個JVM線程併發創建節點時的順序性,越早創建成功越早激活鎖
- 如果已經獲取了鎖,無需監聽任何節點,否則需要監聽上一順序節點,,即 ourIndex-1
- 鎖是公平的,因此無需監聽除了 ourIndex-1 以外的任何節點
- 如果 ourIndex < 0,則表示會話過期或連接丟失等原因,該線程創建的臨時有序節點被zk刪除,拋出異常
- 如果在重試策略允許的範圍內重新嘗試獲取鎖,然後重新生成新的臨時有序節點
- 釋放鎖邏輯
- newLockCount記錄了鎖重入次數,初始值爲1,當 newLockCount=0,鎖釋放
- 然後從映射表中移除對應線程的鎖信息
zookeeper的leader選舉原理
-
在zk的分佈式集羣中,有着三種角色,leader、follower、observer
-
curator提供了兩種選舉 recipe -> Leader Latch 和 Leader Selector
-
Leader Latch:參與選舉的所有節點都會創建一個順序節點最小的節點會被設置爲master節點
沒有搶佔到Leader節點的節點都會對前一個節點的刪除事件進行監聽,
當其刪除或master節點手動調用close()或master節點掛掉,後續節點會搶佔master
-
Leader Selector:和Leader Latch的差別在於leader節點在釋放領導權之後會繼續參與競爭
-
zookeeper數據同步
- zookeeper通過三種不同的集羣角色(leader、follower、observer),組成整個高性能集羣
- zk集羣中,客戶端會隨機連接到zookeeper集羣中的一個節點,如果是讀請求,直接從當前節點讀取數據
- 如果是寫請求,請求將會被轉發給leader提交事務
- 然後leader會廣播事務,只要有超過半數節點寫入成功,那麼寫請求就會被提交
流程詳解
- 1.客戶端發出寫事務請求到zk集羣中的follower節點
- 2.follower節點轉發事務請求到leader節點進行處理
- 3.leader節點發起proposal事務廣播
- 4.follower節點發回ack消息確認
- 5.commit提交事務
- 6.response返回客戶端
zookeeper——ZAB協議
- ZAB(zookeeper Atomic Broadcast)協議是爲分佈式協調服務zookeeper專門設計的
- 支持 崩潰恢復 的原子廣播協議
- 在zk中,主要依賴ZAB協議來實現分佈式數據的一致性
- 基於該協議,zk實現了主備模式的系統架構中集羣中各個副本之間的數據一致性
- zab協議包含兩種基本模式:崩潰恢復和原子廣播
崩潰恢復和原子廣播
- 當zk集羣啓動後,leader節點出現網絡中斷、崩潰等情況時
- 基於zab協議zk會自動進入恢復模式並選舉出新的leader
- 當leader選舉出來之後,並且集羣中有過半機器和leader節點的數據完成同步後
- zab協議就會退出恢復模式,然後整個集羣進入消息廣播模式
- 此時當一臺同樣遵守ZAB協議的服務器啓動後加入到集羣中時,如果集羣中存在leader服務器進行消息廣播
- 新加入的服務器會自動進入數據恢復模式,進行數據同步,然後一起參與到消息廣播流程中去
- 注意點:如果一個事務proposal在一臺機器上處理成功,那麼該事務應該在所有機器上都被處理成功
- 即,已經被處理的消息不能丟棄。被丟棄的消息不能再次出現
- 判斷依據是:消息是否被commit成功
消息廣播實現原理
- 消息廣播的過程實際上是一個簡化版本的二階段提交過程,類似於分佈式事務的2pc協議
- 流程分析
- leader接收到事務請求後,會生成一個全局唯一的64位的自增id,即zxid通過其大小保證消息因果順序
- leader爲每一個follower節點準備了一個FIFO的隊列(通過TCP協議實現,以實現全局有序的的特點)
- 將帶有zxid的消息作爲一個提案(Proposal)分發給所有的follower
- follower節點收到proposal後,會先把proposal寫到磁盤,寫入成功後返回給leader一個ack
- 當leader接收到合法數量即超過半數節點的ACK之後,leader就會向這些follower發送commit命令,同時在本地執行目標消息
- follower節點收到commit命令之後,會提交該消息(leader的投票過程不需要Observer的ack,僅做數據同步即可)
數據一致性保證
- leader選舉算法能夠保證重新選舉出來的leader擁有集羣中所有機器最高編號zxid——zxid最大的事務proposal
- 那麼就可以保證重新選舉的leader一定具有已經提交的proposal
- 這是因爲所有提案被commit之前必須有超過半數的follower節點發出ack確認,即擁有消息提案
- 因此只要有合法數量的節點正常工作,就必然有一個節點保存了所有被commit消息的proposal狀態
關於zxid
-
zxid構成以及作用
-
zxid是一個64位的數據,高32位時epoch編號,每經過一次leader選舉產生一個新的leader,新的leader會將epoch號加1
-
低32位是消息計數器,每收到一條消息,該值+1,新的leader選舉後,消息被重置爲0
- 基於此設計,保證了掛掉的leader重新啓動之後不會被選舉爲leader,它的zxid小於當前新的leader
- 當舊的leader作爲follower接入之後,新的leader會讓其清除所有違背commit的proposal的epoch號
-
zxid說明
-
爲了保證事務的順序一致性,zk採用了遞增的事務id號(zxid)來標識事務
-
所有的提案proposal都會被加上zxid
zookeeper的一致性
- zk集羣內部的數據副本同步是基於半提交策略的,意味着它是最終一致性,而不滿足強一致性要求
- zookeeper基於zxid以及阻塞隊列的方式實現請求的順序一致性
- 如果一個client請求連接到一臺follower節點,讀取到最新數據,由於網絡原因連接到其它節點
- 若是重新連接到的follower節點還沒有完成數據同步,將會讀到舊的數據
- 針對這種情況,client會記錄自己已經讀取到的最大的zxid,如果重新連接到server
- 發現client持有的zxid比server的zxid要大,則連接會失敗
zookeeper——leader選舉原理
- leader選舉存在於兩個階段中,一個是服務器啓動時的leader選舉
- 其二是運行過程中leader節點宕機導致的leader選舉
重要參數
- 服務器ID:myid:myid是服務器的編號,編號越大在選擇算法中的權重越大
- zxid:事務id:值越大說明數據越新,在選舉算法中權重越大
- 邏輯時鐘也叫投票次數:epoch – logicalclock
- 在同一輪投票過程中epoch-logicalclock的值是相同的,每次投票結束,該數值會增加
- 然後接收其它服務器返回的投票信息中的數值進行比較,根據不同的值做出判斷
- 選舉狀態
LOOKING | 競選狀態 |
---|---|
FOLLOWING | 隨從狀態,同步 leader 狀態,參與投票 |
OBSERVING | 觀察狀態,同步 leader 狀態,不參與投票 |
LEADING | 領導者狀態 |
服務啓動時的leader選舉
- 每個節點啓動時的狀態都是LOOKING
- 在集羣初始化階段,當有一臺服務器server1啓動的時候,無法單獨進行和完成leader選舉
- 當有多臺服務器啓動的時候,服務器之間此時可以進行相互通信,開始進入leader選舉
|-每個server發起一次投票,首次投票,會將自身作爲Leader服務器來進行投票
|-票據信息中會包含服務器的myid、zxid和epoch,使用vote進行封裝(myid,zxid,epoch)
|-然後各自的投票信息發送給其它服務器
|-收到來自其它服務器的投票信息後,會首先判斷投票是否有效(epoch),是否來自looking狀態的服務器
|-然後進行pk,
——比較epoch
——檢查zxid,zxid較大的服務器優先作爲Leader,zxid相同比較myid
——myid較大的服務器作爲leader服務器
|-投票結束後會統計投票信息,當有過半服務器接受了相同的投票信息,即選出了leader節點
|-確定leader之後,每個服務器會更新自己的狀態,follower->followering leader->leadering
運行過中的leader選舉
- 當集羣中的 leader 服務器出現宕機或者不可用的情況時,進入新一輪leader選舉
- leader宕機之後,剩餘follower服務器,將自己的狀態改變爲looking,然後開始進入leader選舉過程