Spring Cloud Hystrix:服務容錯保護

jerrysun發表於2021-09-09

SpringBoot實戰電商專案mall(20k+star)地址:

摘要

Spring Cloud Hystrix 是Spring Cloud Netflix 子專案的核心元件之一,具有服務容錯及執行緒隔離等一系列服務保護功能,本文將對其用法進行詳細介紹。

Hystrix 簡介

在微服務架構中,服務與服務之間透過遠端呼叫的方式進行通訊,一旦某個被呼叫的服務發生了故障,其依賴服務也會發生故障,此時就會發生故障的蔓延,最終導致系統癱瘓。Hystrix實現了斷路器模式,當某個服務發生故障時,透過斷路器的監控,給呼叫方返回一個錯誤響應,而不是長時間的等待,這樣就不會使得呼叫方由於長時間得不到響應而佔用執行緒,從而防止故障的蔓延。Hystrix具備服務降級、服務熔斷、執行緒隔離、請求快取、請求合併及服務監控等強大功能。

建立一個hystrix-service模組

這裡我們建立一個hystrix-service模組來演示hystrix的常用功能。

在pom.xml中新增相關依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

在application.yml進行配置

主要是配置了埠、註冊中心地址及user-service的呼叫路徑。

server:
  port: 8401
spring:
  application:
    name: hystrix-service
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: https://localhost:8001/eureka/
service-url:
  user-service: https://user-service

在啟動類上新增@EnableCircuitBreaker來開啟Hystrix的斷路器功能

