客户端容错保护Hystrix
服务容错的核心知识:
雪崩效应
在微服务架构中,一个请求需要调用多个服务是非常常见的。如客户端访问A服务,而A服务需要调用B服务,B服务需要调用C服务,由于网络原因或者自身的原因,如果B服务或者C服务不能及时响应,A服务将处于阻塞状态,直到B服务C服务响应。此时若有大量的请求涌入,容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,造成连锁反应,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。
雪崩是系统中的蝴蝶效应导致其发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。从源头上我们无法完全杜绝雪崩源头的发生,但是雪崩的根本原因来源于服务之间的强依赖,所以我们可以提前评估,做好熔断,隔离,限流。
服务隔离
顾名思义,它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。
熔断降级
熔断这一概念来源于电子工程中的断路器(Circuit Breaker)。在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
所谓降级,就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值, 也可以理解为兜底。
服务限流
限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。比方:推迟解决,拒绝解决,或者部分拒绝解决等等。
Hystrix介绍:
Hystrix 是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。Hystrix主要通过以下几点实现延迟和容错。
包裹请求:使用 HystrixCommand包裹对依赖的调用逻辑,每个命令在独立线程中执行。这使用了设计模式中的“命令模式”。
跳闸机制:当某服务的错误率超过一定的阈值时, Hystrix可以自动或手动跳闸,停止请求该服务一段时间。
资源隔离: Hystrix为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等待,从而加速失败判定。
监控: Hystrix可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时、以及被拒绝的请求等。
回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑由开发人员自行提供,例如返回一个缺省值。
自我修复:断路器打开一段时间后,会自动进入 “半开”状态。
Rest实现服务熔断
1.配置依赖:在服务消费者 order_service 添加Hystrix的相关依赖
<!-- Hystrix的相关依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
2.开启熔断:在启动类 OrderApplication 中添加 @EnableCircuitBreaker 注解开启对熔断器的支持。
@SpringCloudApplication // @SpringBootApplication @EntityScan("com.fgy.common.domain") // @EnableCircuitBreaker public class OrderApplication { /** * 基于Ribbon的服务调用与负载均衡 * @return */ @LoadBalanced @Bean public RestTemplate getRestTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(OrderApplication.class,args); } }
可以看到,我们类上的注解越来越多,在微服务中,经常会引入三个注解,@SpringBootApplication @EnableDiscoveryClient(服务注册) @EnableCircuitBreaker(开启熔断)
于是 Spring就提供了一个组合注解:@SpringCloudApplication
3.配置熔断降级业务逻辑
/** * 订单控制层 */ @RestController @RequestMapping("/order") public class OrderController { @Autowired private RestTemplate restTemplate; /** * 通过订单系统,调用商品服务根据id查询商品信息 * @param id * @return */ @GetMapping("/buy/{id}") @HystrixCommand(fallbackMethod = "orderFallBack") public Product findById(@PathVariable("id") Long id) { return restTemplate.getForObject("http://service-product/product/" + id, Product.class); } /** * 降级方法 * @param id * @return */ public Product orderFallBack(Long id) { Product product = new Product(); product.setId(-1L); product.setProductName("熔断,触发降级方法"); return product; } }
为 findById 方法编写一个回退方法orderFallBack,该方法与 findById 方法具有相同的参数与返回值类型,该方法返回一个默认的错误信息。
在 findById 方法上,使用注解@HystrixCommand的fallbackMethod属性,指定熔断触发的降级方法是 orderFallBack 。
熔断的降级逻辑方法必须跟正常逻辑方法保证: 相同的参数列表和返回值声明。
在 findById 方法上 HystrixCommand(fallbackMethod = "orderFallBack") 用来声明一个降级逻辑的方法
4.测试
当 product-service 微服务正常时,浏览器访问 http://localhost:9002/order/buy/1
可以正常调用服务提供者获取数据。当将商品微服务停止时继续访问
此时 Hystrix配置已经生效进入熔断降级方法。
默认的Fallback:
我们刚才把fallback写在了某个业务方法上,如果这样的方法很多,那岂不是要写很多。所以我们可以把Fallback配置加在类上,实现默认fallback
全局的降级方法要求返回值类型要和要被熔断的方法一致,参数列表必须为空
/** * 订单控制层 */ @RestController @RequestMapping("/order") @DefaultProperties(defaultFallback = "defaultFallBack") public class OrderController { @Autowired private RestTemplate restTemplate; /** * 通过订单系统,调用商品服务根据id查询商品信息 * @param id * @return */ @GetMapping("/buy/{id}") // @HystrixCommand(fallbackMethod = "orderFallBack") @HystrixCommand public Product findById(@PathVariable("id") Long id) { return restTemplate.getForObject("http://service-product/product/" + id, Product.class); } /** * 统一降级方法 * 方法没有参数 * @return */ public Product defaultFallBack() { Product product = new Product(); product.setId(-2L); product.setProductName("熔断,触发统一降级方法"); return product; } /** * 降级方法 * @param id * @return */ public Product orderFallBack(Long id) { Product product = new Product(); product.setId(-1L); product.setProductName("熔断,触发降级方法"); return product; } }
超时设置:
在上面测试中,我们是直接通过停止商品微服务来模拟熔断效果,因为Hystix内部默认机制是请求在超过1秒后才会返回错误信息
可以通过配置修改这个值:
hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 2000
Feign 实现服务熔断
SpringCloud Fegin默认已为Feign整合了hystrix,所以添加Feign依赖后就不用在添加hystrix,那么怎么才能让Feign的熔断机制生效呢,只要按以下步骤开发:
1.修改application.yml,在Fegin中开启hystrix:
在Feign中已经内置了hystrix,但是默认是关闭的,需要在工程的 application.yml 中开启对hystrix的支持
#配置Feign feign: hystrix: enabled: true #开启hystrix
2.配置FeignClient
基于Feign实现熔断降级,需要实现自定义的ProductFeginClient接口,且实现类需要扫描到容器中(@Component),降级方法就是实现接口的方法
@Component public class ProductFeignClientImpl implements ProductFeignClient { /** * 熔断降级的方法 * @param id * @return */ @Override public Product findById(Long id) { Product product = new Product(); product.setId(-1L); product.setProductName("熔断,触发降级方法"); return product; } }
修改FeignClient添加hystrix熔断
/** * 声明需要调用的服务名称 * @FeignClient * name:服务提供者的服务名称 * fallback:配置熔断发生的降级方法实现类 */ @FeignClient(name = "service-product", fallback = ProductFeignClientImpl.class) public interface ProductFeignClient { /** * 配置需要调用的微服务接口 */ @GetMapping(value = "/product/{id}") Product findById(@PathVariable("id") Long id); }
3.启动测试 .........
熔断器的状态:
熔断器有三个状态 CLOSED 、 OPEN 、 HALF_OPEN 熔断器默认关闭状态,当触发熔断后状态变更为OPEN ,在等待到指定的时间,Hystrix会放请求检测服务是否开启,这期间熔断器会变为 HALF_OPEN 半开启状态,熔断探测服务可用则继续变更为 CLOSED 关闭熔断器。
Closed :关闭状态(断路器关闭),所有请求都正常访问。代理类维护了最近调用失败的次数,如果某次调用失败,则使失败次数加1。如果最近失败次数超过了在给定时间内允许失败的阈值,则代理类切换到断开(Open)状态。此时代理开启了一个超时时钟,当该时钟超过了该时间,则切换到半断开(Half-Open)状态。该超时时间的设定是给了系统一次机会来修正导致调用失败的错误。
Open :打开状态(断路器打开),所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全关闭。默认失败比例的阈值是50%,请求次数最少不低于20次。
Half Open :半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放1次请求通过,若这个请求是健康的,则会关闭断路器,否则继续保持打开,再次进行5秒休眠计时。
熔断器的默认触发阈值是20次请求。休眠时间是5秒,可以通过配置修改熔断策略:
circuitBreaker.requestVolumeThreshold=5 circuitBreaker.sleepWindowInMilliseconds=10000 circuitBreaker.errorThresholdPercentage=5
requestVolumeThreshold :触发熔断的最小请求次数,默认20
errorThresholdPercentage :触发熔断的失败请求最小占比,默认50%
sleepWindowInMilliseconds :熔断多少秒后去尝试请求
熔断器的隔离策略:
微服务使用Hystrix熔断器实现了服务的自动降级,让微服务具备自我保护的能力,提升了系统的稳定性,也较好的解决雪崩效应。其使用方式目前支持两种策略:
线程池隔离策略: 使用一个线程池来存储当前的请求,线程池对请求作处理,设置任务返回处理超时时间,堆积的请求堆积入线程池队列。这种方式需要为每个依赖的服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理)。
信号量隔离策略: 使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,请求来先判断计数器的数值,若超过设置的最大线程个数则丢弃该类型的新请求,若不超过则执行计数操作请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)
线程池和型号量两种策略功能支持对比如下: