分散式系統:時間、時鐘和事件序列
在程式中,我們經常需要知道事件序列,在單體應用中,事件序列是較為簡單的,最簡單的辦法就是用時間戳,但在分散式系統中,事件序列是很困難的,Leslie Lamport大神在論文Time, Clocks, and the Ordering of Events in a Distributed System討論了在分散式系統中時間、時鐘和事件序列的問題。
【1】分散式系統中物理時鐘存在的問題
邏輯時鐘是相對物理時鐘這個概念的,為什麼要提出邏輯時鐘,因為物理時鐘在分散式系統中存在一系列問題。在一臺機器上的多個程序可以從一個物理時鐘中獲取時間戳,不管這個物理時鐘是否準確,只要是從一個物理時鐘獲取時間戳,我們都能獲得多個事件的相對時間順序。但是在分散式系統中,我們無法從一個物理時鐘獲取時間戳,只能從各自機器上物理時鐘獲取時間戳,而各臺機器的物理時鐘是很難完全同步的,即使有NTP,精度也是有限的。所以在分散式系統中,是不能通過物理時鐘決定事件序列的。
物理時鐘在分散式系統中也不是毫無用處,至少它一定程度上可以判斷在一臺機器上的事件順序,同時分散式系統中還是有必要讓不同機器上的物理時鐘在一定精度內同步時間的,只是不作為決定事件序列的方法。
【2】偏序(Partial Ordering)
事件序列有兩種:偏序事件序列和全序事件序列。所謂的偏序指的是隻能為系統中的部分事件定義先後順序。這裡的部分其實是有因果關係的事件。在論文Time, Clocks, and the Ordering of Events in a Distributed System中,偏序是由“happened before”引出的,我們先看一下"happened before"(表示為“->
Definition. The relation "->
"on the set of events of a system is the smallest relation satisfying the following three conditions:
(1) If a
and b
are events in the same process, and a
comes before b
, then a->b
.
(2) If a
is the sending of a message by one process and b
is the receipt of the same message by another process, then a->b
(3) If a->b
and b->c
then a->c
.
在分散式系統中,只有兩個發生關聯的事件(有因果關係),我們才會去關心兩者的先來後到關係。對於併發事件,他們兩個誰先發生,誰後發生,其實我們並不關心。偏序就是用來定義兩個因果事件的發生次序,即‘happens before’。而對於併發事件(沒有因果關係),並不能決定其先後,所以說這種‘happens before’的關係,是一種偏序關係。
If two entities do not exchange any messages, then they probably do not need to share a common clock; events occurring on those entities are termed as concurrent events.”
【3】邏輯時鐘
論文原文中有這樣一句:We begin with an abstract point of view in which a clock is just a way of assigning a number to an event, where the number is thought of as the time at which the event occurred. 這句話的意思是,可以把時間進行抽象,把時間值看成是事件發生順序的一個序列號,這個值可以<20190515,20190516,20190517>
,也可以是<1,2,3>
。後面就有了邏輯時鐘的概念。定義如下:
we define a
clock Ci
for each process Pi
to be a function which assigns a number Ci(a)
to any event a
in that process.
Clock Condition. For any events a,b
: if a->b
then C(a) < C(b)
.
C1. If a
and b
are events in process Pi
, and a
comes before b
, then Ci(a) < Ci(b)
.
C2. If a
is the sending of a message by process Pi
and b
is the receipt of that message by process Pi
, then Ci(a) < Ci(b)
.
具體的,根據上面的定義條件,我們做如下實現規則:
- 每個事件對應一個Lamport時間戳,初始值為0
- 如果事件在節點內發生,本地程序中的時間戳加1
- 如果事件屬於傳送事件,本地程序中的時間戳加1並在訊息中帶上該時間戳
- 如果事件屬於接收事件,本地程序中的時間戳 = Max(本地時間戳,訊息中的時間戳) + 1
根據上面的定義,我們知道a->b
,C(a)<C(b)
,但如果C(a)=C(b)
,那麼a,b
是什麼順序呢?它們肯定不是因果關係,所以它們之間的先後其實並不會影響結果,我們這裡只需要給出一種確定的方式來定義它們之間的先後就能得到全序關係。
一種可行的方式是利用給程序編號,利用程序編號的大小來排序。假設a、b
分別在節點P、Q
上發生,Pi、Qj
分別表示我們給P、Q
的編號,如果 C(a)=C(b)
並且 Pi<Qj
,同樣定義為a
發生在b
之前,記作 a⇒b
(全序關係)。假如我們上圖的A、B、C
分別編號Ai=1、Bj=2、Ck=3
,因 C(B4)=C(C3)
並且 Bj<Ck
,則 B4⇒C3
。
通過以上定義,我們可以對所有事件排序,獲得事件的全序關係(total order)。上圖例子,我們可以進行排序:C1⇒B1⇒B2⇒A1⇒B3⇒A2⇒C2⇒B4⇒C3⇒A3⇒B5⇒C4⇒C5⇒A4
。觀察上面的全序關係你可以發現,從時間軸來看B5
是早於A3
發生的,但是在全序關係裡面我們根據上面的定義給出的卻是A3
早於B5
,這是因為Lamport邏輯時鐘只保證因果關係(偏序)的正確性,不保證絕對時序的正確性。
【4】嘗試用邏輯時鐘解決分散式鎖的問題
單機多程序程式可由鎖進行同步,那是因為這些程序都執行在作業系統上,有center為它們的請求排序,這個center知道所有需要進行同步的程序的所有資訊。但是在分散式系統中,各個程序執行在各自的主機上,沒有一個center的概念,那分散式系統中多程序該怎麼進行同步呢?或者說分散式鎖該怎麼實現呢?論文中提出瞭解決這一問題的演算法要滿足下面三個條件:
(I) A process which has been granted the resource must release it before it can be granted to another process.
(II) Different requests for the resource must be granted in the order in which they are made.
(III) If every process which is granted the resource eventually releases it, then every request is eventually granted.
為了簡化問題,我們做如下假設:
- 任何兩個程序
Pi
,Pj
它們之間接收到的訊息的順序與傳送訊息的順序一致,並且每個訊息一定能夠被接收到。 - 每個程序都維護一個不被其他程序所知的請求佇列。並且請求佇列初始化為包含一個
T0:P0
請求,P0
用於該共享資源,T0
是初始值小於任何時鐘值
演算法如下:
- To request the resource, process
Pi
sends the messageTm:Pi
requests resource to every other process, and puts that message on its request queue, whereTm
is the timestamp of the message.(請求資源,傳送請求給其他程序,在自己的請求佇列中新增該請求) - When process
Pj
receives the messageTm:Pi
requests resource, it places it on its request queue and sends a (timestamped) acknowledgment message toPi
.(收到其他程序的請求,放到請求佇列中,迴應發起請求的程序) - To release the resource, process
Pi
removes anyTm:Pi
requests resource message from its request queue and sends a (timestamped)Pi
releases resource message to every other process.(釋放資源,從請求佇列中移除該資源請求,傳送給其他程序,告訴它們我釋放了該資源) - When process
Pj
receives aPi
releases resource message, it removes anyTm:Pi
requests resource message from its request queue.(收到其他程序釋放資源的訊息,從請求佇列中移除該資源請求) - Process
Pi
granted the resource when the following two conditions are satisfied: (i) There is aTm:Pi
requests resource message in its request queue which is ordered before any other request in its queue by the relation⇒
. (ii)Pi
has received a message from every other process timestamped later thanTm
.
(判斷自己是否可以獲得該資源,有兩個條件:其一,按全序排序後,Tm:Pi
請求在請求佇列的最前面;其二,自己Pi
已經收到了所有其他程序的時戳大於Tm
的訊息)
下面我們舉個例子說明上面的演算法過程:
初始狀態為P0
擁有資源,請求佇列中為0:0
(T0:P0
的簡寫),而後P1
請求資源,將1:1
新增到請求佇列中,此時P0
讓佔有資源,P1
還無法獲取資源,等到P0
釋放資源後,0:0
從請求佇列中移除(下圖中沒有畫出),此時請求佇列中1:1
的請求在最前面,同時P1
收到了其他兩個程序的大於1
的迴應訊息,滿足了佔有資源的條件,此時P1
佔有資源。
其實關鍵思想很簡單,既然分散式系統中沒有“center”的概念,那我請求共享資源時我就讓其他所有程序都知道我要請求該資源,擁有資源的程序釋放資源時也告訴所有程序,我要釋放該資源,想請求該資源的你們可以按序(邏輯時鐘的作用,這裡再次說明一下,並不能保證在絕對物理時間上請求的排序)請求了。這樣每個程序都知道其他程序的狀態,就相當於有個“center”。
對於分散式鎖問題,多個請求不一定是一定按照絕對物理時鐘排序才可以,只要我們有這樣一個演算法,這個演算法可以保證多個程序的請求按照這個演算法總能得到同一個排序,就可以了,按照絕對物理時鐘排序只是其中一個可行的演算法。
到這裡是否就萬事大吉了呢,其實並沒有,這個實現是很脆弱的,它要求所有程序都非常可靠,一旦一個程序掛了或出現網路分割槽的情況,是無法工作的,同時我們提出的網路要求也非常嚴格,要求發出的訊息一定被接收到,這個在實用的系統中是很難做到的。所以這是一個理想狀況下的演算法實現,並不是一個可以工業級應用的演算法實現。但它仍然是非常有意義的,給了我們關於分散式系統中解決一致性、共識演算法等思想啟迪。
參考文件:
大神論文:Time, Clocks, and the Ordering of Events in a Distributed System
Lamport timestamps
分散式系統:Lamport 邏輯時鐘
關注微信公眾號,每天進步一點點!