失敗難免發生

一項服務或系統彼此叫用時,難免會發生失敗。失敗可能來自諸多因素。包括伺服器、網路、負載平衡器、軟體、作業系統,甚至系統操作人員的錯誤。我們設計減少失敗機率的系統,但想要建立從不失敗的系統是緣木求魚。因此在 Amazon,我們設計可容受和減少失敗機率的系統,並避免讓原本低比例的失敗成為全面性中斷故障。為了建立具有彈性的系統,我們有三項法寶:逾時、重試和退避。

許多失敗類型的表現方式是請求花費的時間超過平常,且可能永遠無法完成。用戶端等候請求完成的時間比平常久的時候,佔用該請求所用資源的時間也會拉長。一旦許多請求長時間佔用資源,伺服器的這些資源就可能耗用殆盡。資源包括記憶體、執行緒、連接、暫時性連接埠或其他任何有限度的資源。為了避免此情境,用戶端會設定逾時。逾時是用戶端等候請求完成的時間上限。

通常,再次嘗試相同請求可讓請求成功。這是因為我們所建立系統的類型,作為單一單位時不常失敗,而是苦於部分或暫時性失敗。部分失敗是指請求有某個比例成功。暫時性失敗是指請求短時間失敗。重試透過再次傳送相同請求,讓面臨隨機部分失敗和短暫暫時性失敗的用戶端正常執行。

重試有時並不安全。如果系統已經因為應用程式過載而失敗,重試可能增加被叫用系統的負載。為了避免此問題,我們會實作用戶端以使用退避。這樣做可以延長後續重試的間隔時間,讓後端負載保持均衡。重試的其他問題還包括某些遠端叫用帶來的負面影響。逾時或失敗不見得表示該負面影響尚未發生。如果不樂見負面影響多次出現,最佳作法是將 API 設計為等冪,意思是可安全重試。

最後,抵達 Amazon 服務的流量速率並不穩定,請求的抵達速率常大量暴增。暴增的原因可能是用戶端的行為、失敗復原,甚至是簡單的定期 Cron 工作所造成。如果是負載造成錯誤,如果所有用戶端同時重試,重試可能會失效。為了避免此問題,我們採用抖動。這是做出或重試請求之前的隨機時間量,可將抵達速率分散,有助避免流量大量暴增。

前述各項解決方案,會在各自的後續章節中詳細討論。

逾時

Amazon 所採用的其中一項最佳作法,是為任何遠端叫用設定逾時,並且針對跨程序(即使在同一方塊中)的任何叫用設定一般逾時。包括連線逾時和請求逾時。許多標準用戶端提供強大的內建逾時功能。
一般而言,最困難的問題是選擇要設定的逾時值。設定的逾時過高會減損其實用性,因為用戶端在等候逾時的時候,資源仍在消耗。設定的逾時過低則有兩個風險:
 
• 增加後端流量及延遲,因為重試的請求過多。
• 後端延遲小幅增加即導致完全中斷故障,因為所有請求都開始重試。
 
針對 AWS 區域內叫用選擇逾時的良好作法是從下游服務的延遲數據開始著手。因此在 Amazon,我們讓一項服務叫用另一項服務時,會選擇可接受的偽逾時率(例如 0.1%)。接著,我們會查看下游服務相應的延遲百分位數(本例為 p99.9)。此作法在多數案例中運作良好,但其中也有一些陷阱,詳述如下:
 
• 用戶端遭遇網路大量延遲時(例如透過網際網路),此法並無效果。在這些案例中,我們會合理考量最差的網路延遲狀況,別忘了,用戶端可能位於全球各地。
• 服務的延遲界線緊繃時(此時 p99.9 與 p50 很接近),此法也沒有效果。在這些案例中,加入填補值有助我們避免造成大量逾時的小幅延遲增加。
• 我們在實作逾時的時候,遭遇到一個常見的陷阱。Linux 的 SO_RCVTIMEO 功能固然強大,但有一些缺點使它不適合作為端對端通訊端逾時。有些語言(如 Java)直接採用此控制項。有些語言(如 Go)則提供更為強大的逾時機制。
• 另外,在某些實作中,逾時並未涵蓋所有遠端叫用,例如 DNS 或 TLS 交握。一般而言,我們偏好使用建立於測試良好用戶端的逾時。如果我們實作自家的逾時,會特別注意逾時通訊端選項的確切意義,以及進行的工作內容。
 
