高可用系统常用利器(一) - 服务降级 Hystrix

0、前言

互联网高并发系统一般QPS、TPS都比较高,当流量比较大的时候,有三种常见手段可以保证系统高可用和稳定:

  • 缓存
  • 服务降级与熔断
  • 服务限流

缓存的目的是减少数据库的压力和提升系统访问速度,使用缓存要需要考虑缓存穿透、缓存失效以及高并发情况下DB与缓存不一致的问题;服务降级与熔断是用来解决核心服务受到非核心服务的影响的一种手段,比如用户下单页面的优惠券展示,当优惠券展示的服务挂掉后,可以将这个服务屏蔽掉,不要影响用户正常的下单;有些场景是不可以用缓存和服务降级来解决,比如用户下单、抢购等,这个时候就需要用到服务限流,服务限流是用来在一定时间内限制访问服务的请求量,达到保护系统的手段。
本篇主要讲解服务降级中Hystrix技术栈的使用。

一、什么是Hystrix

在互联网分布式应用中,常常会将核心业务抽取出来当做独立的服务供别的服务使用。举个栗子,电商系统中会拆分成订单、库存、评论、C端展示等多个服务,每个服务都会有专门的RD进行维护。

用户下单的时候,首先会去订单服务中调用创建订单接口,创建订单接口又会去调用库存服务去查询用户选用的商品库存是否充足,如果
库存服务因为网络、bug等问题挂掉后,会导致创建订单的线程一直等待挂起,如果此时有大量请求进入系统会导致大量线程挂起,从而使整个系统瘫痪。

解决措施之一就是如果库存服务不可用后,可以用熔断的思想,使其快速失败返回一个库存不足的response,这样避免调用方服务不可用(可以通过代码容错进行实现,设置RPC接口的调用超时时间)。

又比如用户下单页面优惠券展示,在调用优惠券查询接口时发生故障,会导致用户下单无法继续。这个时候就可以用降级的思想,使优惠券查询接口直接返回空,这样用户端最多就看不到所能使用的优惠券,但是可以正常下单,不影响核心业务的使用。

但是不能一次调用超时就直接熔断或降级,如何设置次数、发生熔断的时间、发生熔断之后的后续操作等都需要有个服务进行处理,而Hystrix就提供了一种服务降级与熔断的实现。

Hystrix的介绍

