基於 Redis 的分佈式鎖到底安全嗎?

【完整版】javascript

網上有關Redis分佈式鎖的文章可謂多如牛毛了,不信的話你能夠拿關鍵詞「Redis 分佈式鎖」隨便到哪一個搜索引擎上去搜索一下就知道了。這些文章的思路大致相近,給出的實現算法也看似合乎邏輯,但當咱們着手去實現它們的時候,卻發現若是你越是仔細推敲,疑慮也就愈來愈多。html

實際上,大概在一年之前,關於Redis分佈式鎖的安全性問題,在分佈式系統專家Martin Kleppmann和Redis的做者antirez之間就發生過一場爭論。因爲對這個問題一直以來比較關注,因此我前些日子仔細閱讀了與這場爭論相關的資料。這場爭論的大概過程是這樣的:爲了規範各家對基於Redis的分佈式鎖的實現,Redis的做者提出了一個更安全的實現,叫作Redlock。有一天,Martin Kleppmann寫了一篇blog,分析了Redlock在安全性上存在的一些問題。而後Redis的做者當即寫了一篇blog來反駁Martin的分析。但Martin表示仍然堅持原來的觀點。隨後,這個問題在Twitter和Hacker News上引起了激烈的討論,不少分佈式系統的專家都參與其中。java

對於那些對分佈式系統感興趣的人來講,這個事件很是值得關注。無論你是剛接觸分佈式系統的新手,仍是有着多年分佈式開發經驗的老手,讀完這些分析和評論以後,大概都會有所收穫。要知道,親手實現過Redis Cluster這樣一個複雜系統的antirez,足以算得上分佈式領域的一名專家了。但對於由分佈式鎖引起的一系列問題的分析中,不一樣的專家卻能得出迥異的結論,從中咱們能夠窺見分佈式系統相關的問題具備何等的複雜性。實際上,在分佈式系統的設計中常常發生的事情是:許多想法初看起來毫無破綻,而一旦詳加考量,卻發現不是那麼完美無缺。node

下面,咱們就從頭到尾把這場爭論過程當中各方的觀點進行一下回顧和分析。在這個過程當中,咱們把影響分佈式鎖的安全性的那些技術細節展開進行討論,這將是一件頗有意思的事情。這也是一個比較長的故事。固然,其中也免不了包含一些小「八卦」。web

Redlock算法

就像本文開頭所講的,藉助Redis來實現一個分佈式鎖(Distributed Lock)的作法,已經有不少人嘗試過。人們構建這樣的分佈式鎖的目的,是爲了對一些共享資源進行互斥訪問。redis

可是,這些實現雖然思路大致相近,但實現細節上各不相同,它們能提供的安全性和可用性也不盡相同。因此,Redis的做者antirez給出了一個更好的實現,稱爲Redlock,算是Redis官方對於實現分佈式鎖的指導規範。Redlock的算法描述就放在Redis的官網上:算法

在Redlock以前,不少人對於分佈式鎖的實現都是基於單個Redis節點的。而Redlock是基於多個Redis節點(都是Master)的一種實現。爲了能理解Redlock,咱們首先須要把簡單的基於單Redis節點的算法描述清楚,由於它是Redlock的基礎。數據庫

基於單Redis節點的分佈式鎖

首先,Redis客戶端爲了獲取鎖,向Redis節點發送以下命令:apache

SET resource_name my_random_value NX PX 30000複製代碼

上面的命令若是執行成功,則客戶端成功獲取到了鎖,接下來就能夠訪問共享資源了;而若是上面的命令執行失敗,則說明獲取鎖失敗。c#

注意,在上面的SET命令中:

  • my_random_value是由客戶端生成的一個隨機字符串,它要保證在足夠長的一段時間內在全部客戶端的全部獲取鎖的請求中都是惟一的。
  • NX表示只有當resource_name對應的key值不存在的時候才能SET成功。這保證了只有第一個請求的客戶端才能得到鎖,而其它客戶端在鎖被釋放以前都沒法得到鎖。
  • PX 30000表示這個鎖有一個30秒的自動過時時間。固然,這裏30秒只是一個例子,客戶端能夠選擇合適的過時時間。

最後,當客戶端完成了對共享資源的操做以後,執行下面的Redis Lua腳原本釋放鎖

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end複製代碼

這段Lua腳本在執行的時候要把前面的my_random_value做爲ARGV[1]的值傳進去,把resource_name做爲KEYS[1]的值傳進去。

至此,基於單Redis節點的分佈式鎖的算法就描述完了。這裏面有好幾個問題須要重點分析一下。

首先第一個問題,這個鎖必需要設置一個過時時間。不然的話,當一個客戶端獲取鎖成功以後,假如它崩潰了,或者因爲發生了網絡分割(network partition)致使它再也沒法和Redis節點通訊了,那麼它就會一直持有這個鎖,而其它客戶端永遠沒法得到鎖了。antirez在後面的分析中也特別強調了這一點,並且把這個過時時間稱爲鎖的有效時間(lock validity time)。得到鎖的客戶端必須在這個時間以內完成對共享資源的訪問。

第二個問題,第一步獲取鎖的操做,網上很多文章把它實現成了兩個Redis命令:

SETNX resource_name my_random_value
EXPIRE resource_name 30複製代碼

雖然這兩個命令和前面算法描述中的一個SET命令執行效果相同,但卻不是原子的。若是客戶端在執行完SETNX後崩潰了,那麼就沒有機會執行EXPIRE了,致使它一直持有這個鎖。

第三個問題,也是antirez指出的,設置一個隨機字符串my_random_value是頗有必要的,它保證了一個客戶端釋放的鎖必須是本身持有的那個鎖。假如獲取鎖時SET的不是一個隨機字符串,而是一個固定值,那麼可能會發生下面的執行序列:

  1. 客戶端1獲取鎖成功。
  2. 客戶端1在某個操做上阻塞了很長時間。
  3. 過時時間到了,鎖自動釋放了。
  4. 客戶端2獲取到了對應同一個資源的鎖。
  5. 客戶端1從阻塞中恢復過來,釋放掉了客戶端2持有的鎖。

以後,客戶端2在訪問共享資源的時候,就沒有鎖爲它提供保護了。

第四個問題,釋放鎖的操做必須使用Lua腳原本實現。釋放鎖其實包含三步操做:'GET'、判斷和'DEL',用Lua腳原本實現能保證這三步的原子性。不然,若是把這三步操做放到客戶端邏輯中去執行的話,就有可能發生與前面第三個問題相似的執行序列:

  1. 客戶端1獲取鎖成功。
  2. 客戶端1訪問共享資源。
  3. 客戶端1爲了釋放鎖,先執行'GET'操做獲取隨機字符串的值。
  4. 客戶端1判斷隨機字符串的值,與預期的值相等。
  5. 客戶端1因爲某個緣由阻塞住了很長時間。
  6. 過時時間到了,鎖自動釋放了。
  7. 客戶端2獲取到了對應同一個資源的鎖。
  8. 客戶端1從阻塞中恢復過來,執行DEL操縱,釋放掉了客戶端2持有的鎖。

實際上,在上述第三個問題和第四個問題的分析中,若是不是客戶端阻塞住了,而是出現了大的網絡延遲,也有可能致使相似的執行序列發生。