我曾經在 Amazon 處理過一套系統,當時我們在部署後立即發現出現少量相依性對話逾時。逾時設定值非常低,約為 20 毫秒。在部署之外,就算逾時值這麼低,我們也沒有看見固定發生的逾時。深入探究之後,我發現系統中包含的計時器建立了一個新的安全連線,且被後續的請求重複使用。因為建立連線花費的時間超過 20 毫秒,所以我們在部署完成新伺服器開始服務後,才會發現少量的請求逾時。在某些案例中,請求重試並成功。我們一開始的解決方法是拉高建立連線時的逾時值。後來,我們透過在程序啟動時、接收流量之前建立連線的方式改進系統。種種方式,讓我們得以解決逾時問題。

重試與退避

重試是「自利的」。 也就是說,用戶端重試時,會花費較多伺服器的時間換取較高成功率。在少有失敗或為暫時性時,這一點不成問題。原因是重試請求的整體次數少,而且付出增加實際可用性的代價也還說得過去。因過載導致失敗時,會提高負載的重試可能讓事態嚴重惡化。甚至可能在原本的問題解決之後,仍長時間維持高負載,導致復原延遲。重試就像特效藥,劑量恰到好處時效果良好,一旦服藥過量,也可能造成嚴重損傷。可惜的是,在分散式系統中,幾乎沒辦法協調所有用戶端,以達成適當的重試次數。

Amazon 偏好使用的解決方案是退避。用戶端不立刻積極重試,而在兩次重試之間等待某段時間。最常見的型態是指數退避,也就是在每次嘗試之後,等待的時間呈指數增加。指數退避可能造成非常長的退避時間,因為指數函數的成長速度很快。為了避免重試時間過長,實作通常會設定退避的最大值。顧名思義,就稱為指數退避上限。但是,這就引發了另一個問題。現在所有用戶端都以上限速率持續重試。幾乎在所有案例中,我們的解決方案都是限制用戶端重試的次數,並在服務導向架構中,及早處理造成的失敗。在多數案例中,用戶端終究會放棄叫用,因為它有自己的逾時設定。

重試還有以下其他問題:

• 分散式系統通常有多層。試想這樣一套系統:客戶的叫用會導致堆疊深度達到五層的服務叫用。以資料庫查詢結束,且每一層會重試三次。資料庫承受負載後若開始查詢失敗,會發生什麼事? 如果每一層獨立重試,資料庫的負載會增加 243 倍,因此幾乎不可能復原。這是因為每一層的重試會倍增 -- 剛開始是三次,然後九次,以此類推。相對的,堆疊最高層的重試可能浪費先前叫用的工作成果,因此影響效率。一般而言,對於低成本的控制平面和資料平面操作,我們的最佳作法是在堆疊中的單點重試。
• 負載。即使單層重試,在錯誤開始時,流量還是可能顯著增加。斷路器會在超過錯誤閾值時完全停止叫用下游服務,在解決這個問題上被廣為運用。可惜的是,斷路器會在系統中引入可能難以測試的強制回應行為,並大幅延長額外復原時間。我們已經發現使用字符桶限制本機重試,可以減緩此風險。這樣一來,只要有字符存在,就允許所有叫用重試,等到字符用盡之後,再以固定速率重試。AWS 於 2016 年在 AWS SDK 中加入此行為。因此,使用 SDK 的客戶可以享有此內建調節行為
• 決定何時重試。一般而言,我們的法是伴隨負面影響的 API 重試並不安全,除非提供冪等。無論重試有多頻繁,這樣做可以保證負面影響只發生一次。唯讀 API 通常為等冪,而資源建立 API 可能不是。有些 API,例如 Amazon Elastic Compute Cloud (Amazon EC2) RunInstances API,提供明確的字符式機制來提供冪等,使其可安全重試。優秀的 API 設計及實作用戶端時的細膩度,是預防負面影響反覆出現的要素。
• 確認哪些失敗值得重試。HTTP 清楚界定了用戶端伺服器錯誤的區別。據其指出,用戶端錯誤不應以相同的請求重試,因為之後也不會成功,但伺服器錯誤後續嘗試後可能成功。很可惜,系統的最終一致性使這條界線模糊許多。隨著狀態傳播,當下的用戶端錯誤轉眼可能成功。

