有“韌性”才能更“任性”| 微軟雲原生韌性設計指南

微軟技術棧發表於2021-11-18

面對複雜多變的業務和運維環境,很多人絞盡腦汁想要維持業務的持續運轉。
然而很多時候,無論刪庫跑路導致企業丟失所有關鍵業務資料,或外部施工出錯挖斷光纜電線,甚至某些內部或外部基礎服務上一個小小的錯誤配置導致半個地球範圍內的服務中斷……所有這些或大或小的問題,總會讓很多人手忙腳亂處理半天,還會讓業務甚至企業聲譽遭受不小的影響。

雖然那句話說得好:破壞穩態的難度越大,我們對系統行為的信心就越強;並且只要能發現某個弱點,我們就有了一個改進目標

然而以往在本地部署和執行關鍵應用時,包括基礎架構、底層硬體在內的很多因素可由企業自行掌控,因此發現並解決弱點還是好處理的(也許需要投入大量資金和人力)。但當企業開始上雲,通過雲平臺執行這些關鍵應用時,底層基礎架構的管理和維護是由雲平臺承擔的,這時又該如何解決弱點,打造更穩定、更有韌性的運維環境和應用程式?

本文將從設計思路角度出發,告訴你如何提高雲原生應用的韌性,確保在遇到事件後業務依然能夠穩妥執行。


與遠端服務和資源通訊的所有應用程式必須對臨時性故障敏感。對於雲中執行的應用程式尤其如此,因為其環境的性質與通過網際網路建立連線的特點,意味著更容易遇到此類問題。臨時性故障包括客戶端和服務瞬間斷開網路連線、後臺服務暫時不可用,或者併發過大出現的超時等。這些錯誤通常是可以自我修復的,如果能把故障影響控制在一定範圍內,則可將對終端使用者的影響降至最低。

為什麼雲中會出現臨時性故障?

任何環境、任何平臺或作業系統以及任何型別的應用程式都會發生臨時性故障。在本地基礎架構上執行的解決方案中,應用程式及其元件的效能和可用性通常由昂貴且利用率不足的冗餘硬體來保證。雖然此方法使故障的可能性降低,但仍可能導致臨時性故障,甚至因外部電源/網路問題或其他災難情況等不可預測的事件而中斷。

託管型雲服務(PaaS)可以跨多個計算節點使用共享資源、冗餘、自動故障轉移和動態資源分配,實現更高的整體可用性。但是這些環境的性質意味著更可能發生臨時性故障,原因包括:

  • 雲環境中的許多資源是共享的,為了有效管理這些資源,雲通常會嚴格管控對這些資源的訪問。例如,某些服務在負載上升到特定級別,或到達吞吐量比率上限時,會拒絕額外連線以便處理現有請求,併為所有現存使用者維持服務效能。限制有助於為共享資源的鄰居與其他租戶維持服務質量。
  • 雲環境是使用大量商用硬體單元構建而成的。雲環境將負載動態分散到多個計算單元和基礎架構元件上以獲得更多效能,並通過自動回收或更換故障單元來提供可靠性。這種動態性意味著可能偶爾會發生臨時性故障或暫時性連線失敗。
  • 在應用程式與資源及其使用的服務之間通常有多個硬體元件,包括網路基礎架構,例如路由器和負載均衡器。這些附加的元件偶爾會導致額外的連線延遲或臨時性連線故障。
  • 客戶端與伺服器之間的網路狀況會不時改變,尤其是通過網際網路通訊時。即使在本地位置,高流量負載也可能減慢通訊速度,並造成間歇性的連線故障。

面臨的挑戰

臨時性故障可能會對使用者感知的可用性產生巨大影響,即使應用程式已在所有可預見的情況下進行了全面測試。若要確保雲託管的應用程式可靠執行,應用程式必須能夠應對以下挑戰:

  • 應用程式必須能夠檢測到故障的發生,並確定這些故障可能是臨時性的、永續性的還是終端故障。發生故障時,不同資源可能返回不同響應,這些響應可能會根據不同操作而有所不同。例如,針對從儲存讀取時所發生錯誤返回的響應,與針對寫入儲存時所發生錯誤返回的響應不同。許多資源和服務都妥善制定了臨時性故障的策略。但是若不提供此類資訊,則很難發現故障的性質,以及故障是否是臨時性的。
  • 如果確定故障可能是臨時性的,應用程式必須能夠重試操作,並跟蹤操作重試的次數。
  • 應用程式必須使用適當的重試策略。此策略指定應用程式應該重試的次數、每兩次嘗試的延遲時間,以及嘗試失敗後執行的操作。適當的嘗試次數以及每兩次嘗試的延遲時間通常難以確定,會根據資源型別以及應用程式本身的當前操作條件而有所不同。

韌性設計指南

