隨着服務框架和服務治理體系的逐步成熟,服務化已成爲系統設計的趨勢。隨着業務複雜度的增長,依賴的服務也逐步增長,出現了很多因爲服務調用出現異常問題而致使的重大事故,如:
1)系統依賴的某個服務發生延遲或者故障,數秒內致使全部應用資源(線程,隊列等)被耗盡,形成所謂的雪崩效應 (Cascading Failure),致使整個系統拒絕對外提供服務。
2)系統遭受惡意爬蟲襲擊,在放大效應下沒有對下游依賴服務作好限速處理,最終致使下游服務崩潰。
容錯是一個很大的話題,受篇幅所限,本文將介紹僅限定在服務調用間經常使用的一些容錯模式。html
服務容錯的設計有個基本原則,就是「Design for Failure」。爲了不出現「千里之堤潰於蟻穴」這種狀況,在設計上須要考慮到各類邊界場景和對於服務間調用出現的異常或延遲狀況,同時在設計和編程時也要考慮周到。這一切都是爲了達到如下目標:
1)一個依賴服務的故障不會嚴重破壞用戶的體驗。
2)系統能自動或半自動處理故障,具有自我恢復能力。
基於這個原則和目標,衍生出下文將要介紹的一些模式,可以解決分佈式服務調用中的一些問題,提升系統在故障發生時的存活能力。java
所謂模式,其實就是某種場景下一類問題及其解決方案的總結概括,每每能夠重用。模式能夠指導咱們完成任務,做出合理的系統設計方案,達到事半功倍的效果。而在服務容錯這個方向,行業內已經有了很多實踐總結出來的解決方案。web
超時模式
是一種最多見的容錯模式,在工程實踐中大量存在。常見的有設置網絡鏈接超時時間,一次RPC的響應超時時間等。在分佈式服務調用的場景中,它主要解決了當依賴服務出現創建網絡鏈接或響應延遲,不用無限等待的問題,調用方能夠根據事先設計的超時時間中斷調用,及時釋放關鍵資源,如Web容器的鏈接數,數據庫鏈接數等,避免整個系統資源耗盡出現拒絕對外提供服務這種狀況。算法
重試模式
通常和超時模式結合使用,適用於對於下游服務的數據強依賴的場景(不強依賴的場景不建議使用!),經過重試來保證數據的可靠性或一致性,經常使用於因網絡抖動等致使服務調用出現超時的場景。與超時時間設置結合使用後,須要考慮接口的響應時間分佈狀況,超時時間能夠設置爲依賴服務接口99.5%響應時間的值,重試次數通常1-2次爲宜,不然會致使請求響應時間延長,拖累到整個系統。數據庫
一些實現說明:編程
public class RetryCommand<T> { private int maxRetries = 2;// 重試次數 默認2次 private long retryInterval = 5;//重試間隔時間ms 默認5ms private Map<String, Object> params; public RetryCommand() { } public RetryCommand(long retryInterval, int maxRetries) { this.retryInterval = retryInterval; this.maxRetries = maxRetries; } public T command(Map<String, Object> params){ //Some remote service call with timeout serviceA.doSomethingWithTimeOut(timeout); } private final T retry() throws RuntimeException { int retryCounter = 0; while (retryCounter < maxRetries) { try { return command(params); } catch (Exception e) { retryCounter++; if (retryCounter >= maxRetries) { break; } } } throw new RuntimeException("Command failed on all of " + maxRetries + " retries"); } //省略 }
public class RetryCommand<T> { private int maxRetries = 2;// 重試次數 默認2次 private long retryInterval = 5;//重試間隔時間ms 默認5ms private Map<String, Object> params; public RetryCommand() { } public RetryCommand(long retryInterval, int maxRetries) { this.retryInterval = retryInterval; this.maxRetries = maxRetries; } public T command(Map<String, Object> params){ //Some remote service call with timeout serviceA.doSomethingWithTimeOut(timeout); } private final T retry() throws RuntimeException { int retryCounter = 0; while (retryCounter < maxRetries) { try { return command(params); } catch (Exception e) { retryCounter++; if (retryCounter >= maxRetries) { break; } } } throw new RuntimeException("Command failed on all of " + maxRetries + " retries"); } //省略 }
限流模式,經常使用於下游服務容量有限,但又怕出現突發流量猛增(如惡意爬蟲,節假日大促等)而致使下游服務因壓力過大而拒絕服務的場景。常見的限流模式有控制併發和控制速率,一個是限制併發的數量,一個是限制併發訪問的速率。緩存
public class SemaphoreTest { private static final int THREAD_COUNT = 30; private static ExecutorService threadPool = Executors .newFixedThreadPool(THREAD_COUNT); private static Semaphore s = new Semaphore(10); public static void main(String[] args) { for (int i = 0; i < THREAD_COUNT; i++) { threadPool.execute(new Runnable() { @Override public void run() { try { s.acquire(); System.out.println("save data"); s.release(); } catch (InterruptedException e) { e.printStack(); } } }); } threadPool.shutdown(); } }
public class SemaphoreTest { private static final int THREAD_COUNT = 30; private static ExecutorService threadPool = Executors .newFixedThreadPool(THREAD_COUNT); private static Semaphore s = new Semaphore(10); public static void main(String[] args) { for (int i = 0; i < THREAD_COUNT; i++) { threadPool.execute(new Runnable() { @Override public void run() { try { s.acquire(); System.out.println("save data"); s.release(); } catch (InterruptedException e) { e.printStack(); } } }); } threadPool.shutdown(); } }
在代碼中,雖然有30個線程在執行,可是隻容許10個併發的執行。Semaphore的構造方法Semaphore(int permits) 接受一個整型的數字,表示可用的許可證數量。Semaphore(10)表示容許10個線程獲取許可證,也就是最大併發數是10。Semaphore的用法也很簡單,首先線程使用Semaphore的acquire()獲取一個許可證,使用完以後調用release()歸還許可證,還能夠用tryAcquire()方法嘗試獲取許可證。網絡
在Wikipedia上,令牌桶算法是這麼描述的:併發
令牌桶控制的是一個時間窗口內經過的數據量,在API層面咱們常說的QPS、TPS,正好是一個時間窗口內的請求量或者事務量,只不過期間窗口限定在1s罷了。 以一個恆定的速度往桶裏放入令牌,而若是請求須要被處理,則須要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。令牌桶的另一個好處是能夠方便的改變速度,一旦須要提升速率,則按需提升放入桶中的令牌的速率。框架
在咱們的工程實踐中,一般使用Guava中的Ratelimiter來實現控制速率,如咱們不但願每秒的任務提交超過兩個:
//速率是每秒兩個許可 final RateLimiter rateLimiter = RateLimiter.create(2.0); void submitTasks(List tasks, Executor executor) { for (Runnable task : tasks) { rateLimiter.acquire(); // 也許須要等待 executor.execute(task); } }
//速率是每秒兩個許可 final RateLimiter rateLimiter = RateLimiter.create(2.0); void submitTasks(List tasks, Executor executor) { for (Runnable task : tasks) { rateLimiter.acquire(); // 也許須要等待 executor.execute(task); } }
在咱們的工程實踐中,偶爾會遇到一些服務因爲網絡鏈接超時,系統有異常或load太高出現暫時不可用等狀況,致使對這些服務的調用失敗,可能須要一段時間才能修復,這種對請求的阻塞可能會佔用寶貴的系統資源,如:內存,線程,數據庫鏈接等等,最壞的狀況下會致使這些資源被消耗殆盡,使得系統裏不相關的部分所使用的資源也耗盡從而拖累整個系統。在這種狀況下,調用操做可以當即返回錯誤而不是等待超時的發生或者重試多是一種更好的選擇,只有當被調用的服務有可能成功時咱們再去嘗試。
熔斷器模式能夠防止咱們的系統不斷地嘗試執行可能會失敗的調用,使得咱們的系統繼續執行而不用等待修正錯誤,或者浪費CPU時間去等到長時間的超時產生。熔斷器模式也可使咱們系統可以檢測錯誤是否已經修正,若是已經修正,系統會再次嘗試調用操做。 下圖是個使用熔斷器模式的調用流程:
能夠從圖中看出,當超時出現的次數達到必定條件後,熔斷器會觸發打開狀態,客戶端的下次調用將直接返回,不用等待超時產生。
在熔斷器內部,每每有如下幾種狀態:
1)閉合(closed)狀態:該狀態下可以對目標服務或方法進行正常的調用。熔斷器類維護了一個時間窗口內調用失敗的次數,若是某次調用失敗,則失敗次數加1。若是最近失敗次數超過了在給定的時間窗口內容許失敗的閾值(能夠是數量也能夠是比例),則熔斷器類切換到斷開(Open)狀態。此時熔斷器設置了一個計時器,當時鍾超過了該時間,則切換到半斷開(Half-Open)狀態,該睡眠時間的設定是給了系統一次機會來修正致使調用失敗的錯誤。
2)斷開(Open)狀態:在該狀態下,對目標服務或方法的請求會當即返回錯誤響應,若是設置了fallback方法,則會進入fallback的流程。
3)半斷開(Half-Open)狀態:容許對目標服務或方法的必定數量的請求能夠去調用服務。 若是這些請求對服務的調用成功,那麼能夠認爲以前致使調用失敗的錯誤已經修正,此時熔斷器切換到閉合狀態(而且將錯誤計數器重置);若是這必定數量的請求有調用失敗的狀況,則認爲致使以前調用失敗的問題仍然存在,熔斷器切回到斷開方式,而後開始重置計時器來給系統必定的時間來修正錯誤。半斷開狀態可以有效防止正在恢復中的服務被忽然而來的大量請求再次拖垮。
在咱們的工程實踐中,熔斷器模式每每應用於服務的自動降級,在實現上主要基於Netflix開源的組件Hystrix來實現,下圖和代碼分別是Hystrix中熔斷器的原理和定義,更多瞭解能夠查看Hystrix的源碼:
public interface HystrixCircuitBreaker { /** * Every {@link HystrixCommand} requests asks this if it is allowed to proceed or not. * <p> * This takes into account the half-open logic which allows some requests through when determining if it should be closed again. * * @return boolean whether a request should be permitted */ public boolean allowRequest(); /** * Whether the circuit is currently open (tripped). * * @return boolean state of circuit breaker */ public boolean isOpen(); /** * Invoked on successful executions from {@link HystrixCommand} as part of feedback mechanism when in a half-open state. */ public void markSuccess(); }
public interface HystrixCircuitBreaker { /** * Every {@link HystrixCommand} requests asks this if it is allowed to proceed or not. * <p> * This takes into account the half-open logic which allows some requests through when determining if it should be closed again. * * @return boolean whether a request should be permitted */ public boolean allowRequest(); /** * Whether the circuit is currently open (tripped). * * @return boolean state of circuit breaker */ public boolean isOpen(); /** * Invoked on successful executions from {@link HystrixCommand} as part of feedback mechanism when in a half-open state. */ public void markSuccess(); }
在造船行業,每每使用此類模式對船艙進行隔離,利用艙壁將不一樣的船艙隔離起來,這樣若是一個船艙破了進水,只損失一個船艙,其它船艙能夠不受影響,而借鑑造船行業的經驗,這種模式也在軟件行業獲得使用。
線程隔離(Thread Isolation)就是這種模式的常見的一個場景。例如,系統A調用了ServiceB/ServiceC/ServiceD三個遠程服務,且部署A的容器一共有120個工做線程,採用線程隔離機制,能夠給對ServiceB/ServiceC/ServiceD的調用各分配40個線程。當ServiceB慢了,給ServiceB分配的40個線程因慢而阻塞並最終耗盡,線程隔離能夠保證給ServiceC/ServiceD分配的80個線程能夠不受影響。若是沒有這種隔離機制,當ServiceB慢的時候,120個工做線程會很快所有被對ServiceB的調用吃光,整個系統會所有慢下來,甚至出現系統中止響應的狀況。
這種Case在咱們實踐中常常遇到,如某接口因爲數據庫慢查詢,外部RPC調用超時致使整個系統的線程數太高,鏈接數耗盡等。咱們可使用艙壁隔離模式,爲這種依賴服務調用維護一個小的線程池,當一個依賴服務因爲響應慢致使線程池任務滿的時候,不會影響到其餘依賴服務的調用,它的缺點就是會增長線程數。
不管是超時/重試,熔斷器,仍是艙壁隔離模式,它們在使用過程當中都會出現異常狀況,異常狀況的處理方式間接影響到用戶的體驗,針對異常狀況的處理也有一種模式支撐,這就是回退(fallback)模式。
在超時,重試失敗,熔斷或者限流發生的時候,爲了及時恢復服務或者不影響到用戶體驗,須要提供回退的機制,常見的回退策略有:
自定義處理:在這種場景下,可使用默認數據,本地數據,緩存數據來臨時支撐,也能夠將請求放入隊列,或者使用備用服務獲取數據等,適用於業務的關鍵流程與嚴重影響用戶體驗的場景,如商家/產品信息等核心服務。
故障沉默(fail-silent):直接返回空值或缺省值,適用於可降級功能的場景,如產品推薦之類的功能,數據爲空也不太影響用戶體驗。
快速失敗(fail-fast):直接拋出異常,適用於數據非強依賴的場景,如非核心服務超時的處理。
在實際的工程實踐中,這四種模式既能夠單獨使用,也能夠組合使用,爲了讓讀者更好的理解這些模式的應用,下面以Netflix的開源組件Hystrix的流程爲例說明。
圖中流程的說明:
服務容錯模式在系統的穩定性保障方面應用不少,學習模式有助於新人直接利用熟練軟件工程師的經驗,對於提高系統的穩定性有很大的幫助。服務容錯的目的主要是爲了防微杜漸,除此以外錯誤的及時發現和監控其實同等重要。隨着技術的演化,新的模式在不斷的學習與實踐中沉澱出來,在構建一個高可用高性能的系統目標以外,讓系統愈來愈有彈性(Resilience)也是咱們新的追求。
參考:
https://tech.meituan.com/service_fault_tolerant_pattern.html
Netflix Hystrix Wiki(https://martinfowler.com/bliki/CircuitBreaker.html)
Martin Fowler. CircuitBreaker(https://martinfowler.com/bliki/CircuitBreaker.html) Hanmer R. Patterns for Fault Tolerant Software. Wiley, 2007. Nygard M. 發佈!軟件的設計與部署. 凃鳴 譯. 人民郵電出版社, 2015.