分散式系統:Lamport邏輯時鐘

風馬蕭蕭發表於2019-02-01

分散式系統解決了傳統單體架構的單點問題和效能容量問題,另一方面也帶來了很多的問題,其中一個問題就是多節點的時間同步問題:不同機器上的物理時鐘難以同步,導致無法區分在分散式系統中多個節點的事件時序。1978年Lamport在《Time, Clocks and the Ordering of Events in a Distributed System》中提出了邏輯時鐘的概念,來解決分散式系統中區分事件發生的時序問題。

什麼是邏輯時鐘

邏輯時鐘是為了區分現實中的物理時鐘提出來的概念,一般情況下我們提到的時間都是指物理時間,但實際上很多應用中,只要所有機器有相同的時間就夠了,這個時間不一定要跟實際時間相同。更進一步,如果兩個節點之間不進行互動,那麼它們的時間甚至都不需要同步。 因此問題的關鍵點在於節點間的互動要在事件的發生順序上達成一致,而不是對於時間達成一致。

綜上, 邏輯時鐘指的是分散式系統中用於區分事件的發生順序的時間機制。 從某種意義上講,現實世界中的物理時間其實是邏輯時鐘的特例。

為什麼需要邏輯時鐘

時間是在現實生活中是很重要的概念,有了時間我們就能比較事情發生的先後順序。如果是單個計算機內執行的事務,由於它們共享一個計時器,所以能夠很容易通過時間戳來區分先後。同理在分散式系統中也通過時間戳的方式來區分先後行不行?

答案是NO,因為在分散式系統中的不同節點間保持它們的時鐘一致是一件不容易的事情。因為每個節點的CPU都有自己的計時器,而不同計時器之間會產生時間偏移,最終導致不同節點上面的時間不一致。也就是說如果A節點的時鐘走的比B節點的要快1分鐘,那麼即使B先發出的訊息(附帶B的時間戳),A的訊息(附帶A的時間戳)在後一秒發出,A的訊息也會被認為先於B發生。

那麼是否可以通過某種方式來同步不同節點的物理時鐘呢?答案是有的,NTP就是常用的時間同步演算法,但是即使通過演算法進行同步,總會有誤差,這種誤差在某些場景下(金融分散式事務)是不能接受的。

因此, Lamport提出邏輯時鐘就是為了解決分散式系統中的時序問題,即如何定義a在b之前發生。值得注意的是,並不是說分散式系統只能用邏輯時鐘來解決這個問題,如果以後有某種技術能夠讓不同節點的時鐘完全保持一致,那麼使用物理時鐘來區分先後是一個更簡單有效的方式。

如何實現邏輯時鐘

時序關係與相對論

通過前面的討論我們知道通過物理時鐘(即絕對參考系)來區分先後順序的前提是所有節點的時鐘完全同步,但目前並不現實。因此,在沒有絕對參考系的情況下,在一個分散式系統中,你無法判斷事件A是否發生在事件B之前,除非A和B存在某種依賴關係,即分散式系統中的事件僅僅是部分有序的。

上面的結論跟狹義相對論有異曲同工之妙,在狹義相對論中,不同觀察者在同一參考系中觀察到的事件先後順序是一致的,但是在不同的觀察者在不同的參考系中對兩個事件誰先發生可能具有不同的看法。當且僅當事件A是由事件B引起的時候,事件A和B之間才存在一個先後關係。 兩個事件可以建立因果關係的前提是:兩個事件之間可以用等於或小於光速的速度傳遞資訊。 值得注意的是這裡的因果關係指的是時序關係,即時間的前後,並不是邏輯上的原因和結果。

那麼是否我們可以參考狹義相對論來定義分散式系統中兩個事件的時序呢?在分散式系統中,網路是不可靠的,所以我們去掉 可以速度 的約束,可以得到 兩個事件可以建立因果(時序)關係的前提是:兩個事件之間是否發生過資訊傳遞。在分散式系統中,程式間通訊的手段(共享記憶體、訊息傳送等)都屬於資訊傳遞,如果兩個程式間沒有任何互動,實際上他們之間內部事件的時序也無關緊要。但是有互動的情況下,特別是多個節點的要保持同一副本的情況下,事件的時序非常重要。

