网关 Spring Cloud Gateway 科普

网关 Spring Cloud Gateway 科普

Spring Cloud Gateway是在Spring生态系统之上构建的API网关服务,它旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能, 例如:熔断、限流、重试等。

Spring Cloud Gateway 具有如下特性:

  • 基于Spring Framework 5、Project Reactor 和 Spring Boot 2.0 进行构建;
  • 动态路由:能够匹配任何请求属性;
  • 可以对路由指定 Predicate(断言)和 Filter(过滤器);
  • 集成Hystrix的断路器功能;
  • 集成 Spring Cloud 服务发现功能;
  • 易于编写的 Predicate(断言)和 Filter(过滤器);
  • 请求限流功能;
  • 支持路径重写。

Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的 Zuul 2.0之前的非Reactor模式的老版本。(有一个版本的说法是 Zuul2.x 的连续跳票和 Zuul1.x 的性能并不是很理想,从而催生了Spring Cloud Gateway。)而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。

网上很多地方都说 Zuul 是阻塞的,Spring Cloud Gateway 是非阻塞的,这么说并不严谨的,准确的讲 Zuul1.x 是阻塞的,而在2.x的版本中,Zuul也是基于Netty,也是非阻塞的,如果一定要说性能,其实这个真没多大差距。Zuul的有关信息可以参考《网关Zuul科普》。

Spring Cloud Gateway有以下几个重要的概念,后面的内容也主要围绕这几个概念展开。

  • Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由;
  • Predicate(断言):指的是Java 8 的 Function Predicate。 输入类型是Spring框架中的ServerWebExchange。 这使开发人员可以匹配HTTP请求中的所有内容,例如请求头或请求参数。如果请求与断言相匹配,则进行路由;
  • Filter(过滤器):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前后对请求进行修改。

快速入门

定义2个服务:hello-server和user-server,他们分别都注册到euraka服务上,示例如下:

在未经过网关时,我们可以通过以下2个接口来分别访问hello-server和user-server:

http://localhost:8081/hello
http://localhost:8082/user

现在我们来定义一个Spring Cloud Gateway服务,相关的Maven依赖如下:

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

application.yml文件中添加如下配置:

server:
port: 9091
spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
routes:
- id: path_route_1
uri: http://localhost:8082/user
predicates:
- Path=/user

各字段含义如下:

  • id: 我们自定义的路由ID,唯一。
  • uri:目标服务地址
  • predicates:路由条件,Predicate接受一个输入参数,返回一个布尔值结果。该接口中包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与、或、非)

这段配置的意思是:配置了一个id为 path_route_1的URI代理规则,当访问地址 http://localhost:9091/user时,会路由到地址http://localhost:8082/user

Spring Cloud Gateway的路由配置和Zuul的路由配置有较大的区别,玩过Zuul的注意一下两者的区分。

启动类如下,平平无奇:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

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

现在我们启动Spring Cloud Gateway服务,然后请求http://localhost:9091/user:

hidden:~ hidden$ curl -i localhost:9091/user
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 11
Date: Thu, 02 Apr 2020 09:28:58 GMT

User Info!

由于没有配置过hello-server的服务,所以还不能通过网关访问它:

hidden:~ hidden$ curl -i localhost:9091/hello
HTTP/1.1 404 Not Found
Content-Type: application/json
Content-Length: 133

{"timestamp":"2020-04-02T09:30:09.840+0000","path":"/hello","status":404,"error":"Not Found","message":null,"requestId":"8b0a00e1-4"}

除了使用yml配置文件的方式,还可以通过代码(Java Bean)来实现路由配置。我们在启动类中添加方法 customRouteLocator() 来定制转发规则,详情如下:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class DemoApplication {

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

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder){
return builder.routes()
.route("path_route_2", r -> r.path("/hello")
.uri("http://localhost:8081/hello"))
.build();
}
}

重启服务,再次访问hello-server的接口,可以看到访问成功:

hidden:~ hidden$ curl -i localhost:9091/hello
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 19
Date: Thu, 02 Apr 2020 09:53:25 GMT

Hello!

路由规则

Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。 Spring Cloud Gateway 包括许多内置的 Predicate 工厂,这些 Predicate 工厂通过不同的 HTTP 请求参数来匹配,多个 Predicate 工厂可以组合使用。

Predicate 来源于 Java 8,是 Java 8 中引入的一个函数,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。

在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。网上有一张图总结了 Spring Cloud 内置的几种 Predicate 的实现。

说白了 Predicate 就是为了实现一组匹配规则,方便让请求过来找到对应的 Route 进行处理,接下来我们接下 Spring Cloud GateWay 内置几种 Predicate 的使用。

带有指定Cookie的请求会匹配改路由。将application.yml中的spring.cloud.gateway.routes配置修改为如下内容:

spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
routes:
- id: cookie_route
uri: http://localhost:8082/user
predicates:
- Cookie=username,pipisi

此时使用curl工具发送带有cookie为 username=pipisi 的请求就可以匹配改路由:

curl -i localhost:9091/user --cookie "username=pipisi"

Query

带指定查询参数的请求可以匹配该路由。将application.yml中的spring.cloud.gateway.routes配置修改为如下内容:

spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
routes:
- id: cookie_route
uri: http://localhost:8082/user
predicates:
- Query=username