前面的四個問題,只要實現分佈式鎖的時候加以注意,就都可以被正確處理。但除此以外,antirez還指出了一個問題,是由failover引發的,倒是基於單Redis節點的分佈式鎖沒法解決的。正是這個問題催生了Redlock的出現。

這個問題是這樣的。假如Redis節點宕機了,那麼全部客戶端就都沒法得到鎖了,服務變得不可用。爲了提升可用性,咱們能夠給這個Redis節點掛一個Slave,當Master節點不可用的時候,系統自動切到Slave上(failover)。但因爲Redis的主從複製(replication)是異步的,這可能致使在failover過程當中喪失鎖的安全性。考慮下面的執行序列:

  1. 客戶端1從Master獲取了鎖。
  2. Master宕機了,存儲鎖的key尚未來得及同步到Slave上。
  3. Slave升級爲Master。
  4. 客戶端2重新的Master獲取到了對應同一個資源的鎖。

因而,客戶端1和客戶端2同時持有了同一個資源的鎖。鎖的安全性被打破。針對這個問題,antirez設計了Redlock算法,咱們接下來會討論。

其它疑問

前面這個算法中出現的鎖的有效時間(lock validity time),設置成多少合適呢?若是設置過短的話,鎖就有可能在客戶端完成對於共享資源的訪問以前過時,從而失去保護;若是設置太長的話,一旦某個持有鎖的客戶端釋放鎖失敗,那麼就會致使全部其它客戶端都沒法獲取鎖,從而長時間內沒法正常工做。看來真是個兩難的問題。

並且,在前面對於隨機字符串my_random_value的分析中,antirez也在文章中認可的確應該考慮客戶端長期阻塞致使鎖過時的狀況。若是真的發生了這種狀況,那麼共享資源是否是已經失去了保護呢?antirez從新設計的Redlock是否能解決這些問題呢?

分佈式鎖Redlock

因爲前面介紹的基於單Redis節點的分佈式鎖在failover的時候會產生解決不了的安全性問題,所以antirez提出了新的分佈式鎖的算法Redlock,它基於N個徹底獨立的Redis節點(一般狀況下N能夠設置成5)。

運行Redlock算法的客戶端依次執行下面各個步驟,來完成獲取鎖的操做:

  1. 獲取當前時間(毫秒數)。
  2. 按順序依次向N個Redis節點執行獲取鎖的操做。這個獲取操做跟前面基於單Redis節點的獲取鎖的過程相同,包含隨機字符串my_random_value,也包含過時時間(好比PX 30000,即鎖的有效時間)。爲了保證在某個Redis節點不可用的時候算法可以繼續運行,這個獲取鎖的操做還有一個超時時間(time out),它要遠小於鎖的有效時間(幾十毫秒量級)。客戶端在向某個Redis節點獲取鎖失敗之後,應該當即嘗試下一個Redis節點。這裏的失敗,應該包含任何類型的失敗,好比該Redis節點不可用,或者該Redis節點上的鎖已經被其它客戶端持有(注:Redlock原文中這裏只提到了Redis節點不可用的狀況,但也應該包含其它的失敗狀況)。
  3. 計算整個獲取鎖的過程總共消耗了多長時間,計算方法是用當前時間減去第1步記錄的時間。若是客戶端從大多數Redis節點(>= N/2+1)成功獲取到了鎖,而且獲取鎖總共消耗的時間沒有超過鎖的有效時間(lock validity time),那麼這時客戶端才認爲最終獲取鎖成功;不然,認爲最終獲取鎖失敗。
  4. 若是最終獲取鎖成功了,那麼這個鎖的有效時間應該從新計算,它等於最初的鎖的有效時間減去第3步計算出來的獲取鎖消耗的時間。
  5. 若是最終獲取鎖失敗了(可能因爲獲取到鎖的Redis節點個數少於N/2+1,或者整個獲取鎖的過程消耗的時間超過了鎖的最初有效時間),那麼客戶端應該當即向全部Redis節點發起釋放鎖的操做(即前面介紹的Redis Lua腳本)。

固然,上面描述的只是獲取鎖的過程,而釋放鎖的過程比較簡單:客戶端向全部Redis節點發起釋放鎖的操做,無論這些節點當時在獲取鎖的時候成功與否。

因爲N個Redis節點中的大多數能正常工做就能保證Redlock正常工做,所以理論上它的可用性更高。咱們前面討論的單Redis節點的分佈式鎖在failover的時候鎖失效的問題,在Redlock中不存在了,但若是有節點發生崩潰重啓,仍是會對鎖的安全性有影響的。具體的影響程度跟Redis對數據的持久化程度有關。

假設一共有5個Redis節點:A, B, C, D, E。設想發生了以下的事件序列:

  1. 客戶端1成功鎖住了A, B, C,獲取鎖成功(但D和E沒有鎖住)。
  2. 節點C崩潰重啓了,但客戶端1在C上加的鎖沒有持久化下來,丟失了。
  3. 節點C重啓後,客戶端2鎖住了C, D, E,獲取鎖成功。

這樣,客戶端1和客戶端2同時得到了鎖(針對同一資源)。

在默認狀況下,Redis的AOF持久化方式是每秒寫一次磁盤(即執行fsync),所以最壞狀況下可能丟失1秒的數據。爲了儘量不丟數據,Redis容許設置成每次修改數據都進行fsync,但這會下降性能。固然,即便執行了fsync也仍然有可能丟失數據(這取決於系統而不是Redis的實現)。因此,上面分析的因爲節點重啓引起的鎖失效問題,老是有可能出現的。爲了應對這一問題,antirez又提出了延遲重啓(delayed restarts)的概念。也就是說,一個節點崩潰後,先不當即重啓它,而是等待一段時間再重啓,這段時間應該大於鎖的有效時間(lock validity time)。這樣的話,這個節點在重啓前所參與的鎖都會過時,它在重啓後就不會對現有的鎖形成影響。

關於Redlock還有一點細節值得拿出來分析一下:在最後釋放鎖的時候,antirez在算法描述中特別強調,客戶端應該向全部Redis節點發起釋放鎖的操做。也就是說,即便當時向某個節點獲取鎖沒有成功,在釋放鎖的時候也不該該漏掉這個節點。這是爲何呢?設想這樣一種狀況,客戶端發給某個Redis節點的獲取鎖的請求成功到達了該Redis節點,這個節點也成功執行了SET操做,可是它返回給客戶端的響應包卻丟失了。這在客戶端看來,獲取鎖的請求因爲超時而失敗了,但在Redis這邊看來,加鎖已經成功了。所以,釋放鎖的時候,客戶端也應該對當時獲取鎖失敗的那些Redis節點一樣發起請求。實際上,這種狀況在異步通訊模型中是有可能發生的:客戶端向服務器通訊是正常的,但反方向倒是有問題的。

其它疑問

前面在討論單Redis節點的分佈式鎖的時候,最後咱們提出了一個疑問,若是客戶端長期阻塞致使鎖過時,那麼它接下來訪問共享資源就不安全了(沒有了鎖的保護)。這個問題在Redlock中是否有所改善呢?顯然,這樣的問題在Redlock中是依然存在的。

另外,在算法第4步成功獲取了鎖以後,若是因爲獲取鎖的過程消耗了較長時間,從新計算出來的剩餘的鎖有效時間很短了,那麼咱們還來得及去完成共享資源訪問嗎?若是咱們認爲過短,是否是應該當即進行鎖的釋放操做?那到底多短纔算呢?又是一個選擇難題。

