兩個將軍問題與分散式Saga

banq發表於2022-05-10

想象一下,在一個山谷裡有一座城市。在山谷的兩邊,有一支由將軍指揮的軍隊。左邊的山上站著愛麗絲將軍和她的軍隊。右邊的山頭上,站著鮑勃將軍和他的軍隊。
愛麗絲和鮑勃想佔領這座城市,但雙方都沒有足夠大的軍隊來單獨完成這一任務。
愛麗絲和鮑勃必須同時進攻城市,才有機會佔領它。

這裡就是我們的問題所在:愛麗絲和鮑勃只能通過山谷中的信使進行交流。這些信使有可能被城市的軍隊抓住。
鑑於這種情況,以及愛麗絲和鮑勃只有在確信對方會同時進攻時才會進攻的條件,愛麗絲和鮑勃如何協調他們的進攻?

比方說,愛麗絲決定採取主動。她給鮑勃發了一條資訊,內容是
"讓我們在5月17日進行攻擊。傳送一個信使回來,並附上你的回覆。-愛麗絲"

如果信使沒有送到和得到回應,愛麗絲就不會進攻,顯然,鮑勃也不會。
如果訊息傳到鮑勃那裡,他就會發出回應:
"好,我們開始吧。就在5月17日! -鮑勃"

現在,有趣的部分來了:
鮑勃的回覆是否能送回來並不重要。無論怎樣,鮑勃都不會進攻。
記住,鮑勃只有在他相信愛麗絲也會攻擊時才會攻擊。鮑勃知道愛麗絲只有在收到他的回覆時才會攻擊,但是鮑勃沒有辦法知道他的回覆是否回到了愛麗絲那裡。如果沒有,她就不會進攻,而如果他在沒有她的情況下進攻,他的軍隊就會被消滅掉。
由於鮑勃不知道他的資訊是否傳回來了,所以他在等待,什麼也不做。

我們有什麼可以做的嗎?
讓我們改變一下,讓鮑勃要求愛麗絲髮送確認函,確認她收到了他的回覆。他傳送的不是他原來的回覆,而是現在這個:
"好吧,我們開始吧。5月17日就這樣了! 傳送另一個信使回來確認你收到了這個訊息。-鮑勃"

進入第三階段:現在愛麗絲髮送了資訊,鮑勃傳送了他的回覆,愛麗絲準備傳送她收到他回覆的確認資訊。
但她意識到,她沒有辦法知道他是否會收到確認資訊。如果他沒有收到,他就不會進攻,而她的軍隊就會被打敗。所以她決定傳送這個資訊。
"我收到了你的回信。請再派一個信使來確認這個確認。-愛麗絲"

總結一下:
現在我們有一條資訊,一個回覆,一個對回覆的確認,以及對確認的確認。

假設鮑勃收到了回執,準備傳送回執的回執。但是,等等! 他意識到他沒有辦法知道愛麗絲是否會收到確認書的確認。如果她沒有收到,她就不會攻擊,所以鮑勃決定傳送。

"我確認了你的確認書。請再派一個信使來確認這個確認的確認。-鮑勃"

看到這裡發生了什麼?不管我們增加多少層確認,最後一個傳送信使的人永遠無法知道那個信使是否通過了。
因此,最後傳送信使的人永遠無法確信對方會攻擊,所以也永遠不會攻擊自己。

兩位將軍問題的輕視解決
上面的問題被稱為 "兩個將軍問題",它被證明是無法解決的。
有沒有一個約束條件較少的寬鬆版本的問題是可以解決的?如果答案是否定的,我可能就不會問這個問題了--事實證明,是的,有。
如果我們去掉兩個將軍都必須百分之百相信另一個會進攻的條件,並改變一些其他的東西,我們就能在大多數時候解決這個問題。
我們仍然不能完全保證兩個將軍都會進攻,但我們可以合理地確定他們會進攻。

在這個問題的新版本中,愛麗絲是領導者。她決定什麼時候進攻,一旦她決定什麼時候進攻,無論如何她都會進攻。如果鮑勃收到訊息說他應該在某個特定的時間進行攻擊,他必須這樣做。

讓我們看一個例子:
愛麗絲決定她要攻擊。她知道她不可能100%確定鮑勃也會攻擊,但她決定她要99%確定鮑勃會攻擊。愛麗絲知道信使穿越山谷所需的時間長度,她也知道信使被抓的可能性。

知道這兩件事後,她計算出需要多少名信使才能有99%的把握通過。然後,她計算出傳送這麼多信使需要多長時間,並將攻擊時間設定在足夠長的未來,以考慮到這一點。

接下來愛麗絲髮送了一個帶有以下資訊的訊息:
"下週四上午9點攻擊。傳送一個信使回來確認你收到了這個訊息"。

然後愛麗絲等待足夠的時間讓信使到達鮑勃那裡,並讓鮑勃的確認信使到達她那裡。如果信使到達了,愛麗絲就停止傳送信使,愛麗絲和鮑勃都在等待,直到下週四上午9點他們的協調攻擊。

如果確認資訊從未到達,愛麗絲就會傳送另一個相同資訊的訊息。
"下週四上午9點的攻擊。傳送一個信使回來確認你收到了這個訊息"。

她不斷重複這個過程,直到確認資訊到達或者是下週四上午9點,這時她就會攻擊,不管資訊是否通過。因為Alice選擇了一個足夠遠的時間(基於資訊的失敗率),所以有99%的機會在攻擊時間之前至少有一個信使能傳給Bob。

我們知道,下週四上午9點,兩個將軍都有99%的機會進攻。我們可以增加這個確定性數字嗎?是的,我們可以。愛麗絲可以通過將攻擊時間設定得越來越遠,使這個數字儘可能接近100%。然而,如果不把攻擊時間無限地設定到未來,她永遠無法達到100%(這意味著它永遠不會發生,所以這將是無用的)。

這裡還有一件事要指出。如果我們不關心這些攻擊是否一起發生,愛麗絲可以傳送一條資訊說。
"現在攻擊。傳送一個信使來確認你收到了這個訊息。"

鮑勃在愛麗絲之前進行攻擊:
在這種情況下,愛麗絲可以不斷地重複傳送訊息,直到她收到確認,並在攻擊前等待確認。

這種方法確實能保證雙方最終都會攻擊,但關鍵是,他們不會同時攻擊。愛麗絲的攻擊會比鮑勃晚,至少要比確認信使從鮑勃到愛麗絲所花的時間晚,而且可能比這個時間晚得多,這取決於有多少信使被捕獲。

一個更實際的例子
讓我們來看看一個更實際的例子。假設您正在設計一個系統,需要通過第三方服務預訂一場戰鬥,並向您的使用者傳送一封電子郵件,告知該航班已被預訂。

起初,這聽起來是一個簡單的問題。對航班預訂系統進行一次網路呼叫。如果它返回一個OK狀態,那麼就傳送電子郵件。如果它返回錯誤狀態,就不要傳送郵件(或傳送一封寫有稍後再試的郵件)。

但請記住,網路呼叫是不可靠的,所以我們實際上有的只是上述放鬆的兩將軍問題的一個版本。如果網路呼叫返回OK或錯誤狀態,那麼我們就沒事了,但網路呼叫也可能因為沒有收到響應而超時(就像信使被俘一樣)。在這種情況下,我們該怎麼做呢?

就像在二將問題輕視解決一樣,我們可以不斷重試訊息,直到得到回應。只有在得到回應後,我們才能決定下一步該怎麼做。如果響應是確定的,我們就傳送電子郵件(我們有意忽略了傳送電子郵件也是一個不可靠的操作)。如果響應是錯誤的,我們就不傳送。

你認為這種方法有什麼問題嗎?如果網路出了問題,我們從來沒有得到過回應怎麼辦?我們是否要一直重試下去,可能會阻塞資源,直到有人注意到問題,並手動殺死這個任務?

我們可以用我們原來對輕鬆的兩個將軍問題的部分解決方案來處理這個問題:
我們將新增一個定時器。具體來說,我們可以給我們的重試過程新增一個超時器。在我們第一次傳送預訂航班的資訊時,我們將啟動計時器。然後,我們將不斷地重試訊息(只要我們沒有得到回應),直到定時器倒數為零。一旦定時器完成,我們將停止傳送訊息並採取行動。

