天天看點

​我們的系統需要什麼樣的分布式鎖?

​我們的系統需要什麼樣的分布式鎖?

一 從單機鎖到分布式鎖

在單機環境中,當共享資源自身無法提供互斥能力的時候,為了防止多線程/多程序對共享資源的同時讀寫通路造成的資料破壞,就需要一個第三方提供的互斥的能力,這裡往往是核心或者提供互斥能力的類庫,如下圖所示,程序首先從核心/類庫擷取一把互斥鎖,拿到鎖的程序就可以排他性的通路共享資源。演化到分布式環境,我們就需要一個提供同樣功能的分布式服務,不同的機器通過該服務擷取一把鎖,擷取到鎖的機器就可以排他性的通路共享資源,這樣的服務我們統稱為分布式鎖服務,鎖也就叫分布式鎖。

​我們的系統需要什麼樣的分布式鎖?

由此抽象一下分布式鎖的概念,首先分布式鎖需要是一個資源,這個資源能夠提供并發控制,并輸出一個排他性的狀态,也就是:

鎖 = 資源 + 并發控制 + 所有權展示

以常見的單機鎖為例:

  • Spinlock = BOOL +CAS(樂觀鎖)
  • Mutex = BOOL + CAS + 通知(悲觀鎖)

Spinlock 和 Mutex 都是一個 Bool 資源,通過原子的 CAS 指令:當現在為 0 設定為 1,成功的話持有鎖,失敗的話不持有鎖,如果不提供所有權的展示,例如 AtomicInteger,也是通過資源(Interger)+ CAS,但是不會明确的提示所有權,是以不會被視為一種鎖,當然,可以将“所有權展示”這個更多地視為某種服務提供形式的包裝。

單機環境下,核心具備“上帝視角”,能夠知道程序的存活,當程序挂掉的時候可以将該程序持有的鎖資源釋放,但發展到分布式環境,這就變成了一個挑戰,為了應對各種機器故障、當機等,就需要給鎖提供了一個新的特性:可用性。

如下圖所示,任何提供三個特性的服務都可以提供分布式鎖的能力,資源可以是檔案、KV 等,通過建立檔案、KV 等原子操作,通過建立成功的結果來表明所有權的歸屬,同時通過 TTL 或者會話來保證鎖的可用性。

​我們的系統需要什麼樣的分布式鎖?

二 分布式鎖的系統分類

根據鎖資源本身的安全性,我們将分布式鎖分為兩個陣營:

  • 基于異步複制的分布式系統,例如 mysql,tair,redis 等。
  • 基于 paxos 協定的分布式一緻性系統,例如 zookeeper,etcd,consul 等。

基于異步複制的分布式系統,存在資料丢失(丢鎖)的風險,不夠安全,往往通過 TTL 的機制承擔細粒度的鎖服務,該系統接入簡單,适用于對時間很敏感,期望設定一個較短的有效期,執行短期任務,丢鎖對業務影響相對可控的服務。

基于 paxos 協定的分布式系統,通過一緻性協定保證資料的多副本,資料安全性高,往往通過租約(會話)的機制承擔粗粒度的鎖服務,該系統需要一定的門檻,适用于對安全性很敏感,希望長期持有鎖,不期望發生丢鎖現象的服務。

三 阿裡雲存儲分布式鎖

阿裡雲存儲在長期的實踐過程中,在如何提升分布式鎖使用時的正确性、保證鎖的可用性以及提升鎖的切換效率方面積累比較多的經驗。

1 嚴格互斥性

互斥性作為分布式鎖最基本的要求,對使用者而言就是不能出現“一鎖多占”,那麼存儲分布式鎖是如何避免該情況的呢?

答案是,服務端每把鎖都和唯一的會話綁定,用戶端通過定期發送心跳來保證會話的有效性,也就保證了鎖的擁有權。當心跳不能維持時,會話連同關聯的鎖節點都會被釋放,鎖節點就可以被重新搶占。這裡有一個關鍵的地方,就是如何保證用戶端和服務端的同步,在服務端會話過期的時候,用戶端也能感覺。

如下圖所示,在用戶端和服務端都維護了會話的有效期的時間,用戶端從心跳發送時刻(S0)開始計時,服務端從收到請求(S1)開始計時,這樣就能保證用戶端會先于服務端過期。 使用者在建立鎖之後,核心工作線程在進行核心操作之前可以判斷是否有足夠的有效期,同時我們不再依賴牆上時間,而是基于系統時鐘來對時間進行判斷,系統時鐘更加精确,且不會向前或者向後移動(秒級别誤差毫秒級,同時在 NTP 跳變的場景,最多會修改時鐘的速率)。

​我們的系統需要什麼樣的分布式鎖?

在分布式鎖互斥性上,我們是不是做到完美了?并非如此,還是存在一種情況,業務基于分布式鎖服務的通路互斥會被破壞。

我們來看下面的例子:如下圖所示,用戶端在時間點(S0)嘗試去搶鎖,在時間點(S1)在後端搶鎖成功,是以也産生了一個分布式鎖的有效期視窗。在有效期内,時間點(S2)做了一個通路存儲的操作,很快完成,然後在時間點(S3)判斷鎖的有效期依舊成立,繼續執行通路存儲操作,結果這個操作耗時良久,超過了分布式鎖的過期時間,那麼可能這個時候,分布式鎖已經被其他用戶端搶占成功,進而出現兩個用戶端同時操作同一批資料的可能性,這種可能性是存在的,雖然機率很小。

