1. 程式人生 > >分散式服務彈性框架“Hystrix”實踐與原始碼研究(一)

分散式服務彈性框架“Hystrix”實踐與原始碼研究(一)

文章初衷

為了應對將來線上(特別是無線端)業務量的成倍增長,後端服務的分散式化程度需要不斷提高,對於服務的延遲和容錯管理將面臨更大挑戰,公司框架和開源團隊選擇內部推廣Netflix的Hystrix,一是為了推進各部門的服務使用覆蓋率,二是為了增加C Sharp語言版本的參與度(目前公司至少三成服務由.NET編寫)。該博文屬於個人對Hystrix研究和實踐經驗。

什麼是Hystrix?

Hystrix 是世界最大線上影片租賃服務商Netflix開源,針對分散式系統的延遲和容錯庫。該庫由Java寫成,專案源於Netflix API團隊在2011年啟動的彈性工程專案。專案在github上釋出至今,已經有接近三千顆星,只有少數優秀的開源專案才能享受到千星級別的待遇,Hystrix成功可見一斑。 

 為什麼使用Hystrix?

在大中型 分散式系統 中,通常系統很多依賴( HTTP ,hession,Netty,Dubbo等),如下圖:

在高併發訪問下,這些依賴的穩定性與否對系統的影響非常大,但是依賴有很多不可控問題:如網路連線緩慢,資源繁忙,暫時不可用,服務離線等.

如下圖:QPS為50的依賴 I 出現不可用,但是其他依賴仍然可用.

當依賴I 阻塞時,大多數伺服器的 執行緒 池就出現阻塞(BLOCK),影響整個線上服務的穩定性.如下圖:

在複雜的分散式 

架構 的應用程式有很多的依賴,都會不可避免地在某些時候失敗。高併發的依賴失敗時如果沒有隔離措施,當前應用服務就有被拖垮的風險。

例如:一個依賴30個SOA服務的系統,每個服務99.99%可用。
99.99%的30次方 ≈ 99.7%
0.3% 意味著一億次請求 會有 3,000,00次失敗
換算成時間大約每月有2個小時服務不穩定.
隨著服務依賴數量的變多,服務不穩定的概率會成指數性提高.
解決問題方案:對依賴做隔離,Hystrix就是處理依賴隔離的框架,同時也是可以幫我們做依賴服務的治理和監控.

到底能做什麼呢?

1)Hystrix使用命令模式HystrixCommand(Command)包裝依賴呼叫邏輯,每個命令在單獨執行緒中/訊號 

授權 下執行

2)提供熔斷器元件,可以自動執行或手動呼叫,停止當前依賴一段時間(10秒),熔斷器預設 錯誤率閾值為50%,超過將自動執行。

3)可配置依賴呼叫 超時 時間,超時時間一般設為比99.5%平均時間略高即可.當呼叫超時時,直接返回或執行fallback邏輯。

4)為每個依賴提供一個小的執行緒池(或訊號),如果執行緒池已滿呼叫將被立即拒絕,預設不採用排隊.加速失敗判定時間。

5)依賴呼叫結果分:成功,失敗(丟擲 異常 ),超時,執行緒拒絕,短路。 請求失敗(異常,拒絕,超時,短路)時執行fallback(降級)邏輯。

6)提供近實時依賴的統計和監控

7) 支援非同步執行。支援併發請求快取。自動批處理失敗請求。

Hystrix設計理念

想要知道如何使用,必須先明白其核心設計理念,Hystrix基於命令模式,通過UML圖先直觀的認識一下這一設計模式

可見,Command是在Receiver和Invoker之間新增的中間層,Command實現了對Receiver的封裝。那麼Hystrix的應用場景如何與上圖對應呢?

API既可以是Invoker又可以是reciever,通過繼承Hystrix核心類HystrixCommand來封裝這些API(例如,遠端介面呼叫,資料庫查詢之類可能會產生延時的操作)。就可以為API提供彈性保護了。

Hello World

Hello World的例子旨在展示,如何在專案中低侵入式的改造,使API置於Hystrix保護之下!

引入maven依賴

<!-- 依賴版本 -->
<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>
<!-- 倉庫地址 -->
<repository>
	<id>nexus</id>
	<name>local private nexus</name>
	<url>http://maven.oschina.net/content/groups/public/</url>
	<releases>
		<enabled>true</enabled>
	</releases>
	<snapshots>
		<enabled>false</enabled>
	</snapshots>
</repository>

下面是一個沒有使用Hystrix保護的sayHello的服務以及它的呼叫

//API呼叫,可能會產生延時
public class HelloService {
	public static String sayHello(final String name)
	{
	    return String.format("Hello %s!", name);
	}
 }