此时使用curl工具发送带有username=pipisi 查询参数的请求就可以匹配改路由:

curl -i localhost:9091/user?username=pipisi

其余的还有(参考上图):通过Header匹配(将predicates中的 -Query 换成 -Header,以下类同)、通过Host匹配(-Host)、通过请求方法匹配(-Method)、通过IP地址匹配(-RemoteAddr)、通过请求时间(-After、-Before、-Between)、通过权重(-Weight)以及最开始在入门示例中就提及的通过请求路径匹配(-Path)。还可以进行组合使用,比如:

predicates:
- Query=username
- Cookie=username,pipisi
- Path=/user

各种 Predicates 同时存在于同一个路由时,请求必须同时满足所有的条件才被这个路由匹配。

过滤器

网关经常需要对路由请求进行过滤,执行一些操作,如鉴权之后构造头部之类的。过滤的种类很多,如增加请求头、增加请求参数 、增加响应头以及断路器等等,这就用到了Spring Cloud Gateway 的 过滤器(Filter)。

当我们有很多服务时,比如前面所提及的user-server和hello-server,客户端请求各个服务的API接口时,每个服务都要做相同的事情,比如鉴权、限流、日志等,如下图(上半部)。

对于这样重复的工作,可以在微服务的上一层加一个全局的权限控制、限流、日志的API网关服务,然后将请求转发到具体的业务服务层。这个API网关服务就是起到一个服务边界的作用,外界的请求访问系统,必须先通过网关层,如上图(下半部)。

Spring Cloud Gateway同 Zuul 类似,也有 prepost 两种方式的过滤器:pre 过滤器在请求被路由之前调用,我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等;post 过滤器在路由到微服务以后执行,这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。

Spring Cloud Gateway的 Filter生命周期如下图所示。

Zuul 不同的是,Filter除了分为prepost 两种方式的 Filter 外,在Spring Cloud Gateway中,Filter 根据作用范围可分为另外两种,一种是针对于单个路由的 GatewayFilter,它在配置文件中的写法同 Predicate类似;另外一种是针对于所有路由的 GlobalFilter。

过滤器允许以某种方式修改传入的HTTP Request 或传出的HTTP Response,它可以限定作用在某些特定请求路径上。 Spring Cloud Gateway包含了30多个内置的GatewayFilter工厂,比如AddRequestHeaderGatewayFilterFactory(添加请求头的过滤器工厂)、AddRequestParameterGatewayFilterFactory(添加请求参数的过滤器工厂)、AddResponseHeaderGatewayFilterFactory(添加响应头的过滤器工厂)、HystrixGatewayFilterFactory(Hystrix熔断过滤器工厂)、RequestRateLimiterGatewayFilterFactory(请求限流的过滤器工厂)等(全部的GatewayFilter工厂可以查看jar包中org.springframework.cloud.gateway.filter.factory目录,除了这些,你也可以自定义自己的过滤器)。

GatewayFilter工厂与前面介绍的Predicate工厂类似,都是在配置文件 application.yml 中配置,遵循了约定大于配置的思想,只需要在配置文件配置GatewayFilter Factory的名称,而不需要写全部的类名,比如 AddRequestHeaderGatewayFilterFactory 只需要在配置文件中写AddRequestHeader,而不是全部类名。官方文档都给出了这些过滤器工厂详细的使用案例,下面挑选1个案例以作演示。

案例:AddRequestHeader

application.yml配置如下:

server:
port: 9091
spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
routes:
- id: cookie_route
uri: http://localhost:8082/user
filters:
- AddRequestHeader=X-Request-Foo, Bar
predicates:
- Path=/user

过滤器工厂会在匹配的请求头加上一对请求头“x-request-foo=Bar”。可以在下游的user-server服务中使用@RequestHeader来查看所接收请求的请求头信息。

结合注册中心Eureka使用

Zuul 作为网关结合注册中心进行使用时,默认情况下 Zuul 会根据注册中心注册的服务列表,以服务名为路径创建动态路由,Spring Cloud Gateway同样也实现了该功能。下面我们演示下 Spring Cloud Gateway结合注册中心如何使用默认的动态路由。

首先添加Maven依赖:

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

其次修改application.yml配置文件:

server:
port: 9091
spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/

重启服务之后,我们就可以像 Zuul 那样采用下面的方式访问接口了:

http://localhost:9091/hello-server/hello
http://localhost:9091/user-server/user

工作原理

Spring Cloud Gateway 的核心处理流程如上图,核心逻辑就是路由转发,执行过滤器链。Gateway的客户端回向Spring Cloud Gateway发起请求,然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。在Filter链中,通过虚线分割Filter的原因是:过滤器可以在转发请求之前处理或者接收到被代理服务的返回结果之后处理。所有的Pre类型的Filter执行完毕之后,才会转发请求到被代理的服务处理。被代理的服务把所有请求完毕之后,才会执行Post类型的过滤器。

References

  1. https://www.cnblogs.com/crazymakercircle/p/11704077.html
  2. http://www.imooc.com/article/297943
  3. https://www.cnblogs.com/bjlhx/p/9785926.html
  4. https://www.jianshu.com/p/17bbc8e10545

欢迎支持笔者的作品《深入理解Kafka: 核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客(ID: hiddenkafka)。
本文作者: 朱小厮

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×