分布式初探 判斷因果關係的向量時鐘演算法

2022-01-11 03:16:10 字數 4320 閱讀 3711

今天的文章來聊聊向量時鐘,在前文介紹分布式系統一致性的時候,曾經介紹過,在弱一致性模型當中會有乙個因果性的問題。向量時鐘演算法正是設計出來解決因果關係問題的。

我們來回顧一下因果問題,在實際日常的網頁行為當中,部分行為存在因果關係。比方說知乎裡面回答問題,顯然得先有乙個同學提出問題,然後才能有各路大v謝邀解答問題。但是由於是分布式系統,有可能問題和回答並不是存放在同一臺機器,導致有可能它們更新的順序不一致,所以就有可能會出現使用者在訪問知乎的時候,發現自己關注的大v回答了某個問題,但是點進去問題卻是空的。這種幽靈情況不是靈異事件,只是單純的分布式系統設計沒過關,沒有考慮因果問題。

有的同學可以會說,這個不難啊,我們加入時間戳啊。時間戳的確可以解決一部分問題,但是並不能解決所有問題。有了時間戳之後,我們可以獲得事件發生的時間,但是仍然不知道不同的資料之間的因果關係。由於分布式系統存在延遲,也不能簡單地通過時間戳來做過濾或者篩選。不過,雖然單純的時間戳不行,但已經非常接近了。

我們日常生活當中用事件發生的時間來反應事物發生的順序,我們說的先後順序,其實是以客觀上的時間作為參考係參考得到的結果。問題來了,我們能不能找到或者構造出其他的參考係來反應事物發生的順序呢?

當然是可以的,不然也沒有這篇文章了,這就是大名鼎鼎的邏輯時鐘演算法。多說一句,邏輯時鐘演算法和許多其他分布式演算法一樣,同樣源於大神lamport的發明。

我們還用之前的例子來思考一下,乙個人在知乎提交了問題,另乙個人回答了問題,這是兩個事件。我們第一反應自然是通過兩個事件發生的時間來反應因果順序,但我們仔細分析一下這個場景。後面那個人既然能回答問題,說明他一定是看到了問題。也就是說回答問題和看到問題之間發生了互動,所以很自然地可以想到,我們是不是可以用兩個系統或者是兩個事件之間有沒有發生過資訊互動來反應因果順序呢?

於是基於這個思想,lamport大神提出了邏輯時鐘的概念。邏輯時鐘概念的核心就是剛才我們說的,兩個事件之間建立因果關係的前提是,兩個事件之間發生過資訊傳遞

我們梳理一下可能發生的事件的種類,可以分成三種。第一種是發生在某個節點內部,也就是說沒有和其他任何節點發生聯絡。第二種是傳送事件,是事件的傳送方。第三種是接收事件,和第二種對應,是事件的接收方。明確了這三點之後,我們就可以用時間戳來表示這三種情況了。首先,我們假設每個節點內部都會維護乙個時間戳,記錄當下節點的狀態。

針對上面說是三種時序關係呢,我們設定三種策略。

首先是內部事件,對於節點內部發生事件呢,很簡單,我們只需要將它的時間戳增大1,表示發生過了某件事情。

其次是傳送事件,節點內部的時間戳自增1,並且在傳送訊息當中加上這個時間戳。

最後是接收事件,由於會額外置收到乙個時間戳,所以我們需要利用這個時間戳來更新節點內部的時間戳。更新的方法也很簡單,假設節點內部的時間戳是\(t\),跟著訊息傳遞而來的時間戳是\(t'\), 那麼: \(t_ = max(t + 1, t')\)。

我們分析一下上面這個關係,假設當下有事件a和b,如果事件a是事件b發生的前提。那麼顯然事件a的時間戳小於事件b。如果反過來,事件a的時間戳小於b,能說明事件a是事件b的前提嗎?並不能,所以時間戳較小是因果關係的必要條件,但不是充分條件

由於會存在多個節點或者程序時間戳相等的情況,所以我們把程序id也作為比較的銀子。我們用c表示乙個事件的時間戳,p表示事件的程序pid。如果事件a排在事件b前面,只有兩種可能:

\(c(a) < c(b)\)

\(c(a) = c(b), p(a) < p(b)\)

我們來看乙個例子:

上圖當中有a、b和c三個程序,其中p(a) < p(b) < p(c)。圖中每乙個箭頭都代表傳遞的訊息

我們根據重新定義的時序關係,可以得到這些點的先後順序是:

c1⇒b1⇒b2⇒a1⇒b3⇒a2⇒c2⇒b4⇒c3⇒a3⇒b5⇒c4⇒c5⇒a4

如果仔細觀察這條鏈的話會發現它並不能真實反映事件發生的順序,會存在不公平的情況。但至少因果關係可以保證。

以上這個演算法被稱為是邏輯時鐘演算法,它相當於重新定義了乙個邏輯上的時間來代替真實物理世界的時間。由於它是lamport大神提出的,所以也被稱為是lamport邏輯時鐘演算法

在上面的文章當中我們也分析了,邏輯時鐘演算法有乙個問題是雖然保證了因果順序,但也犧牲了公平。比如上圖當中b3和a2發生在同一時間,但是b3排在a2的前面。也就是說我們通過比較\(c(a)\)和\(c(b)\)無法得出真實的發生順序。

為了解決這個問題,大神們在lamport的邏輯時鐘上做了改進,提出了向量時鐘演算法

在事件的處理上,向量時鐘演算法和邏輯時鐘基本一致。