Hystrix是netflix开源的一个容灾框架,解决当外部依赖故障时拖垮业务系统、甚至引起雪崩的问题
(wiki see:https://github.com/Netflix/Hystrix/wiki)。
Hystrix是豪猪的意思。豪猪是一种全身是刺的动物,netflix使用Hystrix意味着Hystrix能够像豪猪的刺一样保护你的应用。Hystrix是Netflix(网飞公司)开源的一款容错系统。该框架为分布式环境提供了弹性、增加了延迟容忍和容错。

Hystrix提供了如下操作:

  • 降级:超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回兜底数据;
  • 隔离(线程池隔离和信号量隔离):限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其他服务调用。
  • 融断:当失败率达到阀值自动触发降级(如因网络故障/超时造成的失败率高),熔断器触发的快速失败会进行快速恢复;
  • 缓存:提供了请求缓存、请求合并实现;
  • 支持实时监控、报警、控制(修改配置)。

二、Hystrix的原理

下面简单介绍下Hystrix提供的降级、熔断和缓存的实现原理。

2.1 工作主流程

  • 1、通过继承HystrixCommand / HystrixObservableCommand,生成Hystrix命令;
  • 2、执行命令
    • execute() — 同步,直接返回结果,execute()=queue().get();
    • queue() — 异步,返回Future,queue()=toObservable().toBlocking().toFuture();
    • observe() —请求返回一个观察者,观察者需要注册行为监听器,处理返回结果。(在调用observe时就已经触发请求);
    • toObservable() — 返回一个延迟的观察者,在注册监听器的动作完成后才会触发请求。
  • 3、判断缓存中是否已经有返回数据;
  • 4、判断断路器是否开启;如果开启,则进入步骤8,否则进入步骤5;
  • 5、判断Thread Pool/Semaphore 是否已满
    如果并发量已满,则执行步骤8,否则进入步骤6
  • 6、执行;如果执行失败,Hystrix将会执行走向步骤8,并抛弃最终返回的结果;如果执行结束,Hystrix会返回结果并提供日志和度量数据
  • 7、计算回路健康度
    Hystrix报告成功、失败、拒绝的结果给断路器(circuit breaker),circuit breaker维护一个滚动的计数器计算统计数据。circuit breaker使用这些数据去决定是否将状态置为开启
  • 8、执行fallback
    4/5/6不满足以及异常都会进入fallback,实现HystrixCommand.getFallback()或HystrixObservableCommand.resumeWithFallback()

在这里插入图片描述

2.2 断路器

当访问的请求数大于阈值数(默认10秒内大于20次),并且错误的比率超过阈值(默认是50%),开启断路器,拒绝后续请求。

指定时间窗口过后,允许一个请求去尝试调用,根据请求的结果决定是否关闭断路器。
在这里插入图片描述

三、Hystrix的使用

先来看看如何搭建一个使用Hystrix实现熔断的案例。

1、添加 Maven 依赖

(与Guava依赖包会有冲突,需要解决冲突)

    <dependency>
      <groupId>com.netflix.hystrix</groupId>
      <artifactId>hystrix-core</artifactId>
      <version>1.5.12</version>
    </dependency>

    <dependency>
      <groupId>com.netflix.hystrix</groupId>
      <artifactId>hystrix-javanica</artifactId>
      <version>1.5.12</version>
    </dependency>

2、配置 Hystrix Aspect:在 applicationContext.xml 中加入

<aop:aspectj-autoproxy/>
<bean id="hystrixAspect" class="com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect"></bean>
<context:annotation-config/>

3、demo

写一个库存服务StockService,用来查询商品库存,并设置当调用方调用时接口超时超过400ms就会进入熔断,快速失败。

Slf4j
Service("stockService")
public class StockServic{
    public String queryStock () {
        return "库存充足;
    }
}

使用Hystrix,封装StockService:

Slf4j
Component
public class StockServiceCommand {

    @Autowired
    private StockServic stockService;

    @HystrixCommand(groupKey = "stockService", commandKey = "queryStock", fallbackMethod = "queryStockFallback",
            commandProperties = {
                    //指定多久超时,单位毫秒。超时进fallback
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "400"),
                    //判断熔断的最少请求数,默认是10;只有在一个统计窗口内处理的请求数量达到这个阈值,才会进行熔断与否的判断
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
                    //判断熔断的阈值,默认值50,表示在一个统计窗口内有50%的请求处理失败,会触发熔断
                    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "10")
            }
    )
    public String queryStock() {
        // 模拟网络延时:超过400ms,就会进入熔断
        try {
            Thread.sleep(1000);
        }catch (Exception e) {
            log.error("thred sleep error,e:", e);
        }
        return stockService.queryStock();
    }

@HystrixCommand(groupKey = "stockService, commandKey = "queryStockFallback")
    public String queryStockFallback(Throwable e) {
        log.error("Hystrix fall method, queryStockFallback error, e:", e);
        return "库存不足,快速失败";
    }

}

测试:

Service("orderService")
@Slf4j
public class OrderService {

    @Autowired
    private StockServiceCommand stockServiceCommand;

    public String createOrder () {
        log.info("用户开始下单了");
        // 调用库存服务
        String result = stockServiceCommand.queryStock();
        return result;
    }
}

@Controller
public class OrderController {

    @Autowired
    private OrderService orderService;

    @RequestMapping(value = "/createOrder", method = RequestMethod.GET)
    @ResponseBody
    public String createOrder() {
        return orderService.createOrder();
    }
}

测试结果:
可以看到订单服务调用StockService时由于网络超时,进入到StockService fallback method方法中进行快速失败:
在这里插入图片描述

而调用方也会得到快速失败的结果:
在这里插入图片描述

这样调用方不至于由于网络超时而一直在等待。

四、Hystrix常用参数介绍

下面介绍下Hystrix常用参数。

  • groupKey:方法分组名,影响展示分组和 THREAD 模式下线程池的选择,不设置时的默认值: @HystrixCommand 注解的方法所在的类名;
  • commandKey:方法名, 方法之间只靠 commandKey 互相区分, 若 2 个方法具有相同的 commandKey 将使用同一套方法配置及健康值
    不设置时的默认值: @HystrixCommand 注解的方法名;
  • fallbackMethod:指定 fallback 方法,默认值: 无 fallback 方法, Hystrix 需要执行 fallback 时抛出异常

