服務保障框架-Hystrix快速入門

祝大家國慶快樂! 對大部分電商和快遞公司來說,每年年底(Q4季度)由於雙11等大促活動的存在,將面對大量的用戶流量,尤其是屬於大促的那幾天,無論是用戶的商品訂單還是物流訂單,都將是平時的3倍以上。對於技術人員來說,提前落地相應的服務保障體系,並進行相應的壓測和演習,是題中應有之意。整個保障體系的實現涉及的環節很多,本文將選取奈飛Netflix公司的Hystrix"豪豬"框架(其基於Java語言和最近比較流行RxJava流式框架),針對分佈式應用的服務保障問題進行探討,之後將按照基本知識、應用實踐、配置知識和源碼分析的順序進行介紹,不足之處望不吝賜教。

首先通過一張思維導圖來展示本文的思路,有標記部分的推薦程度高,圖可以拖到瀏覽器新窗口放大。經過大半年的調整,手還是比較生,我熊二哥又回來了!:)

基本知識

爲了便於理解本文的意圖,首先提出並解答兩個問題。

1.爲什麼需要在項目中引入Hystrix,其可以應用在什麼場景中?

在分佈式系統中,單個應用通常會有多個不同類型的外部依賴服務,內部通常依賴於各種RPC服務,外部則依賴於各種HTTP服務。這些依賴服務不可避免的會出現調用失敗,比如超時、異常等情況,如何在外部依賴出問題的情況,仍然保證自身應用的穩定,就是Hystrix這類服務保障框架的工作了。常見的服務依賴如下圖所示,應用X依賴於服務A、B和C,A和B正常提供服務,C服務出錯,這是如何避免C服務對A、B服務產生影響,也引出了一個隔離的概念。

舉個例子來說,某個應用中依賴了30個外部服務,實際應用中通常比這還要多,假設每個服務的可用性爲99.9%,3個9的可用性,算是不錯了,但99.9%的30次冪≈ 97.0%,這個可用性已經是無法容忍的了。

2.Hytrix的目標是什麼,其採用了什麼手段來達到該目標?

Hystrix的目標就是能夠在1個或多個依賴出現問題時,系統依然可以穩定的運行,其手段包括隔離、限流和降級有等,接下來詳細介紹這些手段。補充一點,張開濤老師曾對系統高可用手段進行過總結,除了以上的限流、隔離和降級,還有負載均衡、超時與重試、回滾、壓測與預案,共7種手段。

隔離

隔離說到底還是分治思想的體現,在當前場景中,就是將不同的外部依賴進行分類,確定其邊界,然後隔離開來分開進行管理。Hystrix支持的隔離策略isolationStrategy包括信號量和線程池兩種,具體內容將在之後限流知識中介紹。

限流

在基於服務化(包括SOA和微服務)的系統架構中,對服務請求進行限流是保護服務穩定性的一個常見手段。此外,關於限流有兩個比較重要的概念:限流算法,包括計數限流、令牌桶和漏桶等;限流粒度,包括方法級別、接口級別、應用級別、集羣級別等。對於Hystrix來說,其採用了自己的一套限流方式,這裏首先延續之前隔離知識中提到的信號量和線程池概念進行介紹。

信號量概念比較簡單,常用於獲取共享資源的場景中,比如計算機連接了兩個打印機,那麼初始的信號量就是2,被某個進程或線程獲取後減1,信號量爲0後,需要獲取的線程或進程進入資源等待狀態。Hystrix的處理有些不同,其不等待,直接返回失敗。

線程池採用的就是jdk的線程池,其默認選用不使用阻塞隊列的線程池,例如線程池大小爲10,如果某時刻10個線程均被使用,那麼新的請求將不會進入等待隊列,而是直接返回失敗,起到限流的作用。

此外,其還引入了一個斷路器機制,當斷路器處於打開狀態時,直接返回失敗或進入降級流程。斷路器打開和關閉的觸發流程爲:當總的請求數達到可閾值HystrixCommandProperties.circuitBreakerRequestVolumeThreshold(),或總的請求失敗百分比達到了閾值HystrixCommandProperties.circuitBreakerErrorThresholdPercentage(),這時將斷路器的狀態由關閉設置爲打開。當斷路器打開時,所有的請求均被短路,在經過指定休眠時間窗口後,讓下一個請求通過(斷路器被認爲是半開狀態)。如果請求失敗,斷路器進入打開狀態,並進入新的休眠窗口;否則進入關閉狀態。

