​分佈式系統與單節點系統的本質區別是什麼?

例如,我們可以定義一個變量,並對它執行簡單的算術運算:

這裏我們只會得到唯一的執行歷史:聲明一個變量,把它加上 2,再乘以 2,然後得到結果 6。假設這些運算不是由單個線程執行的,而是有兩個線程讀寫變量 x,我們就需要考慮併發執行的問題。

1 併發執行

一旦兩個執行線程都能訪問變量,除非我們在線程間同步這些步驟,否則這些併發步驟的執行結果是無法預知的。如圖 8-1 所示,我們得到了 4 個可能的結果。注 1

a) x = 2,如果兩個線程都讀到初始值,加法寫入結果,但被乘法的結果覆蓋了。

b) x = 3,如果兩個線程都讀到初始值,乘法寫入結果,但被加法的結果覆蓋了。

c) x = 4,如果乘法讀到初始值,且乘法在加法之前執行。

d) x = 6,如果加法讀到初始值,且加法在乘法之前執行。

即便僅在單個節點上,我們就已經遇到了分佈式系統中的第一個問題:併發。每個併發程序都具有分佈式系統的某些特性。線程訪問共享狀態,在本地執行一些運算,再將結果傳回共享變量。

圖 1:併發執行中可能的交錯情形

爲了精確定義執行歷史並減少可能的結果數量,我們需要一致性模型。一致性模型描述併發執行的過程,並且確定了運算執行以及對其他參與者可見的順序。使用不同的一致性模型,我們可以約束或放鬆系統可能的狀態數量。

分佈式系統和併發計算在術語和學術研究上有許多重疊之處,但也存在一些差異。併發系統中存在共享內存,進程可以用它來交換信息。在分佈式系統中,各個進程擁有自己的本地狀態,參與者之間通過傳遞消息進行通信。

併發和並行

我們經常互換使用併發和並行計算這兩個術語,但是這兩個概念在語義上有細微的差異。當兩個步驟序列併發執行時,二者都在進行中,但任意時刻都只有其中一個在執行。當兩個步驟序列並行執行時,它們的步驟可以(在某一時刻)同時執行。併發的操作時間上存在重疊,而並行的操作由多個處理器執行 [WEIKUM01]。

Erlang 編程語言的創建者 Joe Armstrong 舉過一個例子:併發執行就像一臺咖啡機前排了兩隊,而並行執行就像兩臺咖啡機前排了兩隊。即便如此,絕大部分資料都用術語 “併發” 來描述擁有多個並行執行線程的系統,而 “並行” 這個詞則很少見。

分佈式系統中的共享狀態

我們可以嘗試在分佈式系統中引入共享內存的概念,例如,單一信息源(比如數據庫)。即使我們解決了併發訪問的問題,我們依然無法保證所有進程都是同步的。

爲了訪問數據庫,進程需要通過通信介質發送和接受消息,以查詢或修改狀態。但是,如果一個進程很久都沒有從數據庫得到響應會如何?爲了回答這個問題,我們首先要定義什麼是很久。爲此,必須從同步性的角度來描述系統:通信是否是全異步的?是否存在某些時序假設?如果存在的話,這些時序假設將允許我們引入操作超時和重試機制。

我們無從知曉數據庫沒有響應是因爲過載、不可用、響應太慢還是網絡問題。這描述了崩潰的本質—進程可能以各種方式崩潰:可能因某種原因無法繼續執行後面的算法步驟;可能遇到了臨時性的故障;也可能是消息丟失。我們需要定義一個故障模型並描述故障可能發生的方式,然後再決定如何處理它們。

如果系統在故障發生時仍然能繼續正常運行,我們將這樣的特性稱爲容錯性。故障是不可避免的,所以我們需要構建出具有可靠組件的系統。消除單點故障,比如前文提到的單節點數據庫,可能是我們朝此方向邁出的第一步。我們可以引入一些冗餘,增設備份數據庫。然而這就引出了另一個問題:如何使共享狀態的多個副本保持同步?

到目前爲止,在我們這個簡單系統中引入共享狀態所帶來的問題比答案還多。現在我們知道,共享狀態不像引入數據庫那樣簡單,還必須採取更細化的方法,即基於消息傳遞來描述各個獨立進程之間的交互。

