In depth understanding of Spring Cloud and microservice construction Chapter 10 routing gateway Spring Cloud Zuul

Keywords: Spring Cloud Microservices

In depth understanding of Spring Cloud and microservice construction Chapter 10 routing gateway Spring Cloud Zuul

1, Introduction to Zuul

Zuul, as a network management component of microservice system, is used to build Edge Service and is committed to dynamic routing, filtering, monitoring, elastic scaling and security. Zuul's important role in micro service architecture is mainly reflected in the following six aspects:

  • The combination of zuul, Ribbon and Eureka can realize the functions of intelligent routing and load balancing. Zuul can distribute the request traffic to multiple service instances in the cluster state according to a certain policy
  • The gateway aggregates the API interfaces of all services and exposes them to the public. When the external system calls the API interface, it is the API interface exposed by the gateway. The external system does not need to know the complexity of mutual calls of services in the microservice system. The microservice system also protects the API interface of its internal microservice unit to prevent it from being directly called by the outside world, resulting in the exposure of sensitive information of the service
  • The gateway service can achieve user identity authentication and authority authentication, prevent illegal requests to operate the API interface, and protect the server
  • The gateway can realize the monitoring function, real-time log output and record requests
  • The gateway can be used to monitor traffic and degrade services in case of high traffic
  • API interfaces are separated from internal services to facilitate testing

2, How Zuul works

Zuul is implemented through a Servlet. Zuul controls requests through a custom ZuulServlet (similar to the DispatchServlet of Spring MVC). Zuul's core is a series of filters that can be executed during the initiation and response of HTTP requests. Zuul includes the following 4 types of filters:

  • PRE filter: it is executed before the request is routed to a specific service. This type of filter can perform security verification, such as authentication, parameter verification, etc
  • ROUTING filter: it is used to route requests to specific microservice instances. By default, it uses Http Client for network requests
  • POST filter: it is executed after the request has been routed to the microservice. Generally, it is used to collect statistics, indicators, and transmit responses to the client
  • ERROR filter: it is executed when other filters have errors

Zuul adopts dynamic reading, compiling and running these filters. Filters cannot communicate with each other directly, but share data through the RequestContext object. Each request will create a RequestContext object. Zuul filter has the following key characteristics:

  • Type: the type of Zuul filter, which determines the stage in which the filter works, such as Pre and Post stages
  • Execution Order: Specifies the execution Order of the filter. The smaller the value of Order, the earlier it will be executed
  • Criteria: conditions required for filter execution
  • Action: if the service executes the condition, the action (i.e. logical code) will be executed

The life cycle of Zuul request is shown in the figure:

When a client requests to enter the Zuul gateway service, the gateway first enters the "pre filter" for a series of verification, operation or judgment. Then it is handed over to the "routing filter" for routing forwarding, and forwarded to the specific service instance for logical processing and return data. When the specific service is processed, it is finally processed by the "post filter". After processing, the processor of this type returns the Response information to the client

ZuulServlet is the core Servlet of Zuul. ZuulServlet is used to initialize zuulfilters and arrange the execution order of these zuulfilters. There is a service() method in this class, which executes the logic executed by the filter

		public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        try {
            this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                this.preRoute();
            } catch (ZuulException var12) {
                this.error(var12);
                this.postRoute();
                return;
            }

            try {
                this.route();
            } catch (ZuulException var13) {
                this.error(var13);
                this.postRoute();
                return;
            }

            try {
                this.postRoute();
            } catch (ZuulException var11) {
                this.error(var11);
            }
        } catch (Throwable var14) {
            this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }

As can be seen from the above code, first execute the preRoute() method, which executes the logic of the PRE type filter. If there is an error in executing this method, error(e) and postRoute() will be executed. Then execute the route() method, which is the logic that executes the ROUTING type filter. Finally, execute postRoute(), which executes the logic of the POST type filter

3, Case practice

1. Build Zuul service

This case is built on the basis of the cases in the previous chapter. A new Spring Boot project, named Eureka Zuul client, introduces relevant dependencies into the pom file, including the pom file inherited from the main Maven project, and the start dependency of Eureka Client, spring cloud starter Netflix Eureka Client, and Zuul's start dependency spring cloud starter Netflix Zuul The start of Web function depends on Spring Boot starter Web, and the start of Spring Boot test depends on Spring Boot starter test. The code is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>Eureka</artifactId>
        <groupId>org.sisyphus</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>eureka-zuul-client</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>

</project>

