面對復(fù)雜多變的業(yè)務(wù)和運(yùn)維環(huán)境,很多人絞盡腦汁想要維持業(yè)務(wù)的持續(xù)運(yùn)轉(zhuǎn)。
然而很多時候,無論刪庫跑路導(dǎo)致企業(yè)丟失所有關(guān)鍵業(yè)務(wù)數(shù)據(jù),或外部施工出錯挖斷光纜電線,甚至某些內(nèi)部或外部基礎(chǔ)服務(wù)上一個小小的錯誤配置導(dǎo)致半個地球范圍內(nèi)的服務(wù)中斷……所有這些或大或小的問題,總會讓很多人手忙腳亂處理半天,還會讓業(yè)務(wù)甚至企業(yè)聲譽(yù)遭受不小的影響。
雖然那句話說得好:破壞穩(wěn)態(tài)的難度越大,我們對系統(tǒng)行為的信心就越強(qiáng);并且只要能發(fā)現(xiàn)某個弱點(diǎn),我們就有了一個改進(jìn)目標(biāo)。
然而以往在本地部署和運(yùn)行關(guān)鍵應(yīng)用時,包括基礎(chǔ)架構(gòu)、底層硬件在內(nèi)的很多因素可由企業(yè)自行掌控,因此發(fā)現(xiàn)并解決弱點(diǎn)還是好處理的(也許需要投入大量資金和人力)。但當(dāng)企業(yè)開始上云,通過云平臺運(yùn)行這些關(guān)鍵應(yīng)用時,底層基礎(chǔ)架構(gòu)的管理和維護(hù)是由云平臺承擔(dān)的,這時又該如何解決弱點(diǎn),打造更穩(wěn)定、更有韌性的運(yùn)維環(huán)境和應(yīng)用程序?
本文將從設(shè)計(jì)思路角度出發(fā),告訴你如何提高云原生應(yīng)用的韌性,確保在遇到事件后業(yè)務(wù)依然能夠穩(wěn)妥運(yùn)行。
與遠(yuǎn)程服務(wù)和資源通信的所有應(yīng)用程序必須對臨時性故障敏感。對于云中運(yùn)行的應(yīng)用程序尤其如此,因?yàn)槠洵h(huán)境的性質(zhì)與通過互聯(lián)網(wǎng)建立連接的特點(diǎn),意味著更容易遇到此類問題。臨時性故障包括客戶端和服務(wù)瞬間斷開網(wǎng)絡(luò)連接、后臺服務(wù)暫時不可用,或者并發(fā)過大出現(xiàn)的超時等。這些錯誤通常是可以自我修復(fù)的,如果能把故障影響控制在一定范圍內(nèi),則可將對最終用戶的影響降至最低。
為什么云中會出現(xiàn)臨時性故障?
任何環(huán)境、任何平臺或操作系統(tǒng)以及任何類型的應(yīng)用程序都會發(fā)生臨時性故障。在本地基礎(chǔ)架構(gòu)上運(yùn)行的解決方案中,應(yīng)用程序及其組件的性能和可用性通常由昂貴且利用率不足的冗余硬件來保證。雖然此方法使故障的可能性降低,但仍可能導(dǎo)致臨時性故障,甚至因外部電源/網(wǎng)絡(luò)問題或其他災(zāi)難情況等不可預(yù)測的事件而中斷。
托管型云服務(wù)(PaaS)可以跨多個計(jì)算節(jié)點(diǎn)使用共享資源、冗余、自動故障轉(zhuǎn)移和動態(tài)資源分配,實(shí)現(xiàn)更高的整體可用性。但是這些環(huán)境的性質(zhì)意味著更可能發(fā)生臨時性故障,原因包括:
云環(huán)境中的許多資源是共享的,為了有效管理這些資源,云通常會嚴(yán)格管控對這些資源的訪問。例如,某些服務(wù)在負(fù)載上升到特定級別,或到達(dá)吞吐量比率上限時,會拒絕額外連接以便處理現(xiàn)有請求,并為所有現(xiàn)存用戶維持服務(wù)性能。限制有助于為共享資源的鄰居與其他租戶維持服務(wù)質(zhì)量。
云環(huán)境是使用大量商用硬件單元構(gòu)建而成的。云環(huán)境將負(fù)載動態(tài)分散到多個計(jì)算單元和基礎(chǔ)架構(gòu)組件上以獲得更多性能,并通過自動回收或更換故障單元來提供可靠性。這種動態(tài)性意味著可能偶爾會發(fā)生臨時性故障或暫時性連接失敗。
在應(yīng)用程序與資源及其使用的服務(wù)之間通常有多個硬件組件,包括網(wǎng)絡(luò)基礎(chǔ)架構(gòu),例如路由器和負(fù)載均衡器。這些附加的組件偶爾會導(dǎo)致額外的連接延遲或臨時性連接故障。
客戶端與服務(wù)器之間的網(wǎng)絡(luò)狀況會不時改變,尤其是通過互聯(lián)網(wǎng)通信時。即使在本地位置,高流量負(fù)載也可能減慢通信速度,并造成間歇性的連接故障。
面臨的挑戰(zhàn)
臨時性故障可能會對用戶感知的可用性產(chǎn)生巨大影響,即使應(yīng)用程序已在所有可預(yù)見的情況下進(jìn)行了全面測試。若要確保云托管的應(yīng)用程序可靠運(yùn)行,應(yīng)用程序必須能夠應(yīng)對以下挑戰(zhàn):
應(yīng)用程序必須能夠檢測到故障的發(fā)生,并確定這些故障可能是臨時性的、持久性的還是終端故障。發(fā)生故障時,不同資源可能返回不同響應(yīng),這些響應(yīng)可能會根據(jù)不同操作而有所不同。例如,針對從存儲讀取時所發(fā)生錯誤返回的響應(yīng),與針對寫入存儲時所發(fā)生錯誤返回的響應(yīng)不同。許多資源和服務(wù)都妥善制定了臨時性故障的策略。但是若不提供此類信息,則很難發(fā)現(xiàn)故障的性質(zhì),以及故障是否是臨時性的。
如果確定故障可能是臨時性的,應(yīng)用程序必須能夠重試操作,并跟蹤操作重試的次數(shù)。
應(yīng)用程序必須使用適當(dāng)?shù)闹卦嚥呗?。此策略指定?yīng)用程序應(yīng)該重試的次數(shù)、每兩次嘗試的延遲時間,以及嘗試失敗后執(zhí)行的操作。適當(dāng)?shù)膰L試次數(shù)以及每兩次嘗試的延遲時間通常難以確定,會根據(jù)資源類型以及應(yīng)用程序本身的當(dāng)前操作條件而有所不同。
韌性設(shè)計(jì)指南
以下指南將幫助您為應(yīng)用程序設(shè)計(jì)合適的臨時性故障處理機(jī)制:
確定是否存在內(nèi)置重試機(jī)制
許多服務(wù)提供SDK或包含臨時性故障處理機(jī)制的客戶端庫。服務(wù)使用的重試策略通常是根據(jù)目標(biāo)服務(wù)的性質(zhì)和要求定制的。或者對于確定重試是否正確,以及在下一次嘗試重試之前要等待多長時間方面,服務(wù)的REST接口可能會返回有用的信息。
如果可用,請使用內(nèi)置重試機(jī)制,除非有使不同重試行為更合適的具體且明確的要求。
確定操作是否適合重試
應(yīng)該僅在暫時性故障,以及在重新嘗試時至少有一定成功的可能性之下才進(jìn)行重試操作。對于指示無效操作(如對不存在的項(xiàng)進(jìn)行數(shù)據(jù)庫更新,或?qū)Πl(fā)生致命錯誤的服務(wù)或資源的請求)的操作,重新嘗試是沒有意義的。
一般而言,只有在能夠確定操作的全部影響并充分了解狀況并可進(jìn)行驗(yàn)證時,才建議實(shí)施重試。否則應(yīng)該由調(diào)用代碼來實(shí)施重試。請記住,從無法控制的資源與服務(wù)返回的錯誤可能會隨著時間而演進(jìn),可能需要重新建立訪問臨時性故障的檢測邏輯。
創(chuàng)建服務(wù)或組件時,請考慮實(shí)現(xiàn)錯誤檢查代碼和消息處理,以幫助客戶端確定是否應(yīng)該重試失敗的操作。特別是,指示客戶端是否應(yīng)該重試該操作,并在下一次重試嘗試之前建議一個適當(dāng)?shù)难舆t。如果構(gòu)建Web服務(wù),請考慮返回在服務(wù)契約中定義的自定義錯誤。即使通用客戶端可能無法讀取這些信息,但在構(gòu)建自定義客戶端時它們將非常有用。
確定適當(dāng)?shù)闹卦囉?jì)數(shù)與間隔
優(yōu)化用例類型的重試計(jì)數(shù)和間隔是至關(guān)重要的。如果沒有重試足夠次數(shù),應(yīng)用程序?qū)o法完成操作,并可能經(jīng)歷失敗;如果重試過多次,或者重試間隔過短,應(yīng)用程序可能會長期占用線程、連接和內(nèi)存等資源,這將對應(yīng)用程序的運(yùn)行狀況產(chǎn)生不利影響。
時間間隔和重試次數(shù)的適當(dāng)值取決于正在嘗試的操作類型。例如,如果操作是用戶交互的一部分,那么間隔應(yīng)該很短,并且只嘗試幾次,以避免讓用戶等待響應(yīng)(這會保持打開的連接并降低其他用戶的可用性;如果操作是長時間運(yùn)行或關(guān)鍵工作流的一部分,其中取消和重新啟動流程是費(fèi)時費(fèi)力的,那么在嘗試和重試之間等待更長時間是合適的。
確定重試之間的適當(dāng)間隔是設(shè)計(jì)成功策略中最困難的部分。典型策略使用以下類型的重試間隔:
(a)指數(shù)延遲:應(yīng)用程序在第一次重試之前短暫等待,每個后續(xù)重試的間隔時間呈指數(shù)增加。例如,在3秒、12秒、30秒后重試操作。
(b)增量間隔:應(yīng)用程序在第一次重試之前短暫等待,每個后續(xù)重試的間隔時間增量遞增。例如,在3秒、7秒、13秒后重試操作。
(c)固定間隔:應(yīng)用程序每次嘗試的間隔時間相同。例如,固定每3秒重試操作
(d)立即重試:有時臨時性故障很短暫,可能是由于網(wǎng)絡(luò)數(shù)據(jù)包沖突或硬件組件出現(xiàn)峰值等事件。在此情況下,適合立即重試操作,因?yàn)槿绻收显诓僮髯寫?yīng)用程序組合并發(fā)送下一個請求時已清除,則操作可能會成功。但是不應(yīng)有多次立即重試嘗試,如果立即重試失敗,應(yīng)切換到備用策略,例如指數(shù)退讓或回退操作。
(f)隨機(jī)化:上面列出的任何重試策略都可能包括隨機(jī)化,以防止客戶機(jī)的多個實(shí)例同時發(fā)送隨后的重試嘗試。例如,一個實(shí)例可能在3秒、11秒、28秒之后重試該操作,而另一個實(shí)例可能在4秒、12秒、26秒之后重試該操作。隨機(jī)化是一種有用的技術(shù),可以與其他策略相結(jié)合。
一般指導(dǎo)原則是,為后臺操作使用指數(shù)退讓策略,為交互式操作使用立即或固定間隔重試策略。在上述兩種情況下,應(yīng)該選擇延遲與重試計(jì)數(shù),使所有重試的延遲上限都在所需的端到端延遲要求范圍內(nèi)。
考慮影響重試操作的總的最大超時的所有因素的組合。這些因素包括失敗連接產(chǎn)生響應(yīng)所花費(fèi)的時間(通常由客戶端中的超時值設(shè)置)以及重試嘗試和最大重試次數(shù)之間的延遲。所有這些時間的總和可能會導(dǎo)致較長的總體操作時間,特別是當(dāng)使用指數(shù)延遲策略時,其中重試間隔在每次失敗后迅速增長。如果流程必須滿足特定的服務(wù)水平協(xié)議SLA,則整個操作時間(包括所有超時和延遲)必須在SLA定義的限制內(nèi)。
過于激進(jìn)的重試策略(間隔太短或重試太頻繁)可能會對目標(biāo)資源或服務(wù)產(chǎn)生不利影響。這可能會阻止資源或服務(wù)從其重載狀態(tài)中恢復(fù),并且它將繼續(xù)阻塞或拒絕請求。這導(dǎo)致了一個惡性循環(huán),越來越多的請求被發(fā)送到資源或服務(wù),從而進(jìn)一步降低了其恢復(fù)能力。
在選擇重試間隔時,要考慮操作的超時時間,以避免立即啟動后續(xù)的嘗試(例如,如果超時時間與重試間隔類似)。還要考慮是否需要將可能的總時間(超時加上重試間隔)保持在特定的總時間以下。超時時間異常短或異常長的操作可能會影響等待多長時間以及重試操作的頻率。
使用異常的類型和它包含的任何數(shù)據(jù),或者從服務(wù)返回的錯誤代碼和消息,來優(yōu)化重試的間隔和次數(shù)。例如,一些異常或錯誤代碼(例如響應(yīng)中帶有Retry-After頭的HTTP代碼503 Service Unavailable)可能指示錯誤可能持續(xù)多長時間,或者服務(wù)已經(jīng)失敗,不會響應(yīng)任何后續(xù)的嘗試。
避免反模式
在絕大多數(shù)情況下,應(yīng)該避免包含重復(fù)重試代碼層的實(shí)現(xiàn)。避免包含級聯(lián)重試機(jī)制的設(shè)計(jì),或者在涉及請求層次結(jié)構(gòu)的操作的每個階段實(shí)現(xiàn)重試的設(shè)計(jì),除非有要求這樣做的特定需求。在這些異常情況下,請使用防止過多重試次數(shù)和延遲時間的策略,并確保理解其后果。例如,如果某個組件對另一個組件發(fā)出請求,后者再訪問目標(biāo)服務(wù),并且要對這兩個調(diào)用各實(shí)施重試三次,則總共會對該服務(wù)重試九次。許多服務(wù)和資源實(shí)施內(nèi)置重試機(jī)制,如果需要在較高級別實(shí)施重試,應(yīng)調(diào)查如何禁用或修改此設(shè)置。
永遠(yuǎn)不要實(shí)現(xiàn)無止境的重試機(jī)制。這可能會阻止資源或服務(wù)從過載情況中恢復(fù),并導(dǎo)致節(jié)流和拒絕連接持續(xù)較長時間。使用有限的次數(shù)或重試,或?qū)崿F(xiàn)一個模式(如斷路器),以允許服務(wù)恢復(fù)。
立即重試不要超過一次。
避免使用常規(guī)重試間隔,特別是當(dāng)訪問Azure中的服務(wù)和資源時,有大量的重試嘗試時。在這種情況下,最優(yōu)的方法是采用具有斷路能力的指數(shù)后退策略。
防止同一客戶端的多個實(shí)例或不同客戶端的多個實(shí)例在同一時間發(fā)送重試。如果可能發(fā)生這種情況,請?jiān)谥卦囬g隔中引入隨機(jī)化。
測試重試策略與實(shí)施
確保在盡可能廣泛的情況下全面測試重試策略實(shí)現(xiàn),特別是當(dāng)應(yīng)用程序和它使用的目標(biāo)資源或服務(wù)都處于極端負(fù)載下時。要在測試期間檢查行為,可以:
將瞬態(tài)和非瞬態(tài)故障注入服務(wù)。例如,發(fā)送無效請求或添加檢測測試請求的代碼,并使用不同類型的錯誤進(jìn)行響應(yīng)。
創(chuàng)建資源或服務(wù)的模擬,該模擬返回真實(shí)服務(wù)可能返回的一系列錯誤。確保覆蓋了重試策略旨在檢測的所有類型的錯誤。
如果是自己創(chuàng)建和部署的自定義服務(wù),則通過臨時禁用或重載該服務(wù)來強(qiáng)制發(fā)生瞬態(tài)錯誤(當(dāng)然,我們不應(yīng)該嘗試重載Azure中的任何共享資源或共享服務(wù))。
對于基于HTTP的API,可以考慮在自動化測試中使用FiddlerCore庫,通過添加額外的往返時間或更改響應(yīng)(如HTTP狀態(tài)代碼、頭、正文或其他因素)來更改HTTP請求的結(jié)果。這樣就可以對故障條件的子集進(jìn)行確定性測試,無論是瞬時故障還是其他類型的故障。
執(zhí)行高負(fù)載系數(shù)和并發(fā)測試,以確保重試機(jī)制和策略在這些條件下正確工作,并且不會對客戶機(jī)的操作產(chǎn)生不利影響或?qū)е抡埱笾g的交叉污染。
管理重試策略配置
重試策略是重試策略的所有元素的組合。它定義了確定故障是否可能是暫時的檢測機(jī)制、要使用的間隔類型(如常規(guī)、指數(shù)后退和隨機(jī)化)、實(shí)際間隔值和重試次數(shù)。
即使是最簡單的應(yīng)用程序,也必須在許多地方實(shí)現(xiàn)重試,更復(fù)雜的應(yīng)用程序的每一層都必須實(shí)現(xiàn)重試。考慮使用一個中心點(diǎn)來存儲所有策略,而不是在多個位置硬編碼每個策略的元素。例如,將間隔和重試計(jì)數(shù)等值存儲在應(yīng)用程序配置文件中,在運(yùn)行時讀取它們,并以編程方式構(gòu)建重試策略。這使得管理設(shè)置、修改和微調(diào)值以響應(yīng)不斷變化的需求和場景變得更加容易。但是,要設(shè)計(jì)系統(tǒng)來存儲這些值,而不是每次都重新讀取配置文件,并確保在無法從配置中獲得這些值時使用合適的默認(rèn)值。
在Azure云原生應(yīng)用程序中,考慮將用于構(gòu)建運(yùn)行時重試策略的值存儲在服務(wù)配置文件中,這樣就可以在不重啟應(yīng)用程序的情況下更改它們。
利用客戶端API中提供的內(nèi)置或默認(rèn)重試策略,但只在它們適合的場景使用。這些策略通常是通用的。在某些場景中,它們可能是所有必需的,但在其他場景中,它們可能不會提供所有選項(xiàng)來滿足特定需求。通過測試確定最合適的值,我們必須了解設(shè)置將如何影響應(yīng)用程序。
記錄和跟蹤瞬態(tài)和非瞬態(tài)故障
作為重試策略的一部分,包括異常處理和記錄重試嘗試時的其他檢測。雖然偶爾出現(xiàn)短暫的故障和重試是可以預(yù)期的,并且并不表明有問題,但定期的和不斷增加的重試次數(shù)通常是一個可能導(dǎo)致故障的問題的指示器,或者當(dāng)前正在降低應(yīng)用程序的性能和可用性。
將瞬態(tài)故障記錄為警告項(xiàng)而不是錯誤項(xiàng),以便監(jiān)控系統(tǒng)不會將它們檢測為可能觸發(fā)錯誤警報(bào)的應(yīng)用程序錯誤。
考慮在日志條目中存儲一個值,該值指示重試是由服務(wù)中的節(jié)流引起的,還是由其他類型的錯誤(如連接失?。┮鸬模员阍诜治鰯?shù)據(jù)時對它們進(jìn)行區(qū)分。節(jié)流錯誤數(shù)量的增加通常表明應(yīng)用程序存在設(shè)計(jì)缺陷,或者需要轉(zhuǎn)向提供專用硬件的優(yōu)質(zhì)服務(wù)。
考慮測量和記錄包含重試機(jī)制的操作所花費(fèi)的總時間。這是暫時性錯誤對用戶響應(yīng)時間、處理延遲和應(yīng)用程序用例效率的總體影響的一個很好的指示器。還要記錄發(fā)生的重試次數(shù),以便了解影響響應(yīng)時間的因素。
考慮實(shí)現(xiàn)一個遙測和監(jiān)控系統(tǒng),當(dāng)失敗的數(shù)量和比率、平均重試次數(shù)或操作成功所需的總時間增加時,該系統(tǒng)可以發(fā)出警報(bào)。
管理持續(xù)失敗的操作
在某些情況下,每次行動都會失敗,考慮如何處理這種情況是至關(guān)重要的。
盡管重試策略將定義一個操作應(yīng)該重試的最大次數(shù),但它不會阻止應(yīng)用程序以相同的重試次數(shù)再次重復(fù)該操作。例如,如果一個訂單處理服務(wù)由于一個致命錯誤而失敗,使其永久停止操作,重試策略可能會檢測到連接超時,并認(rèn)為這是一個暫時的錯誤。代碼將重試指定次數(shù)的操作,然后放棄。然而當(dāng)另一個客戶下訂單時,該操作將再次嘗試——即使每次都肯定會失敗。
為了防止對不斷失敗的操作進(jìn)行不斷重試,考慮實(shí)現(xiàn)斷路器模式。在此模式中,如果指定時間窗口內(nèi)的失敗次數(shù)超過閾值,則請求將立即作為錯誤返回給調(diào)用者,而不嘗試訪問失敗的資源或服務(wù)。
應(yīng)用程序可以周期性地測試服務(wù)(斷斷續(xù)續(xù)的,請求之間的間隔很長),以檢測服務(wù)何時可用。適當(dāng)?shù)拈g隔取決于場景,例如操作的關(guān)鍵程度和服務(wù)的性質(zhì),可能是幾分鐘到幾個小時之間的任何時間。在測試成功時,應(yīng)用程序可以恢復(fù)正常操作,并將請求傳遞給新恢復(fù)的服務(wù)。
與此同時,可以退回到服務(wù)的另一個實(shí)例(可能在不同的數(shù)據(jù)中心或應(yīng)用程序中),使用提供兼容(可能更簡單)功能的類似服務(wù),或者執(zhí)行一些替代操作,希望該服務(wù)很快可用。例如,可以將服務(wù)請求存儲在隊(duì)列或數(shù)據(jù)存儲中,稍后再重放它們。否則我們可能會將用戶重定向到應(yīng)用程序的另一個實(shí)例,降低應(yīng)用程序的性能,但仍然提供可接受的功能,或者只是向用戶返回一條消息,指示該應(yīng)用程序目前不可用。
其他的考慮
在決定重試次數(shù)和策略重試間隔的值時,請考慮服務(wù)或資源上的操作是否是長時間運(yùn)行或多步驟操作的一部分。當(dāng)一個操作步驟失敗時,補(bǔ)償所有其他已經(jīng)成功的操作步驟可能是困難的或昂貴的。在這種情況下,很長的間隔和大量的重試是可以接受的,只要它不通過持有或鎖定稀缺資源來阻塞其他操作。
請考慮重試同一操作是否可能導(dǎo)致數(shù)據(jù)不一致。如果重復(fù)多步驟流程的某些部分,且操作不是冪等的,則可能導(dǎo)致不一致。例如,遞增值的操作如果重復(fù),將產(chǎn)生無效的結(jié)果。如果無法檢測到重復(fù)的消息,重復(fù)將消息發(fā)送到隊(duì)列的操作可能會導(dǎo)致消息使用者中出現(xiàn)不一致。要防止這種情況,請確保將每個步驟設(shè)計(jì)為冪等操作。
考慮將要重試的操作的范圍。例如,在包含多個操作的級別上實(shí)現(xiàn)重試代碼可能更容易,如果其中一個操作失敗,則重試所有操作。但是,這樣做可能會導(dǎo)致冪等問題或不必要的回滾操作。
如果選擇一個包含多個操作的重試范圍,那么在確定重試間隔、監(jiān)視所花費(fèi)的時間以及發(fā)出失敗警報(bào)之前,請考慮所有操作的總延遲。
在為云原生應(yīng)用考慮韌性設(shè)計(jì)時,請務(wù)必慎重思索重試策略可能會如何影響共享應(yīng)用程序中的鄰居和其他租戶。激進(jìn)的重試策略可能導(dǎo)致其他用戶以及共享資源和服務(wù)的應(yīng)用程序出現(xiàn)越來越多的瞬時錯誤。
同樣,我們的應(yīng)用程序可能會受到資源和服務(wù)的其他用戶所實(shí)現(xiàn)的重試策略影響。對于關(guān)鍵任務(wù)應(yīng)用程序,我們可以決定使用不共享的高級服務(wù)。這為我們提供了更多的負(fù)載控制以及相應(yīng)資源和服務(wù)的節(jié)流,這有助于證明額外的成本是合理的。
遵循上述思路,并結(jié)合具體情況進(jìn)行調(diào)整,我們就可以順利設(shè)計(jì)出具備足夠韌性的云原生應(yīng)用架構(gòu)。
希望本文對你能有所幫助。如果對這個話題感興趣,那么敬請期待后續(xù)發(fā)布的更多系列文章,我們將繼續(xù)從重試模式、斷路器模式等角度深入探討相關(guān)機(jī)制的實(shí)現(xiàn)思路和方法。更多精彩敬請期待!