Martin的分析

Martin Kleppmann在2016-02-08這一天發表了一篇blog,名字叫"How to do distributed locking
",地址以下:

Martin在這篇文章中談及了分佈式系統的不少基礎性的問題(特別是分佈式計算的異步模型),對分佈式系統的從業者來講很是值得一讀。這篇文章大致能夠分爲兩大部分:

  • 前半部分,與Redlock無關。Martin指出,即便咱們擁有一個完美實現的分佈式鎖(帶自動過時功能),在沒有共享資源參與進來提供某種fencing機制的前提下,咱們仍然不可能得到足夠的安全性。
  • 後半部分,是對Redlock自己的批評。Martin指出,因爲Redlock本質上是創建在一個同步模型之上,對系統的記時假設(timing assumption)有很強的要求,所以自己的安全性是不夠的。

首先咱們討論一下前半部分的關鍵點。Martin給出了下面這樣一份時序圖:

在上面的時序圖中,假設鎖服務自己是沒有問題的,它老是能保證任一時刻最多隻有一個客戶端得到鎖。上圖中出現的lease這個詞能夠暫且認爲就等同於一個帶有自動過時功能的鎖。客戶端1在得到鎖以後發生了很長時間的GC pause,在此期間,它得到的鎖過時了,而客戶端2得到了鎖。當客戶端1從GC pause中恢復過來的時候,它不知道本身持有的鎖已通過期了,它依然向共享資源(上圖中是一個存儲服務)發起了寫數據請求,而這時鎖實際上被客戶端2持有,所以兩個客戶端的寫請求就有可能衝突(鎖的互斥做用失效了)。

初看上去,有人可能會說,既然客戶端1從GC pause中恢復過來之後不知道本身持有的鎖已通過期了,那麼它能夠在訪問共享資源以前先判斷一下鎖是否過時。但仔細想一想,這絲毫也沒有幫助。由於GC pause可能發生在任意時刻,也許剛好在判斷完以後。

也有人會說,若是客戶端使用沒有GC的語言來實現,是否是就沒有這個問題呢?Martin指出,系統環境太複雜,仍然有不少緣由致使進程的pause,好比虛存形成的缺頁故障(page fault),再好比CPU資源的競爭。即便不考慮進程pause的狀況,網絡延遲也仍然會形成相似的結果。

總結起來就是說,即便鎖服務自己是沒有問題的,而僅僅是客戶端有長時間的pause或網絡延遲,仍然會形成兩個客戶端同時訪問共享資源的衝突狀況發生。而這種狀況其實就是咱們在前面已經提出來的「客戶端長期阻塞致使鎖過時」的那個疑問。

那怎麼解決這個問題呢?Martin給出了一種方法,稱爲fencing token。fencing token是一個單調遞增的數字,當客戶端成功獲取鎖的時候它隨同鎖一塊兒返回給客戶端。而客戶端訪問共享資源的時候帶着這個fencing token,這樣提供共享資源的服務就能根據它進行檢查,拒絕掉延遲到來的訪問請求(避免了衝突)。以下圖:

在上圖中,客戶端1先獲取到的鎖,所以有一個較小的fencing token,等於33,而客戶端2後獲取到的鎖,有一個較大的fencing token,等於34。客戶端1從GC pause中恢復過來以後,依然是向存儲服務發送訪問請求,可是帶了fencing token = 33。存儲服務發現它以前已經處理過34的請求,因此會拒絕掉此次33的請求。這樣就避免了衝突。

如今咱們再討論一下Martin的文章的後半部分。

Martin在文中構造了一些事件序列,可以讓Redlock失效(兩個客戶端同時持有鎖)。爲了說明Redlock對系統記時(timing)的過度依賴,他首先給出了下面的一個例子(仍是假設有5個Redis節點A, B, C, D, E):

  1. 客戶端1從Redis節點A, B, C成功獲取了鎖(多數節點)。因爲網絡問題,與D和E通訊失敗。
  2. 節點C上的時鐘發生了向前跳躍,致使它上面維護的鎖快速過時。
  3. 客戶端2從Redis節點C, D, E成功獲取了同一個資源的鎖(多數節點)。
  4. 客戶端1和客戶端2如今都認爲本身持有了鎖。

上面這種狀況之因此有可能發生,本質上是由於Redlock的安全性(safety property)對系統的時鐘有比較強的依賴,一旦系統的時鐘變得不許確,算法的安全性也就保證不了了。Martin在這裏實際上是要指出分佈式算法研究中的一些基礎性問題,或者說一些常識問題,即好的分佈式算法應該基於異步模型(asynchronous model),算法的安全性不該該依賴於任何記時假設(timing assumption)。在異步模型中:進程可能pause任意長的時間,消息可能在網絡中延遲任意長的時間,甚至丟失,系統時鐘也可能以任意方式出錯。一個好的分佈式算法,這些因素不該該影響它的安全性(safety property),只可能影響到它的活性(liveness property),也就是說,即便在很是極端的狀況下(好比系統時鐘嚴重錯誤),算法頂可能是不能在有限的時間內給出結果而已,而不該該給出錯誤的結果。這樣的算法在現實中是存在的,像比較著名的Paxos,或Raft。但顯然按這個標準的話,Redlock的安全性級別是達不到的。

隨後,Martin以爲前面這個時鐘跳躍的例子還不夠,又給出了一個由客戶端GC pause引起Redlock失效的例子。以下:

  1. 客戶端1向Redis節點A, B, C, D, E發起鎖請求。
  2. 各個Redis節點已經把請求結果返回給了客戶端1,但客戶端1在收到請求結果以前進入了長時間的GC pause。
  3. 在全部的Redis節點上,鎖過時了。
  4. 客戶端2在A, B, C, D, E上獲取到了鎖。
  5. 客戶端1從GC pause從恢復,收到了前面第2步來自各個Redis節點的請求結果。客戶端1認爲本身成功獲取到了鎖。
  6. 客戶端1和客戶端2如今都認爲本身持有了鎖。

Martin給出的這個例子其實有點小問題。在Redlock算法中,客戶端在完成向各個Redis節點的獲取鎖的請求以後,會計算這個過程消耗的時間,而後檢查是否是超過了鎖的有效時間(lock validity time)。也就是上面的例子中第5步,客戶端1從GC pause中恢復過來之後,它會經過這個檢查發現鎖已通過期了,不會再認爲本身成功獲取到鎖了。隨後antirez在他的反駁文章中就指出來了這個問題,但Martin認爲這個細節對Redlock總體的安全性沒有本質的影響。

拋開這個細節,咱們能夠分析一下Martin舉這個例子的意圖在哪。初看起來,這個例子跟文章前半部分分析通用的分佈式鎖時給出的GC pause的時序圖是基本同樣的,只不過那裏的GC pause發生在客戶端1得到了鎖以後,而這裏的GC pause發生在客戶端1得到鎖以前。但兩個例子的側重點不太同樣。Martin構造這裏的這個例子,是爲了強調在一個分佈式的異步環境下,長時間的GC pause或消息延遲(上面這個例子中,把GC pause換成Redis節點和客戶端1之間的消息延遲,邏輯不變),會讓客戶端得到一個已通過期的鎖。從客戶端1的角度看,Redlock的安全性被打破了,由於客戶端1收到鎖的時候,這個鎖已經失效了,而Redlock同時還把這個鎖分配給了客戶端2。換句話說,Redis服務器在把鎖分發給客戶端的途中,鎖就過時了,但又沒有有效的機制讓客戶端明確知道這個問題。而在以前的那個例子中,客戶端1收到鎖的時候鎖仍是有效的,鎖服務自己的安全性能夠認爲沒有被打破,後面雖然也出了問題,但問題是出在客戶端1和共享資源服務器之間的交互上。

