写点什么

万字长文带你吃透 SpringCloudGateway 工作原理 + 动态路由 + 源码解析

作者:Java快了!
  • 2022 年 9 月 12 日
    湖南
  • 本文字数:8325 字

    阅读完需:约 27 分钟

Spring Cloud Gateway

Spring Cloud 2.x 实 现 了 社 区 生 态 下 的 Spring CloudGateway(简称 SCG)微服务网关项目。Spring Cloud Gateway 基于 WebFlux 框架开发,目标是替换掉 Zuul。

 Spring Cloud Gateway 概述

Spring Cloud Gateway 主要有两个特性:

①非阻塞,默认使用 RxNetty 作为响应式 Web 容器,通过非阻塞方式,利用较少的线程和资源来处理高并发请求,并提升服务资源利用的可伸缩性。

②函数式编程端点,通过使用 Spring WebFlux 的函数式编程模式定义路由端点,处理请求。

Spring Cloud Gateway 可与 Eureka、Ribbon、Hystrix 等组件配合使用,基于 Spring 5 的 Reactor 和 Spring Boot 2 构建,使用 Netty 作为底层通信框架,支持异步非阻塞编程模型和响应式编程框架,解决了 Zuul 框架的 I/O 阻塞问题和线程收敛问题。使用 Spring WebFlux 框架可以使 Spring Cloud Gateway 在高并发场景下具有更好的性能表现,占用更少的资源。

下面是 Spring Cloud 官方对 Spring Cloud Gateway 特征的介绍。

● 基于 Spring Framework 5、Reactor 和 Spring Boot 2.0 框架。

● 根据请求的属性可以匹配对应的路由。

● 集成 Hystrix。

● 集成 Spring Cloud DiscoveryClient。

● 把易于编写的 Predicates 和 Filters 作用于特定路由。

● 具备一些网关的高级功能,如动态路由、限流、路径重写。

对于微服务网关来说,最核心的特征包括路由和过滤器机制。从功能特性上来看,Spring Cloud Gateway 和 Zuul 具备相似的特性。它们都可以集成 Hystrix、Ribbon 负载均衡及 Spring Cloud 的现有组件来实现附加功能。而且 Spring Cloud Gateway 的本质特性还体现在底层的通信框架上,它可以基于 Netty 的 I/O 多路复用和事件响应机制来实现网络通信;它的另外一大特性就是使用 Spring Framework 5 的响应式编程模型,允许通过 Spring WebFlux 实现异步非阻塞特性,在性能和资源利用率上,都有了质的提升。在编程范式上,Spring CloudGateway 使用函数式编程模式。官方提供的 Spring Cloud Gateway 的架构图如下所示。


Spring Cloud Gateway 的核心概念

简单说明一下架构图中的三个术语。

● Filter(过滤器):和 Zuul 的过滤器在概念上类似,可以使用 Filter 拦截和修改请求,实现对上游的响应,进行二次处理,实现横切与应用无关的功能,如安全、访问超时设置、限流等功能。

● Route(路由):网关配置的基本组成模块,和 Zuul 的路由配置模块类似。一个 Route 模块由一个 ID、一个目标 URI、一组断言和一组过滤器组成。如果断言为真,则路由匹配,目标 URI 会被访问。

● Predicate(断言):Predicate 来自 Java 8 的接口,它可以用来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。接口包含多种默认方法,并将 Predicate 组合成复杂的逻辑(与、或、非),可以用于接口参数校验、路由转发判断等。

Spring Cloud Gateway 的接入和配置

Spring Cloud Gateway 依赖 Spring WebFlux 提供的 Netty 运行时环境,所以 Spring Boot 必须是 2.0 或者以上版本。基本的 Spring Cloud 环境配置确认后,主要的接入步骤如下。

1.Maven 依赖引入



2.路由配置方式一:配置文件方式


各字段含义如下。

● id:自定义的路由 ID,保持唯一。

● uri:目标服务地址。

● predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。

