Spring Cloud(六):服务容错之断路器 Hystrix
微服务架构中,会有多个可调用的微服务,一个请求可能会连续调用多个服务,若其中某一个服务失败可能会导致级联失败(连锁反应),最终导致整个系统不可用,这种情况称之为服务雪崩效应。
Netflix 为微服务架构开发了一个 Hystrix 中间件(库),用于实现断路器功能,类似于电路中的保险丝。Hystrix 通过 HystrixCommand 对服务调用进行隔离来阻止故障连锁反应,能够让接口调用快速失败并迅速恢复正常、或者回退并优雅降级到另一个受 hystrix 保护的调用。
原生 Netflix Hystrix,Netflix Hystrix Wiki 文档,Spring Cloud Hystrix 文档。
Spring Cloud Hystrix
引入 Hystrix 依赖
1 | <dependency> |
Hystrix 简单使用
在消费者应用 Spring Boot 的启动类上添加 @EnableHystrix 或 @EnableCircuitBreaker 注解,注意 @EnableHystrix 中包含了 @EnableCircuitBreaker。
1
2
3
4
5
6
7
8
9
public class SakilaConsumer1Application {
public static void main(String[] args) {
SpringApplication.run(SakilaConsumer1Application.class, args);
}
}在消费者应用执行远程调用的类方法中,添加 @HystrixCommand 注解,指定 fallbackMethod 属性的值为调用服务失败后回退调用的方法名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ConsumerController {
private FeignInterface feignInterface;
public String feignCall(){
String str = feignInterface.callHome();
return str;
}
public String defaultCallHome(){
return "fail:调用失败,执行回退";
}
}只启动消费者服务,不启动服务提供者服务,访问 /consumer1/feign 接口。
返回的是回退执行的方法中的内容。@HystrixCommand 注解是由名为 contrib 的 Netflix contrib 提供的,Spring Cloud 会自动将带有该注释的 Spring Bean 包装在连接到 Hystrix 断路器的代理中。
可以通过配置该注解的属性来更细致的控制断路器的开关,也支持在配置文件使用 commandProperties 属性配置。
Hystrix 与 Feign 整合
项目中引入 spring-cloud-starter-openfeign 并使用 @FeignClient 注解接口。配置开启 Feign 对 Hystrix 的支持,Feign 将所有方法包装在 断路器 中,返回一个可用的 com.netflix.hystrix.HystrixCommand
整合示例
- 通过属性配置开启 Feign 对 Hystrix 的支持:
1
true =
- 在 @FeignClient 注解中,设置 fallback 属性指定回退处理类
1
2
3
4
5
6
7
public interface FeignInterface {
public String callHome();
} - fallback 指定的回退类必须要实现 @FeignClient 注解的接口,并声明为 Bean,实现接口中的方法,该实现类中的方法即为接口中远程调用失败后回退执行的方法。该类要声明为 Bean 。
1
2
3
4
5
6
7
8
9
public class FeignInterfaceHystrix implements FeignInterface {
public String callHome() {
//回退方法可定义统一的标准信息
return "fail:调用失败,执行回退";
}
}
FallbackFactory
@FeignClient 注解的 fallback 属性可以实现服务不可用时的回退功能,但不能知道触发回退的原因,可使用 FallbackFactory 来实现回退功能并可获取触发回退的原因。
- @FeignClient 注解,使用 fallbackFactory 属性指定执行回退处理类
1
2
3
4
5
6
7
public interface FeignInterface {
String callHome();
} - fallbackFactory 指定的回退类实现 FallbackFactory 接口并指定泛型为 @FeignClient 注解的接口 重写了 create() 方法,返回 FallbackFactory 接口的泛型匿名对象,重写泛型接口里的方法。
1
2
3
4
5
6
7
8
9
10
11
12
public class FeignInterfaceHystrixFactory implements FallbackFactory<FeignInterface> {
public FeignInterface create(Throwable throwable) {
return new FeignInterface() {
public String callHome() {
return throwable.getMessage();
}
};
}
}
Feign 中禁用 Hystrix
属性设置禁用,对所有 Feign 客户端有效
1
false =
禁用某一个 Feign 客户端使用 Hystrix ,编写配置文件,@FeignClient 客户端 configuration 属性指定该配置类。
1
2
3
4
5
6
7
8
public class FooConfiguration {
public Feign.Builder feignBuilder() {
return Feign.builder();
}
}Hystrix 相关设置
Hystrix 属性
- 当对服务调用超过 circuitBreaker.requestVolumeThreshold (默认:20个请求),并且失败率在 metrics.rollingStats.timeInMilliseconds(默认 10 秒)时间内 大于 circuitBreaker.errorThresholdPercentage(默认: >50%),则断路器断开,不进行调用,开发可以提供回退方法。
- @HystrixCommand 属性配置,可以使用 commandProperties 属性配置,搭配 @HystrixProperty 注解列表。
Hystrix 超时
Hystrix 与 Ribbon 一起使用时,要确保将 Hystrix 超时配置大于 Ribbon 的超时时长,包括重试时长。
例如:如果 Ribbon 连接超时为 1 秒,重试 3次,则 Hystrix 超时应该大于 3 秒。
Hystrix 线程作用域
传播安全上下文或使用Spring范围:
如果希望一些线程本地上下文传播到 @HystrixCommand,默认的声明是不起作用的,因为它是在线程池工作的(在超时情况下)。可以通过配置或直接在注释中切换 hystrix 使用与调用者相同的线程,方法是设置不同的 隔离策略。如下所示:
1 |
如果使用 @SessionScope 或 @RequestScope ,也同样适用。如果遇到运行时异常,表示无法找到作用域上下文,则需要使用同一线程。
还可以选择将 hystrix.shareSecurityContext 设置为 true,这样会自动配置一个 hystrix 并发策略插件钩子,将SecurityContext 从主线程传输到 Hystrix 命令使用的线程。
Hystrix 不允许注册多个 Hystrix 并发策略,因此可以通过将自己的 HystrixConcurrencyStrategy 声明为SpringBean来使用扩展机制。SpringCloud 在 Spring 上下文中查找您的实现,并将其包装在自己的插件中。
Hystrix 监控
单实例监控
要开启 Hystrix 监控,需要引入 Actuator ,并设置访问端点。
引入 Actuator 依赖
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>访问端点配置
1
hystrix.stream =
页面访问 hystrix.stream 端点:http://localhost:8081/actuator/hystrix.stream
在没有调用接口时,一直输出的是 ping,调用服务时,Hystrix 会实时累积关于 HystrixCommand 的执行信息,如每秒的请求数,成功数等并输出。信息如下:1
2
3
4
5ping:
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
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>在 Spring Boot 启动类上添加 @EnableHystrixDashboard 注解开启仪表盘
1
2
3
4
5
6
7
8
9
10
public class SakilaConsumer1Application {
public static void main(String[] args) {
SpringApplication.run(SakilaConsumer1Application.class, args);
}
}访问 /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 配置参数:
turbine.appConfig=customers,stores,ui,admin
注册到 Eureka 的服务名(spring.application.name),允许多个,英文逗号分隔。Turbine 根据该值去查找实例。turbine.aggregator.clusterConfig=CUSTOMERS,STORES,ADMIN
配置聚合集群名,该值是注册到 Eureka 中的服务名的大写,取的是 eureka.instance.appname 的值。假如有个服务名为 customers,则 clusterConfig 的值为 CUSTOMERS,如下:1
2customers =
CUSTOMERS =cluster 参数必须与 clusterConfig 的值匹配。如果为所有服务设置聚合集群名 clusterConfig 设置值为 default,聚合数据访问 url 的 cluster 参数就可以省略。
如果想自定义聚合集群名,但不使用 turbine.aggregator.clusterConfig 配置,可以提供 TurbineClustersProvider 类型的 Bean。turbine.aggregator.clusterNameExpression=new String(“default”)
通过表达式(clusterNameExpression)设置集群名,集群名表达式是从实例的元数据中取值。1
2
3# 如果 clusterNameExpression 设置为 default,需要使用字符串表达式,以下两种方式
"'default'" =
new String("default") =1
2
3
4
5customers =
# 从 Eureka 实例的元数据中读取
metadata['cluster'] =
# Eureka 实例需要配置元数据,如下:
customers =监控仪表盘
监控仪表盘 url 地址:http://localhost:9000/hystrix
单实例监控,仪表盘输入要监控的集群地址:http://hystrix-app:port/actuator/hystrix.stream
如果监控集群名为 default,则输入仪表盘的监控地址:http://turbine-hostname:port/turbine.stream?cluster=[clusterName]
聚合配置示例:
引入依赖 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>Turbine 聚合服务器配置
1
2
3
4
5
6
7
8
9#端口
9000 =
#应用名
hystrix-dashboard-turbine =
turbine =
#eureka 注册中心地址
http://admin:123456@eureka.master.com:8761/eureka,http://admin:123456@eureka.slave.com:8762/eureka =
consumer1 =
CONSUMER1 =服务两个实例,端口分别是 8081 和 8082
1
2
3
4
5
6
7
8
9#端口
8081 =
consumer1 =
http://admin:123456@eureka.master.com:8761/eureka,http://admin:123456@eureka.slave.com:8762/eureka =
#feign整合hystrix
true =
hystrix.stream =
#元数据
consumer1 =服务消费者 consumer1 通过 @FeignClient 客户端调用远程接口,开启了服务容错。
监控数据访问地址
Hystrix Dashboard URL 地址:http://localhost:9000/hystrix
数据聚合端点监控仪表盘:http://localhost:9000/turbine.stream?cluster=CONSUMER1
通过调用 consumer1 的接口来调用远程的服务,监控仪表盘显示接口的监控数据。验证成功的配置,turbine.aggregator.clusterConfig 必须是 turbine.aggregator.appConfig 值的全部大写,访问时 URL 必须带上 cluster 参指定具体的集群。不能使用 default 将所有的实例的监控数据聚合。
Turbine 集群端点
查看有多少个聚合集群,GET /clusters: http://localhost:9000/clusters, 假如有两个不同的服务,服务名分别是 consumer1、consumer2,且配置了这两个的集群名,获取的集群信息如下。
1 | [ |
该端点可通过配置 turbine.endpoints.clusters.enabled=false 来禁用
默认集群名问题
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]如果是多个不同的服务,服务名不同,通过元数据来设置相同的监控集群名,监控服务器通过 turbine.cluster-name-expression 来读取服务名,监控页面不能使用,一直在 Loading… 界面。
Turbine Stream
在某些环境中(例如在 PaaS 设置中),经典的 Turbine 模型从所有分布式 Hystrix Command 中拉取监测(度量)数据可能不起作用。
在这种情况下,我们可能希望让 HystrixCommand 将监测数据主动推送到 Turbine。Spring Cloud 支持通过消息传递来实现这个功能。
在客户端应用上,添加 spring-cloud-netflix-hystrix-stream 和 spring-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 环境中有 customers 和 products 两个 ServiceId,则 Turbine Stream 服务器会提供以下 URL,如果集群名为空 或为 default,Turbine Stream 服务器将提供所有能接收到的度量数据。
1 | http://my.turbine.sever:8989/turbine.stream?cluster=customers |
也可以使用 Eureka ServiceId 作为 Turbine 仪表盘(或者任何可兼容的仪表盘)的集群名,就不需要为 Turbine Stream 服务器配置如下参数:
1 | turbine.appConfig |
注意:Turbine Stream 服务器使用 Spring Clound Steam 从配置的输入通道收集所有指标。意思是不会主动收集每个实例的 Hystrix 指标数据,只提供每个实例已经收集到输入通道中的指标数据。
其它参考
Spring Cloud(六):服务容错之断路器 Hystrix
http://blog.gxitsky.com/2019/03/27/SpringCloud-06-Hystrix-circuit-breaker/