1. 程式人生 > >Spring-cloud 微服務架構搭建 03 - Hystrix 深入理解與配置使用

Spring-cloud 微服務架構搭建 03 - Hystrix 深入理解與配置使用

文章目錄

1. hystrix簡介

分散式的服務系統中,出現服務宕機是常有的事情,hystrix提供的客戶端彈性模式設計

可以快速失敗客戶端,保護遠端資源,防止服務消費進行“上游”傳播。

Hystrix庫是高度可配置的,可以讓開發人員嚴格控制使用它定義的斷路器模式和艙壁模式 的行為 。 開發人員可以通過修改 Hystrix 斷路器的配置,控制 Hystrix 在超時遠端呼叫之前需要等 待的時間 。 開發人員還可以控制 Hystrix 斷路器何時跳閘以及 Hystrix何時嘗試重置斷路器 。

使用 Hystrix, 開發人員還可以通過為每個遠端服務呼叫定義單獨的執行緒組,然後為每個執行緒組配置相應的執行緒數來微調艙壁實現。 這允許開發人員對遠端服務呼叫進行微調,因為某些遠 程資源呼叫具有較高的請求量 。

客戶端彈性模式?有以下四點:

  1. 客戶端負載均衡模式,由ribbon模組提供;
  2. 斷路器模式(circuit breaker);
  3. 快速失敗模式(fallback);
  4. 艙壁模式(bulkhead);
  • 下面我們通過Feign-service(Feign結合Hystrix)模組和Demo-dervice(上篇文章的基礎服務模組)對hystix元件進行功能測試和理解。

2. hystrix-service 模組快速搭建

注:本文專案採用idea工具進行搭建

  • 使用idea自身的spring initializr進行專案的初始化,專案名為:feign-service,其主要測試基於feign的遠端呼叫,也有restTemplate的測試;
  • 初始化完成專案之後進行pom檔案匯入
<!-- config-server 啟動引入 -->
<!-- Eureka客戶端啟動類 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- feign 啟動類 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
  • 修改application.yml檔案,新增如下配置:
management:
  endpoints:
    web:
      exposure:
        include: "*"  # 暴露所有服務監控埠,也可以只暴露 hystrix.stream埠
  endpoint:
    health:
      show-details: ALWAYS
# feign配置
feign:
  compression:
    request:
      enabled:  true  #開啟請求壓縮功能
      mime-types: text/xml;application/xml;application/json #指定壓縮請求資料型別
      min-request-size: 2048  #如果傳輸超過該位元組,就對其進行壓縮
    response:
    #開啟響應壓縮功能
      enabled:  true
  hystrix:
    # 在feign中開啟hystrix功能,預設情況下feign不開啟hystrix功能
  • 修改bootstrap.yml檔案,連結eureka-config,新增如下配置:
# 指定服務註冊中心的位置。
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  instance:
    hostname: localhost
    preferIpAddress: true
  • 最後修改服務啟動類:
@ServletComponentScan
@EnableFeignClients
@SpringCloudApplication
public class FeignServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(FeignServiceApplication.class, args);
    }

    /**
     * 設定feign遠端呼叫日誌級別
     * Logger.Level有如下幾種選擇:
     * NONE, 不記錄日誌 (預設)。
     * BASIC, 只記錄請求方法和URL以及響應狀態程式碼和執行時間。
     * HEADERS, 記錄請求和應答的頭的基本資訊。
     * FULL, 記錄請求和響應的頭資訊,正文和元資料。
     */
    @Bean
    Logger.Level feignLoggerLevel(){
        return Logger.Level.HEADERS;
    }

    /**
     * 引入restTemplate負載均衡
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

最後新增遠端呼叫客戶端介面如下:

/**
 * feign 註解來繫結該介面對應 demo-service 服務
 * name 為其它服務的服務名稱
 * fallback 為熔斷後的回撥
 */
@FeignClient(value = "demo-service",
//        configuration = DisableHystrixConfiguration.class, // 區域性關閉斷路器
        fallback = DemoServiceHystrix.class)
public interface DemoClient {

    @GetMapping(value = "/test/hello")
    ResultInfo hello();

    @GetMapping(value = "/test/{id}",consumes = "application/json")
    ResultInfo getTest(@PathVariable("id") Integer id);

    @PostMapping(value = "/test/add")
    ResultInfo addTest(Test test);

    @PutMapping(value = "/test/update")
    ResultInfo updateTest(Test test);