在Martin的這篇文章中,還有一個頗有見地的觀點,就是對鎖的用途的區分。他把鎖的用途分爲兩種:

  • 爲了效率(efficiency),協調各個客戶端避免作重複的工做。即便鎖偶爾失效了,只是可能把某些操做多作一遍而已,不會產生其它的不良後果。好比重複發送了一封一樣的email。
  • 爲了正確性(correctness)。在任何狀況下都不容許鎖失效的狀況發生,由於一旦發生,就可能意味着數據不一致(inconsistency),數據丟失,文件損壞,或者其它嚴重的問題。

最後,Martin得出了以下的結論:

  • 若是是爲了效率(efficiency)而使用分佈式鎖,容許鎖的偶爾失效,那麼使用單Redis節點的鎖方案就足夠了,簡單並且效率高。Redlock則是個太重的實現(heavyweight)。
  • 若是是爲了正確性(correctness)在很嚴肅的場合使用分佈式鎖,那麼不要使用Redlock。它不是創建在異步模型上的一個足夠強的算法,它對於系統模型的假設中包含不少危險的成分(對於timing)。並且,它沒有一個機制可以提供fencing token。那應該使用什麼技術呢?Martin認爲,應該考慮相似Zookeeper的方案,或者支持事務的數據庫。

Martin對Redlock算法的形容是:

neither fish nor fowl (非驢非馬)

其它疑問

  • Martin提出的fencing token的方案,須要對提供共享資源的服務進行修改,這在現實中可行嗎?
  • 根據Martin的說法,看起來,若是資源服務器實現了fencing token,它在分佈式鎖失效的狀況下也仍然能保持資源的互斥訪問。這是否是意味着分佈式鎖根本沒有存在的意義了?
  • 資源服務器須要檢查fencing token的大小,若是提供資源訪問的服務也是包含多個節點的(分佈式的),那麼這裏怎麼檢查才能保證fencing token在多個節點上是遞增的呢?
  • Martin對於fencing token的舉例中,兩個fencing token到達資源服務器的順序顛倒了(小的fencing token後到了),這時資源服務器檢查出了這一問題。若是客戶端1和客戶端2都發生了GC pause,兩個fencing token都延遲了,它們幾乎同時到達了資源服務器,但保持了順序,那麼資源服務器是否是就檢查不出問題了?這時對於資源的訪問是否是就發生衝突了?
  • 分佈式鎖+fencing的方案是絕對正確的嗎?能證實嗎?

(以上是上部)


自從我寫完這個話題的上半部分以後,就感受頭腦中出現了許多細小的聲音,久久揮之不去。它們就像是在爲了一些雞毛蒜皮的小事而相互爭吵個不停。的確,有關分佈式的話題就是這樣,瑣碎異常,並且每一個人說的話聽起來彷佛都有道理。

今天,咱們就繼續探討這個話題的後半部分。本文中,咱們將從antirez反駁Martin Kleppmann的觀點開始講起,而後會涉及到Hacker News上出現的一些討論內容,接下來咱們還會討論到基於Zookeeper和Chubby的分佈式鎖是怎樣的,並和Redlock進行一些對比。最後,咱們會提到Martin對於這一事件的總結。

antirez的反駁

Martin在發表了那篇分析分佈式鎖的blog (How to do distributed locking)以後,該文章在Twitter和Hacker News上引起了普遍的討論。但人們更想聽到的是Redlock的做者antirez對此會發表什麼樣的見解。

Martin的那篇文章是在2016-02-08這一天發表的,但據Martin說,他在公開發表文章的一星期以前就把草稿發給了antirez進行review,並且他們之間經過email進行了討論。不知道Martin有沒有意料到,antirez對於此事的反應很快,就在Martin的文章發表出來的次日,antirez就在他的博客上貼出了他對於此事的反駁文章,名字叫"Is Redlock safe?",地址以下:

這是高手之間的過招。antirez這篇文章也條例很是清晰,而且中間涉及到大量的細節。antirez認爲,Martin的文章對於Redlock的批評能夠歸納爲兩個方面(與Martin文章的先後兩部分對應):

  • 帶有自動過時功能的分佈式鎖,必須提供某種fencing機制來保證對共享資源的真正的互斥保護。Redlock提供不了這樣一種機制。
  • Redlock構建在一個不夠安全的系統模型之上。它對於系統的記時假設(timing assumption)有比較強的要求,而這些要求在現實的系統中是沒法保證的。

antirez對這兩方面分別進行了反駁。

首先,關於fencing機制。antirez對於Martin的這種論證方式提出了質疑:既然在鎖失效的狀況下已經存在一種fencing機制能繼續保持資源的互斥訪問了,那爲何還要使用一個分佈式鎖而且還要求它提供那麼強的安全性保證呢?即便退一步講,Redlock雖然提供不了Martin所講的遞增的fencing token,但利用Redlock產生的隨機字符串(my_random_value)能夠達到一樣的效果。這個隨機字符串雖然不是遞增的,但倒是惟一的,能夠稱之爲unique token。antirez舉了個例子,好比,你能夠用它來實現「Check and Set」操做,原話是:

When starting to work with a shared resource, we set its state to 「<token>」, then we operate the read-modify-write only if the token is still the same when we write.
(譯文:當開始和共享資源交互的時候,咱們將它的狀態設置成「<token>」,而後僅在token沒改變的狀況下咱們才執行「讀取-修改-寫回」操做。)

第一遍看到這個描述的時候,我我的是感受沒太看懂的。「Check and Set」應該就是咱們日常聽到過的CAS操做了,但它如何在這個場景下工做,antirez並無展開說(在後面講到Hacker News上的討論的時候,咱們還會提到)。

而後,antirez的反駁就集中在第二個方面上:關於算法在記時(timing)方面的模型假設。在咱們前面分析Martin的文章時也提到過,Martin認爲Redlock會失效的狀況主要有三種:

  • 時鐘發生跳躍。
  • 長時間的GC pause。
  • 長時間的網絡延遲。

antirez確定意識到了這三種狀況對Redlock最致命的實際上是第一點:時鐘發生跳躍。這種狀況一旦發生,Redlock是無法正常工做的。而對於後兩種狀況來講,Redlock在當初設計的時候已經考慮到了,對它們引發的後果有必定的免疫力。因此,antirez接下來集中精力來講明經過恰當的運維,徹底能夠避免時鐘發生大的跳動,而Redlock對於時鐘的要求在現實系統中是徹底能夠知足的。

Martin在提到時鐘跳躍的時候,舉了兩個可能形成時鐘跳躍的具體例子:

  • 系統管理員手動修改了時鐘。
  • 從NTP服務收到了一個大的時鐘更新事件。

antirez反駁說:

  • 手動修改時鐘這種人爲緣由,不要那麼作就是了。不然的話,若是有人手動修改Raft協議的持久化日誌,那麼就算是Raft協議它也無法正常工做了。
  • 使用一個不會進行「跳躍」式調整系統時鐘的ntpd程序(多是經過恰當的配置),對於時鐘的修改經過屢次微小的調整來完成。

而Redlock對時鐘的要求,並不須要徹底精確,它只須要時鐘差很少精確就能夠了。好比,要記時5秒,但可能實際記了4.5秒,而後又記了5.5秒,有必定的偏差。不過只要偏差不超過必定範圍,這對Redlock不會產生影響。antirez認爲呢,像這樣對時鐘精度並非很高的要求,在實際環境中是徹底合理的。

好了,到此爲止,若是你相信antirez這裏關於時鐘的論斷,那麼接下來antirez的分析就基本上瓜熟蒂落了。

關於Martin提到的能使Redlock失效的後兩種狀況,Martin在分析的時候剛好犯了一個錯誤(在本文上半部分已經提到過)。在Martin給出的那個由客戶端GC pause引起Redlock失效的例子中,這個GC pause引起的後果至關於在鎖服務器和客戶端之間發生了長時間的消息延遲。Redlock對於這個狀況是能處理的。回想一下Redlock算法的具體過程,它使用起來的過程大致能夠分紅5步:

  1. 獲取當前時間。
  2. 完成獲取鎖的整個過程(與N個Redis節點交互)。
  3. 再次獲取當前時間。
  4. 把兩個時間相減,計算獲取鎖的過程是否消耗了太長時間,致使鎖已通過期了。若是沒過時,
  5. 客戶端持有鎖去訪問共享資源。

在Martin舉的例子中,GC pause或網絡延遲,實際發生在上述第1步和第3步之間。而無論在第1步和第3步之間因爲什麼緣由(進程停頓或網絡延遲等)致使了大的延遲出現,在第4步都能被檢查出來,不會讓客戶端拿到一個它認爲有效而實際卻已通過期的鎖。固然,這個檢查依賴系統時鐘沒有大的跳躍。這也就是爲何antirez在前面要對時鐘條件進行辯護的緣由。

有人會說,在第3步以後,仍然可能會發生延遲啊。沒錯,antirez認可這一點,他對此有一段頗有意思的論證,原話以下:

The delay can only happen after steps 3, resulting into the lock to be considered ok while actually expired, that is, we are back at the first problem Martin identified of distributed locks where the client fails to stop working to the shared resource before the lock validity expires. Let me tell again how this problem is common with all the distributed locks implementations, and how the token as a solution is both unrealistic and can be used with Redlock as well.
(譯文:延遲只能發生在第3步以後,這致使鎖被認爲是有效的而實際上已通過期了,也就是說,咱們回到了Martin指出的第一個問題上,客戶端沒可以在鎖的有效性過時以前完成與共享資源的交互。讓我再次申明一下,這個問題對於全部的分佈式鎖的實現是廣泛存在的,並且基於token的這種解決方案是不切實際的,但也能和Redlock一塊兒用。)

這裏antirez所說的「Martin指出的第一個問題」具體是什麼呢?在本文上半部分咱們提到過,Martin的文章分爲兩大部分,其中前半部分與Redlock沒有直接關係,而是指出了任何一種帶自動過時功能的分佈式鎖在沒有提供fencing機制的前提下都有可能失效。這裏antirez所說的就是指的Martin的文章的前半部分。換句話說,對於大延遲給Redlock帶來的影響,剛好與Martin在文章的前半部分針對全部的分佈式鎖所作的分析是一致的,而這種影響不僅僅針對Redlock。Redlock的實現已經保證了它是和其它任何分佈式鎖的安全性是同樣的。固然,與其它「更完美」的分佈式鎖相比,Redlock彷佛提供不了Martin提出的那種遞增的token,但antirez在前面已經分析過了,關於token的這種論證方式自己就是「不切實際」的,或者退一步講,Redlock能提供的unique token也可以提供徹底同樣的效果。

另外,關於大延遲對Redlock的影響,antirez和Martin在Twitter上有下面的對話:

antirez:
@martinkl so I wonder if after my reply, we can at least agree about unbound messages delay to don’t cause any harm.

Martin:
@antirez Agree about message delay between app and lock server. Delay between app and resource being accessed is still problematic.

(譯文:
antirez問:我想知道,在我發文回覆以後,咱們可否在一點上達成一致,就是大的消息延遲不會給Redlock的運行形成損害。
Martin答:對於客戶端和鎖服務器之間的消息延遲,我贊成你的觀點。但客戶端和被訪問資源之間的延遲仍是有問題的。)

經過這段對話能夠看出,對於Redlock在第4步所作的鎖有效性的檢查,Martin是予以確定的。但他認爲客戶端和資源服務器之間的延遲仍是會帶來問題的。Martin在這裏說的有點模糊。就像antirez前面分析的,客戶端和資源服務器之間的延遲,對全部的分佈式鎖的實現都會帶來影響,這不僅僅是Redlock的問題了。

以上就是antirez在blog中所說的主要內容。有一些點值得咱們注意一下:

  • antirez是贊成大的系統時鐘跳躍會形成Redlock失效的。在這一點上,他與Martin的觀點的不一樣在於,他認爲在實際系統中是能夠避免大的時鐘跳躍的。固然,這取決於基礎設施和運維方式。
  • antirez在設計Redlock的時候,是充分考慮了網絡延遲和程序停頓所帶來的影響的。可是,對於客戶端和資源服務器之間的延遲(即發生在算法第3步以後的延遲),antirez是認可全部的分佈式鎖的實現,包括Redlock,是沒有什麼好辦法來應對的。

討論進行到這,Martin和antirez之間誰對誰錯其實並非那麼重要了。只要咱們可以對Redlock(或者其它分佈式鎖)所能提供的安全性的程度有充分的瞭解,那麼咱們就能作出本身的選擇了。

Hacker News上的一些討論

針對Martin和antirez的兩篇blog,不少技術人員在Hacker News上展開了激烈的討論。這些討論所在地址以下:

在Hacker News上,antirez積極參與了討論,而Martin則始終置身事外。

下面我把這些討論中一些有意思的點拿出來與你們一塊兒分享一下(集中在對於fencing token機制的討論上)。

關於antirez提出的「Check and Set」操做,他在blog裏並無詳加說明。果真,在Hacker News上就有人出來問了。antirez給出的答覆以下:

You want to modify locked resource X. You set X.currlock = token. Then you read, do whatever you want, and when you write, you "write-if-currlock == token". If another client did X.currlock = somethingelse, the transaction fails.

翻譯一下能夠這樣理解:假設你要修改資源X,那麼遵循下面的僞碼所定義的步驟。

  1. 先設置X.currlock = token。
  2. 讀出資源X(包括它的值和附帶的X.currlock)。
  3. 按照"write-if-currlock == token"的邏輯,修改資源X的值。意思是說,若是對X進行修改的時候,X.currlock仍然和當初設置進去的token相等,那麼才進行修改;若是這時X.currlock已是其它值了,那麼說明有另一方也在試圖進行修改操做,那麼放棄當前的修改,從而避免衝突。

隨後Hacker News上一位叫viraptor的用戶提出了異議,它給出了這樣一個執行序列:

  • A: X.currlock = Token_ID_A
  • A: resource read
  • A: is X.currlock still Token_ID_A? yes
  • B: X.currlock = Token_ID_B
  • B: resource read
  • B: is X.currlock still Token_ID_B? yes
  • B: resource write
  • A: resource write

