今天直焙,我們就繼續(xù)探討這個話題的后半部分谢鹊。本文中镜硕,我們將從antirez反駁Martin Kleppmann的觀點(diǎn)開始講起闻妓,然后會涉及到Hacker News上出現(xiàn)的一些討論內(nèi)容睦优,接下來我們還會討論到基于Zookeeper和Chubby的分布式鎖是怎樣的渗常,并和Redlock進(jìn)行一些對比。最后汗盘,我們會提到Martin對于這一事件的總結(jié)皱碘。
還沒有看過上半部分的同學(xué),請先閱讀:
antirez的反駁
Martin在發(fā)表了那篇分析分布式鎖的blog (How to do distributed locking)之后隐孽,該文章在Twitter和Hacker News上引發(fā)了廣泛的討論癌椿。但人們更想聽到的是Redlock的作者antirez對此會發(fā)表什么樣的看法。
Martin的那篇文章是在2016-02-08這一天發(fā)表的菱阵,但據(jù)Martin說踢俄,他在公開發(fā)表文章的一星期之前就把草稿發(fā)給了antirez進(jìn)行review,而且他們之間通過email進(jìn)行了討論晴及。不知道Martin有沒有意料到都办,antirez對于此事的反應(yīng)很快,就在Martin的文章發(fā)表出來的第二天虑稼,antirez就在他的博客上貼出了他對于此事的反駁文章琳钉,名字叫"Is Redlock safe?",地址如下:
這是高手之間的過招蛛倦。antirez這篇文章也條例非常清晰歌懒,并且中間涉及到大量的細(xì)節(jié)。antirez認(rèn)為溯壶,Martin的文章對于Redlock的批評可以概括為兩個方面(與Martin文章的前后兩部分對應(yīng)):
帶有自動過期功能的分布式鎖及皂,必須提供某種fencing機(jī)制來保證對共享資源的真正的互斥保護(hù)甫男。Redlock提供不了這樣一種機(jī)制。
Redlock構(gòu)建在一個不夠安全的系統(tǒng)模型之上验烧。它對于系統(tǒng)的記時假設(shè)(timing assumption)有比較強(qiáng)的要求查剖,而這些要求在現(xiàn)實的系統(tǒng)中是無法保證的。
antirez對這兩方面分別進(jìn)行了反駁噪窘。
首先笋庄,關(guān)于fencing機(jī)制。antirez對于Martin的這種論證方式提出了質(zhì)疑:既然在鎖失效的情況下已經(jīng)存在一種fencing機(jī)制能繼續(xù)保持資源的互斥訪問了倔监,那為什么還要使用一個分布式鎖并且還要求它提供那么強(qiáng)的安全性保證呢直砂?即使退一步講,Redlock雖然提供不了Martin所講的遞增的fencing token浩习,但利用Redlock產(chǎn)生的隨機(jī)字符串(my_random_value
)可以達(dá)到同樣的效果静暂。這個隨機(jī)字符串雖然不是遞增的,但卻是唯一的谱秽,可以稱之為unique token洽蛀。antirez舉了個例子,比如疟赊,你可以用它來實現(xiàn)“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.(譯文:當(dāng)開始和共享資源交互的時候,我們將它的狀態(tài)設(shè)置成“<token>
”近哟,然后僅在token沒改變的情況下我們才執(zhí)行“讀取-修改-寫回”操作驮审。)
第一遍看到這個描述的時候,我個人是感覺沒太看懂的吉执》枰“Check and Set”應(yīng)該就是我們平常聽到過的CAS操作了,但它如何在這個場景下工作戳玫,antirez并沒有展開說(在后面講到Hacker News上的討論的時候熙掺,我們還會提到)。
然后咕宿,antirez的反駁就集中在第二個方面上:關(guān)于算法在記時(timing)方面的模型假設(shè)币绩。在我們前面分析Martin的文章時也提到過,Martin認(rèn)為Redlock會失效的情況主要有三種:
時鐘發(fā)生跳躍荠列。
長時間的GC pause类浪。
長時間的網(wǎng)絡(luò)延遲。
antirez肯定意識到了這三種情況對Redlock最致命的其實是第一點(diǎn):時鐘發(fā)生跳躍肌似。這種情況一旦發(fā)生费就,Redlock是沒法正常工作的。而對于后兩種情況來說川队,Redlock在當(dāng)初設(shè)計的時候已經(jīng)考慮到了力细,對它們引起的后果有一定的免疫力睬澡。所以,antirez接下來集中精力來說明通過恰當(dāng)?shù)倪\(yùn)維眠蚂,完全可以避免時鐘發(fā)生大的跳動煞聪,而Redlock對于時鐘的要求在現(xiàn)實系統(tǒng)中是完全可以滿足的。
Martin在提到時鐘跳躍的時候逝慧,舉了兩個可能造成時鐘跳躍的具體例子:
系統(tǒng)管理員手動修改了時鐘昔脯。
從NTP服務(wù)收到了一個大的時鐘更新事件。
antirez反駁說:
手動修改時鐘這種人為原因笛臣,不要那么做就是了云稚。否則的話,如果有人手動修改Raft協(xié)議的持久化日志沈堡,那么就算是Raft協(xié)議它也沒法正常工作了静陈。
使用一個不會進(jìn)行“跳躍”式調(diào)整系統(tǒng)時鐘的ntpd程序(可能是通過恰當(dāng)?shù)呐渲茫瑢τ跁r鐘的修改通過多次微小的調(diào)整來完成诞丽。
而Redlock對時鐘的要求鲸拥,并不需要完全精確,它只需要時鐘差不多精確就可以了僧免。比如刑赶,要記時5秒,但可能實際記了4.5秒猬膨,然后又記了5.5秒角撞,有一定的誤差呛伴。不過只要誤差不超過一定范圍勃痴,這對Redlock不會產(chǎn)生影響。antirez認(rèn)為呢热康,像這樣對時鐘精度并不是很高的要求沛申,在實際環(huán)境中是完全合理的。
好了姐军,到此為止铁材,如果你相信antirez這里關(guān)于時鐘的論斷,那么接下來antirez的分析就基本上順理成章了奕锌。
關(guān)于Martin提到的能使Redlock失效的后兩種情況著觉,Martin在分析的時候恰好犯了一個錯誤(在本文上半部分已經(jīng)提到過)。在Martin給出的那個由客戶端GC pause引發(fā)Redlock失效的例子中惊暴,這個GC pause引發(fā)的后果相當(dāng)于在鎖服務(wù)器和客戶端之間發(fā)生了長時間的消息延遲饼丘。Redlock對于這個情況是能處理的×苫埃回想一下Redlock算法的具體過程肄鸽,它使用起來的過程大體可以分成5步:
獲取當(dāng)前時間卫病。
完成獲取鎖的整個過程(與N個Redis節(jié)點(diǎn)交互)。
再次獲取當(dāng)前時間典徘。
把兩個時間相減蟀苛,計算獲取鎖的過程是否消耗了太長時間,導(dǎo)致鎖已經(jīng)過期了逮诲。如果沒過期帜平,
客戶端持有鎖去訪問共享資源。
在Martin舉的例子中梅鹦,GC pause或網(wǎng)絡(luò)延遲罕模,實際發(fā)生在上述第1步和第3步之間。而不管在第1步和第3步之間由于什么原因(進(jìn)程停頓或網(wǎng)絡(luò)延遲等)導(dǎo)致了大的延遲出現(xiàn)帘瞭,在第4步都能被檢查出來淑掌,不會讓客戶端拿到一個它認(rèn)為有效而實際卻已經(jīng)過期的鎖。當(dāng)然蝶念,這個檢查依賴系統(tǒng)時鐘沒有大的跳躍抛腕。這也就是為什么antirez在前面要對時鐘條件進(jìn)行辯護(hù)的原因。
有人會說媒殉,在第3步之后担敌,仍然可能會發(fā)生延遲啊。沒錯廷蓉,antirez承認(rèn)這一點(diǎn)全封,他對此有一段很有意思的論證,原話如下:
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.(譯文:延遲只能發(fā)生在第3步之后桃犬,這導(dǎo)致鎖被認(rèn)為是有效的而實際上已經(jīng)過期了刹悴,也就是說,我們回到了Martin指出的第一個問題上攒暇,客戶端沒能夠在鎖的有效性過期之前完成與共享資源的交互土匀。讓我再次申明一下,這個問題對于所有的分布式鎖的實現(xiàn)是普遍存在的形用,而且基于token的這種解決方案是不切實際的就轧,但也能和Redlock一起用。)
這里antirez所說的“Martin指出的第一個問題”具體是什么呢田度?在本文上半部分我們提到過妒御,Martin的文章分為兩大部分,其中前半部分與Redlock沒有直接關(guān)系镇饺,而是指出了任何一種帶自動過期功能的分布式鎖在沒有提供fencing機(jī)制的前提下都有可能失效乎莉。這里antirez所說的就是指的Martin的文章的前半部分。換句話說,對于大延遲給Redlock帶來的影響梦鉴,恰好與Martin在文章的前半部分針對所有的分布式鎖所做的分析是一致的李茫,而這種影響不單單針對Redlock。Redlock的實現(xiàn)已經(jīng)保證了它是和其它任何分布式鎖的安全性是一樣的肥橙。當(dāng)然魄宏,與其它“更完美”的分布式鎖相比,Redlock似乎提供不了Martin提出的那種遞增的token存筏,但antirez在前面已經(jīng)分析過了宠互,關(guān)于token的這種論證方式本身就是“不切實際”的,或者退一步講椭坚,Redlock能提供的unique token也能夠提供完全一樣的效果予跌。
另外,關(guān)于大延遲對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問:我想知道券册,在我發(fā)文回復(fù)之后,我們能否在一點(diǎn)上達(dá)成一致垂涯,就是大的消息延遲不會給Redlock的運(yùn)行造成損害烁焙。Martin答:對于客戶端和鎖服務(wù)器之間的消息延遲,我同意你的觀點(diǎn)耕赘。但客戶端和被訪問資源之間的延遲還是有問題的骄蝇。)
通過這段對話可以看出,對于Redlock在第4步所做的鎖有效性的檢查操骡,Martin是予以肯定的九火。但他認(rèn)為客戶端和資源服務(wù)器之間的延遲還是會帶來問題的。Martin在這里說的有點(diǎn)模糊册招。就像antirez前面分析的岔激,客戶端和資源服務(wù)器之間的延遲,對所有的分布式鎖的實現(xiàn)都會帶來影響跨细,這不單單是Redlock的問題了鹦倚。
以上就是antirez在blog中所說的主要內(nèi)容。有一些點(diǎn)值得我們注意一下:
antirez是同意大的系統(tǒng)時鐘跳躍會造成Redlock失效的冀惭。在這一點(diǎn)上,他與Martin的觀點(diǎn)的不同在于掀鹅,他認(rèn)為在實際系統(tǒng)中是可以避免大的時鐘跳躍的散休。當(dāng)然,這取決于基礎(chǔ)設(shè)施和運(yùn)維方式乐尊。
antirez在設(shè)計Redlock的時候戚丸,是充分考慮了網(wǎng)絡(luò)延遲和程序停頓所帶來的影響的。但是,對于客戶端和資源服務(wù)器之間的延遲(即發(fā)生在算法第3步之后的延遲)限府,antirez是承認(rèn)所有的分布式鎖的實現(xiàn)夺颤,包括Redlock,是沒有什么好辦法來應(yīng)對的胁勺。
討論進(jìn)行到這世澜,Martin和antirez之間誰對誰錯其實并不是那么重要了。只要我們能夠?qū)edlock(或者其它分布式鎖)所能提供的安全性的程度有充分的了解署穗,那么我們就能做出自己的選擇了寥裂。
Hacker News上的一些討論
針對Martin和antirez的兩篇blog,很多技術(shù)人員在Hacker News上展開了激烈的討論案疲。這些討論所在地址如下:
針對Martin的blog的討論:https://news.ycombinator.com/item?id=11059738
針對antirez的blog的討論:https://news.ycombinator.com/item?id=11065933
在Hacker News上封恰,antirez積極參與了討論,而Martin則始終置身事外褐啡。
下面我把這些討論中一些有意思的點(diǎn)拿出來與大家一起分享一下(集中在對于fencing token機(jī)制的討論上)诺舔。
關(guān)于antirez提出的“Check and Set”操作,他在blog里并沒有詳加說明备畦。果然混萝,在Hacker News上就有人出來問了。antirez給出的答復(fù)如下:
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.
翻譯一下可以這樣理解:假設(shè)你要修改資源X萍恕,那么遵循下面的偽碼所定義的步驟逸嘀。
先設(shè)置X.currlock = token。
讀出資源X(包括它的值和附帶的X.currlock)允粤。
按照"write-if-currlock == token"的邏輯崭倘,修改資源X的值。意思是說类垫,如果對X進(jìn)行修改的時候司光,X.currlock仍然和當(dāng)初設(shè)置進(jìn)去的token相等,那么才進(jìn)行修改悉患;如果這時X.currlock已經(jīng)是其它值了残家,那么說明有另外一方也在試圖進(jìn)行修改操作,那么放棄當(dāng)前的修改售躁,從而避免沖突坞淮。
隨后Hacker News上一位叫viraptor的用戶提出了異議,它給出了這樣一個執(zhí)行序列:
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同時進(jìn)行寫操作回窘,沖突了。不過市袖,這位用戶應(yīng)該是理解錯了antirez給出的修改過程了啡直。按照antirez的意思,判斷X.currlock是否修改過和對資源的寫操作,應(yīng)該是一個原子操作酒觅。只有這樣理解才能合乎邏輯撮执,否則的話,這個過程就有嚴(yán)重的破綻舷丹。這也是為什么antirez之前會對fencing機(jī)制產(chǎn)生質(zhì)疑:既然資源服務(wù)器本身都能提供互斥的原子操作了抒钱,為什么還需要一個分布式鎖呢?因此掂榔,antirez認(rèn)為這種fencing機(jī)制是很累贅的继效,他之所以還是提出了這種“Check and Set”操作,只是為了證明在提供fencing token這一點(diǎn)上装获,Redlock也能做到瑞信。但是,這里仍然有一些不明確的地方穴豫,如果將"write-if-currlock == token"看做是原子操作的話凡简,這個邏輯勢必要在資源服務(wù)器上執(zhí)行,那么第二步為什么還要“讀出資源X”呢精肃?除非這個“讀出資源X”的操作也是在資源服務(wù)器上執(zhí)行秤涩,它包含在“判斷-寫回”這個原子操作里面。而假如不這樣理解的話司抱,“讀取-判斷-寫回”這三個操作都放在客戶端執(zhí)行筐眷,那么看不出它們?nèi)绾尾拍軐崿F(xiàn)原子性操作。在下面的討論中习柠,我們暫時忽略“讀出資源X”這一步匀谣。
這個基于random token的“Check and Set”操作,如果與Martin提出的遞增的fencing token對比一下的話资溃,至少有兩點(diǎn)不同:
“Check and Set”對于寫操作要分成兩步來完成(設(shè)置token武翎、判斷-寫回),而遞增的fencing token機(jī)制只需要一步(帶著token向資源服務(wù)器發(fā)起寫請求)溶锭。
遞增的fencing token機(jī)制能保證最終操作共享資源的順序宝恶,那些延遲時間太長的操作就無法操作共享資源了。但是基于random token的“Check and Set”操作不會保證這個順序趴捅,那些延遲時間太長的操作如果后到達(dá)了垫毙,它仍然有可能操作共享資源(當(dāng)然是以互斥的方式)。
對于前一點(diǎn)不同驻售,我們在后面的分析中會看到露久,如果資源服務(wù)器也是分布式的,那么使用遞增的fencing token也要變成兩步欺栗。
而對于后一點(diǎn)操作順序上的不同,antirez認(rèn)為這個順序沒有意義,關(guān)鍵是能互斥訪問就行了迟几。他寫下了下面的話:
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.(譯文: 我們的目標(biāo)是消请,當(dāng)競爭條件出現(xiàn)的時候,能夠以某種方式避免类腮。......還需要注意的是臊泰,當(dāng)那種競爭條件出現(xiàn)的時候,比如由于延遲蚜枢,客戶端是同時來訪問的缸逃,鎖的ID的大小順序跟那些操作真正想執(zhí)行的順序,是沒有什么關(guān)系的厂抽。)
這里的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服務(wù)器)執(zhí)行“添加名字”的操作,A本來是排在B前面的藐守,但獲得鎖的順序卻是B排在A前面挪丢。因此,antirez說卢厂,鎖的ID的大小順序跟那些操作真正想執(zhí)行的順序乾蓬,是沒有什么關(guān)系的。關(guān)鍵是能排出一個順序來慎恒,能互斥訪問就行了任内。那么,至于鎖的ID是遞增的巧号,還是一個random token族奢,自然就不那么重要了。
Martin提出的fencing token機(jī)制丹鸿,給人留下了無盡的疑惑越走。這主要是因為他對于這一機(jī)制的描述缺少太多的技術(shù)細(xì)節(jié)。從上面的討論可以看出靠欢,antirez對于這一機(jī)制的看法是廊敌,它跟一個random token沒有什么區(qū)別,而且门怪,它需要資源服務(wù)器本身提供某種互斥機(jī)制骡澈,這幾乎讓分布式鎖本身的存在失去了意義。圍繞fencing token的問題掷空,還有兩點(diǎn)是比較引人注目的肋殴,Hacker News上也有人提出了相關(guān)的疑問:
(1)關(guān)于資源服務(wù)器本身的架構(gòu)細(xì)節(jié)囤锉。
(2)資源服務(wù)器對于fencing token進(jìn)行檢查的實現(xiàn)細(xì)節(jié),比如是否需要提供一種原子操作护锤。
關(guān)于上述問題(1)官地,Hacker News上有一位叫dwenzek的用戶發(fā)表了下面的評論:
...... 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?
(譯文:...... 關(guān)于使用fencing token拒絕掉延遲請求的相關(guān)議題,是不夠清晰的烙懦,因為受保護(hù)的資源以及對它的訪問方式本身是沒有被明確定義過的驱入。資源服務(wù)是不是分布式的呢?如果是氯析,資源服務(wù)有沒有一種方式能確保token在所有節(jié)點(diǎn)上遞增呢亏较?對于客戶端的Session由于過期而被中斷的情況帮非,資源服務(wù)有辦法將它的影響回滾嗎贯要?)
這些疑問在Hacker News上并沒有人給出解答。而關(guān)于分布式的資源服務(wù)器架構(gòu)如何處理fencing token蜗元,另外一名分布式系統(tǒng)的專家Flavio Junqueira在他的一篇blog中有所提及(我們后面會再提到)拾因。
關(guān)于上述問題(2)旺罢,Hacker News上有一位叫reza_n的用戶發(fā)表了下面的疑問:
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?
(譯文: 我理解當(dāng)兩個客戶端同時獲得鎖的時候fencing token是如何防止亂序的。但是如果兩個寫操作恰好按序到達(dá)了绢记,而且它們在對同一個值進(jìn)行修改扁达,那會發(fā)生什么呢?難道不會仍然是依賴某種數(shù)據(jù)版本號或者樂觀鎖的機(jī)制蠢熄?這不會讓分布式鎖變得沒有必要了嗎跪解?)
一位叫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.
(譯文: 我認(rèn)為“第一個”寫請求會失敗,因為它傳入的token不再是“最新的”了签孔,這意味著鎖已經(jīng)釋放或者過期了叉讥。)
Terr_的回答到底對不對呢?這不好說饥追,取決于資源服務(wù)器對于fencing token進(jìn)行檢查的實現(xiàn)細(xì)節(jié)图仓。讓我們來簡單分析一下。
為了簡單起見但绕,我們假設(shè)有一臺(先不考慮分布式的情況)通過RPC進(jìn)行遠(yuǎn)程訪問文件服務(wù)器救崔,它無法提供對于文件的互斥訪問(否則我們就不需要分布式鎖了)。現(xiàn)在我們按照Martin給出的說法捏顺,加入fencing token的檢查邏輯六孵。由于Martin沒有描述具體細(xì)節(jié),我們猜測至少有兩種可能幅骄。
第一種可能劫窒,我們修改了文件服務(wù)器的代碼,讓它能多接受一個fencing token的參數(shù)拆座,并在進(jìn)行所有處理之前加入了一個簡單的判斷邏輯主巍,保證只有當(dāng)前接收到的fencing token大于之前的值才允許進(jìn)行后邊的訪問冠息。而一旦通過了這個判斷,后面的處理不變煤禽。
現(xiàn)在想象reza_n描述的場景铐达,客戶端1和客戶端2都發(fā)生了GC pause岖赋,兩個fencing token都延遲了檬果,它們幾乎同時到達(dá)了文件服務(wù)器,而且保持了順序唐断。那么选脊,我們新加入的判斷邏輯,應(yīng)該對兩個請求都會放過脸甘,而放過之后它們幾乎同時在操作文件恳啥,還是沖突了。既然Martin宣稱fencing token能保證分布式鎖的正確性丹诀,那么上面這種可能的猜測也許是我們理解錯了钝的。
當(dāng)然,還有第二種可能铆遭,就是我們對文件服務(wù)器確實做了比較大的改動硝桩,讓這里判斷token的邏輯和隨后對文件的處理放在一個原子操作里了。這可能更接近antirez的理解枚荣。這樣的話碗脊,前面reza_n描述的場景中,兩個寫操作都應(yīng)該成功橄妆。
基于ZooKeeper的分布式鎖更安全嗎衙伶?
很多人(也包括Martin在內(nèi))都認(rèn)為,如果你想構(gòu)建一個更安全的分布式鎖害碾,那么應(yīng)該使用ZooKeeper矢劲,而不是Redis。那么慌随,為了對比的目的芬沉,讓我們先暫時脫離開本文的題目,討論一下基于ZooKeeper的分布式鎖能提供絕對的安全嗎儒陨?它需要fencing token機(jī)制的保護(hù)嗎花嘶?
我們不得不提一下分布式專家Flavio Junqueira所寫的一篇blog,題目叫“Note on fencing and distributed locks”蹦漠,地址如下:
Flavio Junqueira是ZooKeeper的作者之一椭员,他的這篇blog就寫在Martin和antirez發(fā)生爭論的那幾天。他在文中給出了一個基于ZooKeeper構(gòu)建分布式鎖的描述(當(dāng)然這不是唯一的方式):
客戶端嘗試創(chuàng)建一個znode節(jié)點(diǎn)笛园,比如
/lock
隘击。那么第一個客戶端就創(chuàng)建成功了侍芝,相當(dāng)于拿到了鎖;而其它的客戶端會創(chuàng)建失斅裢(znode已存在)州叠,獲取鎖失敗。持有鎖的客戶端訪問共享資源完成后凶赁,將znode刪掉咧栗,這樣其它客戶端接下來就能來獲取鎖了。
znode應(yīng)該被創(chuàng)建成ephemeral的虱肄。這是znode的一個特性致板,它保證如果創(chuàng)建znode的那個客戶端崩潰了,那么相應(yīng)的znode會被自動刪除咏窿。這保證了鎖一定會被釋放斟或。
看起來這個鎖相當(dāng)完美,沒有Redlock過期時間的問題集嵌,而且能在需要的時候讓鎖自動釋放萝挤。但仔細(xì)考察的話,并不盡然根欧。
ZooKeeper是怎么檢測出某個客戶端已經(jīng)崩潰了呢怜珍?實際上,每個客戶端都與ZooKeeper的某臺服務(wù)器維護(hù)著一個Session咽块,這個Session依賴定期的心跳(heartbeat)來維持绘面。如果ZooKeeper長時間收不到客戶端的心跳(這個時間稱為Sesion的過期時間),那么它就認(rèn)為Session過期了侈沪,通過這個Session所創(chuàng)建的所有的ephemeral類型的znode節(jié)點(diǎn)都會被自動刪除揭璃。
設(shè)想如下的執(zhí)行序列:
客戶端1創(chuàng)建了znode節(jié)點(diǎn)
/lock
,獲得了鎖亭罪。客戶端1進(jìn)入了長時間的GC pause瘦馍。
客戶端1連接到ZooKeeper的Session過期了。znode節(jié)點(diǎn)
/lock
被自動刪除应役。客戶端2創(chuàng)建了znode節(jié)點(diǎn)
/lock
情组,從而獲得了鎖。客戶端1從GC pause中恢復(fù)過來箩祥,它仍然認(rèn)為自己持有鎖院崇。
最后,客戶端1和客戶端2都認(rèn)為自己持有了鎖袍祖,沖突了底瓣。這與之前Martin在文章中描述的由于GC pause導(dǎo)致的分布式鎖失效的情況類似。
看起來蕉陋,用ZooKeeper實現(xiàn)的分布式鎖也不一定就是安全的捐凭。該有的問題它還是有拨扶。但是,ZooKeeper作為一個專門為分布式應(yīng)用提供方案的框架茁肠,它提供了一些非常好的特性患民,是Redis之類的方案所沒有的。像前面提到的ephemeral類型的znode自動刪除的功能就是一個例子垦梆。
還有一個很有用的特性是ZooKeeper的watch機(jī)制匹颤。這個機(jī)制可以這樣來使用,比如當(dāng)客戶端試圖創(chuàng)建/lock
的時候奶赔,發(fā)現(xiàn)它已經(jīng)存在了惋嚎,這時候創(chuàng)建失敗,但客戶端不一定就此對外宣告獲取鎖失敗站刑。客戶端可以進(jìn)入一種等待狀態(tài)鼻百,等待當(dāng)/lock
節(jié)點(diǎn)被刪除的時候绞旅,ZooKeeper通過watch機(jī)制通知它,這樣它就可以繼續(xù)完成創(chuàng)建操作(獲取鎖)温艇。這可以讓分布式鎖在客戶端用起來就像一個本地的鎖一樣:加鎖失敗就阻塞住因悲,直到獲取到鎖為止。這樣的特性Redlock就無法實現(xiàn)勺爱。
小結(jié)一下晃琳,基于ZooKeeper的鎖和基于Redis的鎖相比在實現(xiàn)特性上有兩個不同:
在正常情況下,客戶端可以持有鎖任意長的時間琐鲁,這可以確保它做完所有需要的資源訪問操作之后再釋放鎖卫旱。這避免了基于Redis的鎖對于有效時間(lock validity time)到底設(shè)置多長的兩難問題。實際上围段,基于ZooKeeper的鎖是依靠Session(心跳)來維持鎖的持有狀態(tài)的顾翼,而Redis不支持Sesion。
基于ZooKeeper的鎖支持在獲取鎖失敗之后等待鎖重新釋放的事件奈泪。這讓客戶端對鎖的使用更加靈活适贸。
順便提一下,如上所述的基于ZooKeeper的分布式鎖的實現(xiàn)涝桅,并不是最優(yōu)的拜姿。它會引發(fā)“herd effect”(羊群效應(yīng)),降低獲取鎖的性能冯遂。一個更好的實現(xiàn)參見下面鏈接:
我們重新回到Flavio Junqueira對于fencing token的分析蕊肥。Flavio Junqueira指出,fencing token機(jī)制本質(zhì)上是要求客戶端在每次訪問一個共享資源的時候债蜜,在執(zhí)行任何操作之前晴埂,先對資源進(jìn)行某種形式的“標(biāo)記”(mark)操作究反,這個“標(biāo)記”能保證持有舊的鎖的客戶端請求(如果延遲到達(dá)了)無法操作資源。這種標(biāo)記操作可以是很多形式儒洛,fencing token是其中比較典型的一個精耐。
隨后Flavio Junqueira提到用遞增的epoch number(相當(dāng)于Martin的fencing token)來保護(hù)共享資源。而對于分布式的資源琅锻,為了方便討論卦停,假設(shè)分布式資源是一個小型的多備份的數(shù)據(jù)存儲(a small replicated data store),執(zhí)行寫操作的時候需要向所有節(jié)點(diǎn)上寫數(shù)據(jù)恼蓬。最簡單的做標(biāo)記的方式惊完,就是在對資源進(jìn)行任何操作之前小槐,先把epoch number標(biāo)記到各個資源節(jié)點(diǎn)上去。這樣凿跳,各個節(jié)點(diǎn)就保證了舊的(也就是小的)epoch number無法操作數(shù)據(jù)疮方。
當(dāng)然,這里再展開討論下去可能就涉及到了這個數(shù)據(jù)存儲服務(wù)的實現(xiàn)細(xì)節(jié)了骡显。比如在實際系統(tǒng)中,可能為了容錯壁顶,只要上面講的標(biāo)記和寫入操作在多數(shù)節(jié)點(diǎn)上完成就算成功完成了(Flavio Junqueira并沒有展開去講)。在這里我們能看到的博助,最重要的痹愚,是這種標(biāo)記操作如何起作用的方式富岳。這有點(diǎn)類似于Paxos協(xié)議(Paxos協(xié)議要求每個proposal對應(yīng)一個遞增的數(shù)字,執(zhí)行accept請求之前先執(zhí)行prepare請求)拯腮。antirez提出的random token的方式顯然不符合Flavio Junqueira對于“標(biāo)記”操作的定義窖式,因為它無法區(qū)分新的token和舊的token。只有遞增的數(shù)字才能確保最終收斂到最新的操作結(jié)果上动壤。
在這個分布式數(shù)據(jù)存儲服務(wù)(共享資源)的例子中萝喘,客戶端在標(biāo)記完成之后執(zhí)行寫入操作的時候,存儲服務(wù)的節(jié)點(diǎn)需要判斷epoch number是不是最新,然后確定能不能執(zhí)行寫入操作阁簸。如果按照上一節(jié)我們的分析思路爬早,這里的epoch判斷和接下來的寫入操作,是不是在一個原子操作里呢启妹?根據(jù)Flavio Junqueira的相關(guān)描述筛严,我們相信,應(yīng)該是原子的饶米。那么既然資源本身可以提供原子互斥操作了桨啃,那么分布式鎖還有存在的意義嗎?應(yīng)該說有檬输≌振客戶端可以利用分布式鎖有效地避免沖突,等待寫入機(jī)會丧慈,這對于包含多個節(jié)點(diǎn)的分布式資源尤其有用(當(dāng)然析命,是出于效率的原因)。
Chubby的分布式鎖是怎樣做fencing的伊滋?
提到分布式鎖碳却,就不能不提Google的Chubby。
Chubby是Google內(nèi)部使用的分布式鎖服務(wù)笑旺,有點(diǎn)類似于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.
(譯文: 一個進(jìn)程持有鎖L雳攘,發(fā)起了請求R吨灭,但是請求失敗了涩咖。另一個進(jìn)程獲得了鎖L并在請求R到達(dá)目的方之前執(zhí)行了一些動作檩互。如果后來請求R到達(dá)了闸昨,它就有可能在沒有鎖L保護(hù)的情況下進(jìn)行操作饵较,帶來數(shù)據(jù)不一致的潛在風(fēng)險循诉。)
這跟Martin的分析大同小異茄猫。
Chubby給出的用于解決(緩解)這一問題的機(jī)制稱為sequencer划纽,類似于fencing token機(jī)制勇劣。鎖的持有者可以隨時請求一個sequencer,這是一個字節(jié)串卸耘,它由三部分組成:
鎖的名字侈百。
鎖的獲取模式(排他鎖還是共享鎖)。
lock generation number(一個64bit的單調(diào)遞增數(shù)字)讽坏。作用相當(dāng)于fencing token或epoch number路呜。
客戶端拿到sequencer之后胀葱,在操作資源的時候把它傳給資源服務(wù)器抵屿。然后轧葛,資源服務(wù)器負(fù)責(zé)對sequencer的有效性進(jìn)行檢查艇搀。檢查可以有兩種方式:
調(diào)用Chubby提供的API衷笋,CheckSequencer()右莱,將整個sequencer傳進(jìn)去進(jìn)行檢查。這個檢查是為了保證客戶端持有的鎖在進(jìn)行資源訪問的時候仍然有效亚再。
將客戶端傳來的sequencer與資源服務(wù)器當(dāng)前觀察到的最新的sequencer進(jìn)行對比檢查则剃∪缤保可以理解為與Martin描述的對于fencing token的檢查類似己肮。
當(dāng)然谎僻,如果由于兼容的原因,資源服務(wù)本身不容易修改赤拒,那么Chubby還提供了一種機(jī)制:
- lock-delay。Chubby允許客戶端為持有的鎖指定一個lock-delay的時間值(默認(rèn)是1分鐘)蕉朵。當(dāng)Chubby發(fā)現(xiàn)客戶端被動失去聯(lián)系的時候敷存,并不會立即釋放鎖觅闽,而是會在lock-delay指定的時間內(nèi)阻止其它客戶端獲得這個鎖蛉拙。這是為了在把鎖分配給新的客戶端之前孕锄,讓之前持有鎖的客戶端有充分的時間把請求隊列排空(draining the queue)畸肆,盡量防止出現(xiàn)延遲到達(dá)的未處理請求轴脐。
可見大咱,為了應(yīng)對鎖失效問題碴巾,Chubby提供的三種處理方式:CheckSequencer()檢查厦瓢、與上次最新的sequencer對比、lock-delay碳锈,它們對于安全性的保證是從強(qiáng)到弱的售碳。而且贸人,這些處理方式本身都沒有保證提供絕對的正確性(correctness)。但是十拣,Chubby確實提供了單調(diào)遞增的lock generation number夭问,這就允許資源服務(wù)器在需要的時候缰趋,利用它提供更強(qiáng)的安全性保障秘血。
關(guān)于時鐘
在Martin與antirez的這場爭論中灰粮,沖突最為嚴(yán)重的就是對于系統(tǒng)時鐘的假設(shè)是不是合理的問題。Martin認(rèn)為系統(tǒng)時鐘難免會發(fā)生跳躍(這與分布式算法的異步模型相符),而antirez認(rèn)為在實際中系統(tǒng)時鐘可以保證不發(fā)生大的跳躍韧骗。
Martin對于這一分歧發(fā)表了如下看法(原話):
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.
(譯文:從根本上來說袍暴,這場討論最后歸結(jié)到了一個問題上:為了確保安全性而做出的記時假設(shè)到底是否合理政模。我認(rèn)為不合理耗式,而antirez認(rèn)為合理 —— 但是這也沒關(guān)系刊咳。工程問題的討論很少只有一個正確答案娱挨。)
那么跷坝,在實際系統(tǒng)中柴钻,時鐘到底是否可信呢顿颅?對此,Julia Evans專門寫了一篇文章绍些,“TIL: clock skew exists”柬批,總結(jié)了很多跟時鐘偏移有關(guān)的實際資料,并進(jìn)行了分析上沐。這篇文章地址:
Julia Evans在文章最后得出的結(jié)論是:
clock skew is real(時鐘偏移在現(xiàn)實中是存在的)
Martin的事后總結(jié)
我們前面提到過龄广,當(dāng)各方的爭論在激烈進(jìn)行的時候蕴侧,Martin幾乎始終置身事外择同。但是Martin在這件事過去之后,把這個事件的前后經(jīng)過總結(jié)成了一個很長的故事線净宵。如果你想最全面地了解這個事件發(fā)生的前后經(jīng)過敲才,那么建議去讀讀Martin的這個總結(jié):
在這個故事總結(jié)的最后,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.
(譯文:對我來說最重要的一點(diǎn)在于:我并不在乎在這場辯論中誰對誰錯 —— 我只關(guān)心從其他人的工作中學(xué)到的東西塘娶,以便我們能夠避免重蹈覆轍归斤,并讓未來更加美好。前人已經(jīng)為我們創(chuàng)造出了許多偉大的成果:站在巨人的肩膀上刁岸,我們得以構(gòu)建更棒的軟件。......對于任何想法,務(wù)必要詳加檢驗,通過論證以及檢查它們是否經(jīng)得住別人的詳細(xì)審查返奉。那是學(xué)習(xí)過程的一部分污尉。但目標(biāo)應(yīng)該是為了獲得知識缩抡,而不應(yīng)該是為了說服別人相信你自己是對的蘑险。有時候,那只不過意味著停下來,好好地想一想斗这。)
關(guān)于分布式鎖的這場爭論彼水,我們已經(jīng)完整地做了回顧和分析叛赚。
按照鎖的兩種用途溪掀,如果僅是為了效率(efficiency)随闪,那么你可以自己選擇你喜歡的一種分布式鎖的實現(xiàn)畜吊。當(dāng)然驱证,你需要清楚地知道它在安全性上有哪些不足获高,以及它會帶來什么后果游两。而如果你是為了正確性(correctness),那么請慎之又慎蕴忆。在本文的討論中,我們在分布式鎖的正確性上走得最遠(yuǎn)的地方,要數(shù)對于ZooKeeper分布式鎖、單調(diào)遞增的epoch number以及對分布式資源進(jìn)行標(biāo)記的分析了。請仔細(xì)審查相關(guān)的論證。
Martin為我們留下了不少疑問土浸,尤其是他提出的fencing token機(jī)制扶叉。他在blog中提到,會在他的新書《Designing Data-Intensive Applications》的第8章和第9章再詳加論述搪桂。目前背率,這本書尚在預(yù)售當(dāng)中划滋。我感覺同窘,這會是一本值得一讀的書钱雷,它不同于為了出名或賺錢而出版的那種短平快的書籍〗盖玻可以看出作者在這本書上投入了巨大的精力。
最后肩刃,我相信祟霍,這個討論還遠(yuǎn)沒有結(jié)束杏头。分布式鎖(Distributed Locks)和相應(yīng)的fencing方案,可以作為一個長期的課題浅碾,隨著我們對分布式系統(tǒng)的認(rèn)識逐漸增加大州,可以再來慢慢地思考它。思考它更深層的本質(zhì)垂谢,以及它在理論上的證明厦画。