一、容错
如果服务提供者相应非常缓慢,那么消费者对提供者的请求就会被强制等待,知道提供者相应超时。在高负载场景下,如果不作任何处理,此类问题可能会导致服务消费者的资源耗尽甚至整个系统崩溃。
1、雪崩效应
微服务架构的应用系统通常包含多个服务层,微服务之间通过网络进行通信,从而支撑起整个应用系统,因此,微服务之间难免存在依赖关系。我们常把“基础服务故障”导致“级联故障”的现象成为雪崩效应。雪崩效应描述的是提供者不可用导致消费者不可用,并将不可用逐步扩大的过程。
如下图所示:
A\B作为微服务消费者分别消费微服务C\D,微服务C\D同时作为微服务消费者消费微服务E。
当微服务E出现故障。
由于微服务E不可用,微服务C\D调用失败,请求堆积,占用系统资源,最后导致微服务C\D不可用。
由于微服务C\D不可用,微服务A\B调用失败,请求堆积,占用系统资源,最后导致微服务A\B也不可用。
最终的结果就是由于微服务E出现故障已至整个微服务集群瘫痪掉。
2、容错手段
1.为网络请求设置超时
必须为网络请求设置超时。正常情况下,一个远程调用一般在几十毫秒内就能得到响应了。如果依赖的服务不可用或者网络有问题,那么响应时间就会变得特别长。
通常情况下,一次远程调用对应着一个线程/进程。如果响应太慢,这个线程/进程就得不到释放。而线程/进程又对应着系统资源,如果得不到释放的线程/进程约积越多,资源就会逐渐被耗尽,最终导致服务的不可用。
2.使用断路器模式
如果对某个微服务的请求有大量超时(常常说明该微服务不可用),再去让新的请求访问该服务已经没有任何意义,只会无所谓消耗资源。例如,设置了超时时间为1秒,如果短时间内有大量的请求无法在1秒内得到响应,就没有必要再去请求依赖的服务了。
短路器可理解为对容易导致错误的操作的代理。这种代理能够统计一段时间内调用失败的次数,并决定是正常请求依赖的服务还是直接返回。
断路器可以实现快速失败,如果它在一段时间内检测到许多类似的错误(例如超时),就会在之后的一段时间内,强迫对该服务的调用快速失败,即不再请求所依赖的服务。这样,应用程序就无需再浪费cpu时间去等待长时间的超时。
短路器也可自动诊断是否已经恢复正常。如果发现依赖的服务已经恢复正常,那么就会恢复请求该服务。使用这种方式,就可以实现微服务的“自我修复”——当依赖的服务不正常打开断路器时快速失败,从而防止雪崩效应;当发现依赖的服务恢复正常时,又会恢复请求。
断路器状态转换逻辑如上图所示:
- 正常情况下,断路器关闭,可正常请求依赖的服务
- 当一段时间内,请求失败率达到一定阀值(例如错误率达到50%,或100次/分钟等),断路器就会打开。此时,不会再去请求依赖的服务。
- 断路器打开一段时间后,会自动进入“半开”状态。此时,断路器可允许一个请求访问依赖的服务。如果该请求能够调用成功,则关闭断路器;否则继续保持打开状态。
二、Hystrix实现容错
Hystrix是一个实现了超时机制和断路器模式的工具类库。是由Netfix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统可用性与容错性。Hystrix主要通过以下几点实现延迟和容错。
- 包裹请求:使用HystrixCommand(或者HystrixObservableCommand)包裹对依赖的调用逻辑,每个命令在独立线程中执行。这使用了设计模式中的“命令模式”。
- 跳闸机制:当某服务的错误率超过一定阀值时,Hystrix可以自动或者手动跳闸,停止请求该服务一段时间。
- 资源隔离:Hystrix为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判断
- 监控:Hystrix可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时、以及被拒绝的请求等。
- 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑可由开发人员自行提供,例如返回一个缺省值。
- 自我修复:断路器打开一段时间后,会自动进入“半开”状态。
1、整合Hystrix
1.复制项目eureka-client-consumer-ribbon为eureka-client-consumer-hystrix
2.添加Hystrix依赖
完整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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springclouddemo</groupId>
<artifactId>eureka-client-consumer-hystrix</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-client-consumer-hystrix</name>
<description>整合Hystrix</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.为main类添加@EnableHystrix启动熔断器
package com.springclouddemo.eurekaclientconsumerhystrix;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
/**
* @author 何昌杰
*/
@SpringBootApplication
@EnableEurekaClient
@EnableHystrix
public class EurekaClientConsumerHystrixApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientConsumerHystrixApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
4.修改application配置文件
spring.application.name=eureka-client-consumer-hystrix
server.port=7206
eureka.client.service-url.defaultZone=http://localhost:7000/eureka/
eureka.instance.prefer-ip-address=true
5.将DemoController.java修改如下
package com.springclouddemo.eurekaclientconsumerhystrix.controllers;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.ribbon.proxy.annotation.Hystrix;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @author 何昌杰
*/
@RestController
public class DemoController {
private static final Logger log= LoggerFactory.getLogger(DemoController.class);
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/helloribbon")
public void ribbonDemo(){
ServiceInstance choose = this.loadBalancerClient.choose("eureka-client-provider");
log.info(choose.getInstanceId()+","+choose.getHost()+":"+choose.getPort());
}
@GetMapping("/hello/{name}")
@HystrixCommand(fallbackMethod = "demo2Fallback")
public String demo1(@PathVariable("name") String name){
return this.restTemplate.getForObject("http://localhost:7100/hello/"+name,String.class);
}
public String demo2Fallback(String name){
return "模拟默认请求响应";
}
}
- 为demo1方法编写了一个回退方法demo2Fallback,该方法的方法声明与demo1一致,具有相同的请求参数与响应类型,不过该方法默认返回一个默认字符串。
- @HystrixCommand注解的fallbackMethod属性指定了回退方法为demo2Fallback。
- 当请求demo1方法对应接口失败、被拒绝、超时或者熔断时,都会进入回退方法。但是进入回退方法并不意味断路器以及被打开,确认断路器被打开需要通过状态监控实现
2、HyStrix的状态监控与深入解析
断路器的状态监控需要通过Actuator实现
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
断路器的状态会暴露在Actuator提供的/health端点中,这样就可以直观了解断路器的状态。
测试:
这里我们发现虽然执行的回退方案返回了默认值,但是Hystrix的状态依然是UP,这是因为失败请求还未达到阈值(默认是5秒20次失败)
- 启动项目eureka-server、eureka-client-provider、eureka-client-consumer-hystrix
- 访问http://localhost:7206/hello/hcj,可以正常请求
- 访问http://localhost:7206/health,响应如下:
{ "status":"UP", "hystrix":{ "status":"UP" } ... }
- 停止eureka-client-provider
- 访问http://localhost:7206/hello/hcj,响应如下:
模拟默认请求响应
- 访问http://localhost:7206/health,响应如下:
{ "status":"UP", "hystrix":{ "status":"UP" } ... }
- 连续快速访问http://localhost:7206/health,(5秒内请求大于20次)响应如下:
{
"status":"UP",
"hystrix":{
"status":"CIRCUIT_OPEN",
"openCircuitBreakers":[
"DemoController::demo1"
]
}
...
}
Hystrix的状态为CIRCUIT_OPEN说明断路器已经打开,不会去调用其他微服务了。
3、隔离策略
Hystrix的隔离策略有两种:线程隔离和信号量隔离。
- THREAD(线程隔离):使用该方式,HystrixCommand将会在单独的线程执行,并发请求受线程池数据的限制
- SEMAPHORE(信号量隔离):使用该方式,HystrixCommand将会在调用线程上执行,开销相对较小,并发请求收到信号量个数的限制
Hystrix中默认使用线程隔离(THREAD),因为这种方式有一个除网络超时以外的额外保护层。
一般来说,只有当调用负债非常高时(至少每秒数百次)才需要使用信号量隔离,因为这种场景使用线程隔离开销会比较高,信号量格式一般仅适用于非网络调用的隔离。
可以使用execution.isolation.strategy属性指定隔离策略。
@GetMapping("/hello/{name}")
@HystrixCommand(fallbackMethod = "demo2Fallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.strategy",value = "SEMAPHORE")
}
)
public String demo1(@PathVariable("name") String name){
return this.restTemplate.getForObject("http://localhost:7100/hello/"+name,String.class);
}
更多关于Hystrix的内容各位可移步至此:Hystrix详解(如何保护、限流、熔断、隔离、降级、源码分析)
三、Feign整合Hystrix
1、为Feign添加回退
1.复制项目eureka-client-consumer-feign为eureka-client-consumer-feign-fallback
2、将ProviderFeign.java修改如下:
package com.springclouddemo.eurekaclientconsumerfeignfallback.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @author 何昌杰
*/
@Component
@FeignClient(name = "eureka-client-provider",fallback = ProviderFeignFallback.class)
public interface ProviderFeign {
@GetMapping("/hello/{name}")
String demo1(@PathVariable("name") String name);
}
class ProviderFeignFallback implements ProviderFeign{
@Override
public String demo1(String name) {
return "默认返回值";
}
}
- @FeignClient的fallback属性指定该接口的回退类
2、通过Fallback Factory检查回退原因
通过上一小节我们了解到了如何在Feign中添加回退,但是当执行回退方法时,我们无从得知为何进行了回退。本小节为例讲述为Feign添加回退日志。
1.复制项目eureka-client-consumer-feign为eureka-client-consumer-feign-fallback-factory
2.将ProviderFeign.java修改如下
package com.springclouddemo.eurekaclientconsumerfeignfallbackfactory.feign;
import feign.hystrix.FallbackFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @author 何昌杰
*/
@Component
@FeignClient(name = "eureka-client-provider",fallbackFactory = FeignClientFallbackFactory.class)
public interface ProviderFeign {
@GetMapping("/hello/{name}")
String demo1(@PathVariable("name") String name);
}
@Component
class FeignClientFallbackFactory implements FallbackFactory<ProviderFeign>{
private static final Logger log = LoggerFactory.getLogger(FeignClientFallbackFactory.class);
@Override
public ProviderFeign create(Throwable throwable) {
return new ProviderFeign() {
/**
* 将日志置于fallback方法中防止引用启动时输出日志
* @param name 请求参数
* @return 响应
*/
@Override
public String demo1(String name) {
log.info("fallback; the reason was:",throwable);
return "默认返回值";
}
};
}
}
3.编写application配置文件
spring.application.name=eureka-client-consumer-feign-fallback-factory
server.port=7208
eureka.client.service-url.defaultZone=http://localhost:7000/eureka/
eureka.instance.prefer-ip-address=true
feign.hystrix.enabled=true
- 开启熔断器,必须将feign.hystrix.enabled设置为true(默认为false)
测试:
- 启动eureka-server、eureka-client-provider、eureka-client-consumer-feign-fallback-factory
- 访问http://localhost:8010/hello/hcj能正常响应
- 停止eureka-client-provider
- 再次访问http://localhost:8010/hello/hcj,响应如下:
2019-07-09 12:21:03.991 INFO 9076 --- [ HystrixTimer-1] c.s.e.feign.FeignClientFallbackFactory : fallback; the reason was:Load balance does not have available server for client: eureka-client-provider
控制台输出如下:
2019-07-09 12:21:03.991 INFO 9076 --- [ HystrixTimer-1] c.s.e.feign.FeignClientFallbackFactory : fallback; the reason was:Load balance does not have available server for client: eureka-client-provider
说明进入了回退方法。
- fallbackFactory其实还有很多用途,可以根据不同的异常类型选择返回不同的日志:
@Component
class FeignClientFallbackFactory implements FallbackFactory<ProviderFeign>{
private static final Logger log = LoggerFactory.getLogger(FeignClientFallbackFactory.class);
@Override
public ProviderFeign create(Throwable throwable) {
return new ProviderFeign() {
/**
* 将日志置于fallback方法中防止引用启动时输出日志
* @param name 请求参数
* @return 响应
*/
@Override
public String demo1(String name) {
if(throwable instanceof IllegalAccessException){
//..
}else if(throwable instanceof ClassCastException){
//...
}
return "默认返回值";
}
};
}
}
3、为Feign禁用Hystrix
使用Feign自定义配置可以方便的禁用Hystrix,禁用Hystrix可分为两种方式,一种禁用指定Feign接口,一种是全局禁用Feign
1.指定Feign禁用
创建配置类
@Configuration
public class FeignDisableHystrixConfiguration{
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder(){
return Feign.builder();
}
}
为需要禁用Hystrix的FeignClient引用该配置
@Component
@FeignClient(name = "eureka-client-provider", configuration = FeignDisableHystrixConfiguration.class)
public interface ProviderFeign {
//...
}
2.全局禁用Hystrix
在application中添加配置项:
feign.hystrix.enabled=false