分散式系統的延時和故障容錯之Spring Cloud Hystrix

  • 2019 年 10 月 11 日
  • 筆記

本示例主要介紹 Spring Cloud 系列中的 Eureka,如何使用Hystrix熔斷器容錯保護我們的應用程式。

在微服務架構中,系統被拆分成很多個服務單元,各個服務單元的應用通過 HTTP 相互調用、依賴,在某個服務由於網路或其他原因自身出現故障、延遲時,調用方也會出現延遲。若調用方請求不斷增加,可能會形成任務積壓,最終導致調用方服務癱瘓,服務不可用現象逐漸放大。

解決方案

Spring Cloud Hystrix 是一個專用於服務熔斷處理的開源項目,實現了一系列服務保護措施,當依賴的服務方出現故障不可用時,hystrix實現服務降級、服務熔斷等功能,對延遲和故障提供強大的容錯能力,從而防止故障進一步擴大。

Hystrix 主要作用介紹

  • 保護和控制底層服務的高延遲和失效對上層服務的影響。
  • 避免複雜分散式中服務失效的雪崩效應。在大型的分散式系統中,存在各種複雜的依賴關係。如果某個服務失效,很可能會對其他服務造成影響,形成連鎖反應。
  • 快速失效和迅速恢復。以Spring為例,一般在實現controller的時候,都會以同步的邏輯調用依賴的服務。如果服務失效,而且沒有客戶端失效機制,就會導致請求長時間的阻塞。如果不能快速的發現失效,而就很難通過高可用機制或者負載均衡實現迅速的恢復。
  • 實現服務降級。這一點是從用戶體驗來考慮的,一個預定義默認返回會比請求卡死或者500好很多。
  • 實現了服務監控、報警和運維控制。Hystrix Dashboard和Turbine可以配合Hystrix完成這些功能。

Hystrix 主要特性:

  • 服務熔斷

    Hystrix 會記錄各個服務的請求資訊,通過 成功、失敗、拒絕、超時 等統計資訊判斷是否打開斷路器,將某個服務的請求進行熔斷。一段時間後切換到半開路狀態,如果後面的請求正常則關閉斷路器,否則繼續打開斷路器。

  • 服務降級

    服務降級是請求失敗時的後備方法,故障時執行降級邏輯。

  • 執行緒隔離

    Hystrix 通過執行緒池實現資源的隔離,確保對某一服務的調用在出現故障時不會對其他服務造成影響。

程式碼實現

創建三個項目來完成示例,分別為:服務註冊中心hystrix-eureka-server,服務提供者hystrix-service-provider,服務消費者hystrix-service-consumer

1.創建hystrix-eureka-server服務註冊中心

pom.xml配置

<?xml version="1.0" encoding="UTF-8"?>  <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"           xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">      <modelVersion>4.0.0</modelVersion>        <groupId>com.easy</groupId>      <artifactId>hystrix-eureka-server</artifactId>      <version>0.0.1-SNAPSHOT</version>      <packaging>jar</packaging>        <name>hystrix-eureka-server</name>      <description>Demo project for Spring Boot</description>        <parent>          <artifactId>cloud-hystrix</artifactId>          <groupId>com.easy</groupId>          <version>1.0.0</version>      </parent>        <dependencies>          <dependency>              <groupId>org.springframework.cloud</groupId>              <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>          </dependency>          <dependency>              <groupId>org.springframework.boot</groupId>              <artifactId>spring-boot-starter-test</artifactId>              <scope>test</scope>          </dependency>      </dependencies>        <build>          <plugins>              <plugin>                  <groupId>org.springframework.boot</groupId>                  <artifactId>spring-boot-maven-plugin</artifactId>              </plugin>          </plugins>      </build>  </project>  

application.yml配置文件

server:    port: 8761    spring:    application:      name: eureka-server    eureka:    instance:      hostname: localhost   # eureka 實例名稱    client:      register-with-eureka: false # 不向註冊中心註冊自己      fetch-registry: false       # 是否檢索服務      service-url:        defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/  # 註冊中心訪問地址

HystrixEurekaServerApplication.java啟動類

package com.easy.eurekaServer;    import org.springframework.boot.SpringApplication;  import org.springframework.boot.autoconfigure.SpringBootApplication;  import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;    @EnableEurekaServer  @SpringBootApplication  public class HystrixEurekaServerApplication {      public static void main(String[] args) {          SpringApplication.run(HystrixEurekaServerApplication.class, args);      }  }

2.創建hystrix-service-provider服務提供者

pom.xml配置

<?xml version="1.0" encoding="UTF-8"?>  <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"           xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">      <modelVersion>4.0.0</modelVersion>        <groupId>com.easy</groupId>      <artifactId>hystrix-service-provider</artifactId>      <version>0.0.1-SNAPSHOT</version>      <packaging>jar</packaging>        <name>hystrix-service-provider</name>      <description>Demo project for Spring Boot</description>        <parent>          <artifactId>cloud-hystrix</artifactId>          <groupId>com.easy</groupId>          <version>1.0.0</version>      </parent>        <dependencies>          <dependency>              <groupId>org.springframework.cloud</groupId>              <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>          </dependency>            <dependency>              <groupId>org.springframework.boot</groupId>              <artifactId>spring-boot-starter-web</artifactId>          </dependency>      </dependencies>        <build>          <plugins>              <plugin>                  <groupId>org.springframework.boot</groupId>                  <artifactId>spring-boot-maven-plugin</artifactId>              </plugin>          </plugins>      </build>  </project>  

application.yml配置文件