Lamport 邏輯時鐘

分散式系統中按是否存在節點互動可分為三類事件,一類發生於節點內部,二是傳送事件,三是接收事件。注意: 以下文章中提及的時間戳如無特別說明,都指的是Lamport 邏輯時鐘的時間戳,不是物理時鐘的時間戳

邏輯時鐘定義

Clock Condition.對於任意事件$a$, $b$:如果$a o b$($ o$表示a先於b發生),那麼$C(a)< C(b)$, 反之不然, 因為有可能是併發事件
C1.如果$a$和$b$都是程式$P_i$裡的事件,並且$a$在$b$之前,那麼$C_i(a) < C_i(b)$
C2.如果$a$是程式$P_i$裡關於某訊息的傳送事件,$b$是另一程式$P_j$裡關於該訊息的接收事件,那麼$C_i(a) < C_j(b)$

Lamport 邏輯時鐘原理如下:
圖1

  1. 每個事件對應一個Lamport時間戳,初始值為0
  2. 如果事件在節點內發生,本地程式中的時間戳加1
  3. 如果事件屬於傳送事件,本地程式中的時間戳加1並在訊息中帶上該時間戳
  4. 如果事件屬於接收事件,本地程式中的時間戳 = Max(本地時間戳,訊息中的時間戳) + 1

假設有事件$a、b,C(a)、C(b)$分別表示事件$a、b$對應的Lamport時間戳,如果$a$發生在$b$之前(happened before),記作 $a o b$,則有$C(a) < C(b)$,例如圖1中有 $C1 o B1$,那麼 $C(C1) < C(B1)$。通過該定義,事件集中Lamport時間戳不等的事件可進行比較,我們獲得事件的偏序關係(partial order)。注意:如果$C(a) < C(b)$,並不能說明$a o b$,也就是說$C(a) < C(b)$是$a o b$的必要不充分條件

如果$C(a) = C(b)$,那$a、b$事件的順序又是怎樣的?值得注意的是當$C(a) = C(b)$的時候,它們肯定不是因果關係,所以它們之間的先後其實並不會影響結果,我們這裡只需要給出一種確定的方式來定義它們之間的先後就能得到全序關係。 注意:Lamport邏輯時鐘只保證因果關係(偏序)的正確性,不保證絕對時序的正確性。

一種可行的方式是利用給程式編號,利用程式編號的大小來排序。假設$a、b$分別在節點$P、Q$上發生,$P_i、Q_j$分別表示我們給$P、Q$的編號,如果 $C(a) = C(b)$ 並且 $P_i < Q_j$,同樣定義為$a$發生在$b$之前,記作 $a Rightarrow b$(全序關係)。假如我們對圖1的$A、B、C$分別編號$A_i = 1、B_j = 2、C_k = 3$,因 $C(B4) = C(C3)$ 並且 $B_j < C_k$,則 $B4 Rightarrow C3$。

通過以上定義,我們可以對所有事件排序,獲得事件的全序關係(total order)。上圖例子,我們可以進行排序:$C1 Rightarrow B1 Rightarrow B2 Rightarrow A1 Rightarrow B3 Rightarrow A2 Rightarrow C2 Rightarrow B4 Rightarrow C3 Rightarrow A3 Rightarrow B5 Rightarrow C4 Rightarrow C5 Rightarrow A4$

觀察上面的全序關係你可以發現,從時間軸來看$B5$是早於$A3$發生的,但是在全序關係裡面我們根據上面的定義給出的卻是$A3$早於$B5$,可以發現Lamport邏輯時鐘是一個正確的演算法,即有因果關係的事件時序不會錯,但並不是一個公平的演算法,即沒有因果關係的事件時序不一定符合實際情況。

如何使用邏輯時鐘解決分散式鎖問題

上面的分析過於理論,下面我們來嘗試使用邏輯時鐘來解決分散式鎖問題。