到了最後兩步,兩個客戶端A和B同時進行寫操做,衝突了。不過,這位用戶應該是理解錯了antirez給出的修改過程了。按照antirez的意思,判斷X.currlock是否修改過和對資源的寫操做,應該是一個原子操做。只有這樣理解才能合乎邏輯,不然的話,這個過程就有嚴重的破綻。這也是爲何antirez以前會對fencing機制產生質疑:既然資源服務器自己都能提供互斥的原子操做了,爲何還須要一個分佈式鎖呢?所以,antirez認爲這種fencing機制是很累贅的,他之因此仍是提出了這種「Check and Set」操做,只是爲了證實在提供fencing token這一點上,Redlock也能作到。可是,這裏仍然有一些不明確的地方,若是將"write-if-currlock == token"看作是原子操做的話,這個邏輯勢必要在資源服務器上執行,那麼第二步爲何還要「讀出資源X」呢?除非這個「讀出資源X」的操做也是在資源服務器上執行,它包含在「判斷-寫回」這個原子操做裏面。而假如不這樣理解的話,「讀取-判斷-寫回」這三個操做都放在客戶端執行,那麼看不出它們如何才能實現原子性操做。在下面的討論中,咱們暫時忽略「讀出資源X」這一步。

這個基於random token的「Check and Set」操做,若是與Martin提出的遞增的fencing token對比一下的話,至少有兩點不一樣:

  • 「Check and Set」對於寫操做要分紅兩步來完成(設置token、判斷-寫回),而遞增的fencing token機制只須要一步(帶着token向資源服務器發起寫請求)。
  • 遞增的fencing token機制能保證最終操做共享資源的順序,那些延遲時間太長的操做就沒法操做共享資源了。可是基於random token的「Check and Set」操做不會保證這個順序,那些延遲時間太長的操做若是後到達了,它仍然有可能操做共享資源(固然是以互斥的方式)。

對於前一點不一樣,咱們在後面的分析中會看到,若是資源服務器也是分佈式的,那麼使用遞增的fencing token也要變成兩步。

而對於後一點操做順序上的不一樣,antirez認爲這個順序沒有意義,關鍵是能互斥訪問就好了。他寫下了下面的話:

So the goal is, when race conditions happen, to avoid them in some way.
......
Note also that when it happens that, because of delays, the clients are accessing concurrently, the lock ID has little to do with the order in which the operations were indented to happen.
(譯文: 咱們的目標是,當競爭條件出現的時候,可以以某種方式避免。
......
還須要注意的是,當那種競爭條件出現的時候,好比因爲延遲,客戶端是同時來訪問的,鎖的ID的大小順序跟那些操做真正想執行的順序,是沒有什麼關係的。)

這裏的lock ID,跟Martin說的遞增的token是一回事。

隨後,antirez舉了一個「將名字加入列表」的操做的例子:

  • T0: Client A receives new name to add from web.
  • T0: Client B is idle
  • T1: Client A is experiencing pauses.
  • T1: Client B receives new name to add from web.
  • T2: Client A is experiencing pauses.
  • T2: Client B receives a lock with ID 1
  • T3: Client A receives a lock with ID 2

你看,兩個客戶端(實際上是Web服務器)執行「添加名字」的操做,A原本是排在B前面的,但得到鎖的順序倒是B排在A前面。所以,antirez說,鎖的ID的大小順序跟那些操做真正想執行的順序,是沒有什麼關係的。關鍵是能排出一個順序來,能互斥訪問就好了。那麼,至於鎖的ID是遞增的,仍是一個random token,天然就不那麼重要了。

Martin提出的fencing token機制,給人留下了無盡的疑惑。這主要是由於他對於這一機制的描述缺乏太多的技術細節。從上面的討論能夠看出,antirez對於這一機制的見解是,它跟一個random token沒有什麼區別,並且,它須要資源服務器自己提供某種互斥機制,這幾乎讓分佈式鎖自己的存在失去了意義。圍繞fencing token的問題,還有兩點是比較引人注目的,Hacker News上也有人提出了相關的疑問:

  • (1)關於資源服務器自己的架構細節。
  • (2)資源服務器對於fencing token進行檢查的實現細節,好比是否須要提供一種原子操做。

關於上述問題(1),Hacker News上有一位叫dwenzek的用戶發表了下面的評論:

...... the issue around the usage of fencing tokens to reject any late usage of a lock is unclear just because the protected resource and its access are themselves unspecified. Is the resource distributed or not? If distributed, does the resource has a mean to ensure that tokens are increasing over all the nodes? Does the resource have a mean to rollback any effects done by a client which session is interrupted by a timeout?

(譯文:...... 關於使用fencing token拒絕掉延遲請求的相關議題,是不夠清晰的,由於受保護的資源以及對它的訪問方式自己是沒有被明肯定義過的。資源服務是否是分佈式的呢?若是是,資源服務有沒有一種方式能確保token在全部節點上遞增呢?對於客戶端的Session因爲過時而被中斷的狀況,資源服務有辦法將它的影響回滾嗎?)

這些疑問在Hacker News上並無人給出解答。而關於分佈式的資源服務器架構如何處理fencing token,另一名分佈式系統的專家Flavio Junqueira在他的一篇blog中有所說起(咱們後面會再提到)。

關於上述問題(2),Hacker News上有一位叫reza_n的用戶發表了下面的疑問:

I understand how a fencing token can prevent out of order writes when 2 clients get the same lock. But what happens when those writes happen to arrive in order and you are doing a value modification? Don't you still need to rely on some kind of value versioning or optimistic locking? Wouldn't this make the use of a distributed lock unnecessary?

(譯文: 我理解當兩個客戶端同時得到鎖的時候fencing token是如何防止亂序的。可是若是兩個寫操做剛好按序到達了,並且它們在對同一個值進行修改,那會發生什麼呢?難道不會仍然是依賴某種數據版本號或者樂觀鎖的機制?這不會讓分佈式鎖變得沒有必要了嗎?)

一位叫Terr_的Hacker News用戶答:

I believe the "first" write fails, because the token being passed in is no longer "the lastest", which indicates their lock was already released or expired.

(譯文: 我認爲「第一個」寫請求會失敗,由於它傳入的token再也不是「最新的」了,這意味着鎖已經釋放或者過時了。)

Terr_的回答到底對不對呢?這很差說,取決於資源服務器對於fencing token進行檢查的實現細節。讓咱們來簡單分析一下。

爲了簡單起見,咱們假設有一臺(先不考慮分佈式的狀況)經過RPC進行遠程訪問文件服務器,它沒法提供對於文件的互斥訪問(不然咱們就不須要分佈式鎖了)。如今咱們按照Martin給出的說法,加入fencing token的檢查邏輯。因爲Martin沒有描述具體細節,咱們猜想至少有兩種可能。

第一種可能,咱們修改了文件服務器的代碼,讓它能多接受一個fencing token的參數,並在進行全部處理以前加入了一個簡單的判斷邏輯,保證只有當前接收到的fencing token大於以前的值才容許進行後邊的訪問。而一旦經過了這個判斷,後面的處理不變。

