分布式協調服務 ( 服務治理 ).


分布式協調服務 ( 服務治理 ).

標簽(空格分隔): 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. 鎖超時

  • 鎖超時是什么意思呢? 如果一個得到鎖的線程在執行任務的過程中掛掉, 來不及顯示的釋放鎖, 這塊資源將會被永遠的鎖住(死鎖), 別的線程再也別想進來. 所以setnxkey必須設置一個超時時間, 以保證及時沒有被顯式的釋放, 這把鎖也要在一定的時間后自動釋放. 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. setnxexpire的非原子性

假設一個極端的場景, 上述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. 服務端接收到請求, 返回節點數據, 並且在對應的哈希表里插入被WatchZnode的路徑,以及Watcher列表.
設置/動物/貓 Znode的watch

異步獲取反饋信息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之后返回ACKLeader,帶上各自最大的ZXID和歷史事務事務日志,Leader從中選出最大的ZXID, 並更新自身的歷史日志.

3. Synchronization

  • 同步階段, 把Leader剛才收集到的最新歷史事務日志, 同步給集群中所有的Follower, 只有當半數Follower同步成功, 這個准Leader才能成為正式的Leader.
  • 自此故障恢復完成, 其大約需 30-120S , 期間服務注冊與發現 集群是無法正常工作的.

ZAB數據寫入(Broadcast)

  • ZAB的數據寫入涉及到Broadcast階段, 簡單來說, 就是Zookeeper常規情況下更新數據的時候, 有Leader廣播到所有的Follower. 其過程如下. (Zookeeper 數據一致性的更新方式)
  1. 客戶端發出寫入數據請求給任意的Follower.
  2. Follower把寫入數據請求轉發給Leader.
  3. Leader采用二階段提交方式, 先發送Propose廣播給Follower.
  4. Follower接收到Propose消息,寫入日志成功后, 返回ACK消息給Leader.( 類似數據庫的insert操作 )
  5. Leader接收到半數以上的ACK(類似Http狀態碼)消息, 返回成功給客戶端, 並且廣播Commit請求給Follower. (在第四步, insert之后, 執行commit操作,進行數據持久化)

ZAB 協議既不是強一致性也不是弱一致性, 而是處於兩者之間的單調一致性(順序一致性). 它依靠事務的ID和版本號, 保證了數據的更新和讀取時有序的.


Zookeeper的應用場景


1. 分布式鎖

這里雅虎研究院設計Zookeeper的初衷, 利用Zookeeper的臨時順序節點可以輕松的實現 分布式鎖.

2. 服務注冊與發現

利用ZnodeWatch, 可以實現分布式服務的注冊與發現. 最著名的應用就是阿里的分布式RPC框架Dubbo .

3. 共享配置和狀態信息

Redis的分布式解決方案Codis, 就利用了Zookeeper來存放數據路由表和codis-proxy節點的元信息. 同時codis-config發起的命令都會通過Zookeeper同步到各個存活的codis-proxy.

此外, kafka,Hbase,Hadoop也都依靠Zookeeper同步節點信息,實現高可用.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM