Soul网关第11天:深度学习接入SpringCloud

334 阅读3分钟

一、背景

目前Soul网关能代理的协议或者叫服务类型有

  • http (插件名 divide)
  • SpringCloud
  • Dubbo
  • SOFA
  • Tars

Soul 使用SPI做的插件化设计,代码层次清晰。接下来打算深度的去学习代理服务的代码设计。但精力有限,感觉能用上网关的业务架构,不是微服务只是负载加Web服务器的可能性不大,另外SOFA、Tars两种服务框架自己都没用过。

综上,接下来几天打算看 SpringCloud 和 Dubbo 服务接入代码,在周末的时候,补一补类图和时序图。

二、启动服务

  1. 启动 Eureka
  2. 启动控制台(关掉不相关插件,只留SpringCloud)
  3. 网关(去掉不相关的依赖如Dubbo、SOFA)
  4. 启动两个 SpringCloud-example 实例服务

可以在 Eureka 上看到服务正常

请求多次发现可以正常转发到两台业务服务上。PS:这里日志里有个警告是什么鬼,记录下以后看看

三、知识点

时间有限,我来罗列知识点,同学们课后学吧,哈哈偷懒~~

1、WebHandler

网关使用的是 Spring-WebFlux,所有没有 Servlet 也就没有 Filter,但在 WebFlux 框架中 WebHandler 替代 Filter,其实 zuul、Spring Cloud GateWay 不也是各种 Filter,对流量的链式处理吗

所以入口 SoulWebHandler#handle

    @Override
    public Mono<Void> handle(@NonNull final ServerWebExchange exchange) {
        MetricsTrackerFacade.getInstance().counterInc(MetricsLabelEnum.REQUEST_TOTAL.getName());
        Optional<HistogramMetricsTrackerDelegate> startTimer = MetricsTrackerFacade.getInstance().histogramStartTimer(MetricsLabelEnum.REQUEST_LATENCY.getName());
        return new DefaultSoulPluginChain(plugins).execute(exchange).subscribeOn(scheduler)
                .doOnSuccess(t -> startTimer.ifPresent(time -> MetricsTrackerFacade.getInstance().histogramObserveDuration(time)));
    }

2、SoulPluginChain

使用插件对流量进行链式处理,这里区别了 Zuul 使用 Filter。

插件入口类 DefaultSoulPluginChain#execute

        @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();
            });
        }

插件列表

虽然我从控制台关了各类干扰插件,但是抵不住pom文件加载了很多无关插件(放心这些插件后续都会学习的)

我们一个个插件连蒙带猜走一遍:

1) GlobalPlugin

算了,车开走了(其实是懒了再看吧)。

2) SpringCloudPlugin

这里有个模板方法提一下, AbstractSoulPlugin#execute写好了处理 selector 和 rule 存在不存在走不同分支的逻辑,然后在再调用具体插件自定义好的 doExecute 方法

    @Override
    public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
        String pluginName = named();
        final PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName);
        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);
    }

可以看到只有一个插件是打开的,

在那句 if (pluginData != null && pluginData.getEnabled()) {},我们可以直接略过控制台这些插件,只看打开的SpringCloud插件了。

ps: 我们可以看到Soul为了性能,连日志打印都是支持动态开关 ruleLog(rule, pluginName)

ps: 这里还有一个有意思的地方就是抛错误时,只检查 divide、dubbo、springCloud 三类插件的selector,为什么不检查SOFA、Tars,以后再看看,记录下来

    public static Mono<Void> checkSelector(final String pluginName, final ServerWebExchange exchange, final SoulPluginChain chain) {
        if (PluginEnum.DIVIDE.getName().equals(pluginName)
                || PluginEnum.DUBBO.getName().equals(pluginName)
                || PluginEnum.SPRING_CLOUD.getName().equals(pluginName)) {
            log.error("can not match selector data: {}", pluginName);
            Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_SELECTOR.getCode(), SoulResultEnum.CANNOT_FIND_SELECTOR.getMsg(), null);
            return WebFluxResultUtils.result(exchange, error);
        }
        return chain.execute(exchange);
    }

回到SpringCloudPlugin,可以看到一顿操作,匹配出对应的rule啦

也就是能走一走SpringCloudPlugin#doExecute方法啦,又是一顿操作,其实就是补充了 ServerWebExchange 实现类 DefaultServerWebExchange 请求上下文信息。

3)WebClientPlugin

插件链上不只是会运行控制台配置的插件,还会运行一些默认插件,也就是和控制台无关,为了逻辑看起来清晰有一些默认插件,比如没有上车的 1)GlobalPlugin 和这一小节要讲的 WebClientPlugin

    @Override
    public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
        final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
        assert soulContext != null;
        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);
    }

一顿操作在 exchange 中加入了 http 请求需要的参数,然后使用 WebFlux 框架发起http请求,具体怎么实现以后讲讲 Project Reactor 和 Spring-WebFlux 。

4)WebClientResponsePlugin

最后一个插件

    @Override
    public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
        return chain.execute(exchange).then(Mono.defer(() -> {
            ServerHttpResponse response = exchange.getResponse();
            ClientResponse clientResponse = exchange.getAttribute(Constants.CLIENT_RESPONSE_ATTR);
            if (Objects.isNull(clientResponse)
                    || response.getStatusCode() == HttpStatus.BAD_GATEWAY
                    || response.getStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR) {
                Object error = SoulResultWrap.error(SoulResultEnum.SERVICE_RESULT_ERROR.getCode(), SoulResultEnum.SERVICE_RESULT_ERROR.getMsg(), null);
                return WebFluxResultUtils.result(exchange, error);
            }
            if (response.getStatusCode() == HttpStatus.GATEWAY_TIMEOUT) {
                Object error = SoulResultWrap.error(SoulResultEnum.SERVICE_TIMEOUT.getCode(), SoulResultEnum.SERVICE_TIMEOUT.getMsg(), null);
                return WebFluxResultUtils.result(exchange, error);
            }
            response.setStatusCode(clientResponse.statusCode());
            response.getCookies().putAll(clientResponse.cookies());
            response.getHeaders().putAll(clientResponse.headers().asHttpHeaders());
            return response.writeWith(clientResponse.body(BodyExtractors.toDataBuffers()));
        }));
    }

这是请求拿到结果后的处理。