Service gateway – streaming plug in

Time:2021-3-1

The streaming plug-in is very similar to the traffic forwarding function of nginx, or reverse proxy.

background

Although the traffic forwarding function of nginx is also very powerful, some changes in the business may lead to various configurations of nginx and make it tired of coping. For example, an app evolves with the development of the business and has many business lines: hotel business line, air ticket business line, catering business line and local travel business line. Behind these lines of business are often different departments and different technical teams, so they will provide different services for app docking. If every new service is added, it needs to be configured in nginx, the configuration of nginx will become heavy and difficult to maintain with the development of the business. Therefore, we can define nginx as a traffic gateway, which has nothing to do with the specific business and is only responsible for the domain name based request forwarding, while the service gateway is responsible for the different services of the back-end business line.

The other is the requirement of logical grouping of services. For example, some important requests are divided into a group of servers, which are high-performance; the requests of other edge services are divided into another group of servers, which are relatively low in configuration. For example, our gray environment needs some basic rules to allocate traffic.

programme

Capture the traffic of Webflux, then forward the request to the corresponding service in the background, and finally return the result of the response to the client.

realization

Capture traffic

Implement a custom webhandler,

@Override
public Mono handle(final ServerWebExchange exchange) {

    return new DefaultDiabloPluginChain(plugins).execute(exchange);
}

Implement a plug-in responsibility chain, call different plug-ins, and finally return the response results.

private static class DefaultDiabloPluginChain implements DiabloPluginChain {

        private int index;

        private final List plugins;

        DefaultDiabloPluginChain(final List plugins) {
            this.plugins = plugins;
        }

        @Override
        public Mono execute(final ServerWebExchange exchange) {

            if (this.index < plugins.size()) {
                DiabloPlugin plugin = plugins.get(this.index++);
                try {
                    return plugin.execute(exchange, this);
                } catch (Exception ex) {
                    log.error("DefaultDiabloPluginChain.execute, traceId: {}, uri: {}, error:{}", exchange.getAttribute(Constants.CLIENT_RESPONSE_TRACE_ID), exchange.getRequest().getURI().getPath(), Throwables.getStackTraceAsString(ex));

                    throw ex;
                }
            } else {
                return Mono.empty(); // complete
            }
        }
    }

Implementation of streaming plug in

public class DividePlugin extends AbstractDiabloPlugin {

    private final UpstreamCacheManager upstreamCacheManager;

    private final WebClient webClient;

    public DividePlugin(final LocalCacheManager localCacheManager, final UpstreamCacheManager upstreamCacheManager, final WebClient webClient) {
        super(localCacheManager);
        this.upstreamCacheManager = upstreamCacheManager;
        this.webClient = webClient;
    }

    @Override
    protected Mono doExecute(final ServerWebExchange exchange, final DiabloPluginChain chain, final SelectorData selector, final RuleData rule) {
        final RequestDTO requestDTO = exchange.getAttribute(Constants.REQUESTDTO);
        final String traceId = exchange.getAttribute(Constants.CLIENT_RESPONSE_TRACE_ID);
        final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);

        String ruleId = rule.getId();
        final List upstreamList = upstreamCacheManager.findUpstreamListByRuleId(ruleId);
        if (CollectionUtils.isEmpty(upstreamList)) {

            log.warn("DividePlugin.doExecute upstreamList is empty, traceId: {}, uri: {}, ruleName:{}", traceId, exchange.getRequest().getURI().getPath(), rule.getName());
            exchange.getResponse().setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
            return chain.execute(exchange);
        }

        final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();

