Soul网关源码分析-限流插件Hystrix

292 阅读7分钟

一、简介

这一篇,准备来分析一下Hystrix插件的处理逻辑,看看使用了Hystrix插件是如何处理限流的。

二、压测效果

首先,修改一下soul-admin中Hystrix插件的配置,修改后的配置如下所示: 从图中可以看到,我们将跳闸最小请求数设置为1,最大并发数也设置为1,方便测试,接下来用sb对http://localhost:9195/http/order/findById?id=3 这个请求进行压测,观察效果,使用命令如下:

 sb -u http://localhost:9195/http/order/findById?id=3 -c 2 -N 10

就用2个并发以及运行10秒测试,看看效果如何,压测运行结果如下:

PS C:\Users\wenhu> sb -u http://localhost:9195/http/order/findById?id=3 -c 2 -N 10
Starting at 2021/2/4 0:34:44
[Press C to stop the test]
3903    (RPS: 180.5)
---------------Finished!----------------
Finished at 2021/2/4 0:35:05 (took 00:00:21.6973249)
Status 500:    3566
Status 200:    337

RPS: 352.6 (requests/second)
Max: 509ms
Min: 0ms
Avg: 3ms

  50%   below 2ms
  60%   below 2ms
  70%   below 3ms
  80%   below 4ms
  90%   below 6ms
  95%   below 8ms
  98%   below 10ms
  99%   below 14ms
99.9%   below 201ms

可以看到,状态为500(失败)的请求达到3566次,而状态为200(成功)的只有337次 状态为500的就是被Hystrix插件拒绝的请求,我们再看看压测过程中,在soul网关打印的日志,如下:

2021-02-04 00:35:05.802 ERROR 30664 --- [-work-threads-4] o.d.soul.plugin.hystrix.HystrixPlugin    : hystrix execute have circuitBreaker is Open! groupKey:/http,commandKey:/http/order/findById
2021-02-04 00:35:05.802 ERROR 30664 --- [-work-threads-5] o.d.soul.plugin.hystrix.HystrixPlugin    : hystrix execute have circuitBreaker is Open! groupKey:/http,commandKey:/http/order/findById
2021-02-04 00:35:05.802  INFO 30664 --- [-work-threads-6] o.d.soul.plugin.base.AbstractSoulPlugin  : hystrix selector success match , selector name :/http-hystrix
2021-02-04 00:35:05.802  INFO 30664 --- [-work-threads-6] o.d.soul.plugin.base.AbstractSoulPlugin  : hystrix selector success match , selector name :/hystrix-http/http/order/findById
2021-02-04 00:35:05.806  INFO 30664 --- [-work-threads-7] o.d.soul.plugin.base.AbstractSoulPlugin  : hystrix selector success match , selector name :/http-hystrix
2021-02-04 00:35:05.806 ERROR 30664 --- [-work-threads-6] o.d.soul.plugin.hystrix.HystrixPlugin    : hystrix execute have circuitBreaker is Open! groupKey:/http,commandKey:/http/order/findById
2021-02-04 00:35:05.806  INFO 30664 --- [-work-threads-7] o.d.soul.plugin.base.AbstractSoulPlugin  : hystrix selector success match , selector name :/hystrix-http/http/order/findById
2021-02-04 00:35:05.806 ERROR 30664 --- [-work-threads-7] o.d.soul.plugin.hystrix.HystrixPlugin    : hystrix execute have circuitBreaker is Open! groupKey:/http,commandKey:/http/order/findById

从日志中可也可以看出来,日志级别为ERROR的,有“hystrix execute have circuitBreaker is Open”关键字的都是被限流的请求,接下来我们从源码的角度看看是如何实现的。

三、源码分析

前面演示完对Hrstrix 的压测后,我们就开始分析 HystrixPlugin 的具体实现。HystrixPlugin 继承于模板类 AbstractSoulPlugin,所以我们直接看其 doExecutor 方法,如下:

    @Override
    protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
        final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
		//... 忽略一些检测代码
		//构建 Command 对象,上面示例中 HystrixObservableCommand 就是其中一种 Command 实现。
        Command command = fetchCommand(hystrixHandle, exchange, chain);
        return Mono.create(s -> {
        	//执行具体的请求
            Subscription sub = command.fetchObservable().subscribe(s::success,
                    s::error, s::success);
			s.onCancel(sub::unsubscribe);
			//熔断已经打开,这里日志进行输出
            if (command.isCircuitBreakerOpen()) {
                log.error("hystrix execute have circuitBreaker is Open! groupKey:{},commandKey:{}", hystrixHandle.getGroupKey(), hystrixHandle.getCommandKey());
            }
        }).doOnError(throwable -> {
            //... 省略错误处理
            //跳回插件连处理
            chain.execute(exchange);
        }).then();
    }

通过上面代码我们可以总结出几个关键点:

fetchCommand 方法构建 Command 对象。 command.fetchObservable().subscribe 发起真正的任务请求。 doOnError 方法进行错误处理,并且跳回插件链,但是 Hystrix 的 fallback 逻辑不是在这里。 执行成功是在哪里跳回插件链的? 我们带着上面的关键点和疑问进行逐一的分析,首先是 fetchCommand 方法

private Command fetchCommand(final HystrixHandle hystrixHandle, final ServerWebExchange exchange, final SoulPluginChain chain) {
	//基于信号量
    if (hystrixHandle.getExecutionIsolationStrategy() == HystrixIsolationModeEnum.SEMAPHORE.getCode()) {
        return new HystrixCommand(HystrixBuilder.build(hystrixHandle),
            exchange, chain, hystrixHandle.getCallBackUri());
    }
    //基于线程池
    return new HystrixCommandOnThread(HystrixBuilder.buildForHystrixCommand(hystrixHandle),
        exchange, chain, hystrixHandle.getCallBackUri());
}

