目录
十一、Gateway新一代网关
1.概述
Gateway:是在 Spring 生态系统之上构建的 API 网关服务,基于 Spring6,Spring Boot 3和 Project Reactor 等技术。它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式,并为它们提供跨领域的关注点,例如:安全性、监控/度量和恢复能力。
体系定位:
Cloud 全家桶中有个很重要的组件就是网关,在 1.x 版本中都是采用的Zuul网关;
但在 2.x 版本中,zuul 的升级一直跳票,SpringCloud 最后自己研发了一个网关 SpringCloud Gateway 替代 Zuul,那就是 SpringCloud Gateway
所以,gateway 是原 zuul1.x 版的替代
作用:
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
总结:
Spring Cloud Gateway 组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。
Spring Cloud Gateway 是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点 IP 端口信息,从而加强安全保护。
Spring Cloud Gateway本身也是一个微服务,需要注册进服务注册中心。
Question:nginx 和 gateway网关 到底有什么区别?
网关作为微服务的请求入口,也就是客户端的所有请求都会先到达微服务网关,在通过网关的路由转发到我们的真实服务。
理论上,这就是 nginx 干的活,为什么又多一个网关呢?
网关可以做非常多的事情,比如:解决跨域问题,过滤请求,验证 token 信息,接口服务的保护、限流、安全等。也就是说,对于每一个微服务,如果有一些重复的代码,我们完全可以在网关里进行统一处理,从而减少代码冗余性。
举个例子,假设我们现在需要验证我们的 token 会话信息:
对于 gateway网关,他是由 java 语言编写的,我直接调用后端 java 编写的 api 去连接到 redis 验证即可。
但对于 nginx,它是由 c 语言编写的,想要直接连接到 redis,是需要在中间通过 lua 脚本实现的。对于 java 程序员来说,这是十分繁琐的,还要学习 lua 脚本。
所以,这时候 gateway网关就要比 nginx 方便的多,可以帮助开发人员快速上手。
再举个例子,比如灰度发布,他是需要在中间去连接到我们的 nacos 的:
对于 gateway网关,本身就是由 java 语言编写的,而 nacos 也是由 java 语言编写的,所以连接起来就很方便。
但对于 ngnix,去连接 nacos,就得写 lua 脚本去发送一个 http 请求去连接到 nacos,所以还是学习 lua。
所以,微服务之间需要做的事情,就可以通过网关来实现,更加容易快速上手。
如果不是微服务之间的事情,nginx 也是可以做到的。而且相比之下,由于 nginx 是由 c 语言编写的;而 gateway 网关是由 java 编写的(java 底层还是通过 c 封装的),所以 nginx 性能更强,它在处理高并发请求时性能非常优越,适合用作前端的负载均衡器和反向代理。
在大多数情况下,Spring Gateway 和 Nginx 是可以搭配使用的。使用 Nginx 作为前端的反向代理和负载均衡器,负责处理来自外部客户端的请求,然后将请求转发到 Spring Gateway。Spring Gateway 再将请求路由到后端的微服务,负责微服务之间的复杂路由、限流、安全等功能
总结:Nginx 处理高并发、静态资源和边缘服务的流量,而 Spring Gateway 处理更复杂的 API 管理和微服务之间的流量。
2.Gateway三大核心
① Route(路由)
- 路由是构建网关的基本模块,它由 ID,目标 URI,一系列的断言和过滤器组成,如果断言为true 则匹配该路由
② Predicate(断言)
- 参考的是Java8的java.util.function.Predicate
- 开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
③ Filter(过滤)
- 指的是 Spring 框架中 GatewayFilter 的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
web 前端请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
predicate就是我们的匹配条件;
filter,就可以理解为一个无所不能的拦截器;
有了这两个元素,再加上目标 uri ,就可以实现一个具体的路由了
3.工作流程
核心逻辑:路由转发 + 断言判断 + 执行过滤器链
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(Pre)或之后(Post)执行业务逻辑。
在 “pre” 类型的过滤器可以做:参数校验、权限校验、流量监控、日志输出、协议转换等;
在 “post” 类型的过滤器中可以做:响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
4.入门配置
步骤:
① 建 module(cloud-gateway9527),在 pom 文件中导入依赖
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--服务注册发现consul discovery,网关也要注册进服务注册中心统一管控-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- 指标监控健康检查的actuator,网关是响应式编程删除掉spring-boot-starter-web dependency-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
② 编写 application.yml
server:
port: 9527
spring:
application:
name: cloud-gateway #以微服务注册进consul或nacos服务列表内
cloud:
consul: #配置consul地址
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
③ 修改主启动类
@SpringBootApplication
@EnableDiscoveryClient //服务注册和发现
public class Main9527
{
public static void main(String[] args)
{
SpringApplication.run(Main9527.class,args);
}
}
注意:网关和业务无关,所以不写任何业务代码。
④ 先启动 8500 服务中心 Consul,再启动 9527 网关入驻
5.路由映射
(1)8001 外部添加网关
需求:我们目前不想暴露 8001 端口,希望在 8001 真正的支付微服务外面套一层 9527 网关
(让前端不会直接访问 8001,而是先访问 9527)
准备:
① 8001 新建 PayGateWayController
@RestController
public class PayGateWayController
{
@Resource
PayService payService;
@GetMapping(value = "/pay/gateway/get/{id}")
public ResultData<Pay> getById(@PathVariable("id") Integer id)
{
Pay pay = payService.getById(id);
return ResultData.success(pay);
}
@GetMapping(value = "/pay/gateway/info")
public ResultData<String> getGatewayInfo()
{
return ResultData.success("gateway info test:"+ IdUtil.simpleUUID());
}
}
② 启动 8001 自测,检查是否通过:
http://localhost:8001/pay/gateway/get/1
http://localhost:8001/pay/gateway/info
步骤:
① 9527 网关 YML 新增配置
server:
port: 9527
spring:
application:
name: cloud-gateway #以微服务注册进consul或nacos服务列表内
cloud:
consul: #配置consul地址
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${ spring.application.name}
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- id: pay_routh2 #pay_routh2 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由
② 启动 8001 和 9527
添加网关前:http://localhost:8001/pay/gateway/get/1
http://localhost:8001/pay/gateway/info
添加网关后:http://localhost:9527/pay/gateway/get/1
http://localhost:9527/pay/gateway/info
隐真示假,映射说明:
运行结果:
目前 8001 支付微服务前面添加 GateWay 成功:GateWay9527 → Pay8001
(2)服务间调用添加网关
需求:我们启动 80 订单微服务,要求访问 9527 网关后才能访问 8001:80 → 9527 → 8001
Question:网关不是用于前端 web 请求转发到后端吗?怎么用于微服务之间了,我们不是有 openfeign 实现服务间接口调用了吗?
没错,微服务之间的确没必要使用网关,但该前提是这些服务都是同一家公司的(系统内环境)。
设想,如果淘宝调用顺丰的接口,这是不是微服务之间的调用,但这属于系统外访问,应当先找网关,在找真实服务。
我们现在模拟的就是后者!你可以把订单 80 服务和支付 8001 服务理解成两个不同公司的服务。
同时,这也很好的解释了我们一开始将 gateway 和 nginx 做对比,说:微服务之间需要做的事情,就可以通过网关来实现,更加容易快速上手。设想,这里如果使用 nginx 来实现,会不会就没这么简单了?
步骤:
① 修改 PayFeignApi 接口,添加方法
@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi
{
...
//GateWay进行网关测试案例01
@GetMapping(value = "/pay/gateway/get/{id}")
public ResultData getById(@PathVariable("id") Integer id);
//GateWay进行网关测试案例02
@GetMapping(value = "/pay/gateway/info")
public ResultData<String> getGatewayInfo();
}
② feign80 新建 OrderGateWayController
@RestController
public class OrderGateWayController
{
@Resource
private PayFeignApi payFeignApi;
@GetMapping(value = "/feign/pay/gateway/get/{id}")
public ResultData getById(@PathVariable("id") Integer id)
{
return payFeignApi.getById(id);
}
@GetMapping(value = "/feign/pay/gateway/info")
public ResultData<String> getGatewayInfo()
{
return payFeignApi.getGatewayInfo();
}
}
③ 测试,启动 80、网关9527、8001
http://localhost/feign/pay/gateway/get/1
http://localhost/feign/pay/gateway/info
测试成功!
但真的成功了吗?我们关闭网关再测一次(只启动 80 和 8001)
http://localhost/feign/pay/gateway/get/1
http://localhost/feign/pay/gateway/info
我们发现,开不开网关的效果都一样,openfeign 直接绕过了网关。
这是为什么呢?
还是刚才说的,同一家公司(系统内环境),微服务之间无需网关,直接通过 openfeign 调用。
但不同家公司(系统外访问),需要先找网关,在找真实服务。
我们真的将 80 和 8001 分隔开了吗?
在 PayFeignApi 中,我们 feign 接口中的服务名还是 8001,能够直接访问。
如果真的要将 80 和 8001 分隔开,我们这时候是不是应该是 gateway网关的微服务名?让 80 直接通过 feign 先去找网关?
此时,打开 80、网关9527、8001,进行测试:
打开 80、8001,关闭网关9527,进行测试:
当没有网关的时候,发生 503 异常,无法访问到 8001。
至此,才是真正测试成功!
(3)存在问题
在网关9527 的 yml 配置中,我们将映射(地址和端口号)写死 ,后续如果发生改变,只能一个个修改,这是非常不利的!
6.Gateway高级特性
(1)Route(路由)
因为 openfeign 天然支持负载均衡(lb:LoadBalancer),所以我们可以用 lb://微服务名,来动态获取服务 uri,解决映射写死问题!
但如果不支持负载均衡(lb),就只能手动指定服务的实际 URL。
修改 9527网关的 yml:
server: port: 9527 spring: application: name: cloud-gateway #以微服务注册进consul或nacos服务列表内 cloud: consul: #配置consul地址 host: localhost port: 8500 discovery: prefer-ip-address: true #优先使用服务ip进行注册 service-name: ${spring.application.name} gateway: routes: - id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名 uri: lb://cloud-payment-service #匹配后提供服务的路由地址 predicates: - Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由 - id: pay_routh2 #pay_routh2 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名 uri: lb://cloud-payment-service #