一文搞懂 Spring Cloud Gateway 源码

一文搞懂 Spring Cloud Gateway 源码

Scroll Down

前言

哈喽,好久不见,鸽了几个月,小年我回来了!

最近项目上需要做架构设计的优化,某个业务模块发展比较快,我们打算把它拆分出单独的服务。

有关服务拆分的小年前面也分享过:http://edisonz.cn/archives/%E6%9C%8D%E5%8A%A1%E6%8B%86%E5%88%86%E8%BF%81%E7%A7%BB

小年方案设计阶段的时候碰到一个比较棘手的问题:如何迁移共用 API ?

共用 API 是什么?就是指多个业务模块共用的接口,一般是通过参数来区分业务类型。

一开始想到的方案是:由网关服务层支持自定的参数路由规则(当然,前提是你们微服务有网关层)

然而负责网关层同学告诉小年不支持这个能力。而且,共用的 API 不仅只有网关层上游调用,还有服务之间的调用,而这部分是不经过网关层!

所以,这个方案并不能解决后者的问题。

当然,如果在原业务服务一个一个对 API 做切流和转发也是能实现,But!关键是接口的数量不少,而且每个 API 的切流规则可能不尽相同,这样人工开发的工作量属实不少,不太现实。

既然是接口转发,那么业务层直接实现网关功能不就可以了?

业务系统使用的是 Spring Cloud 全家桶,直接可以接入 Spring Cloud Gateway 框架来实现网关层能力,并且它也支持自定义拦截器,通过拦截器来实现参数的分流和灰度。

今天主要分享一下 Spring Cloud Gateway 的基本概念和入门操作,通过阅读源码了解其背后的工作原理。

正文

基本介绍

Spring Cloud Gateway 是 Spring Cloud 体系中的一个 API 网关组件,它基于 Spring Framework 5 和 Spring Boot 2.0 开发,并且构建在Project Reactor(响应式编程模型)之上,因此它也是一种异步非阻塞的 API 网关,适用于高并发场景。

当然,它也具备网关所应有的能力:路由转发、过滤、限流、熔断等。

而在 Spring Boot 2.0 之前网关组件是使用 Netflix Zuul,为什么在 Spring Cloud 的 Finchley 版本之后被引入作为推荐的 API 网关替代 Zuul 呢?

这两者之间的有什么区别?小年就直接贴上 GPT 的回答

Spring Cloud Gateway 和 Zuul 都是微服务架构中常用的API网关,但它们在设计理念、性能、功能等方面存在一些差异。以下是两者的一些主要对比点:

  1. 架构和性能
    • Zuul 1.x 是基于Servlet 2.5,并且是同步阻塞的,不支持长连接,比如WebSockets。
    • Spring Cloud Gateway 基于 Spring WebFlux,使用非阻塞、异步请求处理,支持WebSockets,适合高并发场。
  2. 编程模型
    • Zuul 1.x 使用的是传统的Servlet模型。
    • Spring Cloud Gateway 支持响应式编程,利用了WebFlux框架,可以构建异步的、非阻塞的、事件驱动的服务。
  3. 集成和生态
    • Zuul 是Netflix的开源项目,虽然Spring Cloud有集成Zuul,但Zuul 2.x版本Spring Cloud并未集成,Zuul 2.x是基于Netty的,支持异步非阻塞。
    • Spring Cloud Gateway 是Spring Cloud的子项目,与Spring生态更加紧密集成,提供了更好的开发体验。
  4. 性能优化
    • Zuul 1.x 性能可以通过参数调优提高,但理论上Spring Cloud Gateway 由于其异步非阻塞的特性,不需要太多优化即可达到接近极限的性能。
  5. 功能和扩展性
    • Zuul 提供了基本的路由、过滤功能,但社区活跃度和扩展性相对较低。
    • Spring Cloud Gateway 提供了更丰富的路由、过滤器等扩展点,方便用户定制化配置。
  6. 社区和维护
    • Zuul 1.x 社区活跃度相对较低,且Netflix已经发布了Zuul 2.x,但Spring Cloud没有整合计划。
    • Spring Cloud Gateway 作为Spring Cloud生态的一部分,得到了Spring社区的积极维护和更新。
  7. 跨服务通信
    • Zuul 1.x 作为基于阻塞I/O的API Gateway,性能相对较差。
    • Spring Cloud Gateway 支持异步通信,能进一步提高系统的吞吐量和响应性能。

核心概念

Route(路由)

路由是网关的基本配置单元,它定义了请求如何被发送到后端服务。路由包含了一个或多个断言(Predicate)和一个过滤器(Filter)列表,用于匹配来自客户端的请求并将请求转发到指定的服务。

Predicate(断言)

用于检查请求是否与路由匹配。Spring Cloud Gateway 提供了一系列内置的断言工厂,例如,检查请求头、请求方法、请求路径等。

Filter(过滤器)

过滤器用于在路由期间对请求和响应进行处理,例如修改请求头、日志记录、认证等。Spring Cloud Gateway 允许开发者自定义过滤器或使用内置的过滤器。

代码示例

