1.Hystrix 是什么?
在分布式系統(tǒng)中迹栓,每個服務(wù)都可能會調(diào)用很多其他服務(wù)荐虐,被調(diào)用的那些服務(wù)就是依賴服務(wù)藕夫,有的時候某些依賴服務(wù)出現(xiàn)故障也是很正常的。
Hystrix 可以讓我們在分布式系統(tǒng)中對服務(wù)間的調(diào)用進(jìn)行控制渴语,加入一些調(diào)用延遲或者依賴故障的容錯機(jī)制苹威。
Hystrix 通過將依賴服務(wù)進(jìn)行資源隔離,進(jìn)而阻止某個依賴服務(wù)出現(xiàn)故障時在整個系統(tǒng)所有的依賴服務(wù)調(diào)用中進(jìn)行蔓延驾凶;同時 Hystrix 還提供故障時的 fallback 降級機(jī)制牙甫。
總而言之掷酗,Hystrix 通過這些方法幫助我們提升分布式系統(tǒng)的可用性和穩(wěn)定性。
Hystrix 的歷史
Hystrix 是高可用性保障的一個框架腹暖。Netflix(可以認(rèn)為是國外的優(yōu)酷或者愛奇藝之類的視頻網(wǎng)站)的 API團(tuán)隊從 2011 年開始做一些提升系統(tǒng)可用性和穩(wěn)定性的工作汇在,Hystrix 就是從那時候開始發(fā)展出來的。
在 2012 年的時候脏答,Hystrix 就變得比較成熟和穩(wěn)定了糕殉,Netflix 中,除了 API 團(tuán)隊以外殖告,很多其他的團(tuán)隊都開始使用 Hystrix阿蝶。
時至今日,Netflix 中每天都有數(shù)十億次的服務(wù)間調(diào)用黄绩,通過 Hystrix 框架在進(jìn)行羡洁,而 Hystrix 也幫助Netflix 網(wǎng)站提升了整體的可用性和穩(wěn)定性。
2018 年 11 月爽丹,Hystrix 在其 Github 主頁宣布筑煮,不再開放新功能,推薦開發(fā)者使用其他仍然活躍的開源
項目粤蝎。維護(hù)模式的轉(zhuǎn)變絕不意味著 Hystrix 不再有價值真仲。相反,Hystrix 激發(fā)了很多偉大的想法和項目初澎,我們高可用的這一塊知識還是會針對 Hystrix 進(jìn)行講解秸应。
Hystrix 的設(shè)計原則
? ? ? ? ?對依賴服務(wù)調(diào)用時出現(xiàn)的調(diào)用延遲和調(diào)用失敗進(jìn)行控制和容錯保護(hù)。
????????在復(fù)雜的分布式系統(tǒng)中碑宴,阻止某一個依賴服務(wù)的故障在整個系統(tǒng)中蔓延软啼。比如某一個服務(wù)故障了,導(dǎo)致其它服務(wù)也跟著故障延柠。
????????提供 fail-fast(快速失敾雠病)和快速恢復(fù)的支持。
????????提供 fallback 優(yōu)雅降級的支持贞间。
????????支持近實時的監(jiān)控匕积、報警以及運維操作。
舉個栗子榜跌。
有這樣一個分布式系統(tǒng)闪唆,服務(wù) A 依賴于服務(wù) B,服務(wù) B 依賴于服務(wù) C/D/E钓葫。在這樣一個成熟的系統(tǒng)內(nèi)悄蕾,比如說最多可能只有 100 個線程資源。正常情況下,40 個線程并發(fā)調(diào)用服務(wù) C帆调,各 30 個線程并發(fā)調(diào)用D/E奠骄。
調(diào)用服務(wù) C,只需要 20ms番刊,現(xiàn)在因為服務(wù) C 故障了含鳞,比如延遲,或者掛了芹务,此時線程會 hang 住 2s 左右蝉绷。40 個線程全部被卡住,由于請求不斷涌入枣抱,其它的線程也用來調(diào)用服務(wù) C熔吗,同樣也會被卡住。這樣導(dǎo)致服務(wù) B 的線程資源被耗盡佳晶,無法接收新的請求桅狠,甚至可能因為大量線程不斷的運轉(zhuǎn),導(dǎo)致自己宕機(jī)轿秧。服務(wù) A 也掛中跌。
Hystrix 可以對其進(jìn)行資源隔離,比如限制服務(wù) B 只有 40 個線程調(diào)用服務(wù) C菇篡。當(dāng)此 40 個線程被 hang住時晒他,其它 60 個線程依然能正常調(diào)用工作。從而確保整個系統(tǒng)不會被拖垮逸贾。
Hystrix 更加細(xì)節(jié)的設(shè)計原則
????????????????阻止任何一個依賴服務(wù)耗盡所有的資源,比如 tomcat 中的所有線程資源津滞。
????????????????避免請求排隊和積壓铝侵,采用限流和 fail fast 來控制故障。
????????????????提供 fallback 降級機(jī)制來應(yīng)對故障触徐。
????????????????使用資源隔離技術(shù)咪鲜,比如 bulkhead(艙壁隔離技術(shù))、swimlane(泳道技術(shù))撞鹉、circuit breaker(斷路技術(shù))來限制任何一個依賴服務(wù)的故障的影響疟丙。
????????????????通過近實時的統(tǒng)計/監(jiān)控/報警功能,來提高故障發(fā)現(xiàn)的速度鸟雏。
????????????????通過近實時的屬性和配置熱修改功能享郊,來提高故障處理和恢復(fù)的速度。
????????????????保護(hù)依賴服務(wù)調(diào)用的所有故障情況孝鹊,而不僅僅只是網(wǎng)絡(luò)故障情況炊琉。
2.基于 Hystrix 線程池技術(shù)實現(xiàn)資源隔離
上一講提到,如果從 Nginx 開始,緩存都失效了苔咪,Nginx 會直接通過緩存服務(wù)調(diào)用商品服務(wù)獲取最新商品數(shù)據(jù)(我們基于電商項目做個討論)锰悼,有可能出現(xiàn)調(diào)用延時而把緩存服務(wù)資源耗盡的情況。這里团赏,我們就來說說箕般,怎么通過 Hystrix 線程池技術(shù)實現(xiàn)資源隔離。
資源隔離舔清,就是說丝里,你如果要把對某一個依賴服務(wù)的所有調(diào)用請求,全部隔離在同一份資源池內(nèi)鸠踪,不會去用其它資源了丙者,這就叫資源隔離。哪怕對這個依賴服務(wù)营密,比如說商品服務(wù)械媒,現(xiàn)在同時發(fā)起的調(diào)用量已經(jīng)到了 1000,但是線程池內(nèi)就 10 個線程评汰,最多就只會用這 10 個線程去執(zhí)行纷捞,不會說,對商品服務(wù)的請求被去,因為接口調(diào)用延時主儡,將 tomcat 內(nèi)部所有的線程資源全部耗盡。
Hystrix 進(jìn)行資源隔離惨缆,其實是提供了一個抽象糜值,叫做 command。這也是 Hystrix 最最基本的資源隔離技術(shù)坯墨。
利用 HystrixCommand 獲取單條數(shù)據(jù)
我們通過將調(diào)用商品服務(wù)的操作封裝在 HystrixCommand 中寂汇,限定一個 key,比如下面的 GetProductInfoCommandGroup捣染,在這里我們可以簡單認(rèn)為這是一個線程池骄瓣,每次調(diào)用商品服務(wù),就只會用該線程池中的資源耍攘,不會再去用其它線程資源了榕栏。
public class GetProductInfoCommand extends HystrixCommand<ProductInfo> {
private Long productId;
public GetProductInfoCommand(Long productId) {
super(HystrixCommandGroupKey.Factory.asKey("GetProductInfoCommandGroup"));
this.productId = productId;
}
@Override
protected ProductInfo run() {
String url = "http://localhost:8081/getProductInfo?productId=" + productId;
// 調(diào)用商品服務(wù)接口
String response = HttpClientUtils.sendGetRequest(url);
return JSONObject.parseObject(response, ProductInfo.class);
}
}
我們在緩存服務(wù)接口中,根據(jù) productId 創(chuàng)建 command 并執(zhí)行蕾各,獲取到商品數(shù)據(jù)扒磁。
@RequestMapping("/getProductInfo")@ResponseBodypublic String getProductInfo(Long productId) {
HystrixCommand<ProductInfo> getProductInfoCommand = new GetProductInfoCommand(productId);
// 通過 command 執(zhí)行,獲取最新商品數(shù)據(jù)
ProductInfo productInfo = getProductInfoCommand.execute();
System.out.println(productInfo);
return "success";
}
上面執(zhí)行的是 execute() 方法式曲,其實是同步的渗磅。也可以對 command 調(diào)用 queue() 方法,它僅僅是將command 放入線程池的一個等待隊列,就立即返回始鱼,拿到一個 Future 對象仔掸,后面可以繼續(xù)做其它一些事情,然后過一段時間對 Future 調(diào)用 get() 方法獲取數(shù)據(jù)医清。這是異步的起暮。
利用 HystrixObservableCommand 批量獲取數(shù)據(jù)
只要是獲取商品數(shù)據(jù),全部都綁定到同一個線程池里面去会烙,我們通過 HystrixObservableCommand 的一個線程去執(zhí)行负懦,而在這個線程里面,批量把多個 productId 的 productInfo 拉回來柏腻。
public class GetProductInfosCommand extends HystrixObservableCommand<ProductInfo> {
private String[] productIds;
public GetProductInfosCommand(String[] productIds) {
// 還是綁定在同一個線程池
super(HystrixCommandGroupKey.Factory.asKey("GetProductInfoGroup"));
this.productIds = productIds;
}
@Override
protected Observable<ProductInfo> construct() {
return Observable.unsafeCreate((Observable.OnSubscribe<ProductInfo>) subscriber -> {
for (String productId : productIds) {
// 批量獲取商品數(shù)據(jù)
String url = "http://localhost:8081/getProductInfo?productId=" + productId;
String response = HttpClientUtils.sendGetRequest(url);
ProductInfo productInfo = JSONObject.parseObject(response, ProductInfo.class);
subscriber.onNext(productInfo);
}
subscriber.onCompleted();
}).subscribeOn(Schedulers.io());
}
}
在緩存服務(wù)接口中纸厉,根據(jù)傳來的 id 列表,比如是以 , 分隔的 id 串五嫂,通過上面的HystrixObservableCommand颗品,執(zhí)行 Hystrix 的一些 API 方法,獲取到所有商品數(shù)據(jù)沃缘。
public String getProductInfos(String productIds) {
String[] productIdArray = productIds.split(",");
HystrixObservableCommand<ProductInfo> getProductInfosCommand = new
GetProductInfosCommand(productIdArray);
Observable<ProductInfo> observable = getProductInfosCommand.observe();
observable.subscribe(new Observer<ProductInfo>() {
@Override
public void onCompleted() {
System.out.println("獲取完了所有的商品數(shù)據(jù)");
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
/** * 獲取完一條數(shù)據(jù)躯枢,就回調(diào)一次這個方法 * @param productInfo
*/
@Override
public void onNext(ProductInfo productInfo) {
System.out.println(productInfo);
}
});
return "success";
}
我們回過頭來,看看 Hystrix 線程池技術(shù)是如何實現(xiàn)資源隔離的槐臀。
從 Nginx 開始锄蹂,緩存都失效了,那么 Nginx 通過緩存服務(wù)去調(diào)用商品服務(wù)水慨。緩存服務(wù)默認(rèn)的線程大小是 10個得糜,最多就只有 10 個線程去調(diào)用商品服務(wù)的接口。即使商品服務(wù)接口故障了晰洒,最多就只有 10 個線程會hang 死在調(diào)用商品服務(wù)接口的路上朝抖,緩存服務(wù)的 tomcat 內(nèi)其它的線程還是可以用來調(diào)用其它的服務(wù),干其它的事情欢顷。
3.基于 Hystrix 信號量機(jī)制實現(xiàn)資源隔離
Hystrix 里面核心的一項功能,其實就是所謂的資源隔離捉蚤,要解決的最最核心的問題抬驴,就是將多個依賴服務(wù)的調(diào)用分別隔離到各自的資源池內(nèi)。避免說對某一個依賴服務(wù)的調(diào)用缆巧,因為依賴服務(wù)的接口調(diào)用的延遲或者失敗布持,導(dǎo)致服務(wù)所有的線程資源全部耗費在這個服務(wù)的接口調(diào)用上。一旦說某個服務(wù)的線程資源全部耗盡的話陕悬,就可能導(dǎo)致服務(wù)崩潰题暖,甚至說這種故障會不斷蔓延。
Hystrix 實現(xiàn)資源隔離,主要有兩種技術(shù):
????????????線程池
????????????信號量
默認(rèn)情況下胧卤,Hystrix 使用線程池模式唯绍。
前面已經(jīng)說過線程池技術(shù)了,這一小節(jié)就來說說信號量機(jī)制實現(xiàn)資源隔離枝誊,以及這兩種技術(shù)的區(qū)別與具體應(yīng)用場景况芒。
信號量機(jī)制
信號量的資源隔離只是起到一個開關(guān)的作用,比如叶撒,服務(wù) A 的信號量大小為 10绝骚,那么就是說它同時只允許有 10 個 tomcat 線程來訪問服務(wù) A,其它的請求都會被拒絕祠够,從而達(dá)到資源隔離和限流保護(hù)的作用压汪。
線程池與信號量區(qū)別
線程池隔離技術(shù),并不是說去控制類似 tomcat 這種 web 容器的線程古瓤。更加嚴(yán)格的意義上來說止剖,Hystrix 的線程池隔離技術(shù),控制的是 tomcat 線程的執(zhí)行湿滓。Hystrix 線程池滿后滴须,會確保說,tomcat 的線程不會因為依賴服務(wù)的接口調(diào)用延遲或故障而被 hang 住叽奥,tomcat 其它的線程不會卡死扔水,可以快速返回,然后支撐其它的事情朝氓。
線程池隔離技術(shù)魔市,是用 Hystrix 自己的線程去執(zhí)行調(diào)用;而信號量隔離技術(shù)赵哲,是直接讓 tomcat 線程去調(diào)用依賴服務(wù)待德。信號量隔離,只是一道關(guān)卡枫夺,信號量有多少将宪,就允許多少個 tomcat 線程通過它,然后去執(zhí)行橡庞。
適用場景:
? 線程池技術(shù)较坛,適合絕大多數(shù)場景,比如說我們對依賴服務(wù)的網(wǎng)絡(luò)請求的調(diào)用和訪問扒最、需要對調(diào)用的timeout 進(jìn)行控制(捕捉 timeout 超時異常)丑勤。
? 信號量技術(shù),適合說你的訪問不是對外部依賴的訪問吧趣,而是對內(nèi)部的一些比較復(fù)雜的業(yè)務(wù)邏輯的訪問法竞,并且系統(tǒng)內(nèi)部的代碼耙厚,其實不涉及任何的網(wǎng)絡(luò)請求,那么只要做信號量的普通限流就可以了岔霸,因為不需要去捕獲 timeout 類似的問題薛躬。
信號量簡單 Demo
業(yè)務(wù)背景里,比較適合信號量的是什么場景呢秉剑?
比如說泛豪,我們一般來說,緩存服務(wù)侦鹏,可能會將一些量特別少诡曙、訪問又特別頻繁的數(shù)據(jù),放在自己的純內(nèi)存中略水。
舉個栗子价卤。一般我們在獲取到商品數(shù)據(jù)之后,都要去獲取商品是屬于哪個地理位置渊涝、省慎璧、市、賣家等跨释,可能在自己的純內(nèi)存中胸私,比如就一個 Map 去獲取。對于這種直接訪問本地內(nèi)存的邏輯鳖谈,比較適合用信號量做一下簡單的隔離岁疼。
優(yōu)點在于,不用自己管理線程池啦缆娃,不用 care timeout 超時啦捷绒,也不需要進(jìn)行線程的上下文切換啦。信號量做隔離的話贯要,性能相對來說會高一些暖侨。
假如這是本地緩存,我們可以通過 cityId崇渗,拿到 cityName字逗。
public class LocationCache {
private static Map<Long, String> cityMap = new HashMap<>();
static {
cityMap.put(1L, "北京");
}
/** * 通過 cityId 獲取 cityName * * @param cityId 城市 id * @return 城市名
*/
public static String getCityName(Long cityId) {
return cityMap.get(cityId);
}
}
寫一個 GetCityNameCommand,策略設(shè)置為信號量宅广。run() 方法中獲取本地緩存葫掉。我們目的就是對獲取本地緩存的代碼進(jìn)行資源隔離。
public class GetCityNameCommand extends HystrixCommand<String> {
private Long cityId;
public GetCityNameCommand(Long cityId) {
// 設(shè)置信號量隔離策略
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetCityNameGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIs
olationStrategy.SEMAPHORE)));
this.cityId = cityId;
}
@Override
protected String run() {
// 需要進(jìn)行信號量隔離的代碼
return LocationCache.getCityName(cityId);
}
}
在接口層乘碑,通過創(chuàng)建 GetCityNameCommand挖息,傳入 cityId金拒,執(zhí)行 execute() 方法兽肤,那么獲取本地 cityName緩存的代碼將會進(jìn)行信號量的資源隔離套腹。
@RequestMapping("/getProductInfo")@ResponseBodypublic String getProductInfo(Long productId) {
HystrixCommand<ProductInfo> getProductInfoCommand = new GetProductInfoCommand(productId);
// 通過 command 執(zhí)行,獲取最新商品數(shù)據(jù)
ProductInfo productInfo = getProductInfoCommand.execute();
Long cityId = productInfo.getCityId();
GetCityNameCommand getCityNameCommand = new GetCityNameCommand(cityId);
// 獲取本地內(nèi)存(cityName)的代碼會被信號量進(jìn)行資源隔離
String cityName = getCityNameCommand.execute();
productInfo.setCityName(cityName);
System.out.println(productInfo);
return "success";
}