斷路器依賴的統計信息如下圖所示,默認情況下10s爲一個統計週期,10個滾動窗口,每個負責統計1s內的數據,包括請求成功、失敗、超時和拒絕次數。

降級

這裏的降級具體來說就是服務質量的降級,需要注意的是,只有方法所屬的業務場景適合降級時才採用,一般爲查詢場景。Hystrix通過配置fallbackMethod指定降級時的處理方法,觸發降級動作的4種情況如下所示。

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

Hystrix整體的處理流程

主題流程如圖所示,Hystrix框架通過命令模式來實現方法粒度上的服務保障,主要涉及HystrixCommandHystrixObservableCommand類,前者提供同步的execute和異步的queue方法,後者提供立即執行observe和延遲執行toObservable的回調方法。此外,實際項目中通常不會使用Hystrix集成的本地緩存。

tip: 目前在服務保障方面,除了hystrix框架外,阿里巴巴公司開源的sentinel框架也是一個不錯的可選方案。

應用實踐

該節將從基礎應用、項目應用、動態配置和監控等幾個方面進行介紹。Hystrix基礎應用比較簡單,包括直接編碼和使用註解等兩種方式,一般選用註解方式,其基於javanica子包,hystrix-javanica官網,之後簡要展示Hystrix如何在基於gradle依賴管理的Springboot應用中集成。

基礎應用

1.Gradle配置和SpringBoot配置

//Gradle中添加Hystrix核心
compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix')

@Configuration
public class HystrixConfiguration {
  @Bean
  public HystrixCommandAspect hystrixAspect() {
    return new HystrixCommandAspect();
  }
}

2.同步使用方式(異步和響應式可以參考javanica的wiki頁面)

@DefaultProperties(groupKey = "UserQueryGroup", threadPoolProperties = {
        @HystrixProperty(name = "coreSize", value = "20"),
        @HystrixProperty(name = "maxQueueSize", value = "20")
})
@Service
public class UserQueryManager {
    @HystrixCommand(commandKey = "GetUserCommand",fallbackMethod = "getUserFallback")
    public String getUser(long id) {
        return "test_user";
    }
    
    public String getUserFallback(long id) {
        return "test_user_fallback";
    }
}

項目應用

新建項目直接在涉及外部依賴服務的方法上加上相應註解即可,比較簡單。而對於既有系統,爲了降低相關風險,推薦採用引入開關變量,AB灰度的方式,具體方式如下圖所示。

動態配置

在實際應用中,當發現在線應用的命令或線程池相關參數不合理時,如何進行參數的實時調優?目前,Hystrix提供了ConfigurationManager配置管理類來實時管理配置信息,是配置相關的核心類,既可以通過實現PolledConfigurationSource類,藉助FixedDelayPollingScheduler類定時的PULL最新的配置信息,也可以通過自定義的方式監聽相關配置項的修改以PUSH方式對配置進行修改。此外,每個Hystrix參數都有4個地方可以配置,優先級從低到高如下,如果每個地方都配置相同的屬性,則優先級高的值會覆蓋優先級低的值。

  • 內置全局默認值:寫死在代碼裏的值 採用ConcurrentHashMap 有 HystrixCommandProperties HystrixThreadPoolProperties HystrixCollapserProperties
  • 動態全局默認屬性:通過屬性文件配置全局的值
  • 內置實例默認值:寫死在代碼裏的實例的值
  • 動態配置實例屬性:通過屬性文件配置特定實例的值

Tip: Hystrix默認的配置框架,archaius

應用監控

Hystrix源生提供了單機和集羣的監控服務,單機藉助Hystrix-Dashboard,集羣藉助Turbine,這裏只介紹單機監控的實現,代碼如下。

//Gradle中添加Hystrix面板
compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix-dashboard')

