1
头图

What is a microservice gateway

SpringCloud Gateway is a relatively new project in Spring's family bucket. The Spring community introduced it as follows:

The project leveraged the capabilities of Spring WebFlux to create an API gateway. It aims to provide a simple and effective way to route API services and provide them with various enhancements, such as: security, monitoring and scalability.

In the real business field, we often use SpringCloud Gateway as a microservice gateway. If you don’t understand the difference between a gateway and a traditional gateway, you can read this article 160a6646b5a410 Service Mesh and API Gateway Deep Discussion to understand the two The positioning difference.

In my superficial understanding, traditional API gateways are often independent of various back-end services. Requests are first sent to the independent gateway layer, and then to the service cluster. The micro-service gateway changes the flow from north-south to east-west (see the figure below). The micro-service gateway and the back-end service are in the same container, so they also have an alias called Gateway Sidecar.

Why is it called Sidecar, how should this word be understood? Have you seen the three jumpers in the chicken:

motorcycle is your back-end service, and the extra seat hanging next to it is the micro-service gateway, which is attached to the back-end service (usually two processes are in the same container), is it vivid? Some.

Due to the lack of knowledge of this talent, there will inevitably be deviations in the understanding of microservice-related concepts. I won’t elaborate on the principle text here.

This article only discusses the introductory construction and actual combat of SpringCloud Gateway. If you are interested in the principle, you can wait for the follow-up principle analysis article.

Note: The gateway project in this article has been put into operation in the author's company, and it undertakes millions of requests every day. It is a project that has been verified by actual combat.

Article Directory

  • Build a gateway by hand

    • Introduce pom dependency
    • Write yml file
    • Interface escaping issues
    • Get the request body (Request Body)
  • Stepping on the pit

    • Get the real IP of the client
    • Suffix match
  • to sum up

Source code

The complete project source code has been included in my Github:

https://github.com/qqxx6661/springcloud_gateway_demo

Build a gateway by hand

Introduce pom dependency

I used spring-boot 2.2.5.RELEASE as the parent dependency:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

In dependencyManagement, we need to specify the version to ensure that we can introduce the version of SpringCloud Gateway we want, so we need to use dependencyManagement:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Finally, the spring-cloud-starter-gateway is introduced in the dependency:

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

In this way, we have introduced the 2.2.5.RELEASE version of the gateway:

Also, please check your dependent if they contain spring-boot-starter-web, if any, please get rid of it . Because our SpringCloud Gateway is a web server implemented by netty+webflux, it conflicts with Springboot Web itself.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

If you do this, your project can actually be started, run SpringcloudGatewayApplication, and you will get the result as shown in the figure:

Write yml file

The core concept of SpringBoot is that the agreement takes precedence over the configuration . When I first learned Spring, I never understood the meaning of this sentence. When using Spring Cloud Gateway, I understood this sentence more deeply. By default, you don't need any configuration to be able to run the most basic gateway. For your specific needs in the future, add additional configurations.

The more powerful point of SpringCloud Gateway is that it has a lot of built-in default function implementations, most of the functions you need, such as adding a header to the request, adding a parameter, you only need to introduce the corresponding built-in filter in yml. can.

It can be said that yml is the soul of the entire SpringCloud Gateway.

The most basic function of a gateway is to configure routing. In this regard, Spring Cloud Gateway supports many ways. such as:

  • Match by time
  • Match by cookie
  • Match by Header property
  • Match by Host
  • Match by request
  • Match by request path
  • Match by request parameters
  • Match by requesting ip address

These are all introduced in detail in the official website tutorials. Even if you download on Baidu, there will be many introductory tutorials for folk translation. I will not repeat them. I will only use a request path to make a simple example.

In the company's project, since there are two sets of back-end services, the old and the new, we use different uri paths to distinguish them.

  • The old service path is: url/api/xxxxxx, the service port number is 8001
  • The new service path is: url/api/v2/xxxxx, and the service port number is 8002

Then you can configure it directly in yml:

logging:
  level:
    org.springframework.cloud.gateway: DEBUG
    reactor.netty.http.client: DEBUG

