spirngcloud服务降级Hystrix
概述
Hystrix目前已经入维护模式,官方推荐使用resilience4j,但是国内用的比较多的是阿里巴巴的sentinel
分布式系统面临的问题
复杂分布式体系结构中的应用程序 有数10个依赖关系,每个依赖关系在某些时候将不可避免地失败
服务血崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,也就是所谓的“雪崩效应”。
对于高流量的应用来说,单一的后端依赖可能会导致所有的服务器上的所有资源都在几秒内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便某个依赖关系的失败不能影响整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接受流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
Hystrix是什么
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(Fallback),而不是长时间的等待或抛出调用方无法处理的异常,这样就保证了当服务调用方的线程不被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至血崩。
Hystrix功能
- 服务降级
- 服务熔断
- 接近实时地监控
- 服务限流
- 服务隔离
Hystrix重要概念
服务降级(fallback)和 服务熔断(break)
降级
举个例子解释,我们去银行排队办理业务,大部分的银行分为普通窗口、特殊窗口(VIP窗口,老年窗口)。某一天银行大厅排普通窗口的人巨多。这时特殊窗口贴出告示说某时刻之后再开放。那么这时特殊窗口的工作人员就可以空出来去帮其他窗口办理业务,提高办事效率,已达到解决普通窗口排队的人过的目的。这时即为降级,降级的目的是为了解决整体项目的压力,而牺牲掉某一服务模块而采取的措施。
哪些情况需要降级
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满
熔断
熔断的目的是当A服务模块中的某块程序出现故障后为了不影响其他客户端的请求而做出的及时回应。
两者对比
把两者放在一起说是因为两者从有些角度看是有一定的类似性的:
- 目的很一致,都是从可用性可靠性着想,为防止系统的整体缓慢甚至崩溃,采用的技术手段;
- 最终表现类似,对于两者来说,最终让用户体验到的是某些功能暂时不可达或不可用;
- 粒度一般都是服务级别,当然,业界也有不少更细粒度的做法,比如做到数据持久层(允许查询,不允许增删改);
- 自治性要求很高,熔断模式一般都是服务基于策略的自动触发,降级虽说可人工干预,但在微服务架构下,完全靠人显然不可能,开关预置、配置中心都是必要手段;
当然两者的区别也是明显的:
- 触发原因不太一样,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;
- 管理目标的层次不太一样,熔断其实是一个框架级的处理,每个微服务都需要(无层级之分),而降级一般需要对业务有层级之分(比如降级一般是从最外围服务开始)
服务限流(flowlimit)
秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
Hystrix案例
准备工作
实际演示hystrix的各项功能在项目中如何集成,首先新建一个服务提供者工程提供两个接口,一个处理正常,一个处理超时。
-
新建工程
cloud-provider-hystrix-payment8001
-
service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
package com.eh.cloud2020.payment.service; import lombok.SneakyThrows; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service public class PaymentService { public String paymentOK(Integer id) { return "线程号:" + Thread.currentThread().getName() + " payment ok, id:" + id; } @SneakyThrows public String paymentTimeout(Integer id) { long consumeTime = 3; TimeUnit.SECONDS.sleep(consumeTime); return "线程号:" + Thread.currentThread().getName() + " payment timeout, id:" + id + "耗时(s):" + consumeTime; } }
-
controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
package com.eh.cloud2020.payment.controller; import com.eh.cloud2020.payment.service.PaymentService; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController @Slf4j public class PaymentController { private final PaymentService paymentService; public PaymentController(PaymentService paymentService) { this.paymentService = paymentService; } @GetMapping("/payment/hystrix/ok/{id}") public String paymentOK(@PathVariable Integer id) { String result = paymentService.paymentOK(id); return "result========> " + result; } @GetMapping("/payment/hystrix/timeout/{id}") public String paymentTimeout(@PathVariable Integer id) { String result = paymentService.paymentTimeout(id); return "result========> " + result; } }
-
正常测试
- http://localhost:8001/payment/hystrix/ok/1 非常快,几乎感受不到转圈圈
- http://localhost:8001/payment/hystrix/timeout/1
高并发测试
Jmeter压测
开启Jmeter,来20000个并发压死8001,20000个请求都去访问paymentInfo_TimeOut服务
再来访问http://localhost:8001/payment/hystrix/ok/1 ,会发现响应没有之前快了,会转一会儿圈圈
Jmeter压测结论
如果一个服务大量耗时严重甚至超时会影响系统整体性能,本例中本来正常的ok服务也受到了影响,因为tomcat线程池里面的工作线程数有限,大量耗时的线程会严重占用线程资源从而影响其他服务的运行,所以需要对大量超时的服务进行降级以缓解系统整体压力。
从客户端进行访问
-
新建单机环境下的EurekaServer注册中心工程
yml
1 2 3 4 5 6 7 8 9 10 11
server: port: 7000 eureka: instance: hostname: localhost # eureka服务端实例名称 client: register-with-eureka: false # false, 表示不向注册中心注册自己 fetch-registry: false # false表示自己就是注册中心,我的职责就是维护服务实例,并不去检索服务 service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
-
cloud-provider-hystrix-payment8001将服务注册到注册中心
pom
1 2 3 4
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
server: port: 8001 spring: application: name: cloud-payment-hystrix-service eureka: client: # 表示向注册中心注册自己 默认为true register-with-eureka: true # 是否从EurekaServer抓取已有的注册信息,默认为true,单节点无所谓, # 集群必须设置为true才能配合ribbon使用负载均衡 fetch-registry: false service-url: defaultZone: http://localhost:7000/eureka/ # 入驻地址
主启动类上加注解@EnableEurekaClient
-
新建客户端工程cloud-consumer-feign-hystrix-order80
从cloud-consumer-feign-order80拷贝修改
yml
1 2 3 4 5 6 7 8 9
server: port: 80 eureka: client: # 只需要从注册中心拉取服务地址列表即可,无需向注册中心注册自己 register-with-eureka: false fetch-registry: true service-url: defaultZone: http://localhost:7000/eureka
com.eh.cloud2020.payment.service.PaymentHystrixService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
package com.eh.cloud2020.payment.service; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(value = "CLOUD-PAYMENT-HYSTRIX-SERVICE") public interface PaymentHystrixService { @GetMapping("/payment/hystrix/ok/{id}") String paymentOK(@PathVariable("id") Integer id); @GetMapping("/payment/hystrix/timeout/{id}") String paymentTimeout(@PathVariable("id") Integer id); }
正常访问ok
给timeout接口进行负载,这次更狠一点,对timeout接口 5w/s qps打到服务器,再访问ok接口
因为feign使用ribbion做超时控制,ribbion默认read time 是1秒,说明消费者客户端收到提供者服务端的影响,资源也被消耗尽了,原本瞬间响应的服务现在超过1秒还没有响应。
如何解决
问题
- 服务提供者的服务耗时严重导致消费者服务变慢甚至超时
- 对方服务ok,调用者自己有超时控制要求
解决
服务降级
服务降级
使用注解@HystrixCommand
- 设置调用的超时时间,一旦超过超时时间就调用兜底方法(服务降级fallback)进行处理;
- 一旦方法出错也会调用fallback进行处理
服务端和消费端分别引入hystrix依赖
|
|
服务端降级
com.eh.cloud2020.payment.controller.PaymentController#paymentTimeout
|
|
主启动类上加注解@EnableCircuitBreaker
测试访问:http://localhost:8001/payment/hystrix/timeout/1
|
|
如果方法内部出错也会降级,一旦出错立即调用fallback返回
属性名参考com.netflix.hystrix.HystrixCommandProperties
|
|
代码重构
服务端每个Controller每个接口都配置一个fallback方法会很麻烦,可以在Controler上使用注解@DefaultProperties,之后只需要在接口上增加@HystrixCommand注解即可,示例程序如下:
|
|
客户端降级
客户端降级是指在调用服务端服务时如果调用情况不满足客户端自身要求则对这次服务端调用降级
如何降级:根据cloud-consumer-feign-hystrix-order80已经有的PaymentHystrixService接口,重新新建一个类(PaymentFallbackService)实现接口,统一为接口里面的方法进行降级处理
yml
|
|
主启动类添加注解@EnableHystrix
com.eh.cloud2020.payment.service.PaymentFallbackService
|
|
调用服务端接口增加fallback属性
|
|
测试
上面容错降级只是使用系统默认设置,下面我们单独给某一个timeout接口设置相关属性
Feign Hystrix设置单独的接口超时时间和FallBack
先说结论:HystrixCommonKey生成方法:类名#方法名(入参类型)
再说原理:
在package feign.hystrix.SetterFactory中看是如何生产commandKey的:
生成key的实现很简单,在此不展开
注意事项:
Feign+Hystrix
默认基本配置
最基本的配置,是 Hystrix 自己的一长串配置:hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
,但在 Feign 模块中,单独设置这个超时时间不行,还要额外设置 Ribbon 的超时时间,比如:
|
|
关于 Hystrix 的配置,这里有官方的说明:
Title | Desc |
---|---|
Default Value | 1000 |
Default Property | hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds |
Instance Property | hystrix.command.HystrixCommandKey.execution.isolation.thread.timeoutInMilliseconds |
How to Set Instance Default | HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(int value) |
不同实例分别配置
如果更进一步,想把超时时间细分到不同的 service 实例上也可以实现,比如:
|
|
Ribbon + Hystrix
在使用 Ribbon 时,只需要配置 Hystrix 的超时时间就可以生效,不需要额外配置 Ribbon 的超时时间,比如:
|
|
最后实操:
综上,我们可以如下配置
|
|
上面有段注释很重要,单独拎出来
|
|
服务端暂停时间3s,示例程序结果:
- timeoutInMilliseconds设置2800,“我对服务端/payment/timeout 调用情况不满意,本次调用已经降级,请稍后再试”
- timeoutInMilliseconds设置3550, “result========> 线程号:http-nio-8001-exec-9 payment timeout, id:1耗时(s):3”
使用注解给服务单独设置超时时间
Feign的超时设置
目前基本使用Feign都是与ribbon结合使用的,最重要的两个超时是连接超时ConnectTimeout和读超时ReadTimeout 在Spring Cloud中使用Feign进行微服务调用分为两层:Ribbon的调用及Hystrix的调用。所以Feign的超时时间就是Ribbon和Hystrix超时时间的结合,而如果不启用Hystrix则Ribbon的超时时间就是Feign的超时时间配置,Feign自身的配置会被覆盖。 而如果开启了Hystrix,那么Ribbon的超时时间配置与Hystrix的超时时间配置则存在依赖关系,因为涉及到Ribbon的重试机制,所以一般情况下都是Ribbon的超时时间小于Hystrix的超时时间,否则会出现以下错误:
|
|
在Ribbon超时但Hystrix没有超时的情况下,Ribbon便会采取重试机制;而重试期间如果时间超过了Hystrix的超时配置则会立即被熔断(fallback)。 下面按优先级从高到低配置
默认配置 在默认配置下,Feign的超时时间配置如下:
|
|
从上面一看是2s和5s,但是这是个坑,因为在构造完这个类后,又使用ribbon的配置把默认配置覆盖掉了:
|
|
ribbon全局配置:
|
|
ribbon指定服务配置: app-server为服务名
|
|
Feign全局配置 需要注意的是connectTimeout和readTimeout必须同时配置,要不然不会生效,还是以ribbon为准
|
|
Feign指定服务配置 和全局配置类似
|
|
hystrix指定方法配置:
|
|
示例:
|
|
上述设置可以使feign的方法超时时间设置为10秒钟
服务熔断
熔断机制概述
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
在Spring Cloud框架里,熔断机制是通过Hystrix实现,Hystrix会监控微服务间调用的状况。当失败的调用到达一定阈值后,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand
Martin Fowler’s 论文 断路器
It’s common for software systems to make remote calls to software running in different processes, probably on different machines across a network. One of the big differences between in-memory calls and remote calls is that remote calls can fail, or hang without a response until some timeout limit is reached. What’s worse if you have many callers on a unresponsive supplier, then you can run out of critical resources leading to cascading failures across multiple systems. In his excellent book Release It, Michael Nygard popularized the Circuit Breaker pattern to prevent this kind of catastrophic cascade.
The basic idea behind the circuit breaker is very simple. You wrap a protected function call in a circuit breaker object, which monitors for failures. Once the failures reach a certain threshold, the circuit breaker trips, and all further calls to the circuit breaker return with an error, without the protected call being made at all. Usually you’ll also want some kind of monitor alert if the circuit breaker trips.
使用Hystrix做熔断演示
演示步骤
-
创建client包,用于调用FeignClient接口
-
编写Client类, 使用@HystrixCommand注解设置断路器属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
package com.eh.cloud2020.payment.client; import com.eh.cloud2020.payment.service.PaymentHystrixService; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import org.springframework.stereotype.Component; @Component public class PaymentHystrixClient { private final PaymentHystrixService paymentHystrixService; public PaymentHystrixClient(PaymentHystrixService paymentHystrixService) { this.paymentHystrixService = paymentHystrixService; } // 属性参考com.netflix.hystrix.HystrixCommandProperties @HystrixCommand(fallbackMethod = "fallback", commandProperties = { @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),// 是否开启断路器 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 请求次数 @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 短路多久以后开始尝试恢复,默认5秒 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),// 失败率达到多少后跳闸 }) public String paymentOK(Integer id) { if (id < 0) { throw new RuntimeException("******id 不能负数"); } else { return paymentHystrixService.paymentOK(id); } } public String fallback(Integer id) { return "程序异常导致降级,请稍后再试"; } }
-
修改Controller,改成调用Client
1 2 3 4
@GetMapping("/order/payment/ok/{id}") public String paymentOK(@PathVariable("id") Integer id) { return paymentHystrixClient.paymentOK(id); }
-
验证
多次访问:http://localhost/order/payment/ok/-1,之后再访问http://localhost/order/payment/ok/1
发现刚开始不满足条件,就算是正确的访问也不能进行,后面就恢复正常访问了。
注意:yaml还是之前的配置,熔断降级和容错降级各自工作。
原理研究
This simple circuit breaker avoids making the protected call when the circuit is open, but would need an external intervention to reset it when things are well again. This is a reasonable approach with electrical circuit breakers in buildings, but for software circuit breakers we can have the breaker itself detect if the underlying calls are working again. We can implement this self-resetting behavior by trying the protected call again after a suitable interval, and resetting the breaker should it succeed.
状态类型:
-
Closed
熔断关闭后不会对服务进行熔断
-
Open
请求不再调用当前服务走降级分支,当打开持续时间超过reset timeout时则进入半熔断状态
-
Half Open
部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
The precise way that the circuit opening and closing occurs is as follows:
- Assuming the volume across a circuit meets a certain threshold (
HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
)… - And assuming that the error percentage exceeds the threshold error percentage (
HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
)… - Then the circuit-breaker transitions from
CLOSED
toOPEN
. - While it is open, it short-circuits all requests made against that circuit-breaker.
- After some amount of time (
HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()
), the next single request is let through (this is theHALF-OPEN
state). If the request fails, the circuit-breaker returns to theOPEN
state for the duration of the sleep window. If the request succeeds, the circuit-breaker transitions toCLOSED
and the logic in 1. takes over again.
|
|
当最近10次失败率达到60%则打开断路器,此时所有请求都降级,10秒之后断路器进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器闭合,主逻辑恢复;如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗口重新计时。
服务限流
hystrix已经入维护模式,在此先不展开,之后会着重介绍alibaba的Sentinel
Hystrix工作流程
The following sections will explain this flow in greater detail:
-
Construct a
HystrixCommand
orHystrixObservableCommand
Object构造一个HystrixCommand或者HystrixObservableCommand对象,一般使用@HystrixCommand注解
-
执行Command
-
查看缓存里是否存在返回值
-
熔断器是否处于打开状态
-
Is the Thread Pool/Queue/Semaphore Full?
检查Hystrix线程池、队列和信号量是否已经满了
-
HystrixObservableCommand.construct()
orHystrixCommand.run()
HystrixObservableCommand执行构造方法,HystrixCommand对象执行run方法
-
计算断路器健康情况
-
断路器不健康则返回fallback结果
-
Return the Successful Response
断路器健康则返回正常执行结果
Hystrix图形化Dashboard
在hystrix的回退方法中做好报警通知就可以了,Hystrix的监控仪表盘在实际开发中用得不多,此处只是作为了解。
hystrix的监控可以检测消费者调用提供者的情况,hystrix是在消费者中设置的,hystrix的监控自然也是在消费者中设置的。
actuator 服务调用监控
1、在消费者中添加依赖:
|
|
2、配置文件
|
|
默认只会监控部分数据,此配置是监控服务调用所有的数据
3、在浏览器地址栏输入http://localhost:80/actuator/hystrix.stream ,ip、port都是消费者的
刷新一下服务调用http://localhost/order/payment/ok/1,已经监控到数据
dashboard 仪表盘
上面密密麻麻的数据不直观,Hystrix提供了仪表盘可以将数据直观地展示出来。可以在消费者中配置仪表盘,也可以单独写一个子模块作为仪表盘。
-
手动添加依赖
1 2 3 4
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency>
如果是单独用一个服务来作为仪表盘,只加这个依赖即可,里面已经包含了spring-boot-start-web。
-
yml
1 2
server: port: 9090
-
引导类上加@EnableHystrixDashboard
1 2 3 4 5 6 7
@EnableHystrixDashboard @SpringBootApplication public class HystrixDashboardMain9090 { public static void main(String[] args) { SpringApplication.run(HystrixDashboardMain9090.class, args); } }
-
浏览器地址栏输入 http://localhost:9090/hystrix, ip、port都是 dasdboard 所在应用的。输入要监控的 actuator 的地址(也就是上文的http://localhost:80/actuator/hystrix.stream),dasdboard 启动时会在控制台打印出可监控的actuator地址。
-
多次访问:http://localhost/order/payment/ok/-1,之后再访问http://localhost/order/payment/ok/1,查看仪表盘变化
可以看到熔断已经打开,等待resetTime后再多次访问http://localhost/order/payment/ok/1,可以发现熔断关闭。
仪表盘说明
七色:七种状态颜色说明
1圈:实心圈共有两种含义,颜色的变化代表了实例的健康程度,它的健康从绿色<黄色<橙色<红色
递减。除了颜色的变化外,它的大小也根据实例的请求量发生变化,流量越大该实心圆就越大。所以通过实心圆的展示,就可以在大量的实例中快速的发现故障实例和高压实例。
1线:用来记录2分钟流量的相对变化,可以通过它来观察到流量的上升和下降趋势
整图说明
当消费者调用的服务实例较多时就会呈现下面这张图的情况