1. 程式人生 > >微服務學習筆記(4)--Hystrix服務的降級限流和熔斷

微服務學習筆記(4)--Hystrix服務的降級限流和熔斷

分散式系統中,會出現哪些問題?

分散式系統中一定會遇到的一個問題:服務雪崩效應 或者叫級聯效應
那麼什麼是服務雪崩效應呢?
在一個高度服務化的系統中,我們實現的一個業務邏輯通常會依賴多個服務,比如:商品詳情展示服務會依賴商品服務, 價格服務, 商品評論服務. 如圖所示:

呼叫三個依賴服務會共享商品詳情服務的執行緒池. 如果其中的商品評論服務不可用, 就會出現執行緒池裡所有執行緒都因等待響應而被阻塞, 從而造成服務雪崩. 如圖所示:

服務雪崩效應:因服務提供者的不可用導致服務呼叫者的不可用,並將不可用逐漸放大的過程,就叫服務雪崩效應講了什麼是服務雪崩效應

那麼為什麼服務不可用?

導致服務不可用的原因可能有幾點: 程式Bug,大流量請求,硬體故障,快取擊穿

  • 【大流量請求】:在秒殺和大促開始前,如果準備不充分,瞬間大量請求會造成服務提供者的不可用.
  • 【硬體故障】:可能為硬體損壞造成的伺服器主機宕機, 網路硬體故障造成的服務提供者的不可訪問.
  • 【快取擊穿】:一般發生在快取應用重啟, 快取失效時高併發, 所有快取被清空時,以及短時間內大量快取失效時. 大量的快取不命中, 使請求直擊後端,造成服務提供者超負荷執行,引起服務不可用.而且,在服務提供者不可用的時候,會出現重試的情況:使用者重試、程式碼邏輯重試
  • 使用者重試:在服務提供者不可用後, 使用者由於忍受不了介面上長時間的等待,會不斷重新整理頁面甚至提交表單.
  • 程式碼邏輯重試:服務呼叫端的會存在大量服務異常後的重試邏輯.
  • 這些重試最終導致:進一步加大請求流量.

那麼,歸根結底導致雪崩效應的最根本原因是:

  1. 大量請求執行緒同步等待造成的資源耗盡
  2. 當服務呼叫者使用 同步呼叫 時, 會產生大量的等待執行緒佔用系統資源. 一旦執行緒資源被耗盡,
  3. 服務呼叫者提供的服務也將處於不可用狀態, 於是服務雪崩效應產生了.

那麼知道了分散式系統中的服務雪崩效應,以及產生的原因。那麼,問題來了,怎麼解決?

解決方案
那麼解決方案有很多:如

1. 超時機制
2. 服務限流
3. 服務熔斷
4. 服務降級

超時機制

服務級聯失敗(服務雪崩效應)的最根本原因是:大量請求執行緒同步等待造成的資源耗盡那麼,在不做任何處理的情況下,服務提供者不可用會導致消費者請求執行緒強制等待,而造成系統資源耗盡,而且,既然服務提供者已經不可用了,還在作死的請求的話,是毫無意的那麼,如果我們加入超時機制,例如2s,那麼超過2s就會直接返回了,那麼這樣是不是一定程度上可以抑制消費者資源耗盡的問題。

服務限流(資源隔離)

也就是限制請求核心服務提供者的流量,使大流量攔截在核心服務之外,這樣可以更好的保證核心服務提供者不出問題,對於一些出問的服務可以限制流量訪問,只分配固定執行緒資源訪問,這樣能使整體的資源不至於被出問題的服務耗盡,進而整個系統雪崩那麼服務之間怎麼限流,怎麼資源隔離了?例如通過執行緒池+佇列的方式,通過訊號量的方式。如下圖所示, 當商品評論服務不可用時, 即使商品服務獨立分配的20個執行緒全部處於同步等待狀態,也不會影響其他依賴服務的呼叫.

服務熔斷

遠端服務不穩定或網路抖動時暫時關閉,就叫服務熔斷。舉個通俗易懂的例子,就跟我們現實生活中的“跳閘”一樣,比如說家裡有點短路了,那是不是閘會跳掉,等你把短路的問題找到並且修復後,然後你把這個閘一送,是不是整個家庭的電路又恢復了正常。這就是熔斷
器。

所以,同樣的道理,當依賴的服務有大量超時時,在讓新的請求去訪問根本沒有意義,只會無畏的消耗現有資源。比如我們設定了超時時間為1s,如果短時間內有大量請求在1s內都得不到響應,就意味著這個服務出現了異常,此時就沒有必要再讓其他的請求去訪問這個依賴了,這個時候就應該使用熔斷器避免資源浪費。

服務降級

有服務熔斷,必然要有服務降級。所謂降級,就是當某個服務熔斷之後,服務將不再被呼叫,此時客戶端可以自己準備一個本地的fallback(回退)回撥,返回一個預設值。 例如:(備用介面/快取/mock資料)這樣做,雖然服務水平下降,但好歹可用,比直接掛掉要強,當然這也要看適合的業務場景。

實戰Hystrix-降級,超時