○ 第一个 Predicate 基于 URL 的方式。配置文件的第一个路由的配置采用 URL 方式,配置了一个 ID 为 url-proxy-1 的 URI 代 理 规 则 。 路 由 的 规 则 为 : 当 访 问 地 址 为 http://localhost:8080/csdn/1.jsp 时,会路由到上游地址 https://localhost:8010/1.jsp。○ 第二个 Predicate 基于服务 ID 发现的方式。配置文件的第二个路由的配置采用与注册中心相结合的服务发现方式,与单个 URI 的路由配置相比,区别其实很小,仅在于 URI 的 schema 协议不同。单个 URI 地址的 schema 协议,一般为 HTTP 或者 HTTPs 协议。

3.基于代码 DSL 方式的路由配置接入

路由转发功能同样可以通过代码来实现,我们可以在启动类 GatewayApplication 中添加 customRouteLocator 方法来定制转发规则,代码如下:


Spring Cloud Gateway 的工作原理

客户端向 Spring Cloud Gateway 发出 HTTP 请求后,如果 GatewayHandlerMapping 确 定 请 求 与 路 由 匹 配 , 则 将 其 发 送 到 GatewayWebHandler。WebHandler 通过该请求的特定过滤器链处理请求。过滤器 可 以 在 发 送 代 理 请 求 之 前 或 之 后 执 行 逻 辑 。 在 Spring CloudGateway 的执行流程中,首先执行所有“pre filter”逻辑,然后进行回源请求代理。在请求代理执行完后,执行“post filter”逻辑。在“pre”类型的过滤器中,可以实现参数校验、权限校验、流量监控、日志输出、协议转换等功能;在“post”类型的过滤器中,可以实现响应内容、响应头的修改,日志的输出、流量监控等功能。核心工作流程如下图所示。


Predicate 条件

在 Spring Cloud Gateway 中,Spring 利用 Predicate 的特性实现了各种路由匹配规则,通过 Header、请求参数等不同条件来匹配对应的路由。

我们来看 Spring Cloud Gateway 内置的几种 Predicate 的使用方法。



在上述配置文件中,如果多种 Predicates 同时存在于同一个路由,请求必须同时满足所有条件才能被这个路由匹配。当一个请求满足多个路由的 Predicate 条件时,请求只会被首个成功匹配的路由转发。下面分别对不同规则的路由匹配进行解释。

● 通过请求路径匹配(Path Route Predicate)

路由断言工厂接收一个参数,根据 Path 定义好的规则来判断访问的 URI 是否匹配。配置示例如下:


如果请求路径符合要求,则此路由将匹配,例如/hello/1 或者/hello/world。

使用 curl 测试,命令行输入:


经过测试发现,第一条和第二条命令可以正常获取页面返回值,最后一个命令报 404 错误,证明路由是通过指定路径来匹配的。

● 通过请求参数匹配(Query Route Predicate)

路由断言工厂接收两个参数:一个必需的参数和一个可选的正则表达式。配置示例如下:



在这样的配置中,只要请求中包含 helloworld 属性的参数即可匹配路由。使用 curl 测试,命令行输入:


经过测试发现,只要请求中带有 helloworld 参数就会匹配路由,不带 helloworld 参数则不会匹配。还可以将 Query 的值以键值对的方式进行配置,这样在请求时会对属性值和正则表达式都进行匹配,键值对匹配后才会正确执行路由逻辑。


在上述路由匹配中,请求中包含 hello 属性并且参数值是以 world 开头的、长度为三位的字符串,才会进行匹配和路由。使用 curl 测试,命令行输入:


测试可以返回正确的页面代码。如果将 hello 的属性值改为 ok,再次访问就会报 404 错误,证明路由需要匹配正则表达式才会进行路由。

● 通过请求方法匹配

路由断言工厂接收一个参数,即需要匹配 HTTP 方法。通过 POST、GET、PUT、DELETE 等不同的请求方式来进行路由。


使用 curl 测试(#curl 默认以 GET 的方式去请求),命令行输入:


测试返回页面代码,证明匹配到路由。

我们再以 POST 的方式请求测试。


返回 404 错误表示没有找到,证明没有匹配上路由。

● 通过 Header 属性匹配

路由断言工厂接收两个参数,分别是请求头名称和正则表达式。

Header Route Predicate 和 Cookie Route Predicate 一样,也是接收 2 个参数:一个 header 的属性值和一个正则表达式。这个属性值和正则表达式匹配则执行。


● 通过 Host 路由匹配

Spring Cloud Gateway 可 以 根 据 Host 名 进 行 匹 配 转 发 , HostRoute Predicate 接收一组参数、一组匹配的域名列表。它通过参数中的主机地址作为匹配规则。


使用 curl 测试,命令行输入:


通过测试以上两种 Host 设置方式,均可匹配到 host_route,去掉 host 参数则会报 404 错误。

● 时间匹配

Predicate 支持设置时间,在请求转发时,先判断这个时间与我们设置的时间,然后进行转发,所以又细分为设置时间后断言、设置时间前断言、设置时间之间断言。

设置时间后断言:从 After Route Predicate Factory 中获取一个 UTC 时间格式的参数,当请求的当前时间在配置的 UTC 时间之后,则成功匹配,否则不能成功匹配。下面是实例配置:


设置时间前断言:从 Before Route Predicate Factory 中获取一个 UTC 时间格式的参数,当请求的当前时间在配置的 UTC 时间之前,则成功匹配,否则不能成功匹配。下面是实例配置:


设置时间之间断言:从 Between Route Predicate Factory 中获取一个 UTC 时间格式的参数,当请求的当前时间在配置的 UTC 时间之间,则成功匹配,否则不能成功匹配。下面是实例配置:


● 通过 Cookie 匹配

Cookie 路由断言会取两个参数,一个是 Cookie name,一个是正则表达式,路由规则是通过获取的对应 Cookie name 值和正则表达式进行匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。


● 通过 IP 地址匹配

RemoteAddr Route Predicate Factory 配置一个 IPv4 或者 IPv6 网段的字符串或者 IP 地址。当请求的 IP 地址在网段之内或者与配置的 IP 地址相同,匹配成功,则进行转发,否则不进行转发。


可以将 curl localhost:8080 设置为本机的 IP 地址进行测试,如果请求的远程地址是 192.168.1.30,则此路由将匹配。

GatewayFilter 与 GlobalFilter

Spring Cloud Gateway 中 有 两 种 Filter , 一 种 是 GlobalFilter(全局过滤器),一种是 GatewayFilter。GlobalFilter 默认对所有路由有效,GatewayFilter 需要通过路由分组指定。

GlobalFilter 接口与 GatewayFilter 具有相同的签名,是有条件地应用于所有路由的特殊过滤器。

当请求进入路由匹配逻辑时,Web Handler 会将 GlobalFilter 的所有实例和所有 GatewayFilter 路由特定实例添加到 Filter Chain 组件。Filter 组合执行的顺序由 Ordered 接口决定,可以通过 getOrder 方法或使用 @Order 注释来设置。Spring Cloud Gateway 通过执行过滤器将逻辑分为“前置”和“后置”阶段,优先级较高的前置过滤器会优先被执行,而优先级较高的后置过滤器的执行顺序正好相反,最后执行。

GatewayFilter Factories

过滤器允许以某种方式修改传入的 HTTP 请求或返回的 HTTP 响应。

过滤器的作用域是某些特定路由。Spring Cloud Gateway 包括许多内置的过滤器工厂。

● 实现前缀修改(增加前缀、去掉前缀)

PrefixPathGatewayFilterFactory 及 StripPrefixGatewayFilterFactory 是 一 对 处 理 请 求 URL 的 前 缀 的 Filter 工厂,前者添加前缀,后者去除前缀。

配置文件 application.yml 如下:


○PrefixPathGatewayFilterFactory 允许你在对应的路由请求前增加前缀。例如实例配置中的请求/hello,最后转发到目标服务的路径变为/mypath/hello。

○StripPrefixGatewayFilterFactory 允许你在对应的路由请求前去除前缀,例如实例配置中的请求/name/bar/foo,去除前面两个前缀后,最后转发到目标服务的路径为/foo。

● 实现请求头内容添加和改写

AddRequestHeader GatewayFilter Factory 采用一对名称和值作为参数,配置文件 application.yml 如下:


对于所有匹配的请求,将在向下游请求的头内容中添加 xrequest-foo:bar header。

● 实现请求体内容添加和改写

AddRequestParameter GatewayFilter Factory 采用一对名称和值作为参数,配置参数 application.yml 如下:


对于所有匹配的请求,将向下游请求添加 foo=bar 查询字符串。

● 实现熔断降级

Hystrix GatewayFilter 允许向网关路由引入 Hystrix,保护服务不受级联故障的影响,并允许在下游故障时提供 fallback 响应。要在项 目 中 启 用 Hystrix 网 关 过 滤 器 , 需 要 向 Hystrix 的 依 赖 HystrixGatewayFilter Factory 添加一个 name 参数,即 HystrixCommand 的名称,配置文件 application.yml 如下:


当调用 hystrixfallback 时,将转发到/incaseoffailureusethis。注意,这个示例还演示了通过目标 URI 上的“lb”前缀使 Spring Cloud Netflix Ribbon 客户端实现负载均衡。主要场景是网关应用程序中的内部控制器或处理程序使用 fallbackUri,它也可以将请求重新路由到外部应用程序中的控制器或处理程序。

● 分布式限流

SpringCloudGateway 内置的 RequestRateLimiterGatewayFilterFactory 提供限流的能力,基于令牌桶算法实现。目前它内置的 RedisRateLimiter,依赖 Redis 来存储限流配置和统计数据。当然你也可以实现自己的 RateLimiter,只需实现 Spring Cloud Gateway 自 带 的 RateLimiter 接 口 或 者 继 承 AbstractRateLimiter。

首先,添加 Maven 依赖。


其次,添加限流配置。


最后,完成对 Path 的 KeyResolver(可以通过 KeyResolver 来指定限流的 Key),实现对特定 Path 下的限流控制配置。在过滤器中可以配置一个可选的 KeyResolver,KeyResolver 在配置中根据名称使用 SpEL 引用 Bean。#{@myKeyResolver}是引用名为“pathKeyResolver”的 Bean 的 SpEL 表达式。KeyResolver 接口允许使用可插拔策略来派生限制请求的 Key。代码如下:



Spring Cloud Gateway 的动态路由

下面介绍基于 Spring Cloud Gateway 的动态路由实现(相关代码将会随书附带),实现方式与 Zuul 的动态路由实现方式类似,具有比 Zuul 更加灵活的路由策略和匹配模式。这两种解决方案如下。

●通过 SpringCloudGateway 提供的 GatewayControllerEndpointduan 端点功能,实现路由的增删改 查 , 或 者 自 己 实 现 ApplicationEventPublisherAware 接口,实现自定义的路由操作方法。具体可以参考源码:GatewayControllerEndpointduan 类。

● 通过实现 RouteDefinitionRepository 接口,实现自定义的 Repository 类,实现从数据库或者缓存中动态加载路由信息的功能。架构模式与 Zuul 的动态路由采用相似的路由加载策略,架构流程图如下。


动态路由思路及解决方案具体如下。

首先,Admin 作为前端管理界面,将用户对路由的添加、修改等操作通过 RouteAsynchService 存储到 DB 中。DB 中的存储结构如下图所示。


字段映射关系如下。

● routeid:标识路由的唯一 ID,可以根据路由 ID 查找路由,路由 ID 不能重复。

● routename:应用名称是标识路由的别名,是非必选项。

● routeorder:对应 RouteDefinition 中的 order 属性。

● routestatus:路由状态,包括编辑、发布、下线等状态。

● strategy:路由策略,和 Zuul 的路由策略相似,也支持 ServiceID 策略和 URL 策略。

●predicates:对应 RouteDefinition 中的 List<PredicateDefinition> predicates 策略集合,以键值对的形式对应断言策略。

● filters:对应 RouteDefinition 中的 List<FilterDefinition>filters 集合,以键值对的形式对应过滤器策略。

● uri:对应后端服务,可以是后端服务的 ServiceID,也可以是服务的 URL 地址,与路由策略对应。

● groupname:标识这个新建的路由归属在哪个网关集群下面。

其次,Spring Cloud Gateway 的动态路由管理策略都通过 Admin 接收对网关路由的增删改查命令,然后通过 RouteAsynchService 将路由更新服务并发布到对应的网关节点,网关节点从数据库动态获得最新的路由状态,更新缓存和当前路由。下面对网关节点的事件监听机制进行讲解。



说明 1#:在代码段中,refreshRoute 方法是事件监听的入口方法,该方法会向 Admin 管理服务暴露一个 REST 服务。当 Admin 对路由进行更改后,会调用 refreshRoute 方法,触发 Spring Cloud Gateway 自带的 RefreshRoutesEvent 事件,同时设置原子布尔变量 routedefine 为 true,在下面的动态路由加载中根据该原子布尔变量决定是从数据库中读取路由还是从缓存中读取路由。

下面是定制化的核心路由动态加载和缓存管理的关键代码,主要通过实现自定义的路由 Repository 加载类来动态地加载路由,通过继承 RouteDefinitionRepository 父类来提供路由的配置信息,实现逻辑如下:



说明 2#:在代码段中,SagRouteDefinationRepository 是自定义的路由加载实现类,这个类实现了 RouteDefinitionRepository 接口。

该接口的源码如下:


然 后, 跟 进 getRouteDefinitions 方 法, 它是 RouteDefinitionRouteLocator 的回调方法,可以实时更新路由信息,代码如下:


从源码中调用链路追溯,可以发现下面的调用链路:


说明 3#:在代码段中,refreshNeed()方法是判断缓存是否失效的标识原子布尔变量,当 Admin 回调 1#代码段中的刷新接口时,会将该失效接口打开。在路由加载时,如果 refreshNeed 为 false 并且 routeDefinitions 不为空,那么优先加载缓存中的路由信息。如果 refreshNeed 为 true,那么优先执行加载数据库的操作,通过这段代码的逻辑处理就可以保证网关中路由的刷新效率和缓存与数据库中路由信息的同步。

说 明 4 # : 该 代 码 段 是 从 数 据 库 中 加 载 路 由 的 核 心 实 现 。

localteRoutefromDB ( ) 方 法 从 数 据 库 中 加 载 路 由 , 返 回 RouteDefinitionVo 模型的数据库路由列表信息。下面是该模型类的代码:


transfer( ) 方 法 实 现 了 从 RouteDefinitionVo 到 RouteDefinition 的类型转换,下面是 transfer()方法调用的类型转换的核心代码:



说明 5#:在代码段中,GatewayPredicateDefinitionFactory 完成断言的模式匹配转换。Predicate-Definition 是断言的模型定义,定义 name 为 Key、args 为 Value。举例如下:


GatewayPredicateDefinitionFactory 完成过滤器的模式匹配转换。FilterDefinition 是过滤器的模型定义,定义 name 为 Key、args 为 Value。举例如下:


Spring Cloud Gateway 源码解析

启动 Spring Cloud Gateway,需要依赖官方的 Starter 组件。下面我们从 Maven 依赖开始,对 Spring Cloud Gateway 的源码进行解析。


初始化加载

上述是 spring-cloud-starter-gateway 启动前需要引用的一个自动配置 Starter,可以通过查询该 Starter 的源码发现 Spring CloudGateway 的实现所依赖的组件,Maven 配置如下:



可 以 看 到 Spring Cloud Gateway 的 Starter 启 动 类 主 要 依 赖 spring-cloud-gateway-core 组件。使用 EnableAutoConfiguration 注解完成自动配置初始化信息,我们在 Spring Cloud Gateway 下的 spring.factories(在包 spring-cloud-gateway-core)声明文件如下:



GatewayAutoConfiguration



说明:

GatewayAutoConfiguration 配置是 Spring Cloud Gateway 的核心配置类,初始化如下组件:

● NettyConfiguration

●GlobalFilter(AdaptCachedBodyGlobalFilter、RouteToRequestUrlFilter、 ForwardRoutingFilter、ForwardPathFilter、WebsocketRoutingFilter、WeightCalculatorWebFilter 等)

● FilteringWebHandler

● GatewayProperties

● PrefixPathGatewayFilterFactory

● RoutePredicateFactory

● RouteDefinitionLocator● RouteLocator

●RoutePredicateHandlerMapping(查找匹配到的 Route 并进行处理)

● GatewayWebfluxEndpoint(管理网关的 HTTP API)

HTTP 请求路由源码分析

Spring Cloud Gateway 中使用 HandlerMapping 对请求的链接进行解析,匹配对应的 Route,转发到对应的服务。下图为整个请求的流程 , 用 户 请 求 先 通 过 DispatcherHandler 找 到 对 应 的 GatewayHandlerMapping,再通过 GatewayHandlerMapping 解析匹配到的 Handler;Handler 处理完后,经过 Filter 处理,最终将请求转发到后端服务。


在前面的动态路由加载过程中,其实已经贯穿了整个 HTTP 请求的调用链路,具体如下:


请求先由 DispatcherHandler 进行处理,DispatcherHandler 在初始化时会在 Spring IoC 容器中查找实现 HandlerMapping 接口的实现类 。 然 后 保 存 到 内 部 变 量 handlerMappings 数 据 结 构 中 。

DispatcherHandler 调 用 handler 方 法 迭 代 handlerMappings 中 的 HandlerMapping 接口,主要源码如下:



AbstractHandlerMapping 在 getHandler 方 法 中 封 装 了 CORS(Cross-Origin Resource Sharing,跨域资源共享)。因为所有 Handler 都可能涉及 CORS 的处理,所以抽象类 AbstractHandlerMapping 提供了 getHandlerInternal 子类来实现查找 Handler 的具体方法。


RoutePredicateHandlerMapping 用于匹配具体的路由,并返回 FilteringWebHandler 。 通 过 RoutePredicateHandlerMapping 中 的 RouteLocator 对 象 存 储 启 动 时 加 载 的 路 由 对 象 信 息 。 当 RoutePredicateHandlerMapping 获取对应的路由时,会将 Route 信息存储到 ServerWebExchanges 属性中,然后返回实现了 WebHandler 接口的 FilteringWebHandler 。 FilteringWebHandler 是 一 个 存 放 过 滤 器 的 Handler。

调用 RoutePredicateHandlerMapping 的 getHandlerInternal 方法从 RouteLocator 获取路由,并存放在 ServerWebExchange 中,返回 webFilter 对象,代码如下:


DispatcherHandler 通 过 SimpleHandlerAdapter 组 件 调 用 FilteringWebHandler 模块的 handler 方法,FilteringWebHandler 模块接 着 调 用 之 前 在 容 器 中 注 册 的 所 有 Filter , 处 理 完 毕 后 返 回 Response,代码如下:



小结

构建响应式微服务可以获得异步、响应性、弹性、快速恢复、背压等系统特性,同时响应式微服务架构在资源占用、高并发、高吞吐、异步处理场景中具有更强的优势。目前响应式框架技术选型众多,如果将响应式编程应用到大规模生产系统中,则需要进行周密的调研,并对实际项目周期、人员经验、技术框架等因素进行综合权衡考虑,避免技术的复杂度问题成为业务发展过程中的瓶颈。

本文给大家讲解的内容是响应式微服务架构,SpringCloudGateway 工作原理+动态路由+源码解析

用户头像

Java快了!

关注

还未添加个人签名 2022.09.03 加入

还未添加个人简介

评论

发布
暂无评论
万字长文带你吃透SpringCloudGateway工作原理+动态路由+源码解析_Java快了!_InfoQ写作社区