微服務容錯框架:Hystrix實現服務熔斷、降級、限流

業務背景

在微服務架構體系下,服務間不可避免地會發生依賴關係,一般來說會通過REST Api來進行通信,這裏先盜一個圖來舉例說明一個具體的業務場景(逃):

img

比如一個商城系統的微服務化結構,訂單、商品、庫存這三個服務是緊密依賴的,在理想情況下,什麼問題都不發生當然是最好的。但服務運行期間難免會出現各種問題,如網絡阻塞,延遲過高(比如因爲內存泄露導致的Full GC次數飆高) ,甚至服務直接掛掉(比如流量激增把服務打掛了)等情況都是很有可能發生的。倘若庫存服務掛掉了,那對於所有對庫存服務有依賴關係的服務都會受到很大影響,最終甚至會擴散到整個微服務體系,這種就稱之爲雪崩效應。

因此,在某一個服務發生故障時,我們要及時對該服務的故障進行隔離,不能讓其擴散到整個微服務體系中。因爲,爲了搭建一個穩定且可靠的微服務系統,我們就需要給系統加上自我保護,出現故障自動隔離的能力。而Hystrix就能做到這一點

什麼是Hystrix

Hystrix是Netflix開源的一款分佈式容錯框架,Netflix旗下還有Eureka,Zuul等優秀的分佈式開源項目,Spring Cloud也提供了對Netflix中部門項目的支持,成爲了SpringCloud下的一些子項目 。

Hystrix的功能:

  • 阻止故障的連鎖反應,實現熔斷
  • 快速失敗,實現優雅降級
  • 提供實時的監控和告警

Hystrix簡單實現

public class QueryUserIdCommand extends HystrixCommand<Integer> {
    private final static Logger logger = LoggerFactory.getLogger(QueryUserIdCommand.class);
    private UserServiceProvider userServiceProvider;

    public QueryUserAgeCommand(UserServiceProvider userServiceProvider) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("userService"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("queryByUserId"))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withCircuitBreakerRequestVolumeThreshold(10)//至少有10個請求,熔斷器纔會開始進行錯誤率計算
                        .withCircuitBreakerSleepWindowInMilliseconds(5000)//熔斷器中斷請求,5秒後會進入一個半打開狀態,放開部分請求去進行重試
                        .withCircuitBreakerErrorThresholdPercentage(50)//錯誤率達到50%就開啓熔斷保護
                        .withExecutionTimeoutEnabled(true))
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties
                        .Setter().withCoreSize(10)));
        this.userServiceProvider = userServiceProvider;
    }
     
    @Override
    protected Integer run() {
        return userServiceProvider.queryByUserId();
    }
     
    @Override
    protected Integer getFallback() {
        return -1;
    }

}

發起請求

Integer res = new QueryUserIdCommand(userServiceProvider).execute();
log.info("result:{}", res);

訪問接口,正常情況下,會返回正確的信息,當把UserServiceProvider所依賴的服務的接口改爲直接拋出一個異常,就會發現總是返回-1了。這樣就做到了對錯誤進行隔離。

Hystrix容錯

那接下來從三個角度來聊一下Hystrix提供的容錯功能,分別是資源隔離熔斷降級

資源隔離

我們之前也討論過,微服務體系中,各個服務之間都通過REST Api來進行調用,從而建立依賴關係。倘若該服務調用和業務代碼在同一個線程會中執行的話,如果 api在調用的時候出現了網絡堵塞等情況,那麼不僅會對業務代碼進行阻塞,也會對後面的請求造成阻塞,因爲線程池的線程數是額定的。所以,Hystrix也提供了資源隔離的機制,主要是線程隔離和信號量隔離

資源隔離-線程池

剛纔我們簡單地應用了一下Hystrix,很明顯看到我們得先實現一個自己的HystrixCommand,然後把服務調用的操作封裝在這個類裏面。而實際上,線程級別的資源隔離就是在HystrixCommand中實現。Hystrix會給每一個Command分配一個單獨的線程池,這樣在進行單個服務調用的時候,就可以在獨立的線程池裏面進行,而不會對其他線程池造成影響。

Hystrix通過一個ConcurrentHashMap來維護這些線程池:

final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();

//其他代碼

if (!threadPools.containsKey(key)) {
    threadPools.put(key, new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));
}