    @GetMapping(value = "/test/collapse/{id}")
    Test collapse(@PathVariable("id") Integer id);

    @GetMapping(value = "/test/collapse/findAll")
    List<Test> collapseFindAll(@RequestParam(value = "ids") List<Integer> ids);
}

/**
 * restTemplate 客戶端
*/
@Component
public class RestClient {

    @Autowired
    RestTemplate restTemplate;

    @HystrixCommand
    public ResultInfo getTest(Integer id){
        log.info(">>>>>>>>> 進入restTemplate 方法呼叫 >>>>>>>>>>>>");
        ResponseEntity<ResultInfo> restExchange =
                restTemplate.exchange(
                        "http://demo-service/test/{id}",
                        HttpMethod.GET,
                        null, ResultInfo.class, id);
        return restExchange.getBody();
    }
}
  • 新增測試介面
@Log4j2
@RestController
@RequestMapping("/test")
public class FeignController {

    @Autowired
    private DemoClient demoClient;

    @Autowired
    private RestClient restClient;

    @HystrixCommand
    @GetMapping("/feign/{id}")
    public ResultInfo testFeign(@PathVariable("id") Integer id){
        log.info("使用feign進行遠端服務呼叫測試。。。");
        ResultInfo test = demoClient.getTest(id);
        log.info("服務呼叫獲取的資料為: " + test);
        /**
         * hystrix 預設呼叫超時時間為1秒
         * 此處需要配置 fallbackMethod 屬性才會生效
         */
        //log.info("服務延時:" + randomlyRunLong() + " 秒");
        return test;
    }

    @HystrixCommand
    @GetMapping("/rest/{id}")
    public ResultInfo testRest(@PathVariable("id") Integer id){
        log.info("使用restTemplate進行遠端服務呼叫測試。。。");
        return restClient.getTest(id);
    }
  • 到此配置完成hystrix,可以啟動進行測試使用,測試遠端呼叫的服務例項為demo-service,通過postman或者其他工具我們可以發現配置很完美,下面我們分別介紹hystrix的具體配置和使用。

3. hystrix 回退機制

  • 回退機制也叫後備機制,就是在我們的服務呼叫不可達或者服務呼叫超時失敗的情況下的後備操作。有兩種fallback定義方式:
  1. feign的@FeignClient中定義fallback屬性,定義一個實現c次client介面的類。
  2. @HystrixCommand(fallbackMethod = “buildFallbackMethod”)方式;
  • 我們使用服務呼叫延時的機制處理如下:
@HystrixCommand(
            // 開啟此項 feign呼叫的回退處理會直接呼叫此方法
            fallbackMethod = "buildFallbacktestFeign",
    )
    @GetMapping("/feign/{id}")
    public ResultInfo testFeign(@PathVariable("id") Integer id){
        ... 省略程式碼
        /**
         * hystrix 預設呼叫超時時間為1秒
         * 此處需要配置 fallbackMethod 屬性才會生效
         */
        log.info("服務延時:" + randomlyRunLong() + " 秒");
        return test;
    }
        ... 省略程式碼
    /**
     * testFeign 後備方法
     * @return
     */
    private ResultInfo buildFallbacktestFeign(Integer id){
        return ResultUtil.success("testFeign 介面後備處理,引數為: " + id );
    }
    // 模擬服務呼叫延時
    private Integer randomlyRunLong(){
        Random rand = new Random();
        int randomNum = rand.nextInt(3) + 1;
        if (randomNum==3) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return randomNum;
    }
  • 如上我們在呼叫testfeign介面時,當隨機數為3時會進行執行緒的休眠,那麼會超過hystrix 預設呼叫超時時間,介面返回後臺方法buildFallbacktestFeign的返回值。

注意:我們在回退方法中進行遠端介面的呼叫時,也需要使用@HystrixCommand進行包裹,不然出現問題會吃大虧。

4. hystrix 執行緒池隔離和引數微調

