使用Hystrix实现微服务的容错处理

Posted shi_zi_183

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用Hystrix实现微服务的容错处理相关的知识,希望对你有一定的参考价值。

使用Hystrix实现微服务的容错处理

至此,已用Eureka实现类微服务的注册与发现,Ribbon实现了客户端侧的负载均衡,Feign实现了声明式的API调用。

实现容错的手段

如果服务提供者响应非常缓慢,那么消费者对提供者的请求就会被强制等待,直到提供者响应或超时。在高负载场景下,如果不做任何处理,此类问题可能会导致服务消费者的资源耗尽甚至整个系统的崩溃。例如,曾经发生过一个案例——某电子商务网站在一个黑色星期五发生过载。过多的并发请求,导致用户支付的请求延迟很久都没有响应,在等待很长时间后最终失败。支付失败又导致用户重新刷新页面并再次尝试支付,进一步增加了服务器的负载,最终整个系统都崩溃了。

雪崩效应

微服务架构的应用系统通常包含多个服务层。微服务之间通过网络进行通信,从而支撑起整个应用系统,因此,微服务之间难免存在依赖关系。我们知道,任何微服务都并非100%可用,网络往往也很脆弱,因此难免有些请求会失败。
我们通常把“基础服务故障”导致“级联故障”的现象称为雪崩效应。雪崩效应描述的是提供者不可用导致消费者不可用,并将不可用逐渐放大的过程。

如何容错

要想防止雪崩效应,必须有一个强大的容错机制。该容错机制需实现以下两点。

  • 为网络请求设置超时:必须为网络请求设置超时。正常情况下,一个远程调用一般在几十毫秒内就能得到响应了。如果依赖的服务不可用或者网络有问题,那么响应时间就会变得很长(几十秒)。通常情况下,一次远程调用对应一个线程/进程。如果响应太慢,这个线程/进程就得不到释放。而线程/进程又对应着系统资源,如果得不到释放的线程/进程越积越多,资源就会逐渐耗尽,最终导致服务的不可用。因此,必须为每个网络请求设置超时,让资源尽快释放。
  • 使用断路器模式:如果对某个微服务的请求有大量的超时(常常说明该微服务不可用),再去让新的请求访问该服务已经没有任何意义,只会无谓消耗资源。例如,设置了超时时间为1s,如果短时间内有大量的请求无法在1s内得到响应,就没有必要再去请求依赖的服务了。
    断路器可理解为容易导致错误的操作的代理。这种代理能够统计一段时间内调用失败的次数,并决定是正常请求依赖的服务还是直接返回。
    断路器也可以辅助诊断依赖的服务是否已经恢复正常。如果发现依赖的服务已经恢复正常,那么就会恢复请求该服务。使用这种方式,就可以实现微服务的"自我修复"——当依赖的服务不正常时,打开断路器时快速失败,从而防止雪崩效应;当发现依赖的服务恢复正常时,又会恢复请求。

断路器状态转换的逻辑

  • 正常请求下,断路器关闭,可正常请求依赖的服务。
  • 当一段时间内,请求失败率达到一定阈值(例如错误率达到50%,或100次/分钟等),断路器就会打开。此时,不会再去请求依赖的服务。
  • 断路器打开一段时间后,会自动进入"半开"状态。此时,断路器可允许一个请求访问依赖的服务。如果该请求能够调用成功,则关闭断路器;否则继续保持打开状态。

使用Hystrix实现容错

Hystrix简介

Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。

  • 包裹请求:使用HystrixCommand包裹对依赖的调用逻辑,每个命令在独立线程中执行。这使用了设计模式中的"命令模式"。
  • 跳闸机制:当某服务的错误率超过一定阈值时,Hystrix开源自动或手动跳闸,停止请求该服务一段时间。
  • 资源隔离:Hystrix为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就会被立即拒绝,而不是排队等待,从而加速失败判定。
  • 监控:Hystrix可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时和被拒绝的请求等。
  • 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑可由开发人员自行提供,例如返回一个缺省值。
  • 自我修复:断路器打开一段时间后,会自动进入”半开状态“。

通用方式整合Hystrix

在Spring Cloud中,整合Hystrix非常方便。以项目microservice-consumer-movie-ribbon为例,我们来为它整合Hystrix。
1)复制项目microservice-consumer-movie-ribbon,将ArtifactId修改为microservice-comsumer-movie-ribbon-hystrix。
2)为项目添加以下依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

3)在启动类上添加注解@EnableCircuitBreaker或@EnableHystrix,从而为项目启用断路器支持。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableHystrix
public class MicroserviceConsumerMovieRibbonHystrixApplication {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

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

4)修改MovieController,让其中的findById方法具备容错能力。

package com.example.controller;

import com.example.entity.User;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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;

import java.util.List;

@RestController
public class MovieController {
    @Autowired
    private RestTemplate restTemplate;

    @HystrixCommand(fallbackMethod = "findByIdFallback")
    @GetMapping("/user/{id}")
    public User findById(@PathVariable Long id){
        return this.restTemplate.getForObject("http://microservice-provider-user/"+id,User.class);
    }

    public User findByIdFallback(Long id){
        User user = new User();
        user.setId(-1L);
        user.setName("默认用户");
        return user;
    }
}

由代码可知,为findById方法编写了一个回退方法findByIdFallback,该方法与findById方法具有相同的参数与返回值类型,该方法返回了一个默认的User。
在findById方法上,使用注解@HystrixCommand的fallbackMethod属性,指定回退方法是findByIdFallback。注解@HystrixCommand由名为javanica的Hystrix contrib库提供。javanica是一个Hystrix的子项目,用于简化Hystrix的使用。Spring Cloud自动将Spring bean与该注解封装在一个连接到Hystrix断路器的代理中。

@HystrixCommand的配置非常灵活,可使用注解@HystrixProperty的commandProperties属性来配置@HystrixCommand。
测试
1)启动项目microservice-discovery-eureka。
2)启动项目microservice-provider-user。
3)启动项目microservice-consumer-movie-ribbon-hystrix。
4)访问http://localhost:8010/user/1,可获得如下结果

5)停止microservice-provider-user


6)再次访问http://localhost:8010/user/1

我们知道,当请求失败、被拒绝、超时或者断路器打开时,都会进入回退方法。但进入回退方法并不意味着断路器已经被打开。那么,如何才能明确了解断路器当前的状态?
在很多场景下,我们需要获得造成回退的原因,只需在falllback方法上添加一个Throwable参数即可。

package com.example.controller;

import com.example.entity.User;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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;

import java.util.List;

@RestController
public class MovieController {
    private static final Logger LOGGER=LoggerFactory.getLogger(MovieController.class);
    @Autowired
    private RestTemplate restTemplate;

    @HystrixCommand(fallbackMethod = "findByIdFallback")
    @GetMapping("/user/{id}")
    public User findById(@PathVariable Long id){
        return this.restTemplate.getForObject("http://microservice-provider-user/"+id,User.class);
    }

    public User findByIdFallback(Long id,Throwable throwable){
        LOGGER.error("进入回退方法,异常:",throwable);
        User user = new User();
        user.setId(-1L);
        user.setName("默认用户");
        return user;
    }
}


多数场景下,当发生业务异常时,我们并不想触发fallback。此时要怎么办呢?Hystrix有个HystrixBadRequestException类,这是一个特殊的异常类,当该异常发生时,不会触发回退。因此,可将自定义的业务异常继承该类,从而达到业务异常不回退的效果。
另外,@HystrixCommand为我们提供了ignoreExceptions属性,也可借助该属性来配置不想执行回退的异常类。


    @HystrixCommand(fallbackMethod = "findByIdFallback",ignoreExceptions = {IllegalArgumentException.class, HystrixTimeoutException.class})
    @GetMapping("/user/{id}")
    public User findById(@PathVariable Long id){
        return this.restTemplate.getForObject("http://microservice-provider-user/"+id,User.class);
    }

这样,即使在findById中发生IllegalArgumentException或者HystrixTimeoutException,也不会执行findByIdFallback方法。

Hystrix断路器的状态监控与深入理解

引入Spring Boot Actuator

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

关闭安全管理,让health显示更多内容

management:
  security:
    enabled: false

断路器的状态也会暴露在Actuator提供的/health端点中,这样就可以直观地了解断路器的状态。
测试
1)启动项目microservice-discovery-eureka。
2)启动项目microservice-provider-user。
3)启动项目microservice-consumer-movie-ribbon-hystrix。
4)访问http://localhost:8010/user/1,可正常获得结果。
5)访问http://localhost:8010/health,可获得类似如下的结果。

		"eureka": {
			"description": "Remote status from Eureka server",
			"status": "UP",
			"applications": {
				"MICROSERVICE-CONSUMER-MOVIE": 1,
				"MICROSERVICE-PROVIDER-USER": 1
			}
		}

6)停止microservice-provider-user,再次访问

		"eureka": {
			"description": "Remote status from Eureka server",
			"status": "UP",
			"applications": {
				"MICROSERVICE-CONSUMER-MOVIE": 1,
				"MICROSERVICE-PROVIDER-USER": 1
			}
		}

我们发现尽管执行了回退逻辑,返回了默认用户,但此时Hystrix的状态依然是UP,这是因为我们失败率还没有达到阈值(默认是5s内20次失败)
8)持续快速的访问http://locahost:8010/user/1,直到请求快速返回。再次查看状态。

	"hystrix": {
		"status": "CIRCUIT_OPEN",
		"openCircuitBreakers": ["MovieController::findById"]
	}

Hystrix线程隔离策略与传播上下文

Hystrix的隔离策略有两种:分别是线程隔离和信号量隔离。

  • THREAD(线程隔离):使用该方式,HystrixCommand将在单独的线程上执行,并发请求受到线程池中的线程数量的限制。
  • SEMAPHORE(信号量隔离):使用该方法,HystrixCommand将在调用线程上执行,开销相对较小,并发请求受到信号量个数的限制。
    Hystrix中默认并且推荐使用线程隔离(THREAD),因为这种方式有一个除网络超时以外的额外保护层。
    一般来说,只有当调用负载非常高时(例如每个实例每秒调用数百次)才需要使用信号量隔离,因为在这种场景下使用THREAD开销会比较高。信号量隔离一般仅适用于非网络调用的隔离。
    可使用execution.isolation.strategy属性指定隔离策略。

Feign使用Hystrix

前文是使用注解@HystrixCommand的fallbackMethod属性实现回退的。然而,Feign是以接口形式工作的,它没有方法体,前文讲解的方式显然不适用于Feign。
在Spring Cloud中,为Feign添加回退更加简单。事实上,Spring Cloud默认已为Feign整合了Hystrix,要想为Feign打开Hystrix支持,只需设置feign.hystrix.enabled=true

为Feign添加回退

1)复制项目microservice-consumer-movie-feign,将ArtifactId修改为microservice-consumer-movie-feign-hystrix-fallback。

2)在application.yml中添加feign.hystrix.enable: true,从而开启Feign的Hystrix支持。

feign:
  hystrix:
    enabled: true

3)将之前编写的Feign接口修改成如下内容

package com.example.feign;

import com.example.entity.FeignClientFallback;
import com.example.entity.User;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(name = "microservice-provider-user",fallback = FeignClientFallback.class)
public interface UserFeignClient {
    @RequestMapping(value = "/{id}",method = RequestMethod.GET)
    public User findById(@PathVariable("id") Long id);
}
package com.example.entity;

import com.example.feign.UserFeignClient;
import org.springframework.stereotype.Component;

@Component
public class FeignClientFallback implements UserFeignClient {
    @Override
    public User findById(Long id){
        User user = new User();
        user.setId(-1L);
        user.setUsername("默认用户");
        return user;
    }
}

测试
1)启动项目microservice-discovery-eureka。
2)启动项目microservice-provider-user。
3)启动项目microservice-consumer-movie-feign-hystrix-fallback。
4)访问http://localhost:8010/user/1,可正常获得结果。

5)停止microservice-provider-user。
6)再次访问http://localhost:8010/user/1

在Spring Cloud Dalston之间的版本中,Feign默认已开启Hystrix支持,无须设置feign.hystrix.enabled=true;从Spring Cloud Dalston开始,Feign的Hystrix支持默认关闭,必须设置该属性。

通过Fallback Factory检查回退原因

1)复制项目microservice-consumer-movie-feign,将ArtifactId修改为microservice-consumer-movie-feign-hystrix-fallback-factory。
2)在application.yml中添加feign.hystrix.enabled:true,从而开启Feign的Hystrix支持。
3)将UserFeignClient改为

package com.example.entity;

import com.example.feign.UserFeignClient;
import feign.hystrix.FallbackFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.feign.FeignLoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class FeignClientFallbackFactory implements FallbackFactory<UserFeignClient> {
    private static final Logger LOGGER = LoggerFactory.getLogger(FeignClientFallbackFactory.class);
    @Override
    public UserFeignClient create(Throwable cause) {
        return new UserFeignClient() {
            @Override
            public User findById(Long id) {
                //日志最好放在各个fallback中,而不要直接放在create方法中
                //否则在引用启动时,就会打印该日志
                FeignClientFallbackFactory.LOGGER.info("fallback; reason was:",cause);
                User user=new User();
                user.setId(-1L);
                user.setUsername("默认用户");
                return user;
            }
        };
    }
}

测试
1)启动microservice-discovery-eureka。
2)启动microservice-provider-user。
3)启动microservice-consumer-movie-feign-hystrix-fallback-factory。
4)访问http://localhost:8010/user/1能正常获得结果。

5)停止microservice-provider-user。
6)再次访问http://localhost:8010/user/1



注:fallbackFactory属性还有很多其他的用途,例如让不同的异常返回不同的回退结果,从而使Feign的回退更加灵活。

为Feign禁用Hystrix

前文说过,在Spring Cloud中,只要Hystrix在项目的classpath中,Feign就会使用断路器包裹Feign客户端的所有方法。这里虽然方便,但很多场景并不需要该功能。

  • 为指定Feign客户端禁用Hystrix:借助Feign的自定义配置,可轻松为指定名称的Feign客户端禁用Hystrix。
package com.example.config;

import feign.Feign;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

@Configuration
public class FeignDisableHystrixConfiguration {
    @Bean
    @Scope("proptotye")
    public Feign.Builder feignBuilder(){
        return Feign.builder();
    }
}

想要禁用Hystrix的@FeignClient,引用该配置类即可。

@FeignClient(name = "microservice-provider-user",configuration = FeignDisableHystrixConfiguration.class)
public interface UserFeignClient {
	//....
}
  • 全局禁用Hystrix:也可为Feign全局禁用Hystrix。只需在application.yml中配置
    feign.hystrix.enable = false即可。

Hystrix的监控

除实现容错外,Hystrix还提供了近乎实时的监控。HystrixCommand和HystrixObservableCommand在执行时,会生成执行结果和运行指标,比如每秒执行的请求数、成功数等,这些监控数据对分析应用系统的状态很有用。
使用Hystrix的模块hystrix-metrics-event-stream,就可将这些监控的指标信息以text/event-stream的格式暴露给外部系统。spring-cloud-starter-netflix-hystrix已包含该模块,在此基础上,只需为项目添加spring-boot-starter-actuator,就可使用/hystrix.stream端点获得Hystrix的健康信息了。
如上所述,前文的项目microservice-consumer-movie-ribbon-hystrix已具备监控Hystrix的能力。

测试
1)启动项目microservice-discovery-eureka。
2)启动项目microservice-provider-user。
3)启动项目microservice-consumer-movie-ribbon-hystrix。
4)访问http://localhost:8010/hystrix.stream,可看到浏览器一直处于请求的状态,页面空白。这是因为此时项目中注解了@HystrixCommand的方法还没有被执行,因此也没有任何的监控数据。

5)访问http://localhost:8010/user/1后,再次访问http://localhost:8010/hystrix.stream

这是因为系统会不断地刷新以获得实时地监控数据。Hystrix地监控指标非常全面,例如HystrixCommand地名称、group名称、断路器状态、错误率、错误数等。

Feign项目地Hystrix监控

启动前文的microservice-consumer-movie-feign-hystrix-fallback项目,并使用类似的方法测试,访问http://localhost:8010/hystrix.stream,发现返回的是404。
查看项目的依赖树会发现,

以上是关于使用Hystrix实现微服务的容错处理的主要内容,如果未能解决你的问题,请参考以下文章

springCloud(11):使用Hystrix实现微服务的容错处理-简介与实现

springcloud-hystrix断路器对微服务的容错处理

Hystrix微服务容错处理及回退方法源码分析(微服务)

Hystrix服务容错处理

将hystrix整合至spring cloud feign实现容错处理

Hystrix 容错处理