以下指南將幫助您為應用程式設計合適的臨時性故障處理機制:

確定是否存在內建重試機制

  • 許多服務提供SDK或包含臨時性故障處理機制的客戶端庫。服務使用的重試策略通常是根據目標服務的性質和要求定製的。或者對於確定重試是否正確,以及在下一次嘗試重試之前要等待多長時間方面,服務的REST介面可能會返回有用的資訊。
  • 如果可用,請使用內建重試機制,除非有使不同重試行為更合適的具體且明確的要求。

確定操作是否適合重試

  • 應該僅在暫時性故障,以及在重新嘗試時至少有一定成功的可能性之下才進行重試操作。對於指示無效操作(如對不存在的項進行資料庫更新,或對發生致命錯誤的服務或資源的請求)的操作,重新嘗試是沒有意義的。
  • 一般而言,只有在能夠確定操作的全部影響並充分了解狀況並可進行驗證時,才建議實施重試。否則應該由呼叫程式碼來實施重試。請記住,從無法控制的資源與服務返回的錯誤可能會隨著時間而演進,可能需要重新建立訪問臨時性故障的檢測邏輯。
  • 建立服務或元件時,請考慮實現錯誤檢查程式碼和訊息處理,以幫助客戶端確定是否應該重試失敗的操作。特別是,指示客戶端是否應該重試該操作,並在下一次重試嘗試之前建議一個適當的延遲。如果構建Web服務,請考慮返回在服務契約中定義的自定義錯誤。即使通用客戶端可能無法讀取這些資訊,但在構建自定義客戶端時它們將非常有用。

