HystrixCircuitBreaker詳解

一、熔斷器(Circuit Breaker)介紹

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


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

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

 

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

二、如何使用熔斷器(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

三、熔斷器(Circuit Breaker)源代碼HystrixCircuitBreaker.java分析

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;
                }
            }
        }

    }

四、總結

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章