Hystrix技術解析

weixin_34321977發表於2017-08-29

一、認識Hystrix

Hystrix是Netflix開源的一款容錯框架,包含常用的容錯方法:執行緒池隔離、訊號量隔離、熔斷、降級回退。在高併發訪問下,系統所依賴的服務的穩定性對系統的影響非常大,依賴有很多不可控的因素,比如網路連線變慢,資源突然繁忙,暫時不可用,服務離線等。我們要構建穩定、可靠的分散式系統,就必須要有這樣一套容錯方法。
本文將逐一分析執行緒池隔離、訊號量隔離、熔斷、降級回退這四種技術的原理與實踐。

二、執行緒隔離

2.1為什麼要做執行緒隔離

比如我們現在有3個業務呼叫分別是查詢訂單、查詢商品、查詢使用者,且這三個業務請求都是依賴第三方服務-訂單服務、商品服務、使用者服務。三個服務均是通過RPC呼叫。當查詢訂單服務,假如執行緒阻塞了,這個時候後續有大量的查詢訂單請求過來,那麼容器中的執行緒數量則會持續增加直致CPU資源耗盡到100%,整個服務對外不可用,叢集環境下就是雪崩。如下圖


4098122-2426748fc3bd1a54.png
訂單服務不可用.png

4098122-499c05814f28f1a5.png
整個tomcat容器不可用.png
2.2、執行緒隔離-執行緒池
2.2.1、Hystrix是如何通過執行緒池實現執行緒隔離的

Hystrix通過命令模式,將每個型別的業務請求封裝成對應的命令請求,比如查詢訂單->訂單Command,查詢商品->商品Command,查詢使用者->使用者Command。每個型別的Command對應一個執行緒池。建立好的執行緒池是被放入到ConcurrentHashMap中,比如查詢訂單:

final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();
threadPools.put(“hystrix-order”, new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));

當第二次查詢訂單請求過來的時候,則可以直接從Map中獲取該執行緒池。具體流程如下圖:

4098122-51f24903f15ae4e2.png
hystrix執行緒執行過程和非同步化.png

建立執行緒池中的執行緒的方法,檢視原始碼如下:

public ThreadPoolExecutor getThreadPool(final HystrixThreadPoolKey threadPoolKey, HystrixProperty<Integer> corePoolSize, HystrixProperty<Integer> maximumPoolSize, HystrixProperty<Integer> keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    ThreadFactory threadFactory = null;
    if (!PlatformSpecific.isAppEngineStandardEnvironment()) {
        threadFactory = new ThreadFactory() {
            protected final AtomicInteger threadNumber = new AtomicInteger(0);

            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r, "hystrix-" + threadPoolKey.name() + "-" + threadNumber.incrementAndGet());
                thread.setDaemon(true);
                return thread;
            }

        };
    } else {
        threadFactory = PlatformSpecific.getAppEngineThreadFactory();
    }

    final int dynamicCoreSize = corePoolSize.get();
    final int dynamicMaximumSize = maximumPoolSize.get();

    if (dynamicCoreSize > dynamicMaximumSize) {
        logger.error("Hystrix ThreadPool configuration at startup for : " + threadPoolKey.name() + " is trying to set coreSize = " +
                dynamicCoreSize + " and maximumSize = " + dynamicMaximumSize + ".  Maximum size will be set to " +
                dynamicCoreSize + ", the coreSize value, since it must be equal to or greater than the coreSize value");
        return new ThreadPoolExecutor(dynamicCoreSize, dynamicCoreSize, keepAliveTime.get(), unit, workQueue, threadFactory);
    } else {
        return new ThreadPoolExecutor(dynamicCoreSize, dynamicMaximumSize, keepAliveTime.get(), unit, workQueue, threadFactory);
    }
}