//客戶端直接呼叫API
public class Client{
	 public static void main(String[] args)
	 {
		 System.out.println(HelloService.sayHello("World"));	
	 }  
}    

假設字串生成過程是一個需要保護的操作,下面我們用Hystrix進行封裝。

需要注意的是,雖然使用命令模式,但是我們這裡不建議覆蓋execute方法,而是實現run的模版方法,多數框架的實現會採用template設計模式,並且將模版方法設定為protected簽名,這樣做的好處是,既可以將具體的業務交給業務實現者,又可以為之新增其他功能,而業務實現者只需要關注自己的業務就好了。比如這裡HystrixCommand.execute方法實際上是呼叫了HystrixCommand.queue().get(),而queue方法除了最終呼叫run之外,還需要為run方法提供超時和異常等保護功能,外部也不能直接呼叫非安全的run方法,這一實踐非常值得我們學習。

OK,現在我們通過實現run方法來包裝sayHello功能,我們通過一個私有域_name,通過建構函式來傳遞訊息,獲取構造引數的拷貝來保持不變性。

public class SayHelloCommand extends HystrixCommand<String> {
	private final String _name;
	public SayHelloCommand(String name)
	{
		super(HystrixCommandGroupKey.Factory.asKey("HelloService"));
		_name = new String(name);//unmutable
		
	}
	@Override
	protected String run() {
		return String.format("Hello %s!", _name);
	}
}

API改造如下,作為門面方法最好不要改動函式的簽名(除非引數和返回型別有變動,這是因為客戶端程式碼的改動代價往往是巨大的),同時提供版本sayHelloAsync,該方法提供了非同步功能

public class HelloService {
//    public static String sayHello(final String name)
//    {
//        return "Hello " + name + "!";
//    }
    
    /**
     * sayHello under protection of Hystrix
     * @param name
     * @return <code>"Hello " + name + "!"</code> 
     */
    public static String sayHello(final String name)
    {
        return new SayHelloCommand(name).execute();
    }
    
    /**
     * call async
     * @param name
     * @return
     */
    public static Future<String> sayHelloAsync(final String name)
    {
        return new SayHelloCommand(name).queue();
    }
}

接下來我們來看看,如何在超時熔斷情況下使用FallBack策略,這點在專案中是相當有用的,比如超時後訪問資料備庫,或者直接返回重試響應

首先SayHelloCommand建構函式使用Hystrix的Setter來設定超時時間,這裡解釋下Setter這個類涉及到的幾個最佳實踐

1.Setter使用builder模式,想想建構函式有很多引數要設定,作為構造引數傳遞會大大降低可閱讀性,用靜態工廠方法一個個設定又可能造成多執行緒併發下的不一致性,而且這種bug往往非常難以定位,所以builder模式是非常好的實踐。將Setter作為構造器傳給HystrixCommand的建構函式,Setter中又很多靜態方法,可以通過方法名明確的知道元素的意義。

2.Setter是HystrixCommand內部靜態類,Hystrix程式碼大量的使用了內部靜態類,來作為該類的工廠方法,或者構造器,我覺得這樣劃分使程式碼職責更加清晰,比單獨的工廠類更易於維護。

3.Setter使用函式式串聯,每個靜態工廠方法返回Setter例項,這樣我們可以把構造過程串聯起來,使程式碼更加易於閱讀。

4.Setter是不可變類,每個靜態工廠方法返回一個新的Setter拷貝,所以Setter是執行緒安全的。

OK,Setter介紹到這裡,這裡我們繼續設定超時時間為500

public SayHelloCommand(final String name)
    {
        //builder for HystrixCommand
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloServiceGroup"))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withTimeoutInMilliseconds(500)));
        _name = new String(name);
    }

run方法我們使用Thread.sleep(600)來特意達到超時的效果,同時實現getFallback方法,程式超時後會立即執行FallBack

	@Override
protected String getFallback() {
	return String.format("[FallBack]Hello %s!", _name);
}
@Override
protected String run() throws Exception {
	//TimeOut
	Thread.sleep(600);
	return String.format("Hello %s!", _name);
}

最終輸出:

[FallBack]Hello World!

回顧重點

1.Hystrix可以為分散式服務提供彈性保護

2.Hystrix通過命令模式封裝呼叫,來實現彈性保護,繼承HystrixCommand並且實現run方法,就完成了最簡單的封裝。

3. 實現getFallBack方法可以為熔斷或者異常提供後備處理方法。

4.HystrixCommand中Setter類的最佳實踐。

5.模版方法在框架中的實踐。