分佈式系統:Lamport 邏輯時鐘

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

什麼是邏輯時鐘

邏輯時鐘是爲了區分現實中的物理時鐘提出來的概念,通常狀況下咱們提到的時間都是指物理時間,但實際上不少應用中,只要全部機器有相同的時間就夠了,這個時間不必定要跟實際時間相同。更進一步,若是兩個節點之間不進行交互,那麼它們的時間甚至都不須要同步。所以問題的關鍵點在於節點間的交互要在事件的發生順序上達成一致,而不是對於時間達成一致。算法

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

爲何須要邏輯時鐘

時間是在現實生活中是很重要的概念,有了時間咱們就能比較事情發生的前後順序。若是是單個計算機內執行的事務,因爲它們共享一個計時器,因此可以很容易經過時間戳來區分前後。同理在分佈式系統中也經過時間戳的方式來區分前後行不行?架構

答案是NO,由於在分佈式系統中的不一樣節點間保持它們的時鐘一致是一件不容易的事情。由於每一個節點的CPU都有本身的計時器,而不一樣計時器之間會產生時間偏移,最終致使不一樣節點上面的時間不一致。也就是說若是A節點的時鐘走的比B節點的要快1分鐘,那麼即便B先發出的消息(附帶B的時間戳),A的消息(附帶A的時間戳)在後一秒發出,A的消息也會被認爲先於B發生。併發

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

所以,Lamport提出邏輯時鐘就是爲了解決分佈式系統中的時序問題,即如何定義a在b以前發生。 值得注意的是,並非說分佈式系統只能用邏輯時鐘來解決這個問題,若是之後有某種技術可以讓不一樣節點的時鐘徹底保持一致,那麼使用物理時鐘來區分前後是一個更簡單有效的方式。分佈式

如何實現邏輯時鐘

時序關係與相對論

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

上面的結論跟狹義相對論有殊途同歸之妙,在狹義相對論中,不一樣觀察者在同一參考系中觀察到的事件前後順序是一致的,可是在不一樣的觀察者在不一樣的參考系中對兩個事件誰先發生可能具備不一樣的見解。當且僅當事件A是由事件B引發的時候,事件A和B之間才存在一個前後關係。兩個事件能夠創建因果關係的前提是:兩個事件之間能夠用等於或小於光速的速度傳遞信息。 值得注意的是這裏的因果關係指的是時序關係,即時間的先後,並非邏輯上的緣由和結果。cdn

那麼是否咱們能夠參考狹義相對論來定義分佈式系統中兩個事件的時序呢?在分佈式系統中,網絡是不可靠的,因此咱們去掉能夠速度的約束,能夠獲得兩個事件能夠創建因果(時序)關係的前提是:兩個事件之間是否發生過信息傳遞。 在分佈式系統中,進程間通訊的手段(共享內存、消息發送等)都屬於信息傳遞,若是兩個進程間沒有任何交互,實際上他們之間內部事件的時序也可有可無。可是有交互的狀況下,特別是多個節點的要保持同一副本的狀況下,事件的時序很是重要。htm

Lamport 邏輯時鐘

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

邏輯時鐘定義

Clock Condition.對於任意事件a, b:若是a \to b\to表示a先於b發生),那麼C(a)< C(b), 反之否則, 由於有多是併發事件 C1.若是ab都是進程P_i裏的事件,而且ab以前,那麼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 \to b,則有C(a) < C(b),例如圖1中有 C1 \to B1,那麼 C(C1) < C(B1)。經過該定義,事件集中Lamport時間戳不等的事件可進行比較,咱們得到事件的偏序關係(partial order)。注意:若是C(a) < C(b),並不能說明a \to b,也就是說C(a) < C(b)a \to 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 = 一、B_j = 二、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_iP_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提出的邏輯時鐘能夠說是分佈式一致性算法的開山鼻祖,後續的全部分佈式算法都有它的影子。咱們不能想象現實世界中沒有時間,而邏輯時鐘定義了分佈式系統裏面的時間概念,解決了分佈式系統中區分事件發生的時序問題。

參考資料

相關文章
相關標籤/搜索