計時器的長度相當於在給鮑勃的訊息中宣告攻擊時間。如果我們在週三上午9點開始傳送訊息,而Alice決定在週四上午9點進行攻擊,我們就有一個24小時的超時計時器。就像設定攻擊時間一樣,我們可以通過增加超時的長度來增加資訊通過的機會。然而,我們必須平衡這一點,不允許我們的系統佔用資源太長時間。

我們知道超時後我們會採取行動,但我們還沒有說這個行動是什麼。由於我們一直沒有得到回應,我們不知道航班是否被預訂了。我們可以傳送一封郵件說它已經被預訂了,也可以傳送一封郵件說它可能已經被預訂了,或者什麼都不傳送(並記錄一個超時錯誤)。最好的決定是因地制宜的,但在我們的案例中,我們會選擇傳送一封電子郵件說該航班可能已經被預訂了。然後使用者可以跟進。

為了使我們的解決方案奏效,我們必須處理另一個複雜的問題,這是 "兩位將軍問題 "所提出的問題的直接後果--在不可靠的網路上,確切地說是一次資訊傳遞不可能。我們可以傳送一次資訊,並希望它能到達(但它可能不會),或者我們可以多次傳送資訊。在這種情況下,它可能會到達不止一次(也許更多次)。

想象一下,一個信使在前往鮑勃的路上迷路了。然後,另一個信使被派出去,直接到達目的地。在第二個信使到達後,第一個信使設法找到了他們去鮑勃的路。同樣的事情也可以發生在計算機網路中。資訊不是丟失,而是沿途卡在緩衝區。

就我們的航班預訂系統而言,這可能是一個大問題。如果有多個訊息說要預訂一個航班,系統可能會預訂多個航班。為了解決這個問題,預訂航班請求必須是empotent的。同位素的意思是,如果一個請求被提出一次以上,請求的效果必須只發生一次。

有幾種解決方法:一種方法是在請求中包含一個唯一的ID。只要我們的系統在每次重試時傳送相同的ID,航班系統就可以儲存這個ID,並使用它來忽略所有的資訊,除了它收到的第一個資訊。

增加第二個分散式操作
到目前為止,(引入重試與超時)似乎已經解決了所有的問題。然而,如果我們增加第二個分散式操作,我們的系統就會崩潰。
當我們希望我們的系統也能處理預訂酒店時,會發生什麼?
新的要求是,我們應該把航班和酒店一起預訂。也就是說,只有當航班被成功預訂後,我們才應該預訂酒店,只有當酒店被成功預訂後,我們才應該預訂航班。在我們預訂了它們之後,我們會傳送一封帶有狀態更新的電子郵件。

即使不考慮超時,要求兩者都被預訂或都不被預訂也會增加複雜性。我們可以用幾種不同的方法來處理這種複雜性,但最直接的方法是確保我們的系統能夠處理撤銷這兩種操作。這種方法被稱為 "分散式Saga"。

下面是我們的航班和酒店預訂系統的分散式Saga的樣子(忽略了超時和丟失的訊息):
首先,我們傳送一條訊息來預訂航班。如果我們得到一個錯誤的響應,我們就中止整個過程。如果航班預訂系統返回成功,我們就傳送一個訊息來預訂酒店。如果成功了,我們就完成了,我們可以傳送成功的電子郵件。

但是,如果酒店系統返回一個錯誤,我們就必須通過呼叫航班系統,告訴它撤銷我們剛剛預訂的航班,從而撤銷航班預訂。這就叫回滾Saga。在我們回滾Saga之後,我們可以傳送一個適當的訊息或記錄一個錯誤。

在我們的實際實現中,我們實際上不能忽略丟失的資訊:
我們如何在我們的傳奇中處理這種可能性?
每個分散式動作(預訂航班和預訂酒店)必須有一個相應的補償撤銷動作(取消航班預訂和取消酒店預訂)。

我們可以使用補償動作來處理呼叫原始動作時丟失的資訊。比如說:
我們啟動Saga,併傳送一條訊息來預訂航班。如果預訂航班的網路呼叫超時,我們可以不重試,而是回滾整個Saga。我們呼叫取消航班預訂的端點,並將ID與原始預訂航班請求一起傳送。

