在微服务的架构中,一个业务的实现通常包括多层服务的调用,底层基础服务的故障可能会导致上层服务级联发生故障,进而故障不断蔓延导致系统整体不可用,这种现象称为服务的雪崩效应。

断路器

断路器(CircuitBreaker, 或叫熔断器)是用来避免服务雪崩效应,提升系统整体可用性的一种技术手段。通过在一个时间窗口内监测服务的调用失败情况,在失败时通过回调返回默认实现(这叫服务降级),当失败达到一定程度时,后续的调用直接导致快速失败,不再访问远程服务(这叫熔断机制),防止不断尝试调用可能会失败的服务,以使服务有机会恢复,一定时间后,当断路器监测到服务恢复后,会继续尝试调用。

断路器原理有点类似于电路中的保险丝或电闸,当发现电路中出现短路异常情况时,通过保险丝熔断或电闸跳闸来断开电路,避免事故发生。

Hystrix服务降级

当使用Hystrix时,每一个远程服务的调用都被封装到一个HystrixCommand中,HystrixCommand有两个主要方法:run(), getFallback()。
其中run方法封装了远程服务的调用逻辑,如果run方法超时或者抛出异常,并且启用了服务降级,则会调用getFallback方法来进行降级处理。

降级处理由 HystrixCommandProperties.Setter 中定义的配置属性来控制,主要包括:

  • fallbackEnabled, 是否启用降级处理,如果启用则在超时或异常时调用getFallback来进行降级处理,默认启用
  • fallbackIsolationSemaphoreMaxConcurrentRequests,控制getFallback方法并发请求的信号量,默认为10,如果请求超过了信号量限制,则不再尝试调用getFallback方法,而是快速失败
  • executionIsolationThreadInterruptOnFutureCancel,隔离策略为THREAD,当Future#cancel(true)时,是否进行中断处理,默认为false
  • executionIsolationThreadInterruptOnTimeout,隔离策略为THREAD,当执行线程超时时,是否进行中断,默认为true
  • executionTimeoutInMilliseconds,执行超时时间,默认为1000ms,如果隔离策略为THREAD(线程池隔离),且配置了executionIsolationThreadInterruptOnTimeout=true,则执行线程将被中断,如果隔离策略为SEMAPHORE(信号量隔离),则终止操作,信号量隔离下执行线程与主线程是同一个线程,所以不会中断线程处理

在进行降级处理调用getFallback方法时,需注意:

  1. 该方法最大并发数受fallbackIsolationSemaphoreMaxConcurrentRequests控制,默认为10,如果失败率很高,则需配置该参数,如果并发数超过了配置,则不会执行getFallback,而是快速失败,抛出异常“HystrixRuntimeException: xxx fallback executionrejected”

  2. 尽量避免在getFallback中进行网络请求,而是能快速返回的缓存数据或静态数据(如默认值);如果需要做网络请求,则应该是调用另一个被Hystrix保护的请求,即对fallback进行串联,第一个fallback中请求网络做业务调用,第二个fallback中回调缓存或静态数据

上文提到Hystrix的线程池隔离策略与信号量隔离策略,两者如何理解?

  1. 线程池隔离:执行在一个单独的线程中,通过线程池中线程数量来控制并发请求量。每一个服务使用一个单独的线程池进行隔离,避免互相影响。这种策略下的服务调用是异步的,可通过hystrix来配置超时。
  2. 信号量隔离:执行在调用线程中,通过信号量来控制控制并发请求量(executionIsolationSemaphoreMaxConcurrentRequests, 默认为10),如果并发量超过该值,则调用getFallback方法对服务进行降级。这种策略下的服务调用是同步的,无法对调用进行超时配置,只能通过调用协议(如http)的超时。信号量隔离策略一般只有在高并发量的情况下使用(如一秒几百次),这种情况下使用单独的线程池开销比较大;或者如果需要在调用服务的线程中,如RequestInterceptor中使用ThreadLocal中的变量,也可以通过将隔离策略设置为信号量来实现(hystrix.command.default.execution.isolation.strategy=SEMAPHORE)

注: 如果只是需要在服务调用中使用安全上下文 SecurityContext, 则也可以通过配置 hystrix.shareSecurityContext=true 来实现,这样Hystrix的并发策略插件会将SecurityContext从主线程传递到Hystrix command使用的线程。

线程池隔离策略与信号量隔离策略两者之间的区别

隔离策略 实现原理 调用模式 是否支持超时配置 降级实现 资源消耗
线程池隔离 每个服务使用单独的线程池 异步调用 支持 线程池满则请求拒绝,降级处理 较大,容易造成服务器负载高
信号量隔离 使用信号量的计数器 同步调用 不支持 信号量达到最大值则请求拒绝,降级处理 较小

Hystrix熔断机制

Hystrix客户端会对调用失败情况进行采样统计。当在一个时间窗口(由metrics.rollingStats.timeInMilliseconds配置, 默认10s)内,调用某个服务超过一定次数(由circuitBreaker.requestVolumeThreshold配置,默认20),失败率超过一定比例(由circuitBreaker.errorThresholdPercentage配置,默认50%),则熔断开关打开,调用会被快速失败(不再进行远程调用),如果开发者提供了fallback,则会调用fallback进行降级处理,如果没有,则抛出 异常。

调用失败包括如下几种情况:

  1. 调用中抛出异常
  2. 调用超时
  3. 线程池拒绝
  4. 信号量拒绝