Add @ EnableEurekaClient annotation to the startup class EurekaZuulClientApplication of the program to enable the function of eurekclient; Add @ SpringBootApplication annotation to indicate that you are a Spring Boot project; Add the @ EnableZuulProxy annotation to enable the function of Zuul. The code is as follows:

package com.sisyphus;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class EurekaZuulClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaZuulClientApplication.class, args);
    }
}

Make relevant configuration in the project configuration file application.yml, including configuring the address of the service registry as http://localhost:8761/eureka The port number of the program is 5000 and the program name is service zuul

In this case, zuul.routes.hiapi.path is "/ hiapi / * *", and zuul.routes.hiapi.serviceId is "Eureka client". These two configurations can route URLs starting with "/ hiapi" to Eureka client services. Among them, "hiapi" in zuul.routes.hiapi is self-defined, and its path and serviceid need to be specified. When the two are used together, the request Url of the specified type can be routed to the specified serviceid. Similarly, URLs satisfying requests beginning with "/ ribbonapi" will be distributed to Eureka Ribbon client, and URLs satisfying requests beginning with "/ feignapi" will be distributed to Eureka feign client service. If there are multiple instances of a service, Zuul and Ribbon will perform load balancing and route the evenly distributed part of the request to different service instances

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
      
server:
  port: 5000
  
spring:
  application:
    name: service-zuul
    
