Gateway 入门教程 - 中级篇

上篇 - Gateway 入门教程(基础篇)

前言

版本说明

在本文中,默认使用的 Spring Cloud 版本是 Finchley.RELEASE,对应的 Spring Boot 版本是 2.0.3,特别声明除外。

Gateway 基于服务发现的路由规则

Gateway 的服务发现路由概述

Spring Cloud 对 Zuul 进行封装处理之后,当通过 Zuul 访问后端微服务时,基于服务发现的默认路由规则是:http://zuul_host:zuul_port/微服务在 Eureka 上的 serviceld/**。 Spring Cloud Gateway 在设计的时候考虑了从 Zuul 迁移到 Gateway 的 兼容性和迁移成本等,Gateway 基于服务发现的路由规则和 Zuul 的设计类似,但是也有很大差别。Spring Cloud Gateway 基于服务发现的路由规则,在不同注册中心下其差异如下:

  • 如果把 Gateway 注册到 Consul 上,通过网关转发服务调用,服务名默认小写,不需要做任何处理
  • 如果把 Gateway 注册到 Zookeeper 上,通过网关转发服务调用,服务名默认小写,不需要做任何处理
  • 如果把 Gateway 注册到 Eureka 上,通过网关转发服务调用,访问网关的 URL 是 http://Gateway_HOST:Gateway_PORT/大写的 serviceld/*,其中服务名默认必须是大写,否则会抛 404 错误;如果服务名要用小写访问,可以在属性配置文件里面加 spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true 配置解决

Gateway 服务发现的路由规则案例

下面将使用 Eureka 作为注册中心来剖析 Gateway 服务发现的路由规则,其中各个模块的说明如下,由于篇幅有限,这里只给出核心的配置和代码,点击下载完整的案例代码。

模块端口说明
micro-service-gateway-routeN/A 聚合父 Maven 工程
micro-service-eureka9000Eureka 注册中心
micro-service-gateway9001 基于 Spring Cloud Gateway 的网关服务
micro-service-provider9002 服务提供者
micro-service-consumer9003 服务消费者

1. 创建 Maven 父级 Pom 工程

在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下:

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
46
47
48
49
50
51
52
53
54
55
56
57
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
</parent>

<!-- 利用传递依赖,公共部分 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<!-- 管理依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<!--注意:这里需要添加以下配置,否则可能会有各种依赖问题 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>

2. 创建 Micro Service Eureka 工程

创建 Micro Service Eureka 的 Maven 工程,配置工程里的 pom.xml 文件:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

创建 Micro Service Eureka 的启动主类:

1
2
3
4
5
6
7
8
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}

创建 Micro Service Eureka 的 application.yml 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 9000

spring:
application:
name: eureka-server

eureka:
instance:
hostname: localhost #Eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己
fetch-registry: false #false表示自己就是注册中心,职责就是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

3. 创建 Micro Service Gateway 工程

创建 Micro Service Gateway 的 Maven 工程,配置工程里的 pom.xml 文件,由于需要将 Gateway 服务注册到 Eureka,因此需要引入 Eureka Client;同时为了避免 Gateway 的依赖冲突,排除引入 spring-webmvcspring-boot-starter-tomcat

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
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

创建 Micro Service Gateway 的启动主类:

1
2
3
4
5
6
7
@SpringBootApplication
public class GatewayServerApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class, args);
}
}

创建 Micro Service Gateway 的 application.yml 配置文件,其中 spring.cloud.gateway.discovery.locator.enabled 表示是否与服务发现组件进行结合,通过 serviceId 转发到具体的服务实例,默认为 false,若为 true 则开启基于服务发现的路由规则。spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true 表示当注册中心为 Eureka 时,设置为 true 表示开启用小写的 serviceId 进行基于服务路由的转发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server:
port: 9001

spring:
application:
name: gateway-server
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:9000/eureka
instance:
instance-id: gateway-server-${server.port}
prefer-ip-address: true

4. 创建 Micro Service Provider 工程

创建 Micro Service Provider 的 Maven 工程,配置工程里的 pom.xml 文件:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

创建 Micro Service Provider 的启动主类:

1
2
3
4
5
6
7
8
@EnableDiscoveryClient
@SpringBootApplication
public class ProviderApplication {

public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}

创建 Micro Service Provider 的测试控制类:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/provider")
public class ProviderController {

@Value("${server.port}")
private String port;

@GetMapping("/sayHello/{name}")
public String sayHello(@PathVariable("name") String name) {
return "from port: " + port + ", hello " + name;
}
}

创建 Micro Service Provider 的 application.yml 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 9002

spring:
application:
name: provider-service

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:9000/eureka
instance:
instance-id: provider-service-${server.port}
prefer-ip-address: true

5. 创建 Micro Service Consumer 工程

创建 Micro Service Consumer 的 Maven 工程,配置工程里的 pom.xml 文件:

1
2
3
4
5
6
7
8
<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-openfeign</artifactId>
</dependency>

创建 Micro Service Consumer 的启动主类:

1
2
3
4
5
6
7
8
9
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {

public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}

创建 Micro Service Consumer 的服务调用接口:

1
2
3
4
5
6
@FeignClient("provider-service")
public interface ProviderService {

@RequestMapping(value = "/provider/sayHello/{name}", method = RequestMethod.GET)
public String sayHello(@PathVariable("name") String name);
}

创建 Micro Service Consumer 的测试控制类:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/consumer")
public class ConsumerController {

@Autowired
private ProviderService providerService;

@GetMapping("/sayHello/{name}")
public String sayHello(@PathVariable("name") String name) {
return providerService.sayHello(name);
}
}

创建 Micro Service Consumer 的 application.yml 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 9003

spring:
application:
name: consumer-service

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:9000/eureka
instance:
instance-id: consumer-service-${server.port}
prefer-ip-address: true

6. 测试结果

  1. 依次启动 micro-service-eureka、micro-service-gateway、micro-service-provider、micro-service-consumer 应用
  2. 访问 http://127.0.0.1:9000/,查看各个服务是否都成功注册到 Eureka
  3. 访问 http://127.0.0.1:9001/consumer-service/consumer/sayHello/Peter,查看是否可以成功通过 Gateway 调用 Consumer 的接口

Gateway Filter 和 Global Filter

Spring Cloud Gateway 中的 Filter 从接口实现上分为两种:一种是 Gateway Filter,另外一种是 Global Filter。下面将给出这两种 Filter 的自定义使用示例,点击下载完整的案例代码。

Gateway Filter 和 Global Filter 的概述

  • Gateway Filter: 从 Web Filter 中复制过来的,相当于一个 Filter 过滤器,可以对访问的 URL 过滤,进行横切处理(切面处理),应用场景包括超时处理、安全检查等。
  • Global Filter: Spring Cloud Gateway 定义了 Global Filter 的接口,可以让开发者自定义实现自己的 Global Filter。顾名思义,Global Filter 是一个全局的 Filter,作用于所有路由。

Gateway Filter 和 Global Filter 的区别

从路由的作用范围来看,Global Filter 会被应用到所有的路由上,而 Gateway Filter 则应用到单个路由或者一个分组的路由上。从源码设计来看,Gateway Filter 和 Global Filter 两个接口中定义的方法一样,都是 Mono filter(),唯一的区别就是 Gateway Filter 继承了 ShortcutConfigurable,而 Global Filter 没有任何继承。

自定义 Gateway Filter 案例

  1. 创建自定义的 Gateway Filter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CustomGatewayFilter implements GatewayFilter, Ordered {

private static final Logger logger = LoggerFactory.getLogger(CustomGatewayFilter.class);
private static final String COUNT_START_TIME = "countProcessTime";

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(COUNT_START_TIME, System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(COUNT_START_TIME);
if (startTime != null) {
Long countTime = System.currentTimeMillis() - startTime;
logger.info(exchange.getRequest().getURI().getRawPath() + ": " + countTime + " ms");
}
}));
}

@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
  1. 将 Gateway Filter 配置到路由上,由于 Gateway Filter 是作用于单个路由或者一个分组的路由上的,因此这里需要使用 Java 的流式 API 绑定 Gateway Filter 和路由,或者使用 YML 文件的方式配置路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator customGatewayFilter(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/custom/gateway/filter")
.filters(f -> f.filter(new CustomGatewayFilter()))
.uri("http://127.0.0.1:9090/provider/sayHello/Jim/")
.order(0)
.id("custom-gateway-filter")
)
.build();
}
}

自定义 Global Filter 案例

下面通过简单定义一个名为 CustomGlobalFilter 的全局过滤器,对请求到网关的 URL 进行权限校验,判断请求的 URL 是否为合法请求。全局过滤器处理的逻辑是通过从 Gateway 的 上下文 ServerWebExchange 对象中获取 authToken 对应的值进行判 Null 处理,也可以根据需求定制开发更复杂的校验逻辑。因为 Global Filter 是作用在所有的路由上,因此只需要添加 @Component 注解,将 CustomGlobalFilter 的 Bean 注入进 Spring 的容器内即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("authToken");
if (null == token || token.isEmpty()) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}

@Override
public int getOrder() {
return -400;
}
}

Gateway 实战场景

Spring Cloud Gateway 权重路由

WeightRoutePredicateFactory 是一个路由断言工厂,在 Spring Cloud Gateway 中可以使用它对 URL 进行权重路由,只需在配置时指定分组和权重值即可。

权重路由的使用场景

在开发、测试的时候,或者线上发布、线上服务多版本控制的时候,需要对服务进行权重路由。最常见的使用场景就是一个服务有两个版本:旧版本 V1、新版本 V2。在线上灰度发布的时候,需要通过网关动态实时推送路由权重信息。比如 95% 的流量走服务 V1 版本,5% 的流量走服务 V2 版本。

权重路由案例

下面的案例中,Spring Cloud Gateway 会根据权重路由规则,针对特定的服务,把 95% 的请求流量分发给服务的 V1 版本,把剩余 5% 的流量分发给服务的 V2 版本,由此进行权重路由,点击下载完整的案例代码。

创建 Gateway Server 工程里的 pom.xml 配置文件:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

创建 Gateway Server 工程里的启动主类:

1
2
3
4
5
6
7
@SpringBootApplication
public class GatewayServerApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class, args);
}
}

创建 Gateway Server 工程里的 application.yml 配置文件,添加两个针对 /test 路径转发的路由定义配置,这两个路由属于同一个权重分组,权重的分组名称为 group

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
server:
port: 9090

spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: provider-service-v1
uri: http://127.0.0.1:9091/v1/
predicates:
- Path=/test
- Weight=group, 95
- id: provider-service-v2
uri: http://127.0.0.1:9091/v2/
predicates:
- Path=/test
- Weight=group, 5

logging:
level:
org.springframework.cloud.gateway: TRACE
org.springframework.http.server.reactive: DEBUG
org.springframework.web.reactive: DEBUG
reactor.ipc.netty: DEBUG

management:
endpoints:
web:
exposure:
include: '*'
security:
enabled: false

创建 Provider Service 工程里的测试控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class ProviderController {

@GetMapping("/v1")
public String v1() {
return "version: v1";
}

@GetMapping("/v2")
public String v2() {
return "version: v2";
}
}

创建 Provider Service 工程里的启动主类:

1
2
3
4
5
6
7
@SpringBootApplication
public class ProviderApplication {

public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}

创建 Provider Service 工程里的 application.yml 配置文件:

1
2
3
4
5
6
server:
port: 9091

spring:
application:
name: provider-service

测试结果:

  1. 依次启动 gateway-server、provider-service 应用
  2. 多次访问 http://127.0.0.1:9090/test ,会发现按权重配置返回对应的请求内容

Spring Cloud Gateway 的 HTTPS 使用

大型互联网应用的生产环境基本是全站 HTTPS,常规的做法是通过 Nginx 来配置 SSL 证书。如果使用 Spring Cloud Gateway 作为 API 网关,统一管理所有 API 请求的入口和出口,此时 Spring Cloud Gateway 就需要支持 HTTPS。由于 Spring Cloud Gateway 是基于 Spring Boot 2.0 构建的,所以只需要将生成的 HTTPS 证书放到 Spring Cloud Gateway 应用的类路径下面即可。

