分布式協調服務 ( 服務治理 ).
標簽(空格分隔): Java
1. 問題所在
- 主要用於解決分布式環境中多個進程之間的同步控制, 讓他們有序的去訪問某種臨界資源, 防止造成臟數據的后果.
訂單服務JVM1->商品服務(庫存五個): 我要五個
訂單服務JVM2->商品服務(庫存五個): 我要五個
訂單服務JVM3->商品服務(庫存五個): 我要五個
商品服務(庫存五個)-->訂單服務JVM1:給你五個
商品服務(庫存五個)-->訂單服務JVM2:給你五個
商品服務(庫存五個)-->訂單服務JVM3:給你五個
三個JVM 同時發送清空庫存,這個時候就造成了臟數據的問題, 庫存變成了 \(-10\)個
2. 解決方案
- 分布式鎖: 在第一個訂單服務訪問到商品服務的時候, 我們將商品服務加鎖. 這個時候 第二個訂單服務去訪問 商品服務的時候會被拒絕.
- 分布式協調的核心就是 實現分布式鎖, 而Zookeeper就是分布式鎖的實現框架.
分布式鎖
1. 目的
- 為了防止分布式系統中的多個進程之間的相互干擾, 我們需要一種分布式協調技術去對這些進程進行調度, 而這個分布式協調技術的核心就是來實現這個分布式鎖, 而Zookeeper 就是分布式鎖的實現框架 .
2. 完備條件
- 在分布式系統環境下, 一個方法在同一時間只能被一個機器的一個線程執行.
- 高可用的獲取鎖和釋放鎖.
- 高性能的獲取鎖和釋放鎖.
- 具備非阻塞特性 , 即沒有獲取到鎖將直接放回獲取鎖失敗.
- 具備失效機制, 防止死鎖. ( 在加鎖以后因為發生某些意外, 這個時候可以讓鎖失效, 而不是一直持有鎖, 造成死鎖. )
- 具備可重入特征(可以理解為重新進入, 由於多於一個任務並發適用, 而不必擔心數據錯誤).
3. 常用方案
- Memcached: 利用Memcached的add 命令. 此命令是原子性操作, 只有在key不存在的情況下, 才能add 成功, 也就意味着線程得到了鎖.
- Redis: 和Memcached的方法類似, 利用Redis的setnx命令. 此命令同樣是原子性操作, 只有在key不為空的情況下,才能set成功.
- Zookeeper: 利用Zookeeper的順序臨時節點, 來實現分布式鎖和等待隊列, Zookeeper設計的初衷,就是為了實現分布式鎖.
- Chubby: Google公司實現的粗粒度分布式鎖服務,底層利用了Paxos一致性算法.
分布式鎖實現的三個核心要素:
1. 加鎖
- 最簡單的方式是使用
setnx
命令. key是鎖的唯一標識, 按業務來決定命名. 比如想要給一種商品的秒殺活動加鎖, 可以給key
命名為lock_sale_商品ID
. 而value可以姑且設置為1
. 加鎖的偽代碼如下:
setnx(lock_sale_商品ID,1);
- 當一個線程執行
setnx
返回1
, 說明key
原本不存在, 則該線程成功得到了鎖; 當一個線程執行setnx
返回0
, 說明key
已經存在, 該線程搶鎖失敗.
2. 解鎖
- 有加鎖就有解鎖. 當得到鎖的線程執行完任務之后,需要釋放鎖, 以便其他線程可以進入. 釋放鎖的最簡單方式是執行
del
指令, 偽代碼如下:
del(lock_sale_商品ID);
3. 鎖超時
- 鎖超時是什么意思呢? 如果一個得到鎖的線程在執行任務的過程中掛掉, 來不及顯示的釋放鎖, 這塊資源將會被永遠的鎖住(死鎖), 別的線程再也別想進來. 所以
setnx
的key
必須設置一個超時時間, 以保證及時沒有被顯式的釋放, 這把鎖也要在一定的時間后自動釋放.setnx
不支持超時參數, 所以需要額外的指令,偽代碼如下:
expire(lock_sale_商品ID, 30)
綜合偽代碼如下: 如果可以獲得鎖的話, 先設置自動釋放的時間, 然后去do something
if(setnx(lock_sale_商品ID,1) == 1){
expire(lock_sale_商品ID,30)
try {
do something ......
} finally {
del(lock_sale_商品ID)
}
}
以上代碼存在三個致命問題
1. setnx
和expire
的非原子性
假設一個極端的場景, 上述setnx執行完畢得到了鎖, 但是在沒有執行expire的時候服務器宕機了, 這個時候依然是沒有過期時間的死鎖, 別的線程再也無法獲得鎖了. setnx本身是不支持傳入操作時間的, 但是set指令增加了可選參數, 其偽代碼如下:
set(lock_sale_商品ID,1,30,NX);
2. del
誤刪
不確定到底expire到底設置為多長的時間, 如果是30s的話 ,那么萬一30s內A任務沒有將 something執行完畢, 這個時候 依然將鎖釋放掉了, 此時B任務進程成果或得到了鎖. 然后A進程執行完畢, 按照
del
來釋放鎖, 這個時候就出問題了.
3. 第一種問題已經有解決方案了, 現在是第二個問題的解決方案.
為了避免這種情況的發生我們可以在
del
釋放鎖之前做一個判斷, 驗證當前的鎖是不是自己加的鎖. 具體的實現: 我們在加鎖的時候把當前的線程ID作為鎖的value
,並且在刪除之前驗證key
對應的value
是不是自己的線程ID.
// 加鎖
String threadId = Thread.currentThread().getId();
set(key,threadId,30,NX);
// 解鎖
if(threadId .equals(redisClient.get(key))){
del(key)
}
4. 但是這樣又出現 第二點的問題. 解鎖的代碼不是原子性操作.
如果判斷結束之后, 發現當前線程的ID, 當時在沒有執行
del
的時候,expire
了, 這樣就又回到了第二種方案的致命問題.
5. 致命大殺器
現在可以確定的是目前的問題解決思路是存在問題的, 應該換一種思路. 應該從第二種del誤刪這里向下繼續解決這個問題.
第二點問題描述: 可能存在多個線程同時執行該代碼塊.
第二點問題原因分析: 因為不確定代碼的執行時間, 可能設置30S的話 大家都會瘋狂超時.
第二點問題解決思路: 設置一個可以動態變化,可以滿足代碼塊運行時間的, 且可以應對宕機情況的守護進程.
方案: 給鎖開啟一個守護進程, 用來給鎖進行續航操作, 當時間到29S , 發現還沒有執行完畢的時候, 守護進程執行expire 給鎖續命, 如果宕機的話 沒人給鎖續命, 時間到了之后 也會自動釋放鎖.
什么是Zookeeper
主要有兩個功能, 分布式鎖和服務注冊與發現. 以下主要說明服務注冊與發現部分.
Zookeeper是一種分布式協調服務,用於管理大型主機. 在分布式環境中協調和管理服務是一個復雜的過程. Zookeeper通過其簡單的架構和API解決了這個問題, Zookeeper允許開發人員專注於核心應用程序邏輯, 而不必擔心應用程序的分布式特性.
Zookeeper的數據模型是一個標准的二叉樹結構. 樹是由節點Znode組成的, 但是不同於樹的節點, Znode的飲用方式是路徑引用, 類似於文件路徑.
/動物/貓
/汽車/寶馬
Znode的數據結構
// 元數據: 數據的數據, 例如數據的創建,修改時間, 大小等.
Znode{
data; // Znode存儲的信息
ACL; // 記錄Znode的訪問權限
stat; // 包含Znode的各種元數據, 比如事務的ID,版本號,時間戳,大小
child;// 當前節點的子節點引用
}
Zookeeper這樣的數據結構是為了讀多寫少的場景所設計的. Znode並不是用來存儲大規模業務數據,而是用於存儲少量的狀態和配置信息, 每個節點的數據最大不能超過1MB.
1. Zookeeper的基本操作
- 創建節點
create
- 刪除節點
delete
- 判斷節點是否存在
exists
- 獲得一個節點的數據
getData
- 設置一個節點的數據
setData
- 獲取節點下的所有子節點
getChildren
其中exists,getData,getChildren屬於讀操作. Zookeeper客戶端在請求讀操作的時候,可以選擇是否設置
Watch
.
Zookeeper的事件通知
根據事件通知機制, 如果某個服務下線,
Zookeeper
會及時發現並且將消息異步傳送至客戶端A(API GateWay
), 這個時候網關發現服務下線會及時啟用該服務的備用服務, 從而達到高可用的特性.
整個服務注冊與發現 也是基於事件通知機制.
我們可以把Watch理解成是注冊在特定Znode上的觸發器, 當這個Znode發生改變, 也就是調用了該節點的
create, delete, setData
方法的時候, 將會出發Znode上注冊的對應事件, 請求Watch的客戶端會接收到異步通知.
設置
watch
示例: 客戶端調用getData
方法,watch
參數是true
. 服務端接收到請求, 返回節點數據, 並且在對應的哈希表里插入被Watch
的Znode
的路徑,以及Watcher
列表.
異步獲取反饋信息
watch
示例: 根據上述操作WatchTable
只用已經有了/動物/貓
節點的信息, 這個時候我們對其進行delete
操作. 服務端會查找HashTable
發現該節點的信息, 然后異步通知客戶端A
,並且刪除哈希表中對應的Key-Value
.
Zookeeper的一致性
為了防止服務注冊與發現(Zookeeper)掛掉的情況, 我們需要對Zookeeper的自身實現高可用, 這個時候我們需要維護一個Zookeeper集群, 假設目前集群中有 ZkA,ZkB,ZkC , 三台機器. 該項目下存在多個項目, 每個項目將自身鏈接到 ZkA,ZkB,ZkC中某個服務注冊與發現中心. 在更新數據(包括服務注冊)的時候, 先將數據更新到主節點(Leader), 然后同步到從節點(Follwer) .
Zookeeper Atomic Broadcast
1. ZAB協議定義的三種狀態
- Looking: 選舉狀態
- Following: Follower節點所處的狀態
- Leading: Lead接待所處的狀態
最大ZXID
最大ZXID也就是節點本地的最新事務編號, 包含epoch和計數兩部分. epoch是紀元的意思, 相當於Raft算法選主時候的term.
ZXID是一個64位的數字, 低32位代表一個單調遞增計數器, 高32位代表Leader的周期. 當有新的Leader產生的時候,Leader的epoch+1, 計數器從0開始; 每當處理一個新的請求的時候, 計數器+1.
Epoch | 計數器 |
---|---|
Leader周期 | 單調遞增,從0開始 |
高32位 | 低32位 |
崩潰恢復
1. Leader Selection
- 選舉階段,此時集群中的節點處於Looking狀態( Zookeeper剛開啟的時候也是這個狀態 ), 他們會向其它節點發起投票, 投票中包含自己的服務器ID和最新事務ID.
- 將自己的ZXID和其它機器的ZXID比較, 如果發現別人的ZXID比自己的大, 也就是數據比自己的新, 那么重新發起投票, 投票給目前最大的ZXID所屬節點. (比較ZXID的大小的時候,前32位是一致的. 只能從后32位比較, 這樣就是處理請求越多的節點的ZXID的越大)
- 每次投票結束之后,服務器都會統計投票數量, 判斷是否有某個節點得到半數以上的投票. 如果存在這樣的節點, 該節點會成為准
Leader
,狀態變為Leading
. 其他節點的狀態變為```Following.
2. Discovery
- 發現階段, 用於在從節點中發現最新的ZXID和事務日志. 或許有人會問: 既然
Leader
被選為主節點, 已經是集群里面數據最新的了, 為什么還要從節點中尋找最新的事務呢?
- 為了防止某些意外情況, 比如因為網絡原因在上一個階段產生多個
Leader
的情況.
Leader
接收所有Follower
發送過來各自的epoch
值, Leader從中選出最大的epoch
,基於此值+1, 生成新的epoch分發給各個Follower
.
- 各個
Follower
收到全新的epoch
之后返回ACK
給Leader
,帶上各自最大的ZXID
和歷史事務事務日志,Leader
從中選出最大的ZXID
, 並更新自身的歷史日志.
3. Synchronization
- 同步階段, 把
Leader
剛才收集到的最新歷史事務日志, 同步給集群中所有的Follower
, 只有當半數Follower
同步成功, 這個准Leader
才能成為正式的Leader
.
- 自此故障恢復完成, 其大約需 30-120S , 期間服務注冊與發現 集群是無法正常工作的.
ZAB數據寫入(Broadcast)
- ZAB的數據寫入涉及到
Broadcast
階段, 簡單來說, 就是Zookeeper
常規情況下更新數據的時候, 有Leader
廣播到所有的Follower
. 其過程如下. (Zookeeper 數據一致性的更新方式)
- 客戶端發出寫入數據請求給任意的
Follower
. Follower
把寫入數據請求轉發給Leader
.Leader
采用二階段提交方式, 先發送Propose
廣播給Follower
.Follower
接收到Propose
消息,寫入日志成功后, 返回ACK
消息給Leader
.( 類似數據庫的insert
操作 )Leader
接收到半數以上的ACK
(類似Http狀態碼)消息, 返回成功給客戶端, 並且廣播Commit
請求給Follower
. (在第四步,insert
之后, 執行commit
操作,進行數據持久化)
ZAB 協議既不是強一致性也不是弱一致性, 而是處於兩者之間的
單調一致性(順序一致性)
. 它依靠事務的ID和版本號, 保證了數據的更新和讀取時有序的.
Zookeeper的應用場景
1. 分布式鎖
這里雅虎研究院設計Zookeeper的初衷, 利用Zookeeper的臨時順序節點可以輕松的實現 分布式鎖.
2. 服務注冊與發現
利用
Znode
和Watch
, 可以實現分布式服務的注冊與發現. 最著名的應用就是阿里的分布式RPC
框架Dubbo .
3. 共享配置和狀態信息
Redis的分布式解決方案Codis, 就利用了Zookeeper來存放數據路由表和codis-proxy節點的元信息. 同時codis-config發起的命令都會通過Zookeeper同步到各個存活的codis-proxy.
此外, kafka,Hbase,Hadoop也都依靠Zookeeper同步節點信息,實現高可用.