本文是對 Martin Kleppmann 的文章 How to do distributed locking 部分內(nèi)容的翻譯和總結(jié)绳矩,上次寫 Redlock 的原因就是看到了 Martin 的這篇文章,寫得很好玖翅,特此翻譯和總結(jié)翼馆。感興趣的同學(xué)可以翻看原文,相信會收獲良多金度。
開篇作者認為現(xiàn)在 Redis 逐漸被使用到數(shù)據(jù)管理領(lǐng)域应媚,這個領(lǐng)域需要更強的數(shù)據(jù)一致性和耐久性,這使得他感到擔(dān)心猜极,因為這不是 Redis 最初設(shè)計的初衷(事實上這也是很多業(yè)界程序員的誤區(qū)中姜,越來越把 Redis 當(dāng)成數(shù)據(jù)庫在使用),其中基于 Redis 的分布式鎖就是令人擔(dān)心的其一魔吐。
Martin 指出首先你要明確你為什么使用分布式鎖扎筒,為了性能還是正確性?為了幫你區(qū)分這二者酬姆,在這把鎖 fail 了的時候你可以詢問自己以下問題:
- 要性能的:擁有這把鎖使得你不會重復(fù)勞動(例如一個 job 做了兩次)嗜桌,如果這把鎖 fail 了,兩個節(jié)點同時做了這個 Job辞色,那么這個 Job 增加了你的成本骨宠。
- 要正確性的:擁有鎖可以防止并發(fā)操作污染你的系統(tǒng)或者數(shù)據(jù),如果這把鎖 fail 了兩個節(jié)點同時操作了一份數(shù)據(jù)相满,結(jié)果可能是數(shù)據(jù)不一致层亿、數(shù)據(jù)丟失、file 沖突等立美,會導(dǎo)致嚴重的后果匿又。
上述二者都是需求鎖的正確場景,但是你必須清楚自己是因為什么原因需要分布式鎖建蹄。
如果你只是為了性能碌更,那沒必要用 Redlock,它成本高且復(fù)雜洞慎,你只用一個 Redis 實例也夠了痛单,最多加個從防止主掛了。當(dāng)然劲腿,你使用單節(jié)點的 Redis 那么斷電或者一些情況下旭绒,你會丟失鎖,但是你的目的只是加速性能且斷電這種事情不會經(jīng)常發(fā)生,這并不是什么大問題挥吵。并且如果你使用了單節(jié)點 Redis重父,那么很顯然你這個應(yīng)用需要的鎖粒度是很模糊粗糙的,也不會是什么重要的服務(wù)蔫劣。
那么是否 Redlock 對于要求正確性的場景就合適呢坪郭?Martin 列舉了若干場景證明 Redlock 這種算法是不可靠的。
用鎖保護資源
這節(jié)里 Martin 先將 Redlock 放在了一邊而是僅討論總體上一個分布式鎖是怎么工作的脉幢。在分布式環(huán)境下歪沃,鎖比 mutex 這類復(fù)雜,因為涉及到不同節(jié)點嫌松、網(wǎng)絡(luò)通信并且他們隨時可能無征兆的 fail 沪曙。
Martin 假設(shè)了一個場景,一個 client 要修改一個文件萎羔,它先申請得到鎖液走,然后修改文件寫回,放鎖贾陷。另一個 client 再申請鎖 ... 代碼流程如下:
// THIS CODE IS BROKEN
function writeData(filename, data) {
var lock = lockService.acquireLock(filename);
if (!lock) {
throw 'Failed to acquire lock';
}
try {
var file = storage.readFile(filename);
var updated = updateContents(file, data);
storage.writeFile(filename, updated);
} finally {
lock.release();
}
}
可惜即使你的鎖服務(wù)非常完美缘眶,上述代碼還是可能跪,下面的流程圖會告訴你為什么:
上述圖中髓废,得到鎖的 client1 在持有鎖的期間 pause 了一段時間巷懈,例如 GC 停頓。鎖有過期時間(一般叫租約慌洪,為了防止某個 client 崩潰之后一直占有鎖)顶燕,但是如果 GC 停頓太長超過了鎖租約時間,此時鎖已經(jīng)被另一個 client2 所得到冈爹,原先的 client1 還沒有感知到鎖過期涌攻,那么奇怪的結(jié)果就會發(fā)生,曾經(jīng) HBase 就發(fā)生過這種 Bug频伤。即使你在 client1 寫回之前檢查一下鎖是否過期也無助于解決這個問題恳谎,因為 GC 可能在任何時候發(fā)生,即使是你非常不便的時候(在最后的檢查與寫操作期間)憋肖。
如果你認為自己的程序不會有長時間的 GC 停頓因痛,還有其他原因會導(dǎo)致你的進程 pause。例如進程可能讀取尚未進入內(nèi)存的數(shù)據(jù)瞬哼,所以它得到一個 page fault 并且等待 page 被加載進緩存婚肆;還有可能你依賴于網(wǎng)絡(luò)服務(wù)租副;或者其他進程占用 CPU坐慰;或者其他人意外發(fā)生 SIGSTOP 等。
... .... 這里 Martin 又增加了一節(jié)列舉各種進程 pause 的例子,為了證明上面的代碼是不安全的结胀,無論你的鎖服務(wù)多完美赞咙。
使用 Fencing (柵欄)使得鎖變安全
修復(fù)問題的方法也很簡單:你需要在每次寫操作時加入一個 fencing token。這個場景下糟港,fencing token 可以是一個遞增的數(shù)字(lock service 可以做到)攀操,每次有 client 申請鎖就遞增一次:
client1 申請鎖同時拿到 token33,然后它進入長時間的停頓鎖也過期了秸抚。client2 得到鎖和 token34 寫入數(shù)據(jù)速和,緊接著 client1 活過來之后嘗試寫入數(shù)據(jù),自身 token33 比 34 小因此寫入操作被拒絕剥汤。注意這需要存儲層來檢查 token颠放,但這并不難實現(xiàn)。如果你使用 Zookeeper 作為 lock service 的話那么你可以使用 zxid 作為遞增數(shù)字吭敢。
但是對于 Redlock 你要知道碰凶,沒什么生成 fencing token 的方式,并且怎么修改 Redlock 算法使其能產(chǎn)生 fencing token 呢鹿驼?好像并不那么顯而易見欲低。因為產(chǎn)生 token 需要單調(diào)遞增,除非在單節(jié)點 Redis 上完成但是這又沒有高可靠性畜晰,你好像需要引進一致性協(xié)議來讓 Redlock 產(chǎn)生可靠的 fencing token砾莱。
使用時間來解決一致性
Redlock 無法產(chǎn)生 fencing token 早該成為在需求正確性的場景下棄用它的理由,但還有一些值得討論的地方舷蟀。
學(xué)術(shù)界有個說法恤磷,算法對時間不做假設(shè):因為進程可能pause一段時間、數(shù)據(jù)包可能因為網(wǎng)絡(luò)延遲延后到達野宜、時鐘可能根本就是錯的扫步。而可靠的算法依舊要在上述假設(shè)下做正確的事情。
對于 failure detector 來說匈子,timeout 只能作為猜測某個節(jié)點 fail 的依據(jù)河胎,因為網(wǎng)絡(luò)延遲、本地時鐘不正確等其他原因的限制虎敦∮卧溃考慮到 Redis 使用 gettimeofday,而不是單調(diào)的時鐘其徙,會受到系統(tǒng)時間的影響胚迫,可能會突然前進或者后退一段時間,這會導(dǎo)致一個 key 更快或更慢地過期唾那。
可見访锻,Redlock 依賴于許多時間假設(shè),它假設(shè)所有 Redis 節(jié)點都能對同一個 Key 在其過期前持有差不多的時間、跟過期時間相比網(wǎng)絡(luò)延遲很小期犬、跟過期時間相比進程 pause 很短河哑。
用不可靠的時間打破 Redlock
這節(jié) Martin 舉了個因為時間問題,Redlock 不可靠的例子龟虎。
- client1 從 ABC 三個節(jié)點處申請到鎖璃谨,DE由于網(wǎng)絡(luò)原因請求沒有到達
- C節(jié)點的時鐘往前推了,導(dǎo)致 lock 過期
- client2 在CDE處獲得了鎖鲤妥,AB由于網(wǎng)絡(luò)原因請求未到達
- 此時 client1 和 client2 都獲得了鎖
在 Redlock 官方文檔中也提到了這個情況佳吞,不過是C崩潰的時候,Redlock 官方本身也是知道 Redlock 算法不是完全可靠的棉安,官方為了解決這種問題建議使用延時啟動容达,相關(guān)內(nèi)容可以看之前的這篇文章。但是 Martin 這里分析得更加全面垂券,指出延時啟動不也是依賴于時鐘的正確性的么花盐?
接下來 Martin 又列舉了進程 Pause 時而不是時鐘不可靠時會發(fā)生的問題:
- client1 從 ABCDE 處獲得了鎖
- 當(dāng)獲得鎖的 response 還沒到達 client1 時 client1 進入 GC 停頓
- 停頓期間鎖已經(jīng)過期了
- client2 在 ABCDE 處獲得了鎖
- client1 GC 完成收到了獲得鎖的 response,此時兩個 client 又拿到了同一把鎖
同時長時間的網(wǎng)絡(luò)延遲也有可能導(dǎo)致同樣的問題菇爪。
Redlock 的同步性假設(shè)
這些例子說明了算芯,僅有在你假設(shè)了一個同步性系統(tǒng)模型的基礎(chǔ)上,Redlock 才能正常工作凳宙,也就是系統(tǒng)能滿足以下屬性:
- 網(wǎng)絡(luò)延時邊界熙揍,即假設(shè)數(shù)據(jù)包一定能在某個最大延時之內(nèi)到達
- 進程停頓邊界,即進程停頓一定在某個最大時間之內(nèi)
- 時鐘錯誤邊界氏涩,即不會從一個壞的 NTP 服務(wù)器處取得時間
結(jié)論
Martin 認為 Redlock 實在不是一個好的選擇届囚,對于需求性能的分布式鎖應(yīng)用它太重了且成本高;對于需求正確性的應(yīng)用來說它不夠安全是尖。因為它對高危的時鐘或者說其他上述列舉的情況進行了不可靠的假設(shè)意系,如果你的應(yīng)用只需要高性能的分布式鎖不要求多高的正確性,那么單節(jié)點 Redis 夠了饺汹;如果你的應(yīng)用想要保住正確性蛔添,那么不建議 Redlock,建議使用一個合適的一致性協(xié)調(diào)系統(tǒng)兜辞,例如 Zookeeper迎瞧,且保證存在 fencing token。