前言
哈喽,好久不见,鸽了几个月,小年我回来了!
最近项目上需要做架构设计的优化,某个业务模块发展比较快,我们打算把它拆分出单独的服务。
有关服务拆分的小年前面也分享过: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网关,但它们在设计理念、性能、功能等方面存在一些差异。以下是两者的一些主要对比点:
- 架构和性能:
- Zuul 1.x 是基于Servlet 2.5,并且是同步阻塞的,不支持长连接,比如WebSockets。
- Spring Cloud Gateway 基于 Spring WebFlux,使用非阻塞、异步请求处理,支持WebSockets,适合高并发场。
- 编程模型:
- Zuul 1.x 使用的是传统的Servlet模型。
- Spring Cloud Gateway 支持响应式编程,利用了WebFlux框架,可以构建异步的、非阻塞的、事件驱动的服务。
- 集成和生态:
- Zuul 是Netflix的开源项目,虽然Spring Cloud有集成Zuul,但Zuul 2.x版本Spring Cloud并未集成,Zuul 2.x是基于Netty的,支持异步非阻塞。
- Spring Cloud Gateway 是Spring Cloud的子项目,与Spring生态更加紧密集成,提供了更好的开发体验。
- 性能优化:
- Zuul 1.x 性能可以通过参数调优提高,但理论上Spring Cloud Gateway 由于其异步非阻塞的特性,不需要太多优化即可达到接近极限的性能。
- 功能和扩展性:
- Zuul 提供了基本的路由、过滤功能,但社区活跃度和扩展性相对较低。
- Spring Cloud Gateway 提供了更丰富的路由、过滤器等扩展点,方便用户定制化配置。
- 社区和维护:
- Zuul 1.x 社区活跃度相对较低,且Netflix已经发布了Zuul 2.x,但Spring Cloud没有整合计划。
- Spring Cloud Gateway 作为Spring Cloud生态的一部分,得到了Spring社区的积极维护和更新。
- 跨服务通信:
- 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;
});
}
这里有几个关键的步骤:
this.routeLocator.getRoutes()
是获取配置文件中的路由信息,也就是上面例子中在 yaml 中配置的路由规则- concatMap 方法,顺序遍历每一个route,判断当前路由的断言(predicate)是否匹配。
- 上面也有提过,Spring Cloud Gateway 内置了很多 Predicate,可以针对特殊的请求
- 断言匹配这里的代码设计其实比较有意思,后面另外文章展开说说
- 然后 .next() 方法就会从过滤后的路由中选择第一个匹配的路由。
Predicate
在路由匹配方法的代码中 r.getPredicate().apply(exchange)
判断当前请求是否符合路由断言的规则。
代码示例 yaml 配置中的 routeID=payment_route,断言的条件是 Path=/test/**,Method=GET,使用到了 PathRoutePredicateFactory
和 MethodRoutePredicateFactory
。
Spring Cloud Gateway 内置了比较丰富的断言实现,开发者可以自由发挥。
Spring Cloud Gateway 断言部分的代码设计结构还是比较有意思的,下一篇小年展开讲一讲,这里就先不展开了。
Filter
RoutePredicateHandlerMapping
getHandlerInternal
返回 FilteringWebHandler
,然后就执行 handler 中的过滤器
Filter 可以分成两种:GlobalFilter、GatewayFilter
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上。