HTTPS 案例

下面将介绍如何在 Spring Cloud Gateway 中使用 HTTPS,其中各个模块的说明如下。由于本案例是基于上面的 “Gateway 服务发现的路由规则案例 “ 改造而来的,因此 micro-service-eureka、micro-service-provider-1、micro-service-provider-2 工程里的配置和代码不再累述,点击下载完整的案例代码。

模块端口说明
micro-service-gateway-httpsN/A 聚合父 Maven 工程
micro-service-eureka9000Eureka 注册中心
micro-service-gateway9001 带有 HTTPS 证书的网关服务,使用 HTTPS 协议访问
micro-service-provider-19002 服务提供者,使用 HTTP 协议
micro-service-provider-29003 服务提供者,使用 HTTP 协议

创建 Micro Service Gateway 工程里的 pom.xml 配置文件:

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
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

创建 Micro Service Gateway 工程里的启动主类:

1
2
3
4
5
6
7
@SpringBootApplication
public class GatewayServerApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class, args);
}
}

创建 Micro Service Gateway 工程里的 application.yml 配置文件,通过 key-store 指定 HTTPS 证书的路径:

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
server:
port: 9001
ssl:
enabled: true
key-alias: spring
key-password: spring
key-store: classpath:self-signed.jks
key-store-type: JKS
key-store-provider: SUN
key-store-password: spring

spring:
application:
name: gateway-server
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:9000/eureka
instance:
instance-id: gateway-server-${server.port}
prefer-ip-address: true

测试结果:

  1. 依次启动 micro-service-eureka、micro-service-provider-1、micro-service-provider-2、micro-service-gateway 应用
  2. 通过 HTTPS 协议访问 https://127.0.0.1:9001/provider-service/provider/sayHello/Jim,会出现如下的错误:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
io.netty.handler.ssl.NotSslRecordException: not an SSL/TLS record: 485454502f312e3120343030200d0a5472616e736665722d456e636f646 ...
at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1156) [netty-handler-4.1.25.Final.jar:4.1.25.Final]
at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1221) [netty-handler-4.1.25.Final.jar:4.1.25.Final]
at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:489) ~[netty-codec-4.1.25.Final.jar:4.1.25.Final]
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:428) ~[netty-codec-4.1.25.Final.jar:4.1.25.Final]
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:265) ~[netty-codec-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:808) ~[netty-transport-native-epoll-4.1.25.Final-linux-x86_64.jar:4.1.25.Final]
at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:408) ~[netty-transport-native-epoll-4.1.25.Final-linux-x86_64.jar:4.1.25.Final]
at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:308) ~[netty-transport-native-epoll-4.1.25.Final-linux-x86_64.jar:4.1.25.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:884) ~[netty-common-4.1.25.Final.jar:4.1.25.Final]
at java.lang.Thread.run(Thread.java:745) ~[na:1.8.0_102]

HTTPS 转 HTTP 的问题

上述错误出现的原因是通过 Spring Cloud Gateway 请求进来的协议是 HTTPS,而后端被代理的服务是 HTTP 协议的请求,所以当 Gateway 用 HTTPS 请求转发调用 HTTP 协议的服务时,就会出现 not an SSL/TLS record 的错误。本质上这是一个 Spring Cloud Gateway 将 HTTPS 请求转发调用 HTTP 服务的问题。由于服务的拆分,在微服务的应用集群中会存在很多服务提供者和服务消费者,而这些服务提供者和服务消费者基本都是部署在企业内网中,没必要全部加 HTTPS 进行调用。因此 Spring Cloud Gateway 对外的请求是 HTTPS,对后端代理服务的请求可以是 HTTP。通过 Debug 调试源码分析,LoadBalancerClientFilter.filter() 方法如下:

1
2
3
4
5
6
7
8
9
URI uri = exchange.getRequest().getURI();
String overrideScheme = null;
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}

URI requestUrl = this.loadBalancer.reconstructURI(new LoadBalancerClientFilter.DelegatingServiceInstance(instance, overrideScheme), uri);
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);

从上面的代码可以看出,LoadBalancer 对 HTTP 请求进行封装,如果从 Spring Cloud Gateway 进来的请求是 HTTPS,它就用 HTTPS 封装,如果是 HTTP 就用 HTTP 封装,而且没有预留 任何扩展修改的接口,只能通过自定义 Global Filter 的方式对其修改。下面介绍两种修改方法,在实践中任选其中一种即可。

官方 Issues 说明
第一种解决方案

在 LoadBalancerClientFilter 执行之前将 HTTPS 修改为 HTTP 协议:

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
@Component
public class HttpsToHttpFilter implements GlobalFilter, Ordered {

private static final int HTTPS_TO_HTTP_FILTER_ORDER = 10099;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI originalUri = exchange.getRequest().getURI();
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String forwardedUri = request.getURI().toString();
if (forwardedUri != null && forwardedUri.startsWith("https")) {
try {
URI mutatedUri = new URI("http",
originalUri.getUserInfo(),
originalUri.getHost(),
originalUri.getPort(),
originalUri.getPath(),
originalUri.getQuery(),
originalUri.getFragment());
mutate.uri(mutatedUri);
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
ServerHttpRequest build = mutate.build();
return chain.filter(exchange.mutate().request(build).build());
}

/**
* 由于LoadBalancerClientFilter的order是10100
* 要在LoadBalancerClientFilter执行之前将Https修改为Http,需要设置order为10099
* @return
*/
@Override
public int getOrder() {
return HTTPS_TO_HTTP_FILTER_ORDER;
}
}
第二种解决方案

在 LoadBalancerClientFilter 执行之后将 HTTPS 修改为 HTTP,拷贝 RibbonUtils 中的 upgradeconnection 方法来自定义全局过滤器:

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
@Component
public class HttpSchemeFilter implements GlobalFilter, Ordered {

private static final int HTTPS_TO_HTTP_FILTER_ORDER = 10101;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Object uriObj = exchange.getAttributes().get(GATEWAY_REQUEST_URL_ATTR);
if (uriObj != null) {
URI uri = (URI) uriObj;
uri = this.upgradeConnection(uri, "http");
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri);
}
return chain.filter(exchange);
}

private URI upgradeConnection(URI uri, String scheme) {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(uri).scheme(scheme);
if (uri.getRawQuery() != null) {
// When building the URI, UriComponentsBuilder verify the allowed characters and does not
// support the '+' so we replace it for its equivalent '%20'.
// See issue https://jira.spring.io/browse/SPR-10172
uriComponentsBuilder.replaceQuery(uri.getRawQuery().replace("+", "%20"));
}
return uriComponentsBuilder.build(true).toUri();
}

/**
* 由于LoadBalancerClientFilter的order是10100,所以设置HttpSchemeFilter的的order是10101
* 在LoadBalancerClientFilter之后将https修改为http
* @return
*/
@Override
public int getOrder() {
return HTTPS_TO_HTTP_FILTER_ORDER;
}
}

Spring Cloud Gateway 集成 Swagger

Swagger 是一个可视化 API 测试工具,可以和应用完美融合。通过声明接口注解的方式,可以方便快捷地获取 API 调试界面进行测试。Zuul 可以很方便地与 Swagger 整合在一起,由于 Spring Cloud Finchley 版是基于 Spring Boot 2.0 的,而 Spring Cloud Gateway 的底层是基于 WebFlux 实现的,且经验证,WebFlux 和 Swagger 不兼容。如果按照 Zuul 集成 Swagger 的方式,应用启动的时候会报错。下面将介绍 Spring Cloud Gateway 如何集成 Swagger,其中各个模块的说明如下。由于本案例是基于上面的 “Gateway 服务发现的路由规则案例 “ 改造而来的,因此 micro-service-eureka 工程里的配置和代码不再累述,点击下载完整的案例代码。

模块端口说明
micro-service-gateway-swaggerN/A 聚合父 Maven 工程
micro-service-eureka9000Eureka 注册中心
micro-service-gateway9001 基于 Spring Cloud Gateway 的网关服务
micro-service-provider-19002 服务提供者
micro-service-provider-29003 服务提供者

1. 创建 Micro Service Gateway 工程

创建 Micro Service Gateway 工程里的 pom.xml 配置文件:

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
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

创建 Micro Service Gateway 工程里的 SwaggerProvider 类,因为 Swagger 暂不支持 WebFlux 项目,所以不能在 Gateway 中配置 SwaggerCoufig,需要编写 GatewaySwaggerProvider 实现 SwaggerResourcesProvider 接口,用于获取 SwaggerResources:

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
/**
* @Primary注解的实例优先于其他实例被注入
*/
@Primary
@Component
public class GatewaySwaggerProvider implements SwaggerResourcesProvider {

private final RouteLocator routeLocator;
private final GatewayProperties gatewayProperties;
public static final String API_URI = "/v2/api-docs";

public GatewaySwaggerProvider(RouteLocator routeLocator, GatewayProperties gatewayProperties) {
this.routeLocator = routeLocator;
this.gatewayProperties = gatewayProperties;
}

@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
//取出Spring Cloud Gateway中的route
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
//结合application.yml中的路由配置,只获取有效的route节点
gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId()))
.forEach(routeDefinition -> routeDefinition.getPredicates().stream()
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition -> resources.add(swaggerResource(routeDefinition.getId(),
predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
.replace("/**", API_URI)))));
return resources;
}

private SwaggerResource swaggerResource(String name, String location) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}

创建 Micro Service Gateway 工程里的 Swagger-Resource 端点,因为没有在 Gateway 中配置 SwaggerConfig,但是运行 Swagger-UI 的时候需要依赖一些接口,所以需要建立相应的 Swagger-Resource 端点:

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
@RestController
@RequestMapping("/swagger-resources")
public class SwaggerHandler {

@Autowired(required = false)
private SecurityConfiguration securityConfiguration;

@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;

@Autowired
public SwaggerHandler(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}

@GetMapping("/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}

@GetMapping("/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}

@GetMapping("")
public Mono<ResponseEntity> swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}

创建 Micro Service Gateway 工程里的 GwSwaggerHeaderFilter 类,由于在路由规则为 admin/test/{a}/{b} 时,Swagger 界面上会显示为 test/{a}/{b},缺少了 /admin 这个路由节点。通过 Debug 断点调试发现,Swagger 会根据 X-Forwarded-Prefix 这个 Header 来获取 BasePath,因此需要将它添加到接口路径与 Host 之间才能正常工作。但是 Gateway 在做转发的时候并没有将这个 Header 添加到 Request 上,从而导致接口调试出现 404 错误。为了解决该问题,需要在 Gateway 中编写一个过滤器来添加这个 Header。特别注意,Spring Boot 版本为 2.0.6 以上的可以跳过这一步骤,最新源码里 Spring Boot 修复了该 Bug,已经默认添加上了这个 Header。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class GwSwaggerHeaderFilter extends AbstractGatewayFilterFactory {

private static final String HEADER_NAME = "X-Forwarded-Prefix";

@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (!StringUtils.endsWithIgnoreCase(path, GatewaySwaggerProvider.API_URI)) {
return chain.filter(exchange);
}
String basePath = path.substring(0, path.lastIndexOf(GatewaySwaggerProvider.API_URI));
ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
};
}
}

创建 Micro Service Gateway 工程里的 application.yml 配置文件,添加上面编写的 GwSwaggerHeaderFilter 过滤器, URI 指定为 lb://provider-service-1,表示负载均衡到 provider-service-1 服务。由于 Swagger 发出请求 的 URL 都是以 /xxxx 开头,因此需要使用 StripPrefix 过滤器将第一个路由节点(/xxxx)去掉。

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
server:
port: 9001

spring:
application:
name: gateway-server
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: provider-service-1
uri: lb://provider-service-1
predicates:
- Path=/provider1/**
filters:
- GwSwaggerHeaderFilter
- StripPrefix=1
- id: provider-service-2
uri: lb://provider-service-2
predicates:
- Path=/provider2/**
filters:
- GwSwaggerHeaderFilter
- StripPrefix=1

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:9000/eureka
instance:
instance-id: gateway-server-${server.port}
prefer-ip-address: true

management:
endpoints:
web:
exposure:
include: '*'
security:
enabled: false

2. 创建 Micro Service Provider 1 工程

创建 Micro Service Provider 1 工程里的 pom.xml 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>

创建 Micro Service Provider 1 工程里的 SwaggerConfig 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@EnableSwagger2
public class SwaggerConfig {

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Swagger API")
.description("验证 Gateway 集成 Swagger 的效果")
.termsOfServiceUrl("")
.version("2.0")
.build();
}
}

创建 Micro Service Provider 1 工程里的测试控制类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@Api("provider-service-1 接口测试")
@RequestMapping("/provider1")
public class ProviderOneController {

@ApiOperation(value = "计算+", notes = "加法")
@ApiImplicitParams({
@ApiImplicitParam(name = "a", value = "数字a", required = true, dataType = "Long"),
@ApiImplicitParam(name = "b", value = "数字b", required = true, dataType = "Long")
})
@GetMapping("/{a}/{b}")
public String get(@PathVariable Integer a, @PathVariable Integer b) {
return "from provider service 1, the result is: " + (a + b);
}
}

创建 Micro Service Provider 1 工程里的 application.xml 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 9002

spring:
application:
name: provider-service-1

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:9000/eureka
instance:
instance-id: provider-service-1-${server.port}
prefer-ip-address: true

3. 创建 Micro Service Provider 2 工程

由于 Micro Service Provider 2 工程 与 Micro Service Provider 1 工程里的配置和代码都差不多,这里不再累述。

4. 测试结果

  1. 依次启动 micro-service-eureka、micro-service-provider-1、micro-service-provider-2、micro-service-gateway 应用
  2. 访问 http://127.0.0.1:9000/,查看各个服务是否都成功注册到 Eureka
  3. 访问 http://127.0.0.1:9001/swagger-ui.html,查看 Swagger 的界面是否正常工作,查看截图
  4. 在 Swagger 的界面上打开对应的 URL,输入测试数据,验证 Swagger 经过 Gateway 是否可以正常访问 Provider1 和 Provider2 服务的接口,查看截图

Spring Cloud Gateway 限流

Gateway 限流概述

在开发高并发系统时可以用三把利器来保护系统:缓存、降级和限流。缓存的目的是提升系统访问速度和增大系统处理的容量,是抗高并发流量的 “银弹”;而降级是当服务出现问题或者影响到核心流程时,需要暂时将其屏蔽掉,待高峰过去之后或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询等,因此需要有一种手段来限制这些场景的并发 / 请求量,即限流。限流的目的是通过对并发访问 / 请求进行限速或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或友好的展示页)、排队或等待(比如秒杀、评论、下单等场景)、降级(返回兜底数据或默认数据)。主流的中间件都会有单机限流框架,一般支持两种限流模式:控制速率和控制并发。Spring Cloud Zuul 通过第三方扩展 spring-cloud-zuul-ratelimit 也可以支持限流。Spring Cloud Gateway 是一个 API 网关中间件,网关是所有请求流量的入口;特别是像天猫双十一、双十二等高并发场景下,当流量迅速剧增,网关除了要保护自身之外,还要限流保护后端应用。常见的限流算法有漏桶和令牌桶,计数器也可以进行粗暴限流实现。对于限流算法,可以参考 Guava 中的 RateLimiter、Bucket4jRateLimitJ 等项目的具体实现。下面将介绍如何基于 Bucket4j、Gateway 内置的限流过滤器工厂(RequestRateLimiterGatewayFilterFactory)、CPU 使用率实现限流,点击下载完整的案例代码。

Gateway 限流方案

基于 Bucket4j 实现限流

在 Spring Cloud Gateway 中实现限流比较简单,只需要编写一个过滤器就可以。下面介绍在 Spring Cloud Gateway 中使用 Bucket4j 实现限流,由于篇幅有限,只给出 Gateway Server 工程的核心代码和配置。

  1. 添加 Maven 依赖
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>4.10.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
  1. 编写自定义过滤器对特定资源进行限流
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* 针对客户端IP进行限流
*/
public class GatewayRateLimitFilterByIp implements GatewayFilter, Ordered {

private final Logger log = LoggerFactory.getLogger(GatewayRateLimitFilterByIp.class);

/**
* 单机网关限流用一个ConcurrentHashMap来存储 bucket,
* 如果是分布式集群限流的话,可以采用 Redis等分布式解决方案
*/
private static final Map<String, Bucket> LOCAL_CACHE = new ConcurrentHashMap<>();

/**
* 令牌桶的最大容量,即能装载令牌的最大数量
*/
int capacity;

/**
* 每次补充令牌的数量
*/
int refillTokens;

/**
* 补充令牌的时间间隔
*/
Duration refillDuration;

public GatewayRateLimitFilterByIp() {

}

public GatewayRateLimitFilterByIp(int capacity, int refillTokens, Duration refillDuration) {
this.capacity = capacity;
this.refillTokens = refillTokens;
this.refillDuration = refillDuration;
}

private Bucket createNewBucket() {
Refill refill = Refill.greedy(refillTokens, refillDuration);
Bandwidth limit = Bandwidth.classic(capacity, refill);
return Bucket4j.builder().addLimit(limit).build();
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
Bucket bucket = LOCAL_CACHE.computeIfAbsent(ip, k -> createNewBucket());
log.info("IP:{} ,令牌桶可用的令牌数量:{} ", ip, bucket.getAvailableTokens());
if (bucket.tryConsume(1)) {
return chain.filter(exchange);
} else {
//当可用的令牌数为0时,进行限流,返回429状态码
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
}
}

@Override
public int getOrder() {
return -1000;
}

// 省略Get和Set方法 ...
}
  1. 通过 Java 流式 API 的方式配置路由规则,其中 http://127.0.0.1:9091/sayHello/peter/ 对应的是后端的服务,这里不再累述
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator rateLimitFilterByIp(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/rateLimit")
.filters(f -> f.filter(new GatewayRateLimitFilterByIp(10, 1, Duration.ofSeconds(1))))
.uri("http://127.0.0.1:9091/sayHello/peter/")
.id("ratelimit_route"))
.build();
}
}
  1. 编写 application.yml 配置文件