Hystrix 提供的隔离策略由两种,一种是基于线程的,但是线程隔离会带来线程开销,有些场景(比如无网络请求场景)可能会因为用开销换隔离得不偿失,为此 hystrix 提供了另外一种隔离策略:信号量隔离,当服务的并发数大于信号量阈值时将进入fallback。

下面来看看基于信号量的隔离策略的实现:
HystrixCommand 继承于 HystrixObservableCommand,我们先来看 construct 方法:

protected Observable<Void> construct() {
    return RxReactiveStreams.toObservable(chain.execute(exchange));
}

通过上面的代码可以看到,HrstrixPlugin 本身并不提供额外的功能,只是把网关的后续执行包装到 Hystrix 中,command.fetchObservable()方法注册任务执行事件:

@Override
public Observable<Void> fetchObservable() {
    return this.toObservable();
}

上面的代码关键是理解一下 toObservable() 的用法,HystrixCommand 有两种注册方式。

  • observe():注册的事件会立即得到执行,subscribe 会触发后续执行结果的发送。
  • toObservable():调用 subscribe 方法后才会真正执行注册的事件。 所以真正发起任务执行的是在 doExecutor 方法中调用了 subscribe 方法的地方。
    理解了任务执行的内容,也了解任务发起的地方,那任务执行失败了怎么办? 所以接下来再看一下对于 Hrstrix fallback 的处理。
    @Override
    protected Observable<Void> resumeWithFallback() {
        return RxReactiveStreams.toObservable(doFallback());
    }
	
    private Mono<Void> doFallback() {
        if (isFailedExecution()) {
            log.error("hystrix execute have error: ", getExecutionException());
        }
        final Throwable exception = getExecutionException();
        //真正执行 fallback 逻辑的地方
        return doFallback(exchange, exception);
    }

esumeWithFallback 是 HystrixObservableCommand 提供的方法,会在任务执行失败或者熔断打开的时候被执行,其是返回一个新的 Observable,HystrixCommand 这里是重写了该方法,真正的事件处理是 doFallback 方法中调用的 doFallback(exchange, exception) 方法,后者来自 HystrixCommand 实现的另外一个接口 Command,这是一个 default 方法。

default Mono<Void> doFallback(ServerWebExchange exchange, Throwable exception) {
	//回调地址为空
    if (Objects.isNull(getCallBackUri())) {
		...
        return WebFluxResultUtils.result(exchange, error);
    }
    //不为空的话,通过 DispatcherHandler 重新发起一次 fallback url 的请求
    //这里要注意一下,上一篇提到过的 fallback 接口,只能在网关这里实现,需要用户自己实现,暂时不支持其他服务接口
    DispatcherHandler dispatcherHandler =
        SpringBeanUtils.getInstance().getBean(DispatcherHandler.class);
    ServerHttpRequest request = exchange.getRequest().mutate().uri(getCallBackUri()).build();
    ServerWebExchange mutated = exchange.mutate().request(request).build();
    return dispatcherHandler.handle(mutated);
}

到这里我们就理清楚了其中几个关键点: construct + fetchObservable 负责注册请求任务事件,其中 fetchObservable 是 soul 自定义接口 Command 的方法,为了提供统一的 API 给 doExecutor 调用。 事件由 doExecutor 方法中的 subscribe 调用真正发起执行 resumeWithFallback 触发 Hrstrix fallback 机制,接口 Command 中 default 方法 doFallback 负责真正执行 fallback 逻辑。

分析完基于信号量的隔离策略之后,我们在反过来分析一下基于线程的隔离策略的实现HystrixCommandOnThread,我们主要分析 HystrixCommandOnThread 和 HystrixCommand 的几个不同的地方:

  • 1.继承的父类不一样

HystrixCommandOnThread 的父类是 com.netflix.hystrix.HystrixCommand,主要这个类是位于 hystrix 包下,和上面基于信号量的实现 HystrixCommand 不一样,只是类名称一样而已。

  • 2.注册事件的方法不一样
protected Mono<Void> run() {
    RxReactiveStreams.toObservable(chain.execute(exchange)).toBlocking().subscribe();
    return Mono.empty();
}

这里注册的方法是 run,而基于信号量策略的是 construct,所以到这里就清楚了为什么 Command 会额外定义一个对外注册任务的 API,因为它们两种策略官方提供的注册接口不一样,需要适配一下。 这里另外一个注意的点是 .toBlocking().subscribe(),因为 HystrixCommandOnThread 是基于线程隔离的,也就是任务会在Hrstrix 提供的线程中执行,而不是 soul 网关本身执行的线程了,所以这里其实要变成同步阻塞的执行,并且在执行完毕后返回一个空的 Mono 对象。

  • 3.fallback 触发的方法不一样
//基于信号量的是 resumeWithFallback 方法
protected Mono<Void> getFallback() {
    if (isFailedExecution()) {
        log.error("hystrix execute have error: ", getExecutionException());
    }
    final Throwable exception = getExecutionException();
    return doFallback(exchange, exception);
}

到这里 HystrixCommandOnThread 也分析完了,除了这些不同点之外,它们都是通过 Command 接口提供统一的 API,并且使用其相同的 default 方法实现。

总结

本篇文章,我们从如何使用 hystrix 框架开始,然后分析了 HystrixPlugin doExecutor 的执行流程,最后分析两种隔离策略的实现,并且对比了它们的异同。