SpringCloud-服务降级组件Hystrix

目标:

  • 为了避免单点服务故障造成服务雪崩,需要引入服务熔断功能

  • Hystrix是在服务调用者中使用,所以调用链最底层的服务无需使用

  • 虽然Eureka-client包中貌似已经依赖了netflix-hystrix,但是要正常使用断路器功能还需单独引入依赖
  • Feign 默认是关闭服务熔断功能的,需要在yml中修改配置 feign.hystrix.enable: true

1. 使用

服务调用服务,在微服务系统内部一般使用ribbon和feign两种负载方式调用,针对这两种调用方式,开启服务熔断的方式也不相同。

无论使用哪种方式调用底层服务,需要使用断路器都需要增加依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

1.1 restTemplate

  1. 在主类上注解@EnableCircuitBreaker开启熔断器
  2. 在需要熔断的服务接口方法上注解@HystrixCommand,该注解只能用于方法。如下所示
1
2
3
4
5
6
@HystrixCommand(fallbackMethod = "getUserFallback")
public User getUser(Long id) {
//调用远程服务 http请求
String url = URL_PREFIX+"/user/"+id;
return restTemplate.getForObject(url,User.class);
}

该注解中指明了服务不可用时该调用的方法名称,因此在该服务类中,还需要添加fallback方法:

1
2
3
public User getUserFallback(Long id){    
return new User(0L,"null","null");
}

所以当底层服务不可用时,底层服务会返回一个字段都为空的User对象。

1.2 feign

  1. 使用Feign作为负载调用,主类无需注解@EnableCircuitBreaker

下面是feign的配置类,当我们在yml文件中对 feign.hystrix.enabled 配置为true时,feign将自动为我们引入断路器功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
//源码路径:org.springframework.cloud.openfeign.FeignClientsConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
protected static class HystrixFeignConfiguration {

@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled")
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
}
}
  1. feign不使用service方法调用底层服务,所以@HystrixCommand也无法使用。
  2. @FeignClient注解中加一个属性,如下:
1
2
3
4
5
6
@FeignClient(value = "user-provider",fallback = UserServiceFallBack.class)
public interface UserServiceFeignClient {

@GetMapping(value = "/user/{id}")
User getUser(@PathVariable("id")Long id);
}

此外还需要创建UserServiceFallBack类(要实现上面的接口),写入接口中所有方法的fallBack方法

1
2
3
4
5
6
7
8
@Service
@Slf4j
public class UserServiceFallBack implements UserServiceFeignClient {
@Override
public User getUser(Long id) {
return new User(0L,"null","null");
}
}

以上就是在feign调用中使用断路器的方法,一定要手动开启:

1
2
3
feign:
hystrix:
enable: true

2、Hystrix三种服务降级方案

2.1 熔断触发降级

熔断的目的是为了起到保护作用

熔断降级又分为:

  • 主动降级。例如大促的时候关闭非核心服务
  • 被动降级。熔断降级、限流降级
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@HystrixCommand(commandProperties = { 
@HystrixProperty(name="circuitBreaker.enabled",value ="true"), // 开启熔断器
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value= "5"), //最小请求次数
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value ="5000"),//熔断时间
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value = "50") //错误百分比
},fallbackMethod = "fallback")
@GetMapping("/hystrix/order/{num}")
public String queryOrder(@PathVariable("num")int num){
if(num%2==0){
return "正常访问";
}
//restTemplate默认有一个请求超时时间
return restTemplate.getForObject("http://localhost:8082/orders",String.class);
}
public String fallback(int num){ //兜底可以在这里完成
return "系统繁忙 ";
}

注意:

  1. 熔断开启之后,后续的正常请求也无法发送过去. 如何触发熔断?”判断阈值”

  2. 10s钟之内,发起了20次请求,失败率超过50%。 熔断的恢复时间(熔断5s),从熔断开启到后续5s之内的请求,都不会发起到远程服务端.

  3. 熔断会有一个自动恢复。

2.2 超时触发降级

可以配置超时时间,Hystrix能够配置的属性见类HystrixCommandProperties

1
2
3
4
5
6
7
@HystrixCommand(fallbackMethod ="timeoutFallback",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000"),
})
@GetMapping("/hystrix/timeout")
public String queryOrderTimeout(){
return restTemplate.getForObject("http://localhost:8082/orders",String.class);
}

2.3 资源隔离触发降级

资源隔离主要指对线程的隔离。Hystrix提供了两种线程隔离方式:线程池和信号量。

线程隔离-线程池

线程隔离是hystrix默认的资源隔离方案。

Hystrix通过命令模式对发送请求的对象和执行请求的对象进行解耦,将不同类型的业务请求封装为对应的命令请求。如订单服务查询商品,查询商品请求->商品Command;商品服务查询库存,查询库存请求->库存Command。并且为每个类型的Command配置一个线程池,当第一次创建Command时,根据配置创建一个线程池,并放入ConcurrentHashMap,如商品Command:

1
2
3
4
5
final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();
...
if (!threadPools.containsKey(key)) {
threadPools.put(key, new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));
}

后续查询商品的请求创建Command时,将会重用已创建的线程池。

优点:

  • 保护应用程序以免受来自依赖故障的影响,指定依赖线程池饱和不会影响应用程序的其余部分。
  • 当引入新客户端lib时,即使发生问题,也是在本lib中,并不会影响到其他内容。
  • 当依赖从故障恢复正常时,应用程序会立即恢复正常的性能。
  • 当应用程序一些配置参数错误时,线程池的运行状况会很快检测到这一点(通过增加错误,延迟,超时,拒绝等),同时可以通过动态属性进行实时纠正错误的参数配置。
  • 如果服务的性能有变化,需要实时调整,比如增加或者减少超时时间,更改重试次数,可以通过线程池指标动态属性修改,而且不会影响到其他调用请求。
  • 除了隔离优势外,hystrix拥有专门的线程池可提供内置的并发功能,使得可以在同步调用之上构建异步门面(外观模式),为异步编程提供了支持(Hystrix引入了Rxjava异步框架)。

注意:尽管线程池提供了线程隔离,我们的客户端底层代码也必须要有超时设置或响应线程中断,不能无限制的阻塞以致线程池一直饱和。

缺点:

线程池的主要缺点是增加了计算开销。每个命令的执行都在单独的线程完成,增加了排队、调度和上下文切换的开销。因此,要使用Hystrix,就必须接受它带来的开销,以换取它所提供的好处。

通常情况下,线程池引入的开销足够小,不会有重大的成本或性能影响。但对于一些访问延迟极低的服务,如只依赖内存缓存,线程池引入的开销就比较明显了,这时候使用线程池隔离技术就不适合了,我们需要考虑更轻量级的方式,如信号量隔离。

线程隔离-信号量

上面提到了线程池隔离的缺点,当依赖了响应延迟极低的服务时,线程池隔离技术引入的开销超过了它所带来的好处。这时候可以使用信号量隔离技术来代替,通过设置信号量来限制对任何给定依赖的并发调用量。下图说明了线程池隔离和信号量隔离的主要区别:

官网图

使用线程池时,发送请求的线程和执行依赖服务的线程不是同一个,而使用信号量时,发送请求的线程和执行依赖服务的线程是同一个,都是发起请求的线程。先看一个使用信号量隔离线程的示例:

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
public class QueryByOrderIdCommandSemaphore extends HystrixCommand<Integer> {
private final static Logger logger = LoggerFactory.getLogger(QueryByOrderIdCommandSemaphore.class);
private OrderServiceProvider orderServiceProvider;

public QueryByOrderIdCommandSemaphore(OrderServiceProvider orderServiceProvider) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("orderService"))
.andCommandKey(HystrixCommandKey.Factory.asKey("queryByOrderId"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
//至少有10个请求,熔断器才进行错误率的计算
.withCircuitBreakerRequestVolumeThreshold(10)
//熔断器中断请求5秒后会进入半打开状态,放部分流量过去重试
.withCircuitBreakerSleepWindowInMilliseconds(5000)
//错误率达到50开启熔断保护
.withCircuitBreakerErrorThresholdPercentage(50) .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
//最大并发请求量
.withExecutionIsolationSemaphoreMaxConcurrentRequests(10)));
this.orderServiceProvider = orderServiceProvider;
}

@Override
protected Integer run() {
return orderServiceProvider.queryByOrderId();
}

@Override
protected Integer getFallback() {
return -1;
}
}

由于Hystrix默认使用线程池做线程隔离,使用信号量隔离需要显示地将属性execution.isolation.strategy设置为ExecutionIsolationStrategy.SEMAPHORE,同时配置信号量个数,默认为10。客户端需向依赖服务发起请求时,首先要获取一个信号量才能真正发起调用,由于信号量的数量有限,当并发请求量超过信号量个数时,后续的请求都会直接拒绝,进入fallback流程。

信号量隔离主要是通过控制并发请求量,防止请求线程大面积阻塞,从而达到限流和防止雪崩的目的。

总结:

线程池和信号量都支持熔断和限流。相比线程池,信号量不需要线程切换,因此避免了不必要的开销。但是信号量不支持异步,也不支持超时,也就是说当所请求的服务不可用时,信号量会控制超过限制的请求立即返回,但是已经持有信号量的线程只能等待服务响应或从超时中返回,即可能出现长时间等待。线程池模式下,当超过指定时间未响应的服务,Hystrix会通过响应中断的方式通知线程立即结束并返回。

3 熔断器