如今想象reza_n描述的場景,客戶端1和客戶端2都發生了GC pause,兩個fencing token都延遲了,它們幾乎同時到達了文件服務器,並且保持了順序。那麼,咱們新加入的判斷邏輯,應該對兩個請求都會放過,而放過以後它們幾乎同時在操做文件,仍是衝突了。既然Martin宣稱fencing token能保證分佈式鎖的正確性,那麼上面這種可能的猜想也許是咱們理解錯了。

固然,還有第二種可能,就是咱們對文件服務器確實作了比較大的改動,讓這裏判斷token的邏輯和隨後對文件的處理放在一個原子操做裏了。這可能更接近antirez的理解。這樣的話,前面reza_n描述的場景中,兩個寫操做都應該成功。

基於ZooKeeper的分佈式鎖更安全嗎?

不少人(也包括Martin在內)都認爲,若是你想構建一個更安全的分佈式鎖,那麼應該使用ZooKeeper,而不是Redis。那麼,爲了對比的目的,讓咱們先暫時脫離開本文的題目,討論一下基於ZooKeeper的分佈式鎖能提供絕對的安全嗎?它須要fencing token機制的保護嗎?

咱們不得不提一下分佈式專家Flavio Junqueira所寫的一篇blog,題目叫「Note on fencing and distributed locks」,地址以下:

Flavio Junqueira是ZooKeeper的做者之一,他的這篇blog就寫在Martin和antirez發生爭論的那幾天。他在文中給出了一個基於ZooKeeper構建分佈式鎖的描述(固然這不是惟一的方式):

  • 客戶端嘗試建立一個znode節點,好比/lock。那麼第一個客戶端就建立成功了,至關於拿到了鎖;而其它的客戶端會建立失敗(znode已存在),獲取鎖失敗。
  • 持有鎖的客戶端訪問共享資源完成後,將znode刪掉,這樣其它客戶端接下來就能來獲取鎖了。
  • znode應該被建立成ephemeral的。這是znode的一個特性,它保證若是建立znode的那個客戶端崩潰了,那麼相應的znode會被自動刪除。這保證了鎖必定會被釋放。

看起來這個鎖至關完美,沒有Redlock過時時間的問題,並且能在須要的時候讓鎖自動釋放。但仔細考察的話,並不盡然。

ZooKeeper是怎麼檢測出某個客戶端已經崩潰了呢?實際上,每一個客戶端都與ZooKeeper的某臺服務器維護着一個Session,這個Session依賴按期的心跳(heartbeat)來維持。若是ZooKeeper長時間收不到客戶端的心跳(這個時間稱爲Sesion的過時時間),那麼它就認爲Session過時了,經過這個Session所建立的全部的ephemeral類型的znode節點都會被自動刪除。

設想以下的執行序列:

  1. 客戶端1建立了znode節點/lock,得到了鎖。
  2. 客戶端1進入了長時間的GC pause。
  3. 客戶端1鏈接到ZooKeeper的Session過時了。znode節點/lock被自動刪除。
  4. 客戶端2建立了znode節點/lock,從而得到了鎖。
  5. 客戶端1從GC pause中恢復過來,它仍然認爲本身持有鎖。

最後,客戶端1和客戶端2都認爲本身持有了鎖,衝突了。這與以前Martin在文章中描述的因爲GC pause致使的分佈式鎖失效的狀況相似。

看起來,用ZooKeeper實現的分佈式鎖也不必定就是安全的。該有的問題它仍是有。可是,ZooKeeper做爲一個專門爲分佈式應用提供方案的框架,它提供了一些很是好的特性,是Redis之類的方案所沒有的。像前面提到的ephemeral類型的znode自動刪除的功能就是一個例子。

還有一個頗有用的特性是ZooKeeper的watch機制。這個機制能夠這樣來使用,好比當客戶端試圖建立/lock的時候,發現它已經存在了,這時候建立失敗,但客戶端不必定就此對外宣告獲取鎖失敗。客戶端能夠進入一種等待狀態,等待當/lock節點被刪除的時候,ZooKeeper經過watch機制通知它,這樣它就能夠繼續完成建立操做(獲取鎖)。這可讓分佈式鎖在客戶端用起來就像一個本地的鎖同樣:加鎖失敗就阻塞住,直到獲取到鎖爲止。這樣的特性Redlock就沒法實現。

小結一下,基於ZooKeeper的鎖和基於Redis的鎖相比在實現特性上有兩個不一樣:

  • 在正常狀況下,客戶端能夠持有鎖任意長的時間,這能夠確保它作完全部須要的資源訪問操做以後再釋放鎖。這避免了基於Redis的鎖對於有效時間(lock validity time)到底設置多長的兩難問題。實際上,基於ZooKeeper的鎖是依靠Session(心跳)來維持鎖的持有狀態的,而Redis不支持Sesion。
  • 基於ZooKeeper的鎖支持在獲取鎖失敗以後等待鎖從新釋放的事件。這讓客戶端對鎖的使用更加靈活。

順便提一下,如上所述的基於ZooKeeper的分佈式鎖的實現,並非最優的。它會引起「herd effect」(羊羣效應),下降獲取鎖的性能。一個更好的實現參見下面連接:

咱們從新回到Flavio Junqueira對於fencing token的分析。Flavio Junqueira指出,fencing token機制本質上是要求客戶端在每次訪問一個共享資源的時候,在執行任何操做以前,先對資源進行某種形式的「標記」(mark)操做,這個「標記」能保證持有舊的鎖的客戶端請求(若是延遲到達了)沒法操做資源。這種標記操做能夠是不少形式,fencing token是其中比較典型的一個。

隨後Flavio Junqueira提到用遞增的epoch number(至關於Martin的fencing token)來保護共享資源。而對於分佈式的資源,爲了方便討論,假設分佈式資源是一個小型的多備份的數據存儲(a small replicated data store),執行寫操做的時候須要向全部節點上寫數據。最簡單的作標記的方式,就是在對資源進行任何操做以前,先把epoch number標記到各個資源節點上去。這樣,各個節點就保證了舊的(也就是小的)epoch number沒法操做數據。

固然,這裏再展開討論下去可能就涉及到了這個數據存儲服務的實現細節了。好比在實際系統中,可能爲了容錯,只要上面講的標記和寫入操做在多數節點上完成就算成功完成了(Flavio Junqueira並無展開去講)。在這裏咱們能看到的,最重要的,是這種標記操做如何起做用的方式。這有點相似於Paxos協議(Paxos協議要求每一個proposal對應一個遞增的數字,執行accept請求以前先執行prepare請求)。antirez提出的random token的方式顯然不符合Flavio Junqueira對於「標記」操做的定義,由於它沒法區分新的token和舊的token。只有遞增的數字才能確保最終收斂到最新的操做結果上。

在這個分佈式數據存儲服務(共享資源)的例子中,客戶端在標記完成以後執行寫入操做的時候,存儲服務的節點須要判斷epoch number是否是最新,而後肯定能不能執行寫入操做。若是按照上一節咱們的分析思路,這裏的epoch判斷和接下來的寫入操做,是否是在一個原子操做裏呢?根據Flavio Junqueira的相關描述,咱們相信,應該是原子的。那麼既然資源自己能夠提供原子互斥操做了,那麼分佈式鎖還有存在的意義嗎?應該說有。客戶端能夠利用分佈式鎖有效地避免衝突,等待寫入機會,這對於包含多個節點的分佈式資源尤爲有用(固然,是出於效率的緣由)。