spring:
  cloud:
    gateway:
      default-filters:
        - AddRequestHeader=gateway-env, springcloud-gateway
      routes:
        - id: "server_v2"
          uri: "http://127.0.0.1:8002"
          predicates:
            - Path=/api/v2/**
        - id: "server_v1"
          uri: "http://127.0.0.1:8001"
          predicates:
            - Path=/api/**

The above code is explained as follows:

  • logging: Due to the needs of the article, we turn on the Debug mode of gateway and netty to see clearly the execution process after the request comes in, which is convenient for follow-up explanation.
  • default-filters: We can easily use default-filters to add a custom header to the request. We add a KV as gateway-env: springcloud-gateway to indicate that our request has passed through this gateway. The advantage of this is that the subsequent server can also see it.
  • routes: Routing is the key point of the gateway. I believe readers can understand by looking at the code. I configured two routes, one is the old service of server_v1 and the other is the new service of server_v2. Please note that when a request satisfies the predicate conditions of multiple routes, the request will only be forwarded by the first successfully matched route. Because the route of our old service is /xx, we need to put the old service at the back, and match the new service with the suffix /v2 first, and then match with /xx if it is not satisfied.

Take a look at the http://localhost :8080/api/xxxxx:

Take a look at the http://localhost :8080/api/v2/xxxxx:

You can see that the two requests are routed correctly. Since we have not really enabled the back-end service, please ignore the last sentence of error.

Interface escaping issues

In the company's actual project, after I set up the gateway, I encountered an interface escaping problem. I believe many readers may also encounter it, so here we better take precautions before they occur and deal with them first.

The problem is this, many old projects are not escaped on the url, resulting in the following interface request, http://xxxxxxxxx/api/b3d56a6fa19975ba520189f3f55de7f6/140x140.jpg?t= 1"

When such a request comes, the gateway will report an error:

java.lang.IllegalArgumentException: Invalid character '=' for QUERY_PARAM in "http://pic1.ajkimg.com/display/anjuke/b3d56a6fa19975ba520189f3f55de7f6/140x140.jpg?t=1"

Without modifying the service code logic, the gateway can actually solve this problem. The solution is to upgrade to a version above 2.1.1.RELEASE.

The issue was fixed in version spring-cloud-gateway 2.1.1.RELEASE.

So we used the higher version 2.2.5.RELEASE from the beginning to avoid this problem. If the partner finds that the previously used version is lower than 2.1.1.RELEASE, please upgrade.

Get the request body (Request Body)

In the use of the gateway, sometimes it is necessary to get the data in the request body, such as verifying the signature, and the body may need to participate in signature verification.

However, because SpringCloud Gateway uses webflux at the bottom, its request is streamed response, that is, Reactor programming, it is not so easy to read the request parameters in the Request Body.

Google on the Internet for a long time, many solutions are either completely outdated or version incompatible. Fortunately, I finally referred to this article and finally got my idea:

https://www.jianshu.com/p/db3b15aec646

First of all, we need to take the body out of the request. Because of the streaming process, the body of the Request can only be read once. If it is read directly in the Filter, the subsequent services will not be able to read the data.

SpringCloud Gateway provides an assertion factory class ReadBodyPredicateFactory internally. This class implements reading the body content of the Request and putting it into the cache. We can achieve our purpose by getting the body content from the cache.

First, create a new CustomReadBodyRoutePredicateFactory class. Only the key code is posted here. For the complete code, please see the Github warehouse :

@Component
public class CustomReadBodyRoutePredicateFactory extends AbstractRoutePredicateFactory<CustomReadBodyRoutePredicateFactory.Config> {

    protected static final Log log = LogFactory.getLog(CustomReadBodyRoutePredicateFactory.class);
    private List<HttpMessageReader<?>> messageReaders;

    @Value("${spring.codec.max-in-memory-size}")
    private DataSize maxInMemory;

    public CustomReadBodyRoutePredicateFactory() {
        super(Config.class);
        this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
    }

    public CustomReadBodyRoutePredicateFactory(List<HttpMessageReader<?>> messageReaders) {
        super(Config.class);
        this.messageReaders = messageReaders;
    }

    @PostConstruct
    private void overrideMsgReaders() {
        this.messageReaders = HandlerStrategies.builder().codecs((c) -> c.defaultCodecs().maxInMemorySize((int) maxInMemory.toBytes())).build().messageReaders();
    }

    @Override
    public AsyncPredicate<ServerWebExchange> applyAsync(Config config) {
        return new AsyncPredicate<ServerWebExchange>() {
            @Override
            public Publisher<Boolean> apply(ServerWebExchange exchange) {
                Class inClass = config.getInClass();
                Object cachedBody = exchange.getAttribute("cachedRequestBodyObject");
                if (cachedBody != null) {
                    try {
                        boolean test = config.predicate.test(cachedBody);
                        exchange.getAttributes().put("read_body_predicate_test_attribute", test);
                        return Mono.just(test);
                    } catch (ClassCastException var6) {
                        if (CustomReadBodyRoutePredicateFactory.log.isDebugEnabled()) {
                            CustomReadBodyRoutePredicateFactory.log.debug("Predicate test failed because class in predicate does not match the cached body object", var6);
                        }
                        return Mono.just(false);
                    }
                } else {
                    return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> {
                        return ServerRequest.create(exchange.mutate().request(serverHttpRequest).build(), CustomReadBodyRoutePredicateFactory.this.messageReaders).bodyToMono(inClass).doOnNext((objectValue) -> {
                            exchange.getAttributes().put("cachedRequestBodyObject", objectValue);
                        }).map((objectValue) -> {
                            return config.getPredicate().test(objectValue);
                        }).thenReturn(true);
                    });
                }
            }

            @Override
            public String toString() {
                return String.format("ReadBody: %s", config.getInClass());
            }
        };
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        throw new UnsupportedOperationException("ReadBodyPredicateFactory is only async.");
    }
}

The main function of the code: when a request for a body arrives, the body is read out and placed in the memory cache. If there is no body, nothing is done.

In this way, we can use exchange.getAttribute("cachedRequestBodyObject") in the interceptor to get the body.

By the way, we haven't demonstrated how to write a filter, so let's write a complete demofilter first.

Let's create a new class DemoGatewayFilterFactory:

@Component
public class DemoGatewayFilterFactory extends AbstractGatewayFilterFactory<DemoGatewayFilterFactory.Config> {

    private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";

    public DemoGatewayFilterFactory() {
        super(Config.class);
        log.info("Loaded GatewayFilterFactory [DemoFilter]");
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("enabled");
    }

    @Override
    public GatewayFilter apply(DemoGatewayFilterFactory.Config config) {
        return (exchange, chain) -> {
            if (!config.isEnabled()) {
                return chain.filter(exchange);
            }
            log.info("-----DemoGatewayFilterFactory start-----");
            ServerHttpRequest request = exchange.getRequest();
            log.info("RemoteAddress: [{}]", request.getRemoteAddress());
            log.info("Path: [{}]", request.getURI().getPath());
            log.info("Method: [{}]", request.getMethod());
            log.info("Body: [{}]", (String) exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY));
            log.info("-----DemoGatewayFilterFactory end-----");
            return chain.filter(exchange);
        };
    }

    public static class Config {

        private boolean enabled;

        public Config() {}

        public boolean isEnabled() {
            return enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }
    }
}

In this filter, we got a fresh request and printed out its path, method, body, etc.

We send a post request, write "I am the body" in the body, run the gateway, and get the result:

Is it very clear!

Do you think this is over? There are two very large pits here.

1. Processing when the body is empty

The CustomReadBodyRoutePredicateFactory class posted above is actually the code I have fixed, and there is a line .thenReturn(true) that needs to be added. This can ensure that when the body is empty, no exception will be reported. As for why there was a problem with the initial writing, obviously because I was lazy, I copied the code on the Internet directly, hahahahaha.

2. The body size exceeds the maximum buffer limit

This situation was discovered after the company project went online. The body in our request is sometimes larger, but the gateway has a default size limit. So after going online, I found frequent errors:

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144

After Google, I found a solution and need to add the following configuration to the configuration

spring: 
  codec:
    max-in-memory-size: 5MB

Changed the buffer size to 5M.

You think this is over again, too naive, you will find that it may not take effect.

The source of the problem is here: we configured the above parameters in spring, but our custom interceptor will initialize the ServerRequest, and the the DefaultServerRequest 160a6646b5b6bb will use the default 262144

So here we need to take out the CodecConfigurer from Spring and pass the Reader inside to serverRequest.

The detailed debugging process can be seen in this reference:

http://theclouds.io/tag/spring-gateway/

OK, after finding the problem, we can modify our code, in CustomReadBodyRoutePredicateFactory, add:

@Value("${spring.codec.max-in-memory-size}")
private DataSize maxInMemory;

@PostConstruct
private void overrideMsgReaders() {
  this.messageReaders = HandlerStrategies.builder().codecs((c) -> c.defaultCodecs().maxInMemorySize((int) maxInMemory.toBytes())).build().messageReaders();
}

This will use our 5MB as the maximum cache limit each time.

Still reminder, the complete code can be found in the Github repository

Speaking of this, the actual entry is almost the same. Your gateway is ready to go online. All you have to do is to add the business functions you need, such as logging, extension, statistics, etc.

Stepping on the pit

Get the real IP of the client

In many cases, our back-end services will get the user's real IP through the host, but through the outer reverse proxy nginx forwarding, it is likely that you need to take X-Forward-XXX parameters like this from the header to get it. To the real IP.

After we joined the microservice gateway, another ring was added to this complex link.

No, if you don't do any settings, since your gateway and back-end service are in the same container, your back-end service is likely to get an IP like localhost:8080 (your gateway port).

At this time, you need to configure PreserveHostHeader in yml, which is the implementation that comes with SpringCloud Gateway:

filters:
  - PreserveHostHeader # 防止host被修改为localhost

Literally, is to keep the Header of the Host and pass it transparently to the back-end service.

The source code in the filter is posted for everyone:

public GatewayFilter apply(Object config) {
    return new GatewayFilter() {
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            exchange.getAttributes().put(ServerWebExchangeUtils.PRESERVE_HOST_HEADER_ATTRIBUTE, true);
            return chain.filter(exchange);
        }

        public String toString() {
            return GatewayToStringStyler.filterToStringCreator(PreserveHostHeaderGatewayFilterFactory.this).toString();
        }
    };
}

Suffix match

In the company’s project, the old back-end warehouse apis all end with (/api/xxxxxx.json) , which gave rise to a demand. When we refactored the old interface and hoped that it will be used in our new service, we will add. The suffix json is removed. Can be set in filters:

filters:
  - RewritePath=(?<segment>/?.*).json, $\{segment} # 重构接口抹去.json尾缀

In this way, the .json suffix can be removed from the interface to the backend.

to sum up

This article leads the reader to complete the construction of a microservice gateway step by step, and solves many hidden pits. The final product project has been put into operation in the author's company, and has added signature verification, log recording and other services. It undertakes millions of requests every day and is a project that has been verified by actual combat.

Finally, send the project source code repository again:

https://github.com/qqxx6661/springcloud_gateway_demo

Thank you for your support. If the article has helped you a little bit, please like and forward it to support it!

Your feedback is my motivation to keep updating, thank you~

reference

https://cloud.tencent.com/developer/article/1449300

https://juejin.cn/post/6844903795973947400#heading-3

https://segmentfault.com/a/1190000016227780

https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/multi/multi__reactor_netty_access_logs.html

https://www.cnblogs.com/savorboard/p/api-gateway.html

https://www.servicemesher.com/blog/service-mesh-and-api-gateway/

https://www.cnblogs.com/hyf-huangyongfei/p/12849406.html

https://www.codercto.com/a/52970.html

https://github.com/spring-cloud/spring-cloud-gateway/issues/1658

https://blog.csdn.net/zhangzhen02/article/details/109082792

follow me

I am an Internet back-end development engineer struggling in the front line.

Usually, I mainly focus on back-end development, data security, edge computing and other directions. Welcome to communicate.

I can be found on all major platforms

Original article main content

  • Back-end development combat
  • Back-end technical interview
  • Algorithm problem solving/data structure/design pattern
  • Anecdote

personal public number: talk about back-end technology

个人公众号:后端技术漫谈

If the article is helpful to you, please give me a thumbs-up from the bosses, read it and forward to support it, your support is very important to me~


蛮三刀酱
57 声望8 粉丝