為什么需要Hystrix
在大中型分布式系統(tǒng)中,通常系統(tǒng)很多依賴,如下圖:
在高并發(fā)訪問下,這些依賴的穩(wěn)定性與否對系統(tǒng)的影響非常大,但是依賴有很多不可控問題:如網(wǎng)絡(luò)連接緩慢,資源繁忙,暫時(shí)不可用,服務(wù)脫機(jī)等,如下圖:
當(dāng)依賴阻塞時(shí),大多數(shù)服務(wù)器的線程池就出現(xiàn)阻塞,影響整個(gè)線上服務(wù)的穩(wěn)定性,如下圖:
在復(fù)雜的分布式架構(gòu)的應(yīng)用程序有很多的依賴,都會(huì)不可避免地在某些時(shí)候失敗。高并發(fā)的依賴失敗時(shí)如果沒有隔離措施,當(dāng)前應(yīng)用服務(wù)就有被拖垮的風(fēng)險(xiǎn)。
Hystrix如何解決依賴隔離
Hystrix使用命令模式HystrixCommand(Command)包裝依賴調(diào)用邏輯,每個(gè)命令在單獨(dú)線程中/信號授權(quán)下執(zhí)行。
可配置依賴調(diào)用超時(shí)時(shí)間,超時(shí)時(shí)間一般設(shè)為比99.5%平均時(shí)間略高即可。當(dāng)調(diào)用超時(shí)時(shí),直接返回或執(zhí)行fallback邏輯。
為每個(gè)依賴提供一個(gè)小的線程池或信號,如果線程池已滿調(diào)用將被立即拒絕,默認(rèn)不采用排隊(duì)。加速失敗判定時(shí)間。
依賴調(diào)用結(jié)果分:成功、失敗/拋出異常、超時(shí)、線程拒絕、短路。 請求失敗(異常,拒絕,超時(shí),短路)時(shí)執(zhí)行fallback(降級)邏輯。
提供熔斷器組件,可以自動(dòng)運(yùn)行或手動(dòng)調(diào)用,停止當(dāng)前依賴一段時(shí)間(10秒),熔斷器默認(rèn)錯(cuò)誤率閾值為50%,超過將自動(dòng)運(yùn)行。
提供近實(shí)時(shí)依賴的統(tǒng)計(jì)和監(jiān)控。
Hystrix依賴的隔離架構(gòu),如下圖:
如何使用Hystrix
使用maven引入Hystrix依賴
<hystrix.version>1.3.16</hystrix.version>
<hystrix-metrics-event-stream.version>1.1.2</hystrix-metrics-event-stream.version>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>${hystrix.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-metrics-event-stream</artifactId>
<version>${hystrix-metrics-event-stream.version}</version>
</dependency>
使用命令模式封裝依賴邏輯
public class HelloWorldCommand extends HystrixCommand<String> {
private final String name;
public HelloWorldCommand(String name) {
//最少配置:指定命令組名(CommandGroup)
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}
@Override
protected String run() {
// 依賴邏輯封裝在run()方法中
return "Hello " + name +" thread:" + Thread.currentThread().getName();
}
//調(diào)用實(shí)例
public static void main(String[] args) throws Exception{
//每個(gè)Command對象只能調(diào)用一次,不可以重復(fù)調(diào)用,
//重復(fù)調(diào)用對應(yīng)異常信息
HelloWorldCommand helloWorldCommand = new HelloWorldCommand("sync-hystrix");
//使用execute()同步調(diào)用代碼,效果等同于:helloWorldCommand.queue().get();
String result = helloWorldCommand.execute();
System.out.println("result=" + result);
helloWorldCommand = new HelloWorldCommand("async-hystrix");
//異步調(diào)用,可自由控制獲取結(jié)果時(shí)機(jī),
Future<String> future = helloWorldCommand.queue();
//get操作不能超過command定義的超時(shí)時(shí)間,默認(rèn):1秒
result = future.get(100, TimeUnit.MILLISECONDS);
System.out.println("result=" + result);
System.out.println("mainThread=" + Thread.currentThread().getName());
}
}
使用Fallback() 提供降級策略
//重載HystrixCommand的getFallback方法實(shí)現(xiàn)邏輯
public class HelloWorldCommand extends HystrixCommand<String> {
private final String name;
public HelloWorldCommand(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationThreadTimeoutInMilliseconds(500)));
this.name = name;
}
@Override
protected String getFallback() {
return "exeucute Falled";
}
@Override
protected String run() throws Exception {
//sleep 1 秒,調(diào)用會(huì)超時(shí)
TimeUnit.MILLISECONDS.sleep(1000);
return "Hello " + name +" thread:" + Thread.currentThread().getName();
}
public static void main(String[] args) throws Exception{
HelloWorldCommand command = new HelloWorldCommand("test-Fallback");
String result = command.execute();
}
}
NOTE: 除了HystrixBadRequestException異常之外,所有從run()方法拋出的異常都算作失敗,并觸發(fā)降級getFallback()和斷路器邏輯。
HystrixBadRequestException用在非法參數(shù)或非系統(tǒng)故障異常等不應(yīng)觸發(fā)回退邏輯的場景。
依賴命名:CommandKey
public HelloWorldCommand(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
/* HystrixCommandKey工廠定義依賴名稱 */
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld")));
this.name = name;
}
NOTE: 每個(gè)CommandKey代表一個(gè)依賴抽象,相同的依賴要使用相同的CommandKey名稱。依賴隔離的根本就是對相同CommandKey的依賴做隔離。
依賴分組:CommandGroup
命令分組用于對依賴操作分組,便于統(tǒng)計(jì),匯總等。
//使用HystrixCommandGroupKey工廠定義
public HelloWorldCommand(String name) {
Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"))
}
NOTE: CommandGroup是每個(gè)命令最少配置的必選參數(shù),在不指定ThreadPoolKey的情況下,字面值用于對不同依賴的線程池/信號區(qū)分。
線程池/信號:ThreadPoolKey
public HelloWorldCommand(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))
/* 使用HystrixThreadPoolKey工廠定義線程池名稱*/
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")));
this.name = name;
}
NOTE: 當(dāng)對同一業(yè)務(wù)依賴做隔離時(shí)使用CommandGroup做區(qū)分,但是對同一依賴的不同遠(yuǎn)程調(diào)用如(一個(gè)是redis 一個(gè)是http),可以使用HystrixThreadPoolKey做隔離區(qū)分。
最然在業(yè)務(wù)上都是相同的組,但是需要在資源上做隔離時(shí),可以使用HystrixThreadPoolKey區(qū)分。
信號量隔離:SEMAPHORE
隔離本地代碼或可快速返回遠(yuǎn)程調(diào)用(如memcached,redis)可以直接使用信號量隔離,降低線程隔離開銷。
public class HelloWorldCommand extends HystrixCommand<String> {
private final String name;
public HelloWorldCommand(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"))
/* 配置信號量隔離方式,默認(rèn)采用線程池隔離 */
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)));
this.name = name;
}
@Override
protected String run() throws Exception {
return "HystrixThread:" + Thread.currentThread().getName();
}
public static void main(String[] args) throws Exception{
HelloWorldCommand command = new HelloWorldCommand("semaphore");
String result = command.execute();
System.out.println(result);
System.out.println("MainThread:" + Thread.currentThread().getName());
}
}
Hystrix關(guān)鍵組件分析
Hystrix流程結(jié)構(gòu)解析
流程說明:
1,每次調(diào)用創(chuàng)建一個(gè)新的HystrixCommand,把依賴調(diào)用封裝在run()方法中
2,執(zhí)行execute()/queue做同步或異步調(diào)用
3,判斷熔斷器(circuit-breaker)是否打開,如果打開跳到步驟8,進(jìn)行降級策略,否則繼續(xù)后續(xù)步驟
4,判斷線程池/隊(duì)列/信號量是否跑滿,如果跑滿進(jìn)入降級步驟8,否則繼續(xù)后續(xù)步驟
5,調(diào)用HystrixCommand的run方法,運(yùn)行依賴邏輯
a 依賴邏輯調(diào)用超時(shí),進(jìn)入步驟8
6,判斷邏輯是否調(diào)用成功
a 返回成功調(diào)用結(jié)果
b 調(diào)用出錯(cuò),進(jìn)入步驟8
7,計(jì)算熔斷器狀態(tài),所有的運(yùn)行狀態(tài)上報(bào)給熔斷器,用于統(tǒng)計(jì)從而判斷熔斷器狀態(tài)
8,getFallback()降級邏輯
以下四種情況將觸發(fā)getFallback調(diào)用:
- run()方法拋出非HystrixBadRequestException異常
- run()方法調(diào)用超時(shí)
- 熔斷器開啟攔截調(diào)用
- 線程池/隊(duì)列/信號量是否跑滿
沒有實(shí)現(xiàn)getFallback的Command將直接拋出異常
fallback降級邏輯調(diào)用成功直接返回
降級邏輯調(diào)用失敗拋出異常
9,返回執(zhí)行成功結(jié)果
熔斷器:Circuit Breaker
Circuit Breaker 流程架構(gòu)和統(tǒng)計(jì)
每個(gè)熔斷器默認(rèn)維護(hù)10個(gè)bucket,每秒一個(gè)bucket,每個(gè)blucket記錄成功、失敗、超時(shí)、拒絕的狀態(tài),默認(rèn)錯(cuò)誤超過50%且10秒內(nèi)超過20個(gè)請求進(jìn)行中斷攔截.。
隔離(Isolation)分析
Hystrix隔離方式采用線程/信號的方式,通過隔離限制依賴的并發(fā)量和阻塞擴(kuò)散。
(1) 線程隔離
把執(zhí)行依賴代碼的線程與請求線程分離,請求線程可以自由控制離開的時(shí)間(異步過程)。
通過線程池大小可以控制并發(fā)量,當(dāng)線程池飽和時(shí)可以提前拒絕服務(wù),防止依賴問題擴(kuò)散。
線上建議線程池不要設(shè)置過大,否則大量堵塞線程有可能會(huì)拖慢服務(wù)器。
線程池的使用示意圖如下圖所示,當(dāng)n個(gè)請求線程并發(fā)對某個(gè)接口請求調(diào)用時(shí),會(huì)先從hystrix管理的線程池里面獲得一個(gè)線程,然后將參數(shù)傳遞給這個(gè)線程去執(zhí)行真正調(diào)用。線程池的大小有限,默認(rèn)是10個(gè)線程,可以使用maxConcurrentRequests參數(shù)配置,如果并發(fā)請求數(shù)多于線程池線程個(gè)數(shù),就有線程需要進(jìn)入隊(duì)列排隊(duì),但排隊(duì)隊(duì)列也有上限,默認(rèn)是 5,如果排隊(duì)隊(duì)列也滿,則必定有請求線程會(huì)走fallback流程。
線程池模式可以支持異步調(diào)用,支持超時(shí)調(diào)用,支持直接熔斷,存在線程切換,開銷大。
(2) 線程隔離的優(yōu)缺點(diǎn)
線程隔離的優(yōu)點(diǎn):
使用線程可以完全隔離第三方代碼,請求線程可以快速放回。
當(dāng)一個(gè)失敗的依賴再次變成可用時(shí),線程池將清理,并立即恢復(fù)可用,而不是一個(gè)長時(shí)間的恢復(fù)。
可以完全模擬異步調(diào)用,方便異步編程。
線程隔離的缺點(diǎn):
線程池的主要缺點(diǎn)是它增加了cpu,因?yàn)槊總€(gè)命令的執(zhí)行涉及到排隊(duì)(默認(rèn)使用SynchronousQueue避免排隊(duì)),調(diào)度和上下文切換。
對使用ThreadLocal等依賴線程狀態(tài)的代碼增加復(fù)雜性,需要手動(dòng)傳遞和清理線程狀態(tài)。
NOTE: Netflix公司內(nèi)部認(rèn)為線程隔離開銷足夠小,不會(huì)造成重大的成本或性能的影響。
Netflix內(nèi)部API每天100億的HystrixCommand依賴請求使用線程隔,每個(gè)應(yīng)用大約40多個(gè)線程池,每個(gè)線程池大約5-20個(gè)線程。
(3) 信號隔離
信號隔離也可以用于限制并發(fā)訪問,防止阻塞擴(kuò)散, 與線程隔離最大不同在于執(zhí)行依賴代碼的線程依然是請求線程(該線程需要通過信號申請)。
如果客戶端是可信的且可以快速返回,可以使用信號隔離替換線程隔離,降低開銷。
線程隔離與信號隔離區(qū)別如下圖:
信號量的使用示意圖如下圖所示,當(dāng)n個(gè)并發(fā)請求去調(diào)用一個(gè)目標(biāo)服務(wù)接口時(shí),都要獲取一個(gè)信號量才能真正去調(diào)用目標(biāo)服務(wù)接口,但信號量有限,默認(rèn)是10個(gè),可以使用maxConcurrentRequests參數(shù)配置,如果并發(fā)請求數(shù)多于信號量個(gè)數(shù),就有線程需要進(jìn)入隊(duì)列排隊(duì),但排隊(duì)隊(duì)列也有上限,默認(rèn)是 5,如果排隊(duì)隊(duì)列也滿,則必定有請求線程會(huì)走fallback流程,從而達(dá)到限流和防止雪崩的目的。
信號量模式從始至終都只有請求線程自身,是同步調(diào)用模式,不支持超時(shí)調(diào)用,不支持直接熔斷,由于沒有線程的切換,開銷非常小。
(4) 總結(jié)
當(dāng)請求的服務(wù)網(wǎng)絡(luò)開銷比較大的時(shí)候,或者是請求比較耗時(shí)的時(shí)候,我們最好是使用線程隔離策略,這樣的話,可以保證大量的容器(tomcat)線程可用,不會(huì)由于服務(wù)原因,一直處于阻塞或等待狀態(tài),快速失敗返回。而當(dāng)我們請求緩存這些服務(wù)的時(shí)候,我們可以使用信號量隔離策略,因?yàn)檫@類服務(wù)的返回通常會(huì)非常的快,不會(huì)占用容器線程太長時(shí)間,而且也減少了線程切換的一些開銷,提高了緩存服務(wù)的效率。
線程池:適合絕大多數(shù)的場景,99%的。對依賴服務(wù)的網(wǎng)絡(luò)請求的調(diào)用和訪問,timeout這種問題
信號量:適合你的訪問不是對外部依賴的訪問,而是對內(nèi)部的一些比較復(fù)雜的業(yè)務(wù)邏輯的訪問,但是像這種訪問,系統(tǒng)內(nèi)部的代碼,其實(shí)不涉及任何的網(wǎng)絡(luò)請求,那么只要做信號量的普通限流就可以了,因?yàn)椴恍枰ゲ东@timeout類似的問題,算法+數(shù)據(jù)結(jié)構(gòu)的效率不是太高,并發(fā)量突然太高,因?yàn)檫@里稍微耗時(shí)一些,導(dǎo)致很多線程卡在這里的話,不太好,所以進(jìn)行一個(gè)基本的資源隔離和訪問,避免內(nèi)部復(fù)雜的低效率的代碼,導(dǎo)致大量的線程被hang住
Reference:
http://blog.51cto.com/developerycj/1950881
對 JAVA 開發(fā)有興趣的朋友歡迎加入QQ群:833145934 里面資深架構(gòu)師會(huì)分享一些整理好的錄制視頻錄像和BATJ面試題:有Spring,MyBatis,Netty源碼分析,高并發(fā)、高性能、分布式、微服務(wù)架構(gòu)的原理,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備的知識體系。還能領(lǐng)取免費(fèi)的學(xué)習(xí)資源,目前受益良多。
共同探討!