@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class HystrixServiceApplication {

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

建立UserHystrixController介面用於呼叫user-service服務

服務降級演示

  • 在UserHystrixController中新增用於測試服務降級的介面:
@GetMapping("/testFallback/{id}")
public CommonResult testFallback(@PathVariable Long id) {
    return userService.getUser(id);
}
  • 在UserService中新增呼叫方法與服務降級方法,方法上需要新增@HystrixCommand註解:
@HystrixCommand(fallbackMethod = "getDefaultUser")
public CommonResult getUser(Long id) {
    return restTemplate.getForObject(userServiceUrl + "/user/{1}", CommonResult.class, id);
}

public CommonResult getDefaultUser(@PathVariable Long id) {
    User defaultUser = new User(-1L, "defaultUser", "123456");
    return new CommonResult<>(defaultUser);
}
  • 啟動eureka-server、user-service、hystrix-service服務;

圖片描述

  • 呼叫介面進行測試:

圖片描述

  • 關閉user-service服務重新測試該介面,發現已經發生了服務降級:

圖片描述

@HystrixCommand詳解

@HystrixCommand中的常用引數

  • fallbackMethod:指定服務降級處理方法;
  • ignoreExceptions:忽略某些異常,不發生服務降級;
  • commandKey:命令名稱,用於區分不同的命令;
  • groupKey:分組名稱,Hystrix會根據不同的分組來統計命令的告警及儀表盤資訊;
  • threadPoolKey:執行緒池名稱,用於劃分執行緒池。

設定命令、分組及執行緒池名稱

  • 在UserHystrixController中新增測試介面:
@GetMapping("/testCommand/{id}")
public CommonResult testCommand(@PathVariable Long id) {
    return userService.getUserCommand(id);
}
  • 在UserService中新增方式實現功能:
 @HystrixCommand(fallbackMethod = "getDefaultUser",
    commandKey = "getUserCommand",
    groupKey = "getUserGroup",
    threadPoolKey = "getUserThreadPool")
public CommonResult getUserCommand(@PathVariable Long id) {
    return restTemplate.getForObject(userServiceUrl + "/user/{1}", CommonResult.class, id);
 }

使用ignoreExceptions忽略某些異常降級

  • 在UserHystrixController中新增測試介面:
@GetMapping("/testException/{id}")
public CommonResult testException(@PathVariable Long id) {
    return userService.getUserException(id);
}
  • 在UserService中新增實現方法,這裡忽略了NullPointerException,當id為1時丟擲IndexOutOfBoundsException,id為2時丟擲NullPointerException:
@HystrixCommand(fallbackMethod = "getDefaultUser2", ignoreExceptions = {NullPointerException.class})
public CommonResult getUserException(Long id) {
    if (id == 1) {
        throw new IndexOutOfBoundsException();
    } else if (id == 2) {
        throw new NullPointerException();
    }
    return restTemplate.getForObject(userServiceUrl + "/user/{1}", CommonResult.class, id);
}

public CommonResult getDefaultUser2(@PathVariable Long id, Throwable e) {
    LOGGER.error("getDefaultUser2 id:{},throwable class:{}", id, e.getClass());
    User defaultUser = new User(-2L, "defaultUser2", "123456");
    return new CommonResult<>(defaultUser);
}
  • 呼叫介面進行測試:

圖片描述

  • 呼叫介面進行測試:

圖片描述

Hystrix的請求快取

當系統併發量越來越大時,我們需要使用快取來最佳化系統,達到減輕併發請求執行緒數,提供響應速度的效果。

相關注解

  • @CacheResult:開啟快取,預設所有引數作為快取的key,cacheKeyMethod可以透過返回String型別的方法指定key;
  • @CacheKey:指定快取的key,可以指定引數或指定引數中的屬性值為快取key,cacheKeyMethod還可以透過返回String型別的方法指定;
  • @CacheRemove:移除快取,需要指定commandKey。

測試使用快取

  • 在UserHystrixController中新增使用快取的測試介面,直接呼叫三次getUserCache方法:
@GetMapping("/testCache/{id}")
public CommonResult testCache(@PathVariable Long id) {
    userService.getUserCache(id);
    userService.getUserCache(id);
    userService.getUserCache(id);
    return new CommonResult("操作成功", 200);
}
  • 在UserService中新增具有快取功能的getUserCache方法:
@CacheResult(cacheKeyMethod = "getCacheKey")
@HystrixCommand(fallbackMethod = "getDefaultUser", commandKey = "getUserCache")
    public CommonResult getUserCache(Long id) {
    LOGGER.info("getUserCache id:{}", id);
    return restTemplate.getForObject(userServiceUrl + "/user/{1}", CommonResult.class, id);
}

/**
 * 為快取生成key的方法
 */
public String getCacheKey(Long id) {
    return String.valueOf(id);
}
  • 呼叫,這個介面中呼叫了三次getUserCache方法,但是隻列印了一次日誌,說明有兩次走的是快取:

圖片描述

測試移除快取

  • 在UserHystrixController中新增移除快取的測試介面,呼叫一次removeCache方法:
@GetMapping("/testRemoveCache/{id}")
public CommonResult testRemoveCache(@PathVariable Long id) {
    userService.getUserCache(id);
    userService.removeCache(id);
    userService.getUserCache(id);
    return new CommonResult("操作成功", 200);
}
  • 在UserService中新增具有移除快取功能的removeCache方法:
@CacheRemove(commandKey = "getUserCache", cacheKeyMethod = "getCacheKey")
@HystrixCommand
public CommonResult removeCache(Long id) {
    LOGGER.info("removeCache id:{}", id);
    return restTemplate.postForObject(userServiceUrl + "/user/delete/{1}", null, CommonResult.class, id);
}
  • 呼叫,可以發現有兩次查詢都走的是介面:

圖片描述

快取使用過程中的問題

  • 在快取使用過程中,我們需要在每次使用快取的請求前後對HystrixRequestContext進行初始化和關閉,否則會出現如下異常:
java.lang.IllegalStateException: Request caching is not available. Maybe you need to initialize the HystrixRequestContext?
	at com.netflix.hystrix.HystrixRequestCache.get(HystrixRequestCache.java:104) ~[hystrix-core-1.5.18.jar:1.5.18]
	at com.netflix.hystrix.AbstractCommand$7.call(AbstractCommand.java:478) ~[hystrix-core-1.5.18.jar:1.5.18]
	at com.netflix.hystrix.AbstractCommand$7.call(AbstractCommand.java:454) ~[hystrix-core-1.5.18.jar:1.5.18]
  • 這裡我們透過使用過濾器,在每個請求前後初始化和關閉HystrixRequestContext來解決該問題:
/**
 * Created by macro on 2019/9/4.
 */
@Component
@WebFilter(urlPatterns = "/*",asyncSupported = true)
public class HystrixRequestContextFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        try {
            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            context.close();
        }
    }
}

請求合併

微服務系統中的服務間通訊,需要透過遠端呼叫來實現,隨著呼叫次數越來越多,佔用執行緒資源也會越來越多。Hystrix中提供了@HystrixCollapser用於合併請求,從而達到減少通訊消耗及執行緒數量的效果。

@HystrixCollapser的常用屬性

  • batchMethod:用於設定請求合併的方法;
  • collapserProperties:請求合併屬性,用於控制例項屬性,有很多;
  • timerDelayInMilliseconds:collapserProperties中的屬性,用於控制每隔多少時間合併一次請求;

功能演示

  • 在UserHystrixController中新增testCollapser方法,這裡我們先進行兩次服務呼叫,再間隔200ms以後進行第三次服務呼叫:
@GetMapping("/testCollapser")
public CommonResult testCollapser() throws ExecutionException, InterruptedException {
    Future<User> future1 = userService.getUserFuture(1L);
    Future<User> future2 = userService.getUserFuture(2L);
    future1.get();
    future2.get();
    ThreadUtil.safeSleep(200);
    Future<User> future3 = userService.getUserFuture(3L);
    future3.get();
    return new CommonResult("操作成功", 200);
}
  • 使用@HystrixCollapser實現請求合併,所有對getUserFuture的的多次呼叫都會轉化為對getUserByIds的單次呼叫:
@HystrixCollapser(batchMethod = "getUserByIds",collapserProperties = {
    @HystrixProperty(name = "timerDelayInMilliseconds", value = "100")
})
public Future<User> getUserFuture(Long id) {
    return new AsyncResult<User>(){
    @Override
    public User invoke() {
        CommonResult commonResult = restTemplate.getForObject(userServiceUrl + "/user/{1}", CommonResult.class, id);
        Map data = (Map) commonResult.getData();
        User user = BeanUtil.mapToBean(data,User.class,true);
        LOGGER.info("getUserById username:{}", user.getUsername());
        return user;
        }
    };
}

@HystrixCommand
public List<User> getUserByIds(List<Long> ids) {
    LOGGER.info("getUserByIds:{}", ids);
    CommonResult commonResult = restTemplate.getForObject(userServiceUrl + "/user/getUserByIds?ids={1}", CommonResult.class, CollUtil.join(ids,","));
    return (List<User>) commonResult.getData();
}
  • 訪問,由於我們設定了100毫秒進行一次請求合併,前兩次被合併,最後一次自己單獨合併了。

圖片描述

Hystrix的常用配置

全域性配置

hystrix:
  command: #用於控制HystrixCommand的行為
    default:
      execution:
        isolation:
          strategy: THREAD #控制HystrixCommand的隔離策略,THREAD->執行緒池隔離策略(預設),SEMAPHORE->訊號量隔離策略
          thread:
            timeoutInMilliseconds: 1000 #配置HystrixCommand執行的超時時間,執行超過該時間會進行服務降級處理
            interruptOnTimeout: true #配置HystrixCommand執行超時的時候是否要中斷
            interruptOnCancel: true #配置HystrixCommand執行被取消的時候是否要中斷
          timeout:
            enabled: true #配置HystrixCommand的執行是否啟用超時時間
          semaphore:
            maxConcurrentRequests: 10 #當使用訊號量隔離策略時,用來控制併發量的大小,超過該併發量的請求會被拒絕
      fallback:
        enabled: true #用於控制是否啟用服務降級
      circuitBreaker: #用於控制HystrixCircuitBreaker的行為
        enabled: true #用於控制斷路器是否跟蹤健康狀況以及熔斷請求
        requestVolumeThreshold: 20 #超過該請求數的請求會被拒絕
        forceOpen: false #強制開啟斷路器,拒絕所有請求
        forceClosed: false #強制關閉斷路器,接收所有請求
      requestCache:
        enabled: true #用於控制是否開啟請求快取
  collapser: #用於控制HystrixCollapser的執行行為
    default:
      maxRequestsInBatch: 100 #控制一次合併請求合併的最大請求數
      timerDelayinMilliseconds: 10 #控制多少毫秒內的請求會被合併成一個
      requestCache:
        enabled: true #控制合併請求是否開啟快取
  threadpool: #用於控制HystrixCommand執行所線上程池的行為
    default:
      coreSize: 10 #執行緒池的核心執行緒數
      maximumSize: 10 #執行緒池的最大執行緒數,超過該執行緒數的請求會被拒絕
      maxQueueSize: -1 #用於設定執行緒池的最大佇列大小,-1採用SynchronousQueue,其他正數採用LinkedBlockingQueue
      queueSizeRejectionThreshold: 5 #用於設定執行緒池佇列的拒絕閥值,由於LinkedBlockingQueue不能動態改版大小,使用時需要用該引數來控制執行緒數

例項配置

例項配置只需要將全域性配置中的default換成與之對應的key即可。

hystrix:
  command:
    HystrixComandKey: #將default換成HystrixComrnandKey
      execution:
        isolation:
          strategy: THREAD
  collapser:
    HystrixCollapserKey: #將default換成HystrixCollapserKey
      maxRequestsInBatch: 100
  threadpool:
    HystrixThreadPoolKey: #將default換成HystrixThreadPoolKey
      coreSize: 10

配置檔案中相關key的說明

  • HystrixComandKey對應@HystrixCommand中的commandKey屬性;
  • HystrixCollapserKey對應@HystrixCollapser註解中的collapserKey屬性;
  • HystrixThreadPoolKey對應@HystrixCommand中的threadPoolKey屬性。

使用到的模組

springcloud-learning
├── eureka-server -- eureka註冊中心
├── user-service -- 提供User物件CRUD介面的服務
└── hystrix-service -- hystrix服務呼叫測試服務

專案原始碼地址

關於作者

macrozheng 【id:macrozheng】
專注Java技術分享,全套學習教程連載中,作者Github專案mall(20k+star)

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/151/viewspace-2823843/,如需轉載,請註明出處,否則將追究法律責任。

相關文章