天天看點

SpringCloud進階-詳解如何Hystrix實戰服務容錯(一)

作者:架構師面試寶典
SpringCloud進階-詳解如何Hystrix實戰服務容錯(一)

首先,通過前面的介紹,我們知道了在微服務的架構中,存在多個可以調用的服務的時候,如果這些服務在調用過程中其中一個服務出現故障之後就會引起各種的連鎖反應,最終導緻整個系統的不可用,這種情況有一個專業術語叫做服務雪崩。那麼我們如何解決服務雪崩的問題呢?在Spring Cloud中提供了Hystrix元件用來解決這個問題。

SpringCloud進階-詳解如何Hystrix實戰服務容錯(一)

當然,現在市面上有很多的解決服務雪崩的解決方案,有興趣的讀者也可以了解相關的解決方案。這裡我們重點介紹一下Hystrix,通過Hystrix來看一下,在解決服務雪崩問題的時候都有那些需要注意的點。

簡單介紹

Hystrix是Netflix用來解決微服務分布式系統服務熔斷保護機制的中間件,相當于電路中的空氣開關,如果發生電路異常的時候會保護電路。

在整個的微服務體系中,有着各種錯綜複雜的服務調用,如果不對服務進行熔斷保護就會出現上面我們提到的服務雪崩,這就會導緻整個的服務不可用。而Hystrix就提供了一個服務熔斷保護機制,當服務出現問題的時候,就會對出問題的服務進行隔離,保證其他服務不會出現問題,導緻各種連鎖反應,進而實作服務降級的操作。

Hystrix的案例

首先我們來建立一個Maven項目并且在項目中引入如下的依賴。這個依賴就是為項目中增加Hystrix的依賴。

<dependency>
	<groupId>com.netflix.hystrix</groupId>
	<artifactId>hystrix-core</artifactId>
	<version>1.5.18</version>
</dependency>           

接下來我們需要編寫一個HystrixCommand類。

public class MyHystrixCommand extends HystrixCommand<String> {

	private final String name;

	public MyHystrixCommand(String name) {
		super(HystrixCommandGroupKey.Factory.asKey("MyGroup"));
		this.name = name;
	}
	@Override
	protected String run() {
		System.err.println("get data");   
		return this.name + ":" + Thread.currentThread().getName();
	}
}
           

需要注意的是在HystrixCommand的構造函數中我們需要先對其設定一個GroupKey的值。而具體的方法實作邏輯是在繼承了run()方法中來實作,這裡我們向控制台輸出了一個目前線程的名字。然後可以通過測試方法來調用這個方法。

public class HystrixMainApplication {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		String result = new MyHystrixCommand("架構師").execute(); 
		System.out.println(result);	
	}
}
           

從輸出結果上來看,我們設定在構造函數中的GroupKey成了線程的名字。

而上面這種方式是通過同步調用的方式來實作的。當然我們還可以通過異步的方式來實作這種操作。如下

public class HystrixMainApplication {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		Future<String> future = new MyHystrixCommand("架構師").queue();
		System.out.println(future.get()); 
	}
}           

通過上面這種方式可以實作對于方法的異步調用,也就是說不需要等到線程結束擷取調用結果。就可以執行後續的操作。

回退支援

在一些場景中,由于網絡的原因,或者是資料庫查詢的原因會導緻調用逾時的情況發生,這個時候,我們就需要對HystrixCommand進行一個簡單的改造,使其支援如果在調用逾時之後可以調用其他穩定的接口來保證系統的穩定性等問題。

public class MyHystrixCommand extends HystrixCommand<String> {

	private final String name;

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

	@Override
	protected String getFallback() {
		return "調用失敗";
	}

	@Override
	protected String run() {
		System.err.println("get data");   
		return this.name + ":" + Thread.currentThread().getName();
	}
}           

在HystrixCommand中有一個getFallback()方法就是來完成這個操作的。然後我們需要在run方法中設定一個調用逾時時間。