處理超時和丟失的資訊
你可能已經注意到一個問題。我們不知道預訂航班的請求是否通過了。如果它沒有通過,我們仍然傳送取消航班預訂請求怎麼辦?與預訂航班請求必須是等價的一樣,預訂航班和取消航班預訂的組合必須是換位的。航班預訂系統收到它們的順序應該不重要。

如果取消航班預訂訊息先到,航班預訂系統必須儲存請求的ID,並阻止隨後出現的任何帶有該ID的預訂航班請求。如果取消航班預訂資訊在預訂航班資訊之後到達,航班預訂系統必須撤銷原來的行動。

我們已經通過把問題踢給補償行動來處理預訂航班行動的丟失資訊,但如果補償行動請求丟失了會怎樣?

補償行動必須不能夠失敗:
它們永遠不能返回錯誤程式碼。這意味著它們失敗的唯一方式是超時。
正因為如此,我們可以不斷重試補償動作,直到它成功(在實踐中,我們可能希望這裡有一個超時來放棄,並最終提醒人類出了問題)。

如果預訂航班成功,我們就轉到預訂酒店。如果失敗了,我們就回滾到上一個步驟。
如果超時,我們通過呼叫取消酒店預訂來回滾當前步驟,然後我們回滾上一步,取消傳奇並退出。如果一切都成功了,我們就繼續傳送電子郵件。

生產系統的注意事項
對於生產系統,有幾個最後的注意事項值得一提。我們已經處理了遠端系統的故障,但沒有處理我們本地系統的故障。我們還需要儲存我們在當前Saga中的位置,以便在我們的系統在中途崩潰時,我們可以從該點恢復。

此外,你將讀到的關於分散式Saga的大多數文獻都會像我剛才那樣提到commutative換元屬性,但換元commutative 並沒有真正強大到足以描述所需內容。

讓我們舉一個簡單的例子:在一個Saga中,有一個叫做 "增量 "的動作和它的補償動作叫做 "減量"。不出所料,"增加 "將一個數字增加1,"減少 "將其減少1。從計數器0開始,先增量後減量=先減量後增量=0。

0 + 1 - 1 = 0 - 1 + 1 = 0 
無論何種順序,最終結果都是一樣的。但Saga中的補償動作也必須在只有補償動作資訊到達目的地時發揮作用。

也就是說,從0開始,先增後減=先減後增=只減不增=0。

我不再說動作和它的補償動作必須是換元的,而是開始用補償動作必須支配動作的說法來指代這一要求。增量和減量也必須都是等價的,所以不管有多少資訊到達(具有相同的ID),只要有一個減量到達,最後的結果必須和Saga開始前一樣。

另一個從0開始的例子:
增量->減量->增量=0=減量->增量->減量 
由於這些特性,Decrement不是補償動作的好名字。最好叫它Compensate Increment補償增量之類的名字。

實現這一點的一個方法是讓Compensate Increment與Increment共享其ID。當伺服器處理Compensate Increment時,它檢查是否有任何帶有該ID的Increment資訊已經被處理過。如果是這樣,伺服器就會遞減該值以進行補償。如果沒有,它將保持該值不變。然後,在前面兩種情況下,它都會儲存該ID,以便今後任何帶有該ID的訊息都會被忽略。

文獻中也將分散式Saga中的每一步稱為事務,但我在這裡將它們稱為行動actions,以避免與其他併發控制方法相混淆。

關於作者
塞斯-阿切爾-布朗目前正在寫一本名為《計算機網路從零開始》的免費書籍。如果你喜歡這裡的內容,你會在書中找到類似的內容。

從零開始的計算機網路》是對最大的計算機網路--網際網路--的內部運作的一次參觀。它完全從零開始(假設完全沒有網路知識),以一個允許使用者使用彈珠和塑料管而不是電訊號和電線來回傳送資訊的系統為例。然後,它建立了一個類似於60年代初最先進的計算機網路的東西。

本書是一項正在進行中的工作,但在本書結束時,它將提供一個關於網際網路今天工作的概述,以及即將到來的情況。

相關文章