1
2
3
4
5
6
server:
port: 9090

spring:
application:
name: gateway-server
  1. 测试结果

启动各个应用后,多次访问 http://127.0.0.1:9090/rateLimit,可以看到控制台输出如下日志信息。当可用的令牌数量为 0 时,Spring Cloud Gateway 中自定义的限流过滤器开始拒绝处理请求,直接返回 429 状态码(因为请求太多,限流返回 429 状态码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c.s.s.filter.GatewayRateLimitFilterByIp  : IP:127.0.0.1 ,令牌桶可用的令牌数量:10
c.s.s.filter.GatewayRateLimitFilterByIp : IP:127.0.0.1 ,令牌桶可用的令牌数量:9
c.s.s.filter.GatewayRateLimitFilterByIp : IP:127.0.0.1 ,令牌桶可用的令牌数量:8
c.s.s.filter.GatewayRateLimitFilterByIp : IP:127.0.0.1 ,令牌桶可用的令牌数量:7
c.s.s.filter.GatewayRateLimitFilterByIp : IP:127.0.0.1 ,令牌桶可用的令牌数量:7
c.s.s.filter.GatewayRateLimitFilterByIp : IP:127.0.0.1 ,令牌桶可用的令牌数量:6
c.s.s.filter.GatewayRateLimitFilterByIp : IP:127.0.0.1 ,令牌桶可用的令牌数量:5
c.s.s.filter.GatewayRateLimitFilterByIp : IP:127.0.0.1 ,令牌桶可用的令牌数量:4
c.s.s.filter.GatewayRateLimitFilterByIp : IP:127.0.0.1 ,令牌桶可用的令牌数量:3
c.s.s.filter.GatewayRateLimitFilterByIp : IP:127.0.0.1 ,令牌桶可用的令牌数量:2
c.s.s.filter.GatewayRateLimitFilterByIp : IP:127.0.0.1 ,令牌桶可用的令牌数量:2
c.s.s.filter.GatewayRateLimitFilterByIp : IP:127.0.0.1 ,令牌桶可用的令牌数量:1
c.s.s.filter.GatewayRateLimitFilterByIp : IP:127.0.0.1 ,令牌桶可用的令牌数量:0
c.s.s.filter.GatewayRateLimitFilterByIp : IP:127.0.0.1 ,令牌桶可用的令牌数量:0
基于 CPU 的使用率进行限流

在实际项目应用中对网关进行限流时,需要参考的因素比较多,可能会根据网络请求连接数、请求流量、CPU 使用率、内存使用率等进行流控。可以通过 Spring Boot Actuator 提供的 Metrics 获取当前 CPU 的使用情况,当 CPU 使用率高于某个阈值就开启限流,否则不开启限流。值得一提的是,在 Actuator 1.x 里可以通过 SystemPublicMetrics 来获取 CPU 的使用情况,但是在 Actuator 2.x 里只能通过 MetricsEndpoint 来获取。由于篇幅有限,下面只给出 Gateway Server 工程的核心代码和配置。

  1. 添加 Maven 依赖
1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. 编写自定义过滤器对特定资源进行限流
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
/**
* 根据CPU的使用率限流
**/
@Component
public class GatewayRateLimitFilterByCpu implements GatewayFilter, Ordered {

@Autowired
private MetricsEndpoint metricsEndpoint;

private static final double MAX_USAGE = 0.50D;

private static final String METRIC_NAME = "system.cpu.usage";

private final Logger log = LoggerFactory.getLogger(GatewayRateLimitFilterByCpu.class);

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取网关服务所在机器的CPU使用情况
Double systemCpuUsage = metricsEndpoint.metric(METRIC_NAME, null)
.getMeasurements()
.stream()
.filter(Objects::nonNull)
.findFirst()
.map(MetricsEndpoint.Sample::getValue)
.filter(Double::isFinite)
.orElse(0.0D);

boolean isOpenRateLimit = systemCpuUsage > MAX_USAGE;
log.info("system.cpu.usage: {}, isOpenRateLimit:{} ", systemCpuUsage, isOpenRateLimit);

if (isOpenRateLimit) {
//当CPU的使用超过设置的最大阀值时,则开启限流
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
} else {
return chain.filter(exchange);
}
}

@Override
public int getOrder() {
return 0;
}
}
  1. 通过 Java 流式 API 的方式配置路由规则,其中 http://127.0.0.1:9091/sayHello/peter/ 对应的是后端的服务,这里不再累述
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class CommonConfiguration {

@Autowired
private GatewayRateLimitFilterByCpu gatewayRateLimitFilterByCpu;

@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/rateLimit")
.filters(f -> f.filter(gatewayRateLimitFilterByCpu))
.uri("http://127.0.0.1:9091/sayHello/peter/")
.id("rateLimit_route")
).build();
}
}
  1. 编写 application.yml 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 9093

spring:
application:
name: gateway-server

management:
endpoints:
web:
exposure:
include: '*'
security:
enabled: false
  1. 测试结果

i. Linux 系统下执行压测命令 sysbench cpu --cpu-max-prime=20000 --threads=8 --time=60 run 来模拟 CPU 高负载,其中 --threads 是指 CPU 核数,--time 是指运行时间(秒)
ii. 访问 http://localhost:9093/actuator/metrics/system.cpu.usage,查看网关服务所在机器的 CPU 使用情况
iii. 启动各个应用后,多次访问 http://127.0.0.1:9090/rateLimit,当 CPU 使用率超过 50% 后,Spring Cloud Gateway 中自定义的限流过滤器开始拒绝处理请求,直接返回 429 状态码(因为请求太多,限流返回 429 状态码),控制台输出的日志信息如下:

1
2
3
4
5
c.s.s.f.GatewayRateLimitFilterByCpu      : system.cpu.usage: 0.846045400926432, isOpenRateLimit:true
c.s.s.f.GatewayRateLimitFilterByCpu : system.cpu.usage: 0.8458261370178468, isOpenRateLimit:true
c.s.s.f.GatewayRateLimitFilterByCpu : system.cpu.usage: 0.844951044863364, isOpenRateLimit:true
c.s.s.f.GatewayRateLimitFilterByCpu : system.cpu.usage: 0.8547458051590282, isOpenRateLimit:true
c.s.s.f.GatewayRateLimitFilterByCpu : system.cpu.usage: 0.8486913849509269, isOpenRateLimit:true
Gateway 内置的限流过滤器工厂

Spring Cloud Gateway 内置了一个名为 RequestRateLimiterGatewayFilterFactory 的过滤器工厂,可以直接用来限流;其底层的实现依赖于 Redis,使用的算法是令牌桶算法。由于篇幅有限,下面只给出 Gateway Server 工程的核心代码和配置。

  1. 添加 Maven 依赖
1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
  1. 编写 RemoteAddrKeyResolver 类
1
2
3
4
5
6
7
8
9
public class RemoteAddrKeyResolver implements KeyResolver {

public static final String BEAN_NAME = "remoteAddrKeyResolver";

@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
  1. 编写 CommonConfiguration 类
1
2
3
4
5
6
7
8
@Configuration
public class CommonConfiguration {

@Bean(RemoteAddrKeyResolver.BEAN_NAME)
public RemoteAddrKeyResolver remoteAddrKeyResolver() {
return new RemoteAddrKeyResolver();
}
}
  1. 编写 application.yml 配置文件,添加 Gateway 限流相关的配置内容
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
server:
port: 9092

spring:
application:
name: gateway-server
redis:
host: 172.175.0.3
port: 6379
cloud:
gateway:
routes:
- id: rateLimit_route
uri: http://127.0.0.1:9091/sayHello/peter/
order: 0
predicates:
- Path=/rateLimit
filters:
#Filter名称必须是RequestRateLimiter
- name: RequestRateLimiter
args:
#使用SpEL按名称引用bean
key-resolver: "#{@remoteAddrKeyResolver}"
#允许用户每秒处理多少个请求
redis-rate-limiter.replenishRate: 1
#令牌桶的容量,允许在一秒钟内完成的最大请求数
redis-rate-limiter.burstCapacity: 5
  1. 测试结果

启动各个应用后,多次访问 http://127.0.0.1:9092/rateLimit,可以发现当请求太过频繁的时候,Spring Cloud Gateway 会直接返回 429 状态码。

基于 Sentinel 实现限流熔断降级

Spring Cloud Gateway 的动态路由

网关中有两个重要的概念,那就是路由配置和路由规则。路由配置是指配置某请求路径路由到指定的目的地址,而路由规则是指匹配到路由配置之后,再根据路由规则进行转发处理。 Spring Cloud Gateway 作为所有请求流量的入口,在实际生产环境中为了保证高可靠和高可用,以及尽量避免重启,需要实现 Spring Cloud Gateway 动态路由配置。Spring Cloud Gateway 提供了两种方法来配置路由规则(Java 流式 API、YML 配置文件),但都是在 Spring Cloud Gateway 启动时将路由配置和规则加载到内存里,无法做到不重启网关应用就可以动态地对路由的配置和规则进行增加、修改和删除操作。Spring Cloud Gateway 的官方文档并没有讲如何进行动态配置,査看 Spring Cloud Gateway 的源码,发现在 org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint 类中提供了动态配置的 Rest 接口,但是需要开启 Gateway 的端点,而且其提供的功能不是很强大。通过参考与 GatewayControllerEndpoint 相关的代码,可以自己编码实现动态路由配置。

基于 Rest API 的动态路由实现(内存版)

下面将介绍 Gateway 基于 Rest API 的动态路由实现,为了方便演示,下述示例的路由配置信息默认存储在内存;若需要持久化路由配置信息(如 MySQL 持久化),可以扩展实现 RouteDefinitionRepository 接口,点击下载完整的案例代码。