分散式鎖問題本質上是對於共享資源的搶佔問題,我們先對問題進行定義:

  1. 已經獲得資源授權的程式,必須在資源分配給其他程式之前釋放掉它;
  2. 資源請求必須按照請求發生的順序進行授權;
  3. 在獲得資源授權的所有程式最終釋放資源後,所有的資源請求必須都已經被授權了。

首先我們假設, 對於任意的兩個程式$P_i$和$P_j$,它們之間傳遞的訊息是按照傳送順序被接收到的, 並且所有的訊息最終都會被接收到。
每個程式會維護一個它自己的對其他所有程式都不可見的請求佇列。我們假設該請求佇列初始時刻只有一個訊息$(T_0:P_0)$資源請求,$P_0$代表初始時刻獲得資源授權的那個程式,$T_0$小於任意時鐘初始值

  1. 為請求該項資源,程式$P_i$傳送一個$(T_m:P_i)$資源請求(請求鎖)訊息給其他所有程式,並將該訊息放入自己的請求佇列,在這裡$T_m$代表了訊息的時間戳
  2. 當程式$P_j$收到$(T_m:P_i)$資源請求訊息後,將它放到自己的請求佇列中,併傳送一個帶時間戳的確認訊息給$P_i$。(注:如果$P_j$已經傳送了一個時間戳大於$T_m$的訊息,那就可以不傳送)
  3. 釋放該項資源(釋放鎖)時,程式$P_i$從自己的訊息佇列中刪除所有的$(T_m:P_i)$資源請求,同時給其他所有程式傳送一個帶有時間戳的$P_i$資源釋放訊息
  4. 當程式$P_j$收到$P_i$資源釋放訊息後,它就從自己的訊息佇列中刪除所有的$(T_m:P_i)$資源請求
  5. 當同時滿足如下兩個條件時,就將資源分配(鎖佔用)給程式$P_i$:

    • 按照全序關係排序後,$(T_m:P_i)$資源請求排在它的請求佇列的最前面
    • $i$已經從所有其他程式都收到了時間戳>$T_m$的訊息、

下面我會用圖例來說明上面演算法運作的過程,假設我們有3個程式,根據演算法說明,初始化狀態各個程式佇列裡面都是(0:0)狀態,此時鎖屬於P0。
初始狀態

接下來P1會發出請求資源的訊息給所有其他程式,並且放到自己的請求佇列裡面,根據邏輯時鐘演算法,P1的時鐘走到1,而接受訊息的P0和P2的時鐘為訊息時間戳+1。
請求資源

收到P1的請求之後,P0和P2要傳送確認訊息給P1表示自己收到了。注意,由於目前請求佇列裡面第一個不是P1發出的請求,所以此時鎖仍屬於P0。但是由於收到了確認訊息,此時P1已經滿足了獲取資源的第一個條件: P1已經收到了其他所有程式時間戳大於1的訊息。
返回確認

假設P0此時釋放了鎖(這裡為了方便演示做了這個假設,實際上P0什麼時候釋放資源都可以,演算法都是正確的,讀者可自行推導),傳送釋放資源的訊息給P1和P2,P1和P2收到訊息之後把請求(0:0)從佇列裡面刪除。
釋放資源

當P0釋放了資源之後,我們發現P1滿足了獲取資源的兩個條件: 它的請求在佇列最前面;P1已經收到了其他所有程式時間戳大於1的訊息。也就是說此時P1就獲取到了鎖。

值得注意的是,這個演算法並不是容錯的,有一個程式掛了整個系統就掛了,因為需要等待所有其他程式的響應,同時對網路的要求也很高。

總結

如果你之前看過2PC,Paxos之類的演算法,相信你看到最後一定會有一種似曾相識的感覺。實際上,Lamport提出的邏輯時鐘可以說是分散式一致性演算法的開山鼻祖,後續的所有分散式演算法都有它的影子。我們不能想象現實世界中沒有時間,而邏輯時鐘定義了分散式系統裡面的時間概念,解決了分散式系統中區分事件發生的時序問題。

參考資料


相關文章