在程序i發生事件(接收、傳送或者是內部事件)的時候,\(c_i[i] += 1\),這時候c是乙個時間戳向量,i是程序i的下標。

當程序i傳送訊息的時候,會將訊息和自己的時鐘向量一同發出。

當程序i收到程序j傳送來的訊息時,會根據一起傳送來的時鐘向量更新自己的時鐘向量:\(c_i[k] = max(c_i[k], c_j[k]), k = 1, 2, \cdots, n\)

同樣,由於單個時間戳換成了向量時鐘,所以我們判斷因果順序的方式也需要變化。在向量時鐘演算法當中,我們定義如果事件a在事件b之前,那麼需要滿足兩個條件:

對於所有的下標k,都有\(c_a[k] \leq c_b[k]\)

存在下標\(k_0\),使得\(c_a[k_0] < c_b[k_0]\)

也就是說至少需要乙個維度存在嚴格小於,其他維度全部小於等於,才可以看做是因果關係。原因也很簡單,因為如果存在訊息傳遞,那麼至少有乙個維度會帶來增加。如果兩個事件的向量時鐘相等,說明兩者是沒有發生過資訊傳遞的,自然也就不符合我們定義的因果關係。

我們回顧一下之前的例子,將節點改寫成向量時鐘之後,得到的結果如下圖:

將邏輯時鐘優化成向量時鐘之後,就可以嚴格判斷因果關係了。如果兩個節點的時鐘向量沒有大小關係,那麼可以說明這兩個事件之間沒有聯絡。

和我們之前介紹的一樣,向量時鐘演算法主要用在分布式系統的因果關係的檢測上。而因果關係之所以需要檢測,往往是因為我們面臨多個副本同時更新,我們需要檢測這些副本的衝突

我們來一起看乙個例子,這個例子是亞馬遜的dynamo系統, 它是乙個kv的儲存系統,類似於redis,可以簡單理解成快取。為了高可用,dynamo保證即使在出現網路分割槽或者機器宕機的時候,仍然可讀可寫。但是這會導致乙個問題,當網路分割槽恢復之後,多個副本的資料可能會出現不一致的情況,這個時候我們就需要通過向量時鐘演算法來檢測衝突了。

假設一開始的時候客戶端w建立了乙個記錄x,這個記錄是由機器\(s_x\)來負責的。那麼則形成了資料\(d_1\)和它對應的向量時鐘\([s_x, 1]\)。

之後,客戶端繼續更新記錄x,同樣由機器\(s_x\)執行,形成了新資料\(d_2\),它的向量時鐘變成\([s_x, 2]\),它和\([s_x, 1]\)存在因果關係。所以我們可以知道\(d_2\)是最新的資料。

再之後,客戶端繼續更新x,這次由\(s_y\)執行,同樣,可以知道操作結束之後它的向量時鐘為\([s_x, 2], [s_y, 1]\)。

與此同時,另乙個客戶端讀入了\(d_2\)之後,在機器\(s_z\)上更新產生了\(d_4\),此時的向量時鐘是\([s_x, 2], [s_z, 1]\)。

我們來分析一下,如果這時候有客戶端同時讀到\(d_2\)和\(d_3\),那麼通過向量時鐘可以判斷出來,\(d_3\)是最新的資料。如果同時讀到\(d_3\)和\(d_4\),由於這兩者的向量時鐘並沒有因果關係,所以無法判斷誰是最新的資料,這就產生了衝突

但是必須要說明的是,向量時鐘演算法只能檢測衝突,並不能解決衝突,衝突需要客戶端自己解決。如果客戶端判斷,決定應該以\(s_x\)的節點為準,那麼最後的資料就會變成\(d_5\),此時的向量時鐘為\([s_x, 3], [s_y, 1], [s_z, 1]\)。

除了知乎之外,其實還有很多場景下的一致性問題都需要考慮因果關係。因此向量時鐘演算法在分布式領域可以說是鼎鼎大名,使用非常廣泛。在剛聽到這個名字的時候,往往會覺得它非常晦澀難懂,但實際深入了解之後,會發現其實並不困難,反而非常有趣,這也算是學習的樂趣之一吧。

今天的文章就到這裡,如果覺得有所收穫,請順手點個在看或者**吧,你們的支援是我最大的動力。

分布式環境計算時遇到的深坑

以前都沒有關注過hashcode的正負,最近做了分割槽,才遇到了負值引發的深坑。實踐中hashcode 可能算出來的是integer.min value,而這個數是integer型別的最小的負數,當hash值恰好是負數時,就會導致bug 從網上一些資料了解到,在分布式環境中,除了分割槽外,在佇列選擇...

多層分布式開發 MIDAS 使用不同協議時的優缺點

使用的連線 優點缺點 dcom 提供最直接的連線,伺服器端不需要額外的 應用軟體支援 提供有效的安全功能 客戶端需要額外的程式 沒有提供企業級應用 window 95 沒有安裝dcom socket 適用範圍廣 提供防火牆 訪問控制 發布安全 容易使用 客戶端不需要dll 提供poll 和push ...

分布式系統 快取穿透與失效時的雪崩效應

快取系統往往有兩個問題需要面對和考慮 快取穿透與失效時的雪崩效應。1.快取穿透是指查詢乙個一定不存在的資料,由於快取是不命中時被動寫的,並且出於容錯考慮,如果從儲存層查不到資料則不寫入快取,這將導致這個不存在的資料每次請求都要到儲存層去查詢,失去了快取的意義。至於如何有效地解決快取穿透問題,最常見的...