前言
在上一篇文章《Soul网关源码学习(14)- hystrix,resilienc4j,sentinel 插件的使用和对比》中,我们学习了 hystrix,resilienc4j,sentinel 插件的使用,从这一章节起,我们将从 HystrixPlugin 开始逐个分析它们源码上的实现。
Hystrix
什么是Hystrix?
Hystrix是一个库,可通过添加延迟公差和容错逻辑来帮助您控制分布式服务之间的交互。Hystrix通过隔离服务之间的访问点,停止服务之间的级联故障并提供后备选项来实现此目的,所有这些都可以改善系统的整体弹性。
怎么使用?
如果某程序或class要使用Hystrix,只需简单继承HystrixCommand/HystrixObservableCommand并重写run()/construct(),然后调用程序实例化此class并执行execute()/queue()/observe()/toObservable()。
这里我以 HystrixObservableCommand 做为一个简单的示例:
- 包装一个 http 请求
- 失败调用 fallback
public class FindByIdCommand extends HystrixObservableCommand<Response> {
...
//请求方法,通过OkHttp 请求接口 findById
private Response doRequest() {
String url = "http://localhost:9195/http/order/findById?id=1";
Request request = new Request.Builder().url(url).get().build();
try {
return OK_HTTP_CLIENT.newCall(request).execute();
} catch (IOException e) {
throw new HystrixBadRequestException(e.getMessage());
}
}
//通过 construct 方法包装你的 Http 请求流程
@Override
protected Observable<Response> construct() {
return Observable.create(subscriber -> {
try {
Response response = doRequest();
subscriber.onNext(response);
} catch (Exception e) {
subscriber.onError(e);
}
});
}
// 重写 resumeWithFallback,hystrix 会在抛出错误的时候,调用该方法执行 Fallback 操作
@Override
protected Observable<Response> resumeWithFallback() {
return Observable.create(subscriber -> doFallback());
}
// 真正执行 fallback 操作
private void doFallback(){
System.out.println("do fallback!!");
}
}
上面注释都已经说的比较清楚了,这里就不再次解释了,下面我测试一下请求成功的case:
public static void main(String[] args) throws IOException {
FindByIdCommand command = new FindByIdCommand("order");
command.toObservable().subscribe(new Subscriber<Response>() {
...
@Override
public void onNext(Response response) {
//处理请求返回
if (response.isSuccessful()) {
try {
System.out.println(response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
System.in.read();
}
运行后打印结果:
{"id":"1","name":"hello world findById","port":0,"count":0}
接下来我们修改 doRequest 方法中的请求端口,让其超时(这里省略了修改 OkHttp 的超时配置):
//由9195改成9197
String url = "http://localhost:9197/http/order/findById?id=1";
再次运行,打印结果:
HystrixPlugin 的实现
简单演示完 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());
}
隔离策略 - 基于信号量:HystrixCommand
Hystrix 提供的隔离策略由两种,一种是基于线程的,但是线程隔离会带来线程开销,有些场景(比如无网络请求场景)可能会因为用开销换隔离得不偿失,为此 hystrix 提供了另外一种隔离策略:信号量隔离,当服务的并发数大于信号量阈值时将进入fallback。
HystrixCommand 继承于 HystrixObservableCommand,和文章开始的示例是一样的,所以首先我们先来看其 construct 方法:
protected Observable<Void> construct() {
return RxReactiveStreams.toObservable(chain.execute(exchange));
}
通过上面的代码可以看到逻辑非常简单,就是跳回插件链执行后续的插件,因为 HrstrixPlugin 本身并不提供额外的功能,就仅仅是把网关的后续执行包装到 Hystrix 中去而已。所以,上面关于“执行成功是在哪里跳回插件链的?”的问题的答案也是显而易见了。
command.fetchObservable()方法注册任务执行事件:
@Override
//这里为什么会再次 toObservable 呢?而不是直接使用 construct 方法呢?
//主要是为了统一对外的API,这个分析到后面另外一个策略就很清楚了。
public Observable<Void> fetchObservable() {
return this.toObservable();
}
上面的代码关键是理解一下 toObservable() 的用法,HystrixCommand 有两种注册方式。
- observe():返回一个 hot 的 Observable,也就是注册的事件会立即得到执行,subscribe 会触发后续执行结果的发射。
- toObservable():返回一个“冷”的Observable,调用 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);
}
resumeWithFallback 是 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 的几个不同的地方:
继承的父类不一样
HystrixCommandOnThread 的父类是 com.netflix.hystrix.HystrixCommand,主要这个类是位于 hystrix 包下,和上面基于信号量的实现 HystrixCommand 不一样,只是类名称一样而已。
注册事件的方法不一样
protected Mono<Void> run() {
RxReactiveStreams.toObservable(chain.execute(exchange)).toBlocking().subscribe();
return Mono.empty();
}
这里注册的方法是 run,而基于信号量策略的是 construct,所以到这里就清楚了为什么 Command 会额外定义一个对外注册任务的 API,因为它们两种策略官方提供的注册接口不一样,需要适配一下。
这里另外一个注意的点是 .toBlocking().subscribe(),因为 HystrixCommandOnThread 是基于线程隔离的,也就是任务会在Hrstrix 提供的线程中执行,而不是 soul 网关本身执行的线程了,所以这里其实要变成同步阻塞的执行,并且在执行完毕后返回一个空的 Mono 对象。
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 的执行流程,最后分析两种隔离策略的实现,并且对比了它们的异同。下篇文章,我们将开始分析另外两个熔断框架 resilienc4j 和 sentinel 的集成原理。