Introduction to gateway and implementation scheme of gray Publishing

Introduction to gateway and implementation scheme of gray Publishing

Introduction to gateway

Official documents: https://docs.spring.io/spring-cloud-gateway/docs/2.2.8.RELEASE/reference/html/#gateway-starter

Gateway request processing

The client sends a request to the Spring Cloud Gateway. If the gateway handler mapping determines that the request matches the route, it is sent to the gateway Web handler. This handler runs to send requests through a chain of request specific filters. The reason why the filter is divided by dotted lines is that the filter can execute logic before or after sending the proxy request. Execute all "pre" filter logic and then make proxy requests. After the proxy request is issued, the "post" filter logic is executed.

The URI defined in the route without port will be obtained for HTTP and HTTPS URI respectively. The default port is set to 80 and 443.

  • Dispatcher handler: scheduler for all requests, load request distribution
  • RoutePredicateHandlerMapping: a route predicate matcher, which is used to find routes and return the corresponding WebHandler after finding routes. DispatcherHandler will traverse the HandlerMapping set in turn for processing
  • FilteringWebHandler: the WebHandler that uses the Filter linked list to process the request. Routepredictehandlermapping returns the corresponding FilteringWebHandler to process the request after finding the route. The FilteringWebHandler is responsible for assembling the Filter linked list and calling the linked list to process the request.

https://www.cnblogs.com/bjlhx/p/9588739.html

Function and life cycle of filter

effect

When we have many services, such as user service, goods service and sales service in the figure below, when the client requests the Api of each service, each service needs to do the same things, such as authentication, flow restriction, log output, etc.

For this repetitive work, is there any way to do better? The answer is yes. Add an Api Gateway service with global permission control, flow restriction and log output on the upper layer of the micro service, and then forward the request to the specific business service layer. This Api Gateway service serves as a service boundary. External requests to access the system must first pass through the gateway layer.

life cycle

Similar to zuul, Spring Cloud Gateway has "pre" and "post" filters. The client's request first passes through a "pre" type filter, and then forwards the request to a specific business service, such as the user service in the figure above. After receiving the response from the business service, it is processed through a "post" type filter, and finally returns the response to the client.

Different from zuul, in addition to the "pre" and "post" filters, in Spring Cloud Gateway, the filter can be divided into two other types from the scope of action. One is the gateway filter for a single route, and its writing in the configuration file is similar to predict; The other is the global gateway filer for all routes. Now explain these two filters from the dimension of scope division.

gateway filter

Filters allow you to modify incoming HTTP requests or outgoing HTTP responses in some way. Filters can be restricted to certain request paths. Spring Cloud Gateway includes many built-in GatewayFilter factories.

The GatewayFilter Factory is similar to the Predicate factory described in the previous article, which is in the configuration file application The configuration in YML follows the idea that the Convention is greater than the configuration. You only need to configure the name of GatewayFilter Factory in the configuration file, instead of writing all the class names. For example, AddRequestHeaderGatewayFilterFactory only needs to write AddRequestHeader in the configuration file, instead of all the class names. The GatewayFilter Factory configured in the configuration file will eventually be processed by the corresponding filter factory class.

Spring Cloud Gateway has many built-in filter factories in package ` ` org springframework. cloud. gateway. filter. Factory package

The following takes addrequestheader gateway filter factory as an example.

AddRequestHeader GatewayFilter Factory

If the following configuration is added to the yml file:

server:
  port: 8081
spring:
  profiles:
    active: add_request_header_route

---
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: http://httpbin.org:80/get
        filters:
        - AddRequestHeader=X-Request-Foo, Bar
        predicates:
        - After=2017-01-20T17:42:47.789-07:00[America/Denver]
  profiles: add_request_header_route

In the above configuration, the startup port of the project is 8081 and the configuration file is add_request_header_route, in add_ request_ header_ In route configuration, the id of the configured robot is add_request_header_route, the route address is http://httpbin.org:80/get , the router has AfterPredictFactory and one filter is AddRequestHeaderGatewayFilterFactory (the contract is written as AddRequestHeader). The AddRequestHeader filter factory will add a pair of request headers to the request header, with the name of X-Request-Foo and the value of Bar. To verify how AddRequestHeaderGatewayFilterFactory works, check its source code. The source code of AddRequestHeaderGatewayFilterFactory is as follows:

public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {

	@Override
	public GatewayFilter apply(NameValueConfig config) {
		return (exchange, chain) -> {
			ServerHttpRequest request = exchange.getRequest().mutate()
					.header(config.getName(), config.getValue())
					.build();

			return chain.filter(exchange.mutate().request(request).build());
		};
    }

}

As can be seen from the above code, create a new ServerHttpRequest based on the old ServerHttpRequest, add a request header to the new ServerHttpRequest, then create a new ServerWebExchange, submit the filter chain and continue filtering.

https://juejin.cn/post/6844903741867425800

After the request from the client passes through the filter, it will be sent to the target service server.

Gray Publishing

Gray Release (also known as Canary Release)

  • concept

Without stopping the old version, deploy the new version, switch the low proportion traffic (e.g. 5%) to the new version, and the high proportion traffic (e.g. 95%) goes through the old version. There is no problem through monitoring and observation, gradually expand the scope, and finally migrate all traffic to the new version and offline the old version. Non destructive release

  • advantage

Flexible and simple, without user tag driven. High security. If there is a problem with the new version, it will only occur in a low proportion of traffic

  • shortcoming

The configuration modification of increasing flow ratio brings additional operation cost. Narrow user coverage and low proportion of traffic may not find all problems

The above is based on the way of flow control.

For our current application, there is no service cluster and no requirement to distinguish traffic percentage. There is no service monitoring, so it is impossible to judge whether this small part of traffic is normal.

Therefore, gray publishing based on version control can be considered. It is similar to the method based on flow control, except that the front-end service does not belong to the routing scope; Whether the updated function is normal shall be detected by the tester. This brings a problem that increases the complexity of operation: users need to mark through the front end, but this problem is acceptable at present.

Version based gray Publishing

When we release the update, the online environment maintains two versions: beta and prod.

For example, the old version of patient v1 running online and the new version of patient v2 that needs to be updated, in which patient v1 is prod and patient v2 is beta. After the update, patient v2 becomes prod and the prod of patient v1 goes offline.

Based on the above figure, 95% of users are patient V1 on the front line, and 5% are the new version of patient v2.

Steps of gray publishing based on version control:

Assuming the current online version v1, the version to be updated is v2. The steps are as follows:

  1. The backend releases the v2 version to the production environment, that is, the beta version. The front-end releases the beta version and connects the back-end beta version;
  2. Testers access the beta version to test;
  3. After there is no problem in the test, the front end releases the beta version online, and the beta is promoted to prod version. User access v2 version.
  4. After the tester confirms that there is no error. Back end offline v1.

If a BUG that has not been found before appears after the update, there are two solutions depending on the scope of influence.

  1. Bugs that affect core functions and cannot be fixed within half an hour. The front and rear ends can choose to roll back v2 to v1.
  2. For bug s that do not affect core functions, consider solving them in the next version. The update steps are as follows.

Function realization

In the introduction to gateway in the previous chapter, you can see that the front-end request is sent to specific business services after passing through a series of gatewayfilters.

Before the request is sent to the business service, we can modify the target address to the service ip and port conforming to the current request version.

Realization idea:

  1. New grayscale route with version control

  2. New custom filter

  3. Gray level routing pulls service instance metadata from nacos and returns qualified instances to the filter according to the matching algorithm

  4. Filter sets the request address to exchange and enters the next filter, and it is necessary to ensure that the request address is not changed.

The implementation is as follows:

Add dependency:

  <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>

GrayLoadBalancerClientFilter class

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools;
import org.springframework.cloud.client.loadbalancer.reactive.DefaultRequest;
import org.springframework.cloud.client.loadbalancer.reactive.Request;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;

/**
 * Custom filter to filter instances through GrayLoadBalancer
 *
 * @author hy
 * @date 2021/6/1
 */
public class GrayLoadBalancerClientFilter implements GlobalFilter, Ordered {

    private static final Log log = LogFactory.getLog(ReactiveLoadBalancerClientFilter.class);
    private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;
    private final LoadBalancerClientFactory clientFactory;
    private LoadBalancerProperties properties;

    public GrayLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
        this.clientFactory = clientFactory;
        this.properties = properties;
    }

    @Override
    public int getOrder() {
        return LOAD_BALANCER_CLIENT_FILTER_ORDER;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI url = (URI) exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        String schemePrefix = ((URI) exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR)).getScheme();

        //You can use this option if you want to customize the shape of the route that needs gray publishing
//        if (url != null && ("grayLb".equals(url.getScheme()) || "grayLb".equals(schemePrefix))) {
//            ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url);
//            if (log.isTraceEnabled()) {
//                log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);
//            }
//            return doFilter(exchange, chain, url);
//        } else {
//            return chain.filter(exchange);
//        }

        return doFilter(exchange, chain, url);
    }

    private Mono<Void> doFilter(ServerWebExchange exchange, GatewayFilterChain chain, URI url) {
        return this.choose(exchange).doOnNext((response) -> {
            if (!response.hasServer()) {
                throw NotFoundException.create(this.properties.isUse404(), "Unable to find instance for " + url.getHost());
            } else {
                URI uri = exchange.getRequest().getURI();

                //When overrideScheme is null, the route forwarded is http request
                String overrideScheme = null;

                DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance((ServiceInstance) response.getServer(), overrideScheme);
                URI requestUrl = this.reconstructURI(serviceInstance, uri);
                if (log.isTraceEnabled()) {
                    log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
                }
                exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
            }
        }).then(chain.filter(exchange));
    }

    protected URI reconstructURI(ServiceInstance serviceInstance, URI original) {
        return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
    }

    private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
        URI uri = (URI) exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        GrayLoadBalancer loadBalancer = new GrayLoadBalancer(clientFactory.getLazyProvider(uri.getHost(), ServiceInstanceListSupplier.class), uri.getHost());
        if (loadBalancer == null) {
            throw new NotFoundException("No loadbalancer available for " + uri.getHost());
        } else {
            return loadBalancer.choose(this.createRequest(exchange));
        }
    }

    private Request createRequest(ServerWebExchange exchange) {
        HttpHeaders headers = exchange.getRequest().getHeaders();
        Request<HttpHeaders> request = new DefaultRequest<>(headers);
        return request;
    }
}

GrayLoadBalancer class

import com.wolwo.gateway.handler.gray.weight.model.WeightMeta;
import com.wolwo.gateway.handler.gray.weight.util.WeightRandomUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.reactive.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.reactive.Request;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.*;

/**
 * Load balancing of custom grayscale publishing, which provides a way of publishing according to version and weight
 *
 * @author hy
 * @date 2021/6/1
 */
@Slf4j
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
    private String serviceId;

    public GrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        HttpHeaders headers = (HttpHeaders) request.getContext();
        if (this.serviceInstanceListSupplierProvider != null) {
            ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier) this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
            return ((Flux) supplier.get()).next().map(list -> getInstanceResponse((List<ServiceInstance>) list, headers));
        }

        return null;
    }

    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, HttpHeaders headers) {
        if (instances.isEmpty()) {
            return getServiceInstanceEmptyResponse();
        } else {
            return getServiceInstanceResponseByVersion(instances, headers);
        }
    }

    /**
     * Distribute by version
     *
     * @param instances
     * @param headers
     * @return
     */
    private Response<ServiceInstance> getServiceInstanceResponseByVersion(List<ServiceInstance> instances, HttpHeaders headers) {
        String versionNo = headers.getFirst("version");
        log.debug("request head version: {}", versionNo);

        Map<String, String> versionMap = new HashMap<>();
        versionMap.put("version", versionNo);

        final Set<Map.Entry<String, String>> attributes =
                Collections.unmodifiableSet(versionMap.entrySet());

        ServiceInstance serviceInstance = null;
        for (ServiceInstance instance : instances) {
            Map<String, String> metadata = instance.getMetadata();
            if (metadata.entrySet().containsAll(attributes)) {
                serviceInstance = instance;
                break;
            }
        }

        //If the target instance cannot be found
        if (ObjectUtils.isEmpty(serviceInstance)) {

            //If the instance list is not empty, take the first one
            if (CollectionUtils.isEmpty(instances)) {
                return getServiceInstanceEmptyResponse();
            }

            return new DefaultResponse(instances.get(0));
        }

        return new DefaultResponse(serviceInstance);
    }

     /**
     * Distribute according to the weight value configured in nacos
     *
     * @param instances
     * @return
     */
    private Response<ServiceInstance> getServiceInstanceResponseWithWeight(List<ServiceInstance> instances) {
        Map<ServiceInstance, Integer> weightMap = new HashMap<>();
        for (ServiceInstance instance : instances) {
            Map<String, String> metadata = instance.getMetadata();
            System.out.println(metadata.get("version") + "-->weight:" + metadata.get("weight"));
            if (metadata.containsKey("weight")) {
                weightMap.put(instance, Integer.valueOf(metadata.get("weight")));
            }
        }

        WeightMeta<ServiceInstance> weightMeta = WeightRandomUtils.buildWeightMeta(weightMap);
        if (ObjectUtils.isEmpty(weightMeta)) {
            return getServiceInstanceEmptyResponse();
        }

        ServiceInstance serviceInstance = weightMeta.random();
        if (ObjectUtils.isEmpty(serviceInstance)) {
            return getServiceInstanceEmptyResponse();
        }

        System.out.println(serviceInstance.getMetadata().get("version"));

        return new DefaultResponse(serviceInstance);
    }
    
    private Response<ServiceInstance> getServiceInstanceEmptyResponse() {
        log.warn("No servers available for service: " + this.serviceId);
        return new EmptyResponse();
    }
}

GrayLoadBalancerClientAutoConfig class

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Configure custom filter for spring management
 *
 * @author hy
 * @date 2021/6/1
 */
@Configuration
public class GrayLoadBalancerClientAutoConfig {
    public GrayLoadBalancerClientAutoConfig() {
    }

    @Bean
    @ConditionalOnMissingBean({GrayLoadBalancerClientFilter.class})
    public GrayLoadBalancerClientFilter grayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
        return new GrayLoadBalancerClientFilter(clientFactory, properties);
    }

}

Add your own version in the metadata of the service

When sending a request, the head carries version

If there is no version attribute, the gateway will return the instance with the smallest port number in the instance list.

Debugging source code

Break points in the file method in the FilteringWebHandler class, as shown in the figure

As you can see, first execute our customized GrayLoadBalancerClientFilter, and then execute the ReactiveLoadBalancerClientFilter built in gateway

ReactiveLoadBalancerClientFilter may change my request address. Therefore, set scheme to null in the GrayLoadBalancerClientFilter class, as shown in the following figure

It can be seen from the source code of ReactiveLoadBalancerClientFilter that if the sheme is empty, no additional processing will be carried out and the next filer will be entered directly

In this way, we can execute the request according to the version, but we lack the advanced load balancing function.

If you want the source code, you can leave a message and I upload it to github

Keywords: Microservices Nacos gateway

Added by WakeAngel on Tue, 01 Feb 2022 07:16:58 +0200