Spring Cloud GatewayAPI网关对比
技术介绍
文中针对 Nginx、ZUUL、Spring Cloud Gateway等技术进行了对比。
API 网关
API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题(这些问题其实在每个大型系统、每个大型企业的IT组织中都普遍存在):
- 客户端会多次请求不同的微服务,增加了客户端的复杂性【协调、组装,典型的BFF】。
- 存在跨域请求,在一定场景下处理相对复杂【跨域,典型的nginx反向代理】。
- 认证复杂,每个服务都需要独立认证【认证、授权,典型的CAS及类CAS】。
- 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施【微服务重构、重组织】。
- 某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难【协议转换,典型的SOA】。
以上这些问题可以借助 API 网关解决。API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API 网关这一层(避免经过两层物理网关很重要)。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性,典型的架构图如图所示:
使用 API 网关后的优点如下:
- 易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
- 易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
- 减少了客户端与各个微服务之间的交互次数。
网关应当具备以下功能:
- 性能:API高可用,负载均衡,容错机制。
- 安全:权限身份认证、脱敏,流量清洗,后端签名(保证全链路可信调用),黑名单(非法调用的限制)。
- 日志:日志记录(spainid,traceid)一旦涉及分布式,全链路跟踪必不可少。
- 缓存:数据缓存。
- 监控:记录请求响应数据,api耗时分析,性能监控。
- 限流:流量控制,错峰流控,可以定义多种限流规则。
- 灰度:线上灰度部署,可以减小风险。
- 路由:动态路由规则。
NGINX 服务
Nginx 由内核和模块组成,内核的设计非常微小和简洁,完成的工作也非常简单,仅仅通过查找配置文件与客户端请求进行 URL 匹配,用于启动不同的模块去完成相应的工作。
下面这张图反应的是 HTTP 请求的常规处理流程:
Nginx 的模块直接被编译进 Nginx,因此属于静态编译方式。启动 Nginx 后,Nginx 的模块被自动加载,不像 Apache,首先将模块编译为一个 so 文件,然后在配置文件中指定是否进行加载。在解析配置文件时,Nginx 的每个模块都有可能去处理某个请求,但是同一个处理请求只能由一个模块来完成。
Nginx 在启动后,会有一个 Master 进程和多个 Worker 进程,Master 进程和 Worker 进程之间是通过进程间通信进行交互的,如图所示。Worker 工作进程的阻塞点是在像 select()、epoll_wait() 等这样的 I/O 多路复用函数调用处,以等待发生数据可读 / 写事件。Nginx 采用了异步非阻塞的方式来处理请求,也就是说,Nginx 是可以同时处理成千上万个请求的。一个 Worker 进程可以同时处理的请求数只受限于内存大小,而且在架构设计上,不同的 Worker 进程之间处理并发请求时几乎没有同步锁的限制,Worker 进程通常不会进入睡眠状态,因此,当 Nginx 上的进程数与 CPU 核心数相等时(最好每一个 Worker 进程都绑定特定的 CPU 核心),进程间切换的代价是最小的。
Netflix 的 Zuul
Zuul 是 Netflix 开源的微服务网关组件,它可以和 Eureka、Ribbon、Hystrix 等组件配合使用。Zuul 的核心是一系列的过滤器,这些过滤器可以完成以下功能:
- 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求。
- 审查与监控:与边缘位置追踪有意义的数据和统计结果,从而带来精确的生产视图。
- 动态路由:动态地将请求路由到不同的后端集群。
- 压力测试:逐渐增加指向集群的流量,以了解性能。
- 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求。
- 静态响应处理:在边缘位置直接建立部分响应,从而避免其转发到内部集群。
- 多区域弹性:跨越 AWS Region 进行请求路由,旨在实现 ELB(Elastic Load Balancing,弹性负载均衡)使用的多样化,以及让系统的边缘更贴近系统的使用者。
上面提及的这些特性是 Nigix 所没有的,这是因为 Netflix 公司创造 Zuul 是为了解决云端的诸多问题(特别是帮助 AWS 解决跨 Region 情况下的这些特性实现),而不仅仅是做一个类似于 Nigix 的反向代理,当然,我们可以仅使用反向代理功能,这里不多做描述。
Zuul1 是基于 Servlet 框架构建,如图所示,采用的是阻塞和多线程方式,即一个线程处理一次连接请求,这种方式在内部延迟严重、设备故障较多情况下会引起存活的连接增多和线程增加的情况发生。
Zuul2 的巨大区别是它运行在异步和无阻塞框架上,每个 CPU 核一个线程,处理所有的请求和响应,请求和响应的生命周期是通过事件和回调来处理的,这种方式减少了线程数量,因此开销较小。又由于数据被存储在同一个 CPU 里,可以复用 CPU 级别的缓存,前面提及的延迟和重试风暴问题也通过队列存储连接数和事件数方式减轻了很多(较线程切换来说轻量级很多,自然消耗较小)。这一变化一定会大大提升性能,我们在后面的测试环节看看结果。
我们今天谈的是 API 网关性能,这一点也涉及到高可用,简单介绍 Zuul 的高可用特性,高可用是非常关键的,因为外部请求到后端微服务的流量都会经过 Zuul,所以在生产环境中一般都需要部署高可用的 Zuul 来避免单点故障。一般我们有两种部署方案:
- Zuul 客户端注册到 Eureka Server
这种情况是比较简单的情况,只需要将多个 Zuul 节点注册到 Eureka Server 上,就可以实现 Zuul 的高可用。事实上,这种情况下的高可用和其他服务做高可用的方案没有什么区别。我们来看下面这张图,当 Zuul 客户端注册到 Eureka Server 上时,只需要部署多个 Zuul 节点就可以实现高可用。Zuul 客户端会自动从 Eureka Server 查询 Zuul Server 列表,然后使用负载均衡组件(例如 Ribbon)请求 Zuul 集群。
- Zuul 客户端不能注册到 Eureka Server
假如说我们的客户端是手机端 APP,那么不可能通过方案 1 的方式注册到 Eureka Server 上。这种情况下,我们可以通过额外的负载均衡器来实现 Zuul 的高可用,例如 Nginx、HAProxy、F5 等。
如图所示,Zuul 客户端将请求发送到负载均衡器,负载均衡器将请求转发到其代理的其中一个 Zuul 节点,这样就可以实现 Zuul 的高可用。
Spring Cloud Gateway
特性
- 基于Spring Framework 5、Project Reactor和Spring Boot 2.0构建
- 能够在任意请求属性上匹配路由
- predicates(谓词) 和 filters(过滤器)是特定于路由的
- 集成了Hystrix断路器
- 集成了Spring Cloud DiscoveryClient
- 易于编写谓词和过滤器
- 请求速率限制
- 路径重写
概念
- Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由;
- Predicate(断言):指的是Java 8 的 Function Predicate。 输入类型是Spring框架中的ServerWebExchange。 这使开发人员可以匹配HTTP请求中的所有内容,例如请求头或请求参数。如果请求与断言相匹配,则进行路由;
- Filter(过滤器):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前后对请求进行修改。
原理
客户端向Spring Cloud Gateway发出请求。如果Gateway Handler Mapping确定请求与路由匹配,则将其发送给Gateway Web Handler。这个Handler运行通过特定于请求的过滤器链发送请求。过滤器可以在发送代理请求之前或之后执行逻辑。执行所有的“pre”过滤逻辑,然后发出代理请求,最后执行“post”过滤逻辑。
创建 api-gateway模块
这里我们创建一个api-gateway模块来演示Gateway的常用功能。
在pom.xml中添加相关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
复制代码
两种不同的配置路由方式
Gateway 提供了两种不同的方式用于配置路由,一种是通过yml文件来配置,另一种是通过Java Bean来配置,下面我们分别介绍下。
- 使用yml配置
- 在application.yml中进行配置:
server:
port: 9201
service-url:
user-service: http://localhost:8201
spring:
cloud:
gateway:
routes:
- id: path_route #路由的ID
uri: ${service-url.user-service}/user/{id} #匹配后路由地址
predicates: # 断言,路径相匹配的进行路由
- Path=/user/{id}
复制代码
- 启动eureka-server,user-service和api-gateway服务,并调用该地址测试:http://localhost:9201/user/1
- 我们发现该请求被路由到了user-service的该路径上:http://localhost:8201/user/1
- 使用Java Bean配置
- 添加相关配置类,并配置一个RouteLocator对象:
/**
* Created by macro on 2019/9/24.
*/
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route2", r -> r.path("/user/getByUsername")
.uri("http://localhost:8201/user/getByUsername"))
.build();
}
}