執行Command的方式一共四種,直接看官方文件(https://github.com/Netflix/Hystrix/wiki/How-it-Works),具體區別如下:

  • execute():以同步堵塞方式執行run()。呼叫execute()後,hystrix先建立一個新執行緒執行run(),接著呼叫程式要在execute()呼叫處一直堵塞著,直到run()執行完成。

  • queue():以非同步非堵塞方式執行run()。呼叫queue()就直接返回一個Future物件,同時hystrix建立一個新執行緒執行run(),呼叫程式通過Future.get()拿到run()的返回結果,而Future.get()是堵塞執行的。

  • observe():事件註冊前執行run()/construct()。第一步是事件註冊前,先呼叫observe()自動觸發執行run()/construct()(如果繼承的是HystrixCommand,hystrix將建立新執行緒非堵塞執行run();如果繼承的是HystrixObservableCommand,將以呼叫程式執行緒堵塞執行construct()),第二步是從observe()返回後呼叫程式呼叫subscribe()完成事件註冊,如果run()/construct()執行成功則觸發onNext()和onCompleted(),如果執行異常則觸發onError()。

  • toObservable():事件註冊後執行run()/construct()。第一步是事件註冊前,呼叫toObservable()就直接返回一個Observable<String>物件,第二步呼叫subscribe()完成事件註冊後自動觸發執行run()/construct()(如果繼承的是HystrixCommand,hystrix將建立新執行緒非堵塞執行run(),呼叫程式不必等待run();如果繼承的是HystrixObservableCommand,將以呼叫程式執行緒堵塞執行construct(),呼叫程式等待construct()執行完才能繼續往下走),如果run()/construct()執行成功則觸發onNext()和onCompleted(),如果執行異常則觸發onError()
    注:
    execute()和queue()是HystrixCommand中的方法,observe()和toObservable()是HystrixObservableCommand 中的方法。從底層實現來講,HystrixCommand其實也是利用Observable實現的(如果我們看Hystrix的原始碼的話,可以發現裡面大量使用了RxJava),雖然HystrixCommand只返回單個的結果,但HystrixCommand的queue方法實際上是呼叫了toObservable().toBlocking().toFuture(),而execute方法實際上是呼叫了queue().get()。

2.2.2、如何應用到實際程式碼中
package myHystrix.threadpool;

import com.netflix.hystrix.*;
import org.junit.Test;

import java.util.List;
import java.util.concurrent.Future;

/**
 * Created by wangxindong on 2017/8/4.
 */
public class GetOrderCommand extends HystrixCommand<List> {

    OrderService orderService;

    public GetOrderCommand(String name){
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ThreadPoolTestGroup"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("testCommandKey"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(name))
                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                                .withExecutionTimeoutInMilliseconds(5000)
                )
                .andThreadPoolPropertiesDefaults(
                        HystrixThreadPoolProperties.Setter()
                                .withMaxQueueSize(10)   //配置佇列大小
                                .withCoreSize(2)    // 配置執行緒池裡的執行緒數
                )
        );
    }

    @Override
    protected List run() throws Exception {
        return orderService.getOrderList();
    }

    public static class UnitTest {
        @Test
        public void testGetOrder(){
//            new GetOrderCommand("hystrix-order").execute();
            Future<List> future =new GetOrderCommand("hystrix-order").queue();
        }

    }
}

2.2.3、執行緒隔離-執行緒池小結

執行依賴程式碼的執行緒與請求執行緒(比如Tomcat執行緒)分離,請求執行緒可以自由控制離開的時間,這也是我們通常說的非同步程式設計,Hystrix是結合RxJava來實現的非同步程式設計。通過設定執行緒池大小來控制併發訪問量,當執行緒飽和的時候可以拒絕服務,防止依賴問題擴散。

4098122-e076e5673c63696f.png
執行緒隔離.png

執行緒池隔離的優點:
[1]:應用程式會被完全保護起來,即使依賴的一個服務的執行緒池滿了,也不會影響到應用程式的其他部分。
[2]:我們給應用程式引入一個新的風險較低的客戶端lib的時候,如果發生問題,也是在本lib中,並不會影響到其他內容,因此我們可以大膽的引入新lib庫。
[3]:當依賴的一個失敗的服務恢復正常時,應用程式會立即恢復正常的效能。
[4]:如果我們的應用程式一些引數配置錯誤了,執行緒池的執行狀況將會很快顯示出來,比如延遲、超時、拒絕等。同時可以通過動態屬性實時執行來處理糾正錯誤的引數配置。
[5]:如果服務的效能有變化,從而需要調整,比如增加或者減少超時時間,更改重試次數,就可以通過執行緒池指標動態屬性修改,而且不會影響到其他呼叫請求。
[6]:除了隔離優勢外,hystrix擁有專門的執行緒池可提供內建的併發功能,使得可以在同步呼叫之上構建非同步的外觀模式,這樣就可以很方便的做非同步程式設計(Hystrix引入了Rxjava非同步框架)。

儘管執行緒池提供了執行緒隔離,我們的客戶端底層程式碼也必須要有超時設定,不能無限制的阻塞以致執行緒池一直飽和。