​我們的系統需要什麼樣的分布式鎖?

針對這個場景,具體的應對方案是在操作資料的時候確定有足夠的鎖有效期視窗,當然如果業務本身提供復原機制的話,那麼方案就更加完備,該方案也在存儲産品使用分布式鎖的過程中被采用。

還有一個更佳的方案,即,存儲系統本身引入 IOFence 能力。這裡就不得不提 Martin Kleppmann 和 redis 的作者 antirez 之間的讨論了。redis 為了防止異步複制導緻的鎖丢失的問題,引入了 redlock,該方案引入了多數派的機制,需要獲得多數派的鎖,最大程度的保證了可用性和正确性,但仍然有兩個問題:

  • 牆上時間的不可靠(NTP 時間)
  • 異構系統的無法做到嚴格正确性

牆上時間可以通過非牆上時間 MonoticTime 來解決(redis 目前仍然依賴牆上時間),但是異構系統隻有一個系統并沒有辦法保證完全正确。如下圖所示,Client1 擷取了鎖,在操作資料的時候發生了 GC,在 GC 完成時候丢失了鎖的所有權,造成了資料不一緻。

​我們的系統需要什麼樣的分布式鎖?

是以需要兩個系統同時協作來完成一個完全正确的互斥通路,在存儲系統引入 IOFence能力,如下圖所示,全局鎖服務提供全局自增的 token,Client 1 拿到鎖傳回的 token 是 33,并帶入存儲系統,發生 GC,當 Client 2 搶鎖成功傳回 34,帶入存儲系統,存儲系統會拒絕 token 較小的請求,那麼經過了長時間 full gc 重新恢複後的 Client 1 再次寫入資料的時候,因為存儲層記錄的 token 已經更新,攜帶 token 值為 33 的請求将被直接拒絕,進而達到了資料保護的效果(chubby 的論文中有講述,也是 Martin Kleppmann 提出的解決方案)。

​我們的系統需要什麼樣的分布式鎖?

這與阿裡雲分布式存儲平台盤古的設計思路不謀而合,盤古支援了類似 IO Fence 的寫保護能力,引入 Inline File 的檔案類型,配合 SealFile 操作,這就有着類似 IO Fence 的寫保護能力。首先,SealFile 操作用來關閉已經打開的 cs 上面的檔案,防止舊的 Owner 繼續寫資料;其次,InlineFile 可以防止舊的 Owner 打開新的檔案。這兩個功能事實上也是提供了存儲系統中的 Token 支援。

2 可用性

存儲分布式鎖通過持續心跳來保證鎖的健壯性,讓使用者不用投入很多精力關注可用性,但也有可能異常的使用者程序持續占據鎖。針對該場景,為了保證鎖最終可以被排程,提供了可以安全釋放鎖的會話加黑機制。

當使用者需要将發生假死的程序持有的鎖釋放時,可以通過查詢會話資訊,并将會話加黑,此後,心跳将不能正常維護,最終導緻會話過期,鎖節點被安全釋放。這裡我們不是強制删除鎖,而是選用禁用心跳的原因如下:

  • 删除鎖操作本身不安全,如果鎖已經被其他人正常搶占,此時删鎖請求會産生誤删除。
  • 删除鎖後,持有鎖的人會話依然正常,它仍然認為自己持有鎖,會打破鎖的互斥性原則。

3 切換效率

當程序持有的鎖需要被重新排程時,持有者可以主動删除鎖節點,但當持有者發生異常(如程序重新開機,機器當機等),新的程序要重新搶占,就需要等待原先的會話過期後,才有機會搶占成功。預設情況下,分布式鎖使用的會話生命期為數十秒,當持有鎖的程序意外退出後(未主動釋放鎖),最長需要經過很長時間鎖節點才可以被再次搶占。

​我們的系統需要什麼樣的分布式鎖?

要提升切換精度,本質上要壓縮會話生命周期,同時也意味着更快的心跳頻率,對後端更大的通路壓力。我們通過對進行優化,使得會話周期可以進一步壓縮。

同時結合具體的業務場景,例如守護程序發現鎖持有程序挂掉的場景,提供鎖的 CAS 釋放操作,使得程序可以零等待進行搶鎖。比如利用在鎖節點中存放程序的唯一辨別,強制釋放已經不再使用的鎖,并重新争搶,該方式可以徹底避免程序更新或意外重新開機後搶鎖需要的等待時間。

四 結語

分布式鎖提供了分布式環境下共享資源的互斥通路,業務或者依賴分布式鎖追求效率提升,或者依賴分布式鎖追求通路的絕對互斥。同時,在接入分布式鎖服務過程中,要考慮接入成本、服務可靠性、分布式鎖切換精度以及正确性等問題,正确和合理的使用分布式鎖,是需要持續思考并予以優化的。

參考文章

How to do distributed locking - Martin Kleppmann

Is Redlock safe? - antirez

chubby 論文 - google