@Bean
public ServletRegistrationBean getServlet() {
    HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
    ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
    registrationBean.setLoadOnStartup(1);
    registrationBean.addUrlMappings("/hystrix.stream");
    registrationBean.setName("HystrixMetricsStreamServlet");
    return registrationBean;
}

之後直接訪問localhost:port/hystrix即可進入面板管理頁面,配置好hystrix.stream信息後就可以看到如下監控頁面。可以看到getArticle方法的失敗率達到59.0%,斷路器已打開,大量請求被降級,請求峯值得到緩解,這部分可以使用Jmeter進行測試。

配置知識

在瞭解hystrix時,我最開始就曾被3個KEY給打敗過,其分別是CommandGroupKeyCommandKeyThreadPoolKey。其實可以通過一個很簡單的劃方式就可以將這3個KEY區分開,CommandGroupKey是一個純邏輯的概念,其可以管理多個CommandKey,且在默認情況下ThreadPool和它同名,而後兩者則帶有實際意味,之後的配置信息可以看到,所有的配置都是基於CommandThreadPool的。

關鍵配置

Hystrix的配置項比較多,大概有30個左右,但比較基礎和關鍵的就是以下的10來個配置項,主要包括CommandPropertiesThreadPoolProperties兩部分。

命令配置中,隔離策略包括線程池和信號量兩種,默認和推薦使用前者,線程的超時時間一般設置爲比依賴調用的99線平均時間略高即可。斷路器部分,請求數量的熔斷閾值和請求失敗比例的熔斷閾值推薦更加實際的測試請求進行設置,統計信息的滑動窗口大小和分桶數採用默認值通常就可以滿足需求。

線程池配置中,主要就是線程大小的設置,默認爲10個,推薦根據所管理服務的單機QPS和TP99線計算得出,這部分支持動態配置,可以在線實時調整。這部分配置很重要,雖然Hystrix推薦創建40個左右的線程池,每個10個線程左右,但實際項目中,一定要對當前應用的依賴服務進行合理分類,否則大量的線程池和線程會對應用帶來一定不良影響。

源碼分析

Hystrix由於引入了rxJava響應式編程,代碼風格與過去習慣的結構化風格有一些差異,接下來從@HystrixCommand註解解析開始,簡要展示命令執行的整個過程,解析在代碼註釋中。

HystrixCommandAspect

@Around("hystrixCommandAnnotationPointcut() || hystrixCollapserAnnotationPointcut()")
public Object methodsAnnotatedWithHystrixCommand(final ProceedingJoinPoint joinPoint) throws Throwable {
    Method method = getMethodFromTarget(joinPoint);
    ...
    MetaHolderFactory metaHolderFactory = META_HOLDER_FACTORY_MAP.get(HystrixPointcutType.of(method));
    MetaHolder metaHolder = metaHolderFactory.create(joinPoint);
    HystrixInvokable invokable = HystrixCommandFactory.getInstance().create(metaHolder);//1.創建metaHolder
    ExecutionType executionType = metaHolder.isCollapserAnnotationPresent() ?
            metaHolder.getCollapserExecutionType() : metaHolder.getExecutionType();
    Object result;
    try {//2.命令執行
        if (!metaHolder.isObservable()) {
            result = CommandExecutor.execute(invokable, executionType, metaHolder);
        } else {
            result = executeObservable(invokable, executionType, metaHolder);
        }
    } ...
}

CommandExecutor