執行緒池隔離的缺點:
[1]:執行緒池的主要缺點就是它增加了計算的開銷,每個業務請求(被包裝成命令)在執行的時候,會涉及到請求排隊,排程和上下文切換。不過Netflix公司內部認為執行緒隔離開銷足夠小,不會產生重大的成本或效能的影響。

The Netflix API processes 10+ billion Hystrix Command executions per day using thread isolation. Each API instance has 40+ thread-pools with 5–20 threads in each (most are set to 10).
Netflix API每天使用執行緒隔離處理10億次Hystrix Command執行。 每個API例項都有40多個執行緒池,每個執行緒池中有5-20個執行緒(大多數設定為10個)。

對於不依賴網路訪問的服務,比如只依賴記憶體快取這種情況下,就不適合用執行緒池隔離技術,而是採用訊號量隔離。

2.3、執行緒隔離-訊號量。

2.3.1、執行緒池和訊號量的區別

上面談到了執行緒池的缺點,當我們依賴的服務是極低延遲的,比如訪問記憶體快取,就沒有必要使用執行緒池的方式,那樣的話開銷得不償失,而是推薦使用訊號量這種方式。下面這張圖說明了執行緒池隔離和訊號量隔離的主要區別:執行緒池方式下業務請求執行緒和執行依賴的服務的執行緒不是同一個執行緒;訊號量方式下業務請求執行緒和執行依賴服務的執行緒是同一個執行緒

4098122-ced571958d393bfc.png
訊號量和執行緒池的區別.png

2.3.2、如何使用訊號量來隔離執行緒

將屬性execution.isolation.strategy設定為SEMAPHORE ,象這樣 ExecutionIsolationStrategy.SEMAPHORE,則Hystrix使用訊號量而不是預設的執行緒池來做隔離。

public class CommandUsingSemaphoreIsolation extends HystrixCommand<String> {

    private final int id;

    public CommandUsingSemaphoreIsolation(int id) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
                // since we're doing work in the run() method that doesn't involve network traffic
                // and executes very fast with low risk we choose SEMAPHORE isolation
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)));
        this.id = id;
    }

    @Override
    protected String run() {
        // a real implementation would retrieve data from in memory data structure
        // or some other similar non-network involved work
        return "ValueFromHashMap_" + id;
    }

}

2.3.4、執行緒隔離-訊號量小結

訊號量隔離的方式是限制了總的併發數,每一次請求過來,請求執行緒和呼叫依賴服務的執行緒是同一個執行緒,那麼如果不涉及遠端RPC呼叫(沒有網路開銷)則使用訊號量來隔離,更為輕量,開銷更小。

三、熔斷

3.1、熔斷器(Circuit Breaker)介紹

熔斷器,現實生活中有一個很好的類比,就是家庭電路中都會安裝一個保險盒,當電流過大的時候保險盒裡面的保險絲會自動斷掉,來保護家裡的各種電器及電路。Hystrix中的熔斷器(Circuit Breaker)也是起到這樣的作用,Hystrix在執行過程中會向每個commandKey對應的熔斷器報告成功、失敗、超時和拒絕的狀態,熔斷器維護計算統計的資料,根據這些統計的資訊來確定熔斷器是否開啟。如果開啟,後續的請求都會被截斷。然後會隔一段時間預設是5s,嘗試半開,放入一部分流量請求進來,相當於對依賴服務進行一次健康檢查,如果恢復,熔斷器關閉,隨後完全恢復呼叫。如下圖:

4098122-55290c8ca6729751.png
熔斷器開關圖.png

說明,上面說的commandKey,就是在初始化的時候設定的andCommandKey(HystrixCommandKey.Factory.asKey("testCommandKey"))

再來看下熔斷器在整個Hystrix流程圖中的位置,從步驟4開始,如下圖:

4098122-82011c1cd406f16f.png
Hystrix流程圖.png

Hystrix會檢查Circuit Breaker的狀態。如果Circuit Breaker的狀態為開啟狀態,Hystrix將不會執行對應指令,而是直接進入失敗處理狀態(圖中8 Fallback)。如果Circuit Breaker的狀態為關閉狀態,Hystrix會繼續進行執行緒池、任務佇列、訊號量的檢查(圖中5)

3.2、如何使用熔斷器(Circuit Breaker)

