分散式系統中的背壓
研究表明,即使是最堅固、設計最精良的水壩也無法承受失控洪水的破壞力。同樣,在分散式系統的場景中,未經限制的呼叫者通常會使整個系統不堪重負,並導致級聯故障。如果沒有適當的防護措施,重試風暴有可能使整個服務崩潰。本文將探討服務何時應該考慮對其呼叫者應用背壓(Backpressure),如何應用,以及呼叫者可以做些什麼來處理背壓。
背壓
顧名思義,背壓是分散式系統中的機制,指的是系統限制資料消耗或產生速度的能力,以防止自身或其下游元件過載。系統對其呼叫者施加背壓並不總是顯式的,例如以節流或減少負載的形式,但有時也是隱式的,例如透過增加服務請求的延遲而不顯式地減慢自己的系統。隱式和顯式背壓都是為了降低呼叫者的速度,無論是呼叫者表現不佳,還是服務本身狀態不佳,需要時間來恢復。
需要背壓
以下舉例說明系統何時需要施加背壓。在這個例子中,正在建立一個包含三個主要元件的控制平台服務:一個接收客戶請求的前端,一個緩衝客戶請求的內部佇列,以及一個從佇列讀取訊息並寫入資料庫以實現持久性的消費者應用程式。
(1)生產者與消費者不匹配
設想這樣一個場景,參與者/客戶以極高的頻率訪問前端,導致內部隊列已滿或寫入資料庫的工作線程很忙,進而造成隊列滿載。在這種情況下,請求不能排隊,因此與其放棄客戶請求,不如提前通知客戶。這種不匹配可能由於各種原因而發生,例如傳入流量激增或系統出現小故障,其中消費者服務曾經一度停機,但現在必須增加額外的工作時間,以有效清理並解決在停機期間所形成的工作積壓問題。
(2)資源約束與級聯故障
設想這樣一個場景,隊列接近其容量的100%,而平時在50%左右。為了匹配這種傳入速率的增加,可以擴展消費者應用程序,並開始以更高的速率寫入資料庫。但是,資料庫因無法處理這種增長(例如每秒寫入次數的限制)而崩潰。這種故障將導致整個系統癱瘓,並增加平均恢復時間(MTTR)。在這種情況下,在適當的地方施加背壓變得至關重要。
(3)錯過服務等級協定(SLA)
考慮這樣一個場景:寫入資料庫的資料每5分鐘處理一次,另一個應用程式會監聽這些資料以保持自身更新。現在,如果系統因某種原因無法滿足SLA,例如佇列已滿90%,可能需要10分鐘才能清除所有訊息,那麼最好採用背壓技術。可以通知客戶將會錯過SLA,並建議他們稍後再試,或透過從佇列中刪除非緊急請求來套用背壓,以滿足關鍵事件/請求的SLA。
背壓的挑戰
根據上述內容,似乎應該始終應用背壓,聽起來確實如此,主要的挑戰不是是否應該應用背壓,而是如何確定應用背壓的正確點,以及應用反壓力的機制,以滿足特定的服務/業務需求。
背壓迫使在吞吐量和穩定性之間進行權衡,而負載預測的挑戰使這種權衡變得更加複雜。
確定背壓點
(1)找出瓶頸/薄弱環節
每個系統都存在瓶頸。有些瓶頸能夠自我承受和保護,而有些則不能。設想在一個系統中,其中龐大的資料平台叢集(數千台主機)依賴一個小型控制平台叢集(少於5台主機)來接收儲存在資料庫中的配置,如上圖所示。大型集群很容易使小型集群不堪重負。在這種情況下,為了保護自己,小型叢集應該具備對呼叫者應用背壓的機制。架構中另一個常見的弱點是對整個系統做出決策的集中式元件,例如反熵掃描器。如果它們失效,系統就永遠無法達到穩定狀態,甚至可能導致整個服務崩潰。
(2)使用系統動態:監測器/指標
另一種為系統找到回壓點的常見方法是設定適當的監測器/指標。持續監控系統行為,包括佇列深度、CPU/記憶體利用率和網路吞吐量。利用這些即時數據來識別新出現的瓶頸,並相應地調整背壓點。透過指標或觀察者(例如跨不同系統組件的效能金絲雀)來建立綜合視圖,是了解系統是否處於壓力狀態並應對其使用者/呼叫者施加背壓的另一種方法。這些效能金絲雀(P erformance C anaries )可以針對系統的不同方面進行隔離,以找到瓶頸。此外,擁有一個內部資源使用情況的即時儀表板是另一種利用系統動態來發現關鍵點和採取更積極主動措施的好方法。
(3)邊界:最小驚奇原則
對客戶來說,最明顯的是與他們互動的服務表面區域。通常是客戶用來為其要求提供服務的API 。這也是客戶在出現背壓時最不會感到驚訝的地方,因為它清楚地表明系統處於壓力之下。它能夠以節流或減載的形式出現。同樣的原則可以在服務本身中跨不同的子元件和介面應用,它們透過這些子元件和介面相互互動。這些表面是施加背壓的最佳位置,有助於最大限度地減少混亂,使系統的行為更具可預測性。
如何在分散式系統中應用背壓
在上一節中,討論如何找到正確的興趣點以施加背壓。一旦確定了這些點,以下是一些在實際中施加背壓的方法。
建構顯式流控制
這個想法是讓呼叫者能夠看到佇列的大小,並根據它來控制呼叫速率。透過了解佇列大小(或任何成為瓶頸的資源),呼叫者可以增加或減少呼叫率,以避免系統過載。這種技術在多個內部組件協同工作且盡可能不影響彼此的情況下特別有用。以下公式可以在任何時候用來計算呼叫者的速率。註:實際的調用速率將取決於各種其他因素,但以下這個公式應該能夠提供一個很好的思路。
CallRate_new = CallRate_normal * (1 - (Q_currentSize / Q_maxSize))
倒置責任
在某些系統中,可以改變呼叫者不直接地向服務發送請求的順序,而是讓服務請求在準備好提供服務時自行工作。這種技術使接收服務可以完全控制它可以做多少事情,並且可以根據其最新狀態動態更改請求大小。可以採用令牌桶策略,其中接收服務填充令牌,並告訴呼叫者何時以及他們可以向伺服器發送多少令牌。以下是呼叫者可以使用的一個範例演算法:
# Service requests work if it has capacity
if Tokens_available > 0:
Work_request_size = min (Tokens_available, Work_request_size _max) # Request work, up to a maximum limit
send_request_to_caller(Work_request_size) # Caller sends work if it has enough tokens
if Tokens_available >= Work_request_size:
send_work_to_service(Work_request_size)
Tokens_available = Tokens_available – Work_request_size
# Tokens are replenished at a certain rate
Tokens_available = min (Tokens_available + Token_Refresh_Rate, Token_Bucket_size)
主動調整
有時,事先知道系統很快就會不堪重負,於是採取主動措施,例如要求呼叫者降低呼叫量,然後再慢慢增加。設想這樣一個場景:下游服務宕機並拒絕了所有請求。在此期間,將所有工作排在隊列中,現在準備按照SLA將其清空。但是,如果以高於正常速率的速度清空隊列,就有可能導致下游服務癱瘓。為了解決這個問題,可以主動限制呼叫者的請求量,或與呼叫者溝通,要求其減少呼叫量,並慢慢放寬限制。
限流
限制服務能夠處理的請求數量,並丟棄超出此數量的請求。限流可以在服務層級或API層級實施。這種限流是對呼叫者的一種直接回饋,提示其降低呼叫量。可以進一步採取優先順序限流或公平限流策略,以確保對客戶的影響降到最低。
減載
限流是當違反某些預定義的限制時丟棄請求。如果服務面臨過大壓力並決定主動放棄已經承諾服務的請求,客戶請求仍然可以被丟棄。這種行為通常是服務保護自己並讓呼叫者知道它的最後手段。
結論
在分散式系統中,背壓是一個重要的挑戰,它會嚴重影響系統的效能和穩定性。了解背壓的原因和後果,以及掌握有效的管理技術,對於建立健壯且高效能的分散式系統至關重要。如果實施得當,背壓可以增強系統的穩定性、可靠性和可擴展性,從而提升使用者體驗。如果處理不當,可能會削弱客戶信任,甚至導致系統不穩定。透過仔細的系統設計和監控主動應對背壓是維護系統健康的關鍵。雖然實施背壓可能涉及一些權衡,例如可能影響吞吐量,但從整體系統彈性和使用者滿意度來看,其帶來的好處是巨大的。
原文標題:Backpressure in Distributed Systems,作者:Rajesh Pandey