Spring Cloud Gateway
Spring Cloud 2.x 实 现 了 社 区 生 态 下 的 Spring CloudGateway(简称SCG)微服务网关项目。Spring Cloud Gateway基于WebFlux框架开发,目标是替换掉Zuul。
Spring Cloud Gateway概述
Spring Cloud Gateway主要有两个特性:
①非阻塞,默认使用R.NETty作为响应式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 predicates策略集合,以键值对的形式对应断言策略。
● filters:对应RouteDefinition中的Listfilters集合,以键值对的形式对应过滤器策略。
● 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,代码如下:
小结
构建响应式微服务可以获得异步、响应性、弹性、快速恢复、背压等系统特性,同时响应式微服务架构在资源占用、高并发、高吞吐、异步处理场景中具有更强的优势。目前响应式框架技术选型众多,如果将响应式编程应用到大规模生产系统中,则需要进行周密的调研,并对实际项目周期、人员经验、技术框架等因素进行综合权衡考虑,避免技术的复杂度问题成为业务发展过程中的瓶颈。