想提升微服务容错性?试试这5种模式
作者 | ?Igor Perikov
譯者 | 陸離
責編 | 徐威龍
出品 | CSDN云計算(ID:CSDNcloud)?
?
在本文中,我將介紹微服務中的幾種容錯機制及其實現的方法。如果你在維基百科上查找“容錯性”,你將會發現有如下的定義:
?
“容錯性是一種特性,它使系統能夠在某些組件發生錯誤時仍能繼續正常地運行。”
?
對于我們來說,一個組件意味著很多:微服務、數據庫(DB)、負載均衡器(LB),你可以給它命個名。我在這里不討論數據庫和負載均衡器的容錯機制,因為它們是特定于供應商的,并且如果使用它們的話,需要設置一些屬性或者更改部署策略。
?
作為一個軟件工程師,應用程序是我們展現自身力量和承擔責任的地方。下面,我將從以下幾個方面進行介紹容錯性:
超時(Timeouts)
重試(Retries)
熔斷機制(Circuit Breaker)
截止時間(Deadlines)
限流器(Rate limiters)
有些模式是廣為人知的,你甚至可能都會覺得有些不值得一提,但也請緊跟本文的思路,我將要簡要介紹這幾個基本的容錯方法,然后一一討論它們的缺點以及應該如何避免。
超時
超時是指允許等待某個事件發生的指定時間范圍。如果使用SO_TIMEOUT(也稱為socket timeout或read timeout)參數則會出現問題,它表示任何兩個連續數據包之間的超時,而不是整個請求的響應時間,因此很難滿足SLA(Service-Level Agreement,服務等級協議),特別是在應用服務響應負載很大的情況下。通常來說,我們所說的超時,是覆蓋了從建立請求連接到響應最后一個字節完成的整個交互過程。不過,它們不適合使用SO_TIMEOUT參數。想要在JVM中避免使用它,可以使用JDK11或是OkHttp客戶端。Go語言在std庫中也有一個相關的機制。
重試
如果你的請求失敗了,那么請稍等一會兒,然后再試一次。重試的過程基本上就是這樣的。重試是有意義的,因為網絡可能會降級服務一段時間,或者GC(垃圾回收)會命中請求所到達的特定實例?,F在,讓我們來想象有如下所示的服務調用過程:
? ? ? ? ? ? ?
如果我們將每個服務的請求嘗試次數上限設置為3,并且服務D突然發生完全中斷的錯誤,那么會發生什么情況呢?這將導致一場請求重試風暴,當服務鏈中的每個服務開始重試它們的請求時,會由此大大增加了總負載量,所以服務B將面臨擔負是通常情況下3倍的請求負載,C是9倍,而D是27倍。冗余是實現高可用性的關鍵原則之一,但我懷疑在這種情況下,集群C和D上是否還有足夠的空閑資源。將嘗試次數上限設置為2也不會有多大幫助,而且它會使在較小blip上的用戶體驗會更糟。
?
解決方案:
區分可重試錯誤和不可重試錯誤。當用戶沒有權限或負載結構不正確時,重試請求是沒有意義的。相反,重試請求超時或返回代碼5xx是沒有問題的;
設置錯誤重試機制。當重試錯誤次數超過閾值時停止重試,例如,與服務D進行交互,如果20%的結果都是錯誤的,則停止重試并嘗試降級服務。在過去的N秒內,可以使用滾動窗口跟蹤錯誤量;
熔斷機制
熔斷器可以作為一個更嚴格的錯誤率預置方案。當錯誤率過高的時候,任務將不會繼續執行,如果提供了回退結果,那么就會返回。無論如何,首先應該選擇先執行很少的一部分請求,以便了解目標服務是否已經恢復了。在人工干預之前,我們應該給目標服務一個自己恢復的機會。
?
你可能不太贊同上述的方法,如果一個功能處于關鍵路徑上,啟用熔斷器是沒有意義的,但不要忘了,這種短暫的、可控的“自動斷電”可能會阻止一次大的、不可控的服務中斷。
?
盡管熔斷機制和錯誤重試機制有著相似的想法,但是它們在實際應用的過程中是非常有意義的。由于錯誤重試機制的破壞性較小,因此,其閾值盡量要設的低一些。
?
Hystrix是一個在JVM中的go-to模式熔斷機制的實現。到目前為止,它已經進入了維護模式,建議使用resilience4j來替代。
截止時間/分布式超時
我們已經在本文的第一部分討論了超時模式,現在讓我們看看如何實現它們的“分布式”。首先,再一次訪問相互調用的一系列服務鏈:
? ? ? ? ? ? ?
服務A最長可以等待400毫秒,并且一個請求需要依次調用3個下游服務才能完成任務。假設對服務B的請求和響應花了400毫秒,下一步就準備調用服務C。那么,這種情況是不合理的,一旦服務超時,就不應該再等待任何結果。如果繼續下去只會浪費資源,增加請求重試風暴發生的風險。
?
為了實現這個過程,我們必須在請求中添加額外的元數據,這將有助于理解在什么時候進行中斷處理是最合理的。理想情況下,這應該得到所有服務的支持,并在整個調用流程中進行參數的傳遞。
?
在實際的過程中,上述元數據可以分為以下幾種形式:
Timestamp(時間戳):是通過服務停止等待響應的時間點。首先,網關或者前端服務將截止時間設置為“當前時間戳+超時”。接下來,任何下游服務都應該檢查當前時間戳是否超過了截止時間。如果超過了,那么要停止響應,否則,就開始處理。遺憾的是,服務器之間的時間可能是不同的,存在著時鐘偏差的問題。如果存在這種情況,那么,請求將會被阻塞或者立即被拒絕,從而導致服務停止;
Timeout(超時):傳遞允許服務等待的時間。這個問題有點棘手,和之前一樣,你需要盡快設定截止時間。接下來,任何一個下游服務都應該對自身處理過程進行計時,并從設定的超時時間中減去這個時間,然后傳遞給下一個服務。重要的是不要忘記進入請求隊列等待的時間。因此,如果服務A允許等待400毫秒,而調用服務B花費150毫秒,則在調用服務C時必須告知有250毫秒的超時時間限制。雖然它沒有把傳輸時間也計算在內,但截止時間只能在之后而不是更早的時間觸發,因此,這可能會消耗更多的資源,但不會影響結果。在GRPC(Google開發的高性能、通用的開源RPC框架)中的截止日期就是這樣實現的。
?
最后我們要討論的是,當超過了截止時間時,不去中斷調用服務鏈是否有意義,答案是肯定的,如果你的服務有足夠的處理資源,并且在完成請求之后會使其更活躍(緩存/JIT),那么讓其繼續處理就可以了。
限流器
前面討論的模式主要解決了級聯失效的問題,依賴服務在其依賴關系崩潰后也隨之崩潰,最終導致整個服務完全宕掉的情況。現在,讓我們來討論一下當你的服務出現過載的情況。有很多技術方面的原因可能會導致服務過載,那么,現在我們就假設發生了這種情況。
?
每個應用程序都有其未知的處理極限。這個值是動態的,取決于多個變量,例如近期的代碼更新、正在運行的CPU的模式、主機的繁忙程度等等。
?
當負載超過極限時會發生什么呢?通常,這種惡性循環會出現在如下的情況里:
1. ?響應時間增加,GC占用空間增加;
2. ?客戶端出現了越來越多的處理超時,甚至承擔了更多的負載量;
3. ?比1的情況更嚴重;
?
這里有個例子。當然,如果客戶端有錯誤重試機制或者熔斷機制,第二種情況可能不會產生額外的負荷,從而會獲得一個跳出的機會。其它的情況可能會發生,從負載均衡的上游列表中刪除實例可能會在加載和剔除相鄰實例等方面導致更多的不平衡。
?
對于限流器,它們的做法是優雅地拒絕新的請求,這就是理想情況下應該如何處理過度負載:
?
1. ?限流器將額外的負載量降到可承受的范圍以內,從而使應用程序能夠根據SLA進行服務請求;
2. ?過多的負載被重新分配到其它的實例,或者進行集群的自動/被動縮放;
?
有兩種類型的限流器:速率和并發。前者用來限制允許的請求數量,后者用來限制在任何時刻同時處理的最大請求數量;
?
為了簡便起見,假設對服務的所有請求在計算資源的消耗上是一致的,并且具有相同的權重。不同的用戶會有不同數量的數據,例如喜歡的電視節目或者以前的訂單等等。通常,采用分頁技術有助于實現請求在計算資源消耗上的相等。
?
限流器使用的更廣泛,但并不像并發限制那樣能提供強大的保證,所以如果你希望選擇一個的話,建議堅持使用并發限制機制,原因如下。
?
在配置限流器的時候,我們認為會強制執行如下的操作:
服務可以在任何時間點進行每秒N個請求的處理。
但實際上,我們想要說的是:
假設響應時間相同,那么在任何時間點服務都可以進行每秒N個請求的處理。
?
為什么這句話這么重要?我會用直覺來“證明”。而對于那些愿意用數學來證明的人,查一下什么是排隊理論。
?
假設限流器設置為1000rps,響應時間為1000毫秒,SLA為1200毫秒。在給定的SLA的條件下,我們很容易算出,在1秒鐘內服務能同時處理1000個請求。
?
現在,響應時間增加了50毫秒(依賴的服務開始處理額外的工作)。從現在起,由于請求數超過了處理能力,在每1秒服務都將面臨同時需要處理越來越多的請求。如果線程的數量不受限制的增長,那么就意味著你的資源將會被一點一點的耗盡,并直至系統崩潰,尤其是在應用程序的線程1:1地對應到操作系統線程的時候。如何能處理1000個請求的并發限制呢?它可以服務于1000/1.05=~950個RPS,而不會違反SLA協議,然后放棄其余的請求。另外,還不需要重新進行配置。
?
我們可以在每次服務的依賴關系發生變化時更新流量限制,但這是一個巨大的工作量,可能需要在每次變化時對整個生態系統進行重新配置。
?
根據閾值的設置方式,它可以分為靜態的和動態的。
靜態的限流
在這種情況下,限制范圍是通過手動進行配置的。閾值可以通過定期的性能測試來評估。然而它不可能是100%準確的,另外,對于安全性來說,可能也會有一定的影響。這種類型的限制要求圍繞CI/CD(持續集成/持續交付)管道來進行工作,而且資源利用率也較低。靜態的限流器可以通過限制工作線程池的大小(僅限并發)、添加可以計算請求數量的請求過濾器、以及NGINX limiting functionality或envoy sidecar proxy來實現。
動態的限流
在這里,限制取決于度量,度量是在規則的基礎上重新計算的。很有可能的是服務過載與響應時間變長之間存在著相關性。如果是的話,度量可以是響應時間的統計函數,例如百分比、中等或平均值。還記得計算等式屬性嗎?這個屬性是更精確計算的關鍵。
然后,定義一個術語用于表示度量是否正常。例如,p99≥500ms被認為是服務不正常,因此應降低限制范圍。如何增大和減小限制范圍應該由一個請求反饋控制算法來決定,如AIMD(Additive Increase Multiplicative Decrease,在TCP協議中使用)。下面是它的偽代碼:
? ? ? ?? ? ? ?? ? ??? ? ? ?
如你所見,限制的范圍增大緩慢,檢測應用程序是否運行良好,如果發現錯誤行為則急劇減小。
?
Netflix開創了動態限制的思想,并且開源了解決方案,這里是代碼庫:https://github.com/Netflix/concurrency-limits。它實現了幾種反饋算法、靜態限流器的實現、GRPC集成和Java servlet集成。
?
就說到這里吧,希望你從本文里學到一些有幫助的內容。我想指出的是,上述所說的并不是盡善盡美的,你還應該具有良好的觀察能力,因為在實際應用的過程中,可能會發生一些意想不到的問題,需要你能更好地了解當前應用程序的情況。不管怎么樣,實現上述這些方法,將有助于解決你當前出現的或者是潛在的一些問題。
?
原文鏈接:
https://itnext.io/5-patterns-to-make-your-microservice-fault-tolerant-f3a1c73547b3
本文為CSDN翻譯文章,轉載請注明出處。
推薦閱讀
十年云計算大爆發,微軟正在摧毀其它競爭對手
小網站的容器化(上)
盤點丨2019十大邊緣計算項目我國自主開發的編程語言“木蘭”是又一個披著“洋”皮的紅芯瀏覽器嗎?
以太坊 2.0 前途光明!
總結
以上是生活随笔為你收集整理的想提升微服务容错性?试试这5种模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 重磅!云+X 案例征集正式启动啦!
- 下一篇: 阿里云开放国内首个云端数据库测试平台,云