針對共享資源的互斥訪(fǎng)問(wèn)歷來(lái)是很多業(yè)務(wù)系統需要解決的問(wèn)題。在分布式系統中,通常會(huì )采用分布式鎖這一通用型解決方案。本文將就分布式鎖的實(shí)現原理、技術(shù)選型以及阿里云存儲的具體實(shí)踐進(jìn)行論述。

圖1 鎖
2 從單機鎖到分布式鎖
在單機環(huán)境中,當共享資源自身無(wú)法提供互斥能力的時(shí)候,為了防止多線(xiàn)程/多進(jìn)程對共享資源的同時(shí)讀寫(xiě)訪(fǎng)問(wèn)造成的數據破壞,就需要一個(gè)第三方提供的互斥的能力,這里往往是內核或者提供互斥能力的類(lèi)庫,如下圖所示,進(jìn)程首先從內核/類(lèi)庫獲取一把互斥鎖,拿到鎖的進(jìn)程就可以排他性的訪(fǎng)問(wèn)共享資源。演化到分布式環(huán)境,我們就需要一個(gè)提供同樣功能的分布式服務(wù),不同的機器通過(guò)該服務(wù)獲取一把鎖,獲取到鎖的機器就可以排他性的訪(fǎng)問(wèn)共享資源,這樣的服務(wù)我們統稱(chēng)為分布式鎖服務(wù),鎖也就叫分布式鎖。

圖2 單機鎖到分布式鎖
由此抽象一下分布式鎖的概念,首先分布式鎖需要是一個(gè)資源,這個(gè)資源能夠提供并發(fā)控制,并輸出一個(gè)排他性的狀態(tài),也就是:
- 鎖 = 資源 + 并發(fā)控制 + 所有權展示
- Spinlock = BOOL + CAS (樂(lè )觀(guān)鎖)
- Mutex = BOOL + CAS + 通知 (悲觀(guān)鎖)
Spinlock和Mutex都是一個(gè)Bool資源,通過(guò)原子的CAS指令:當現在為0設置為1,成功的話(huà)持有鎖,失敗的話(huà)不持有鎖,如果不提供所有權的展示,例如AtomicInteger,也是通過(guò)資源(Interger)+CAS,但是不會(huì )明確的提示所有權,因此不會(huì )被視為一種鎖,當然,可以將“所有權展示”這個(gè)更多地視為某種服務(wù)提供形式的包裝。
單機環(huán)境下,內核具備“上帝視角”,能夠知道進(jìn)程的存活,當進(jìn)程掛掉的時(shí)候可以將該進(jìn)程持有的鎖資源釋放,但發(fā)展到分布式環(huán)境,這就變成了一個(gè)挑戰,為了應對各種機器故障、宕機等,就需要給鎖提供了一個(gè)新的特性:可用性。
如下圖所示,任何提供三個(gè)特性的服務(wù)都可以提供分布式鎖的能力,資源可以是文件、KV等,通過(guò)創(chuàng )建文件、KV等原子操作,通過(guò)創(chuàng )建成功的結果來(lái)表明所有權的歸屬,同時(shí)通過(guò)TTL或者會(huì )話(huà)來(lái)保證鎖的可用性。

圖3 分布式鎖的特性和實(shí)現
3 分布式鎖的系統分類(lèi)
根據鎖資源本身的安全性,我們將分布式鎖分為兩個(gè)陣營(yíng):
- A:基于異步復制的分布式系統,例如mysql,tair,redis等;
- B:基于paxos協(xié)議的分布式一致性系統,例如zookeeper,etcd,consul等;
基于異步復制的分布式系統,存在數據丟失(丟鎖)的風(fēng)險,不夠安全,往往通過(guò)TTL的機制承擔細粒度的鎖服務(wù),該系統接入簡(jiǎn)單,適用于對時(shí)間很敏感,期望設置一個(gè)較短的有效期,執行短期任務(wù),丟鎖對業(yè)務(wù)影響相對可控的服務(wù)。
基于paxos協(xié)議的分布式系統,通過(guò)一致性協(xié)議保證數據的多副本,數據安全性高,往往通過(guò)租約(會(huì )話(huà))的機制承擔粗粒度的鎖服務(wù),該系統需要一定的門(mén)檻,適用于對安全性很敏感,希望長(cháng)期持有鎖,不期望發(fā)生丟鎖現象的服務(wù)。
4 阿里云存儲分布式鎖
阿里云存儲在長(cháng)期的實(shí)踐過(guò)程中,在如何提升分布式鎖使用時(shí)的正確性、保證鎖的可用性以及提升鎖的切換效率方面積累比較多的經(jīng)驗。
4.1 嚴格互斥性
互斥性作為分布式鎖最基本的要求,對用戶(hù)而言就是不能出現“一鎖多占”,那么存儲分布式鎖是如何避免該情況的呢?
答案是,服務(wù)端每把鎖都和唯一的會(huì )話(huà)綁定,客戶(hù)端通過(guò)定期發(fā)送心跳來(lái)保證會(huì )話(huà)的有效性,也就保證了鎖的擁有權。當心跳不能維持時(shí),會(huì )話(huà)連同關(guān)聯(lián)的鎖節點(diǎn)都會(huì )被釋放,鎖節點(diǎn)就可以被重新?lián)屨肌_@里有一個(gè)關(guān)鍵的地方,就是如何保證客戶(hù)端和服務(wù)端的同步,在服務(wù)端會(huì )話(huà)過(guò)期的時(shí)候,客戶(hù)端也能感知,如下圖所示,在客戶(hù)端和服務(wù)端都維護了會(huì )話(huà)的有效期的時(shí)間,客戶(hù)端從心跳發(fā)送時(shí)刻(S0)開(kāi)始計時(shí),服務(wù)端從收到請求(S1)開(kāi)始計時(shí),這樣就能保證客戶(hù)端會(huì )先于服務(wù)端過(guò)期。用戶(hù)在創(chuàng )建鎖之后,核心工作線(xiàn)程在進(jìn)行核心操作之前可以判斷是否有足夠的有效期,同時(shí)我們不再依賴(lài)墻上時(shí)間,而是基于系統時(shí)鐘來(lái)對時(shí)間進(jìn)行判斷,系統時(shí)鐘更加精確,且不會(huì )向前或者向后移動(dòng)(秒級別誤差毫秒級,同時(shí)在NTP跳變的場(chǎng)景,最多會(huì )修改時(shí)鐘的速率)。

圖4 存儲場(chǎng)景的使用方式
在分布式鎖互斥性上,我們是不是做到完美了?并非如此,還是存在一種情況下業(yè)務(wù)基于分布式鎖服務(wù)的訪(fǎng)問(wèn)互斥會(huì )被破壞。我們來(lái)看下面的例子:如下圖9所示,客戶(hù)端在時(shí)間點(diǎn)S0嘗試去搶鎖,在時(shí)間點(diǎn)S1在后端搶鎖成功,因此也產(chǎn)生了一個(gè)分布式鎖的有效期窗口。在有效期內,時(shí)間點(diǎn)S2做了一個(gè)訪(fǎng)問(wèn)存儲的操作,很快完成,然后在時(shí)間點(diǎn)S3判斷鎖的有效期依舊成立,繼續執行訪(fǎng)問(wèn)存儲操作,結果這個(gè)操作耗時(shí)良久,超過(guò)了分布式鎖的過(guò)期時(shí)間,那么可能這個(gè)時(shí)候,分布式鎖已經(jīng)被其他客戶(hù)端搶占成功,進(jìn)而出現兩個(gè)客戶(hù)端同時(shí)操作同一批數據的可能性,這種可能性是存在的,雖然概率很小。

圖6 越界場(chǎng)景
針對這個(gè)場(chǎng)景,具體的應對方案是在操作數據的時(shí)候確保有足夠的鎖有效期窗口,當然如果業(yè)務(wù)本身提供回滾機制的話(huà),那么方案就更加完備,該方案也在存儲產(chǎn)品使用分布式鎖的過(guò)程中被采用。
還有一個(gè)更佳的方案,即,存儲系統本身引入IO Fence能力。這里就不得不提Martin Kleppmann和redis的作者antirez之間的討論了,redis為了防止異步復制導致的鎖丟失的問(wèn)題,引入redlock,該方案引入了多數派的機制,需要獲得多數派的鎖,最大程度的保證了可用性和正確性,但仍然有兩個(gè)問(wèn)題:
- 墻上時(shí)間的不可靠(NTP時(shí)間)
- 異構系統的無(wú)法做到嚴格正確性
墻上時(shí)間可以通過(guò)非墻上時(shí)間MonoticTime來(lái)解決(redis目前仍然依賴(lài)墻上時(shí)間),但是異構系統的只有一個(gè)系統并沒(méi)有辦法保證完全正確,如下圖10所示,Client1獲取了鎖,在操作數據的時(shí)候發(fā)生了GC,在GC完成時(shí)候丟失了鎖的所有權,造成了數據不一致。

圖7 異構系統無(wú)法做到完全正確性
因此需要兩個(gè)系統同時(shí)協(xié)作來(lái)完成一個(gè)完全正確的互斥訪(fǎng)問(wèn),在存儲系統引入IO Fence能力,如下圖11所示,全局鎖服務(wù)提供全局自增的token,Client1拿到鎖返回的token是33,并帶入存儲系統,發(fā)生GC,當Client2搶鎖成功返回34,帶入存儲系統,存儲系統會(huì )拒絕token較小的請求,那么經(jīng)過(guò)了長(cháng)時(shí)間full gc重新恢復后的Client 1再次寫(xiě)入數據的時(shí)候,因為存儲層記錄的Token已經(jīng)更新,攜帶token值為33的請求將被直接拒絕,從而達到了數據保護的效果(chubby的論文中有講述,也是Martin Kleppmann提出的解決方案)。