        DivideUpstream divideUpstream =
                LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);

        if (Objects.isNull(divideUpstream)) {

            log.warn("DividePlugin.doExecute divideUpstream is empty, traceId: {}, uri: {}, loadBalance:{}, ruleName:{}, upstreamSize: {}", traceId, exchange.getRequest().getURI().getPath(), ruleHandle.getLoadBalance(), rule.getName(), upstreamList.size());
            exchange.getResponse().setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
            return chain.execute(exchange);
        }

        if (exchange.getAttributeOrDefault(Constants.GATEWAY_ALREADY_ROUTED_ATTR, false)) {
            log.warn("DividePlugin.doExecute alread routed, traceId: {}, uri: {}, ruleName:{}", traceId, exchange.getRequest().getURI().getPath(), rule.getName());

            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            return chain.execute(exchange);
        }
        exchange.getAttributes().put(Constants.GATEWAY_ALREADY_ROUTED_ATTR, true);

        exchange.getAttributes().put(Constants.GATEWAY_CONTEXT_UPSTREAM_HOST, divideUpstream.getUpstreamHost());
        exchange.getAttributes().put(Constants.GATEWAY_CONTEXT_RULE_ID, ruleId);

        HttpCommand command = new HttpCommand(exchange, chain,
                requestDTO, divideUpstream, webClient, ruleHandle.getTimeout());
        return command.doHttpInvoke();
    }

    public SelectorData filterSelector(final List selectors, final ServerWebExchange exchange) {
        return selectors.stream()
                        .filter(selector -> selector.getEnabled() && filterCustomSelector(selector, exchange))
                        .findFirst().orElse(null);
    }

    private Boolean filterCustomSelector(final SelectorData selector, final ServerWebExchange exchange) {
        if (selector.getType() == SelectorTypeEnum.CUSTOM_FLOW.getCode()) {

            List conditionList = selector.getConditionList();
            if (CollectionUtils.isEmpty(conditionList)) {
                return false;
            }

            //The background is initially defined as host and the expression is=
            if (MatchModeEnum.AND.getCode() == selector.getMatchMode()) {
                ConditionData conditionData = conditionList.get(0);
                return Objects.equals(exchange.getRequest().getHeaders().getFirst("Host"), conditionData.getParamValue().trim());
            } else {
                return conditionList.stream().anyMatch(c -> Objects.equals(exchange.getRequest().getHeaders().getFirst("Host"), c.getParamValue().trim()));
            }

        }
        return true;
    }

    @Override
    public String named() {
        return PluginEnum.DIVIDE.getName();
    }

    @Override
    public Boolean skip(final ServerWebExchange exchange) {
        final RequestDTO body = exchange.getAttribute(Constants.REQUESTDTO);
        return !Objects.equals(Objects.requireNonNull(body).getRpcType(), RpcTypeEnum.HTTP.getName());
    }

    @Override
    public PluginTypeEnum pluginType() {
        return PluginTypeEnum.FUNCTION;
    }

    @Override
    public int getOrder() {
        return PluginEnum.DIVIDE.getCode();
    }

}

WebClient

Forwarding HTTP requests mainly relies on webclient, which provides a responsive interface. The specific operation is encapsulated in the httpcommand tool class, and the core code is as follows:

public Mono doHttpInvoke() {

        URI uri = buildRealURL(divideUpstream, exchange);
        traceId = exchange.getAttribute(Constants.CLIENT_RESPONSE_TRACE_ID);
        if (uri == null) {
            log.warn("HttpCommand.doNext real url is null, traceId: {}, uri: {}", traceId, exchange.getRequest().getURI().getPath());
            exchange.getResponse().setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
            return chain.execute(exchange).then(Mono.defer(() -> Mono.empty()));
        }
        //If you have time to add todo later, it will not be cleared
        // IssRpcContext.commitParams(IssRpcContextParamKey.TRACE_ID, traceId);

        if (requestDTO.getHttpMethod().equals(HttpMethodEnum.GET.getName())) {

            return webClient.get().uri(f -> uri)
                            .headers(httpHeaders -> {
                                httpHeaders.add(Constants.TRACE_ID, traceId);
                                httpHeaders.addAll(exchange.getRequest().getHeaders());
                            })
                            .exchange()
                            //The default doonerror exception is passed
                            .doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient get execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
                            .timeout(Duration.ofMillis(timeout))
                            .flatMap(this::doNext);
        } else if (requestDTO.getHttpMethod().equals(HttpMethodEnum.POST.getName())) {

            return webClient.post().uri(f -> uri)
                            .headers(httpHeaders -> {
                                httpHeaders.add(Constants.TRACE_ID, traceId);
                                httpHeaders.addAll(exchange.getRequest().getHeaders());
                            })
                            .contentType(buildMediaType())
                            .body(BodyInserters.fromDataBuffers(exchange.getRequest().getBody()))
                            .exchange()
                            .doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient post execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
                            .timeout(Duration.ofMillis(timeout))
                            .flatMap(this::doNext);
        } else if (requestDTO.getHttpMethod().equals(HttpMethodEnum.OPTIONS.getName())) {
            return webClient.options().uri(f -> uri)
                            .headers(httpHeaders -> {
                                httpHeaders.add(Constants.TRACE_ID, traceId);
                                httpHeaders.addAll(exchange.getRequest().getHeaders());
                            })
                            .exchange()
                            .doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient options execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
                            .timeout(Duration.ofMillis(timeout))
                            .flatMap(this::doNext);
        } else if (requestDTO.getHttpMethod().equals(HttpMethodEnum.HEAD.getName())) {
            return webClient.head().uri(f -> uri)
                            .headers(httpHeaders -> {
                                httpHeaders.add(Constants.TRACE_ID, traceId);
                                httpHeaders.addAll(exchange.getRequest().getHeaders());
                            })
                            .exchange()
                            .doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient head execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
                            .timeout(Duration.ofMillis(timeout))
                            .flatMap(this::doNext);
        } else if (requestDTO.getHttpMethod().equals(HttpMethodEnum.PUT.getName())) {

            return webClient.put().uri(f -> uri)
                            .headers(httpHeaders -> {
                                httpHeaders.add(Constants.TRACE_ID, traceId);
                                httpHeaders.addAll(exchange.getRequest().getHeaders());
                            })
                            .contentType(buildMediaType())
                            .body(BodyInserters.fromDataBuffers(exchange.getRequest().getBody()))
                            .exchange()
                            .doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient put execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
                            .timeout(Duration.ofMillis(timeout))
                            .flatMap(this::doNext);

        } else if (requestDTO.getHttpMethod().equals(HttpMethodEnum.DELETE.getName())) {

            return webClient.delete().uri(f -> uri)
                            .headers(httpHeaders -> {
                                httpHeaders.add(Constants.TRACE_ID, traceId);
                                httpHeaders.addAll(exchange.getRequest().getHeaders());
                            })
                            .exchange()
                            .doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient delete execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
                            .timeout(Duration.ofMillis(timeout))
                            .flatMap(this::doNext);

        } else if (requestDTO.getHttpMethod().equals(HttpMethodEnum.PATCH.getName())) {
            return webClient.patch().uri(f -> uri)
                            .headers(httpHeaders -> {
                                httpHeaders.add(Constants.TRACE_ID, traceId);
                                httpHeaders.addAll(exchange.getRequest().getHeaders());
                            })
                            .contentType(buildMediaType())
                            .body(BodyInserters.fromDataBuffers(exchange.getRequest().getBody()))
                            .exchange()
                            .doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient patch execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
                            .timeout(Duration.ofMillis(timeout))
                            .flatMap(this::doNext);
        }

        log.warn("HttpCommand doHttpInvoke Waring no match doHttpInvoke end, traceId: {}, httpMethod: {}, uri: {}", traceId, requestDTO.getHttpMethod(), uri.getPath());

        return Mono.empty();
    }

Gateway project open source

The above content is based on a small module of service gateway. Please see here for details:Diablo is here

Recommended Today

Introduction to Kong and Konga

the front Recently, I have been learning and understandingkongLet’s share with you todaykongandkongabar introduce kongIt’s based on nginx_ The gateway API written by Lua module is a gateway for forwarding API communication between client and service, which can be extended by plug-ins. In a word:Kong is a dynamic enhanced version of nginx Look at some […]