由於Hystrix是一個容錯框架,因此我們在使用的時候,要達到熔斷的目的只需配置一些引數就可以了。但我們要達到真正的效果,就必須要了解這些引數。Circuit Breaker一共包括如下6個引數。
1、circuitBreaker.enabled
是否啟用熔斷器,預設是TURE。
2、circuitBreaker.forceOpen
熔斷器強制開啟,始終保持開啟狀態。預設值FLASE。
3、circuitBreaker.forceClosed
熔斷器強制關閉,始終保持關閉狀態。預設值FLASE。
4、circuitBreaker.errorThresholdPercentage
設定錯誤百分比,預設值50%,例如一段時間(10s)內有100個請求,其中有55個超時或者異常返回了,那麼這段時間內的錯誤百分比是55%,大於了預設值50%,這種情況下觸發熔斷器-開啟。
5、circuitBreaker.requestVolumeThreshold
預設值20.意思是至少有20個請求才進行errorThresholdPercentage錯誤百分比計算。比如一段時間(10s)內有19個請求全部失敗了。錯誤百分比是100%,但熔斷器不會開啟,因為requestVolumeThreshold的值是20. 這個引數非常重要,熔斷器是否開啟首先要滿足這個條件,原始碼如下

// check if we are past the statisticalWindowVolumeThreshold
if (health.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
    // we are not past the minimum volume threshold for the statisticalWindow so we'll return false immediately and not calculate anything
    return false;
}

if (health.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
    return false;
}

6、circuitBreaker.sleepWindowInMilliseconds
半開試探休眠時間,預設值5000ms。當熔斷器開啟一段時間之後比如5000ms,會嘗試放過去一部分流量進行試探,確定依賴服務是否恢復。

測試程式碼(模擬10次呼叫,錯誤百分比為5%的情況下,開啟熔斷器開關。):

package myHystrix.threadpool;

import com.netflix.hystrix.*;
import org.junit.Test;

import java.util.Random;

/**
 * Created by wangxindong on 2017/8/15.
 */
public class GetOrderCircuitBreakerCommand extends HystrixCommand<String> {

    public GetOrderCircuitBreakerCommand(String name){
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ThreadPoolTestGroup"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("testCommandKey"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(name))
                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                                .withCircuitBreakerEnabled(true)//預設是true,本例中為了展現該引數
                                .withCircuitBreakerForceOpen(false)//預設是false,本例中為了展現該引數
                                .withCircuitBreakerForceClosed(false)//預設是false,本例中為了展現該引數
                                .withCircuitBreakerErrorThresholdPercentage(5)//(1)錯誤百分比超過5%
                                .withCircuitBreakerRequestVolumeThreshold(10)//(2)10s以內呼叫次數10次,同時滿足(1)(2)熔斷器開啟
                                .withCircuitBreakerSleepWindowInMilliseconds(5000)//隔5s之後,熔斷器會嘗試半開(關閉),重新放進來請求
//                                .withExecutionTimeoutInMilliseconds(1000)
                )
                .andThreadPoolPropertiesDefaults(
                        HystrixThreadPoolProperties.Setter()
                                .withMaxQueueSize(10)   //配置佇列大小
                                .withCoreSize(2)    // 配置執行緒池裡的執行緒數
                )
        );
    }

    @Override
    protected String run() throws Exception {
        Random rand = new Random();
        //模擬錯誤百分比(方式比較粗魯但可以證明問題)
        if(1==rand.nextInt(2)){
//            System.out.println("make exception");
            throw new Exception("make exception");
        }
        return "running:  ";
    }

    @Override
    protected String getFallback() {
//        System.out.println("FAILBACK");
        return "fallback: ";
    }

    public static class UnitTest{

        @Test
        public void testCircuitBreaker() throws Exception{
            for(int i=0;i<25;i++){
                Thread.sleep(500);
                HystrixCommand<String> command = new GetOrderCircuitBreakerCommand("testCircuitBreaker");
                String result = command.execute();
                //本例子中從第11次,熔斷器開始開啟
                System.out.println("call times:"+(i+1)+"   result:"+result +" isCircuitBreakerOpen: "+command.isCircuitBreakerOpen());
                //本例子中5s以後,熔斷器嘗試關閉,放開新的請求進來
            }
        }
    }
}

測試結果:

call times:1 result:fallback: isCircuitBreakerOpen: false
call times:2 result:running: isCircuitBreakerOpen: false
call times:3 result:running: isCircuitBreakerOpen: false
call times:4 result:fallback: isCircuitBreakerOpen: false
call times:5 result:running: isCircuitBreakerOpen: false
call times:6 result:fallback: isCircuitBreakerOpen: false
call times:7 result:fallback: isCircuitBreakerOpen: false
call times:8 result:fallback: isCircuitBreakerOpen: false
call times:9 result:fallback: isCircuitBreakerOpen: false
call times:10 result:fallback: isCircuitBreakerOpen: false
熔斷器開啟
call times:11 result:fallback: isCircuitBreakerOpen: true
call times:12 result:fallback: isCircuitBreakerOpen: true
call times:13 result:fallback: isCircuitBreakerOpen: true
call times:14 result:fallback: isCircuitBreakerOpen: true
call times:15 result:fallback: isCircuitBreakerOpen: true
call times:16 result:fallback: isCircuitBreakerOpen: true
call times:17 result:fallback: isCircuitBreakerOpen: true
call times:18 result:fallback: isCircuitBreakerOpen: true
call times:19 result:fallback: isCircuitBreakerOpen: true
call times:20 result:fallback: isCircuitBreakerOpen: true
5s後熔斷器關閉
call times:21 result:running: isCircuitBreakerOpen: false
call times:22 result:running: isCircuitBreakerOpen: false
call times:23 result:fallback: isCircuitBreakerOpen: false
call times:24 result:running: isCircuitBreakerOpen: false
call times:25 result:running: isCircuitBreakerOpen: false

3.3、熔斷器(Circuit Breaker)原始碼HystrixCircuitBreaker.java分析
4098122-7abdfbc3c1f9ac2c.png
HystrixCircuitBreaker.java.png

Factory 是一個工廠類,提供HystrixCircuitBreaker例項

public static class Factory {
        //用一個ConcurrentHashMap來儲存HystrixCircuitBreaker物件
        private static ConcurrentHashMap<String, HystrixCircuitBreaker> circuitBreakersByCommand = new ConcurrentHashMap<String, HystrixCircuitBreaker>();
        
//Hystrix首先會檢查ConcurrentHashMap中有沒有對應的快取的斷路器,如果有的話直接返回。如果沒有的話就會新建立一個HystrixCircuitBreaker例項,將其新增到快取中並且返回
        public static HystrixCircuitBreaker getInstance(HystrixCommandKey key, HystrixCommandGroupKey group, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
            
            HystrixCircuitBreaker previouslyCached = circuitBreakersByCommand.get(key.name());
            if (previouslyCached != null) {
                return previouslyCached;
            }

            
            HystrixCircuitBreaker cbForCommand = circuitBreakersByCommand.putIfAbsent(key.name(), new HystrixCircuitBreakerImpl(key, group, properties, metrics));
            if (cbForCommand == null) {
                return circuitBreakersByCommand.get(key.name());
            } else {
                return cbForCommand;
            }
        }

        
        public static HystrixCircuitBreaker getInstance(HystrixCommandKey key) {
            return circuitBreakersByCommand.get(key.name());
        }

        static void reset() {
            circuitBreakersByCommand.clear();
        }
}

HystrixCircuitBreakerImpl是HystrixCircuitBreaker的實現,allowRequest()、isOpen()、markSuccess()都會在HystrixCircuitBreakerImpl有預設的實現。

static class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {
        private final HystrixCommandProperties properties;
        private final HystrixCommandMetrics metrics;

        /* 變數circuitOpen來代表斷路器的狀態,預設是關閉 */
        private AtomicBoolean circuitOpen = new AtomicBoolean(false);

        /* 變數circuitOpenedOrLastTestedTime記錄著斷路恢復計時器的初始時間,用於Open狀態向Close狀態的轉換 */
        private AtomicLong circuitOpenedOrLastTestedTime = new AtomicLong();

        protected HystrixCircuitBreakerImpl(HystrixCommandKey key, HystrixCommandGroupKey commandGroup, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
            this.properties = properties;
            this.metrics = metrics;
        }

        /*用於關閉熔斷器並重置統計資料*/
        public void markSuccess() {
            if (circuitOpen.get()) {
                if (circuitOpen.compareAndSet(true, false)) {
                    //win the thread race to reset metrics
                    //Unsubscribe from the current stream to reset the health counts stream.  This only affects the health counts view,
                    //and all other metric consumers are unaffected by the reset
                    metrics.resetStream();
                }
            }
        }

