深入探索Spring Cloud Gateway:微服务网关的最佳实践

优质博文:IT-BLOG-CN

Spring Cloud Gateway作为Spring Cloud框架的第二代网关,在功能上要比Zuul更加的强大,性能也更好。随着Spring Cloud的版本迭代,Spring Cloud官方有打算弃用Zuul的意思。在笔者调用了Spring Cloud Gateway的使用和功能上,Spring Cloud Gateway替换掉Zuul的成本上是非常低的,几乎可以无缝切换。Spring Cloud Gateway几乎包含了Zuul的所有功能。

一、网关定义

API网关是一个反向路由 ,屏蔽内部细节,为调用者提供统一入口,接收所有调用者请求,通过路由机制转发到服务实例。API网关是一组"过滤器Filter"集合 ,可以实现一系列与核心业务无关的横切面功能,如安全认证、限流熔断、日志监控

网关在系统中所处的位置:

二、快速开始

网关启动步骤(代码演示):

【1】添加依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

【2】配置文件

yml 复制代码
spring:
  cloud:
    gateway:
      discovery: # 启用通过服务发现自动创建路由。
        locator:
          enabled: true
      routes:
        - id: example_route   #路由ID,根据业务自行定义
          uri: lb://example-service  #目标服务的地址,这里使用 lb:// 前缀来表示负载均衡。可以是 HTTP(s) URI 或其他协议的 URI - http://bin.org:80/get。
          predicates:
            - Path=/example/** #predicates: 谓词数组,用于匹配请求。常见的谓词包括 Path、Method、Header 等。
          filters:   #过滤器数组,用于在请求被转发到目标服务之前和之后进行处理。常见的过滤器包括 AddRequestHeader、StripPrefix、RewritePath 等。
            - AddRequestHeader=X-Example, ExampleValue #过滤器会在请求头中添加 X-Example,值为 ExampleValue。
            - StripPrefix=1 #过滤器会移除路径中的第一个前缀。例如,请求路径 /example/test 会变成 /test。
        - id: rate_limited_route
          uri: http://ratelimited.org
          predicates:
            - Path=/ratelimited/**
          filters:
            - RequestRateLimiter=redis-rate-limiter # 限流:通过 RequestRateLimiter 过滤器实现
        - id: retry_route
          uri: http://retry.org
          predicates:
            - Path=/retry/**
          filters:
            - Retry=5 #重试:通过 Retry 过滤器实现。
      default-filters: #default-filters 是全局过滤器数组,适用于所有路由。这个过滤器会在所有响应中添加 X-Response-Default 头,值为 Default。
        - AddResponseHeader=X-Response-Default, Default
      globalcors: #globalcors 用于配置全局的 CORS(跨域资源共享)设置。
        corsConfigurations: #corsConfigurations: 定义 CORS 配置的路径模式。
          '[/**]': #匹配所有路径。
            allowedOrigins: "*" #允许的源,* 表示允许所有源。
            allowedMethods: #允许的 HTTP 方法,包括 GET、POST、DELETE 和 PUT。
              - GET
              - POST
              - DELETE
              - PUT

三、Spring Cloud GateWay 架构图

客户端向Spring Cloud Gateway发出请求。 在Gateway Handler Mapping中找到请求相对匹配路由(这个时候就用到predicate),则将其发送到Gateway web handler处理。 handler处理请求时会经过一系列的过滤器链。 过滤器链被虚线划分的原因是过滤器链可以在发送代理请求之前或之后执行过滤逻辑。 先执行所有pre过滤器逻辑,然后进行代理请求。 在发出代理请求之后,收到代理服务的响应之后执行post过滤器逻辑。这跟Zuul的处理过程很类似。在执行所有pre过滤器逻辑时,往往进行了鉴权、限流、日志输出等功能,以及请求头的更改、协议的转换;转发之后收到响应之后,会执行所有post过滤器的逻辑,在这里可以响应数据进行了修改,比如响应头、协议的转换等。在上面的处理过程中,有一个重要的点就是将请求和路由进行匹配,这时候就需要用到predicate,它是决定了一个请求走哪一个路由。

四、SpringColoud GateWay 核心组件

集合上面的配置和架构图进行说明

【1】Route路由: Gateway的基本构建模块,它由ID、目标URL、断言集合和过滤器集合组成。如果聚合断言结果为真,则匹配到该路由。

Route路由-动态路由实现原理: 配置变化Apollo + 服务地址实例变化NacosSpring Cloud Gateway通过RouteDefinitionLocatorRouteRefreshListener等组件实现动态路由。

先看下配置信息,方便后面原理的理解:SpringCloudGateway bootstrap.yml的配置如下:

yml 复制代码
spring:
  application:
    name: gateway-service
  cloud:
    nacos:
      discovery:
        server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
    apollo:
      bootstrap:
        enabled: true
      meta: ${APOLLO_META:localhost:8080}

application.yml的配置如下:

yml 复制代码
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
apollo:
  bootstrap:
    namespaces: application # 1、登录 Apollo 控制台。 2、创建一个新的配置,例如 application.yml。 3、内容就是上看配置的SpringCloud Gateway 配置的路由信息

1、RouteDefinitionLocatorSpring Cloud Gateway启动时,会通过RouteDefinitionLocatorApollo加载初始的路由定义。

2、DiscoveryClientRouteDefinitionLocator:使用Nacos进行服务发现,从Nacos获取动态路由定义。

3、RouteDefinitionRepository:加载的路由定义会存储在RouteDefinitionRepository中,供后续路由匹配使用。

4、RouteRefreshListener:监听路由定义的变化事件(如配置更新、服务实例变化等)。当监听到路由定义变化事件时,触发路由刷新操作,更新网关的路由规则,重新加载并应用新的路由配置。

GatewayHandlerMapping根据预先配置的路由信息和请求的属性(如路径、方法、头部信息等)来确定哪个路由与请求匹配。它使用谓词Predicates来进行匹配判断。

【2】Predicate断言: 这是一个Java 8 Function Predicate。输入类型是Spring Framework ServerWebExchange。允许开发人员匹配来自HTTP请求的任何内容,例如Header或参数。Predicate接受一个输入参数,返回一个布尔值结果。Spring Cloud Gateway内置了许多Predict,这些Predict的源码在org.springframework.cloud.gateway.handler.predicate包中,如果读者有兴趣可以阅读一下。现在列举各种 Predicate如下图:

在上图中,有很多类型的Predicate,比如说时间类型的 Predicated[AfterRoutePredicateFactory BeforeRoutePredicateFactory BetweenRoutePredicateFactory],当只有满足特定时间要求的请求会进入到此Predicate中,并交由Router处理;Cookie类型的CookieRoutePredicateFactory,指定的Cookie满足正则匹配,才会进入此Router。以及hostmethodpathquerparamremoteaddr类型的Predicate,每一种Predicate都会对当前的客户端请求进行判断,是否满足当前的要求,如果满足则交给当前请求处理。如果有很多个Predicate,并且一个请求满足多个Predicate,则按照配置的顺序第一个生效。

Predicate 断言配置:

yml 复制代码
server:
  port: 8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: gateway-service
          uri: https://www.baidu.com
          order: 0
          predicates:
            - After=2017-01-20T17:42:47.789-07:00[America/Denver]
            - Host=**.foo.org
            - Path=/headers
            - Method=GET
            - Header=X-Request-Id, \d+
            - Query=foo, ba.
            - Query=baz
            - Cookie=chocolate, ch.p

在上面的配置文件中,配置了服务的端口为8080,配置spring cloud gateway相关的配置,id标签配置的是routerid ,每个router都需要一个唯一的iduri配置的是将请求路由到哪里 ,本案例全部路由到https://www.baidu.com

Predicates After=2017-01-20T17:42:47.789-07:00[America/Denver] 会被解析成PredicateDefinition对象name =After ,args= 2017-01-20T17:42:47.789-07:00[America/Denver]。需要注意的是PredicatesAfter这个配置,遵循契约大于配置的思想,它实际被 AfterRoutePredicateFactory这个类所处理,这个After就是指定了它的Gateway web handler类为AfterRoutePredicateFactory,同理,其他类型的Predicate也遵循这个规则。当请求的时间在这个配置的时间之后,请求会被路由到指定的URL。跟时间相关的Predicates还有 Before Route Predicate FactoryBetween Route Predicate Factory,读者可以自行查阅官方文档,再次不再演示。

Query=baz Query的值以键值对的方式进行配置,这样在请求过来时会对属性值和正则进行匹配,匹配上才会走路由。经过测试发现只要请求汇总带有baz参数即会匹配路由[localhost:8080?baz=x&id=2],不带baz参数则不会匹配。

Query=foo, ba.:这样只要当请求中包含foo属性并且参数值是以 ba开头的长度为三位的字符串才会进行匹配和路由。使用curl测试,命令行输入:curl localhost:8080?foo=bab测试可以返回页面代码,将foo的属性值改为babx再次访问就会报404,证明路由需要匹配正则表达式才会进行路由。

Header=X-Request-Id, \d+:使用curl测试,命令行输入:curl http://localhost:8080 -H "X-Request-Id:88" 则返回页面代码证明匹配成功。将参数-H "X-Request-Id:88"改为-H "X-Request-Id:spring"再次执行时返回404证明没有匹配。

【3】Filter过滤器:方案一:写死在代码中

java 复制代码
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        //openapi路由转发
        .route("openapi_route", p -> p.path( "/openapi/**").filters(f->f.removeRequestHeader("Expect"))
        .uri("lb://order-openapi-service"))
        .build();
}

方案二:配置文件yml

yml 复制代码
# gateway 的配置形式
 routes:
  - id: order-service #路由ID,没有规定规则但要求唯一,建议配合服务名。
    uri: lb://order-service
    predicates:
      - Path=/order/**
    filters:
      - ValidateCodeGatewayFilter

Filter过滤器:Filter按处理顺序Pre Filter / Post Filter

Filter按作用范围分为: GlobalFilter全局过滤器。GatewayFilter 指定路由的过滤器。
Filter过滤器-扩展自定义Filter Filter支持通过spi扩展。实现GatewayFilterOrdered接口。
Filter方法: 过滤器处理逻辑。getOrder:定义优先级,值越大优先级越低。

全局过滤器示例: 创建一个全局过滤器类,实现GlobalFilter接口:

java 复制代码
public class TokenFilter implements GlobalFilter, Ordered {

    Logger logger=LoggerFactory.getLogger( TokenFilter.class );
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        if (token == null || token.isEmpty()) {
            logger.info( "token is empty..." );
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
      // // 过滤器的执行顺序,值越小优先级越高
        return -100;
    }
}

自定义路由过滤器示例: 创建自定义的路由过滤器,可以实现GatewayFilter接口:

java 复制代码
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

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

    public MyCustomFilter() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            // 前置过滤逻辑
            System.out.println("Custom Pre Filter executed");
            
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                // 后置过滤逻辑
                System.out.println("Custom Post Filter executed");
            }));
        };
    }

    public static class Config {
        // 配置属性
    }
}

在配置文件中使用自定义过滤器:

yml 复制代码
spring:
  cloud:
    gateway:
      routes:
      - id: my_route
        uri: http://httpbin.org:80
        predicates:
        - Path=/get
        filters:
        - name: MyCustomFilter

五、Gateway 限流

Spring Cloud Gateway中,有Filter过滤器,因此可以在"pre"类型的Filter中自行实现上述三种过滤器。但是限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,适用RedisLua脚本实现了令牌桶链接的方式。具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,Lua脚本在如下图所示的文件夹中:

以案例的形式来讲解如何在SpringCloud Gateway中使用内置的限流过滤器工厂来实现限流。首先在工程的pom文件中引入Gateway的起步依赖和RedisReactive依赖,代码如下:

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifatId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

在配置文件中做以下的配置:

yml 复制代码
server:
  port: 8081
spring:
  cloud:
    gateway:
      routes:
      - id: limit_route
        uri: lb://PRODUCTCENTOR   # PRODUCTCENTOR是注册到注册中心的服务名,格式为 lb://服务名
        predicates:
        - Path=/**
        filters:
        - StripPrefix=1
        - name: RequestRateLimiter  #拦截器,会对上述的请求进行拦击
          args:
            key-resolver: '#{@hostAddrKeyResolver}'
            redis-rate-limiter.replenishRate: 1
            redis-rate-limiter.burstCapacity: 3
  application:
    name: gateway-limiter
  redis:
    host: localhost
    port: 6379
    database: 0

过滤器StripPrefix,作用是去掉请求路径的最前面n个部分截取掉。StripPrefix=1就代表截取路径的个数为1,比如前端过来请求/test/good/1/view,匹配成功后,路由到后端的请求路径就会变成http://localhost:8888/good/1/view

在上面的配置文件,指定程序的端口为8081,配置了Redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:

【1】burstCapacity:令牌桶总容量。

【2】replenishRate:令牌桶每秒填充平均速率。

【3】key-resolver:用于限流的键的解析器的Bean对象的名字。它使用SpEL表达式根据#{@beanName}Spring容器中获取Bean对象。

KeyResolver需要实现resolve方法,比如根据Hostname进行限流,则需要用hostAddress去判断。实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。

java 复制代码
public class HostAddrKeyResolver implements KeyResolver {

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }

}

@Bean
public HostAddrKeyResolver hostAddrKeyResolver() {
    return new HostAddrKeyResolver();
}

可以根据URL去限流,这时KeyResolver代码如下:

java 复制代码
public class UriKeyResolver implements KeyResolver {

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getURI().getPath());
    }

}

@Bean
public UriKeyResolver uriKeyResolver() {
    return new UriKeyResolver();
}

也可以以用户的维度去限流:

java 复制代码
// 省略部分代码
@Bean
KeyResolver userKeyResolver() {
    return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}

jmeter进行压测,配置10 thread去循环请求lcoalhost:8081,循环间隔1s。从压测的结果上看到有部分请求通过,由部分请求失败。通过redis客户端去查看redis中存在的key。如下:

shell 复制代码
127.0.0.1:6379> keys *
1> "request_rate_limiter.<127.0.0.1>.timestamp"
2> "request_rate_limiter.<127.0.0.1>.tokens"

可见,RequestRateLimiter是使用Redis来进行限流的,并在redis中存储了2key。关注这两个key含义可以看lua源代码。

六、Zuul 与 Spring Cloud Gateway 比较

优点 缺点
Gateway 1、线程开销小2、使用轻量级 Netty 异步IO实现通信3、支持各种长连接,WebSocket4、Spring 官方推荐,重点支持,功能较 Zuul更丰富,支持限流监控等
Zuul 1、编码模型简单2、开发调试运维简单

ZuulGateway压测结果: 休眠时间模仿后端请求时间,线程数2000 ,请求时间360秒=6分钟。配置情况:Gateway默认配置,Zuul网关的Tomcat最大线程数为400hystrix超时时间为100000

休眠时间 测试样本,单位=个Zuul/Gateway 平均响应时间,单位=毫秒Zuul/Gateway 99%响应时间,单位=毫秒Zuul/Gateway 错误次数,单位=个Zuul/Gateway 错误比例Zuul/Gateway
休眠100ms 294134/1059321 2026/546 6136/1774 104/0 0.04%/0%
休眠300ms 101194/399909 5595/1489 15056/1690 1114/0 1.10%/0%
休眠600ms 51732/201262 11768/2975 27217/3203 2476/0 4.79%/0%
休眠1000ms 31896/120956 19359/4914 46259/5115 3598/0 11.28%/0%

测试结果:Gateway在高并发和后端服务响应慢的场景下比Zuul的表现要好

七、SpringCloud GateWay 与 Nginx 组合使用

因为和GateWay相关所以这里介绍一下

NginxSpring Cloud Gateway可以组合使用,以实现高效的负载均衡和网关功能。Nginx通常用于处理静态内容、SSL终止、负载均衡等,而Spring Cloud Gateway主要用于动态路由、过滤和服务网关功能。下面是一个基本的配置示例,展示了如何将NginxSpring Cloud Gateway结合使用。这里主要说下Nginx中的配置:

配置Nginx作为反向代理,将外部请求转发到Spring Cloud Gateway

nginx.conf配置示例

yml 复制代码
http {
    upstream gateway {  # 定义一个名为 gateway 的上游服务器组,包含 Spring Cloud Gateway 的地址(localhost:8080)。
        server localhost:8080;
    }

    server { # 配置 Nginx 服务器块。
        listen 80;  # 监听80端口。

        location / { #  匹配所有请求。
            proxy_pass http://gateway; # http://gateway: 将请求转发到上游服务器组 gateway。
            proxy_set_header Host $host; # 设置一些头信息,用于保持客户端信息。
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

工作流程:

【1】客户端请求:客户端发送请求到Nginx

【2】Nginx转发:Nginx接收到请求后,将其转发到Spring Cloud Gateway

【3】Spring Cloud Gateway路由:Spring Cloud Gateway根据配置文件中的路由规则,将请求转发到对应的微服务。

【4】微服务响应:微服务处理请求并返回响应,通过Spring Cloud GatewayNginx返回给客户端。

SpringCloud GateWay 与 Nacos 组合使用

bootstrap.yml用于配置Nacos的基本信息:

yml 复制代码
spring:
  application:
    name: gateway-service #设置网关服务的名称。
  cloud:
    nacos:
      discovery:
        server-addr: ${NACOS_SERVER_ADDR:localhost:8848} #Nacos 服务发现的地址。 ${NACOS_SERVER_ADDR}: 使用环境变量配置 Nacos 服务器地址,方便在不同环境中切换。
      config:
        server-addr: ${NACOS_SERVER_ADDR:localhost:8848} #Nacos 配置管理的地址。
        file-extension: yaml

可以将Spring Cloud Gateway的配置放在Nacos配置中心,这样可以实现配置的集中管理和动态更新。在Nacos中创建配置:

【1】登录Nacos控制台。

【2】创建一个新的配置,例如gateway-service.yaml

【3】在配置文件中添加路由规则,例如:

yml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: example-service
          uri: lb://example-service
          predicates:
            - Path=/example/**
        - id: another-service
          uri: lb://another-service
          predicates:
            - Path=/another/**
相关推荐
孔汤姆4 分钟前
部署实战(二)--修改jar中的文件并重新打包成jar文件
java·pycharm·jar
Ling_suu8 分钟前
SpringMVC——简介及入门
spring·mvc
荆州克莱15 分钟前
Vue3 源码解析(三):静态提升
spring boot·spring·spring cloud·css3·技术
Abladol-aj1 小时前
并发和并行的基础知识
java·linux·windows
清水白石0081 小时前
从一个“支付状态不一致“的bug,看大型分布式系统的“隐藏杀机“
java·数据库·bug
吾日三省吾码7 小时前
JVM 性能调优
java
弗拉唐8 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi778 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
少说多做3439 小时前
Android 不同情况下使用 runOnUiThread
android·java