分布式系統(tǒng)環(huán)境下,服務(wù)間類似依賴非常常見,一個(gè)業(yè)務(wù)調(diào)用通常依賴多個(gè)基礎(chǔ)服務(wù)。例如當(dāng)庫存服務(wù)不可用時(shí),商品服務(wù)請(qǐng)求線程被阻塞,當(dāng)有大批量請(qǐng)求調(diào)用庫存服務(wù)時(shí),最終可能導(dǎo)致整個(gè)商品服務(wù)資源耗盡,無法繼續(xù)對(duì)外提供服務(wù)。并且這種不可用可能沿請(qǐng)求調(diào)用鏈向上傳遞,造成整個(gè)集群服務(wù)的不可用。這種現(xiàn)象被稱為雪崩效應(yīng)。
因此,微服務(wù)架構(gòu)中我們需要提供一些服務(wù)調(diào)用的保護(hù)機(jī)制,用于快速處理依賴故障。Hystrix是一個(gè)提供了服務(wù)隔離,快速失敗和服務(wù)限流的組件。它的設(shè)計(jì)原則如下:
- 在復(fù)雜的分布式系統(tǒng)中,阻止某一個(gè)依賴服務(wù)的故障在整個(gè)系統(tǒng)中蔓延。比如某一個(gè)服務(wù)故障了,導(dǎo)致其它服務(wù)也跟著故障。
- 對(duì)依賴服務(wù)調(diào)用時(shí)的調(diào)用失敗進(jìn)行控制和容錯(cuò)保護(hù)
- 支持快速失敗和快速恢復(fù)??焖偈∈侵府?dāng)服務(wù)不可用時(shí),服務(wù)調(diào)用能夠快速返回一個(gè)默認(rèn)結(jié)果(即fallback機(jī)制)??焖倩謴?fù)是指當(dāng)服務(wù)重新恢復(fù)可用時(shí),Hystrix能夠快速成功調(diào)用服務(wù)。
- 避免請(qǐng)求排隊(duì)和積壓,采用限流和 fail fast 來控制故障
- 支持近實(shí)時(shí)的監(jiān)控、報(bào)警以及運(yùn)維操作
1. 資源隔離
Hystrix提供的一項(xiàng)核心功能,即資源隔離。資源隔離要解決的最最核心的問題,就是將多個(gè)依賴服務(wù)的調(diào)用分別隔離到各自的資源池內(nèi)。避免說對(duì)某一個(gè)依賴服務(wù)的調(diào)用,因?yàn)橐蕾嚪?wù)的接口調(diào)用的延遲或者失敗,導(dǎo)致服務(wù)所有的線程資源全部耗費(fèi)在這個(gè)服務(wù)的接口調(diào)用上。一旦說某個(gè)服務(wù)的線程資源全部耗盡的話,就可能導(dǎo)致服務(wù)崩潰,甚至說這種故障會(huì)不斷蔓延。
Hystrix 實(shí)現(xiàn)資源隔離,主要有兩種技術(shù):線程池和信號(hào)量。默認(rèn)情況下,Hystrix 使用線程池模式。
下面是使用線程池模式的例子:
public class GetProductInfoCommand extends HystrixCommand<ProductInfo> {
private Long productId;
private static final HystrixCommandKey KEY = HystrixCommandKey.Factory.asKey("GetProductInfoCommand");
public GetProductInfoCommand(Long productId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService"))
.andCommandKey(KEY)
// 線程池相關(guān)配置信息
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
// 設(shè)置線程池大小為8
.withCoreSize(8)
// 設(shè)置等待隊(duì)列大小為10
.withMaxQueueSize(10)
.withQueueSizeRejectionThreshold(12))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerEnabled(true)
.withCircuitBreakerRequestVolumeThreshold(20)
.withCircuitBreakerErrorThresholdPercentage(40)
.withCircuitBreakerSleepWindowInMilliseconds(3000)
// 設(shè)置超時(shí)時(shí)間
.withExecutionTimeoutInMilliseconds(20000)
// 設(shè)置fallback最大請(qǐng)求并發(fā)數(shù)
.withFallbackIsolationSemaphoreMaxConcurrentRequests(30)));
this.productId = productId;
}
@Override
protected ProductInfo run() throws Exception {
System.out.println("調(diào)用接口查詢商品數(shù)據(jù),productId=" + productId);
if (productId == -1L) {
throw new Exception();
}
// 請(qǐng)求過來,會(huì)在這里hang住3秒鐘
if (productId == -2L) {
TimeUtils.sleep(3);
}
String url = "http://localhost:8081/getProductInfo?productId=" + productId;
String response = HttpClientUtils.sendGetRequest(url);
System.out.println(response);
return JSONObject.parseObject(response, ProductInfo.class);
}
@Override
protected ProductInfo getFallback() {
ProductInfo productInfo = new ProductInfo();
productInfo.setName("降級(jí)商品");
return productInfo;
}
}
服務(wù)調(diào)用時(shí),通過構(gòu)造Command實(shí)例進(jìn)行調(diào)用:
@RequestMapping("/getProductInfo")
@ResponseBody
public String getProductInfo(Long productId) {
HystrixCommand<ProductInfo> getProductInfoCommand = new GetProductInfoCommand(productId);
// 通過command執(zhí)行,獲取最新商品數(shù)據(jù)
ProductInfo productInfo = getProductInfoCommand.execute();
System.out.println(productInfo);
return "success";
}
我們可以看到,Hystrix的使用模式是通過構(gòu)造一個(gè)Command對(duì)象,通過實(shí)現(xiàn)run()方法實(shí)現(xiàn)調(diào)用邏輯,通過實(shí)現(xiàn)getFallback()方法實(shí)現(xiàn)降級(jí)返回(即服務(wù)調(diào)用失敗時(shí)的默認(rèn)返回)。
而在構(gòu)造函數(shù)中,通過設(shè)置線程池大小和阻塞隊(duì)列大小,我們輕松的實(shí)現(xiàn)了資源隔離和接口限流的功能。
線程池其實(shí)最大的好處就是對(duì)于網(wǎng)絡(luò)訪問請(qǐng)求,如果有超時(shí)的話,可以避免調(diào)用線程阻塞住。
使用信號(hào)量隔離的例子如下:
public class GetCityNameCommand extends HystrixCommand<String> {
private Long cityId;
public GetCityNameCommand(Long cityId) {
// 設(shè)置信號(hào)量隔離策略
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetCityNameGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)))
.withExecutionIsolationSemaphoreMaxConcurrentRequests(10);
this.cityId = cityId;
}
@Override
protected String run() {
// 需要進(jìn)行信號(hào)量隔離的代碼
return LocationCache.getCityName(cityId);
}
}
信號(hào)量隔離無法處理類似線程池隔離中的異步網(wǎng)絡(luò)調(diào)用和timeout等情形。
線程池隔離和信號(hào)量隔離的對(duì)比:
- 線程池技術(shù),適合絕大多數(shù)場(chǎng)景,比如說我們對(duì)依賴服務(wù)的網(wǎng)絡(luò)請(qǐng)求的調(diào)用和訪問、需要對(duì)調(diào)用的 timeout 進(jìn)行控制(捕捉 timeout 超時(shí)異常)。
- 信號(hào)量技術(shù),適合說你的訪問不是對(duì)外部網(wǎng)絡(luò)服務(wù)的調(diào)用,而是對(duì)內(nèi)部的一些比較復(fù)雜的業(yè)務(wù)邏輯的訪問。并且系統(tǒng)內(nèi)部的代碼,其實(shí)不涉及任何的網(wǎng)絡(luò)請(qǐng)求,那么只要做信號(hào)量的普通限流就可以了,因?yàn)椴恍枰ゲ东@ timeout 類似的問題。
2. 斷路器
斷路器是Hystrix中控制服務(wù)調(diào)用快速失敗和恢復(fù)的組件。它的工作模式如下:
- 正常情況下,斷路器關(guān)閉,服務(wù)消費(fèi)者正常請(qǐng)求微服務(wù)
- 一段事件內(nèi),失敗率達(dá)到一定閾值(比如50%失?。?,斷路器將斷開,此時(shí)不再請(qǐng)求服務(wù)提供者,而是只是快速失敗的方法(斷路方法)
- 斷路器打開一段時(shí)間,自動(dòng)進(jìn)入“半開”狀態(tài),此時(shí),斷路器可允許一個(gè)請(qǐng)求方法服務(wù)提供者,如果請(qǐng)求調(diào)用成功,則關(guān)閉斷路器,否則繼續(xù)保持?jǐn)嗦菲鞔蜷_狀態(tài)。
下面是Hystrix一個(gè)Command的執(zhí)行流程:
下面是斷路器的配置示例:
public GetProductInfoCommand(Long productId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService"))
.andCommandKey(KEY)
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
// 是否允許斷路器工作
.withCircuitBreakerEnabled(true)
// 滑動(dòng)窗口中,最少有多少個(gè)請(qǐng)求,才可能觸發(fā)斷路
.withCircuitBreakerRequestVolumeThreshold(20)
// 異常比例達(dá)到多少,才觸發(fā)斷路,默認(rèn)50%
.withCircuitBreakerErrorThresholdPercentage(40)
// 斷路后多少時(shí)間內(nèi)直接reject請(qǐng)求,之后進(jìn)入half-open狀態(tài),默認(rèn)5000ms
.withCircuitBreakerSleepWindowInMilliseconds(3000)));
this.productId = productId;
}
3. Request Cache
首先,有一個(gè)概念,叫做 Request Context 請(qǐng)求上下文,一般來說,在一個(gè) web 應(yīng)用中,如果我們用到了 Hystrix,我們會(huì)在一個(gè) filter 里面,對(duì)每一個(gè)請(qǐng)求都施加一個(gè)請(qǐng)求上下文。就是說,每一次請(qǐng)求,就是一次請(qǐng)求上下文。然后在這次請(qǐng)求上下文中,我們會(huì)去執(zhí)行 N 多代碼,調(diào)用 N 多依賴服務(wù),有的依賴服務(wù)可能還會(huì)調(diào)用好幾次。
在一次請(qǐng)求上下文中,如果有多個(gè) command,參數(shù)都是一樣的,調(diào)用的接口也是一樣的,而結(jié)果可以認(rèn)為也是一樣的。那么這個(gè)時(shí)候,我們可以讓第一個(gè) command 執(zhí)行返回的結(jié)果緩存在內(nèi)存中,然后這個(gè)請(qǐng)求上下文后續(xù)的其它對(duì)這個(gè)依賴的調(diào)用全部從內(nèi)存中取出緩存結(jié)果就可以了。
這樣的話,好處在于不用在一次請(qǐng)求上下文中反復(fù)多次執(zhí)行一樣的 command,避免重復(fù)執(zhí)行網(wǎng)絡(luò)請(qǐng)求,提升整個(gè)請(qǐng)求的性能。
舉個(gè)栗子。比如說我們?cè)谝淮握?qǐng)求上下文中,請(qǐng)求獲取 productId 為 1 的數(shù)據(jù),第一次緩存中沒有,那么會(huì)從商品服務(wù)中獲取數(shù)據(jù),返回最新數(shù)據(jù)結(jié)果,同時(shí)將數(shù)據(jù)緩存在內(nèi)存中。后續(xù)同一次請(qǐng)求上下文中,如果還有獲取 productId 為 1 的數(shù)據(jù)的請(qǐng)求,直接從緩存中取就好了。
注意,Request Cache的使用是針對(duì)同一個(gè)請(qǐng)求上下文而言的。
4. 降級(jí)
Hystrix 出現(xiàn)以下四種情況,都會(huì)去調(diào)用 fallback 降級(jí)機(jī)制:
- 斷路器處于打開的狀態(tài)。
- 資源池已滿(線程池+隊(duì)列 / 信號(hào)量)。
- Hystrix 調(diào)用各種接口,或者訪問外部依賴,比如 MySQL、Redis、Zookeeper、Kafka 等等,出現(xiàn)了任何異常的情況。
- 訪問外部依賴的時(shí)候,訪問時(shí)間過長(zhǎng),報(bào)了 TimeoutException 異常。
兩種最經(jīng)典的降級(jí)機(jī)制
純內(nèi)存數(shù)據(jù)
在降級(jí)邏輯中,你可以在內(nèi)存中維護(hù)一個(gè) ehcache,作為一個(gè)純內(nèi)存的基于 LRU 自動(dòng)清理的緩存,讓數(shù)據(jù)放在緩存內(nèi)。如果說外部依賴有異常,fallback 這里直接嘗試從 ehcache 中獲取數(shù)據(jù)。返回默認(rèn)值
fallback 降級(jí)邏輯中,也可以直接返回一個(gè)默認(rèn)值。