熔断开关的状态:

  • 闭合(closed):如果配置了熔断开关强制闭合,或者当前的请求失败率没有超过设置的阈值,则熔断开关处于闭合状态,不启动熔断机制。但这时如果调用超时或失败,仍会进行降级处理(除非fallbackEnabled为false)
  • 打开(open):如果配置了熔断开关强制打开,或者当前的请求失败率超过了设置的阈值,则熔断开关打开,启动熔断机制,直接进行降级处理,不再进行远程调用
  • 半打开(half-open):当熔断开关处理打开状态,需要在一定的时间窗口后进行重试,检测服务是否恢复,这种状态就是半打开状态。如果测试成功则关闭熔断开关,否则还是处于打开状态

Hystrix熔断开关的状态关系如图所示

hystrix熔断开关状态

熔断相关的参数配置(HystrixCommandProperties.Setter):

  • circuitBreakerEnabled, 是否开启熔断机制,默认true
  • circuitBreakerForceOpen,是否强制打开熔断开关,如果为true,则对请求进行强制降级,默认为false
  • circuitBreakerForceClosed, 是否强制关闭熔断开关,默认为false
  • circuitBreakerRequestVolumeThreshold, 在熔断开关闭合的情况下,一个采样时间窗口内需要进行至少多少个请求才进行采用统计计算失败率,默认为20
  • circuitBreakerErrorThresholdPercentage, 在一个采样时间窗口内,失败率超过该值,则打开熔断开关,进行快速失败,默认采样时间窗口为10s,失败率为50%
  • circuitBreakerSleepWindowInMilliseconds, 熔断后的重试时间窗口,在该时间窗口允许一次重试,如果重试成功,则关闭熔断开关,否则还是打开状态,默认5s

案例演示

熔断是服务调用端的一种保护机制,因此通常与Feign结合使用,Feign的Hystrix支持在Dalston版本之前,hystrix只要在类路径中,feign默认就会自动将所有方法封装到断路器中,Dalston版及以后的版本改变了这一做法,需要进行显示配置 feign.hystrix.enabled=true。

本文案例还是基于前面创建的springcloud-eureka(注册中心), springcloud-eureka-client(一个简单的hello service)两个项目。

  1. 新建springcloud-hystrix项目,pom.xml中引入相关依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     <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-openfeign</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  2. applicaiton.yml配置文件中添加必要配置

    1
    2
    3
    4
    5
    6
    7
    8
    eureka:
    client:
    serviceUrl:
    defaultZone: http://localhost:8761/eureka/

    feign:
    hystrix:
    enabled: true
  3. 启动类添加必要注解

    1
    2
    3
    4
    @SpringBootApplication
    @EnableCircuitBreaker
    @EnableFeignClients
    public class HystrixApplication

其它Feign Client类与Controller类详见源码:https://github.com/ronwxy/springcloud-demos springcloud-hystrix项目

  1. 测试

这里分四种情形分别进行演示。依次启动springcloud-eureka, springcloud-eureka-client(debug模式启动,并在hello接口里设置断点,模拟超时),springcloud-hystrix

  • 启用断路器,未指定fallback
1
2
@FeignClient(name = "hello-service")
public interface HelloClient

访问 http://localhost:8084/hystrix0 得到结果如下

无fallback

抛出异常: HystrixRuntimeException: HelloClient#hello() timed-out and no fallback available.] with root cause java.util.concurrent.TimeoutException: null

  • 启用断路器,指定fallback
1
2
@FeignClient(name = "hello-service", contextId = "hello-with-fallback", fallback = HelloClientFallback.class)
public interface HelloClient1

访问 http://localhost:8084/hystrix1 得到结果返回 “调用hello-service返回:this is returned by fallback”, 不会抛出异常。

  • 启用断路器,指定fallbackFactory
1
2
@FeignClient(name = "hello-service", contextId = "hello-with-fallbackFactory", fallbackFactory = HelloClientFallbackFactory.class)
public interface HelloClient2

访问 http://localhost:8084/hystrix2 得到结果返回 “调用hello-service返回:this is returned from fallbackFactory, cause: com.netflix.hystrix.exception.HystrixTimeoutException”,不会抛出异常。

  • 不启用断路器
1
2
3
4
5
6
7
8
9
10
@FeignClient(name = "hello-service", contextId = "hello-without-circuitBreaker", configuration = DisableCircuitBreakerConfiguration.class)
public interface HelloClient3

public class DisableCircuitBreakerConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder() {
return Feign.builder();
}
}

对单个feign client禁用断路器可以配置一个注入了 prototype scope的 Feign.Builder实例的配置类来实现。

访问 http://localhost:8084/hystrix3 得到结果如下

无Hystrix

抛出异常: feign.RetryableException: Read timed out executing GET http://hello-service/hello] with root cause java.net.SocketTimeoutException: Read timed out

可见,在启用断路器,不指定fallback时,抛出HystrixRuntimeException异常,指定fallback时,调用fallback方法降级处理,但获取不到失败原因,如果需要获取失败原因,可使用fallbackFactory,不启用断路器时,抛出feign.RetryableException异常。

注意上面的contextId, 当使用同一个名称或url来创建多个指向同一服务的feign client时, 需要使用contextId来避免配置bean的名称冲突。该属性可以改变feign 客户端的ApplicationContext的名称,覆盖feign客户端别名,作为客户端配置bean名称的一部分。

本文示例代码



认真生活,快乐分享
欢迎关注微信公众号:空山新雨的技术空间
微信公众号