        @Override
        public boolean allowRequest() {
            //是否設定強制開啟
            if (properties.circuitBreakerForceOpen().get()) {
                return false;
            }
            if (properties.circuitBreakerForceClosed().get()) {//是否設定強制關閉
                isOpen();
                // properties have asked us to ignore errors so we will ignore the results of isOpen and just allow all traffic through
                return true;
            }
            return !isOpen() || allowSingleTest();
        }

        public boolean allowSingleTest() {
            long timeCircuitOpenedOrWasLastTested = circuitOpenedOrLastTestedTime.get();
            //獲取熔斷恢復計時器記錄的初始時間circuitOpenedOrLastTestedTime,然後判斷以下兩個條件是否同時滿足:
            // 1) 熔斷器的狀態為開啟狀態(circuitOpen.get() == true)
            // 2) 當前時間與計時器初始時間之差大於計時器閾值circuitBreakerSleepWindowInMilliseconds(預設為 5 秒)
            //如果同時滿足的話,表示可以從Open狀態向Close狀態轉換。Hystrix會通過CAS操作將circuitOpenedOrLastTestedTime設為當前時間,並返回true。如果不同時滿足,返回false,代表熔斷器關閉或者計時器時間未到。
            if (circuitOpen.get() && System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested + properties.circuitBreakerSleepWindowInMilliseconds().get()) {
                // We push the 'circuitOpenedTime' ahead by 'sleepWindow' since we have allowed one request to try.
                // If it succeeds the circuit will be closed, otherwise another singleTest will be allowed at the end of the 'sleepWindow'.
                if (circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis())) {
                    // if this returns true that means we set the time so we'll return true to allow the singleTest
                    // if it returned false it means another thread raced us and allowed the singleTest before we did
                    return true;
                }
            }
            return false;
        }

        @Override
        public boolean isOpen() {
            if (circuitOpen.get()) {//獲取斷路器的狀態
                // if we're open we immediately return true and don't bother attempting to 'close' ourself as that is left to allowSingleTest and a subsequent successful test to close
                return true;
            }

            // Metrics資料中獲取HealthCounts物件
            HealthCounts health = metrics.getHealthCounts();

            // 檢查對應的請求總數(totalCount)是否小於屬性中的請求容量閾值circuitBreakerRequestVolumeThreshold,預設20,如果是的話表示熔斷器可以保持關閉狀態,返回false
            if (health.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
                
                return false;
            }

            //不滿足請求總數條件,就再檢查錯誤比率(errorPercentage)是否小於屬性中的錯誤百分比閾值(circuitBreakerErrorThresholdPercentage,預設 50),如果是的話表示斷路器可以保持關閉狀態,返回 false
            if (health.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
                return false;
            } else {
                // 如果超過閾值,Hystrix會判定服務的某些地方出現了問題,因此通過CAS操作將斷路器設為開啟狀態,並記錄此時的系統時間作為定時器初始時間,最後返回 true
                if (circuitOpen.compareAndSet(false, true)) {
                    circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());
                    return true;
                } else {
                    return true;
                }
            }
        }

    }
3.4、熔斷器小結

每個熔斷器預設維護10個bucket,每秒一個bucket,每個blucket記錄成功,失敗,超時,拒絕的狀態,預設錯誤超過50%且10秒內超過20個請求進行中斷攔截。下圖顯示HystrixCommand或HystrixObservableCommand如何與HystrixCircuitBreaker及其邏輯和決策流程進行互動,包括計數器在斷路器中的行為。

四、回退降級

4.1、降級

所謂降級,就是指在在Hystrix執行非核心鏈路功能失敗的情況下,我們如何處理,比如我們返回預設值等。如果我們要回退或者降級處理,程式碼上需要實現HystrixCommand.getFallback()方法或者是HystrixObservableCommand. HystrixObservableCommand()。

public class CommandHelloFailure extends HystrixCommand<String> {

    private final String name;

    public CommandHelloFailure(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
        this.name = name;
    }

    @Override
    protected String run() {
        throw new RuntimeException("this command always fails");
    }

    @Override
    protected String getFallback() {
        return "Hello Failure " + name + "!";
    }
}
4.2、Hystrix的降級回退方式

Hystrix一共有如下幾種降級回退模式:

4.2.1、Fail Fast 快速失敗
 @Override
    protected String run() {
        if (throwException) {
            throw new RuntimeException("failure from CommandThatFailsFast");
        } else {
            return "success";
        }
    }

如果我們實現的是HystrixObservableCommand.java則 重寫 resumeWithFallback方法

