閱讀637 返回首頁    go 阿裏雲 go 技術社區[雲棲]


基於Redis的分布式鎖真的安全嗎?(下)

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

 

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

 

還沒有看過上半部分的同學,請先閱讀:《基於Redis的分布式鎖到底安全嗎?(上)》

 

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?",地址如下:https://antirez.com/news/101

 

這是高手之間的過招。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”,地址如下:

https://fpj.me/2016/02/10/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”(羊群效應),降低獲取鎖的性能。一個更好的實現參見下麵鏈接:

https://zookeeper.apache.org/doc/r3.4.9/recipes.html

 

我們重新回到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”,下載地址如下:

https://research.google.com/archive/chubby.html

 

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

https://www.youtube.com/watch?v=PqItueBaiRg&feature=youtu.be&t=487

 

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”,總結了很多跟時鍾偏移有關的實際資料,並進行了分析。這篇文章地址:

https://jvns.ca/blog/2016/02/09/til-clock-skew-exists/

 

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

clock skew is real

(譯文:時鍾偏移在現實中是存在的)

     

 

Martin的事後總結

 

我們前麵提到過,當各方的爭論在激烈進行的時候,Martin幾乎始終置身事外。但是Martin在這件事過去之後,把這個事件的前後經過總結成了一個很長的故事線。如果你想最全麵地了解這個事件發生的前後經過,那麼建議去讀讀Martin的這個總結:

https://storify.com/martinkl/redlock-discussion

 

在這個故事總結的最後,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方案,可以作為一個長期的課題,隨著我們對分布式係統的認識逐漸增加,可以再來慢慢地思考它。思考它更深層的本質,以及它在理論上的證明。

原文發布時間為:2017-03-17

本文來自雲棲社區合作夥伴DBAplus

最後更新:2017-05-16 10:32:41

  上一篇:go  深入淺出XTTS:Oracle數據庫遷移升級利器(附PPT)
  下一篇:go  借鑒人類疾病防疫機製,阿裏雲如何幫助用戶應對大規模安全疫情?