天天看點

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

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

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

解決方案

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服務提供者

<?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>

           

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服務消費者

<?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>

           

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

資料

  • Spring Cloud Hystrix 示例源碼
  • Spring Boot、Spring Cloud示例學習
  • Hystrix源碼

繼續閱讀