  1. 添加 Maven 依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
  1. 定义数据传输模型,分别编写 GatewayRouteDefinition、GatewayPredicateDefinition、GatewayFilterDefinition 类
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
/**
* Gateway的路由定义模型
*/
public class GatewayRouteDefinition {

/**
* 路由的Id
*/
private String id;

/**
* 路由断言集合配置
*/
private List<GatewayPredicateDefinition> predicates = new ArrayList<>();

/**
* 路由过滤器集合配置
*/
private List<GatewayFilterDefinition> filters = new ArrayList<>();

/**
* 路由规则转发的目标uri
*/
private String uri;

/**
* 路由执行的顺序
*/
private int order = 0;

// 省略Get和Set方法 ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 路由断言定义模型
*/
public class GatewayPredicateDefinition {

/**
* 断言对应的Name
*/
private String name;

/**
* 配置的断言规则
*/
private Map<String, String> args = new LinkedHashMap<>();

// 省略Get和Set方法 ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 过滤器定义模型
*/
public class GatewayFilterDefinition {

/**
* Filter Name
*/
private String name;

/**
* 对应的路由规则
*/
private Map<String, String> args = new LinkedHashMap<>();

// 省略Get和Set方法 ...
}
  1. 编写动态路由的实现类 DynamicRouteServicelmpl,需要实现 ApplicationEventPublisherAware 接口
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

/**
* 动态路由实现类
*/
@Service
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {

private ApplicationEventPublisher publisher;

@Autowired
private RouteDefinitionWriter routeDefinitionWriter;

private static final Logger logger = LoggerFactory.getLogger(DynamicRouteServiceImpl.class);

@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}

private void notifyChanged() {
this.publisher.publishEvent(new RefreshRoutesEvent(this));
}

/**
* 增加路由
*
* @param definition
* @return
*/
public boolean add(RouteDefinition definition) {
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
notifyChanged();
} catch (Exception e) {
logger.error("add route fail: " + e.getMessage());
return false;
}
return true;
}


/**
* 更新路由
*
* @param definition
* @return
*/
public boolean update(RouteDefinition definition) {
try {
// 特别注意,这里一定不能执行subscribe()方法,否则更新逻辑存在Bug
this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
} catch (Exception e) {
logger.error("update route fail: " + e.getMessage());
return false;
}
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
notifyChanged();
return true;
} catch (Exception e) {
logger.error("update route fail: " + e.getMessage());
return false;
}
}

/**
* 删除路由
*
* @param id
* @return
*/
public boolean delete(String id) {
try {
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
notifyChanged();
return true;
} catch (Exception e) {
logger.error("delete route fail: " + e.getMessage());
return false;
}
}

/**
* 装配路由配置信息
*
* @param gwdefinition
* @return
*/
public RouteDefinition assembleRouteDefinition(GatewayRouteDefinition gwdefinition) {
RouteDefinition definition = new RouteDefinition();

// ID
definition.setId(gwdefinition.getId());

// Predicates
List<PredicateDefinition> pdList = new ArrayList<>();
for (GatewayPredicateDefinition gpDefinition : gwdefinition.getPredicates()) {
PredicateDefinition predicate = new PredicateDefinition();
predicate.setArgs(gpDefinition.getArgs());
predicate.setName(gpDefinition.getName());
pdList.add(predicate);
}
definition.setPredicates(pdList);

// Filters
List<FilterDefinition> fdList = new ArrayList<>();
for (GatewayFilterDefinition gfDefinition : gwdefinition.getFilters()) {
FilterDefinition filter = new FilterDefinition();
filter.setArgs(gfDefinition.getArgs());
filter.setName(gfDefinition.getName());
fdList.add(filter);
}
definition.setFilters(fdList);

// URI
URI uri = UriComponentsBuilder.fromUriString(gwdefinition.getUri()).build().toUri();
definition.setUri(uri);

return definition;
}
}
  1. 编写 Rest 控制器,对外暴露 Rest API
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
@RestController
@RequestMapping("/route")
public class RouteController {

@Autowired
private DynamicRouteServiceImpl dynamicRouteService;

/**
* 增加路由
*
* @param gwdefinition
* @return
*/
@PostMapping("/add")
public String add(@RequestBody GatewayRouteDefinition gwdefinition) {
RouteDefinition definition = dynamicRouteService.assembleRouteDefinition(gwdefinition);
return this.dynamicRouteService.add(definition) ? "success" : "fail";
}

/**
* 删除路由
*
* @param id
* @return
*/
@GetMapping("/delete/{id}")
public String delete(@PathVariable String id) {
return this.dynamicRouteService.delete(id) ? "success" : "fail";
}

/**
* 更新路由
*
* @param gwdefinition
* @return
*/
@PostMapping("/update")
public String update(@RequestBody GatewayRouteDefinition gwdefinition) {
RouteDefinition definition = dynamicRouteService.assembleRouteDefinition(gwdefinition);
return this.dynamicRouteService.update(definition) ? "success" : "fail";
}
}
  1. 编写应用的启动主类
1
2
3
4
5
6
7
@SpringBootApplication
public class GatewayServerApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class, args);
}
}
  1. 编写 application.yml 配置文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 9090

spring:
application:
name: gateway-server

management:
endpoints:
web:
exposure:
include: '*'
security:
enabled: false
  1. 测试结果

i. 启动 gateway 应用
ii. 访问 http://127.0.0.1:9090/actuator/gateway/routes,此时返回的路由信息应该为空 []
iii. 通过 Postman 访问 http://127.0.0.1:9090/route/add,发起 Post 请求添加路由配置信息,其中需要提交的 JSON 数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"filters": [],
"id": "jd_route",
"order": 0,
"predicates": [
{
"args": {
"pattern": "/jd"
},
"name": "Path"
}
],
"uri": "http://www.jd.com"
}

iiii. 再次访问 http://127.0.0.1:9090/actuator/gateway/routes,此时应该可以返回上面添加的路由配置信息
iiiii. 访问 http://127.0.0.1:9090/jd,发现可以正常跳转到京东商城的首页,说明上面添加的路由配置生效了
iiiiii. 通过 Postman 访问 http://127.0.0.1:9090/route/update,发起 Post 请求更改路由配置信息,其中需要提交的 JSON 数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"filters": [],
"id": "jd_route",
"order": 0,
"predicates": [
{
"args": {
"pattern": "/jd"
},
"name": "Path"
}
],
"uri": "http://www.taobao.com"
}

iiiiiii. 访问 http://127.0.0.1:9090/actuator/gateway/routes,可以发现返回的路由配置信息已经被修改了
iiiiiiii. 访问 http://127.0.0.1:9090/jd,发现可以成功跳转到淘宝网
iiiiiiiii. 通过 Postman 访问 http://127.0.0.1:9090/route/delete/jd_route,发起 Get 请求删除路由配置信息

  1. 最后附上 JSON 版的完整路由配置示例
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
{
"filters": [
{
"args": {
"name": "hystrix",
"fallbackUri": "forward:/fallback"
},
"name": "Hystrix"
},
{
"args": {},
"name": "RateLimit"
}
],
"id": "jd_route",
"order": 0,
"predicates": [
{
"args": {
"pattern": "/jd"
},
"name": "Path"
}
],
"uri": "http://www.jd.com"
}
Gateway 集群下的动态路由实现

上面的示例简单地实现了单机 Gateway 的动态路由,单机 Gateway 中的路由配置信息保存在当前实例的内存中,实例重启后会丢失路由配置信息,同时无法做到整个 Gateway 集群的动态路由控制。通过分析 Spring Cloud Gateway 源码可以发现,默认的 RouteDefinitionWriter 实现类是 InMemoryRouteDefinitionRepository。而 RouteDefinitionRepository 继承了 RouteDefinitionWriter,是 Spring Cloud Gateway 官方预留的接口,因此可以通过下面两种方式来实现集群下的动态路由控制:RouteDefinitionWriter 接口和 RouteDefinitionRepository 接口。在这里推荐实现 RouteDefinitionRepository 这个接口,从数据库或者从配置中心获取路由进行动态配置;具体可以参考上面单机版的动态路由实现,在这里不再累述。

参考资料