falback 方法需要和原方法保持参数列表和返回类型的一致
可以在参数列表的最后新加一个 Throwable 类型的参数, 接收 Hystrix 返回的执行异常
@param ex Hystrix 执行异常, 可能是以下几种:

  • 1、各种业务异常:原方法执行时抛出未捕获异常;
  • 2、HystrixTimeoutException:Hystrix检测到原方法执行超时;
  • 3、HystrixSemaphoreRejectionException : SEMAPHORE 模式下信号量获取失败;
  • 4、HystrixShortCircuitOpenException:处于熔断或手动降级状态;
  • 5、 RejectedExecutionException:THREAD模式下线程池拒绝任务;

Hystrix提供了两种模式:SEMAPHORE 和 Thread模式。

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;

import static com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager.*;

/*
 * 注意: @HystrixCommand 注解方式依赖 AOP, 不支持在同一个类的内部方法之间直接调用, 必须将被调用类作为 bean 注入并调用
 */
public class DemoCircuitBreakerAnnotation {

    /**
     * 使用 SEMAPHORE 模式及通用参数说明
     */
    @HystrixCommand(
            groupKey = "GroupAnnotation",
            commandKey = "HystrixAnnotationSemaphore",
            fallbackMethod = "HystrixAnnotationSemaphoreFallback",
            commandProperties = {
                /*
                 * 以 SEMAPHORE (信号量)模式执行, 原方法将在调用此方法的线程中执行
                 *
                 * 如果原方法无需信号量限制, 可以选择使用 NONE 模式
                 * NONE 模式相比 SEMAPHORE 模式少了信号量获取和判断的步骤, 效率相对较高, 其余执行流程与 SEMAPHORE 模式相同
                 *
                 * 默认值: THREAD
                 */
                @HystrixProperty(name = EXECUTION_ISOLATION_STRATEGY, value = "SEMAPHORE"),
                /*
                 * 执行 run 方法的信号量上限, 即由于方法执行未完成停留在 run 方法内的线程最大个数
                 * 执行线程退出 run 方法后释放信号量, 其他线程获取不到信号量无法执行 run 方法
                 *
                 * 默认值: 1000, SEMAPHORE 模式下有效
                 */
                @HystrixProperty(name = EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS, value = "100"),
                /*
                 * 执行 fallback 方法的信号量上限
                 *
                 * 注意: 所有模式(NONE|SEMAPHORE|THREAD) fallback 的执行都受这个参数影响
                 *
                 * 默认值: Integer.MAX_VALUE
                 */
                @HystrixProperty(name = FALLBACK_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS, value = "1000"),
                /*
                 * 超时时间参数
                 * 在 SEMAPHORE 模式下, 方法超时后 Hystrix 不会中断原方法的执行线程, 只标记这次方法的执行结果为失败(影响方法的健康值)
                 * 同时另开一个线程执行 fallback, 最终返回 fallback 的结果
                 *
                 * 默认值: 1000
                 */
                @HystrixProperty(name = EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "500"),
                /*
                 * 方法各项指标值存活的滑动时间窗口长度, 每经过一个时间窗口长度重置各项指标值, 比如: 方法的健康值
                 *
                 * 默认值: 10000
                 */
                @HystrixProperty(name = METRICS_ROLLING_STATS_TIME_IN_MILLISECONDS, value = "10000"),
                /*
                 * 滑动时间窗口指标采样的时间分片数, 分片数越高时, 指标汇总更新的频率越高, 指标值的实时度越好, 但同时也占用较多 CPU
                 * 采样过程: 将一个滑动时间窗口时长根据分片数等分成多个时间分片, 每经过一个时间分片将最新一个时间分片的内积累的统计数据汇总更新到时间窗口内存活的已有指标值中
                 *
                 * 注意: 这个值只影响 Hystrix Monitor 上方法指标值的展示刷新频率,不影响熔断状态的判断
                 *
                 * 默认值: 10
                 */
                @HystrixProperty(name = METRICS_ROLLING_STATS_NUM_BUCKETS, value = "10"),
                /*
                 * 健康值采样的间隔, 相当于时间片长度, 每经过一个间隔将这个时间片内积累的统计数据汇总更新到时间窗口内存活的已有健康值中
                 *
                 * 健康值主要包括: 方法在滑动时间窗口内的总执行次数、成功执行次数、失败执行次数
                 *
                 * 默认值: 500
                 */
                @HystrixProperty(name = METRICS_HEALTH_SNAPSHOT_INTERVAL_IN_MILLISECONDS, value = "500"),
                /*
                 * 一个滑动时间窗口内, 方法的执行次数达到这个数量后方法的健康值才会影响方法的熔断状态
                 *
                 * 默认值: 20
                 */
                @HystrixProperty(name = CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "10"),
                /*
                 * 一个采样滑动时间窗口内, 方法的执行失败次数达到这个百分比且达到上面的执行次数要求后, 方法进入熔断状态, 后续请求将执行 fallback 流程
                 *
                 * 默认值: 50
                 */
                @HystrixProperty(name = CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "50"),
                /*
                 * 熔断状态停留时间, 方法进入熔断状态后需要等待这个时间后才会再次尝试执行原方法重新评估健康值. 再次尝试执行原方法时若请求成功则重置健康值
                 *
                 * 默认值: 5000
                 */
                @HystrixProperty(name = CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS, value = "5000")
            })
    public String HystrixAnnotationSemaphore(String param) {
        return "Run with " + param;
    }
    public String HystrixAnnotationSemaphoreFallback(String param, Throwable ex) {
        return String.format("Fallback with param: %s, exception: %s", param, ex);
    }
}