Chubby的分佈式鎖是怎樣作fencing的?

提到分佈式鎖,就不能不提Google的Chubby。

Chubby是Google內部使用的分佈式鎖服務,有點相似於ZooKeeper,但也存在不少差別。Chubby對外公開的資料,主要是一篇論文,叫作「The Chubby lock service for loosely-coupled distributed systems」,下載地址以下:

另外,YouTube上有一個的講Chubby的talk,也很不錯,播放地址:

Chubby天然也考慮到了延遲形成的鎖失效的問題。論文裏有一段描述以下:

a process holding a lock L may issue a request R, but then fail. Another process may ac- quire L and perform some action before R arrives at its destination. If R later arrives, it may be acted on without the protection of L, and potentially on inconsistent data.

(譯文: 一個進程持有鎖L,發起了請求R,可是請求失敗了。另外一個進程得到了鎖L並在請求R到達目的方以前執行了一些動做。若是後來請求R到達了,它就有可能在沒有鎖L保護的狀況下進行操做,帶來數據不一致的潛在風險。)

這跟Martin的分析大同小異。

Chubby給出的用於解決(緩解)這一問題的機制稱爲sequencer,相似於fencing token機制。鎖的持有者能夠隨時請求一個sequencer,這是一個字節串,它由三部分組成:

  • 鎖的名字。
  • 鎖的獲取模式(排他鎖仍是共享鎖)。
  • lock generation number(一個64bit的單調遞增數字)。做用至關於fencing token或epoch number。

客戶端拿到sequencer以後,在操做資源的時候把它傳給資源服務器。而後,資源服務器負責對sequencer的有效性進行檢查。檢查能夠有兩種方式:

  • 調用Chubby提供的API,CheckSequencer(),將整個sequencer傳進去進行檢查。這個檢查是爲了保證客戶端持有的鎖在進行資源訪問的時候仍然有效。
  • 將客戶端傳來的sequencer與資源服務器當前觀察到的最新的sequencer進行對比檢查。能夠理解爲與Martin描述的對於fencing token的檢查相似。

固然,若是因爲兼容的緣由,資源服務自己不容易修改,那麼Chubby還提供了一種機制:

  • lock-delay。Chubby容許客戶端爲持有的鎖指定一個lock-delay的時間值(默認是1分鐘)。當Chubby發現客戶端被動失去聯繫的時候,並不會當即釋放鎖,而是會在lock-delay指定的時間內阻止其它客戶端得到這個鎖。這是爲了在把鎖分配給新的客戶端以前,讓以前持有鎖的客戶端有充分的時間把請求隊列排空(draining the queue),儘可能防止出現延遲到達的未處理請求。

可見,爲了應對鎖失效問題,Chubby提供的三種處理方式:CheckSequencer()檢查、與上次最新的sequencer對比、lock-delay,它們對於安全性的保證是從強到弱的。並且,這些處理方式自己都沒有保證提供絕對的正確性(correctness)。可是,Chubby確實提供了單調遞增的lock generation number,這就容許資源服務器在須要的時候,利用它提供更強的安全性保障。

關於時鐘

在Martin與antirez的這場爭論中,衝突最爲嚴重的就是對於系統時鐘的假設是否是合理的問題。Martin認爲系統時鐘不免會發生跳躍(這與分佈式算法的異步模型相符),而antirez認爲在實際中系統時鐘能夠保證不發生大的跳躍。

Martin對於這一分歧發表了以下見解(原話):

So, fundamentally, this discussion boils down to whether it is reasonable to make timing assumptions for ensuring safety properties. I say no, Salvatore says yes — but that's ok. Engineering discussions rarely have one right answer.

(譯文:
從根本上來講,這場討論最後歸結到了一個問題上:爲了確保安全性而作出的記時假設究竟是否合理。我認爲不合理,而antirez認爲合理 —— 可是這也不要緊。工程問題的討論不多隻有一個正確答案。)

那麼,在實際系統中,時鐘究竟是否可信呢?對此,Julia Evans專門寫了一篇文章,「TIL: clock skew exists」,總結了不少跟時鐘偏移有關的實際資料,並進行了分析。這篇文章地址:

Julia Evans在文章最後得出的結論是:

clock skew is real
(時鐘偏移在現實中是存在的)

Martin的過後總結

咱們前面提到過,當各方的爭論在激烈進行的時候,Martin幾乎始終置身事外。可是Martin在這件事過去以後,把這個事件的先後通過總結成了一個很長的故事線。若是你想最全面地瞭解這個事件發生的先後通過,那麼建議去讀讀Martin的這個總結:

在這個故事總結的最後,Martin寫下了不少感性的評論:

For me, this is the most important point: I don't care who is right or wrong in this debate — I care about learning from others' work, so that we can avoid repeating old mistakes, and make things better in future. So much great work has already been done for us: by standing on the shoulders of giants, we can build better software.
......
By all means, test ideas by arguing them and checking whether they stand up to scrutiny by others. That's part of the learning process. But the goal should be to learn, not to convince others that you are right. Sometimes that just means to stop and think for a while.

(譯文:
對我來講最重要的一點在於:我並不在意在這場辯論中誰對誰錯 —— 我只關心從其餘人的工做中學到的東西,以便咱們可以避免重蹈覆轍,並讓將來更加美好。前人已經爲咱們創造出了許多偉大的成果:站在巨人的肩膀上,咱們得以構建更棒的軟件。
......
對於任何想法,務必要詳加檢驗,經過論證以及檢查它們是否經得住別人的詳細審查。那是學習過程的一部分。但目標應該是爲了得到知識,而不該該是爲了說服別人相信你本身是對的。有時候,那隻不過意味着停下來,好好地想想。)


關於分佈式鎖的這場爭論,咱們已經完整地作了回顧和分析。

按照鎖的兩種用途,若是僅是爲了效率(efficiency),那麼你能夠本身選擇你喜歡的一種分佈式鎖的實現。固然,你須要清楚地知道它在安全性上有哪些不足,以及它會帶來什麼後果。而若是你是爲了正確性(correctness),那麼請慎之又慎。在本文的討論中,咱們在分佈式鎖的正確性上走得最遠的地方,要數對於ZooKeeper分佈式鎖、單調遞增的epoch number以及對分佈式資源進行標記的分析了。請仔細審查相關的論證。

Martin爲咱們留下了很多疑問,尤爲是他提出的fencing token機制。他在blog中提到,會在他的新書《Designing Data-Intensive Applications》的第8章和第9章再詳加論述。目前,這本書尚在預售當中。我感受,這會是一本值得一讀的書,它不一樣於爲了出名或賺錢而出版的那種短平快的書籍。能夠看出做者在這本書上投入了巨大的精力。

最後,我相信,這個討論還遠沒有結束。分佈式鎖(Distributed Locks)和相應的fencing方案,能夠做爲一個長期的課題,隨着咱們對分佈式系統的認識逐漸增長,能夠再來慢慢地思考它。思考它更深層的本質,以及它在理論上的證實。

(完)

感謝

由衷地感謝幾位朋友花了寶貴的時間對本文草稿所作的review:CacheCloud的做者付磊,快手的李偉博,阿里的李波。固然,文中若是還有錯漏,由我本人負責^-^。

其它精選文章

相關文章
相關標籤/搜索