服务容错设计:流量控制、服务熔断、服务降级
一、 為什么需要容錯設計:
????????在分布式系統中,因為使用的機器和服務非常多,所以故障發生的頻率會比傳統的單體應用更大。只不過,單體應用的故障影響面很大,而分布式系統中由于故障的影響面可以被隔離,所以影響面較小,但是因為機器和服務多,出故障的頻率也很多。不過我們需要明白下面這些道理:
- 出現故障不可怕,故障影響面過大才可怕
- 出現故障不可怕,故障恢復時間過長才可怕
????????另一方面,因為分布式系統的架構復雜,為了有效對分布式架構進行運維管理,我們會在系統中添加各種監控指標,方便出問題時快速定位。因此有些公司拼命地添加各種監控指標,有的甚至能添加出幾萬個監控指標,但這樣做卻給人一種”使蠻力“的感覺,一方面,信息太多就等于沒有信息,另一方面,SLA 要求我們定義出核心關鍵指標。如果只是拼命添加各種監控指標而不定義出關鍵指標,這其實是一種思維上的惰性。
????????另外,上述的措施都是在 “救火階段” 而不是在 “防火階段”,所謂 “防火勝于救火”,所以我們更要考慮如何進行防火,這就要求我們在設計或者運維時都要為可能發生的故障考慮,即所謂 “Design for Failure”,面向失敗設計,在設計時要考慮如何減輕故障,如果無法避免,也要使用自動化的方式恢復故障,減少故障影響面。而容錯設計就是面向失敗設計中一個非常重要的環節,常見的容錯設計有:
(1)流控:即流量控制,根據流量、并發線程數、響應時間等指標,把隨機到來的流量調整成合適的形狀,即流量塑性,保證系統在流量突增情況下的可用性,避免系統被瞬時的流量高峰沖垮,一旦達到閾值則進行拒絕服務、排隊等降級操作。
(2)熔斷:當下游服務發生一定數量的失敗后,打開熔斷器,后續請求就快速失敗。一段時間過后再判斷下游服務是否已恢復正常,從而決定是否重置熔斷器。
(3)降級:當訪問量劇增、服務出現異常或者非核心服務影響到核心流程時,暫時犧牲掉一些東西,以保障整個系統的平穩運行。
(4)隔離:將系統或資源分隔開,保證系統故障時,能限定傳播范圍和影響范圍,防止滾雪球效應,保證只有出問題的服務不可用,服務間的相互不影響。常見的隔離手段:資源隔離、線程隔離、進程隔離、集群隔離、機房隔離、讀寫隔離、快慢隔離、動靜隔離等。
(5)超時:相當多的服務不可用問題,都是客戶端超時機制不合理導致的,當服務端發生抖動時,如果超時時間過長,客戶端一直處于占用連接等待響應的階段,耗盡服務端資源,最終導致服務端集群雪崩;如果超時時間設置過短又會造成調用服務未完成而返回,所以一個健康的服務,一定要有超時機制,根據業務場景選擇恰當的超時閾值。
(6)冪等:當用戶多次請求同一事件時,得到的結果永遠是同一個。
二、流控設計:
???????限流的目的是通過限制并發訪問數或者時間窗口內的請求數,保證系統在流量突增情況下的穩定性,使系統不至于被高流量擊垮,一旦達到限制閾值就可以拒絕服務(告知沒有資源了或定向到錯誤頁)、排隊或等待(比如秒殺、評論、下單)、降級(返回兜底數據或默認數據,如商品詳情頁庫存默認有貨)。而一般高并發系統常見的限流方式有:
- 限制單位時間的調用量
- 限制系統的總并發數,比如數據庫連接池、線程池
- 限制時間窗口內的并發數,比如漏桶和令牌桶
下面我們就討論各種限流方式的設計與具體應用場景:
1、限制單位時間內的調用量:
????????從字面上很容易理解,就是通過一個計數器來統計單位時間內某個服務的訪問量。如果超過了闕值,則該單位時間段那不允許繼續訪問,或者把請求放入隊列中等下一個時間段繼續訪問。需要注意的是計數器在進入下一個單位時間段時需要先清零。
????????對于單位時間的設置,第一不能太長,太長將導致限流效果不夠“敏感”;第二不能設置得太短,越短的單位時間段將導致閾值越難設置,比如1秒鐘,因為高峰期的1秒鐘和低峰期的1秒鐘的流量有可能相差百倍甚至千倍。最優的單位時間片段應該以閾值設置的難易程度為標準,比如我們的監控系統統計的是服務每分鐘的調用量,那我們自然可以選擇1分鐘作為時間片段,因為我們很容易評估每個服務在高峰期和低峰期的分鐘調用量,并可以通過服務在每分鐘的平均耗時和異常量來評估服務在不同單位時間段的服務質量。
????????當單位時間段和閾值已經確定下來了,接下來就該考慮計數器的實現了,在 Java 中最常用的就是 AtomicLong,每次服務調用時,我們可以通過 AtomicLong.incrementAndGet() 方法來給計數器加1并返回最新值,并通過這個最新值和閾值來進行比較來看該服務單位時間段內是否超過了閾值。對于限制單位時間段內調用量的這種限流方式,實現簡單,適用于大多數場景,但是也有一定的局限性,如果設定的閥值在單位時間段內的前幾秒就被流量突刺消耗完了,將導致該時間段剩余的時間內該服務“拒絕服務”,這種現象被稱為“突刺消耗”。
2、限制系統的總并發數:
????????這種方式通過嚴格限制某服務的并發訪問速度,也就限制了該服務單位時間段內的訪問量。相比于第一種方案,它有著更嚴格的限制邊界,因為如果采用第一種限流方案,如果大量的服務在極短的時間產生,仍然會壓垮系統,甚至雪崩。并發限流一般用于服務資源有嚴格的限制的場景,比如連接數,線程數等。
????????在 Java 中實現,并發限流也非常簡單,比如可以使用信號量 Semaphore,在服務調用的入口調用非阻塞方法 Semaphore.tryAcquire() 來嘗試獲取一個信號量;如果獲取失敗,則直接返回或將調用放入到某個隊列中;當服務執行完成,則使用 Semaphore.release() 來釋放資源。
????????但這種方式仍然可以造成流量尖刺,即每臺服務器上該服務的并發量從 0 上升到閥值是沒有任何阻力的,因為并發量考慮的只是服務能力邊界的問題。? ? ??
3、通過漏桶進行限流:
????????漏桶算法有點像我們生活中用到的漏斗,液體倒進去漏斗后從下端小口中以固定速率流出。漏桶算法就是這樣,不管流量有多大,漏桶都保證了流量的常速率輸出,由于調用的消費速率已經固定,那么當桶的容量堆滿時,就只能丟棄了。示意圖如下:
?????????漏桶算法是一種悲觀策略,它嚴格地限制了系統的吞吐量,從某種角度上來說,它的效果和并發量限流很類似。漏桶算法可以用于大多數場景,但是由于它對于服務吞吐量有著嚴格限制,可能導致某些服務稱為瓶頸。
????????在 Java 中想要實現一個漏桶算法,可以準備一個隊列,當作桶的容量,另外通過一個計劃線程池(ScheduledExecutorService)來定期從隊列中獲取并執行請求調用,可以一次拿多個請求,然后并發執行。
4、通過令牌桶進行限流:
????????令牌桶算法可以看成是漏桶算法的一種改進,漏桶算法能夠強行限制請求調用的速率,而令牌桶算法能夠在限制平均調用速率的同時,允許一定程度的突發調用,實現平滑限流。令牌桶算法中,桶中會有一定數量的令牌,每次請求調用需要去桶中拿取令牌,拿到令牌后才有資格執行,否則必須等待。
????????看到這里或許有些疑問,如果把令牌比喻成信號量,那么好像和并發限流沒什么區別。其實不是,令牌桶算法的精髓在于拿令牌和放令牌的方式,這和單純的并發限流有明顯的區別:
-
每次請求時需要獲取的令牌數不是固定的,比如當桶中的令牌比較多時,每次調用只需要獲取一個令牌,隨著令牌數的逐漸減少,當令牌使用率(使用中的令牌數/令牌總數)達到某個比率時,可能一次需要獲取兩個令牌,獲取令牌的個數可能還會升高。
-
歸還令牌有兩種方法,第一種是直接放回,第二種是什么也不做,由另一個額外的令牌生成步驟將令牌允許放回桶中。
?????????前面講過,令牌桶允許一定程度的突發調用,那么關于令牌桶處理數據報文的方式,RFC 中定義了兩種令牌桶算法:
- 單速率三色標記(single rate three color marker,srTCM,RFC2697 定義,或稱為單速雙桶算法)算法,主要關注報文尺寸的突發。
- 雙速率三色標記(two rate three color marker,trTCM,RFC2698 定義,或稱為雙速雙桶算法)算法,主要關注速率的突發。
????????兩種算法都是為報文打上紅、黃、綠三種顏色的標記,所以稱為“三色標記”。 QoS 會根據報文的顏色,設置報文的丟棄優先級,兩種算法都可工作于色盲模式和非色盲模式。對于單速率三色標記算法和雙速率三色標記算法感興趣的讀者,可以閱讀這篇文章:https://zhuanlan.zhihu.com/p/164503398
小結:令牌桶和漏桶算法區別:
(1)內容上:令牌桶算法是按固定速率生成令牌,請求能從桶中拿到令牌就執行,否則執行失敗。漏桶算法是任務進桶速率不限,但是出桶的速率是固定的,超出桶大小的的任務丟棄,也就是執行失敗。
(2)突發流量適應性上:令牌桶限制的是流入的速率且靈活,允許一次取出多個 token,只要有令牌就可以處理任務,在桶容量上允許處理突發流量。而漏桶算法出桶的速率固定,有突發流量也只能按流出的速率處理任務,所以漏桶算法是平滑流入的速率,限制流出速率。
5、四種限流算法的比較與小結:
????????下面給出在某種特定場景和特定參數下四種限流方式對服務并發能力影響的折線圖,其中X軸表示當前并發調用數,而Y軸表示某服務在不同并發調用程度下采取限流后的實際并發調用數:
????????在不同場景不同參數下,服務采用所述四種限流方式都會有不同的效果,沒有哪種限流算法能保證在所有場景下都是最優限流算法,因為這需要從服務資源特性、限流策略(參數)配置難度、開發難度和效果檢測難度等多方面因素來考慮。但是相比于其他三種限流方式來說,令牌桶算法限流無疑是最為靈活的,因為它有著眾多可配置的參數來直接影響限流的效果,比如 Google 的 Guava 包的 RateLimiter 提供了令牌桶算法的實現,感興趣的讀者可以自行百度。
????????最后,不論是對于令牌桶拿不到令牌被拒絕,還是漏桶的水滿了溢出,或者是其他限流算法,都是為了保證大部分流量的正常使用,而犧牲掉了少部分流量,這是合理的,如果因為極少部分流量需要保證的話,那么就可能導致系統達到極限而掛掉,得不償失。
參考文章:https://www.iteye.com/blog/manzhizhen-2311691
三、熔斷設計:
1、什么是熔斷機制:
????????熔斷機制可以快速地拒絕可能導致異常的調用,防止應用程序不斷執行可能失敗的操作,當感知到下游服務的資源出現不穩定狀態(調用超時或異常比例升高時),暫時切斷對下游服務的調用,而不是一直阻塞等待服務響應,阻止級聯失敗導致的雪崩效應,保證系統的可用性;尤其是后端太忙的時候,使用熔斷設計可以保護后端不會過載。另外,對于服務間的調用一般都會設置超時與重試機制,但如果錯誤太多,或是在短時間內得不到修復,那么再多的重試也沒有任何意義了,這時也需要使用熔斷機制快速返回結果。
????????另外,開啟熔斷之后,也應該不斷檢測下游服務的健康情況,當檢測到該節點的服務調用響應正常后,則恢復調用鏈路。這就要求熔斷器具備感知異常的能力以及對關鍵邏輯實現開關控制,因此,熔斷的設計有兩個關鍵點:
- ① 判斷何時熔斷:客戶端對每次請求服務端的正常、異常(失敗、拒絕、超時)返回計數,當異常返回次數超過設定閾值時進行熔斷。進入熔斷狀態后,后續對該服務接口的調用不再經過網絡,直接執行本地的默認方法,達到服務降級的效果。
- ② 何時從熔斷狀態恢復:處于熔斷狀態時,客戶端每隔一段時間(比如5秒),允許部分請求通過,若這部分請求正常返回,就恢復熔斷。
2、熔斷機制的實現:
(1)以 Hystrix 為例,Hystrix 設計了三種狀態:
- ① 熔斷關閉狀態(Closed): 服務沒有故障時,熔斷器所處的狀態,對調用方的調用不做任何限制。
- ② 熔斷開啟狀態(Open): 在固定時間內,接口調用出錯比率達到一個閾值,會進入熔斷開啟狀態。進入熔斷狀態后, 后續對該服務接口的調用不再經過網絡,直接執行本地的 fallback 方法。
- ③ 半熔斷狀態(Half-Open): 在進入熔斷開啟狀態一段時間之后,熔斷器會進入半熔斷狀態。所謂半熔斷就是嘗試恢復服務調用,允許有限的流量調用該服務,并監控調用成功率。如果成功率達到預期,則說明服務已恢復,進入熔斷關閉狀態;如果成功率仍舊很低,則重新進入熔斷開啟狀態。
(2)阿里研發的 Sentinel 內部熔斷機制的實現其實也是熔斷器的思想。Sentinel 底層會通過優化的滑動窗口數據結構來實時統計所有資源的調用數據(通過 QPS、限流 QPS、異常數、RT 等)。若資源配置了熔斷降級規則,那么調用時就會取出當前時刻的統計數據進行判斷。當達到熔斷閾值時會將熔斷器置為開啟狀態,切斷所有的請求,直到過了降級時間窗口后再重置熔斷器,繼續允許請求通過。
四、降級設計:
1、什么是服務降級:
????????所謂降級,就是指當訪問量劇增、下游服務出現問題 或 非核心服務影響到核心流程的性能時,仍然需要保證服務還是可用的,即使是有損服務,這也是降級的最終目的。本質是為了解決資源不足和訪問量過大的問題,當資源和訪問量出現矛盾的時候,在有限的資源下,為了能夠扛住大量的請求,我們就需要對系統進行降級操作,也就是暫時犧牲掉一些東西,以保障整個系統的平穩運行。一般來說,降級可以犧牲掉的東西有:
(1)降低一致性,從強一致性變成最終一致性:對于降低一致性,把強一致性變成最終一致性的做法可以有效地釋放資源,并且讓系統運行得更快,從而可以扛住更大的流量。通常會有兩種做法:一種是簡化流程的一致性(比如使用異步簡化流程),一種是降低數據的一致性(比如使用緩存)。
(2)停止次要功能:停止訪問不重要的功能,從而讓系統釋放出更多的資源。
(3)簡化功能:把一些功能簡化掉,比如簡化業務流程,或是不再返回全量數據,只返回部分數據。
2、服務降級的類型與策略:
2.1、降級按照是否可以自動化可分為:人工開關降級和自動開關降級。
2.2、按觸發降級的時機可以分為以下幾個大類:
- 限流降級:當服務端請求數的數量超過配置的限制閾值,后續請求會被降級
- 超時降級:事先配置好超時時間和超時重試次數及機制,并使用異步機制探測恢復情況
- 失敗降級:該方式主要是針對不穩定的API,當失敗調用次數或者比例達到一定閥值時自動降級,同樣要使用異步機制探測回復情況
- 故障降級:如要調用的遠程服務掛掉了(比如網絡故障、DNS故障、HTTP服務返回錯誤的狀態碼和RPC服務拋出異常),則可以直接降級
2.3、降級按照功能維度可分為:讀服務降級和寫服務降級
(1)讀降級:可以暫時切換讀數據來源,比如返回緩存數據、默認值、兜底數據。如果后端服務有問題,則可以降級為只讀緩存,這種方式適合對讀一致性要求不高的場景
① 緩存降級:使用緩存方式來降級部分讀頻繁的服務接口。每次服務調用成功時,記錄服務的結果在緩存中,下一次失敗時讀取緩存的舊數據;可以根據時延要求選擇本地緩存、分布式緩存、數據庫或本地文件。適用場景為能夠接受一定延遲的只讀業務,且有足夠的存儲資源。但需要注意以下幾點:
- 分布式緩存或數據庫只是把服務故障轉移到另一個依賴;
- 冷數據無法降級,即較長時間之前的狀態數據無法降級;
- 降級后的數據存在一致性問題,需要根據業務情況設置合理的有效期;
- 數據量大時消耗過多的存儲(特別是緩存)且命中率低,可以設置合理的緩存大小,使用LRU方式替換舊緩存。
② 默認值:直接返回配置中的默認值或者空數據。適用于弱依賴的只讀業務,需要注意的是,默認值盡量有多種選擇,避免千篇一律。
比如主播標語服務,在配置中心配置一批中性的默認標語,標語服務失敗時直接隨機取一條返回給用戶,故障率不高的情況下用戶基本上感知不到異常。
(2)寫降級:可以使用異步處理并保證最終一致性,比如先更新緩存,然后異步同步到數據庫中;或者采用事后處理,執行補償機制,自動或者手動補償。
????????在 CAP 原理和 BASE 理論中寫操作存在數據一致性這個問題,降級的目的是為了提供高可用性,在多數的互聯網架構中,可用性是大于數據一致性的。所以喪失寫入數據的同步,通過上面的理論,我們也能勉強接受數據最終一致性。高并發場景下,如果寫入操作無法及時到達或抗壓,可以異步消費數據、cache更新、log等方式進行補償
文章小結:限流、熔斷、降級都是解決服務間 RPC 過程出現的異常問題,保證服務穩定性的手段。限流發生在Server端,熔斷發生在Client端,而降級是觸發限流、熔斷之后的補償措施。在實際場景中通常會配合實現,比如熔斷和降級,熔斷決定何時降級,降級是服務在熔斷狀態下的對外表現。
總結
以上是生活随笔為你收集整理的服务容错设计:流量控制、服务熔断、服务降级的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Tomcat 的类加载机制
- 下一篇: JUC多线程:系统调用、进程、线程的上下