Hystrix 分佈式系統限流、降級、熔斷框架

爲何須要Hystrix

在大中型分佈式系統中,一般系統不少依賴,以下圖:html

在高併發訪問下,這些依賴的穩定性與否對系統的影響很是大,可是依賴有不少不可控問題:如網絡鏈接緩慢,資源繁忙,暫時不可用,服務脫機等,以下圖:java

當依賴阻塞時,大多數服務器的線程池就出現阻塞,影響整個線上服務的穩定性,以下圖:面試

在複雜的分佈式架構的應用程序有不少的依賴,都會不可避免地在某些時候失敗。高併發的依賴失敗時若是沒有隔離措施,當前應用服務就有被拖垮的風險。redis

Hystrix如何解決依賴隔離

  • Hystrix使用命令模式HystrixCommand(Command)包裝依賴調用邏輯,每一個命令在單獨線程中/信號受權下執行。算法

  • 可配置依賴調用超時時間,超時時間通常設爲比99.5%平均時間略高便可。當調用超時時,直接返回或執行fallback邏輯。編程

  • 爲每一個依賴提供一個小的線程池或信號,若是線程池已滿調用將被當即拒絕,默認不採用排隊。加速失敗斷定時間。緩存

  • 依賴調用結果分:成功、失敗/拋出異常、超時、線程拒絕、短路。 請求失敗(異常,拒絕,超時,短路)時執行fallback(降級)邏輯。tomcat

  • 提供熔斷器組件,能夠自動運行或手動調用,中止當前依賴一段時間(10秒),熔斷器默認錯誤率閾值爲50%,超過將自動運行。性能優化

  • 提供近實時依賴的統計和監控。服務器

Hystrix依賴的隔離架構,以下圖:

如何使用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();  
    }  
    //調用實例 
    public static void main(String[] args) throws Exception{  
        //每一個Command對象只能調用一次,不能夠重複調用, 
        //重複調用對應異常信息
        HelloWorldCommand helloWorldCommand = new HelloWorldCommand("sync-hystrix");  
        //使用execute()同步調用代碼,效果等同於:helloWorldCommand.queue().get(); 
        String result = helloWorldCommand.execute();  
        System.out.println("result=" + result);  

        helloWorldCommand = new HelloWorldCommand("async-hystrix");  
        //異步調用,可自由控制獲取結果時機, 
        Future<String> future = helloWorldCommand.queue();  
        //get操做不能超過command定義的超時時間,默認:1秒 
        result = future.get(100, TimeUnit.MILLISECONDS);  
        System.out.println("result=" + result);  
        System.out.println("mainThread=" + Thread.currentThread().getName());  
    }       
}  
複製代碼

使用Fallback() 提供降級策略

//重載HystrixCommand的getFallback方法實現邏輯 
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 秒,調用會超時 
        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()方法拋出的異常都算做失敗,並觸發降級getFallback()和斷路器邏輯。

HystrixBadRequestException用在非法參數或非系統故障異常等不該觸發回退邏輯的場景。

依賴命名:CommandKey
public HelloWorldCommand(String name) {  
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))  
                /* HystrixCommandKey工廠定義依賴名稱 */  
                .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld")));  
        this.name = name;  
}
複製代碼

NOTE: 每一個CommandKey表明一個依賴抽象,相同的依賴要使用相同的CommandKey名稱。依賴隔離的根本就是對相同CommandKey的依賴作隔離。

依賴分組:CommandGroup

命令分組用於對依賴操做分組,便於統計,彙總等。

//使用HystrixCommandGroupKey工廠定義 
public HelloWorldCommand(String name) {  
    Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"))  
}
複製代碼

NOTE: CommandGroup是每一個命令最少配置的必選參數,在不指定ThreadPoolKey的狀況下,字面值用於對不一樣依賴的線程池/信號區分。

線程池/信號: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: 當對同一業務依賴作隔離時使用CommandGroup作區分,可是對同一依賴的不一樣遠程調用如(一個是redis 一個是http),可使用HystrixThreadPoolKey作隔離區分。

最然在業務上都是相同的組,可是須要在資源上作隔離時,可使用HystrixThreadPoolKey區分。

信號量隔離:SEMAPHORE

隔離本地代碼或可快速返回遠程調用(如memcached,redis)能夠直接使用信號量隔離,下降線程隔離開銷。