線程隔離的優點:

  • 保護當前應用免受來自其他服務故障的影響,最終提高整個微服務體系的穩定性

  • 可以對一個Command裏面的線程調用參數進行單獨設置,而不影響其他Command,如果使用Spring Cloud Hystrix的話,那就是:

@HystrixCommand(groupKey="UserGroup", commandKey = "GetUserByIdCommand",
                commandProperties = {
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "100"),//超時時間,單位毫秒。超時進fallback
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),//判斷熔斷的最少請求數,默認是10;只有在一定時間內請求數量達到該值,纔會進行成功率的計算
                    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "10"),//熔斷的閾值默認值50,表示在一定時間內有50%的請求處理失敗,會觸發熔斷
                },
                threadPoolProperties = {
                        @HystrixProperty(name = "coreSize", value = "30"),
                        @HystrixProperty(name = "maxQueueSize", value = "101"),
                        @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
                        @HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
                        @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
                        @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1440")
        })
 
  • 線程池如果處於飽和態,還往其中添加請求的話,會直接觸發fallback,所以還有限流的作用

而爲每一個Command配置一個線程池的缺點就是增加開銷,線程一多起來,就增加了調度,上下文切換等額外的開銷。但通常情況下,引入線程池的開銷是很小的,相對於它帶來的好處,大多數情況還是非常樂於進行這樣的交換,但如果是一些耗時非常低的請求,比如直接從緩存中獲取數據然後返回,引入線程池帶來的影響可能會顯得比較大,這種時候可以用另一種方法,就是信號量隔離

信號量隔離

信號量隔離本質上並沒有做到資源隔離,只是起到了限流的作用,從而防止出現線程大面積阻塞,功能和JUC下的那個Semaphare類差不多,但區別就是,沒有執行條件的線程在這裏會直接調用fallback,而不是阻塞。啓用方法也很簡單,在實現Hystrix的時候聲明一下即可:

public class QueryUserIdCommand extends HystrixCommand<Integer> {
    private final static Logger logger = LoggerFactory.getLogger(QueryUserIdCommand.class);
    private UserServiceProvider userServiceProvider;
	public QueryUserAgeCommand(UserServiceProvider userServiceProvider) {
    	super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("userService"))
            	.andCommandKey(HystrixCommandKey.Factory.asKey("queryByUserId"))
	            .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
    	                .withCircuitBreakerRequestVolumeThreshold(10)//至少有10個請求,熔斷器纔會開始進行錯誤率計算
        	            .withCircuitBreakerSleepWindowInMilliseconds(5000)//熔斷器中斷請求,5秒後會進入一個半打開狀態,放開部分請求去進行重試
            	        .withCircuitBreakerErrorThresholdPercentage(50)//錯誤率達到50%就開啓熔斷保護                       			 
                	    .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE) //here!!!!! 
                    	.withExecutionTimeoutEnabled(true))
	            .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties
    	                .Setter().withCoreSize(10)));
    	this.userServiceProvider = userServiceProvider;
	}
 
	@Override
	protected Integer run() {
    	return userServiceProvider.queryByUserId();
	}
 
	@Override
	protected Integer getFallback() {
    	return -1;
	}
}

總結一下,資源隔離就大概是這兩種了,大多數情況用的還是線程隔離居多,畢竟它纔是真正意義上的資源隔離,而信號量隔離只是起到一個限流的作用而已

熔斷

一個服務出現故障之後,防止這個故障蔓延到所有依賴它的服務,這就是熔斷。一開始的時候我們也已經寫過代碼來簡單實現熔斷了,所以直接介紹幾個重要的熔斷相關的參數:

  • circuitBreaker.enabled:是否啓用熔斷器,默認true

  • circuitBreaker.forceOpen:強制打開熔斷器,默認false。

  • circuitBreaker.forceClosed:強制關閉熔斷器,默認false。

  • circuitBreaker.errorThresholdPercentage:錯誤率,默認50%。在一段時間內,服務調用超時或者失敗率超過50%,則打開熔斷器

  • circuitBreaker.requestVolumeThreshold:默認20。意思爲在一段時間內要有20個及以上的請求才會去計算錯誤率。比如只來了19個請求,就算全失敗了,那也不算錯誤率100%

  • circuitBreaker.sleepWindowInMilliseconds:半開狀態試探睡眠時間,默認爲5000ms。也就是熔斷器打開5s後,開始半打開狀態,放出一點請求去調用服務,試探一下能否成功

