天天看點

Hystrix淺入淺出:(一)背景與功能初探

終于到了必須要了解Hystrix的時候了,Hystrix直譯過來是刺猬的意思,這裡指的是Netflix開源的一個包含限流、熔斷等功能的庫類,它能給系統提供快速失敗和快速恢複的能力,讓其更具“彈性”。

流控、熔斷和快速恢複是現在大型分布式系統中各個服務節點應該具備的基本抗災和容錯能力,如何在流量突增、依賴服務當機等外界緊急情況發生時不需要人工幹預來自動做到快速止損(比如服務降級)、防止整個分布式系統雪崩?緊急情況消失後又能在短時間内做到整個系統服務的快速恢複?這将是本系列文章的主題之一,也許Hystrix能幫你做到這一點。

Hystrix由Netfilix API團隊研發于2011年,2012年開始在Hystrix公司内部推廣和使用,Hystrix在Netflix中久經沙場,現在已經是一個非常成熟的系統,而最近流行的微服務架構和Spring Cloud,讓Hystrix成為了配套的基礎設施,在國内也逐漸流行起來。然而仍有部分使用者抱怨Hystrix不太好用,那又是為什麼呢?我們來逐漸解開Hystrix的面紗。

如果做一個簡單的限流功能,那是很容易的,《常用限流方案的設計和實作》一文已經比較清楚的介紹了其實作,但如果想做更精準的控制、處理後的細分和快速恢複,還有大量的工作需要做。很多RPC架構也自帶流控和熔斷功能,比如Dubbo,但功能不夠強大,大多需要人工手動操作,離自動還有段距離,這也是為啥需要将其作為一套單獨的解決方案的原因。

這裡先對一些關鍵術語進行簡單的解釋:

限流:即限制流量的最大值,是流控的一種方式,