public static Object execute(HystrixInvokable invokable, ExecutionType executionType, MetaHolder metaHolder) throws RuntimeException {
    ...
    switch (executionType) {
        case SYNCHRONOUS: {//1.同步執行,其實其內部也是用的異步執行queue().get()
            return castToExecutable(invokable, executionType).execute();
        }
        case ASYNCHRONOUS: {//2.異步執行
            HystrixExecutable executable = castToExecutable(invokable, executionType);
            if (metaHolder.hasFallbackMethodCommand()
                    && ExecutionType.ASYNCHRONOUS == metaHolder.getFallbackExecutionType()) {
                return new FutureDecorator(executable.queue());
            }
            return executable.queue();
        }
        case OBSERVABLE: {//3.響應式執行,observable.toObservable()是核心方法
            HystrixObservable observable = castToObservable(invokable);
            return ObservableExecutionMode.EAGER == metaHolder.getObservableExecutionMode() ? observable.observe() : observable.toObservable();
        }
      ...
}

AbstractCommand

public Observable<R> toObservable() {
   ...
    final Func0<Observable<R>> applyHystrixSemantics = new Func0<Observable<R>>() {
        @Override
        public Observable<R> call() {
            if (commandState.get().equals(CommandState.UNSUBSCRIBED)) {
                return Observable.never();
            }
            return applyHystrixSemantics(_cmd);//1.關鍵步驟,命令處理
        }
    };
  ...
  
private Observable<R> applyHystrixSemantics(final AbstractCommand<R> _cmd) {
    ...
    if (circuitBreaker.attemptExecution()) {//1.【斷路器相關處理】,之後HystrixCircuitBreaker中展示
        ..
        if (executionSemaphore.tryAcquire()) {//2.獲取信號量,如果是THREAD線程池策略,【直接返回true】,這裏需要注意,不然流程將進行不下去
            try {
                executionResult = executionResult.setInvocationStartTime(System.currentTimeMillis());
                return executeCommandAndObserve(_cmd)//3.核心執行方法
                        .doOnError(markExceptionThrown)
                        .doOnTerminate(singleSemaphoreRelease)
                        .doOnUnsubscribe(singleSemaphoreRelease);
            } ...
}

private Observable<R> executeCommandAndObserve(final AbstractCommand<R> _cmd) {
    ...
    final Func1<Throwable, Observable<R>> handleFallback = new Func1<Throwable, Observable<R>>() {
        @Override
        public Observable<R> call(Throwable t) {
            circuitBreaker.markNonSuccess();
            Exception e = getExceptionFromThrowable(t);
            executionResult = executionResult.setExecutionException(e);
            if (e instanceof RejectedExecutionException) {
                return handleThreadPoolRejectionViaFallback(e);//1.處理線程池拒絕場景
            } else if (t instanceof HystrixTimeoutException) {
                return handleTimeoutViaFallback();//2.處理請求超時場景
            } else if (t instanceof HystrixBadRequestException) {
                return handleBadRequestByEmittingError(e);
            } else {
                /*
                 * Treat HystrixBadRequestException from ExecutionHook like a plain HystrixBadRequestException.
                 */
                if (e instanceof HystrixBadRequestException) {
                    eventNotifier.markEvent(HystrixEventType.BAD_REQUEST, commandKey);
                    return Observable.error(e);
                }
                return handleFailureViaFallback(e);//3.處理請求失敗場景
            }
        }
    };

HystrixCircuitBreaker

@Override
public boolean attemptExecution() {
    if (properties.circuitBreakerForceOpen().get()) {//1.斷路器是否強制打開
        return false;
    }
    if (properties.circuitBreakerForceClosed().get()) {//2.斷路器是否強制關閉
        return true;
    }
    if (circuitOpened.get() == -1) {//3.短路器關閉
        return true;
    } else {
        if (isAfterSleepWindow()) {//4.是否經過了指定的窗口時間
            if (status.compareAndSet(Status.OPEN, Status.HALF_OPEN)) {//5.設置爲半打開狀態
                //only the first request after sleep window should execute
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }
}

Tip: 老鐵告知,目前阿里已經在部分業務項目中落地基於RxJava的全異步應用,大幅提高了資源的利用效率,大家有空可以關注下,【Java11】本週好像也已新鮮出爐。

附錄

名詞解釋

TP=Top Percentile,Top百分數,是一個統計學裏的術語,與平均數、中位數都是一類。 TP50、TP90和TP99等指標常用於系統性能監控場景,指高於50%、90%、99%等百分線的情況。 OSS = Open Source Software,開源軟件(開放源代碼軟件)。 OSS= Operation Support System,運營支撐系統。

參考資料

官方文檔

Hystrix使用與分析

Hystrix高可用技術框架

Hystrix使用說明,配置參數說明

Hystrix系列-4-Hystrix的動態配置

hystrix dashboard Unable to connect to Command Metric Stream解決辦法

王興:“對未來越有信心,對現在越有耐心!”

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