服务容错模式
https://tech.meituan.com/service-fault-tolerant-pattern.html
背景
隨著美團點評服務(wù)框架和服務(wù)治理體系的逐步成熟,服務(wù)化已成為公司內(nèi)部系統(tǒng)設(shè)計的趨勢。本著大系統(tǒng)小做、職責(zé)單一的原則,我們度假技術(shù)團隊對業(yè)務(wù)系統(tǒng)進行了不少服務(wù)化拆分工作。隨著業(yè)務(wù)復(fù)雜度的增加,依賴的服務(wù)也逐步增加,出現(xiàn)了不少由于服務(wù)調(diào)用出現(xiàn)異常問題而導(dǎo)致的重大事故,如:
1)系統(tǒng)依賴的某個服務(wù)發(fā)生延遲或者故障,數(shù)秒內(nèi)導(dǎo)致所有應(yīng)用資源(線程,隊列等)被耗盡,造成所謂的雪崩效應(yīng) (Cascading Failure),導(dǎo)致整個系統(tǒng)拒絕對外提供服務(wù)。
2)系統(tǒng)遭受惡意爬蟲襲擊,在放大效應(yīng)下沒有對下游依賴服務(wù)做好限速處理,最終導(dǎo)致下游服務(wù)崩潰。
容錯是一個很大的話題,受篇幅所限,本文將介紹僅限定在服務(wù)調(diào)用間常用的一些容錯模式。
設(shè)計原則
服務(wù)容錯的設(shè)計有個基本原則,就是“Design for Failure”。為了避免出現(xiàn)“千里之堤潰于蟻穴”這種情況,在設(shè)計上需要考慮到各種邊界場景和對于服務(wù)間調(diào)用出現(xiàn)的異常或延遲情況,同時在設(shè)計和編程時也要考慮周到。這一切都是為了達到以下目標:
1)一個依賴服務(wù)的故障不會嚴重破壞用戶的體驗。
2)系統(tǒng)能自動或半自動處理故障,具備自我恢復(fù)能力。
基于這個原則和目標,衍生出下文將要介紹的一些模式,能夠解決分布式服務(wù)調(diào)用中的一些問題,提高系統(tǒng)在故障發(fā)生時的存活能力。
一些經(jīng)典的容錯模式
所謂模式,其實就是某種場景下一類問題及其解決方案的總結(jié)歸納,往往可以重用。模式可以指導(dǎo)我們完成任務(wù),作出合理的系統(tǒng)設(shè)計方案,達到事半功倍的效果。而在服務(wù)容錯這個方向,行業(yè)內(nèi)已經(jīng)有了不少實踐總結(jié)出來的解決方案。
超時與重試(Timeout and Retry)
超時模式,是一種最常見的容錯模式,在美團點評的工程實踐中大量存在。常見的有設(shè)置網(wǎng)絡(luò)連接超時時間,一次RPC的響應(yīng)超時時間等。在分布式服務(wù)調(diào)用的場景中,它主要解決了當(dāng)依賴服務(wù)出現(xiàn)建立網(wǎng)絡(luò)連接或響應(yīng)延遲,不用無限等待的問題,調(diào)用方可以根據(jù)事先設(shè)計的超時時間中斷調(diào)用,及時釋放關(guān)鍵資源,如Web容器的連接數(shù),數(shù)據(jù)庫連接數(shù)等,避免整個系統(tǒng)資源耗盡出現(xiàn)拒絕對外提供服務(wù)這種情況。
重試模式,一般和超時模式結(jié)合使用,適用于對于下游服務(wù)的數(shù)據(jù)強依賴的場景(不強依賴的場景不建議使用!),通過重試來保證數(shù)據(jù)的可靠性或一致性,常用于因網(wǎng)絡(luò)抖動等導(dǎo)致服務(wù)調(diào)用出現(xiàn)超時的場景。與超時時間設(shè)置結(jié)合使用后,需要考慮接口的響應(yīng)時間分布情況,超時時間可以設(shè)置為依賴服務(wù)接口99.5%響應(yīng)時間的值,重試次數(shù)一般1-2次為宜,否則會導(dǎo)致請求響應(yīng)時間延長,拖累到整個系統(tǒng)。
一些實現(xiàn)說明:
public class RetryCommand<T> {private int maxRetries = 2;// 重試次數(shù) 默認2次private long retryInterval = 5;//重試間隔時間ms 默認5msprivate 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 timeoutserviceA.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"); }//省略 }限流(Rate Limiting/Load Shedder)
限流模式,常用于下游服務(wù)容量有限,但又怕出現(xiàn)突發(fā)流量猛增(如惡意爬蟲,節(jié)假日大促等)而導(dǎo)致下游服務(wù)因壓力過大而拒絕服務(wù)的場景。常見的限流模式有控制并發(fā)和控制速率,一個是限制并發(fā)的數(shù)量,一個是限制并發(fā)訪問的速率。
控制并發(fā)
屬于一種較常見的限流手段,在工程實踐中可以通過信號量機制(如Java中的Semaphore)來控制,舉個例子:
假如有一個需求,要讀取幾萬個文件的數(shù)據(jù),因為都是IO密集型任務(wù),我們可以啟動幾十個線程并發(fā)的讀取,但是如果讀到內(nèi)存后,還需要存儲到數(shù)據(jù)庫中,而數(shù)據(jù)庫的連接數(shù)只有10個,這時我們必須控制只有十個線程同時獲取數(shù)據(jù)庫連接保存數(shù)據(jù),否則會報錯無法獲取數(shù)據(jù)庫連接。這個時候,我們就可以使用Semaphore來控制并發(fā)數(shù),如:
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() {@Overridepublic void run() {try {s.acquire();System.out.println("save data");s.release();} catch (InterruptedException e) {e.printStack();}}});}threadPool.shutdown();} }在代碼中,雖然有30個線程在執(zhí)行,但是只允許10個并發(fā)的執(zhí)行。Semaphore的構(gòu)造方法Semaphore(int permits) 接受一個整型的數(shù)字,表示可用的許可證數(shù)量。Semaphore(10)表示允許10個線程獲取許可證,也就是最大并發(fā)數(shù)是10。Semaphore的用法也很簡單,首先線程使用Semaphore的acquire()獲取一個許可證,使用完之后調(diào)用release()歸還許可證,還可以用tryAcquire()方法嘗試獲取許可證。
控制速率
在我們的工程實踐中,常見的是使用令牌桶算法來實現(xiàn)這種模式,其他如漏桶算法也可以實現(xiàn)控制速率,但在我們的工程實踐中使用不多,這里不做介紹,讀者請自行了解。
在Wikipedia上,令牌桶算法是這么描述的:
每秒會有r個令牌放入桶中,或者說,每過1/r秒桶中增加一個令牌。
桶中最多存放b個令牌,如果桶滿了,新放入的令牌會被丟棄。
當(dāng)一個n字節(jié)的數(shù)據(jù)包到達時,消耗n個令牌,然后發(fā)送該數(shù)據(jù)包。
如果桶中可用令牌小于n,則該數(shù)據(jù)包將被緩存或丟棄。
令牌桶控制的是一個時間窗口內(nèi)通過的數(shù)據(jù)量,在API層面我們常說的QPS、TPS,正好是一個時間窗口內(nèi)的請求量或者事務(wù)量,只不過時間窗口限定在1s罷了。以一個恒定的速度往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當(dāng)桶里沒有令牌可取時,則拒絕服務(wù)。令牌桶的另外一個好處是可以方便的改變速度,一旦需要提高速率,則按需提高放入桶中的令牌的速率。
在我們的工程實踐中,通常使用Guava中的Ratelimiter來實現(xiàn)控制速率,如我們不希望每秒的任務(wù)提交超過兩個:
//速率是每秒兩個許可 final RateLimiter rateLimiter = RateLimiter.create(2.0);void submitTasks(List tasks, Executor executor) {for (Runnable task : tasks) {rateLimiter.acquire(); // 也許需要等待executor.execute(task);} }電路熔斷器(Circuit Breaker)
在我們的工程實踐中,偶爾會遇到一些服務(wù)由于網(wǎng)絡(luò)連接超時,系統(tǒng)有異常或load過高出現(xiàn)暫時不可用等情況,導(dǎo)致對這些服務(wù)的調(diào)用失敗,可能需要一段時間才能修復(fù),這種對請求的阻塞可能會占用寶貴的系統(tǒng)資源,如:內(nèi)存,線程,數(shù)據(jù)庫連接等等,最壞的情況下會導(dǎo)致這些資源被消耗殆盡,使得系統(tǒng)里不相關(guān)的部分所使用的資源也耗盡從而拖累整個系統(tǒng)。在這種情況下,調(diào)用操作能夠立即返回錯誤而不是等待超時的發(fā)生或者重試可能是一種更好的選擇,只有當(dāng)被調(diào)用的服務(wù)有可能成功時我們再去嘗試。
熔斷器模式可以防止我們的系統(tǒng)不斷地嘗試執(zhí)行可能會失敗的調(diào)用,使得我們的系統(tǒng)繼續(xù)執(zhí)行而不用等待修正錯誤,或者浪費CPU時間去等到長時間的超時產(chǎn)生。熔斷器模式也可以使我們系統(tǒng)能夠檢測錯誤是否已經(jīng)修正,如果已經(jīng)修正,系統(tǒng)會再次嘗試調(diào)用操作。下圖是個使用熔斷器模式的調(diào)用流程:
可以從圖中看出,當(dāng)超時出現(xiàn)的次數(shù)達到一定條件后,熔斷器會觸發(fā)打開狀態(tài),客戶端的下次調(diào)用將直接返回,不用等待超時產(chǎn)生。
在熔斷器內(nèi)部,往往有以下幾種狀態(tài):
1)閉合(closed)狀態(tài):該狀態(tài)下能夠?qū)δ繕朔?wù)或方法進行正常的調(diào)用。熔斷器類維護了一個時間窗口內(nèi)調(diào)用失敗的次數(shù),如果某次調(diào)用失敗,則失敗次數(shù)加1。如果最近失敗次數(shù)超過了在給定的時間窗口內(nèi)允許失敗的閾值(可以是數(shù)量也可以是比例),則熔斷器類切換到斷開(Open)狀態(tài)。此時熔斷器設(shè)置了一個計時器,當(dāng)時鐘超過了該時間,則切換到半斷開(Half-Open)狀態(tài),該睡眠時間的設(shè)定是給了系統(tǒng)一次機會來修正導(dǎo)致調(diào)用失敗的錯誤。
2)斷開(Open)狀態(tài):在該狀態(tài)下,對目標服務(wù)或方法的請求會立即返回錯誤響應(yīng),如果設(shè)置了fallback方法,則會進入fallback的流程。
3)半斷開(Half-Open)狀態(tài):允許對目標服務(wù)或方法的一定數(shù)量的請求可以去調(diào)用服務(wù)。如果這些請求對服務(wù)的調(diào)用成功,那么可以認為之前導(dǎo)致調(diào)用失敗的錯誤已經(jīng)修正,此時熔斷器切換到閉合狀態(tài)(并且將錯誤計數(shù)器重置);如果這一定數(shù)量的請求有調(diào)用失敗的情況,則認為導(dǎo)致之前調(diào)用失敗的問題仍然存在,熔斷器切回到斷開方式,然后開始重置計時器來給系統(tǒng)一定的時間來修正錯誤。半斷開狀態(tài)能夠有效防止正在恢復(fù)中的服務(wù)被突然而來的大量請求再次拖垮。
在我們的工程實踐中,熔斷器模式往往應(yīng)用于服務(wù)的自動降級,在實現(xiàn)上主要基于Netflix開源的組件Hystrix來實現(xiàn),下圖和代碼分別是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(); }艙壁隔離(Bulkhead Isolation)
在造船行業(yè),往往使用此類模式對船艙進行隔離,利用艙壁將不同的船艙隔離起來,這樣如果一個船艙破了進水,只損失一個船艙,其它船艙可以不受影響,而借鑒造船行業(yè)的經(jīng)驗,這種模式也在軟件行業(yè)得到使用。
線程隔離(Thread Isolation)就是這種模式的常見的一個場景。例如,系統(tǒng)A調(diào)用了ServiceB/ServiceC/ServiceD三個遠程服務(wù),且部署A的容器一共有120個工作線程,采用線程隔離機制,可以給對ServiceB/ServiceC/ServiceD的調(diào)用各分配40個線程。當(dāng)ServiceB慢了,給ServiceB分配的40個線程因慢而阻塞并最終耗盡,線程隔離可以保證給ServiceC/ServiceD分配的80個線程可以不受影響。如果沒有這種隔離機制,當(dāng)ServiceB慢的時候,120個工作線程會很快全部被對ServiceB的調(diào)用吃光,整個系統(tǒng)會全部慢下來,甚至出現(xiàn)系統(tǒng)停止響應(yīng)的情況。
這種Case在我們實踐中經(jīng)常遇到,如某接口由于數(shù)據(jù)庫慢查詢,外部RPC調(diào)用超時導(dǎo)致整個系統(tǒng)的線程數(shù)過高,連接數(shù)耗盡等。我們可以使用艙壁隔離模式,為這種依賴服務(wù)調(diào)用維護一個小的線程池,當(dāng)一個依賴服務(wù)由于響應(yīng)慢導(dǎo)致線程池任務(wù)滿的時候,不會影響到其他依賴服務(wù)的調(diào)用,它的缺點就是會增加線程數(shù)。
無論是超時/重試,熔斷器,還是艙壁隔離模式,它們在使用過程中都會出現(xiàn)異常情況,異常情況的處理方式間接影響到用戶的體驗,針對異常情況的處理也有一種模式支撐,這就是回退(fallback)模式。
回退(Fallback)
在超時,重試失敗,熔斷或者限流發(fā)生的時候,為了及時恢復(fù)服務(wù)或者不影響到用戶體驗,需要提供回退的機制,常見的回退策略有:
自定義處理:在這種場景下,可以使用默認數(shù)據(jù),本地數(shù)據(jù),緩存數(shù)據(jù)來臨時支撐,也可以將請求放入隊列,或者使用備用服務(wù)獲取數(shù)據(jù)等,適用于業(yè)務(wù)的關(guān)鍵流程與嚴重影響用戶體驗的場景,如商家/產(chǎn)品信息等核心服務(wù)。
故障沉默(fail-silent):直接返回空值或缺省值,適用于可降級功能的場景,如產(chǎn)品推薦之類的功能,數(shù)據(jù)為空也不太影響用戶體驗。
快速失敗(fail-fast):直接拋出異常,適用于數(shù)據(jù)非強依賴的場景,如非核心服務(wù)超時的處理。
應(yīng)用實例
在實際的工程實踐中,這四種模式既可以單獨使用,也可以組合使用,為了讓讀者更好的理解這些模式的應(yīng)用,下面以Netflix的開源組件Hystrix的流程為例說明。
圖中流程的說明:
將遠程服務(wù)調(diào)用邏輯封裝進一個HystrixCommand。
對于每次服務(wù)調(diào)用可以使用同步或異步機制,對應(yīng)執(zhí)行execute()或queue()。
判斷熔斷器(circuit-breaker)是否打開或者半打開狀態(tài),如果打開跳到步驟8,進行回退策略,如果關(guān)閉進入步驟4。
判斷線程池/隊列/信號量(使用了艙壁隔離模式)是否跑滿,如果跑滿進入回退步驟8,否則繼續(xù)后續(xù)步驟5。
run方法中執(zhí)行了實際的服務(wù)調(diào)用。
a. 服務(wù)調(diào)用發(fā)生超時時,進入步驟8。
判斷run方法中的代碼是否執(zhí)行成功。
a. 執(zhí)行成功返回結(jié)果。
b. 執(zhí)行中出現(xiàn)錯誤則進入步驟8。
所有的運行狀態(tài)(成功,失敗,拒絕,超時)上報給熔斷器,用于統(tǒng)計從而影響熔斷器狀態(tài)。
進入getFallback()回退邏輯。
a. 沒有實現(xiàn)getFallback()回退邏輯的調(diào)用將直接拋出異常。
b. 回退邏輯調(diào)用成功直接返回。
c. 回退邏輯調(diào)用失敗拋出異常。
返回執(zhí)行成功結(jié)果。
總結(jié)
服務(wù)容錯模式在美團點評系統(tǒng)的穩(wěn)定性保障方面應(yīng)用很多,學(xué)習(xí)模式有助于新人直接利用熟練軟件工程師的經(jīng)驗,對于提升系統(tǒng)的穩(wěn)定性有很大的幫助。服務(wù)容錯的目的主要是為了防微杜漸,除此之外錯誤的及時發(fā)現(xiàn)和監(jiān)控其實同等重要。隨著技術(shù)的演化,新的模式在不斷的學(xué)習(xí)與實踐中沉淀出來,美團點評度假技術(shù)團隊在構(gòu)建一個高可用高性能的系統(tǒng)目標之外,讓系統(tǒng)越來越有彈性(Resilience)也是我們新的追求。
參考文獻
Netflix Hystrix Wiki
Martin Fowler.?CircuitBreaker
Hanmer R. Patterns for Fault Tolerant Software. Wiley, 2007.
Nygard M. 發(fā)布!軟件的設(shè)計與部署. 凃鳴 譯. 人民郵電出版社, 2015.
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/articles/9271897.html
《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
- 上一篇: 微服务下的APM全链路监控
- 下一篇: 酷狗音乐的大数据实践