圖8 引入IO Fence能力
這與阿里云分布式存儲平臺盤(pán)古的設計思路不謀而合,盤(pán)古支持了類(lèi)似IO Fence的寫(xiě)保護能力,引入Inline File的文件類(lèi)型,配合Seal File操作,這就有著(zhù)類(lèi)似IO Fence的寫(xiě)保護能力,首先,SealFile操作用來(lái)關(guān)閉已經(jīng)打開(kāi)的cs上面的文件,防止舊的Owner繼續寫(xiě)數據;其次,InlineFile可以防止舊的Owner打開(kāi)新的文件。這兩個(gè)功能事實(shí)上也是提供了存儲系統中的Token支持。
4.2 可用性
存儲分布式鎖通過(guò)持續心跳來(lái)保證鎖的健壯性,讓用戶(hù)不用投入很多精力關(guān)注可用性,但也有可能異常的用戶(hù)進(jìn)程持續占據鎖。針對該場(chǎng)景,為了保證鎖最終可以被調度,提供了可以安全釋放鎖的會(huì )話(huà)加黑機制。
當用戶(hù)需要將發(fā)生假死的進(jìn)程持有的鎖釋放時(shí),可以通過(guò)查詢(xún)會(huì )話(huà)信息,并將會(huì )話(huà)加黑,此后,心跳將不能正常維護,最終導致會(huì )話(huà)過(guò)期,鎖節點(diǎn)被安全釋放。這里我們不是強制刪除鎖,而是選用禁用心跳的原因如下:
刪除鎖操作本身不安全,如果鎖已經(jīng)被其他人正常搶占,此時(shí)刪鎖請求會(huì )產(chǎn)生誤刪除。
b.刪除鎖后,持有鎖的人會(huì )話(huà)依然正常,它仍然認為自己持有鎖,會(huì )打破鎖的互斥性原則。
4.3 切換效率
當進(jìn)程持有的鎖需要被重新調度時(shí),持有者可以主動(dòng)刪除鎖節點(diǎn),但當持有者發(fā)生異常(如進(jìn)程重啟,機器宕機等),新的進(jìn)程要重新?lián)屨迹托枰却鹊臅?huì )話(huà)過(guò)期后,才有機會(huì )搶占成功。默認情況下,分布式鎖使用的會(huì )話(huà)生命期為數十秒,當持有鎖的進(jìn)程意外退出后(未主動(dòng)釋放鎖),最長(cháng)需要經(jīng)過(guò)很長(cháng)時(shí)間鎖節點(diǎn)才可以被再次搶占。

期時(shí)間
要提升切換精度,本質(zhì)上要壓縮會(huì )話(huà)生命周期,同時(shí)也意味著(zhù)更快的心跳頻率,對后端更大的訪(fǎng)問(wèn)壓力。我們通過(guò)對進(jìn)行優(yōu)化,使得會(huì )話(huà)周期可以進(jìn)一步壓縮。
同時(shí)結合具體的業(yè)務(wù)場(chǎng)景,例如守護進(jìn)程發(fā)現鎖持有進(jìn)程掛掉的場(chǎng)景,提供鎖的CAS釋放操作,使得進(jìn)程可以零等待進(jìn)行搶鎖。比如利用在鎖節點(diǎn)中存放進(jìn)程的唯一標識,強制釋放已經(jīng)不再使用的鎖,并重新?tīng)帗專(zhuān)摲绞娇梢詮氐妆苊膺M(jìn)程升級或意外重啟后搶鎖需要的等待時(shí)間。
5 結語(yǔ)
阿里云存儲提供了完整的分布式鎖解決方案,經(jīng)過(guò)了阿里云眾多云產(chǎn)品寶貴的業(yè)務(wù)場(chǎng)景中長(cháng)期錘煉,穩定高可靠,且提供了多種語(yǔ)言的SDK選擇,甚至是RESTful集成方案。
分布式鎖提供了分布式環(huán)境下共享資源的互斥訪(fǎng)問(wèn),業(yè)務(wù)或者依賴(lài)分布式鎖追求效率提升,或者依賴(lài)分布式鎖追求訪(fǎng)問(wèn)的絕對互斥。同時(shí),在接入分布式鎖服務(wù)過(guò)程中,要考慮接入成本、服務(wù)可靠性、分布式鎖切換精度以及正確性等問(wèn)題,正確和合理的使用分布式鎖,是需要持續思考并予以?xún)?yōu)化的。