spring:    application:      name: hystrix-service-provider    eureka:    client:      service-url:        defaultZone: http://localhost:8761/eureka/    # 實例一  server:    port: 8081

HelloController.java提供一個hello介面

package com.easy.serviceProvider.web;    import org.springframework.web.bind.annotation.GetMapping;  import org.springframework.web.bind.annotation.RequestParam;  import org.springframework.web.bind.annotation.RestController;    @RestController  public class HelloController {        @GetMapping("hello")      public String hello(@RequestParam String p1, @RequestParam String p2) throws Exception {  //      用來測試服務超時的情況  //      int sleepTime = new Random().nextInt(2000);  //      System.out.println("hello sleep " + sleepTime);  //      Thread.sleep(sleepTime);          return "hello, " + p1 + ", " + p2;      }  }

最後貼上啟動類HystrixServiceProviderApplication.java

package com.easy.serviceProvider;    import org.springframework.boot.SpringApplication;  import org.springframework.boot.autoconfigure.SpringBootApplication;  import org.springframework.cloud.client.discovery.EnableDiscoveryClient;    @EnableDiscoveryClient  @SpringBootApplication  public class HystrixServiceProviderApplication {        public static void main(String[] args) {          SpringApplication.run(HystrixServiceProviderApplication.class, args);      }  }  

3.創建hystrix-service-consumer服務消費者

pom.xml配置

<?xml version="1.0" encoding="UTF-8"?>  <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"           xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">      <modelVersion>4.0.0</modelVersion>        <groupId>com.easy</groupId>      <artifactId>hystrix-service-consumer</artifactId>      <version>0.0.1-SNAPSHOT</version>      <packaging>jar</packaging>        <name>hystrix-service-consumer</name>      <description>Demo project for Spring Boot</description>        <parent>          <artifactId>cloud-hystrix</artifactId>          <groupId>com.easy</groupId>          <version>1.0.0</version>      </parent>        <dependencies>          <!-- eureka 客戶端 -->          <dependency>              <groupId>org.springframework.cloud</groupId>              <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>          </dependency>            <!-- ribbon -->          <dependency>              <groupId>org.springframework.cloud</groupId>              <artifactId>spring-cloud-starter-netflix-ribbon</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>            <dependency>              <groupId>org.springframework.boot</groupId>              <artifactId>spring-boot-starter-test</artifactId>              <scope>test</scope>          </dependency>      </dependencies>        <build>          <plugins>              <plugin>                  <groupId>org.springframework.boot</groupId>                  <artifactId>spring-boot-maven-plugin</artifactId>              </plugin>          </plugins>      </build>  </project>  

application.yml配置文件

spring:    application:      name: hystrix-eureka-server  eureka:    client:      service-url:        defaultZone: http://localhost:8761/eureka/    hystrix:    command:      default:        execution:          isolation:            thread:              timeoutInMilliseconds: 1000 # 默認超時時間

相關程式碼

異常處理類NotFallbackException.java

package com.easy.serviceConsumer.exception;    public class NotFallbackException extends Exception {  }

服務層HelloService.java

package com.easy.serviceConsumer.service;    import com.easy.serviceConsumer.exception.NotFallbackException;  import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.stereotype.Service;  import org.springframework.web.client.RestTemplate;    @Service  public class HelloService {        @Autowired      RestTemplate restTemplate;        private static final String HELLO_SERVICE = "http://hystrix-service-provider/";        @HystrixCommand(fallbackMethod = "helloFallback", ignoreExceptions = {NotFallbackException.class}              , groupKey = "hello", commandKey = "str", threadPoolKey = "helloStr")      public String hello(String p1, String p2) {          return restTemplate.getForObject(HELLO_SERVICE + "hello?p1=" + p1 + "&p2=" + p2, String.class);      }        private String helloFallback(String p1, String p2, Throwable e) {          System.out.println("class: " + e.getClass());          return "error, " + p1 + ", " + p2;      }  }

控制器ConsumerController.java

package com.easy.serviceConsumer.web;    import com.easy.serviceConsumer.service.HelloService;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.web.bind.annotation.GetMapping;  import org.springframework.web.bind.annotation.RequestParam;  import org.springframework.web.bind.annotation.RestController;    @RestController  public class ConsumerController {        @Autowired      HelloService helloService;        @GetMapping("hello")      public String hello(@RequestParam String p1, @RequestParam String p2) {          System.out.println("hello");          return helloService.hello(p1, p2);      }  }

4.啟動類HystrixServiceConsumerApplication.java

package com.easy.serviceConsumer;    import org.springframework.boot.SpringApplication;  import org.springframework.cloud.client.SpringCloudApplication;  import org.springframework.cloud.client.loadbalancer.LoadBalanced;  import org.springframework.context.annotation.Bean;  import org.springframework.web.client.RestTemplate;    @SpringCloudApplication  public class HystrixServiceConsumerApplication {        @Bean      @LoadBalanced      RestTemplate restTemplate() {          return new RestTemplate();      }        public static void main(String[] args) {          SpringApplication.run(HystrixServiceConsumerApplication.class, args);      }  }

使用示例

分別運行3個服務,HystrixEurekaServerApplication.java(服務註冊中心),HystrixServiceProviderApplication.java(服務提供者),HystrixServiceConsumerApplication.java(服務消費者)

  • 1.訪問 http://localhost:8080/hello?p1=a&p2=b ,正常情況下響應為 hello, a, b
  • 2.關閉 hystrix-service-provider 或在 sleepTime 超過 1000ms 時,訪問 http://localhost:8080/hello?p1=a&p2=b,執行降級邏輯,返回 error, a, b

資料