Circuit Breaker 主要有6个参数:

  1. circuitBreaker.enabled 表示是否启用熔断器
  2. circuitBreaker.forceOpen 熔断器是否强制打开,并始终保持打开状态,不关注熔断开关的实际状态,默认false
  3. circuitBreaker.forceClose 熔断器是否强制关闭,并始终保持关闭状态,不关注熔断开关的实际状态,默认false
  4. circuitBreaker.errorThresholdPercentage 错误率达到设定值会开启熔断开关,默认50%
  5. circuitBreaker.requestVolumeThreshold 一段时间内至少有设定值个请求才进行计算错误率,默认20
  6. circuitBreaker.sleepWindowInMilliseconds 熔断器搬开状态试探睡眠事件,默认5000ms

工作原理:

熔断器工作的详细过程如下:

第一步,调用allowRequest()判断是否允许将请求提交到线程池

  1. 如果熔断器强制打开,circuitBreaker.forceOpen为true,不允许放行,返回。
  2. 如果熔断器强制关闭,circuitBreaker.forceClosed为true,允许放行。此外不必关注熔断器实际状态,也就是说熔断器仍然会维护统计数据和开关状态,只是不生效而已。

第二步,调用isOpen()判断熔断器开关是否打开

  1. 如果熔断器开关打开,进入第三步,否则继续;
  2. 如果一个周期内总的请求数小于circuitBreaker.requestVolumeThreshold的值,允许请求放行,否则继续;
  3. 如果一个周期内错误率小于circuitBreaker.errorThresholdPercentage的值,允许请求放行。否则,打开熔断器开关,进入第三步。

第三步,调用allowSingleTest()判断是否允许单个请求通行,检查依赖服务是否恢复

  1. 如果熔断器打开,且距离熔断器打开的时间或上一次试探请求放行的时间超过circuitBreaker.sleepWindowInMilliseconds的值时,熔断器器进入半开状态,允许放行一个试探请求;否则,不允许放行。

此外,为了提供决策依据,每个熔断器默认维护了10个bucket,每秒一个bucket,当新的bucket被创建时,最旧的bucket会被抛弃。其中每个blucket维护了请求成功、失败、超时、拒绝的计数器,Hystrix负责收集并统计这些计数器。

4. 降级回退方式

  • Fail Fast 快速失败

    快速失败是最普通的命令执行方法,命令没有重写降级逻辑。 如果命令执行发生任何类型的故障,它将直接抛出异常。

  • Fail Silent 无声失败

    指在降级方法中通过返回null,空Map,空List或其他类似的响应来完成。

  • Fallback: Static

    指在降级方法中返回静态的默认值。这不会导致请求以无声失败的方式被删除,而是导致默认行为的发生。

  • Fallback: Stubbed

    当命令返回一个包含多个字段的符合对象时,适合以stubbed方式回退

    1
    2
    3
    protected MissionInfo getFallback() {
    return new MissionInfo("missionName","error");
    }
  • Fallback: Cache via Network

    有时,如果调用依赖服务失败,可以从缓存服务(如redis)中查询旧数据版本。由于又会发起远程调用,所以建议重新封装一个Command,使用不同的ThreadPoolKey,与主线程池进行隔离。

    1
    2
    3
    protected Integer getFallback() {
    return new RedisServiceCommand(redisService).execute();
    }
  • Primary + Secondary with Fallback

3. hystrix仪表盘

3.1 配置dashboard工程
  1. 创建单独的工程hystrix-dashboard

    1
    2
    3
    4
    5
    6
    server:
    port: 12345

    spring:
    application:
    name: hystrix-dashboard
  2. 依赖如下,只有一个:

    1
    2
    3
    4
    5
    6
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
    </dependency>
    </dependencies>
  3. 主类如下:

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableHystrixDashboard //开启dashboard
    public class DashBoardApplication {
    public static void main(String[] args) {
    SpringApplication.run(DashBoardApplication.class,args);
    }
    }
3.2 配置熔断器所在工程
  1. 由于调用者的底层服务调用信息是通过actuator获取,所以需要添加依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  2. 修改配置,暴露hystrix自定义的 actuator-endpoint

    1
    2
    3
    4
    5
    management:
    endpoints:
    web:
    exposure:
    include: "*,hystrix.stream"
3.3 启动
  1. 启动eureka-sever

  2. 启动底层服务

  3. 启动服务调用者(断路器)

  4. 启动hystrix-dashboard

  5. 访问:localhost:12345/hyxtrix

  6. 页面中输入:http://localhost:9002/actuator/hystrix.stream, 点击Monitor stream,进入单机监控页面

    注意:此处不能使用https,因为在本地没有证书,会导致连接不上9002端口的服务

  7. 请求9002端口的服务,就会产生相应的仪表数据

仪表盘数据解析