Spring Cloud Hystrix:服務容錯保護

  • 2019 年 10 月 5 日
  • 筆記

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: http://localhost:8001/eureka/  service-url:    user-service: http://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服務;
  • 調用介面進行測試:http://localhost:8401/user/testFallback/1
  • 關閉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);  }  
  • 調用介面進行測試:http://localhost:8401/user/tesException/1
  • 調用介面進行測試:http://localhost:8401/user/tesException/1

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);  }  
  • 調用介面測試http://localhost:8401/user/testCache/1,這個介面中調用了三次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);  }  
  • 調用介面測試http://localhost:8401/user/testRemoveCache/1,可以發現有兩次查詢都走的是介面:

快取使用過程中的問題

  • 在快取使用過程中,我們需要在每次使用快取的請求前後對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();  }  
  • 訪問介面測試http://localhost:8401/user/testCollapser,由於我們設置了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服務調用測試服務  

項目源碼地址

https://github.com/macrozheng/springcloud-learning

Tags: