第5章 5.1-5.3 微服务容错保护 ---- Hystrix

书籍地址: Spring Cloud 微服务架构开发实战

我们在实践微服务的时候,通常会将业务拆分成一个个微服务,微服务之间通过网络进行互相调用,从而形成了微服务之间的依赖关系。由于网络原因或者自身的原因,微服务并不能保证服务百分之百可用。当遇到问题时,可以及时的启动应急预案,让系统进行自我调节和保护,如果不能及时有效的隔离有问题的微服务,则所有的服务请求有可能会因为这个单节点故障而阻塞,从而产生“雪崩效应”。此时就需要我们对微服务进行容错保护,及时发现微服务故障并进行处理。

5.1 什么是微服务容错保护

在 Netflix 中,关于微服务的容错保护有这样一个方案,就是Hystrix,通过该库我们可以解决以下问题:

  • 对第三方接口/依赖 服务潜在的调用失败提供保护和控制机制。
  • 在分布式系统中隔离资源,降低耦合,防止服务之间相互调用而导致级联失败 。
  • 快速失败以及迅速恢复。
  • 在合适的时机对服务进行优雅降级处理。
  • 对服务提供近乎实时的监控,报警和控制操作。

Hystrix是根据“断路器”模式而创建的。当某个服务单元发生故障之后,通过断路器的故障监控,向调用方返回一个符合预期的服务降级处理(fallback),而不是长时间的等待或者抛出调用方无法处理的异常。这样就保证了服务调用方的线程不会被长时间不必要的占用,从而避免了故障在分布式系统中的蔓延乃至崩溃。

当然,在请求失败频率较低的情况下,Hystrix还会直接把故障返回给客户端。只有当失败次数达到阈值(默认在20秒内失败5次)时,断路器才会被打开并且不再进行后续通信。从而直接进行服务降级(fallback)处理。

Hystrix 的开发和实现遵守如下设计理念:

  • 防止由于单个服务的故障,而耗尽整个系统容器(如Tomcat)的线程资源。
  • 快速失败,而不是在队列中积压服务请求。
  • 提供服务降级(fallback)处理机制。
  • 使用隔离技术(如舱壁隔离,泳道和断路器等模式)来隔离服务依赖之间的影响。
  • 通过近乎实时的监控和告警,及时发现系统中的潜在问题。
  • 通过配置更改可以优化低延迟传播的恢复时间,并且Hystrix支持大多数属性的动态更改,从而允许开发者可以实时对低延迟反馈循环进行修改和优化。
  • 提供对整个所依赖客户端在执行过程中的故障保护与隔离,而不仅仅是网络流量。

5.2 快速启动 Hystrix

接下来看一下如何快速的在之前搭建的示例中引用 Hystrix.

对于微服务容错来说保护的是服务消费方
因此示例中也就是商品服务的product-service工程,接下来代码更改主要集中在对product-service工程进行修改。

1. pom.xml
引入NetflixHystrix

 <dependency>
     <groupId>org.springframework.cloudgroupId>
     <artifactId>spring-cloud-starter-netflix-hystrixartifactId>
     <version>2.1.3.RELEASEversion>
dependency>

2. 开启Hystrix注解

/**
 * @EnableCircuitBreaker 该注解告诉Spring 
 * 微服务需要在项目中使用 Hystrix库,通过该库开启对服务的容错保护
 */
@MapperScan("com.turnsole.productservice.mapper")
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
@SpringBootApplication
public class ProductServiceApplication {

    //创建一个RestTemplate Bean
//    @Bean(value = "restTemplate")
//    @LoadBalanced
//    RestTemplate restTemplate(){
//        return new RestTemplate();
//    }

    public static void main(String[] args) {
        SpringApplication.run(ProductServiceApplication.class, args);
    }
}

3. 对UserService 添加服务降级处理

public class UserServiceImpl implements UserService {

    @Autowired
    private RestTemplate restTemplate;

    @Override
    @HystrixCommand(fallbackMethod = "loadFallback")
    public UserDto load(Integer id) {
        return restTemplate.getForEntity("http://USERSERVICE/users/"+id,UserDto.class).getBody();
    }

    /**
     * 该方法是load方法的降级方法,当restTemplate无法访问到USERSERVICE时,调用该方法返回一个匿名用户信息
     * @param id
     * @return
     */
    private UserDto loadFallback(Integer id) {
        return new UserDto(id,"xukai","xukai.jpg",1);
    }
}

此处我们看到又用到了 restTemplate,这个时候 记得将 ProductServiceApplication类中关于restTemplate的注释开启

4. 容错测试

  1. 以此开启 Eureka Server, 用户微服务(为了测试,需要启动亮哥实例,记得端口要不同) 和商品微服务
  2. 通过 postman 访问 http://localhost:2200/comment/2/comments 得到结果正常
  3. 将启动的两个用户微服务实例停掉一个,模拟服务宕机的情景,这时再访问以上接口,发现返回里面回有默认值出现

5.2.1 服务降级的两种实现方式

  • 通过上述示例中的 @HystrixCommand注解方式实现。
  • 通过继承HystrixCommand类来实现。

5.2.1.1 使用注解完成服务降级实现

使用注解可以最小程度的侵入代码,可以快速让原功能支持服务降级。

  1. 仅需在要进行服务降级处理的方法上增加HystrixCommand注解。
  2. 通过fallbackMethod属性设置该方法在降级处理时所使用的方法。
  3. 在降级方法中实现服务降级逻辑处理。

需要说明的是:
通过 fallbackMethod 所指定的方法要与原方法具有相同的方法签名。否则会降级失败。

@HystrixCommand 注解的属性说明如下:

  • groupKey:设定HystrixCommand分组的名称。
  • commandKey:设定HystrixCommand的名称。
  • threadPoolKey: 设定HystrixCommand执行线程池的名称。
  • fallbackMethod: 设定HystrixCommand服务降级所使用的方法名称,注意该方法需要与主方法定义在同一个类中,并且方法签名也要一致。
  • commandProperties: 设定HystrixCommand属性,比如:断路器失败百分比,断路器时间窗口大小等
  • threadPoolProperties: 设定HystrixCommand所执行线程池的属性,比如,线程池的大小,线程池等待队列长度等。
  • ignoreExceptions: 设定HystrixCommand执行服务降级处理时需要忽略的异常,也就是当出现这些异常时不会执行服务降级处理。
  • observableExecutionMode: 设定HystrixCommand执行的方式;
  • defaultFallback: 设定HystrixCommand默认的服务降级处理方法,如同时设定了fallbackMethod,该属性无效。还有,该属性所指定的方法无形参,所以需注意 与主方法返回值的兼容性。

5.2.1.2 继承HystrixCommand完成服务降级实现

  • HystrixCommand
  • HystrixObservableCommand

后者是用在所依赖服务返回多个操作结果的时候。

在实现服务降级时,如果是继承 HystrixCommand,则需要实现 getFallback()方法,如果是继承 HystrixObservableCommand, 则需要实现 resumeWithFallback()方法。

这里书中并没有给出很实用的案例,先记下,后面找文章学会再来补. 前面有个记下的,我还没忘.是关于feign中如何实现负载均衡的问题

5.2.2 在 Feign中使用Hystrix回退

注意:不要被restTemplate的代码影响

1. 增加用户微服务回退实现

@Component
public class UserServiceFallback implements UserService {
    @Override
    public UserDto load(Integer id) {
        return this.loadFallback(id);
    }
	/**
	* 用户回退的方法
	*/	
    private UserDto loadFallback(Integer id) {
        return new UserDto(id,"xukai","xukai.jpg",1);
    }
}

2. 修改Feign所注解的用户服务

@FeignClient(value = "USERSERVICE", fallback = UserServiceFallback.class)
public interface UserService {

    /**
     * 我们暂时只写测试用到的load方法
     * 注意请求的mapping映射一定要与用户微服务所提供的一致
     */
    @RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
    UserDto load(@PathVariable("id") Integer id);
}

这里最重要的修改,在原来的的 @FeignClient注解中增加 fallback 属性。并将属性的值设置为我们上面所定义的类 UserServiceFallback.class

3. 开启feign的容错开关

#feign开启容错保护
feign.hystrix.enabled=true

4. 测试
开启内容,测试流程和上面一个帕一样的。

5.3 Hystrix 容错机制分析

Hystrix 是如何实现上述目标的呢?

  • Hystrix 通过 HystrixCommand 或 HystrixObservableCommand 对所有第三方依赖/服务调用 进行封装,整个封装对象是运行在一个单独线程之中;
  • 可配置依赖调用超时时间,超时时间一般设为比99.5%平均时间略高即可。当调用超时时,直接返回或进行服务降级处理。
  • 为每个依赖关系/服务调用 维护一个小的线程池(或信号量),如果已满,那么依赖服务调用将立即被拒绝,而不是排队等待。
  • 对服务调用的执行状态:成功,失败(客户端抛出异常),超时及线程拒绝等进行统计;
  • 如果某服务调用的错误百分比高于阈值,则可以通过手动或自动的方式打开断路器,这样在一段时间内停止对该服务调用的所有请求。
  • 当服务请求被拒绝,连接超时或者断路器打开时,可以直接执行服务降级处理;
  • Hystrix提供几乎实时的指标监控和配置变化。

5.3.1 Hystrix整体处理流程

以下为HystrixCommand的执行流程图:
第5章 5.1-5.3 微服务容错保护 ---- Hystrix_第1张图片
该流程图描述了从用户提交请求到服务处理完返回的整个过程。我们来分步进行解读。

1.命令封装与执行

  • Hystrix主要是通过使用命令模式,将用户对业务服务调用请求的操作进行封装,通过该封装实现了调用者与实现者的解耦。
  • 更重要的是:Hystrix通过该模式来完成对整个请求的改造处理,从而实现了在不侵入微服务业务逻辑的情况下,为微服务增加了一层服务容错处理功能;

2.结果缓存是否可用

当我们为Hystrix开启了缓存功能时,Hystrix在执行命令时首先会检查是否缓存命中,如果是,则立即将缓存的结果以Observable对象的形式返回,并不再继续执行该命令。

3.断路器是否已经打开

当结果没有命中时,Hystrix将继续执行该命令,但在执行前将先判断断路器的状态。如果断路器已打开,则说明响应的服务已不可用,那么这时Hystrix将会转入服务降级处理,否则将继续执行。

4.是否有资源执行

接下来,Hystrix将判断与该命令相关的线程池和队列是否已满(如果使用的是信号量隔离,则判断信号量是否已满),如果已满,那么Hystrix将不执行该命令,而是转入到服务降级处理。

5.执行业务逻辑

如果前面的执行条件都满足,Hystrix将会调用HystrixCommand的run() 或者 HystrixObservableCommand的 construct() 方法来执行具体的业务逻辑处理,Hystrix使用 run() 还是 construct() 方法,是由前面所编写的方法来决定的,简单说:

  • run():该方法将返回一个单一的结果或者抛出一个异常。
  • construct(): 该方法将返回一个 Observable() 对象,通过该对象发送一个或多个返回数据,或者发送一个 OnError 错误通知。

在命令执行过程中如果执行时间超时,那么执行线程(如果该命令没有在其自身线程中执行,则会使用一个单独线程)将会抛出一个 TimeoutException 异常,这时Hystrix将会转入到fallback处理。
同时,如果线程没有被取消或者中断,那么run() 或者 construct() 返回的结果将会被抛弃,该超时时间可以通过 execution.isolation.thread.timeoutInMilliseconds设置,默认值为 1000ms.

6.更新断路器健康数据

在上面过程中,Hystrix将会把采集到的“成功”,“失败”,“拒绝”和“超时”等数据提交给断路器,断路器则会把这些统计数据更新到一系列的计数器中,然后根据这些统计数据计算断路器是否需要打开;一旦断路器打开,在恢复期结束之前Hystrix都会对该服务进行熔断处理,在恢复期之后会根据采集到的数据再次进行判断,如果仍未达到健康状态,则将继续对该服务实施熔断处理的操作,直至符合健康状态为止;

7.服务降级处理

断路器已打开,无资源执行命令(线程池,信号量,队列已满),执行命令失败,执行命令超时,以上情况都会进入服务降级处理。

当使用HystrixCommand时降级处理逻辑将通过 getFallback() 来实现,如果HystrixObservableCommand,降级逻辑则是通过 resumeWithFallback()实现。

在实现服务降级处理时,最好能够提供一个默认的处理结果,该结果最好是从内存缓存中或者一个静态逻辑处理中计算得到,不再有任何网络调用的依赖。
这是因为,一旦降级处理中包含网络处理,那么势必需要再次对该网络进行HystrixCommand/HystrixObservableCommand 封装处理,从而造成级联处理,增大了系统的不稳定性,并且降级处理终究还是要回归到一个能够稳定返回的实现上。

8.返回结果
一旦Hystrix命令执行成功(缓存返回,降级处理返回,执行成功返回都是成功),将根据我们调用的不同返回直接处理结果或 Observable。

5.3.2 HystrixCommandHystrixObservableCommand

Hystrix主要是通过HystrixCommandHystrixObservableCommand两个类来实现对服务的保护,现在来看下两者区别:

  • 从命令模式上讲,HystrixCommand是一个阻塞型命令,当执行命令时可以直接获取到执行结果.而HystrixObservableCommand是一个非阻塞型命令,该命令的调用者通过订阅其返回对象来获取执行的结果. 不过HystrixCommand命令也提供了 observe() 方法,可以返回一个非阻塞性对象,但返回的 Observable 对象只能想调用者发送一次数据.
  • 从代码编写上来说,HystrixCommand命令的业务逻辑写在 run() 方法中. 服务降级逻辑写在 getFallback() 方法中; 而HystrixObservableCommand的业务逻辑写在 construct() 方法中, 服务降级逻辑写在 resumeWithFallback() 方法中.
  • 从执行上来说, HystrixCommand的 run() 是由新创建的线程执行; 而HystrixObservableCommand的 construct() 则是由调用程序线程执行.
  • 从执行返回的结果来说, HystrixCommand只能返回一个执行结果; 而HystrixObservableCommand则可以按顺序向调用者发送多条执行结果.

5.3.3 断路器原理分析

分布式架构中,当某个服务单元发生故障之后, 通过断路器的故障监控,向调用方返回一个服务降级处理或错误响应,而不是让客户端长时间的等待. 这样就不会使得线程因调用故障服务被长时间占用而不释放. 避免了故障在分布式系统中蔓延.

Hystrix中的断路器正是起到这种服务容错保护的作用, Hystrix在运行过程中会向每个命令对应的断路器报告成功,失败,超时和拒绝的状态, 断路器维护计算统计的数据,根据这些统计的信息来确定断路器是否打开.
如果打开,后续的请求都会进行服务降级处理. 然后会隔一段时间尝试半开, 放入一部分流量请求进来, 相当于对依赖服务进行一次健康检查, 如果恢复,那么断路器会关闭,随后完全恢复调用.

Hystrix配置参数

  • circuitBreaker.enabled:是否启用断路器,默认为true。
  • circuitBreaker.forceOpen:是否强制打开断路器,如果强制打开那么将拒绝执行任何命令,该配置默认值为false. 该配置的优先级高于下面的 forceClosed
  • circuitBreaker.forceClosed:是否强制关闭断路器,强制关闭后 Hystrix将允许执行任何命令,而不做任何检查,默认值为false。
  • circuitBreaker.errorThresholdPercentage: 设定断路器开启的错误百分比。当一段时间内命令执行中的错误超过了该阈值时,断路器将会被打开,该值的默认值为 50%。
  • circuitBreaker.requestVolumeThreshold:设定在一个滚动统计窗口中,开启断路器计算命令请求的最小阈值。也就是说,在一个滚动统计窗口中如果命令请求数小于该值,即使所有请求都失败,断路器也不会打开,该配置的默认值为20。也就是说在一个统计窗口中至少要有20个命令请求时,Hystrix才进行错误百分比计算。滚动统计窗口时长默认为10秒,可以通过 metrics.rollingStats.timeInMilliseconds 进行设置。
  • circuitBreaker.sleepWindowInMilliseconds:设定断路器进行半开始谈休眠时间,单位为毫秒,改设置的默认值为 5000ms。当断路器开启一段时间之后,Hystrix会尝试释放出一部分命令进行试探,判断相应的服务是否恢复。

5.3.3.1 断路器如何打开

从上面的配置中可以得知,当满足以下条件时,Hystrix就会开启断路器:

  • 当在一个滚动统计窗口中命令请求数超过 requestVolumeThreshold 设置的值(默认值为20)时。
  • 命令执行失败百分比超过了 errorRThresholdPercentage 设置的值(默认值 50%)。

也就是在默认情况下,错误率超过50%且10秒内超过20个命令请求进行中断拦截,这时候断路器将会被打开,Hystrix将会对所有命令执行请求进行服务降级处理。

5.3.3.2 断路器如何关闭

当断路器打开一段时间之后(该值通过 sleepWindowInMilliseconds设置),Hystrix就会进入半开(Half-Open State) 状态,当一个命令请求通过这个断路器时,断路器则尝试不阻断这个命令请求,而是直接将这个命令请求通过,如果这个命令请求仍然执行失败,那么断路器会直接回到打开状态。如果这个命令请求执行成功,那么断路器就会关闭,并且开始进行下一次统计。

5.3.4 Hystrix异常----HystrixBadRequestException

HystrixCommand 的 run() 方法中,如果运行时抛出了异常,就会执行降级处理,但是有一个异常例外,就是 HystrixBadRequestException。
所以开发者需要在命令执行过程中抛出不需要进行降级处理的异常时,就可以抛出该异常。

如果使用的时注解的方式,可以在注解 ignoreException 的值来设置忽略的异常

@HystrixCommand(ignoreException={HystrixBadRequestException.class})
public User load(Integer id){
 ...
}

你可能感兴趣的:(SpringCloud)