public class HelloWorldCommand extends HystrixCommand<String> {  
    private final String name;  
    public HelloWorldCommand(String name) {  
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"))  
                /* 配置信號量隔離方式,默認採用線程池隔離 */  
                .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關鍵組件分析

Hystrix流程結構解析

流程說明:

1,每次調用建立一個新的HystrixCommand,把依賴調用封裝在run()方法中

2,執行execute()/queue作同步或異步調用

3,判斷熔斷器(circuit-breaker)是否打開,若是打開跳到步驟8,進行降級策略,不然繼續後續步驟

4,判斷線程池/隊列/信號量是否跑滿,若是跑滿進入降級步驟8,不然繼續後續步驟

5,調用HystrixCommand的run方法,運行依賴邏輯

a 依賴邏輯調用超時,進入步驟8

6,判斷邏輯是否調用成功

a 返回成功調用結果

b 調用出錯,進入步驟8

7,計算熔斷器狀態,全部的運行狀態上報給熔斷器,用於統計從而判斷熔斷器狀態

8,getFallback()降級邏輯

如下四種狀況將觸發getFallback調用:

  • run()方法拋出非HystrixBadRequestException異常
  • run()方法調用超時
  • 熔斷器開啓攔截調用
  • 線程池/隊列/信號量是否跑滿

沒有實現getFallback的Command將直接拋出異常

fallback降級邏輯調用成功直接返回

降級邏輯調用失敗拋出異常

9,返回執行成功結果

熔斷器:Circuit Breaker

Circuit Breaker 流程架構和統計

每一個熔斷器默認維護10個bucket,每秒一個bucket,每一個blucket記錄成功、失敗、超時、拒絕的狀態,默認錯誤超過50%且10秒內超過20個請求進行中斷攔截.。

隔離(Isolation)分析

Hystrix隔離方式採用線程/信號的方式,經過隔離限制依賴的併發量和阻塞擴散。

(1) 線程隔離

把執行依賴代碼的線程與請求線程分離,請求線程能夠自由控制離開的時間(異步過程)。

經過線程池大小能夠控制併發量,當線程池飽和時能夠提早拒絕服務,防止依賴問題擴散。

線上建議線程池不要設置過大,不然大量堵塞線程有可能會拖慢服務器。

線程池的使用示意圖以下圖所示,當n個請求線程併發對某個接口請求調用時,會先從hystrix管理的線程池裏面得到一個線程,而後將參數傳遞給這個線程去執行真正調用。線程池的大小有限,默認是10個線程,可使用maxConcurrentRequests參數配置,若是併發請求數多於線程池線程個數,就有線程須要進入隊列排隊,但排隊隊列也有上限,默認是 5,若是排隊隊列也滿,則一定有請求線程會走fallback流程。

線程池模式能夠支持異步調用,支持超時調用,支持直接熔斷,存在線程切換,開銷大。

(2) 線程隔離的優缺點

線程隔離的優勢:

  • 使用線程能夠徹底隔離第三方代碼,請求線程能夠快速放回。

  • 當一個失敗的依賴再次變成可用時,線程池將清理,並當即恢復可用,而不是一個長時間的恢復。

  • 能夠徹底模擬異步調用,方便異步編程。

線程隔離的缺點:

  • 線程池的主要缺點是它增長了cpu,由於每一個命令的執行涉及到排隊(默認使用SynchronousQueue避免排隊),調度和上下文切換。

  • 對使用ThreadLocal等依賴線程狀態的代碼增長複雜性,須要手動傳遞和清理線程狀態。

NOTE: Netflix公司內部認爲線程隔離開銷足夠小,不會形成重大的成本或性能的影響。

Netflix內部API天天100億的HystrixCommand依賴請求使用線程隔,每一個應用大約40多個線程池,每一個線程池大約5-20個線程。

(3) 信號隔離

信號隔離也能夠用於限制併發訪問,防止阻塞擴散, 與線程隔離最大不一樣在於執行依賴代碼的線程依然是請求線程(該線程須要經過信號申請)。

若是客戶端是可信的且能夠快速返回,可使用信號隔離替換線程隔離,下降開銷。

線程隔離與信號隔離區別以下圖:

信號量的使用示意圖以下圖所示,當n個併發請求去調用一個目標服務接口時,都要獲取一個信號量才能真正去調用目標服務接口,但信號量有限,默認是10個,可使用maxConcurrentRequests參數配置,若是併發請求數多於信號量個數,就有線程須要進入隊列排隊,但排隊隊列也有上限,默認是 5,若是排隊隊列也滿,則一定有請求線程會走fallback流程,從而達到限流和防止雪崩的目的。

信號量模式從始至終都只有請求線程自身,是同步調用模式,不支持超時調用,不支持直接熔斷,因爲沒有線程的切換,開銷很是小。

(4) 總結

當請求的服務網絡開銷比較大的時候,或者是請求比較耗時的時候,咱們最好是使用線程隔離策略,這樣的話,能夠保證大量的容器(tomcat)線程可用,不會因爲服務緣由,一直處於阻塞或等待狀態,快速失敗返回。而當咱們請求緩存這些服務的時候,咱們可使用信號量隔離策略,由於這類服務的返回一般會很是的快,不會佔用容器線程太長時間,並且也減小了線程切換的一些開銷,提升了緩存服務的效率。

  • 線程池:適合絕大多數的場景,99%的。對依賴服務的網絡請求的調用和訪問,timeout這種問題

  • 信號量:適合你的訪問不是對外部依賴的訪問,而是對內部的一些比較複雜的業務邏輯的訪問,可是像這種訪問,系統內部的代碼,其實不涉及任何的網絡請求,那麼只要作信號量的普通限流就能夠了,由於不須要去捕獲timeout相似的問題,算法+數據結構的效率不是過高,併發量忽然過高,由於這裏稍微耗時一些,致使不少線程卡在這裏的話,不太好,因此進行一個基本的資源隔離和訪問,避免內部複雜的低效率的代碼,致使大量的線程被hang住

Reference:

blog.51cto.com/developeryc…

對 JAVA 開發有興趣的朋友歡迎加入QQ羣:833145934 裏面資深架構師會分享一些整理好的錄製視頻錄像和BATJ面試題:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多。

共同探討!

原文出處:www.linkedkeeper.com/1157.html

相關文章
相關標籤/搜索