/ Spring Cloud  

Spring Cloud 7: API Gateway - Zuul Part 1

With microservices architecture, each microservice exposes a set of fine-grained services to the outside. The client’s request may involve a series of service calls. If all these microservices are exposed to the client, the client needs to request different microservices multiple times to complete a business process, increasing the code complexity at the client side. In addition, for microservices, we may also need unified authentication and verification of service calls and so on. Although the microservice architecture can divide our development unit into smaller pieces and reduce the development difficulty, if we cannot effectively deal with the problems mentioned above, it may cause the implementation of the microservice architecture to fail.

Zuul refers to the Facade pattern in the GOF design pattern, and combines fine-grained services to provide a coarse-grained service. All requests are imported into a unified entrance. Then the entire service only needs to expose an API, which shields the server-side implementation details. It also reduces the number of client-server network calls. This is the API Gateway service. We can think of the API Gateway as an intermediate layer between the client and the server. All external requests will first pass through the API Gateway. Therefore, the API Gateway has almost become a must choice when implementing a microservice architecture.

The Zuul component of Spring Cloud Netflix can be used as a reverse proxy, forwarding requests to coarse-grained services on the back end through routing addressing, and doing some general logic processing.

With Zuul we can complete the following functions:

  • Dynamic routing
  • Monitoring and review
  • Authentication and security
  • Stress test: gradually increase the traffic of a service cluster to understand the service performance;
  • Canary test
  • Service migration
  • Load tailoring: Allocate the corresponding capacity for each load type and discard requests that exceed the limit;
  • Static response processing

Why we need API Gateway

Simplify client call complexity

Under microservice architecture, the number of instances of the backend service is generally dynamic, and it is difficult for the client to find the address information of the dynamically changed service instance. Therefore, in order to simplify the front-end call logic in microservice-based projects, API Gateway is usually introduced as a lightweight gateway. At the same time, API Gateway will also implement related authentication logic to simplify the complexity of mutual calls between internal services.

Data clipping and aggregation

Generally speaking, different clients have inconsistent requirements for data during display, such as mobile phones or Web terminals, or in low-latency network environments or high-latency network environments.

Therefore, in order to optimize the client’s experience, API Gateway can tailor the general response data to meet the needs of different clients. At the same time, multiple API calls can be aggregated to reduce the number of client requests and optimize the client user experience.

Multi-channel support

Of course, we can also provide different API Gateways for different channels and clients. The use of this mode is another well-known method called Backend for front-end. In Backend for front-end mode, we can target different client to create its BFF separately. For more information about BFF, please refer to this article: Pattern: Backends For Frontends



Example Project

Zuul-Server

1. pom.xml

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

2. Main class

1
2
3
4
5
6
7
@EnableZuulProxy
@SpringBootApplication
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}

Added @EnableZuulProxy annotation to the main application class to start Zuul’s routing service.

3. application.properties

1
2
3
4
5
6
7
server.port=8280

spring.application.name=ZUUL-PROXY

eureka.client.service-url.defaultZone=http://localhost:8761/eureka

management.endpoints.web.exposure.include=hystrix.stream

User-Service

We add another service: User-Service here.

1. pom.xml

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2. Main class

1
2
3
4
5
6
7
@EnableDiscoveryClient
@SpringBootApplication
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}

This is the same as Product-Service.

3. User entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class User {
private String loginName;
private String name;
private String avatar;
private String memos;

public User(String loginName, String name, String avatar, String memos) {
this.loginName = loginName;
this.name = name;
this.avatar = avatar;
this.memos = memos;
}

public String getLoginName() {
return loginName;
}

public void setLoginName(String loginName) {
this.loginName = loginName;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAvatar() {
return avatar;
}

public void setAvatar(String avatar) {
this.avatar = avatar;
}

public String getMemos() {
return memos;
}

public void setMemos(String memos) {
this.memos = memos;
}
}

4. User service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/users")
public class UserEndpoint {
private static final Logger LOGGER = LoggerFactory.getLogger(UserEndpoint.class);

@Value("${server.port:2200}")
private int serverPort = 2200;

@RequestMapping(value = "/{loginName}", method = RequestMethod.GET)
public User detail(@PathVariable String loginName) {
String memos = "I come form " + this.serverPort;
return new User(loginName, loginName, "/avatar/default.png", memos);
}
}

The interface is very simple, just query a user’s information based on the given login name.

5. application.properties

1
2
3
4
5
server.port=2200

spring.application.name=USER-SERVICE

eureka.client.service-url.defaultZone=http://localhost:8761/eureka

Testing

1. Launch services

Start each server in the following order:

  1. Service-discovery
  2. User-Service(2200)
  3. User-Service(2300): java -jar user-service-1.0.0-SNAPSHOT.jar --server.port=2300
  4. Zuul-Server

We can see the eureka admin page:



2. Test routing

Visit: http://localhost:8280/user-service/users/admin



Note that here we used service name to retrieve the relevant servicie.

3. Load balance testing

Let’s test if load balancing works. Earlier we have started two User-Service microservices, the ports are: 2200 and 2300. Try visit: http://localhost:8280/user-service/users/admin to make a request, we will see that the following information will be output alternately on the screen:





4. Hystrix fault tolerance and monitoring test

We have integrated Hystrix monitoring in Zuul-Server. Visit: http://localhost:8280/hystrix/ and enter: http://localhost:8280/actuator/hystrix.stream to start monitor.



This means that Zuul has integrated Hystrix.

Spring-cloud-starter-netflix-zuul itself has integrated hystrix and ribbon, so Zuul is inherently equipped with thread isolation and circuit breaker self-protection capabilities, as well as client load balancing for service calls. However, we need to note that when using the mapping relationship between path and url to configure routing rules, requests for routing will not be wrapped with HystrixCommand, so this type of routing request has no thread isolation and circuit breaker protection functions, and also there will be no load balancing capabilities. Therefore, when using Zuul, we try to use a combination of path and serviceId to configure. This not only can ensure the robustness and stability of the API gateway, but also can use Ribbon‘s client load balancing function.

Zuul Configuration

Routing configuration

Maybe you think it’s strange that we didn’t configure anything, but can access service through http://localhost:8280/user-service/users/admin. This is the default route mapping function of Zuul, so let’s take a look at how to configure routing in Zuul.

1. Service routing default rules

When we used Eureka to built the API gateway, Zuul will automatically create a default routing rule for each service: the access path is prefixed with the service name configured by serviceId, which is why we were able to use:

1
http://localhost:8280/user-service/users

to access the users endpoint provided in User-Service.

2. Customized microservice access path

The configuration format is: zuul.routes.Microservice Id = specified path, such as:

1
zuul.routes.user-service = /user/**

In this way, we can access the services provided by user-service through /user/. For example, the previous access can be changed to: http://localhost:8280/user/users/admin.

The path to be configured can specify a regular expression to match the path. Therefore, /user/* can only match the first-level path, but /user/** can match all paths starting with /user/.

3. Ignore specified microservices

Format: zuul.ignored-services = Micro service Id1, Micro service Id2 ..., multiple micro services are separated by commas. Such as:

1
zuul.ignored-services=user-service,product-service

4. Specify the microservice Id and the corresponding path at the same time

1
2
3
4
5
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=service-A

zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=service-B

5. Specify microservice URL and corresponding path at the same time

1
2
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.url=http://localhost:8080/api-a

As mentioned before, the routing configured through the URL will not be executed by HystrixCommand. As a result, Ribbon‘s load balancing, downgrading, and circuit breaker functions will not be obtained. Therefore, try to use serviceId for configuration, you can also use the following configuration.

6. Specify multiple service instances and load balancing

If multiple service instances need to be configured, the configuration is as follows:

1
2
3
4
5
zuul.routes.user.path: /user/**
zuul.routes.user.serviceId: user

ribbon.eureka.enabled=false
user.ribbon.listOfServers: http://192.168.1.10:8081, http://192.168.1.11:8081

7. Forward to local url

1
2
zuul.routes.user.path=/user/**
zuul.routes.user.url=forward:/user

8. Route prefix

You can add a uniform prefix to all mappings through zuul.prefix. For example: /api. By default, the proxy will automatically strip this prefix before forwarding. If you need to prefix with forwarding, you can configure: zuul.stripPrefix = false to turn off this default behavior. E.g:

1
2
zuul.routes.users.path=/myusers/**
zuul.routes.users.stripPrefix=false

Note: zuul.stripPrefix only works on the prefix of zuul.prefix. Does not work for prefix specified by path.

9. Routing configuration order

If you want to control the routing rules according to the configured order, you need to use YAML. If you use the property file, the order will be lost. E.g:

1
2
3
4
5
6
zuul:
routes:
users:
path: /myusers/**
legacy:
path: /**

If the above example is configured using a properties file, the legacy may be effective, so the users have no effect.

10. Custom transformation

We can also have a converter that uses a regular expression between the serviceId and the route to automatically match. E.g:

1
2
3
4
5
6
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}

In this way, the service whose serviceId is users-v1 will be mapped to the route of /v1/users/. Any regular expression is fine, but all named groups must include servicePattern and routePattern. If servicePattern does not match a serviceId, then the default is used. In the above example, a service with serviceId users will be mapped to the route /users/(without version information). This feature is off by default and only applies to services that have been discovered.

Zuul Header Settings

Sensitive header settings

It is not a problem to share information through headers between services in the same system, but if you don’t want some sensitive information in headers to leak out with HTTP forwarding, you need to specify a list of headers to be ignored in the routing configuration.

By default, when Zuul requests routing, it will filter some sensitive information in the HTTP request header information. The default sensitive header information is defined through zuul.sensitiveHeaders, including Cookie, Set-Cookie, and Authorization. The configured sensitiveHeaders can be separated by commas.

The specified routes can be configured with:

1
2
zuul.routes.[route].customSensitiveHeaders=true 
zuul.routes.[route].sensitiveHeaders=[set headers here]

Set global:

1
zuul.sensitiveHeaders=[set headers here]

Ignore Header settings

If you need to configure some additional sensitive headers for each route, you can use zuul.ignoredHeaders to uniformly set the headers to be ignored. Such as:

1
zuul.ignoredHeaders=[set Header to ignore]

There is no such configuration by default. If Spring Security is introduced in the project, then Spring Security will automatically add this configuration. The default values are: Pragma, Cache-Control, X-Frame-Options, X-Content-Type- Options, X-XSS-Protection, Expries.

If you also need to use the Spring Security Header of the downstream microservice, you can add the following settings:

1
zuul.ignoreSecurityHeaders=false

Zuul Http Client

Zuul‘s Http client supports Apache Http, Ribbon’s RestClient, and OkHttpClient. By default, the Apache HTTP client is used. The corresponding clients can be enabled in the following ways:

1
2
3
4
5
# Enable Ribbon'RestClient
ribbon.restclient.enabled=true

# Enable OkHttpClient
ribbon.okhttp.enabled=true

If you need to use OkHttpClient, please note that com.squareup.okhttp3 related packages are already included in your project.

Zuul fault tolerance and fallback

Let’s take a look at the previous monitoring interface of Hystrix:



Please note that the granularity of Zuul‘s Hystrix monitoring is microservices, not an API, that is, all requests passing Zuul will be protected by Hystrix. If we shut down the User-Service service now, what will happen when we visit it again?



So how do we implement fault tolerance and fallback for Zuul? Zuul provides a ZuulFallbackProvider interface. By implementing this interface, we can implement fallback functions for Zuul. So let’s transform Zuul-Server before.

1
2


Note that:

  • The getRoute method returns that we want to provide a fallback for that microservice. Note that the returned value is the name of the route, not the name of the service, and cannot be written as: USER-SERVICE, otherwise the fallback will not work;
  • The fallbackResponse method returns a ClientHttpResponse object as our fallback response.

Now do a test, stop User-Service and visit:http://localhost:8280/user-service/users/admin



Note that the fallback method has worked. If it doesn’t work, double check that getRoute returns correctly.

Check out the source code here: zuul part 1 demo