2、THREAD 模式 + 通用参数说明

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;

import static com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager.*;

/*
 * 注意: @HystrixCommand 注解方式依赖 AOP, 不支持在同一个类的内部方法之间直接调用, 必须将被调用类作为 bean 注入并调用
 */
public class DemoCircuitBreakerAnnotation {

    /**
     * 使用 THREAD 模式及线程池参数、通用参数说明
     */
    @HystrixCommand(
            groupKey = "GroupAnnotation",
            commandKey = "HystrixAnnotationThread",
            fallbackMethod = "HystrixAnnotationThreadFallback",
            /*
             * 线程池名, 具有同一线程池名的方法将在同一个线程池中执行
             *
             * 默认值: 方法的groupKey
             */
            threadPoolKey = "GroupAnnotationxThreadPool",
            threadPoolProperties = {
                /*
                 * 线程池Core线程数及最大线程数
                 *
                 * 默认值: 10
                 */
                @HystrixProperty(name = CORE_SIZE, value = "10"),
                /*
                 * 线程池线程 KeepAliveTime 单位: 分钟
                 *
                 * 默认值: 1
                 */
                @HystrixProperty(name = KEEP_ALIVE_TIME_MINUTES, value = "1"),
                /*
                 * 线程池最大队列长度
                 *
                 * 默认值: -1, 此时使用 SynchronousQueue
                 */
                @HystrixProperty(name = MAX_QUEUE_SIZE, value = "100"),
                /*
                 * 达到这个队列长度后, 线程池开始拒绝后续任务
                 *
                 * 默认值: 5, MaxQueueSize > 0 时有效
                 */
                @HystrixProperty(name = QUEUE_SIZE_REJECTION_THRESHOLD, value = "90"),
            },
            commandProperties = {
                /*
                 * 以 THREAD (线程池)模式执行, run 方法将被一个线程池中的线程执行
                 *
                 * 注意: 由于有额外的线程调度开销, THREAD 模式的性能不如 NONE 和 SEMAPHORE 模式, 但隔离性比较好
                 *
                 * 默认值: THREAD
                 */
                @HystrixProperty(name = EXECUTION_ISOLATION_STRATEGY, value = "THREAD"),
                /*
                 * 方法执行超时后是否中断执行线程
                 *
                 * 默认值: true, THREAD 模式下有效
                 */
                @HystrixProperty(name = EXECUTION_ISOLATION_THREAD_INTERRUPT_ON_TIMEOUT, value = "true"),
                /*
                 * 超时时间参数
                 * 在 THREAD 模式下, 方法超时后 Hystrix 默认会中断原方法的执行线程, 并标记这次方法的执行结果为失败(影响方法的健康值)
                 * 同时另开一个线程执行 fallback, 最终返回 fallback 的结果
                 *
                 * 默认值: 1000
                 */
                @HystrixProperty(name = EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "500")
                /*
                 * 其余参数参考上面的例子, 或者使用默认值
                 */
            })
    public String HystrixAnnotationThread(String param) {
        return "Run with " + param;
    }
    public String HystrixAnnotationThreadFallback(String param, Throwable ex) {
        return String.format("Fallback with param: %s, exception: %s", param, ex);
    }
}

猜你喜欢

转载自blog.csdn.net/noaman_wgs/article/details/88293347