@Override
    protected Observable<String> resumeWithFallback() {
        if (throwException) {
            return Observable.error(new Throwable("failure from CommandThatFailsFast"));
        } else {
            return Observable.just("success");
        }
    }
4.2.2、Fail Silent 無聲失敗

返回null,空Map,空List

4098122-7f5fd45f200d1927.png
fail silent.png
@Override
    protected String getFallback() {
        return null;
    }
@Override
    protected List<String> getFallback() {
        return Collections.emptyList();
    }
@Override
    protected Observable<String> resumeWithFallback() {
        return Observable.empty();
    }
4.2.3、Fallback: Static 返回預設值

回退的時候返回靜態嵌入程式碼中的預設值,這樣就不會導致功能以Fail Silent的方式被清楚,也就是使用者看不到任何功能了。而是按照一個預設的方式顯示。

@Override
    protected Boolean getFallback() {
        return true;
    }
@Override
    protected Observable<Boolean> resumeWithFallback() {
        return Observable.just( true );
    }
4.2.4、Fallback: Stubbed 自己組裝一個值返回

當我們執行返回的結果是一個包含多個欄位的物件時,則會以Stubbed 的方式回退。Stubbed 值我們建議在例項化Command的時候就設定好一個值。以countryCodeFromGeoLookup為例,countryCodeFromGeoLookup的值,是在我們呼叫的時候就註冊進來初始化好的。CommandWithStubbedFallback command = new CommandWithStubbedFallback(1234, "china");主要程式碼如下:

public class CommandWithStubbedFallback extends HystrixCommand<UserAccount> {

protected CommandWithStubbedFallback(int customerId, String countryCodeFromGeoLookup) {
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
        this.customerId = customerId;
        this.countryCodeFromGeoLookup = countryCodeFromGeoLookup;
    }
    @Override
    protected UserAccount getFallback() {
        /**
         * Return stubbed fallback with some static defaults, placeholders,
         * and an injected value 'countryCodeFromGeoLookup' that we'll use
         * instead of what we would have retrieved from the remote service.
         */
        return new UserAccount(customerId, "Unknown Name",
                countryCodeFromGeoLookup, true, true, false);
    }
4.2.5、Fallback: Cache via Network 利用遠端快取

通過遠端快取的方式。在失敗的情況下再發起一次remote請求,不過這次請求的是一個快取比如redis。由於是又發起一起遠端呼叫,所以會重新封裝一次Command,這個時候要注意,執行fallback的執行緒一定要跟主執行緒區分開,也就是重新命名一個ThreadPoolKey。

4098122-7aae5890aa8eb903.png
Cache via Network.png
public class CommandWithFallbackViaNetwork extends HystrixCommand<String> {
    private final int id;

    protected CommandWithFallbackViaNetwork(int id) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceX"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("GetValueCommand")));
        this.id = id;
    }

    @Override
    protected String run() {
        //        RemoteServiceXClient.getValue(id);
        throw new RuntimeException("force failure for example");
    }

    @Override
    protected String getFallback() {
        return new FallbackViaNetwork(id).execute();
    }

    private static class FallbackViaNetwork extends HystrixCommand<String> {
        private final int id;

        public FallbackViaNetwork(int id) {
            super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceX"))
                    .andCommandKey(HystrixCommandKey.Factory.asKey("GetValueFallbackCommand"))
                    // use a different threadpool for the fallback command
                    // so saturating the RemoteServiceX pool won't prevent
                    // fallbacks from executing
                    .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("RemoteServiceXFallback")));
            this.id = id;
        }

        @Override
        protected String run() {
            MemCacheClient.getValue(id);
        }

        @Override
        protected String getFallback() {
            // the fallback also failed
            // so this fallback-of-a-fallback will 
            // fail silently and return null
            return null;
        }
    }
}
4.2.6、Primary + Secondary with Fallback 主次方式回退(主要和次要)

這個有點類似我們日常開發中需要上線一個新功能,但為了防止新功能上線失敗可以回退到老的程式碼,我們會做一個開關比如使用zookeeper做一個配置開關,可以動態切換到老程式碼功能。那麼Hystrix它是使用通過一個配置來在兩個command中進行切換。

4098122-cdd2f967f2bbaf7b.png
Primary + Secondary with Fallback.png
/**
 * Sample {@link HystrixCommand} pattern using a semaphore-isolated command
 * that conditionally invokes thread-isolated commands.
 */
public class CommandFacadeWithPrimarySecondary extends HystrixCommand<String> {

    private final static DynamicBooleanProperty usePrimary = DynamicPropertyFactory.getInstance().getBooleanProperty("primarySecondary.usePrimary", true);

    private final int id;

    public CommandFacadeWithPrimarySecondary(int id) {
        super(Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey("SystemX"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("PrimarySecondaryCommand"))
                .andCommandPropertiesDefaults(
                        // we want to default to semaphore-isolation since this wraps
                        // 2 others commands that are already thread isolated
                        // 採用訊號量的隔離方式
                        HystrixCommandProperties.Setter()
                                .withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)));
        this.id = id;
    }

    //通過DynamicPropertyFactory來路由到不同的command
    @Override
    protected String run() {
        if (usePrimary.get()) {
            return new PrimaryCommand(id).execute();
        } else {
            return new SecondaryCommand(id).execute();
        }
    }

    @Override
    protected String getFallback() {
        return "static-fallback-" + id;
    }

    @Override
    protected String getCacheKey() {
        return String.valueOf(id);
    }

    private static class PrimaryCommand extends HystrixCommand<String> {

        private final int id;

        private PrimaryCommand(int id) {
            super(Setter
                    .withGroupKey(HystrixCommandGroupKey.Factory.asKey("SystemX"))
                    .andCommandKey(HystrixCommandKey.Factory.asKey("PrimaryCommand"))
                    .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("PrimaryCommand"))
                    .andCommandPropertiesDefaults(
                            // we default to a 600ms timeout for primary
                            HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(600)));
            this.id = id;
        }

        @Override
        protected String run() {
            // perform expensive 'primary' service call
            return "responseFromPrimary-" + id;
        }

    }

    private static class SecondaryCommand extends HystrixCommand<String> {

        private final int id;

        private SecondaryCommand(int id) {
            super(Setter
                    .withGroupKey(HystrixCommandGroupKey.Factory.asKey("SystemX"))
                    .andCommandKey(HystrixCommandKey.Factory.asKey("SecondaryCommand"))
                    .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("SecondaryCommand"))
                    .andCommandPropertiesDefaults(
                            // we default to a 100ms timeout for secondary
                            HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(100)));
            this.id = id;
        }

        @Override
        protected String run() {
            // perform fast 'secondary' service call
            return "responseFromSecondary-" + id;
        }

    }

    public static class UnitTest {

        @Test
        public void testPrimary() {
            HystrixRequestContext context = HystrixRequestContext.initializeContext();
            try {
                //將屬性"primarySecondary.usePrimary"設定為true,則走PrimaryCommand;設定為false,則走SecondaryCommand
                ConfigurationManager.getConfigInstance().setProperty("primarySecondary.usePrimary", true);
                assertEquals("responseFromPrimary-20", new CommandFacadeWithPrimarySecondary(20).execute());
            } finally {
                context.shutdown();
                ConfigurationManager.getConfigInstance().clear();
            }
        }

        @Test
        public void testSecondary() {
            HystrixRequestContext context = HystrixRequestContext.initializeContext();
            try {
                //將屬性"primarySecondary.usePrimary"設定為true,則走PrimaryCommand;設定為false,則走SecondaryCommand
                ConfigurationManager.getConfigInstance().setProperty("primarySecondary.usePrimary", false);
                assertEquals("responseFromSecondary-20", new CommandFacadeWithPrimarySecondary(20).execute());
            } finally {
                context.shutdown();
                ConfigurationManager.getConfigInstance().clear();
            }
        }
    }
}
4.3、回退降級小結

降級的處理方式,返回預設值,返回快取裡面的值(包括遠端快取比如redis和本地快取比如jvmcache)。
但回退的處理方式也有不適合的場景:
1、寫操作
2、批處理
3、計算
以上幾種情況如果失敗,則程式就要將錯誤返回給呼叫者。

總結

Hystrix為我們提供了一套線上系統容錯的技術實踐方法,我們通過在系統中引入Hystrix的jar包可以很方便的使用執行緒隔離、熔斷、回退等技術。同時它還提供了監控頁面配置,方便我們管理檢視每個介面的呼叫情況。像spring cloud這種微服務構建模式中也引入了Hystrix,我們可以放心使用Hystrix的執行緒隔離技術,來防止雪崩這種可怕的致命性線上故障。

轉載請註明出處,並附上鍊接 http://www.jianshu.com/p/3e11ac385c73

參考資料:
https://github.com/Netflix/Hystrix/wiki
《億級流量網站架構核心技術》一書

相關文章