2 分佈式計算的誤區

理想情況下,當兩臺計算機在網絡上通信時,一切都能正常工作:進程開啓一個連接、發送數據、收到響應,每個人都很開心。但是假設所有操作總會成功並且沒有任何錯誤是很危險的,因爲當某些東西出問題時,我們的假設也就不成立了,那時系統的行爲將變得難以預測。

大多數時候,假設網絡可靠是合理的。網絡至少在一定程度上可靠纔能有用。我們都曾經歷過這樣的情況,當我們嘗試連接到遠程服務器時,卻收到了一個 “網絡不可達” 的錯誤。即使能建立連接,一個成功的初始連接也無法保證這條鏈路是穩定的,連接隨時可能中斷。消息可能送達了對端,但對端的響應卻可能丟失了,也有可能在對端的響應發送之前連接就中斷了。

網絡交換機會有故障,電纜可能斷開,網絡配置也隨時可能發生變化。我們構建系統時需要適當地處理所有這些情況。

連接可以是穩定的,但我們不能期望遠程調用能像本地調用一樣快。我們應儘可能少地對延遲做出假設,並且永遠不要假設延遲爲零。一條消息要想到達遠程服務器,需要穿過若干個軟件層和一個物理媒介(比如光纖或電纜),所有這些操作都不是瞬間完成的。

Michael Lewis 在他所著的書 Flash Boys(Simon and Schuster 公司出版)中講述了這樣一個故事,公司花費數百萬美元把延遲降低幾毫秒,從而能比競爭對手更快地訪問交易所。這是一個把延遲作爲競爭優勢的絕佳例子,然而值得一提的是,根據其他一些研究,比如文獻 [BARTLETT16],過時報價套利(通過比競爭對手更快地得知價格並執行交易來獲取利潤)並不能使快速交易者從市場中獲利。

從上述教訓當中學習,我們增加了重試和重連機制,並去掉了關於瞬間執行的假設,但是事實證明這還不夠:當我們增加消息的數量、發送速率和大小,並向現有網絡中添加新的進程時,我們不應該假設帶寬是無限的。

1994 年,Peter Deutsch 發佈過一個如今很有名的斷言列表,標題爲 “分佈式計算的誤區”,描述了分佈式計算中易被忽視的一些方面。除了網絡可靠性、延遲和帶寬假設,它還提到了其他問題,比如,網絡的安全性、可能存在的攻擊者、有意或無意的拓撲變化都可能打破我們的一些假設,這些假設包括:某一資源存在性和所在位置,網絡傳輸所消耗的時間和資源,以及最後—存在一個擁有整個網絡的知識和控制權的權威個體。

2.1 處理

        使接收和處理在時間上分開,並各自獨立發生。

流水線化

        不同階段的請求由系統中獨立的部分處理。負責接收消息的子系統不用阻塞到上一條消息處理完成。

吸收瞬時突發流量

        系統負載可能經常變化,但是請求到達的間隔時間對負責處理請求的組件是隱藏的。總體的系統延遲會由於排隊而增加,但這通常仍比響應失敗並重試請求更好。

隊列大小取決於工作負載和應用程序。對於相對穩定的工作負載,我們可以通過測量任務處理時間以及各任務的平均排隊時間來確定隊列大小,從而確保在提升吞吐量的同時,延遲仍保持在可接受的範圍內。在這種情況下,隊列大小相對較小。對於不可預測的工作負載,可能會出現任務提交的突發流量,這時隊列大小也應當考慮突發流量和高負載。

即使遠程服務器可以快速地處理請求,也並不意味着我們總是能獲得正面的響應。它也可能迴應一個失敗:無法進行寫操作、要查找的值不存在或是觸發了 bug。總之,即使是最順利的情況也需要我們的關注。

時間是一種幻覺,尤其是午餐時間。 

                                              —Ford Prefect, The Hitchhiker 抯 Guide to the Galaxy