熔斷/斷路:熔斷其實是一種熔斷器(又叫斷路器)模式中的一種狀态,這裡先說熔斷器(Circuit Breaker),斷路器本質上是一個實體元件,但在這裡指的是軟體系統對緊急情況進行快速止損的一種設計方案(詳見http://martinfowler.com/bliki/CircuitBreaker.html),熔斷器設計中有三種狀态,closed(關閉狀态,流量可以正常進入)、open(即熔斷狀态,一旦錯誤達到門檻值,熔斷器将打開,拒絕所有流量)和half-open(半開狀态,open狀态持續一段時間後将自動進入該狀态,重新接收流量,一旦請求失敗,重新進入open狀态,但如果成功數量達到門檻值,将進入closed狀态),即下圖:

Hystrix淺入淺出:(一)背景與功能初探

 降級:即我們常說的服務降級,其實來自于服務等級(或服務分級),根據服務的品質、功能或其他名額,人為的将服務分成多個等級,便于我們分析和定位服務級别,而服務降級指的是當達到某個條件或特殊場景時,需要下調服務等級。

為了不陷入術語怪圈(比如并發和并行的差別),我們這邊将系統遇到“危險”時采取的整套應急方案和措施統一稱為降級或服務降級。想要幫助服務做到自動降級,需要先做到如下幾個步驟:

  1. 可配置的降級政策:降級政策=達到降級的條件+降級後的處理方案,政策一定得可配置,因為不同的服務對服務的品質定義不一樣,降級的方案也将不一樣。
  2. 可識别的降級邊界:一定要精确的知道需要對誰進行降級,可以是一個對外服務、對下遊的一個依賴或者是内部一段處理邏輯。降級邊界主要用來植入降級邏輯。
  3. 資料采集:是否達到降級條件依賴于采集的資料,這些資料可以是目前某段時間的資料,也可以是很長一段時間的曆史資料。
  4. 行為幹預:進入降級狀态後将會對正常的業務流程産生幹預,可能是限流、熔斷,也可能是同步流程變為異步流程等(比如發送MQ的變成oneway的形式)等。
  5. 結果幹預:是傳回null,還是預設值,還是流程上的同步改異步等。
  6. 快速恢複:即如何從降級狀态變回正常狀态,這也需要達到某些條件。

我們來逐漸看下Hystrix是如何做到以上幾點的,

### 可配置的降級政策 ###

Hystrix提供了三種降級政策:并發、耗時和錯誤率,而Hystrix的設計本身就能很好的支援動态的調整這些政策(簡單的說就是調整并發、耗時和錯誤率的門檻值),當然,如何去動态調整需要使用者自己來實作,Hystrix隻提供了入口,就是說,Hystrix并沒有提供一個服務端界面來動态調整這些政策,這多少有點讓人遺憾。如果要了解Hystrix具體的政策配置,可以看看HystrixCommandProperties和HystrixThreadPoolProperties兩個類。

### 可識别的降級邊界 ###

降級工具面臨的第一個難題就是如何在業務代碼中植入降級邏輯,業務研發人員得提前明确和定義哪些地方是風險點,然後将這些地方的邏輯抽取出來,Hystrix包裝需降級的業務邏輯采用的是Command設計模式,我們知道,指令模式主要是将請求封裝到對象内部,讓我們使用對象一樣來使用請求。這樣對Hystrix大有好處,因為你需要降級的業務邏輯和資料已經封裝成一個Command對象交給Hystrix了,Hystrix直接來接管業務邏輯的執行權,該何時調用,或者甚至不調用都可以,我們來看看Hystrix定義的指令接口(實際是抽象類,這裡已簡化):

public abstract class HystrixCommand<R> {

	protected abstract R run() throws Exception;

	protected R getFallback() {
		throw new UnsupportedOperationException("No fallback available.");
	}
	
	public R execute() {
	    try {
	        return queue().get();
	    } catch (Exception e) {
	        throw Exceptions.sneakyThrow(decomposeException(e));
	    }
	}
	
	public Future<R> queue() {
	    ... 太長了,略 ...
	}
	
}
           

隻需要簡單繼承HystrixCommand,就相當于接入了Hystrix,泛型R代表傳回值類型,在run()方法中直接實作正常的業務邏輯,并傳回R類型的結果,如果降級後需要傳回特殊的值,你隻需要覆寫getFallback()方法即可。舉個例子,我們這裡有個抽獎活動,隻要是我們的注冊使用者,就有一次抽獎機會,前提是不在我們的黑名單内。

public class ChouJiangService {

    /**
     * 嘗試抽獎
     *
     * @param userId
     * @return 中獎結果,false-沒中獎,true-已中獎
     */
    public boolean tryChouJiang(Long userId) {

        if(!checkUserStatus(userId)) {
            return false;
        }

        // 傳回是否中獎
        return ThreadLocalRandom.current().nextInt(2) == 1;
    }

    /**
     * 檢查使用者是否是黑名單使用者
     *
     * @param userId
     * @return true-不在黑名單,false-在黑名單
     */
    private boolean checkUserStatus(Long userId) {
        // 請不要在意内部邏輯有多奇怪,這裡隻是示範
        return System.currentTimeMillis() % 2 == 1;
    }
}
           

對于抽獎服務來說,檢查使用者黑名單并不是必須的行為,如果checkUserStatus内部發生問題(有可能裡面依賴了外部服務),不應該影響正常的抽獎邏輯,因為畢竟在黑名單裡的使用者是少數,如果我們要對checkUserStatus邏輯使用Hystrix,我們就會先建立一個CheckUserStatusCommand類,來封裝檢查使用者黑名單的邏輯:

public class CheckUserStatusCommand extends HystrixCommand<Boolean> {

    private Long userId;

    public CheckUserStatusCommand(Long userId) {
        super(HystrixCommandGroupKey.Factory.asKey("ChouJiangCommandGroup"));
        this.userId = userId;
    }

    @Override
    protected Boolean run() throws Exception {
        // 請不要在意内部邏輯有多奇怪,這裡隻是示範
        return System.currentTimeMillis() % 2 == 1;
    }
}

           

這樣做了以後,ChouJiangService#tryChouJiang方法就該寫成如下樣子:

public boolean tryChouJiang(Long userId) {

    if(!new CheckUserStatusCommand(userId).execute()) {
        return false;
    }

    // 傳回是否中獎
    return ThreadLocalRandom.current().nextInt(2) == 1;
}

           

可以看出,我們建立了一個CheckUserStatusCommand的執行個體,然後調用了execute方法來擷取結果,這樣就基本完成了,Hystrix庫類已經給檢查使用者黑白名單的邏輯附上了自動降級邏輯了,當然裡面使用了大量Hystrix預設的降級政策配置(本文不是Hystrix使用的詳細教程,是以這裡主要突出的是用法而不強調具體的政策配置)。這裡同樣也說明了為什麼動态調整配置是很容易的,因為每個請求都會建立Command對象(注意,Command對象是有狀态的,不能重用),你隻需要在建立時調整政策參數就行了,當然,這得使用者自己來實作。<!--EndFragment-->

雖然看起來很簡單,但老司機馬上會發現問題:

  1. 系統中每一處需要降級的邏輯都需要将其封裝成一個Command類,哪怕需要降級的方法隻有一行代碼。如果一個系統有一百個需要降級的點,那麼我們需要在系統中新增一百個Command類,有時候這讓人難以接受。
  2. 對老的業務系統來說,接入Hystrix将意味着巨大的工作量,因為你要把很多邏輯都封裝成Command,你能接受但測試同學未必願意。
  3. 每次請求都将建立一個Command對象,因為Command對象包含了降級邏輯的大部分操作,是個重狀态的對象,不能複用,如果QPS過高,将産生大量的朝生夕死的對象,對記憶體配置設定和GC将産生一定的壓力。

很多使用者确實也提出過抱怨,為何Hystrix的侵入性那麼強?但Hystrix設計者們這麼做自然有他們的道理(詳見:https://github.com/Netflix/Hystrix/wiki/FAQ%20:%20General 的Why is it so intrusive?部分),他們認為,我們需要給應用的依賴提供一個清晰的屏障,使用Command模式不僅僅是出于功能上的原因,也是作為一種标準機制,通過Command對象來向使用者傳遞它是受保護的資源。可見,Hystrix的設計者們并不建議我們使用基于注解或AOP來作為接入Hystrix的方式,但他們仍然說:If you still feel strongly that you shouldn't have to modify libraries and add command objects then perhaps you can contribute an AOP module.(直譯過來就是如果你嫌麻煩不想建立這麼多Command對象,有本事你自己去實作AOP啊!開個玩笑(*^__^*) )。

### 資料采集 ###

收集資料是必不可少的一步,每個降級點(需要采取降級保護的點)的資料是獨立的,是以我們可以給每個降級點配置單獨的政策。這些政策一般是建立在我們對這些降級點的了解之上的,初期甚至可以先觀察一下采集的資料來指定降級政策。采集哪些資料?資料如何存儲?資料如何上報?這都是Hystrix需要考慮的問題,Hystrix采用的是滑動視窗+分桶的形式來采集資料(具體細節見另一篇),這樣既解決了資料在統計周期間切換而帶來的跳變問題(通過時間視窗),也控制了切換了力度(通過桶大小)。另一個有意思的地方是,與正常的同步統計資料的方式不同,Hystrix采用的是RxJava來進行事件流的異步統計資料,類似于觀察者模式(具體細節見另一篇),這樣做的好處是降低統計時阻塞業務邏輯的風險,在某些情況下還能享受多核CPU所帶來的性能上的收益。

### 行為幹預 ###

一旦發現采集的資料命中了降級政策,那麼降級工具就将對請求進行行為幹預,行為幹預是評價一個降級工具好壞的重要名額,它的設計直接關系到系統的“彈性”到底有多大。但有時候行為幹預和上面提到的資料采集這兩個動作是同時完成的,比如使用信号量、線程池或者令牌桶算法來進行降級的時候。行為幹預的設計是很有技巧的,一般來說有如下兩種方案:

  1. 實時采集(目前某段時間周期的)資料,對每筆請求都進行政策判斷(每筆請求都會加入資料并進行分析),一旦命中政策,當即對這筆請求進行行為幹預,如果沒有命中,則執行正常的業務邏輯。
  2. 實時采集(目前某段時間周期的)資料,對每筆請求都進行政策判斷(每筆請求都會加入資料并進行分析),一旦有一筆請求命中了政策,接下來的一段時間(可配)内的所有請求都會被行為幹預,哪怕接下來再也沒有請求命中政策,一直到該段時間過去。

方案a似乎是比較合理的,它總是将系統的行為盡可能的控制在我們預期之内(即各項名額都在配置的政策之下),但多數情況下,我們配置政策會比較寬泛,不那麼嚴格,那這時候采用方案a對系統來說還是有一定的風險。這時候就出現了相對更激進的方案b!一但某些請求導緻統計資料觸犯了降級政策,那麼系統會對後續一段時間的所有請求進行降級處理,即我們熟知的降級延長。而Hystrix将兩者結合起來了,讓行為幹預更加靈活。

### 結果幹預 ###

被降級後的請求是應該傳回null?還是預設值?還是抛異常?這些都要根據業務而定。Hystrix也在HystrixCommand提供了getFallback方法來友善使用者傳回降級後的結果。

### 快速恢複 ###

快速恢複功能在那些經常由于外部因素而導緻進入降級狀态的系統來說尤為重要,降級系統或工具的一個重大目标就是自動性,擺脫需要人為控制開關來保證功能熔斷的“原始時代”,是以當外部條件已經恢複,系統也應該在最短的時間内恢複到正常服務狀态,這就要求降級系統能夠在讓業務系統進入降級狀态的同時,讓業務系統有探測外界環境的機會。大多數降級系統都會在一段時間後“放”一筆請求進來,讓它去“試一試”,如果結果是成功的,那麼将讓業務系統恢複到正常狀态,Hystrix同樣也是采用這種做法。

如果看到這裡,其實大家已經對Hystrix的功能有一定的了解,這裡再給一張官方的圖:

Hystrix淺入淺出:(一)背景與功能初探

這張圖已經充分說明了官方推薦的是通過Command+線程池的模式來進行業務功能的剝離和管理,這些大大小小的線程池,使用不當,将産生隐患,是以千萬不要讓Hystrix的這種用法變成反模式。

在最後,我們來簡單總結下Hystrix的特色:

  1. Hystrix内部大量使用了響應式程式設計模型,通過RxJava庫,把能異步做的都做成異步了。這似乎能降低代碼複雜度(我是指對RxJava了解的人),并且在多核CPU的伺服器上能帶來意外性能收獲。
  2. Hystrix能做到通過并發、耗時和異常來進行降級,并能在(并發、限流或内部産生的異常導緻的)錯誤率達到一定門檻值時進行服務熔斷,并且還能做到從降級狀态快速恢複。
  3. Hystrix通過Command模式來包裝降級業務,這有時候提高了接入成本。
  4. Hystrix隻提供了政策變更的入口,但具體的政策可視化和動态配置還是得使用者來實作,這确實非常尴尬。
  5. Hystrix預設的儀表盤隻提供了簡單的實時資料顯示,如果要持久化曆史資料,也得使用者來實作。

Hystrix并不完美,但也許簡單也是一種美,後續文章将深入介紹Hystrix的内部設計。

歡迎關注我們的技術公衆号

Hystrix淺入淺出:(一)背景與功能初探

繼續閱讀