示例代码:https://github.com/Zhang-BigSmart/spring-clould-gateway-demo

  • spring-cloud-gateway-demo-eureka:注册中心,负责管理微服务;
  • spring-cloud-gateway-demo-api:网关服务,负责路由转发;
  • spring-cloud-gateway-demo-server: 提供网关转发的 API接口的服务;

重点我们看spring-cloud-gateway-demo-api 模块,引入核心依赖

<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-gateway</artifactId>
</dependency>

application.yaml 配置接口的路由转发规则,比如下面配置中的 routeID=payment_route,当请求路径是 /test/**,并且是 GET 请求的,会将请求转发到 localhost:8080。

也就是说请求如果是 GET: localhost:8111/test/payment 会将请求转发到 localhost:8080/test/payment

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

server:
    port: 8111

spring:
    main:
        web-application-type: reactive
    application:
        name: spring-cloud-gateway-demo-api
    cloud:
        gateway:
            routes:
                - id: payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
                  uri: localhost:8080/        #匹配后提供服务的路由地址
                  predicates:
                      - Path=/test/**         # 断言,路径相匹配的进行路由
                      - Method=GET
                - id: payment_route2
                  uri: lb://spring-cloud-gateway-demo-server
                  predicates:
                      - Path=/payment/lb/**
                  filters:
                      - RewritePath=/test(?<segment>/?.*), $\{segment}

Spring Cloud Gateway 的入门使用还是比较简单和容易上手,当然它还有很多高级的用法,内置各种断言和过滤器,也可以自定义自己的断言和过滤器,更多的高级特性网上一搜都有。

源码解析

工作原理

下面是 Spring Cloud Gateway 官网的工作原理概述图👇

客户端向 Spring Cloud Gateway 发出请求,Gateway Handler Mapping 处理请求,确定请求与路由是否匹配,然后交给 Gateway Web Handler 处理。

Gateway Web Handler 实际上就是一组过滤器(Filter),按顺序执行完所有的 Filter 后,通过 Proxy Filter 转发请求,调用其他服务接口。

对客户端请求的前置处理,其实思想跟 SpringMVC 很相似,如果有了解过 SpringMVC 原理的同学应该知道, DispatchSerlvet 处理请求找到匹配的 HandlerMapping,从而找到相应的 Controller。

Spring Cloud Gateway 是通过 DispatcherHandler 遍历所有的 Mapping ,找到匹配路由的 Mapping,再执行对应的 Handler

RoutePredicateHandlerMapping 是对应上图中的 Gateway Handler Mapping,核心代码片段如下:

public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
  ...
	@Override
	protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
    ...

    return Mono.deferContextual(contextView -> {
      exchange.getAttributes().put(GATEWAY_REACTOR_CONTEXT_ATTR, contextView);
      return lookupRoute(exchange)
          // .log("route-predicate-handler-mapping", Level.FINER) //name this
          .map((Function<Route, ?>) r -> {
            exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
            if (logger.isDebugEnabled()) {
              logger.debug("Mapping [" + getExchangeDesc(exchange) + "] to " + r);
            }

            exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
            return webHandler;
          }).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
            exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
            if (logger.isTraceEnabled()) {
              logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
            }
          })));
    });
  }
  
}  

getHandlerInternal 是整个类的主要方法,判断是否匹配路由,并且返回相应的 webHandler。

再看到核心方法 lookupRoute ,这个是路由匹配的核心关键方法

protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
  return this.routeLocator.getRoutes()
      // individually filter routes so that filterWhen error delaying is not a
      // problem
      .concatMap(route -> Mono.just(route).filterWhen(r -> {
        // add the current route we are testing
        exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
        return r.getPredicate().apply(exchange);
      })
          // instead of immediately stopping main flux due to error, log and
          // swallow it
          .doOnError(e -> logger.error("Error applying predicate for route: " + route.getId(), e))
          .onErrorResume(e -> Mono.empty()))
      // .defaultIfEmpty() put a static Route not found
      // or .switchIfEmpty()
      // .switchIfEmpty(Mono.<Route>empty().log("noroute"))
      .next()
      // TODO: error handling
      .map(route -> {
        if (logger.isDebugEnabled()) {
          logger.debug("Route matched: " + route.getId());
        }
        validateRoute(route, exchange);
        return route;
      });
}

这里有几个关键的步骤:

  1. this.routeLocator.getRoutes() 是获取配置文件中的路由信息,也就是上面例子中在 yaml 中配置的路由规则
  2. concatMap 方法,顺序遍历每一个route,判断当前路由的断言(predicate)是否匹配。
    • 上面也有提过,Spring Cloud Gateway 内置了很多 Predicate,可以针对特殊的请求
    • 断言匹配这里的代码设计其实比较有意思,后面另外文章展开说说
  3. 然后 .next() 方法就会从过滤后的路由中选择第一个匹配的路由。

Predicate

在路由匹配方法的代码中 r.getPredicate().apply(exchange) 判断当前请求是否符合路由断言的规则。

代码示例 yaml 配置中的 routeID=payment_route,断言的条件是 Path=/test/**,Method=GET,使用到了 PathRoutePredicateFactoryMethodRoutePredicateFactory

Spring Cloud Gateway 内置了比较丰富的断言实现,开发者可以自由发挥。

Spring Cloud Gateway 断言部分的代码设计结构还是比较有意思的,下一篇小年展开讲一讲,这里就先不展开了。

Filter

RoutePredicateHandlerMapping getHandlerInternal 返回 FilteringWebHandler ,然后就执行 handler 中的过滤器

Filter 可以分成两种:GlobalFilterGatewayFilter

Global filters 会被应用到所有的路由上,而 Gateway filter 将应用到单个路由上或者一个分组的路由

Spring Cloud Gateway 内置很多 GatewayFilter,开发者可以根据实际场景使用,而且配置的方式很简单。

比如下面的 route 配置,会在匹配的请求头加上一对请求头,名称为 X-Request-Id 值为 blue

spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: https://example.org
        filters:
        - AddRequestHeader=X-Request-red, blue

更多的 GatewayFilter 配置可以参考官网:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gatewayfilter-factories

全局路由 GlobalFilter 有下面几个

ForwardPathFilter

修改请求路径,就是将请求的路径转发到另一个请求路径。比如需要将 /v1/order 转发到 /v2/order

RouteToRequestUrlFilter

把客户端请求路径中的 uri 替换成目标转发的 uri

routes:
  - id: my-route
    uri: http://example.com
    predicates:
      - Path=/my-service/**

比如上面配置,客户端请求路径 http://gateway-service/my-service/resource,而 RouteToRequestUrlFilter ` 会解析出请求路径中的 uri (http://example.com) ,然后把 uri 替换成目标uri,替换成新的请求路径 http://example.com/my-service/resource

ReactiveLoadBalancerClientFilter

Spring Cloud Gateway 作为微服务网关,必定也支持服务集群之间的调用,通过服务注册中心可以通过服务名转发目标服务的接口上。

像上面例子中的配置,uri: lb://spring-cloud-gateway-demo-server,以 lb:// 开头的就是标识微服务间的调用,而且会被 ReactiveLoadBalancerClientFilter 处理。

spring:
    main:
        web-application-type: reactive
    application:
        name: spring-cloud-gateway-demo-api
    cloud:
        gateway:
            routes:
                - id: payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
                  uri: lb://spring-cloud-gateway-demo-server        #匹配后提供服务的路由地址
                  predicates:
                      - Path=/test/**         # 断言,路径相匹配的进行路由
                      - Method=GET

这个过滤器的处理逻辑其实也比较简单,就是拉取转发的目标服务的注册地址列表,选择一个然后替换成真正的IP地址

这一块的代码是有一些研究的意思,SpringCloud 新版本的客户端负载均衡使用了LoadBalancer 替代了原先 NetFlix Ribbon

NettyRoutingFilter

这个是 Spring Cloud Gateway 最核心的过滤器,它负责与目标服务器之间的通信,简单理解就是,把请求转发到目标服务器的接口上。

当然有些与众不同的是,HTTP的通信协议是基于 Netty 框架实现。

Netty 作为一个高性能的网络框架,相信大家多少都有了解。而 Spring Cloud Gateway 选用 Netty 不仅是本身支持高并发、高吞吐、异步非阻塞的特性, Spring 5 开始全面接入了 Reactor 作为底层的响应式编程框架,并且 Netty 就是 Reactor 的默认网络层实现,所以 Spring Cloud Gateway 选择 Netty 也就成为顺理成章的事情。

NettyWriteResponseFilter

NettyWriteResponseFilter 是对响应信息的一些扩展操作,比如可以修改响应头,响应信息转换、加解密,缓存等。

这里比较有意思的是,可以看到上面图片 Filter 的排序,NettyWriteResponseFilter 排在比较前,但是执行的顺序是最后。这里是使用了 chain.filter(exchange).then 方式,所有 filter 执行完之后再执行 then

@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		// NOTICE: nothing in "pre" filter stage as CLIENT_RESPONSE_CONN_ATTR is not added
		// until the NettyRoutingFilter is run
		// @formatter:off
		return chain.filter(exchange)
				.then(Mono.defer(() -> {
					Connection connection = exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR);
          ......
				})).doOnCancel(() -> cleanup(exchange))
				.doOnError(throwable -> cleanup(exchange));
		// @formatter:on
	}

ForwardRoutingFilter

ForwardPathFilter 修改了请求路径之后,由 ForwardRoutingFilter 来转发。当然是直接将请求再交由 dispatcherHandler 进行处理,dispatcherHandler 会根据 path 前缀找到需要目标处理器执行逻辑。

小结

读到这里,相信大家对 Spring Cloud Gateway 的使用以及工作原理和架构设计都有更更进一步的理解。

通过阅读源码,我们学习到 Spring Cloud Gateway 的架构设计和原理,你会发现,其实实现一个网关并没有想象那么复杂,甚至可以说是有趣且有很多值得学习的地方。

简单来说,核心链路的实现其实就是过滤器链的执行框架。

当然Spring Cloud Gateway 集成了 Reactor 框架,如果没有了解过 Reactor 框架的同学,估计最难和最花时间的就在这一part上。