假設不同的遠程計算機上的時鐘都同步也很危險。再加上延遲爲零以及處理是瞬時的這些假設,將會導致不同的特質,尤其是在時序和實時數據處理中。例如,當從時間感知不同的參與者收集和聚合數據時,你必須瞭解它們之間的時間漂移並相應地對時間進行歸一化,而不是依賴源時間戳。除非使用特殊的高精度時間源,否則不能依賴時間戳進行同步或排序。當然,這並不意味着我們完全不能或不該依賴時間:說到底,任何同步系統都依靠本地時鐘實現超時。

我們必須始終注意進程之間可能存在的時間誤差,以及傳遞和處理消息所需的時間。例如,Spanner(參見 13.5 節)使用特殊的時間 API,該 API 返回時間戳和不確定性界限以施加嚴格的事務順序。一些故障檢測算法依賴於共享的時間概念,要求時鐘漂移始終在允許的範圍內才能確保正確性 [GUPTA01]。

除了分佈式系統中的時鐘同步非常困難之外,當前時間也在不斷變化:你可以從操作系統請求當前的 POSIX 時間戳,並在執行幾個步驟後請求另一個當前時間戳,兩次結果是不同的。儘管這是一個明顯的現象,但是瞭解時間的來源以及時間戳捕獲的確切時刻至關重要。

瞭解時鐘源是否是單調的(即永遠不會後退),以及與調度時間相關的操作可能偏移多少,可能也會有所幫助。

2.3 狀態一致性

即使完全的解決方案成本很高,我們也最好事先考慮各種可能的問題。通過了解和處理這些情況,你能以更自然的方式解決問題,比如內置防護措施或修改設計。

2.4 本地和遠程執行

剛開始構建系統的時候,我們可以假設所有節點都可以正常工作,但如果總是這麼想就很危險了。在長時間運行的系統中,節點可能會關機維護(通常會有個優雅關閉的過程)或因爲種種原因(例如軟件問題、內存耗盡(out-of-memory killer [KERRISK10])、運行時 bug、硬件問題等)而崩潰。進程會發生故障,而你能做的最好的事情就是做好準備並知道如何處理它們。

如果遠程服務器沒有響應,我們並不總是知道確切的原因。這可能是由系統崩潰、網絡故障、遠程進程或中間鏈路太慢導致的。一些分佈式算法使用心跳協議和故障檢測機制來確定哪些參與者還活着且可達。

2.6 網絡分區和部分故障

當兩個或更多服務器無法相互通信時,我們稱這種情況爲網絡分區。Seth Gilbert 和 Nancy Lynch 在 Perspectives on the CAP Theorem [GILBERT12] 中區分了以下兩種情況:兩個參與者無法相互通信;幾組參與者彼此隔開,無法交換消息並繼續運行算法。

網絡的總體不可靠性(數據包丟失、重傳、延遲難以預測)令人煩惱但尚可容忍,而網絡分區則會造成更多的麻煩,因爲各個獨立的分組可以繼續執行併產生衝突的結果。網絡鏈路的故障也可能是不對稱的:消息仍然能從一個進程傳遞到另一個進程,反之則不行。

爲了構建在一個或多個進程出現故障的情況下仍健壯的系統,我們必須考慮部分故障的情況 [TANENBAUM06],如何讓系統在部分不可用或運行不正常的情況下仍能繼續工作。

故障很難檢測,並且在系統的不同部分看來,不總是以相同的方式可見。設計高可用性系統時,我們應該始終考慮邊緣情形:如果我們確實複製了數據卻沒有收到確認該怎麼辦?要重試嗎?在發送了確認的節點上,數據仍可用於讀取嗎?

墨菲定律注 2 告訴我們故障一定會發生。編程界又補充道,故障將以最壞的方式發生。因此,作爲分佈式系統工程師,我們的工作是儘可能減少可能出現錯誤的場景,併爲故障做好準備—包括這些故障可能導致的破壞。

避免一切故障是不可能的,但我們仍可以構建一個彈性的系統,使之在故障出現時仍然能正常運行。設計應對故障的最佳方式是進行故障測試。我們無法考慮清楚每種可能的故障場景,並預測多個進程的行爲。最好的解決方法就是通過測試工具來製造網絡分區、模擬比特位腐爛 [GRAY05]、增加延遲、使時鐘發生偏移以及放大相對處理速度。現實世界中分佈式系統的設置可能是對抗性的、不友好的,甚至是 “有創造性的”(然而,以非常敵對的方式),因此測試工作應當嘗試覆蓋儘可能多的場景。

過去幾年中出現了一些開源項目,它們能幫助我們構造出各種故障場景。Toxiproxy 用於模擬網絡問題:限制帶寬、引入延遲、超時等。Chaos Monkey 的方法更爲激進,它通過隨機關閉服務使工程師直面生產環境故障的風險。CharybdeFS 模擬文件系統及硬件錯誤與故障。你可以用這些工具來測試軟件,以確保在這些故障出現時軟件仍能正確工作。CrashMonkey 是一個與文件系統無關的記錄–重放–測試框架,用於測試持久性文件的數據及元數據一致性。

設計分佈式系統時,我們必須認真考慮容錯性、彈性,以及可能的故障場景和邊緣情形。類似於 “足夠多的眼睛,就可讓所有問題浮現”,我們可以說足夠大的集羣最終一定會命中所有可能的問題。與此同時,只要有足夠多的測試,我們最終能夠發現每個存在的問題。

2.7 級聯故障

我們做不到總是完全隔離故障:被高負載壓垮的進程會增加集羣其餘部分的負載,從而使其他節點更有可能發生故障。級聯故障能夠從系統的一部分傳播到另一部分,擴大了問題的範圍。

有時,級聯故障甚至可能來源於完全善意的目的。例如,某個節點離線了一段時間,因而沒有接收到最近的更新。當它恢復在線時,樂於助人的其他節點希望幫助它追趕上最近的變化,於是開始向它發送缺失的數據,而這又導致網絡資源耗盡,或是導致該節點啓動後短時間內再次發生故障。

爲了防止系統的故障擴散並妥善處理故障場景,我們可以使用斷路器(或熔斷機制)。在電氣工程中,斷路器可通過中斷電流來保護昂貴且難以更換的部件,使其免受電流過載或短路的影響。在軟件開發中,熔斷機制會監視故障,並使用回退(fallback)機制保護整個系統:避免使用出故障的服務,給它一些時間進行恢復,並妥善處理失敗的調用。

當與某一臺服務器的連接失敗或服務器沒有響應時,客戶端將開始循環重連。那時候,過載的服務器已經難以應付新的連接請求,因而客戶端的循環重試也無濟於事。爲了避免這一情況,我們可以使用退避(backoff)策略,客戶端不要立即重試,而是等待一段時間。退避通過合理安排重試、增加後續請求之間的時間窗口來避免問題擴大。

退避用於增加單個客戶端的請求間隔。但是,使用相同退避策略的多個客戶端也會產生大量負載。爲了防止多個客戶端在退避期之後同時重試,我們可以引入抖動(jitter)。抖動在退避上增加了一個小的隨機時間間隔,從而降低了多個客戶端同時醒來並重試的可能性。

硬件故障、比特位腐爛和軟件錯誤都會導致數據損壞,而損壞的數據會通過標準的傳遞機制傳播。如果沒有適當的驗證機制,系統可能將損壞的數據傳播到其他節點,甚至可能覆蓋未損壞的數據記錄。爲了避免這一情況,我們應該採用校驗和(checksum)以及驗證機制,來驗證節點之間交換的任何內容的完整性。

通過計劃和協調執行可以避免過載和熱點問題。相比於讓各個對等節點獨立執行操作步驟,我們可以用協調器來依據可用資源準備一份執行計劃,並根據過去的執行數據來預測負載。

瞭解什麼會出問題,並仔細設計和測試我們的系統,可以讓它更健壯、更具彈性。瞭解這些問題可以幫助你在開發過程中識別、發現潛在的問題根源,也能幫助你在生產環境中調試。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s?__biz=MzIxMzEzMjM5NQ==&mid=2651040085&idx=2&sn=e86a46dc8f03c87c428ca632dd0bc590&chksm=8c4c7c51bb3bf547bfb351952c8fe2444c3ff7c4d039e2f79928095c7fa5577c6e48487875f0&scene=178&cur_album_id=1343963749467717633#rd