Spring Cloud(六):服务容错之断路器 Hystrix

  微服务架构中,会有多个可调用的微服务,一个请求可能会连续调用多个服务,若其中某一个服务失败可能会导致级联失败(连锁反应),最终导致整个系统不可用,这种情况称之为服务雪崩效应。

  Netflix 为微服务架构开发了一个 Hystrix 中间件(库),用于实现断路器功能,类似于电路中的保险丝。Hystrix 通过 HystrixCommand 对服务调用进行隔离来阻止故障连锁反应,能够让接口调用快速失败并迅速恢复正常、或者回退并优雅降级到另一个受 hystrix 保护的调用。

  原生 Netflix HystrixNetflix Hystrix Wiki 文档Spring Cloud Hystrix 文档

Spring Cloud Hystrix

引入 Hystrix 依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

Hystrix 简单使用

  1. 在消费者应用 Spring Boot 的启动类上添加 @EnableHystrix 或 @EnableCircuitBreaker 注解,注意 @EnableHystrix 中包含了 @EnableCircuitBreaker。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @SpringBootApplication
    @EnableFeignClients
    @EnableHystrix
    public class SakilaConsumer1Application {

    public static void main(String[] args) {
    SpringApplication.run(SakilaConsumer1Application.class, args);
    }
    }
  2. 在消费者应用执行远程调用的类方法中,添加 @HystrixCommand 注解,指定 fallbackMethod 属性的值为调用服务失败后回退调用的方法名。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @RestController
    @RequestMapping("/consumer1")
    public class ConsumerController {

    @Autowired(required = false)
    private FeignInterface feignInterface;

    @GetMapping("/feign")
    @HystrixCommand(fallbackMethod = "defaultCallHome")
    public String feignCall(){
    String str = feignInterface.callHome();
    return str;
    }

    public String defaultCallHome(){
    return "fail:调用失败,执行回退";
    }
    }
  3. 只启动消费者服务,不启动服务提供者服务,访问 /consumer1/feign 接口。
    返回的是回退执行的方法中的内容。

  4. @HystrixCommand 注解是由名为 contrib 的 Netflix contrib 提供的,Spring Cloud 会自动将带有该注释的 Spring Bean 包装在连接到 Hystrix 断路器的代理中。
    可以通过配置该注解的属性来更细致的控制断路器的开关,也支持在配置文件使用 commandProperties 属性配置。

Hystrix 与 Feign 整合

项目中引入 spring-cloud-starter-openfeign 并使用 @FeignClient 注解接口。配置开启 Feign 对 Hystrix 的支持,Feign 将所有方法包装在 断路器 中,返回一个可用的 com.netflix.hystrix.HystrixCommand

整合示例

  1. 通过属性配置开启 Feign 对 Hystrix 的支持:
    1
    feign.hystrix.enabled=true
  2. 在 @FeignClient 注解中,设置 fallback 属性指定回退处理类
    1
    2
    3
    4
    5
    6
    7
    @FeignClient(name = "${service.application.name.sakila-service}", path = "/service",
    fallback = FeignInterfaceHystrix.class, configuration = FeignCustomConfig.class)
    public interface FeignInterface {

    @GetMapping("/home")
    public String callHome();
    }
  3. fallback 指定的回退类必须要实现 @FeignClient 注解的接口,并声明为 Bean,实现接口中的方法,该实现类中的方法即为接口中远程调用失败后回退执行的方法。该类要声明为 Bean 。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Service
    public class FeignInterfaceHystrix implements FeignInterface {

    @Override
    public String callHome() {
    //回退方法可定义统一的标准信息
    return "fail:调用失败,执行回退";
    }
    }

FallbackFactory

@FeignClient 注解的 fallback 属性可以实现服务不可用时的回退功能,但不能知道触发回退的原因,可使用 FallbackFactory 来实现回退功能并可获取触发回退的原因。

  1. @FeignClient 注解,使用 fallbackFactory 属性指定执行回退处理类
    1
    2
    3
    4
    5
    6
    7
    @FeignClient(name = "${service.application.name.sakila-service}", path = "/service",
    fallbackFactory = FeignInterfaceHystrixFactory.class, configuration = FeignCustomConfig.class)
    public interface FeignInterface {

    @GetMapping("/home")
    String callHome();
    }
  2. fallbackFactory 指定的回退类实现 FallbackFactory 接口并指定泛型为 @FeignClient 注解的接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Component
    public class FeignInterfaceHystrixFactory implements FallbackFactory<FeignInterface> {
    @Override
    public FeignInterface create(Throwable throwable) {
    return new FeignInterface() {
    @Override
    public String callHome() {
    return throwable.getMessage();
    }
    };
    }
    }
    重写了 create() 方法,返回 FallbackFactory 接口的泛型匿名对象,重写泛型接口里的方法。

Feign 中禁用 Hystrix

  1. 属性设置禁用,对所有 Feign 客户端有效

    1
    feign.hystrix.enabled=false
  2. 禁用某一个 Feign 客户端使用 Hystrix ,编写配置文件,@FeignClient 客户端 configuration 属性指定该配置类。

    1
    2
    3
    4
    5
    6
    7
    8
    @Configuration
    public class FooConfiguration {
    @Bean
    @Scope("prototype")
    public Feign.Builder feignBuilder() {
    return Feign.builder();
    }
    }

    Hystrix 相关设置

Hystrix 属性

  1. 当对服务调用超过 circuitBreaker.requestVolumeThreshold (默认:20个请求),并且失败率在 metrics.rollingStats.timeInMilliseconds(默认 10 秒)时间内 大于 circuitBreaker.errorThresholdPercentage(默认: >50%),则断路器断开,不进行调用,开发可以提供回退方法。
  2. @HystrixCommand 属性配置,可以使用 commandProperties 属性配置,搭配 @HystrixProperty 注解列表。

Hystrix 超时

Hystrix 与 Ribbon 一起使用时,要确保将 Hystrix 超时配置大于 Ribbon 的超时时长,包括重试时长。
例如:如果 Ribbon 连接超时为 1 秒,重试 3次,则 Hystrix 超时应该大于 3 秒。

Hystrix 线程作用域

传播安全上下文或使用Spring范围:
如果希望一些线程本地上下文传播到 @HystrixCommand,默认的声明是不起作用的,因为它是在线程池工作的(在超时情况下)。可以通过配置或直接在注释中切换 hystrix 使用与调用者相同的线程,方法是设置不同的 隔离策略。如下所示:

1
2
3
4
5
@HystrixCommand(fallbackMethod = "stubMyService",
commandProperties = {
@HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE")
}
)

如果使用 @SessionScope 或 @RequestScope ,也同样适用。如果遇到运行时异常,表示无法找到作用域上下文,则需要使用同一线程。
还可以选择将 hystrix.shareSecurityContext 设置为 true,这样会自动配置一个 hystrix 并发策略插件钩子,将SecurityContext 从主线程传输到 Hystrix 命令使用的线程。
Hystrix 不允许注册多个 Hystrix 并发策略,因此可以通过将自己的 HystrixConcurrencyStrategy 声明为SpringBean来使用扩展机制。SpringCloud 在 Spring 上下文中查找您的实现,并将其包装在自己的插件中。

Hystrix 监控

单实例监控

要开启 Hystrix 监控,需要引入 Actuator ,并设置访问端点。

  1. 引入 Actuator 依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  2. 访问端点配置

    1
    management.endpoints.web.exposure.include=hystrix.stream
  3. 页面访问 hystrix.stream 端点:http://localhost:8081/actuator/hystrix.stream
    在没有调用接口时,一直输出的是 ping,调用服务时,Hystrix 会实时累积关于 HystrixCommand 的执行信息,如每秒的请求数,成功数等并输出。信息如下:

    1
    2
    3
    4
    5
    ping: 

    data: {"type":"HystrixCommand","name":"FeignInterface#callHome()","group":"sakila-service1","currentTime":1553744805082,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":0,"rollingCountBadRequests":0,"rollingCountCollapsedRequests":0,"rollingCountEmit":0,"rollingCountExceptionsThrown":0,"rollingCountFailure":0,"rollingCountFallbackEmit":0,"rollingCountFallbackFailure":0,"rollingCountFallbackMissing":0,"rollingCountFallbackRejection":0,"rollingCountFallbackSuccess":0,"rollingCountResponsesFromCache":0,"rollingCountSemaphoreRejected":0,"rollingCountShortCircuited":0,"rollingCountSuccess":0,"rollingCountThreadPoolRejected":0,"rollingCountTimeout":0,"currentConcurrentExecutionCount":0,"rollingMaxConcurrentExecutionCount":0,"latencyExecute_mean":0,"latencyExecute":{"0":0,"25":0,"50":0,"75":0,"90":0,"95":0,"99":0,"99.5":0,"100":0},"latencyTotal_mean":0,"latencyTotal":{"0":0,"25":0,"50":0,"75":0,"90":0,"95":0,"99":0,"99.5":0,"100":0},"propertyValue_circuitBreakerRequestVolumeThreshold":20,"propertyValue_circuitBreakerSleepWindowInMilliseconds":5000,"propertyValue_circuitBreakerErrorThresholdPercentage":50,"propertyValue_circuitBreakerForceOpen":false,"propertyValue_circuitBreakerForceClosed":false,"propertyValue_circuitBreakerEnabled":true,"propertyValue_executionIsolationStrategy":"THREAD","propertyValue_executionIsolationThreadTimeoutInMilliseconds":1000,"propertyValue_executionTimeoutInMilliseconds":1000,"propertyValue_executionIsolationThreadInterruptOnTimeout":true,"propertyValue_executionIsolationThreadPoolKeyOverride":null,"propertyValue_executionIsolationSemaphoreMaxConcurrentRequests":10,"propertyValue_fallbackIsolationSemaphoreMaxConcurrentRequests":10,"propertyValue_metricsRollingStatisticalWindowInMilliseconds":10000,"propertyValue_requestCacheEnabled":true,"propertyValue_requestLogEnabled":true,"reportingHosts":1,"threadPool":"sakila-service1"}

    data: {"type":"HystrixThreadPool","name":"sakila-service1","currentTime":1553744805083,"currentActiveCount":0,"currentCompletedTaskCount":161,"currentCorePoolSize":10,"currentLargestPoolSize":10,"currentMaximumPoolSize":10,"currentPoolSize":10,"currentQueueSize":0,"currentTaskCount":161,"rollingCountThreadsExecuted":0,"rollingMaxActiveThreads":0,"rollingCountCommandRejections":0,"propertyValue_queueSizeRejectionThreshold":5,"propertyValue_metricsRollingStatisticalWindowInMilliseconds":10000,"reportingHosts":1}

    更多监控指标请参考官方文档地址:Netflix Hystrix-Metrics and Monitoring

Hystrix 仪表盘

Hystrix 的主要好处之一是它收集了关于每个 HystrixCommand 的指标集。Hystrix 仪表盘以更高效的方式显示每个断路器的运行状态。

  1. 添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
    </dependency>
  2. 在 Spring Boot 启动类上添加 @EnableHystrixDashboard 注解开启仪表盘

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @SpringBootApplication
    @EnableFeignClients
    @EnableHystrix
    @EnableHystrixDashboard
    public class SakilaConsumer1Application {

    public static void main(String[] args) {
    SpringApplication.run(SakilaConsumer1Application.class, args);
    }
    }
  3. 访问 /hystrix 端点,并指向监控端点 hystrix.stream

    访问:http://localhost:8081/hystrix

    指向前面配置的监控端点:http://localhost:8081/actuator/hystrix.stream

Turbine 数据聚合

Turbine 聚合集群环境的监控数据流,基于 Spring Cloud Greenwich.RELEASE 版本搭建仍存在问题,暂未找到解决办法。

Hystrix 只能监控单个节点(单实例),不适用于需要监控多节点的集群环境。Turbine 是个数据流聚合工具,将集群环境中各个节点的监控数据流聚合发送到一组 /turbine.stream 中,以便 在 Hystrix 仪表盘中显示使用。默认是聚合各个节点的 /actuator/hystrix.stream 端点的监控数据。

需要监控的实例和监控服务需要集成 Eureka 和 Turbine,在 Spring Boot 启动类上添加 @EnableTurbin 注解。

默认情况下, Turbine 通过 Eureka 中获取到注册列表,根据实例的 hostName 和端口拼接成 url,将 /actuator/hystrix.stream 拼接到 url 末尾,通过向此节点请求就可以聚合集群节点的监控数据了。

Turbine 配置参数:

  1. turbine.appConfig=customers,stores,ui,admin
    注册到 Eureka 的服务名(spring.application.name),允许多个,英文逗号分隔。Turbine 根据该值去查找实例。

  2. turbine.aggregator.clusterConfig=CUSTOMERS,STORES,ADMIN
    配置聚合集群名,该值是注册到 Eureka 中的服务名的大写,取的是 eureka.instance.appname 的值。假如有个服务名为 customers,则 clusterConfig 的值为 CUSTOMERS,如下:

    1
    2
    turbine.aggregator.appConfig=customers
    turbine.aggregator.clusterConfig=CUSTOMERS

    cluster 参数必须与 clusterConfig 的值匹配。如果为所有服务设置聚合集群名 clusterConfig 设置值为 default,聚合数据访问 url 的 cluster 参数就可以省略。
    如果想自定义聚合集群名,但不使用 turbine.aggregator.clusterConfig 配置,可以提供 TurbineClustersProvider 类型的 Bean。

  3. turbine.aggregator.clusterNameExpression=new String(“default”)
    通过表达式(clusterNameExpression)设置集群名,集群名表达式是从实例的元数据中取值。

    1
    2
    3
    # 如果 clusterNameExpression 设置为 default,需要使用字符串表达式,以下两种方式
    turbine.cluster-name-expression="'default'"
    turbine.cluster-name-expression=new String("default")
    1
    2
    3
    4
    5
    turbine.aggregator.appConfig=customers
    # 从 Eureka 实例的元数据中读取
    turbine.cluster-name-expression=metadata['cluster']
    # Eureka 实例需要配置元数据,如下:
    eureka.instance.metadata-map.cluster=customers
  4. 监控仪表盘
    监控仪表盘 url 地址:http://localhost:9000/hystrix
    单实例监控,仪表盘输入要监控的集群地址:http://hystrix-app:port/actuator/hystrix.stream
    如果监控集群名为 default,则输入仪表盘的监控地址:http://turbine-hostname:port/turbine.stream?cluster=[clusterName]

聚合配置示例:

  1. 引入依赖 Turbine ,依赖了 eureka、actuator、hystrix、hystrix-dashboard

    1
    2
    3
    4
    5
    <dependency>
    <!- trubine 依赖包里包已经有 eureka 包->
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
    </dependency>
  2. Turbine 聚合服务器配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #端口
    server.port=9000
    #应用名
    spring.application.name=hystrix-dashboard-turbine
    eureka.instance.appname=turbine
    #eureka 注册中心地址
    eureka.client.service-url.defaultZone=http://admin:123456@eureka.master.com:8761/eureka,http://admin:123456@eureka.slave.com:8762/eureka
    turbine.appConfig=consumer1
    turbine.aggregator.clusterConfig=CONSUMER1
  3. 服务两个实例,端口分别是 8081 和 8082

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #端口
    server.port=8081
    eureka.instance.appname=consumer1
    eureka.client.service-url.defaultZone=http://admin:123456@eureka.master.com:8761/eureka,http://admin:123456@eureka.slave.com:8762/eureka
    #feign整合hystrix
    feign.hystrix.enabled=true
    management.endpoints.web.exposure.include=hystrix.stream
    #元数据
    eureka.instance.metadata-map.cluster=consumer1
  4. 服务消费者 consumer1 通过 @FeignClient 客户端调用远程接口,开启了服务容错。

  5. 监控数据访问地址
    Hystrix Dashboard URL 地址:http://localhost:9000/hystrix
    数据聚合端点监控仪表盘:http://localhost:9000/turbine.stream?cluster=CONSUMER1
    通过调用 consumer1 的接口来调用远程的服务,监控仪表盘显示接口的监控数据。

  6. 验证成功的配置,turbine.aggregator.clusterConfig 必须是 turbine.aggregator.appConfig 值的全部大写,访问时 URL 必须带上 cluster 参指定具体的集群。不能使用 default 将所有的实例的监控数据聚合。

Turbine 集群端点

查看有多少个聚合集群,GET /clusters: http://localhost:9000/clusters, 假如有两个不同的服务,服务名分别是 consumer1、consumer2,且配置了这两个的集群名,获取的集群信息如下。

1
2
3
4
5
6
7
8
9
10
[
{
"name": "CONSUMER1",
"link": "http://localhost:9000/turbine.stream?cluster=CONSUMER1"
},
{
"name": "CONSUMER2",
"link": "http://localhost:9000/turbine.stream?cluster=CONSUMER2"
}
]

该端点可通过配置 turbine.endpoints.clusters.enabled=false 来禁用

默认集群名问题

  1. turbine.appConfig 设置了多个服务名,而集群名 turbine.aggregator.clusterConfig 指定为 default,监控仪表盘就完全不能显示,页面一直在 Loading….,按我的理解,没有真正达到聚合不同服务的监控数据流。

    错误日志:org.apache.catalina.connector.ClientAbortException: java.io.IOException: 你的主机中的软件中止了一个已建立的连接。

    感觉异常的输出信息:
    Removing event handler: StreamingHandler_906cd13a-326e-4d79-8490-5e7e5a100c56
    Removed handler queue tuple for handler: HandlerQueueTuple [eventHandler=com.netflix.turbine.streaming.TurbineStreamingConnection@76f2c95f]

  2. 如果是多个不同的服务,服务名不同,通过元数据来设置相同的监控集群名,监控服务器通过 turbine.cluster-name-expression 来读取服务名,监控页面不能使用,一直在 Loading… 界面。

Turbine Stream

在某些环境中(例如在 PaaS 设置中),经典的 Turbine 模型从所有分布式 Hystrix Command 中拉取监测(度量)数据可能不起作用。

在这种情况下,我们可能希望让 HystrixCommand 将监测数据主动推送到 Turbine。Spring Cloud 支持通过消息传递来实现这个功能。

在客户端应用上,添加 spring-cloud-netflix-hystrix-streamspring-cloud-starter-stream-* 依赖,关于消息中间件的详细信息及与客户端应用配置,参考 Spring Cloud Stream documentation 文档,这些组件是支持开箱即用的。

使用 Spring Boot 创建服务端应用,在入口启动类上添加 @EnableTurbineStream 注解。Turbine Stream 服务器需要使用 Spring Webflux,所以需要添加 spring-boot-starter-webflux 依赖。默认情况下,添加的 spring-cloud-starter-netflix-turbine-stream 依赖中已经包含了 spring-boot-starter-webflux 依赖。

然后可以将 Hystrix 仪表盘指向 Turbine Stream 服务器,而不是单个 Hystrix stream。如果 Turbine Stream 服务器在 8989 端口上运行,则在 Hystrix 仪表盘的监控地址(stream)中输入 http://myhost:8989。 断路器的前缀是各自的 ServiceId,后面跟一个点( . ),后然是断路器名称。

Spring Cloud 提供了 spring-cloud-starter-netflix-turbine-stream 组件包,包含了 Turbine Stream 服务器需要的所有依赖。所然可以添加流绑定器,例如 spring-cloud-starter-stream-rabbit

Turbine Stream 服务还支持集群参数。与 Turbine 服务不同,Turbine Stream 使用 Eureka ServiceId 作为集群名,这些是不可配置的。

如果 Turbine Stream 服务器运行 my.turbine.server 的 8989 端口上。Eureka 环境中有 customersproducts 两个 ServiceId,则 Turbine Stream 服务器会提供以下 URL,如果集群名为空 或为 default,Turbine Stream 服务器将提供所有能接收到的度量数据。

1
2
3
4
http://my.turbine.sever:8989/turbine.stream?cluster=customers
http://my.turbine.sever:8989/turbine.stream?cluster=products
http://my.turbine.sever:8989/turbine.stream?cluster=default
http://my.turbine.sever:8989/turbine.stream

也可以使用 Eureka ServiceId 作为 Turbine 仪表盘(或者任何可兼容的仪表盘)的集群名,就不需要为 Turbine Stream 服务器配置如下参数:

1
2
3
turbine.appConfig
turbine.aggregator.clusterConfig
turbine.clusterNameExpression

注意:Turbine Stream 服务器使用 Spring Clound Steam 从配置的输入通道收集所有指标。意思是不会主动收集每个实例的 Hystrix 指标数据,只提供每个实例已经收集到输入通道中的指标数据。

其它参考

  1. Hystrix系列之入门

Spring Cloud(六):服务容错之断路器 Hystrix

http://blog.gxitsky.com/2019/03/27/SpringCloud-06-Hystrix-circuit-breaker/

作者

光星

发布于

2019-03-27

更新于

2022-06-17

许可协议

评论