來寫代碼測試一下其中幾個參數:

@HystrixCommand(groupKey = "productStockOpLog", commandKey = "addProductStockOpLog", fallbackMethod = "addProductStockOpLogFallback",
        commandProperties = {
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "400"),//指定多久超時,單位毫秒。超時進fallback
			   @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),//判斷熔斷的最少請求數,默認是10;只有在一個統計窗口內處理的請求數量達到這個閾值,纔會進行熔斷與否的判斷
                @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "10"),//判斷熔斷的閾值,默認值50,表示在一個統計窗口內有50%的請求處理失敗,會觸發熔斷
        }
)

我先設置了超時時間爲400ms。然後在服務提供接口方法改成這樣:

@Service
public class ServiceProviderImpl implements ServiceProvider {
    private int c = 0;
 
    @Override
    public Integer service() {
  
        if (c < 10) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
        return c;
    }
}

實現也很簡單,前十次調用都睡500ms,也就是說,前十次調用肯定是超時的。

而在客戶端,實現如下:

@Test
public void test() throws InterruptedException {
    for (int i = 1; i < 15; i++) {
        HystrixCommand<Integer> command = new QueryByUserIdCommand(userServiceProvider);
        Integer r = command.execute();
        String res = r == -1 ? "fallback" : "success";
        System.out.println("第"i+"次調用,結果爲"+ method);
    }
    //等待5.5s,讓熔斷器進入半打開狀態
    Thread.sleep(5500);
    for (int i = 15; i < 20; i++) {
        HystrixCommand<Integer> command = new UserByOrderIdCommand(userServiceProvider);
        Integer r = command.execute();
        String method = r == -1 ? "fallback" : "success";
        System.out.println("第"i+"次調用,結果爲"+ method);
    }
}

查看執行結果:

第1次調用,結果爲fallback
第2次調用,結果爲fallback
第3次調用,結果爲fallback
第4次調用,結果爲fallback
第5次調用,結果爲fallback
第6次調用,結果爲fallback
第7次調用,結果爲fallback
第8次調用,結果爲fallback
第9次調用,結果爲fallback
第10次調用,結果爲fallback
第11次調用,結果爲fallback
第12次調用,結果爲fallback
第13次調用,結果爲fallback
第14次調用,結果爲fallback
第15次調用,結果爲success
第16次調用,結果爲success
第17次調用,結果爲success
第18次調用,結果爲success
第19次調用,結果爲success
第20次調用,結果爲success

分析可得,1-10次觸發fallback的原因是超時,而11-14次的fallback,是因爲滿足了“請求次數到達10次”和“失敗率超過了50%”而觸發了熔斷器,所以直接快速失敗。待沉睡了5.5秒後,熔斷器進入了半打開狀態,此時熔斷器放開第15個請求去調用服務,發現成功了,此時熔斷器關閉,後續都成功了。

降級

熔斷和降級其實它們的原理都相似,都是服務調用失敗後的進行一些快速失敗措施。但它們的出發點不一樣,熔斷是爲了防止異常不擴散,保證系統的穩定性

而降級則是人爲操作。在一些流量頂峯期,爲了保證某些熱門接口的正常運作,有時候會犧牲一些非核心接口,把資源全都讓給熱點接口,這就是服務降級。每年12306搶票的時候,大家都集中搶購那幾個熱門車次的車票,而如果此時有其他用戶去查詢幾天後的非熱門車票,有可能會查不出來。這就是降級的表現,在秒殺期間,其他不參與秒殺的接口停止服務,把資源都讓給參與秒殺的接口。

所以降級的操作其實也很簡單,和前面的熔斷一樣,編寫好調用失敗的補救邏輯,然後對其他的服務直接停止運行,這樣這些接口就無法正常調用,但又不至於直接報錯,只是服務水平下降了。

@FeignClient(value = "microservicecloud-test", fallbackFactory = UserServiceFallbackFactory.class)
public interface UserService
{
    public boolean add(User user);

    public User queryUserByUserId(Long id); 
}

這裏有一個Service接口,可以看到在註解的地方給它配置了一個fallbackFactory

import com.yufeng.springcloud.entities.Dept;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.List;