zuul:
  routes:
    hiapi:
      path: /hiapi/**
      serviceId: eureka-ribbon-client
    ribbonapi:
      path: /ribbonapi/**
      serviceId: eureka-ribbon-client
    feignapi:
      path: /feignapi/**
      serviceId: eureka-feign-client

Start the project Eureka server, Eureka client, Eureka ribbon client, Eureka feign client and Eureka zuul client successively. Eureka client starts two instances with ports 8762 and 8763. Access multiple times on the browser http://localhost:5000/hiapi/hi?name=sisyphus , the browser displays the following alternately:

hi sisyphus,i am from port:8762
hi sisyphus,i am from port:8763

It can be seen that Zuul has done load balancing in routing and forwarding. Similarly, multiple visits http://localhost:5000/feignapi/hi?name=sisyphus And http://localhost:5000/ribbonapi/hi?name=sisyphus You can also see similar content

If you do not need to use the Ribbon for load balancing, you can specify the Url of the service instance and configure it with zuul.routes.hiapi.url. At this time, you do not need to configure zuul.routes.hiapi.serviceId. Once the Url is specified, Zuul can't do load balancing, but directly access the specified Url. This is not advisable in actual development. The code for modifying the configuration is as follows:

zuul:
  routes:
    hiapi:
      path: /hiapi/**
      url: http://localhost:8762

Restart Eureka zuul service, request http://localhost:5000/hiapi/hi?name=sisyphus , the browser displays only the following:

hi sisyphus,i am from port:8762

If you want to specify a Url and want to do load balancing, you need to maintain your own load balancing service registration list. First, change ribbon.eureka.enable to false, that is, the Ribbon load balancing client does not obtain the service registration list information from Eureka Client. Then you need to maintain a registration list. The corresponding service name of the registration list is hiapi-v1 (this name can be customized). Configure multiple load balancing URLs by configuring hiapi-v1.ribbon.listOfServers. The code is as follows:

zuul:
  routes:
    hiapi:
      path: /hiapi/**
      serviceId: hiapi-v1

ribbon:
  eureka:
    enabled: false

hiapi-v1:
  ribbon:
    listOfServers: http://localhost:8762,http://localhost:8763

Restart the Eureka zuul service and access it on the browser http://localhost:5000/hiapi/hi?name=sisyphus , the browser will alternately display the following:

hi sisyphus,i am from port:8762
hi sisyphus,i am from port:8763

2. Configure the version number of API interface on Zuul

If you want to prefix the API interface of each service, for example http://localhost:5000/v1/hiapi/hi?name=sisyphus/ That is, add a V1 to all API interfaces as the version number. The zuul.prefix configuration is required. The configuration example code is as follows:

zuul:
  routes:
    hiapi:
      path: /hiapi/**
      serviceId: hiapi-v1
    ribbonapi:
      path: /ribbonapi/**
      serviceId: eureka-ribbon-client
    feignapi:
      path: /feignapi/**
      serviceId: eureka-feign-client
  prefix: /v1

Restart the Eureka zuul service and access it on the browser http://localhost:5000/v1/hiapi/hi?name=sisyphus , the browser displays alternately:

hi sisyphus,i am from port:8762
hi sisyphus,i am from port:8763

3. Configure fuse on Zuul

As a Netflix component, Zuul can be combined with Ribbon, Eureka, Hystrix and other components to realize the functions of load balancing and fuse. By default, Zuul and Ribbon combine to realize the function of load balancing. Let's explain how to realize the fusing function on Zuul

To implement the fusing function in Zuul, you need to implement the FallbackProvider interface. There are two methods to implement the interface. One is the getRoute() method, which is used to specify which route services the fuse function applies to; Another method, fallbackResponse(), is the logic executed when entering the fuse function. The source code of ZuulFallbackProvider is as follows:

public interface FallbackProvider {
    String getRoute();

    ClientHttpResponse fallbackResponse(String route, Throwable cause);
}

Implement a fuse for Eureka client service. When Eureka client service fails, enter the fuse logic and input an error prompt to the browser

package com.fallbackProvider;

import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

@Component
public class MyFallbackProvider implements FallbackProvider {
    @Override
    public String getRoute() {
        return "eureka-client";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return 200;
            }

            @Override
            public String getStatusText() throws IOException {
                return "OK";
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("oooops!error!i am the fallback.".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}

Restart the Eureka zuul client project, close all instances of Eureka client, and access it in the browser http://localhost:5000/hiapi/hi?name=sisyphus , the browser displays:

oooops!error!i am the fallback.

If you need to add fusing function to all routing services, you only need to return the matching character of "*" on the getRoute() method. The code is as follows:

@Override
    public String getRoute() {
        return "*";
    }

4. Use filter in Zuul

In the previous chapter, the functions and types of filters are described. Here is how to implement a custom filter. It is very simple to implement the filter. You only need to inherit ZuulFilter and implement the abstract methods in ZuulFilter, including filterType(), filterOrder(), and shouldFilter of IZuulFilter, and implement the abstract methods in ZuulFilter, including filterType(), filterOrder(), and shouldFilter() and Object run() of ZuulFilter. Among them, filtertype () is the type of filter. There are four types, namely "pre", "post", "routing" and "error". Filterorder () is the filtering order. It is a value of Int type. The smaller the value, the earlier the filter will be executed. shouldFilter () indicates whether the filter filters logic. If it is true, execute the run () method; If false, the run () method is not executed. The run () method writes specific filtering logic. In this example, check whether the token parameter is worn in the request parameters. If it is not transmitted, the request will not be routed to the specific service instance, and the response will be returned directly. The status code is 401. The code is as follows:

package com.sisyphus.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

@Component
public class MyFilter extends ZuulFilter {
    private static Logger log = LoggerFactory.getLogger(MyFilter.class);
    
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        Object accessToken = request.getParameter("token");
        if(accessToken == null){
            log.warn("token is empty");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            try{
                ctx.getResponse().getWriter().write("token is empty");
            }catch (Exception e){
                return null;
            }
        }
        log.info("ok");
        return null;
    }
}

Restart the service, open the browser, and access http://localhost:5000/hiapi/hi?name=sisyphus , the browser displays:

token is empty

Enter again on the browser http://localhost:5000/hiapi/hi?name=sisyphus&token=token , that is, the request parameter token is added, and the browser displays:

hi sisyphus,i am from port:8762

It can be seen that after the Bean MyFilter is injected into the IoC container, it filters the request and makes a logical judgment before the request routing forwarding. In actual development, this filter can be used for security verification

5. Common usage of zuul

Zuul is implemented by using the DispatchServlet similar to Spring MVC. It adopts the asynchronous blocking model, so its performance is worse than Nginx. Because zuul and other Netflix components can cooperate with each other and integrate seamlessly, zuul can easily realize functions such as load balancing, intelligent routing and fuse. In most cases, zuul exists in the form of clusters. Because zuul's horizontal scalability is very good, when the load is too high, you can solve the performance bottleneck by adding instances

A common usage is to use different Zuul for routing different channels. For example, the mobile terminal uses a Zuul gateway instance, the WEB terminal uses a Zuul gateway instance, and other clients use a Zuul instance for routing

Another common cluster is load balancing through the combination of nginx and Zuul. The most exposed is that nginx master-slave dual hot standby performs Keepalive. Nginx forwards the request route to Zuul cluster through some routing strategy, and Zuul finally distributes the request to specific services

Posted by TreColl on Fri, 19 Nov 2021 04:14:33 -0800