儘管有前述風險與挑戰,重試仍是一套強大的機制,可在面對暫時性和隨機錯誤時提供高可用性。為了找出每項服務適合的妥協,判斷力是必要的。根據我們的經驗,記住重試的自利性質是很好的起始點。對用戶端來說,重試是主張其請求重要性的一種方式,並要求服務耗費更多資源予以處理。如果用戶端過度自利,造成的問題牽連甚廣。

抖動

如因過載或爭用導致失敗,退避帶來的幫助可能不如預期。原因是相互關聯性。如果所有失敗的叫用退避至同一時間,會在它們重試時再次造成爭用或過載。我們的解決之道是抖動。抖動可增加一定程度的退避隨機性,將重試分散到不同時間。如須有關增加多少抖動及最佳增加方式的資訊,請參閱指數退避與抖動

抖動不只適用於重試。我們從操作經驗得知,我們服務的流量(包括控制平面和資料平面)有大量集中於尖峰的傾向。流量尖峰可能非常短,且經常被彙總數據淹沒。在建立系統時,我們會考慮在所有計時器、定期工作和其他延遲工作中加入一些抖動。這樣做有助分散工作尖峰,讓下游服務更容易依工作負載擴展。

在排程工作中加入抖動時,我們不是隨機選擇每台主機的抖動。而是使用一致的方法,每次在同一台主機上產出相同的數字。如此一來,若有服務過載或競速狀況,會以同樣型態發生。我們人類善於辨別型態,而且比較可能判定根本原因。使用隨機方法可確保若資源無法負荷,此情況只會隨機發生。因此,故障排除難上加難。

在我曾經工作的系統上,如 Amazon Elastic Block Store (Amazon EBS) 和 AWS Lambda,我們發現用戶端以固定間隔頻頻傳送請求,像是每分鐘一次。不過,用戶端若有多部伺服器表現出相同的行為,就可以排隊並同時觸發其請求。時機可以是一分鐘的前幾秒,或針對每日工作,設定為午夜剛過的幾秒鐘。藉由注意每秒負載以及與用戶端合作,在定期工作負載中加入抖動等方式,我們以較少的伺服器容量完成相同工作量。

我們對客戶流量尖峰的掌控力較低。但是,即使是客戶觸發任務,在不影響客戶經驗的前提下,加入抖動仍是不錯的概念。

結論

在分散式系統中,遠端互動發生暫時性失敗或延遲避免不了。逾時可避免系統不上不下的時間不合理的久,重試可以遮蓋這些失敗,退避和抖動則可提高使用率並減少系統壅塞。

在 Amazon,我們學會務必謹慎處理重試。重試可能放大相依系統的負載。如果叫用系統逾時,且該系統過載,重試就可能使過載雪上加霜,而不是好轉。我們只在得知相依性健全時才重試,以此方式避免事態放大。重試無助於改善可用性時,我們則讓其停止。


作者簡介

Marc Brooker 是 Amazon Web Services 的資深首席工程師。他自 2008 年起即進 AWS 工作,從事包括 EC2、EBS 和 IoT 等多項服務。目前,他專注在 AWS Lambda,工作包括擴展和虛擬化。Marc 醉心於閱讀 COE 和事後剖析。他擁有電氣工程的博士學位。

分散式系統的相關挑戰 使用負載卸除以免過載 在分散式系統中避免回復