@Component
public class UserServiceFallbackFactory implements FallbackFactory<UserService>
{
    @Override
    public UserService create(Throwable throwable)
    {
        return new UserService() {
            @Override
            public boolean add(User user)
            {
                return false;
            }

            @Override
            public User queryUserByUserId(Long id)
            {
                User user = new User();
                return user;
            }
        };
    }
}

這樣就大概完成了一個降級配置了。(其實也可以當熔斷來用,是吧?沒有任何問題)

Hystrix執行調用的幾個方法

Hystrix一共有四種調用方法:

execute

還記得一開始的時候,寫了一個簡單使用Hystrix的demo,其中調用的代碼是:

Integer res = new QueryUserIdCommand(userServiceProvider).execute();
log.info("result:{}", res);

調用的是execute方法,它將以同步的方式調用run方法。

那問題來了,如果我係統它以異步的方式執行怎麼辦?我還得自己封裝Callable嗎?哈,其實Hystrix已經考慮到這一點了,那就是使用queue方法

queue

這個調用也挺簡單的,我直接貼代碼

Future<Integer> future = new QueryUserIdCommand(userServiceProvider).queue();
return future.get();

雖然Future.get()方法依然是阻塞的,但是服務調用總是不阻塞了嘛~

observe

接下里又來了一個新的需求了,我需要發多個請求,並且每請求得到一個結果,就要做一下相關的處理 。這個無論用queue還是execute都不太方便。那此時我們可以選擇使用observe。如果使用observe的話,那麼就不再是使用HystrixCommand了,而是HystrixObservableCommand

public class UserServiceObserveCommand extends HystrixObservableCommand<String>{

    private RestTemplate restTemplate;

    protected HelloServiceObserveCommand(String commandGroupKey, RestTemplate restTemplate) {
        super(HystrixCommandGroupKey.Factory.asKey(commandGroupKey));
        this.restTemplate = restTemplate;
    }

    @Override
    protected Observable<String> construct() {      
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                try {
                    if (!subscriber.isUnsubscribed()){
                        System.out.println("服務調用開始!");
                        
                        String result = restTemplate.getForEntity("http://your-url1", String.class).getBody(); 
                        //觸發監聽
                        subscriber.onNext(result);
                        
                        String result1 = restTemplate.getForEntity("http://your-url2", String.class).getBody(); 
                        //觸發監聽
                        subscriber.onNext(result1);
                        subscriber.onCompleted();
                    }
                } catch (Exception e) {
                    subscriber.onError(e);
                }
            }
        });
    }
  //降級Fallback
    @Override
    protected Observable<String> resumeWithFallback() {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                try {
                    if (!subscriber.isUnsubscribed()) {
                        subscriber.onNext("fallback");
                        subscriber.onCompleted();
                    }
                } catch (Exception e) {
                    subscriber.onError(e);
                }
            }
        });
    }
}

可以看到,每調用完一個方法,就調用一次subscriber.onNext來觸發監聽。那具體的監聽實現怎麼寫呢?

    @Test
    public void test() throws ExecutionException, InterruptedException {
        UserServiceObserveCommand command = new HelloServiceObserveCommand("user",restTemplate);
        //observe調用
        Observable<String> observable = command.observe();
    //註冊監聽
        observable.subscribe(new Observer<String>() { 
        	//onCompleted監聽
            @Override
            public void onCompleted() {
                System.out.println("調用完畢!");
            }
      
            @Override
            public void onError(Throwable t) {
                t.printStackTrace();
            }
       
       //onNext監聽
            @Override
            public void onNext(String s) {
                System.out.println("調用完了一個服務!"); 
            }
        });
    }

通過以上代碼應該大概能知道observe的作用了

toObservable

observe叫做熱執行,而toObservable叫做冷執行。有啥區別呢?

observe熱執行,無需等客戶端註冊監聽,就可以直接執行call方法裏面的內容。相當於:你監聽

不監聽是你的事情,反正我該調用的都調用了,要是你不想監聽我也沒辦法(逃)

toObservable冷執行,需要等客戶端註冊監聽,纔可以執行call方法裏面的內容。相當於:你要是不監聽的話,那我就不會執行call方法,直到你註冊監聽,我纔開始執行調用

Hystrix作爲熔斷降級限流的主要功能和用法就是這些。其實Hystrix還能起到監控的功能:Hystrix-DashBorad,這個以後有機會再聊~

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