在整個SpringCloud構建微服務的體系中,有一個提供超時機制,限流,熔斷,降級最全面
的實現:
Hystrix(豪豬)

1、引入Springcloud Hystrix依賴, 那麼在哪裡引入呢?
一定是在呼叫方來做降級,所以需要在消費者這邊引入Hystrix,也就是我們的訂單微服務方

2.在微服務的啟動上新增@EnableHystrix的註解

3.用Hystrix的註解@HystrixCommand可以更簡單的實現上面的降級邏輯直接在介面呼叫方的方法上增加註解@HystrixCommand(fallbackMethod ="findByIdFallback")

4、超時回退怎麼實現?
在使用者微服務工程(ms-provider-person)裡將PersonController的getPersonById介面增加執行等待時間,讓該介面的執行時間變長,Hystrix呼叫介面預設兩秒超時,超時後會自動執行降級方法.

實戰Hystrix-熔斷,限流

熔斷怎麼實現?

首先在使用者微服務工程(ms-provider-person)裡將PersonController的getPersonById介面增加模擬報錯程式碼

測試報錯和正常的情況,我們可以看到當報錯達到一定閾值時,會自動熔斷,閾值可以配置,如下:

hystrix.command.default.circuitBreaker.requestVolumeThreshold:

一個rolling window內最小的請求數。如果設為20,那麼當一個rolling window的時間內(比如說1個rolling window是10秒)收到19個請求,即使19個請求都失敗,也不會觸發circuit break。預設20

hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds:

 觸發短路的時間值,當該值設為5000時,則當觸發circuit break後的5000毫秒內都會拒絕request,也就是5000毫秒後才會關閉circuit。預設5000

熔斷器原理
熔斷器模式定義了熔斷器開關相互轉換的邏輯:

服務的健康狀況 = 請求失敗數 / 請求總數.
熔斷器開關由關閉到開啟的狀態轉換是通過當前服務健康狀況和設定閾值比較決定的.
1、當熔斷器開關關閉時, 請求被允許通過熔斷器. 如果當前健康狀況高於設定閾值, 開關繼續保持關閉. 如果當前健康狀況低於設定閾值, 開關則切換為開啟狀態.
2、當熔斷器開關開啟時, 請求被禁止通過.
3、當熔斷器開關處於開啟狀態, 經過一段時間後, 熔斷器會自動進入半開狀態, 這時熔斷器只允許一個請求通過. 當該請求呼叫成功時, 熔斷器恢復到關閉狀態. 若該請求失敗, 熔斷器繼續保持開啟狀態, 接下來的請求被禁止通過.熔斷器的開關能保證服務呼叫者在呼叫異常服務時, 快速返回結果, 避免大量的同步等待.並且熔斷器能在一段時間後繼續偵測請求執行結果, 提供恢復服務呼叫的可能.

限流,執行緒資源隔離怎麼實現?

首先在使用者微服務工程(ms-provider-person)裡將PersonController的getPersonById介面等待的程式碼

在訂單微服務工程中修改介面超時配置為20秒:

然後用註解配置執行緒池大小:

部分註解意思如下:

  • CommandGroupKey:配置全域性唯一標識服務分組的名稱,比如,庫存系統就是一個服務分組。當我們監控時,相同分組的服務會聚合在一起,必填選項。
  • CommandKey:配置全域性唯一標識服務的名稱,比如,庫存系統有一個獲取庫存服務,那麼就可以為這個服務起一個名字來唯一識別該服務,如果不配置,則預設是簡單類名。
  • ThreadPoolKey:配置全域性唯一標識執行緒池的名稱,相同執行緒池名稱的執行緒池是同一個,如果不配置,則預設是分組名,此名字也是執行緒池中執行緒名字的字首。
  • ThreadPoolProperties:配置執行緒池引數,coreSize配置核心執行緒池大小和執行緒池最大大小,keepAliveTimeMinutes是執行緒池中空閒執行緒生存時間(如果不進行動態配置,那麼是沒有任何作用的),maxQueueSize配置執行緒池佇列最大大小,
  • queueSizeRejectionThreshold限定當前佇列大小,即實際佇列大小由這個引數決定,通過改變queueSizeRejectionThreshold可以實現動態佇列大小調整。
  • CommandProperties:配置該命令的一些引數,如executionIsolationStrategy配置執行隔離策略,預設是使用執行緒隔離,此處我們配置為THREAD,即執行緒池隔離。

此處可以粗粒度實現隔離,也可以細粒度實現隔離,如下所示。

  • 服務分組+執行緒池:粗粒度實現,一個服務分組/系統配置一個隔離執行緒池即可,不配置執行緒池名稱或者相同分組的執行緒池名稱配置為一樣。
  • 服務分組+服務+執行緒池:細粒度實現,一個服務分組中的每一個服務配置一個隔離執行緒池,為不同的命令實現配置不同的執行緒池名稱即可。
  • 混合實現:一個服務分組配置一個隔離執行緒池,然後對重要服務單獨設定隔離執行緒池。本demo可以用Jmeter模擬多執行緒呼叫來驗證結果
  • 那麼,這個是Hystrix基於執行緒池+佇列的方式實現的限流,當然,還有另外一種,基於訊號量來實現的。那麼什麼是訊號量把,說白了就是計數器。當 計數器 + 1  > 設定的最大併發數 時,就限流,或者走降級渠道

