一、简介
这一篇,准备来分析一下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 的执行流程,最后分析两种隔离策略的实现,并且对比了它们的异同。