@Override
protected String run() {
	try {
		Thread.sleep(1000 * 10);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	return this.name + ":" + Thread.currentThread().getName();
}           

繼續執行上面的代碼,就會發現傳回值是調用失敗,也就是說執行了回退方法。在很多場景下,這種方式可以很好地保證系統調用的穩定性。

資源隔離

比如我們現在有3個業務調用分别是查詢訂單、查詢商品、查詢使用者,且這三個業務請求都是依賴第三方服務-訂單服務、商品服務、使用者服務。

三個服務均是通過RPC調用。當依賴的訂單服務變慢了,而這個時候後續有大量的查詢訂單請求過來,那麼容器中的線程數量則會持續增加直緻CPU資源耗盡到100%,整個服務對外不可用,叢集環境下就是雪崩。

是以,有必要将多個依賴服務的調用分别隔離到各自自己的資源池内,不對其他服務造成影響。Hystrix為我們提供了兩種資源隔離政策。一種是信号量政策,一種是線程隔離政策。下面我們分别來看一下這兩種政策。

信号量隔離政策

關于信号量隔離,用于隔離本地代碼或可快速傳回的遠端調用(如memcached,redis)可以直接使用信号量隔離,降低線程隔離的上下文切換開銷。

線程隔離會帶來線程開銷,有些場景(比如無網絡請求場景)可能會因為用開銷換隔離得不償失,為此hystrix提供了信号量隔離。

主要适用場景: 并發需求不大的依賴調用(因為如果并發需求較大,相應的信号量的數量就要設定得夠大,因為Tomcat線程與處理線程為同一個線程,那麼這個依賴調用就會占用過多的Tomcat線程資源,有可能會影響到其他服務的接收)

和線程池隔離類似,同一個HystrixCommandGroupKey共用一個信号量(預設為類名)

public MyHystrixCommand(String name) {

    super(HystrixCommand.Setter
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("MyGroup"))
            .andCommandPropertiesDefaults(
                    HystrixCommandProperties.Setter()
                            .withExecutionIsolationStrategy(
                                    HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE

                            )
            )
    );
    this.name = name;
}           

線程隔離政策

适用場景:适合絕大多數的場景,對依賴服務的網絡調用timeout,TPS要求高的這種問題

執行依賴代碼的線程與請求線程(比如Tomcat線程)分離,請求線程可以自由控制離開的時間,這也是我們通常說的異步程式設計,Hystrix是結合RxJava來實作的異步程式設計。通過為每個包裹了HystrixCommand的API接口設定獨立的、固定大小的線程池(hystrix.threadpool.default.coreSize)來控制并發通路量,當線程飽和的時候可以拒絕服務(走fallback方法),防止依賴問題擴散。

線上建議線程池不要設定過大,否則大量堵塞線程有可能會拖慢伺服器。

public MyHystrixCommand(String name) {
	 super(HystrixCommand.Setter.withGroupKey(
	           HystrixCommandGroupKey.Factory.asKey("MyGroup"))                 
	         .andCommandPropertiesDefaults(     
	             HystrixCommandProperties.Setter()     
	             .withExecutionIsolationStrategy(      
	               HystrixCommandProperties.ExecutionIsolationStrategy.THREAD 
	             )                 
	         ).andThreadPoolPropertiesDefaults(    
	             HystrixThreadPoolProperties.Setter()      
	               .withCoreSize(10)                
	 	       .withMaxQueueSize(100)          
	       	       .withMaximumSize(100)               
	         )         
	);       
	this.name = name;
}           

線程池隔離:

  • 1、調用線程和hystrixCommand線程不是同一個線程,并發請求數受到線程池(不是容器tomcat的線程池,而是hystrixCommand所屬于線程組的線程池)中的線程數限制,預設是10。
  • 2、這個是預設的隔離機制
  • 3、hystrixCommand線程無法擷取到調用線程中的ThreadLocal中的值

信号量隔離:

  • 1、調用線程和hystrixCommand線程是同一個線程,預設最大并發請求數是10
  • 2、調用速度快,開銷小,由于和調用線程是處于同一個線程,是以必須確定調用的微服務可用性足夠高并且傳回快才用

注意:如果發生找不到上下文的運作時異常,可考慮将隔離政策設定為SEMAPHONE。