  • 執行緒池隔離可以全域性設定也可以在@HystrixCommand中下架如下引數進行配置,如果需要動態配置可以利用aop進行配置,配置引數如下:
// 以下為 艙壁模式配置配置單獨的執行緒池
threadPoolKey = "test",
threadPoolProperties = {
        @HystrixProperty(name = "coreSize",value="30"),
        @HystrixProperty(name="maxQueueSize", value="10")},
// 以下為斷路器相關配置 可以根據系統對引數進行微調
commandProperties={
        // 設定hystrix遠端服務呼叫超時時間,一般不建議修改
//  @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="4000"),
        // 請求必須達到以下引數以上才有可能觸發,也就是10秒內發生連續呼叫的最小引數
        @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"),
        // 請求到達requestVolumeThreshold 上限以後,呼叫失敗的請求百分比
        @HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75"),
        // 斷路由半開後進入休眠的時間,期間可以允許少量服務通過
        @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="7000"),
        // 斷路器監控時間 預設10000ms
        @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="15000"),
        // timeInMilliseconds的整數倍,此處設定越高,cpu佔用資源越多我
        @HystrixProperty(name="metrics.rollingStats.numBuckets", value="5")}
  • 除了在方法中新增為,還可以在類上進行類的全域性控制
// 類級別屬性配置
@DefaultProperties(
    commandProperties={
            // 請求必須達到以下引數以上才有可能觸發,也就是10秒內發生連續呼叫的最小引數
            @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"),
            // 請求到達requestVolumeThreshold 上限以後,呼叫失敗的請求百分比
            @HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75")}
)

5. hystrix 快取配置

Hystrix請求快取不是隻寫入一次結果就不再變化的,而是每次請求到達Controller的時候,我們都需要為HystrixRequestContext進行初始化,之前的快取也就是不存在了,我們是在同一個請求中保證結果相同,同一次請求中的第一次訪問後對結果進行快取,快取的生命週期只有一次請求!與使用redis進行url快取的模式不同
測試程式碼如下:

@RestController
@RequestMapping("/cache")
public class CacheTestController {
    @Autowired
    private CacheService cacheService;
    @GetMapping("/{id}")
    public ResultInfo testCache(@PathVariable("id") Integer id){
        // 查詢快取資料
        log.info("第一次查詢: "+cacheService.testCache(id));
        // 再次查詢 檢視日誌是否走快取
        log.info("第二次查詢: "+cacheService.testCache(id));
        // 更新資料
        cacheService.updateCache(new Test(id,"wangwu","121"));
        // 再次查詢 檢視日誌是否走快取,不走快取則再次快取
        log.info("第二次查詢: "+cacheService.testCache(id));
        // 再次查詢 檢視日誌是否走快取
        log.info("第二次查詢: "+cacheService.testCache(id));
        return ResultUtil.success("cache 測試完畢!!!");
    }
}

@Service
public class CacheService {
    @Autowired
    private DemoClient demoClient;
    
    /**
     * commandKey 指定命令名稱
     * groupKey 分組
     * threadPoolKey 執行緒池分組
     *
     * CacheResult 設定請求具有快取
     *  cacheKeyMethod 指定請求快取key值設定方法 優先順序大於 @CacheKey() 的方式
     *
     * CacheKey() 也是指定快取key值,優先順序較低
     * CacheKey("id") Integer id 出現異常,測試CacheKey()讀取物件屬性進行key設定
     *  java.beans.IntrospectionException: Method not found: isId
     *
     * 直接使用以下配置會出現異常:
     *   java.lang.IllegalStateException: Request caching is not available. Maybe you need to initialize the HystrixRequestContext?
     *
     * 原因:請求快取不是隻寫入一次結果就不再變化的,而是每次請求到達Controller的時候,我們都需要為
     *      HystrixRequestContext進行初始化,之前的快取也就是不存在了,我們是在同一個請求中保證
     *      結果相同,同一次請求中的第一次訪問後對結果進行快取,快取的生命週期只有一次請求!
     *      與使用redis進行url快取的模式不同。
     * 因此,我們需要做過濾器進行HystrixRequestContext初始化。
     */
    @CacheResult(cacheKeyMethod = "getCacheKey")
    @HystrixCommand(commandKey = "testCache", groupKey = "CacheTestGroup", threadPoolKey = "CacheTestThreadPool")
    public ResultInfo testCache(Integer id){
        log.info("test cache 服務呼叫測試。。。");
        return demoClient.getTest(id);
    }
    
    /**
     * 這裡有兩點要特別注意:
     * 1、這個方法的入參的型別必須與快取方法的入參型別相同,如果不同被呼叫會報這個方法找不到的異常,
     *    等同於fallbackMethod屬性的使用;
     * 2、這個方法的返回值一定是String型別,報出如下異常:
     *    com.netflix.hystrix.contrib.javanica.exception.HystrixCachingException:
     *            return type of cacheKey method must be String.
     */
    private String getCacheKey(Integer id){
        log.info("進入獲取快取key方法。。。");
        return String.valueOf(id);
    }

    @CacheRemove(commandKey = "testCache")
    @HystrixCommand(commandKey = "updateCache", groupKey = "CacheTestGroup", threadPoolKey = "CacheTestThreadPool")
    public ResultInfo updateCache(@CacheKey("id") Test test){
        log.info("update cache 服務呼叫測試。。。");
        return demoClient.updateTest(test);
    }
}
  • 使用postman進行測試呼叫http://localhost:8090/cache/1;可以發現demo-service服務響應了兩次,說明在第一次快取chen成功,update後刪除了快取。測試過程中遇到的問題已經註釋,讀者可以自行測試;

6. hystrix 異常丟擲處理

  • @HystrixCommandignoreExceptions屬性會將忽略的異常包裝成HystrixBadRequestException,從而不執行回撥.
@RestController
@RequestMapping("/exception")
public class ExceptionTestController {

    @Autowired
    private DemoClient demoClient;

    /**
     * ignoreExceptions 屬性會將RuntimeException包裝
     *   成HystrixBadRequestException,從而不執行回撥.
     */
    @HystrixCommand(ignoreExceptions = {RuntimeException.class},
                    fallbackMethod = "buildFallbackTestException")
    @GetMapping("/{id}")
    public ResultInfo testException(@PathVariable("id") Integer id){
        log.info("test exception 服務呼叫異常丟擲測試。。。");
        if (id == 1){
            throw new RuntimeException("測試服務呼叫異常");
        }
        return demoClient.getTest(id);
    }

    /**
     * testFeign 後備方法
     * @return
     */
    private ResultInfo buildFallbackTestException(Integer id){
        return ResultUtil.success("testException 介面後備處理,引數為: " + id );
    }
}
  • 介面呼叫http://localhost:8090/exception/1 服務丟擲異常,介面接收到異常資訊;如果去除ignoreExceptions = {RuntimeException.class},再次呼叫介面,發現執行了buildFallbackTestException回退方法。

8. hystrix 請求合併

注意:請求合併方法本身時高延遲的命令,對於一般請求延遲低的服務需要考慮延遲時間合理化以及延遲時間窗內的併發量

  • 請求合併的測試
@RestController
@RequestMapping("/collapse")
public class CollapseController {

    @Autowired
    private CollapseService collapseService;

    @GetMapping("/{id}")
    public ResultInfo testRest(@PathVariable("id") Integer id){
        log.info("進行 Collapse 遠端服務呼叫測試,開始時間: " + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        Test test = collapseService.testRest(id);
        log.info("進行 Collapse 遠端服務呼叫測試,結束時間: " + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        /**
         * 啟用請求合併:
         *      開始時間: 2018-10-18T10:40:12.374
         *      結束時間: 2018-10-18T10:40:13.952
         * 不使用請求合併:
         *      開始時間: 2018-10-18T10:43:41.472
         *      結束時間: 2018-10-18T10:43:41.494
         */
        return ResultUtil.success(test);
    }
}

@Service
public class CollapseService {

    @Autowired
    private DemoClient demoClient;

    @HystrixCollapser(
            // 指定請求合併的batch方法
            batchMethod = "findAll",
            collapserProperties = {
                    // 請求合併時間窗為 100ms ,需要根據請求的延遲等進行綜合判斷進行設定
                    @HystrixProperty(name = "timerDelayInMilliseconds", value = "1000")
            })
    public Test testRest(Integer id){
        Test test = demoClient.collapse(id);
        return test;
    }

    // batch method must be annotated with HystrixCommand annotation
    @HystrixCommand
    private List<Test> findAll(List<Integer> ids){
        log.info("使用 findAll 進行遠端服務 Collapse 呼叫測試。。。");

        return demoClient.collapseFindAll(ids);
    }
}

-呼叫介面 http://localhost:8090/collapse/1 ,可以使用併發測試工具進行測試,比如jmeter;返回以上註釋資訊結果,說明我們在設定這些引數需要進行多方面的測試。

9. Hystrix ThreadLocal上下文的傳遞

具體內容可以參考下面的參考博文,也可以下載我github程式碼進行測試。

注意:配置ThreadLocal上下文的傳遞之後,我們在回過頭測試hystrix的cache測試,發現清理快取的功能失效了。希望有思路的博友可以提供建議,謝謝

本文github程式碼地址:
我的github:spring-cloud 基礎模組搭建 ---- 歡迎指正

參考博文:
SpringCloud (八) Hystrix 請求快取的使用
Hystrix實現ThreadLocal上下文的傳遞