確定適當的重試計數與間隔

  • 優化用例型別的重試計數和間隔是至關重要的。如果沒有重試足夠次數,應用程式將無法完成操作,並可能經歷失敗;如果重試過多次,或者重試間隔過短,應用程式可能會長期佔用執行緒、連線和記憶體等資源,這將對應用程式的執行狀況產生不利影響。
  • 時間間隔和重試次數的適當值取決於正在嘗試的操作型別。例如,如果操作是使用者互動的一部分,那麼間隔應該很短,並且只嘗試幾次,以避免讓使用者等待響應(這會保持開啟的連線並降低其他使用者的可用性;如果操作是長時間執行或關鍵工作流的一部分,其中取消和重新啟動流程是費時費力的,那麼在嘗試和重試之間等待更長時間是合適的。
  • 確定重試之間的適當間隔是設計成功策略中最困難的部分。典型策略使用以下型別的重試間隔:
    (a)指數延遲:應用程式在第一次重試之前短暫等待,每個後續重試的間隔時間呈指數增加。例如,在3秒、12秒、30秒後重試操作。
    (b)增量間隔:應用程式在第一次重試之前短暫等待,每個後續重試的間隔時間增量遞增。例如,在3秒、7秒、13秒後重試操作。
    (c)固定間隔:應用程式每次嘗試的間隔時間相同。例如,固定每3秒重試操作
    (d)立即重試:有時臨時性故障很短暫,可能是由於網路資料包衝突或硬體元件出現峰值等事件。在此情況下,適合立即重試操作,因為如果故障在操作讓應用程式組合併傳送下一個請求時已清除,則操作可能會成功。但是不應有多次立即重試嘗試,如果立即重試失敗,應切換到備用策略,例如指數退讓或回退操作。
    (f)隨機化:上面列出的任何重試策略都可能包括隨機化,以防止客戶機的多個例項同時傳送隨後的重試嘗試。例如,一個例項可能在3秒、11秒、28秒之後重試該操作,而另一個例項可能在4秒、12秒、26秒之後重試該操作。隨機化是一種有用的技術,可以與其他策略相結合。
  • 一般指導原則是,為後臺操作使用指數退讓策略,為互動式操作使用立即或固定間隔重試策略。在上述兩種情況下,應該選擇延遲與重試計數,使所有重試的延遲上限都在所需的端到端延遲要求範圍內。
  • 考慮影響重試操作的總的最大超時的所有因素的組合。這些因素包括失敗連線產生響應所花費的時間(通常由客戶端中的超時值設定)以及重試嘗試和最大重試次數之間的延遲。所有這些時間的總和可能會導致較長的總體操作時間,特別是當使用指數延遲策略時,其中重試間隔在每次失敗後迅速增長。如果流程必須滿足特定的服務水平協議SLA,則整個操作時間(包括所有超時和延遲)必須在SLA定義的限制內。
  • 過於激進的重試策略(間隔太短或重試太頻繁)可能會對目標資源或服務產生不利影響。這可能會阻止資源或服務從其過載狀態中恢復,並且它將繼續阻塞或拒絕請求。這導致了一個惡性迴圈,越來越多的請求被髮送到資源或服務,從而進一步降低了其恢復能力。
  • 在選擇重試間隔時,要考慮操作的超時時間,以避免立即啟動後續的嘗試(例如,如果超時時間與重試間隔類似)。還要考慮是否需要將可能的總時間(超時加上重試間隔)保持在特定的總時間以下。超時時間異常短或異常長的操作可能會影響等待多長時間以及重試操作的頻率。
  • 使用異常的型別和它包含的任何資料,或者從服務返回的錯誤程式碼和訊息,來優化重試的間隔和次數。例如,一些異常或錯誤程式碼(例如響應中帶有Retry-After頭的HTTP程式碼503 Service Unavailable)可能指示錯誤可能持續多長時間,或者服務已經失敗,不會響應任何後續的嘗試。

避免反模式

  • 在絕大多數情況下,應該避免包含重複重試程式碼層的實現。避免包含級聯重試機制的設計,或者在涉及請求層次結構的操作的每個階段實現重試的設計,除非有要求這樣做的特定需求。在這些異常情況下,請使用防止過多重試次數和延遲時間的策略,並確保理解其後果。例如,如果某個元件對另一個元件發出請求,後者再訪問目標服務,並且要對這兩個呼叫各實施重試三次,則總共會對該服務重試九次。許多服務和資源實施內建重試機制,如果需要在較高階別實施重試,應調查如何禁用或修改此設定。
  • 永遠不要實現無止境的重試機制。這可能會阻止資源或服務從過載情況中恢復,並導致節流和拒絕連線持續較長時間。使用有限的次數或重試,或實現一個模式(如斷路器),以允許服務恢復。
  • 立即重試不要超過一次。
  • 避免使用常規重試間隔,特別是當訪問Azure中的服務和資源時,有大量的重試嘗試時。在這種情況下,最優的方法是採用具有斷路能力的指數後退策略。
  • 防止同一客戶端的多個例項或不同客戶端的多個例項在同一時間傳送重試。如果可能發生這種情況,請在重試間隔中引入隨機化。

測試重試策略與實施

確保在儘可能廣泛的情況下全面測試重試策略實現,特別是當應用程式和它使用的目標資源或服務都處於極端負載下時。要在測試期間檢查行為,可以:

  • 將瞬態和非瞬態故障注入服務。例如,傳送無效請求或新增檢測測試請求的程式碼,並使用不同型別的錯誤進行響應。
  • 建立資源或服務的模擬,該模擬返回真實服務可能返回的一系列錯誤。確保覆蓋了重試策略旨在檢測的所有型別的錯誤。
  • 如果是自己建立和部署的自定義服務,則通過臨時禁用或過載該服務來強制發生瞬態錯誤(當然,我們不應該嘗試過載Azure中的任何共享資源或共享服務)。
  • 對於基於HTTP的API,可以考慮在自動化測試中使用FiddlerCore庫,通過新增額外的往返時間或更改響應(如HTTP狀態程式碼、頭、正文或其他因素)來更改HTTP請求的結果。這樣就可以對故障條件的子集進行確定性測試,無論是瞬時故障還是其他型別的故障。
  • 執行高負載係數和併發測試,以確保重試機制和策略在這些條件下正確工作,並且不會對客戶機的操作產生不利影響或導致請求之間的交叉汙染。

管理重試策略配置

  • 重試策略是重試策略的所有元素的組合。它定義了確定故障是否可能是暫時的檢測機制、要使用的間隔型別(如常規、指數後退和隨機化)、實際間隔值和重試次數。
  • 即使是最簡單的應用程式,也必須在許多地方實現重試,更復雜的應用程式的每一層都必須實現重試。考慮使用一箇中心點來儲存所有策略,而不是在多個位置硬編碼每個策略的元素。例如,將間隔和重試計數等值儲存在應用程式配置檔案中,在執行時讀取它們,並以程式設計方式構建重試策略。這使得管理設定、修改和微調值以響應不斷變化的需求和場景變得更加容易。但是,要設計系統來儲存這些值,而不是每次都重新讀取配置檔案,並確保在無法從配置中獲得這些值時使用合適的預設值。
  • 在Azure雲原生應用程式中,考慮將用於構建執行時重試策略的值儲存在服務配置檔案中,這樣就可以在不重啟應用程式的情況下更改它們。
  • 利用客戶端API中提供的內建或預設重試策略,但只在它們適合的場景使用。這些策略通常是通用的。在某些場景中,它們可能是所有必需的,但在其他場景中,它們可能不會提供所有選項來滿足特定需求。通過測試確定最合適的值,我們必須瞭解設定將如何影響應用程式。

記錄和跟蹤瞬態和非瞬態故障

  • 作為重試策略的一部分,包括異常處理和記錄重試嘗試時的其他檢測。雖然偶爾出現短暫的故障和重試是可以預期的,並且並不表明有問題,但定期的和不斷增加的重試次數通常是一個可能導致故障的問題的指示器,或者當前正在降低應用程式的效能和可用性。
  • 將瞬態故障記錄為警告項而不是錯誤項,以便監控系統不會將它們檢測為可能觸發錯誤警報的應用程式錯誤。
  • 考慮在日誌條目中儲存一個值,該值指示重試是由服務中的節流引起的,還是由其他型別的錯誤(如連線失敗)引起的,以便在分析資料時對它們進行區分。節流錯誤數量的增加通常表明應用程式存在設計缺陷,或者需要轉向提供專用硬體的優質服務。
  • 考慮測量和記錄包含重試機制的操作所花費的總時間。這是暫時性錯誤對使用者響應時間、處理延遲和應用程式用例效率的總體影響的一個很好的指示器。還要記錄發生的重試次數,以便了解影響響應時間的因素。
  • 考慮實現一個遙測和監控系統,當失敗的數量和比率、平均重試次數或操作成功所需的總時間增加時,該系統可以發出警報。

管理持續失敗的操作

  • 在某些情況下,每次行動都會失敗,考慮如何處理這種情況是至關重要的。
  • 儘管重試策略將定義一個操作應該重試的最大次數,但它不會阻止應用程式以相同的重試次數再次重複該操作。例如,如果一個訂單處理服務由於一個致命錯誤而失敗,使其永久停止操作,重試策略可能會檢測到連線超時,並認為這是一個暫時的錯誤。程式碼將重試指定次數的操作,然後放棄。然而當另一個客戶下訂單時,該操作將再次嘗試——即使每次都肯定會失敗。
  • 為了防止對不斷失敗的操作進行不斷重試,考慮實現斷路器模式。在此模式中,如果指定時間視窗內的失敗次數超過閾值,則請求將立即作為錯誤返回給呼叫者,而不嘗試訪問失敗的資源或服務。
  • 應用程式可以週期性地測試服務(斷斷續續的,請求之間的間隔很長),以檢測服務何時可用。適當的間隔取決於場景,例如操作的關鍵程度和服務的性質,可能是幾分鐘到幾個小時之間的任何時間。在測試成功時,應用程式可以恢復正常操作,並將請求傳遞給新恢復的服務。
  • 與此同時,可以退回到服務的另一個例項(可能在不同的資料中心或應用程式中),使用提供相容(可能更簡單)功能的類似服務,或者執行一些替代操作,希望該服務很快可用。例如,可以將服務請求儲存在佇列或資料儲存中,稍後再重放它們。否則我們可能會將使用者重定向到應用程式的另一個例項,降低應用程式的效能,但仍然提供可接受的功能,或者只是向使用者返回一條訊息,指示該應用程式目前不可用。

其他的考慮

  • 在決定重試次數和策略重試間隔的值時,請考慮服務或資源上的操作是否是長時間執行或多步驟操作的一部分。當一個操作步驟失敗時,補償所有其他已經成功的操作步驟可能是困難的或昂貴的。在這種情況下,很長的間隔和大量的重試是可以接受的,只要它不通過持有或鎖定稀缺資源來阻塞其他操作。
  • 請考慮重試同一操作是否可能導致資料不一致。如果重複多步驟流程的某些部分,且操作不是冪等的,則可能導致不一致。例如,遞增值的操作如果重複,將產生無效的結果。如果無法檢測到重複的訊息,重複將訊息傳送到佇列的操作可能會導致訊息使用者中出現不一致。要防止這種情況,請確保將每個步驟設計為冪等操作。
  • 考慮將要重試的操作的範圍。例如,在包含多個操作的級別上實現重試程式碼可能更容易,如果其中一個操作失敗,則重試所有操作。但是,這樣做可能會導致冪等問題或不必要的回滾操作。
  • 如果選擇一個包含多個操作的重試範圍,那麼在確定重試間隔、監視所花費的時間以及發出失敗警報之前,請考慮所有操作的總延遲。

在為雲原生應用考慮韌性設計時,請務必慎重思索重試策略可能會如何影響共享應用程式中的鄰居和其他租戶。激進的重試策略可能導致其他使用者以及共享資源和服務的應用程式出現越來越多的瞬時錯誤。

同樣,我們的應用程式可能會受到資源和服務的其他使用者所實現的重試策略影響。對於關鍵任務應用程式,我們可以決定使用不共享的高階服務。這為我們提供了更多的負載控制以及相應資源和服務的節流,這有助於證明額外的成本是合理的。

遵循上述思路,並結合具體情況進行調整,我們就可以順利設計出具備足夠韌性的雲原生應用架構。


希望本文對你能有所幫助。如果對這個話題感興趣,那麼敬請期待後續釋出的更多系列文章,我們將繼續從重試模式、斷路器模式等角度深入探討相關機制的實現思路和方法。更多精彩敬請期待!

相關文章