前几天一直在外围转悠,试用了一下默认的 divide 插件,今天来研究下一个请求是如何通过 Soul 网关分发到真实服务节点的。
通过浏览器或 postman 发送一个 GET 请求。
http://localhost:9195/http/order/findById?id=95
selector 和 rule 默认都打开日志了,可以在 SoulBootstrapApplication 的 console 控制台看到如下日志如下:
2021-01-18 18:35:05.357 INFO 20836 --- [work-threads-17] o.d.soul.plugin.base.AbstractSoulPlugin : divide selector success match , selector name :/http
2021-01-18 18:35:05.357 INFO 20836 --- [work-threads-17] o.d.soul.plugin.base.AbstractSoulPlugin : divide selector success match , selector name :/http/order/findById
2021-01-18 18:35:05.358 INFO 20836 --- [work-threads-17] o.d.s.plugin.httpclient.WebClientPlugin : The request urlPath is http://10.0.0.12:8188/order/findById?id=95, retryTimes is 0
可以看到1~2行日志,显示匹配上了 divide selector,日志是在 AbstractSoulPlugin 这个类,Ctrl + Shift + N 找到这个类,再 Ctrl + F 搜索 “selector success match”,可以搜索到2个方法,1个 selectorLog() 1个 ruleLog(),这两个方法都在同一个 execute() 方法中调用了,在 execute() 方法第1行把断点打上,我们再请求一次。
// AbstractSoulPlugin.java
/**
* Process the Web request and (optionally) delegate to the next
* {@code SoulPlugin} through the given {@link SoulPluginChain}.
*
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next plugin
* @return {@code Mono<Void>} to indicate when request processing is complete
*/
@Override
public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
// 拿到插件 plugin 的名字
String pluginName = named();
final PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName);
// 如果插件的 enable 为 true 才进,否则调用责任链的下一个节点
if (pluginData != null && pluginData.getEnabled()) {
final Collection<SelectorData> selectors = BaseDataCache.getInstance().obtainSelectorData(pluginName);
if (CollectionUtils.isEmpty(selectors)) {
return handleSelectorIsNull(pluginName, exchange, chain);
}
final SelectorData selectorData = matchSelector(exchange, selectors);
if (Objects.isNull(selectorData)) {
return handleSelectorIsNull(pluginName, exchange, chain);
}
selectorLog(selectorData, pluginName);
final List<RuleData> rules = BaseDataCache.getInstance().obtainRuleData(selectorData.getId());
if (CollectionUtils.isEmpty(rules)) {
return handleRuleIsNull(pluginName, exchange, chain);
}
RuleData rule;
if (selectorData.getType() == SelectorTypeEnum.FULL_FLOW.getCode()) {
//get last
rule = rules.get(rules.size() - 1);
} else {
rule = matchRule(exchange, rules);
}
if (Objects.isNull(rule)) {
return handleRuleIsNull(pluginName, exchange, chain);
}
ruleLog(rule, pluginName);
return doExecute(exchange, chain, selectorData, rule);
}
return chain.execute(exchange);
}
根据 pluginName 获取 PluginData,debug 时,进去看下,里面有一个 Map(PLUGIN_MAP) 保存了默认14种插件数据,如下图所示,默认情况下只有 divide 插件是打开的(enabled = true),这个类是 BaseDataCache,可以从名字上看出这是一个缓存类。
所以在判断插件不为 null 并且 enabled 为 true 时,能进入的只有 divide 插件。
下面就是匹配 selector 和 rule 的逻辑,这里迎来了1个重点,doExecute 方法,它的签名是:
protected abstract Mono<Void> doExecute(ServerWebExchange exchange, SoulPluginChain chain, SelectorData selector, RuleData rule);
这是一个抽象方法,所在的类 AbstractSoulPlugin 是一个抽象类,这就是一个抽象模板方法,需要子类自己去实现,Ctrl + H 可以看到这么多子类:
此时可以看到 this 就是 DividePlugin 对象:
debug 进入方法,会进到 DividePlugin 子类的覆写方法里:
// DividePlugin.java
@Override
protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
assert soulContext != null;
final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);
// 这里又看到了 Cache 字样,可以想见,里面缓存了真实服务器节点的相关信息。
final List<DivideUpstream> upstreamList = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selector.getId());
if (CollectionUtils.isEmpty(upstreamList)) {
log.error("divide upstream configuration error: {}", rule.toString());
Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
if (Objects.isNull(divideUpstream)) {
log.error("divide has no upstream");
Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
// set the http url
String domain = buildDomain(divideUpstream);
// 获取真实服务器接口地址,此时为 http://10.0.0.12:8188/order/findById?id=95
String realURL = buildRealURL(domain, soulContext, exchange);
exchange.getAttributes().put(Constants.HTTP_URL, realURL);
// set the http timeout 设置超时时间
exchange.getAttributes().put(Constants.HTTP_TIME_OUT, ruleHandle.getTimeout());
// 设置重试次数
exchange.getAttributes().put(Constants.HTTP_RETRY, ruleHandle.getRetry());
return chain.execute(exchange);
}
这里首先拿到了 soulContext:(Context 模式)
根据选择器 ID 从 UpstreamCacheManager 实例里面获取真实服务器节点的相关信息:
从以上信息中,获取到真实服务器接口地址,设置到 ServerWebExchange 对象的 attributes 属性里。
最后,这里的 chain.execute(exchange) 与前面插件没有打开走的逻辑是一样的,都是调用责任链的下一个节点。
// 接口定义
public interface SoulPluginChain {
/**
* Delegate to the next {@code WebFilter} in the chain.
*
* @param exchange the current server exchange
* @return {@code Mono<Void>} to indicate when request handling is complete
*/
Mono<Void> execute(ServerWebExchange exchange);
}
// 默认实现是 SoulWebHandler 的私有内部静态类
// SoulWebHandler.java
private static class DefaultSoulPluginChain implements SoulPluginChain {
private int index;
private final List<SoulPlugin> plugins;
/**
* Instantiates a new Default soul plugin chain.
*
* @param plugins the plugins
*/
DefaultSoulPluginChain(final List<SoulPlugin> plugins) {
this.plugins = plugins;
}
/**
* Delegate to the next {@code WebFilter} in the chain.
*
* @param exchange the current server exchange
* @return {@code Mono<Void>} to indicate when request handling is complete
*/
@Override
public Mono<Void> execute(final ServerWebExchange exchange) {
return Mono.defer(() -> {
if (this.index < plugins.size()) {
SoulPlugin plugin = plugins.get(this.index++);
Boolean skip = plugin.skip(exchange);
if (skip) {
return this.execute(exchange);
}
return plugin.execute(exchange, this);
}
return Mono.empty();
});
}
}
代码进到 execute 方法里时,可以看到 index = 7,plugins 里有14个插件对象:
对于响应式编程,我也是才接触,先忽略掉,后面需要恶补一下。
Supplier 这个 lambda 表达式里的逻辑,根据 index 依次获取对应的 plugin 对象,如果 skip 为 true,直接跳过当前这个插件,再次执行当前这个 execute 方法,进入到下一个插件;如果 skip 为 false,执行这个插件的 execute 方法。
此时拿到的是 WebClientPlugin,skip 为 false,进入到 WebClientPlugin 的 execute 方法。
// WebClientPlugin.java
@Override
public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
assert soulContext != null;
// 把刚才封装进去真实服务器接口地址拿出来 http://10.0.0.12:8188/order/findById?id=95
String urlPath = exchange.getAttribute(Constants.HTTP_URL);
if (StringUtils.isEmpty(urlPath)) {
Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
long timeout = (long) Optional.ofNullable(exchange.getAttribute(Constants.HTTP_TIME_OUT)).orElse(3000L);
int retryTimes = (int) Optional.ofNullable(exchange.getAttribute(Constants.HTTP_RETRY)).orElse(0);
log.info("The request urlPath is {}, retryTimes is {}", urlPath, retryTimes);
HttpMethod method = HttpMethod.valueOf(exchange.getRequest().getMethodValue());
WebClient.RequestBodySpec requestBodySpec = webClient.method(method).uri(urlPath);
return handleRequestBody(requestBodySpec, exchange, timeout, retryTimes, chain);
}
private Mono<Void> handleRequestBody(final WebClient.RequestBodySpec requestBodySpec,
final ServerWebExchange exchange,
final long timeout,
final int retryTimes,
final SoulPluginChain chain) {
return requestBodySpec.headers(httpHeaders -> {
httpHeaders.addAll(exchange.getRequest().getHeaders());
httpHeaders.remove(HttpHeaders.HOST);
})
.contentType(buildMediaType(exchange))
.body(BodyInserters.fromDataBuffers(exchange.getRequest().getBody()))
.exchange()
.doOnError(e -> log.error(e.getMessage()))
.timeout(Duration.ofMillis(timeout))
.retryWhen(Retry.onlyIf(x -> x.exception() instanceof ConnectTimeoutException)
.retryMax(retryTimes)
.backoff(Backoff.exponential(Duration.ofMillis(200), Duration.ofSeconds(20), 2, true)))
.flatMap(e -> doNext(e, exchange, chain));
}
private Mono<Void> doNext(final ClientResponse res, final ServerWebExchange exchange, final SoulPluginChain chain) {
if (res.statusCode().is2xxSuccessful()) {
exchange.getAttributes().put(Constants.CLIENT_RESPONSE_RESULT_TYPE, ResultEnum.SUCCESS.getName());
} else {
exchange.getAttributes().put(Constants.CLIENT_RESPONSE_RESULT_TYPE, ResultEnum.ERROR.getName());
}
exchange.getAttributes().put(Constants.CLIENT_RESPONSE_ATTR, res);
return chain.execute(exchange);
}
在 doNext 方法中,把请求的响应及响应状态封装到了 ServerWebExchange 对象中。
最后,继续调用责任链的 execute 方法,进行下一个插件的逻辑,直到所有的插件都执行了一遍。
经过上面的调试,可以肯定的是2点:
1.通过 divide plugin 获取到真实服务器接口信息。
2.通过 WebClientPlugin 真正向服务器调用接口,获取返回信息。
在这个过程中,使用 ServerWebExchange 对象作为 Session 一样使用,至于为什么这样使用,还需要进一步研究。
整个调用链没有整个搞清楚,只了解了其中一部分,需要我们一步步深入,一点点攻破各个难点,各位,我们一起加油。