Hystrix服務呼叫的內部邏輯

下圖為Hystrix服務呼叫的內部邏輯:

1.構建Hystrix的Command物件, 呼叫執行方法.
2.Hystrix檢查當前服務的熔斷器開關是否開啟, 若開啟, 則執行降級服務getFallback方法.
3.若熔斷器開關關閉, 則Hystrix檢查當前服務的執行緒池是否能接收新的請求, 若執行緒池已滿, 則執行降級服務getFallback方法.
4.若執行緒池接受請求, 則Hystrix開始執行服務呼叫具體邏輯run方法.
5.若服務執行失敗, 則執行降級服務getFallback方法, 並將執行結果上報Metrics更新服務健康狀況.
6.若服務執行超時, 則執行降級服務getFallback方法, 並將執行結果上報Metrics更新服務健康狀況.
7.若服務執行成功, 返回正常結果.
8.若服務降級方法getFallback執行成功, 則返回降級結果.
9.若服務降級方法getFallback執行失敗, 則丟擲異常.

Hystrix Metrics的實現

Hystrix的Metrics中儲存了當前服務的健康狀況, 包括服務呼叫總次數和服務呼叫失敗次數等. 根據Metrics的計數, 熔斷器從而能計算出當前服務的呼叫失敗率, 用來和設定的閾值比較從而決定熔斷器的狀態切換邏輯.

Feign整合Hystrix

Feign是以介面形式工作的,要如何整合Hystrix了?又是如何實現降級了?

事實上,SpringCloud預設已為Feign整合了Hystrix,只要Hystrix在專案的classpath中,Feign預設就會用斷路器包裹所有方法。(注意:從Spring Cloud Dalston開始,Feign預設是不開啟Hystrix的。

因此,如使用Dalston及以上版本請務必額外設定屬性:feign.hystrix.enabled=true,否則斷路器不會生效)

首先要新增Feign的依賴

Feign整合Hystrix的寫法見<PersonFeignClient>類,只需使用@FeignClient註解的fallback屬性就可以為指定名稱Feign客戶端新增降級方法

package com.cym.micreoserviceorder.feign;

import com.cym.micreoserviceorder.model.Person;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * Feign的fallback的測試
 * 使用@FeignClient的fallback的屬性指定回退類
 */
@FeignClient(name = "microservice-provider-user",fallback = PersonFeignClient.FeignClientFallBack.class)
public interface PersonFeignClient {

    @RequestMapping(value = "/person/getPersonById/{id}",method = RequestMethod.GET)
    public Person getPersonById(@PathVariable("id") Integer id);

    /**
     *@component把FeignClientFallBack例項化到spring容器中
     * 回退類FeignClientFallBack需要實現PersonFeignClient介面
     */
    @Component
    class  FeignClientFallBack implements PersonFeignClient{
        @Override
        public Person getPersonById(Integer id) {
            Person person = new Person();
            person.setPersonId(-1);
            person.setName("降級使用者");
            return person;
        }
    }
}

order的Controller

package com.cym.micreoserviceorder.controller;

import com.cym.micreoserviceorder.feign.PersonFeignClient;
import com.cym.micreoserviceorder.model.Person;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/order")
public class OrderController {
    private Logger logger = LoggerFactory.getLogger(OrderController.class);
    @Autowired
    private PersonFeignClient personFeignClient;
    @RequestMapping("/getPersonById/{id}")
    public Person getPersonById(@PathVariable("id") Integer id){
        logger.info("==========使用者請求中心=============");
        return personFeignClient.getPersonById(id);
    }


}

啟動類

測試

先啟動Eureka的服務類

然後啟動服務提供者Persson類

最後啟動Order類

我們先看看正常情況下是否能正常order的微服務訪問person的微服務

然後我們把服務提供者的微服務關了看看。Hystrix的降級是否起作用

Feign禁用Hystrix

SpringCloud為Feign預設整合了Hystrix,也就是說只要Hystrix在專案的classpath中,Feign就會使用斷路器包裹Feign客戶端的所有方法(Dalston及以上版本預設Feign不開啟Hystrix)。這樣雖然方便,但有的場景並不需要該功能,如何為Feign禁用Hystrix呢?

全域性禁用Hystrix
只需在application.yml中配置feign.hystrix.enabled=false即可
為指定Feign客戶端禁用Hystrix:
增加< FeignDisableHystrixConfiguration >類

package com.cym.micreoserviceorder.feign;

import feign.Feign;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

/**
 * 禁用Hystrix的功能
 */
@Configuration
public class FeignDisableHystrixConfiguration {

    @Scope("prototype")
    @Bean
    public Feign.Builder feignBuider(){
        return Feign.builder();
    }
}

然後在FeignClient註解里加上configuration的屬性配置,見下圖:

測試: