文章目录
前言
微服务背景下,一个系统被拆分为多个服务,但是像安全认证,流量控制,日志,监控等功能是每个服务都需要的,没有网关的话,我们就需要在每个服务中单独实现,这使得我们做了很多重复的事情并且没有一个全局的视图来统一管理这些功能。
一般情况下,网关可以为我们提供请求转发、安全认证(身份/权限认证)、流量控制、负载均衡、降级熔断、日志、监控、参数校验、协议转换等功能。
上面介绍了这么多功能,实际上,网关主要做了两件事情:请求转发 + 请求过滤。
由于引入网关之后,会多一步网络转发,因此性能会有一点影响(几乎可以忽略不计,尤其是内网访问的情况下)。 另外,我们需要保障网关服务的高可用,避免单点风险。
如下图所示,网关服务外层通过 Nginx(其他负载均衡设备/软件也行) 进⾏负载转发以达到⾼可⽤。Nginx 在部署的时候,尽量也要考虑高可用,避免单点风险。
网关能提供哪些功能?
绝大部分网关可以提供下面这些功能:
- 请求转发 :将请求转发到目标微服务。
- 负载均衡 :根据各个微服务实例的负载情况或者具体的负载均衡策略配置对请求实现动态的负载均衡。
- 安全认证 :对用户请求进行身份验证并仅允许可信客户端访问 API,并且还能够使用类似 RBAC 等方式来授权。
- 参数校验 :支持参数映射与校验逻辑。
- 日志记录 :记录所有请求的行为日志供后续使用。
- 监控告警 :从业务指标、机器指标、JVM 指标等方面进行监控并提供配套的告警机制。
- 流量控制 :对请求的流量进行控制,也就是限制某一时刻内的请求数。
- 熔断降级:实时监控请求的统计信息,达到配置的失败阈值后,自动熔断,返回默认值。
- 响应缓存:当用户请求获取的是一些静态的或更新不频繁的数据时,一段时间内多次请求获取到的数据很可能是一样的。对于这种情况可以将响应缓存起来。这样用户请求可以直接在网关层得到响应数据,无需再去访问业务服务,减轻业务服务的负担。
- 响应聚合:某些情况下用户请求要获取的响应内容可能会来自于多个业务服务。网关作为业务服务的调用方,可以把多个服务的响应整合起来,再一并返回给用户。
- 灰度发布 :将请求动态分流到不同的服务版本(最基本的一种灰度发布)。
- 异常处理:对于业务服务返回的异常响应,可以在网关层在返回给用户之前做转换处理。这样可以把一些业务侧返回的异常细节隐藏,转换成用户友好的错误提示返回。
- API 文档: 如果计划将 API 暴露给组织以外的开发人员,那么必须考虑使用 API 文档,例如 Swagger 或 OpenAPI。
- 协议转换 :通过协议转换整合后台基于 REST、AMQP、Dubbo 等不同风格和实现技术的微服务,面向 Web Mobile、开放平台等特定客户端提供统一服务。
1、什么是 Spring Cloud Gateway
Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且还基于 Filter 链的方式提供了网关基本的功能。目前最新版 Spring Cloud 中引用的还是 Zuul1.x版本,而这个版本是基于过滤器的,是阻塞IO,不支持长连接。
Spring Cloud GateWay是基于Spring生态系统之上构建的API网关,包括:Spring5、Spring Boot2 和Project Reactor。Spring Cloud GateWay旨在提供一种简单而有效的方法来路由到API,并为它们提供跨域的关注点。例如:安全性、监视/指标、限流等。由于Spring5提供的Netty、Http2,而Spring Boot2支持Spring5,因此Spring Cloud GateWay也一并支持Netty、Http2。
2、什么是服务网关
API Gateway,是出现在系统边界上的一个面向API的、串行集中式的强管控服务,这里的边界是企业IT系统的边界,可以理解为企业级应用防火墙 ,主要起到隔离外部访问与内部系统的作用 。在微服务概念的流行之前,API网关就已经诞生了,例如银行、证券等领域常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。
API网关的流行,源于近几年来移动应用与企业间互联网需求的兴起。移动应用、企业互联,使得后台服务支持的对象,从以前单一的web应用,扩展到多种使用场景,且每种使用场景对后台服务的要求都不尽相同。这不仅增加了后台服务的响应量,还增加了后台服务的复杂性。随着微服务架构概念的提出,API网关成为了微服务架构的一个标配组件。
API网关是一个服务器,是系统对外的唯一入口。API网关封装了系统内部架构,为每个客户端提供了定制的API,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有非业务功能。API网关并不是微服务场景中必须的组件,如下图,不管有没有API网关,后端微服务都可以通过API很好的支持客户端的访问。
但是对于服务数量众多、复杂度比较高、规模比较大的业务来说,引入API网关也有一系列的好处:
- 聚合接口使得服务对调用者透明,客户端与后端的耦合度降低。
- 聚合后台服务,节省流量,提高性能,提升用户体验。
- 提供安全、流控、过滤、缓存、计费、监控等 API 管理功能。
3、为什么使用服务网关
单体应用
浏览器发起请求到单体应用所在的机器,应用从数据库查询数据原路返回给浏览器,对单体应用来说是可以不用网关的。
微服务
微服务的应用是部署在不同机房、不同地区、不同域名下的。此时客户端想要请求对应的服务,都需要知道机器的具体IP或域名;当微服务实例众多时,对客户端来说就难以维护。此时就有了网关,客户端相关的请求直接发送到网关,由网关根据请求标识解析判断出具体的微服务网址,再把请求发送到服务实例上。
如果让客户端直接与各个微服务交互:
- 客户端会多次请求不同的微服务,增加了客户端的复杂性。
- 存在跨域请求,在一定场景下处理相对复杂。
- 身份认证问题,每个微服务需要独立身份认证。
- 难以重构,随着项目的迭代,可能需要重新划分微服务。
- 某些微服务可能使用了防火墙/浏览器不友好的协议,直接访问会有一定的困难。
因此,我们需要网关介个于客户端与服务器之间的中间层,所有外部请求率先经过微服务网关,客户端只需要与网关交互,只需要知道网关地址即可。这样便简化了开发目有以下优点:
- 易于监控,可在做服务网关收集监控数据并将其推送到外部系统进行分析。
- 易于认证,可在微服务网关上进行认证,然后再将请求转发到后端的微服务,无需在每个微服务中进行认证。
- 减少了客户端与各个微服务之间的交互次数。
4、网关解决了什么问题
网关具有身份认证与安全、审查与监控、动态路由、负载均衡、缓存、请求分片与管理、静态响应处理等功能。当然最主要的职责还是与 “外界联系”。
总结一下,网关应当具备以下功能:
- 性能:AP1高可用,负载均衡,容错机制。
- 安全:权限身份认证、脱敏,流量清洗,后端签名(保证全链路可信调用),黑名单(非法调用的限制)。
- 日志:日志记录,一且涉及分布式,全链路跟踪必不可少。
- 缓存:数据缓存。
- 监控:记录请求响应数据,API耗时分析,性能监控。
- 限流:流量控制,措峰流控,可以定义多种限流规则。
- 灰度:线上灰度部署,可以减小风险。
- 路由:动态路由规则。
5、常用网关解决方案
1. Nginx + Lua
Nginx是一个高性能的HTTP和反向代理服务器,Nginx一方面可以做反向代理,另一方面可以做静态资源服务器。Nginx是由C语言开发的,其负载均衡的实现是采用服务器实现的;它适用于服务器端负载均衡,可以整合Lua脚本。Nginx适合做门户网关,是作为整个全局的网关,对外的处于最外层的那种。但是Nginx不具备熔断、重试等功能
2. Kong
Kong是基于 Nginx + Lua 的,比Nginx提供了更简单的配置方式,数据采用了ApacheCassandra/PostgreSQL储存,并且提供了一些优秀的插件,例如验证、日志、调用频次限制等。Kong提供了大量的插件来扩展应用,通过设置不同的插件可以为服务提供各种增强的功能,但是和SpringCloud整合比较难
3. Traefik
Traefik是一个开源的GO语言开发的为了让部署微服务更加便捷而诞生的现代HTTP反向代理、负载均衡工具,它支持多种后台来自动化、动态的应用它的配置文件设置。Traefik拥有一个基于AngularJS编写的简单网站界面,支持Rest API,配置文件热更新,无需重启进程,高可用集群模式。适用于K8s。
4. Spring Cloud Netflix Zuul
Zuul是NetFlix公司开源的一个API网关组件,SpringCloud对其进行二次基于SpringBoot注解式封装做到开箱即用。目前来说结合SpringCloud提供的服务治理体系,可以做到请求转发,根据配置或者默认的路由规则进行路由和LoadBalance,无缝继承Hystrix。Zuul本身是的设计是基于 单线程的接收请求和转发处理,是阻塞IO,不支持长连接。2.0开始支持异步调用模式。
5. Spring Cloud Gateway
Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Zuul,其不仅是提供统一的路由方式,并且还基于Filter链的方式提供了网关的基本功能。它旨在提供一种简单有效的方法来路由到API,并为它们提供跨域的关注点。
6、环境准备
注册中心使用的是 ZK:127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
服务一是 demo 调用:http://localhost:1213/demo/queryOrder,返回结果为
服务二是 canal 调用:http://localhost:1212/canal/queryUser,并且服务二 queryUser 方法中跨服务调用方法一的 queryOrder 返回结果为
7、Nginx 实现 API 网关
之前的已经详细的讲解过Nginx关于反向代理、负载均衡等功能的使用,这里不再赘述。这里主要通过Nginx来实现API网关方便大家更好的学习和理解Spring Cloud Gateway的使用。
7.1、下载安装
官网:https://nginx.org/en/download.html,下载稳定版即可
下载完毕双击启动 nginx.exe,通过浏览器输入localhost网址能进入 nginx 欢迎页面,nginx 启动成功。
7.2、配置路由规则
进入Nginx
的conf
目录,打开nginx.conf
文件,配置路由规则:
http{
...
server{
listen 80;
server_name localhost;
...
#路由到canal服务
location /canal {
proxy_pass http://localhost:1212/;
}
#路由到demo服务
location /demo {
proxy_pass http://localhost:1213/;
}
...
}
...
}
任务管理器关闭 Nginx,然后双击 nginx.exe 重启 Nginx。
7.4、访问
之前我们如果要访问服务,必须由客户端指定具体服务地址访问,现在统一访问Nginx,由Nginx实现网关功能将请求路由至具体的服务。
访问:http://localhost/demo/queryOrder 结果如下
访问:http://localhost/canal/queryUser 结果如下
8、Gateway 实现 API 网关
8.1、核心概念
路由(Route): 路由是网关的基础部分,路由信息由ID、目标URL、一组断言、一组过滤器组成。如果断言路由为真,则说明请求的URL和配置匹配。
断言(Predicate): Java8中的断言函数。Spring Cloud GateWay中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud GateWay中的断言允许开发者去定义匹配来自于Http Request 中的任何信息,比如请求头和参数等。
过滤器(Filter): 一个标准的Spring Web Filter。Spring Cloud GateWay中的Filter分为两种类型,GateWay Filter 和 Global Filter。过滤器将会对请求和响应进行处理。
8.2、工作原理
如上图斯示,客户端向Spring Cloud Gateway
发出请求。再由网关处理程序Gateway Handler Mapping
映射确定与请求相匹配的路由,将其发送到网关Web处理程序Gateway Web Handler
。该处理程序通过指定的过滤器链将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器由虚线分隔的原因是,过滤器可以在发送代理请求之前和之后运行逻辑。所有Pre
过滤器逻辑均被执行。然后发出代理请求。发出代理清求后,将运行post
过滤器逻辑。
8.3、搭建网关服务
8.3、创建项目
8.3、添加依赖
<!-- gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>3.1.5</version>
</dependency>
8.3、配置文件(配置路由规则)
server.port=1997
spring.application.name=gateway-server
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址
spring.cloud.gateway.routes[0].uri=http://localhost:1212
# 断言,匹配对应的url的请求,将匹配到的请求追加到url后
spring.cloud.gateway.routes[0].predicates[0]=Path=/canal/**
8.4、启动访问
访问 http://localhost:1997/canal/queryUser,结果如下
9、路由规则
Spring Cloud GateWay 帮我们内置了很多 Predicates功能,实现了各种路由匹配规则(通过 Header、请求参数等作为条件)匹配到对应的路由。
9.1、Path(请求路径)
server.port=1997
spring.application.name=gateway-server
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址
spring.cloud.gateway.routes[0].uri=http://localhost:1212
# 断言,匹配对应的url的请求,将匹配到的请求追加到url后
spring.cloud.gateway.routes[0].predicates[0]=Path=/canal/**
- 请求路径包含/canal/,请求 http://localhost:1997/canal/queryUser 将会路由到 http://localhost:1212/canal/queryUser
9.2、Query(请求包含某参数)
server.port=1997
spring.application.name=gateway-server
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址
spring.cloud.gateway.routes[0].uri=http://localhost:1212
# 断言,匹配请求参数中包含token属性的请求
spring.cloud.gateway.routes[0].predicates[0]=Query=token
# 匹配请求参数中包含param并且参数满足正则表达式 abc. 的请求
# spring.cloud.gateway.routes[0].predicates[0]=Query=param, abc.
- 入参包含 token,请求 http://localhost:1997/canal/queryUser?token=123 将会路由到 http://localhost:1212/canal/queryUser
- 使用正则,入参包含 abc 开头,请求 http://localhost:1997/canal/queryUser?token=abc1 将会路由到 http://localhost:1212/canal/queryUser
9.3、Method(请求指定请求方式)
server.port=1997
spring.application.name=gateway-server
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址
spring.cloud.gateway.routes[0].uri=http://localhost:1212
# 断言,匹配请求参数中包含 POST 和 GET 请求
spring.cloud.gateway.routes[0].predicates[0]=Method=GET,POST
- POST 和 GET 请求,请求 http://localhost:1997/canal/queryUser 将会路由到 http://localhost:1212/canal/queryUser
9.4、Datetime(请求时间段)
server.port=1997
spring.application.name=gateway-server
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址
spring.cloud.gateway.routes[0].uri=http://localhost:1212
# 断言,时间点后匹配
spring.cloud.gateway.routes[0].predicates[0]=After=2017-01-20T17:42:47.789-07:00[America/Denver]
# 时间点前匹配
# spring.cloud.gateway.routes[0].predicates[0]=Before=2017-01-20T17:42:47.789-07:00[America/Denver]
# 时间区间匹配
# spring.cloud.gateway.routes[0].predicates[0]=Between=2017-01-20T17:42:47.789-07:00[America/Denver],2017-01-21T17:42:47.789-07:00[America/Denver]
9.5、RemoteAddr(请求远程地址)
server.port=1997
spring.application.name=gateway-server
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址
spring.cloud.gateway.routes[0].uri=http://localhost:1212
# 断言,远程地址匹配
spring.cloud.gateway.routes[0].predicates[0]=RemoteAddr=192.168.0.126/0
9.6、Header(指定Header正则匹配指定值)
server.port=1997
spring.application.name=gateway-server
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址
spring.cloud.gateway.routes[0].uri=http://localhost:1212
# 断言,Header正则匹配指定值
spring.cloud.gateway.routes[0].predicates[0]=Header=X-Request-Id, \d+
请求头 X-Request-Id 包含任意数字才可以路由到 http://localhost:1212
9.7、Cookie(指定Cookie正则匹配指定值)
server.port=1997
spring.application.name=gateway-server
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址
spring.cloud.gateway.routes[0].uri=http://localhost:1212
# 断言,指定Cookie正则匹配指定值
spring.cloud.gateway.routes[0].predicates[0]=Cookie=chocolate, ch.p
9.8、Host(请求Host匹配指定值)
server.port=1997
spring.application.name=gateway-server
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址
spring.cloud.gateway.routes[0].uri=http://localhost:1212
# 断言,请求Host匹配指定值
spring.cloud.gateway.routes[0].predicates[0]=Host=**.somehost.org,**.anotherhost.org
10、动态路由(服务发现的路由规则)
动态路由就是面向服务的路由,Spring Cloud Gateway支持与ZK整合开发,根据serviceId自动从注册中心获取服务地址并转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例时不用修改GateWay的路由配置。
10.1、添加依赖
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.8.1</version>
<exclusions>
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
<exclusion>
<artifactId>logback-core</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.2.0</version>
<exclusions>
<exclusion>
<artifactId>zookeeper</artifactId>
<groupId>org.apache.zookeeper</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<version>3.1.3</version>
<exclusions>
<exclusion>
<artifactId>zookeeper</artifactId>
<groupId>org.apache.zookeeper</groupId>
</exclusion>
<exclusion>
<artifactId>curator-framework</artifactId>
<groupId>org.apache.curator</groupId>
</exclusion>
</exclusions>
</dependency>
10.2、动态获取 URI
server.port=1997
spring.application.name=gateway-server
# Zk 配置
spring.cloud.compatibility-verifier.enabled=false
spring.cloud.zookeeper.connect-string=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址,lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
spring.cloud.gateway.routes[0].uri=lb://canal
# 断言,匹配对应的url的请求,将匹配到的请求追加到url后
spring.cloud.gateway.routes[0].predicates[0]=Path=/canal/**
请求 http://localhost:1997/canal/queryUser 将会路由到 canal 服务的地址 http://localhost:1212/canal/queryUser
10.3、服务器名称转发
上面的配置是通过服务名进行动态路由的,除此之外我们还可以结合注册中心自动进行服务名转发到具体的服务实例
server.port=1997
spring.application.name=gateway-server
# Zk 配置
spring.cloud.compatibility-verifier.enabled=false
spring.cloud.zookeeper.connect-string=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
# 路由规则
# 与服务发现组件结合,rue开启基于服务发现的路由规则
spring.cloud.gateway.discovery.locator.enabled=true
# 是否将服务名转小写
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
注意:这种转发方式使用时需要结合服务名使用,例如:
http://localhost:1997/canal/canal/queryUser
11、过滤器
Spring Cloud Gateway 根据作用范围为 GatewayFilter
和 GlobalFilter
,二者区别如下:
GatewayFilter
:网关过滤器,需要通过spring.cloud.routes.filters
配置在具体路由下,只作用在当前路由下或通过spring.cloud.default-filters
配置在全局,作用在所用路由上。GlobalFilter
:全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter
包装成GatewayFilterChain
可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。
11.1、网关过滤器 GatewayFilter
网关过滤器用于拦截并链式处理web请求,可以实现横切与应用无关的需求,例如:安全、访问超时的设置等。修改传入的HTTP请求或传出的HTTP响应。Spring Cloud GateWay 包含许多内置的网关过滤器工厂,包括头部过滤器、路径类过滤器、Hystrix过滤器、重写请求URL的过滤器,还有参数和状态码等其他类型的过滤器。根据过滤器工厂的用途划分,可划分为:header、parameter、path、body、status、session、redirect、retry、ratelimiter、hystrix。
11.1.1、Path 路径过滤器
Path 路径过滤器可以实现URL重写,通过重写URL可以实现隐藏实际路径提高安全性,易于用户记忆和键入,易于被搜索引擎收录等优点。主要有以下几种实现方式:
11.1.1.1、RewritePath GatewayFilter Factory
# 路径 /api/canal/** 重写为 /canal/**
RewritePath 网关过滤器工厂采用路径正则表达式参数和替换参数,使用java正则表达式来灵活的重写请求路径
server.port=1997
spring.application.name=gateway-server
# Zk 配置
spring.cloud.compatibility-verifier.enabled=false
spring.cloud.zookeeper.connect-string=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
# 路由规则
# 与服务发现组件结合,rue开启基于服务发现的路由规则
spring.cloud.gateway.discovery.locator.enabled=true
# 是否将服务名转小写
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址,lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
spring.cloud.gateway.routes[0].uri=lb://canal
# 断言,匹配对应的url的请求,将匹配到的请求追加到url后
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/canal/**
# Path 路径过滤器 将路径 /api/canal/** 重写为 /canl/**
spring.cloud.gateway.routes[0].filters[0]=RewritePath=/api/?(?<segment>.*), /$\{segment}
http://localhost:1997/api/canal/queryUser 路由为 http://localhost:1212/canal/queryUser
11.1.1.2、PrefixPath GatewayFilter Factory
PrefixPath 网关过滤器工厂为匹配到的URL添加指定的前缀
路径 /** 添加前缀 /canal/**
server.port=1997
spring.application.name=gateway-server
# Zk 配置
spring.cloud.compatibility-verifier.enabled=false
spring.cloud.zookeeper.connect-string=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
# 路由规则
# 与服务发现组件结合,rue开启基于服务发现的路由规则
spring.cloud.gateway.discovery.locator.enabled=true
# 是否将服务名转小写
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址,lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
spring.cloud.gateway.routes[0].uri=lb://canal
# 断言,匹配对应的url的请求,将匹配到的请求追加到url后
spring.cloud.gateway.routes[0].predicates[0]=Path=/**
# Path 路径过滤器 将路径 /** 重写为 /canl/**
spring.cloud.gateway.routes[0].filters[0]=PrefixPath=/canl
http://localhost:1997/queryUser 路由为 http://localhost:1212/canal/queryUser
11.1.1.3、StripPrefix GatewayFilter Factory
StripPrefix 网关过滤器工厂采用一个参数StripPrefix ,该参数表示将请求放松到下游前从请求中剥离路径个数,例如它的值为2,请求路径为/api/test/user/info,那么过滤后的请求路径就是 /user/info
路径/api/canal/canal分割为 /canal
server.port=1997
spring.application.name=gateway-server
# Zk 配置
spring.cloud.compatibility-verifier.enabled=false
spring.cloud.zookeeper.connect-string=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
# 路由规则
# 与服务发现组件结合,rue开启基于服务发现的路由规则
spring.cloud.gateway.discovery.locator.enabled=true
# 是否将服务名转小写
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址,lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
spring.cloud.gateway.routes[0].uri=lb://canal
# 断言,匹配对应的url的请求,将匹配到的请求追加到url后
spring.cloud.gateway.routes[0].predicates[0]=Path=/**
# Path 路径过滤器 将路径/api/canal/canal分割为 /canal
spring.cloud.gateway.routes[0].filters[0]=StripPrefix=2
http://localhost:1997/api/canal/canal/queryUser 路由为 http://localhost:1212/canal/queryUser
11.1.1.4、SetPath GatewayFilter Factory
SetPath 网关过滤器工厂采用路径模板参数,他提供了一种通过允许模板化路径段来操作请求路径的简单方法,使用Spring Framework中的uri模板,允许多个匹配段
# 路径 /api/{segment1}/{segment2} 重写为 /{segment1}/{segment2}
server.port=1997
spring.application.name=gateway-server
# Zk 配置
spring.cloud.compatibility-verifier.enabled=false
spring.cloud.zookeeper.connect-string=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
# 路由规则
# 与服务发现组件结合,rue开启基于服务发现的路由规则
spring.cloud.gateway.discovery.locator.enabled=true
# 是否将服务名转小写
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址,lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
spring.cloud.gateway.routes[0].uri=lb://canal
# 断言,匹配对应的url的请求,将匹配到的请求追加到url后
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/{segment1}/{segment2}
# Path 路径过滤器 将路径/api/canal/queryUser 重写为 /canal/queryUser
spring.cloud.gateway.routes[0].filters[0]=SetPath=/{segment1}/{segment2}
http://localhost:1997/api/canal/queryUser 路由为 http://localhost:1212/canal/queryUser
11.1.2、Parameter 参数过滤器
AddRequestParameter GatewayFilter Factory 会将指定参数添加到匹配的下游请求中
在下游请求参数中添加flag=true
server.port=1997
spring.application.name=gateway-server
# Zk 配置
spring.cloud.compatibility-verifier.enabled=false
spring.cloud.zookeeper.connect-string=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
# 路由规则
# 与服务发现组件结合,rue开启基于服务发现的路由规则
spring.cloud.gateway.discovery.locator.enabled=true
# 是否将服务名转小写
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址,lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
spring.cloud.gateway.routes[0].uri=lb://canal
# 断言,匹配对应的url的请求,将匹配到的请求追加到url后
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/{segment1}/{segment2}
# Path 路径过滤器 将路径/api/canal 重写为 /canal
spring.cloud.gateway.routes[0].filters[0]=SetPath=/{segment1}/{segment2}
# 参数过滤器,下游也就是服务增加 flag = true 参数
spring.cloud.gateway.routes[0].filters[1]=AddRequestParameter=flag, true
http://localhost:1997/api/canal/queryUser 路由为 http://localhost:1212/canal/queryUser 并获得 flag = true
11.1.3、Status状态过滤器
SetStatus GatewayFilter Factory 采用单个状态参数,它必须是有效的Spring HttpStatus。它可以是整数404或枚举类NOT_FOUND的字符串表示等
设置响应的状态
server.port=1997
spring.application.name=gateway-server
# Zk 配置
spring.cloud.compatibility-verifier.enabled=false
spring.cloud.zookeeper.connect-string=127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
# 路由规则
# 与服务发现组件结合,rue开启基于服务发现的路由规则
spring.cloud.gateway.discovery.locator.enabled=true
# 是否将服务名转小写
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址,lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
spring.cloud.gateway.routes[0].uri=lb://canal
# 断言,匹配对应的url的请求,将匹配到的请求追加到url后
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/{segment1}/{segment2}
# Path 路径过滤器 将路径/api/canal 重写为 /canal
spring.cloud.gateway.routes[0].filters[0]=SetPath=/{segment1}/{segment2}
# 参数过滤器,下游也就是服务增加 flag = true 参数
spring.cloud.gateway.routes[0].filters[1]=AddRequestParameter=flag, true
# 状态过滤器 状态修改为 520
spring.cloud.gateway.routes[0].filters[2]=SetStatus=520
http://localhost:1997/api/canal/queryUser 路由为 http://localhost:1212/canal/queryUser 并获得 flag = true 返回状态设置为 520
11.2、全局过滤器 GlobalFilter
全局过滤器不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它是请求业务以及路由的URI转换为真实业务服务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。主要包含下图的过滤器。
11.3、自定义过滤器
即使Spring Cloud Gateway
自带了许多实用的 GatewayFilter Factory、Gateway Filter、Global Filter
, 但是在很多情景下我们任然希望可以自定义自己的过滤器,实现一些我们自己的操作
11.3.1、自定义网关过滤器
自定义网关过滤器需要实现以下两个接口:GatewayFilter
、Ordered
① 创建过滤去
package com.example.gateway.yang;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class MyGatewayFilter implements GatewayFilter, Ordered {
/**
* 过滤器执行的业务
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("执行自定义网关过滤器");
return chain.filter(exchange);// 继续向下执行
}
/**
* 过滤器执行的顺序,数值越小,优先级越高
*/
@Override
public int getOrder() {
return 0;
}
}
② 注册过滤器
package com.example.gateway.yang;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 网关路由配置类
*/
@Configuration
public class GatewayServerConfig {
/**
* 注册自定义的过滤器
*/
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
// 路由 ID
return builder.routes().route("gateway-server", r -> r
// 断言(判断条件)
.path("/canal/**")
// 注册自定义网关过滤器
.filters(f -> f.filter(new MyGatewayFilter()))
// 目标 uri
.uri("lb://canal")).build();
}
}
注意:测试时需要将配置文件中网关的配置注释掉才能生效
11.3.2、自定义全局过滤器
自定义全局过滤器需要实现以下两个接口:GlobalFilter
、Ordered
。通过全局过滤器可以实现权限校验、安全性验证等功能。
和网关过滤器不同的是,全局过滤器只需要交给Spring管理即可生效。
① 创建过滤器,实现指定接口,并添加@Component注解交给Spring管理即可
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
/**
* 过滤器执行的业务
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("执行自定义全局过滤器");
return chain.filter(exchange);// 继续向下执行
}
/**
* 过滤器执行的顺序,数值约下,优先级越高
*/
@Override
public int getOrder() {
return -1;
}
}
11.3.3、统一鉴权
接下来演示通过全局管理器实现统一鉴权的案例,通过token判断用户是否登录
@Component
public class AccessFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(AccessFilter.class);
/**
* 权限验证逻辑
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = null;
// 获取请求对象
ServerHttpRequest request = exchange.getRequest();
// 获取请求头中的token
token = request.getHeaders().getFirst("token");
if (!StringUtils.hasText(token)) { // 为空就从参数中获取
token = request.getQueryParams().getFirst("token");
}
// token不存在的逻辑
if (!StringUtils.hasText(token)) {
log.error("token 不存在...");
// 响应对象
ServerHttpResponse response = exchange.getResponse();
// 设置响应信息
response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
String message = "{\"message\":\"未认证无法访问\"}";
DataBuffer buffer = response.bufferFactory().wrap(message.getBytes());
return response.writeWith(Mono.just(buffer));
}
// token 存在执行验证逻辑
log.info("验证通过");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
12、网关限流
顾名思义,限流就是限制流量,就像你宽带包有1个G的流量,用完了就没了。通过限流,我们可以很好地控制系统的QPS,从而达到保护系统的目的。
12.1、为什么需要限流
比如Wb服务、对外API,这种类型的服务有以下几种可能导致机器被拖垮:
- 用户增长过快(例如商品降价)
- 因为某个热点事件(例如微博热搜)
- 竞争对象爬虫
- 恶意的请求
这些情况都是无法预知的,不知道什么时候会有10倍甚至20倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的。
从上图可以看出,对内而言:上游的A、B服务直接依赖了下游的基础服务C,对于A、B服务都依赖的基础服务C这种场景,服务A和B其实处于某种竞争关系,如果服务A的并发阈值设置过大,当流量高峰期来临,有可能直接拖垮基础服务C并影响服务B,即雪崩效应。
12.2、限流算法
计数器算法、漏桶算法、令牌桶算法
12.2.1、计数器算法
计数器算法是限流算法中最简单的也是最容易实现的一种算法。例如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个,那么在一开始的时就设置一个计数器counter,每接收到一个请求counter就加1,如果counter大于100并且与第一个请求的时间间隔还在1分钟内,触发限流;超过一分钟则重置counter重新计数
这个算法非常简单,但是会有一个致命问题:临界问题。如果恶意用户在59秒的时候发送100个请求,并且在1分钟的时候又发送100个请求,相当于在1秒内实际发送了200个请求。用户在时间窗口重置的节点突发请求,瞬间超过了我们的速率限制,瞬间可以压垮我们的应用。如下图:
除此之外,还存在资源浪费问题,例如在30s的时候我们的请求就达到上限了,剩余的30s就处于闲置状态
12.2.2、漏桶算法
漏桶算法可以看做是注水漏水的过程。往桶中以任意速率注入水,以一定速率流出水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。
漏桶算法主要用途在于保护其他服务,假设入水量很大,而出水量较慢,则会造成网关资源堆积,可能导致网关瘫痪。而目标服务可能可以处理大量请求的,但是漏桶算法出水量缓慢反而造成服务的资源浪费。
漏桶算法无法应对突发调用。不管上面流量多大,下面流出的速度始终不变。因为处理的速度是固定的,请求进来的速度是未知的,可能突然出来很多请求,没来得及处理的请求就先放在桶里。当桶满了就会将新进来的请求丢弃掉。
漏桶算法是使用队列机制实现的
12.2.3、令牌桶算法
令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放一定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择等待可用的令牌或直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌。
Spring Cloud Gateway内部使用的就是该算法,大概猫述如下:
- 所有的请求在处理之前都需要拿到一个可用的令牌才会被处理。
- 根据限流大小,设置按照一定的速率往桶里添加令牌。
- 桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝。
- 请求到达后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接别除。
- 令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会别除令牌,以此保证足够的限流。
令牌桶算法主要目的在于保护自己,将请求压力交给目标服务处理。假设突然来了很多请求,只要拿到令牌这些请求就会瞬间被处理调用目标服务。
12.3、Getway 限流
Spring Cloud Gateway官方提供了 RequestRateLimiter GatewayFilter Factory
过滤器工厂,使用Redis
和lua
脚本实现了令牌桶的方式。
具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中。
12.3.1、添加依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
12.3.2、限流规则
配置文件
# 路由规则
# 路由ID,唯一
spring.cloud.gateway.routes[0].id=gateway-server
# 目标URL,路由到微服务的地址,lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
spring.cloud.gateway.routes[0].uri=lb://canal
# 断言,匹配对应的url的请求,将匹配到的请求追加到url后
spring.cloud.gateway.routes[0].predicates[0]=Path=/canal/**
# 限流过滤器
spring.cloud.gateway.routes[0].filters[0].name=RequestRateLimiter
# 令牌桶每秒填充的速率
spring.cloud.gateway.routes[0].filters[0].args.redis-rate-limiter.replenishRate=1
# 令牌桶容量
spring.cloud.gateway.routes[0].filters[0].args.redis-rate-limiter.burstCapacity=1
# 每个请求从桶中提取的令牌数,默认1
spring.cloud.gateway.routes[0].filters[0].args.redis-rate-limiter.requestedTokens=1
# 用于限流的键的解析器的 Bean 对象的名字,它使用 SpEL 表达式根据 #{@beanName}从 Spring 容器中获取 Bean 对象
spring.cloud.gateway.routes[0].filters[0].args.key-resolver=#{@myKeyResolver}
12.3.2.1、URI 限流
自定义限流器
package com.example.gateway.yang;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
@Configuration
public class GatewayServerConfig {
/**
* 限流规则
*/
@Bean
public KeyResolver myKeyResolver() {
// 根据请求地址限流
return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
}
}
多次请求 http://localhost:1997/canal/queryUser 会限流
12.3.2.2、参数限流
自定义限流器
package com.example.gateway.yang;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
@Configuration
public class GatewayServerConfig {
/**
* 限流规则
*/
@Bean
public KeyResolver myKeyResolver() {
// 根据指定参数限流,指定的参数必传,否则会抛出异常
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("str"));
}
}
多次请求 http://localhost:1997/canal/queryUser&str=1 会限流
12.3.2.3、IP 限流
自定义限流器
package com.example.gateway.yang;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
@Configuration
public class GatewayServerConfig {
/**
* 限流规则
*/
@Bean
public KeyResolver myKeyResolver() {
// 根据IP限流
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
多次请求 http://localhost:1997/canal/queryUser 会限流
12.4、Sentinel 限流
12.4.1、创建项目
12.4.2、添加依赖
12.4.3、限流规则配置
12.4.4、自定义异常提示
12.4.5、分组限流
13、高可用网关
13.1、Nginx + 网关集群实现高可用网关
业内通常用多少9来衡量网站的可用性,例如QQ的可用性是4个9,就是说QQ能够保证在一年里,服务在99.99%的时间是可用的,只有0.01%的时间不可用,大约最多53分钟。
对于大多数网站,2个9是基本可用;3个9是叫高可用:4个9是拥有自动恢复能力的高可用。
实现高可用的主要手段是数据的冗余备份
和服务的失效转移
,这两种手段具体可以怎么做呢,在网关里如何体现?主要有以下几个方向:
- 集群部署
- 负载均衡
- 健康检查
- 节点自动重启
- 熔断
- 服务降级
- 接口重试
13.1.1、配置网关集群
进入Nginx
的conf
目录,打开nginx.conf
文件,配置路由规则:
http{
...
#网关集群
upstream gateway{
server 127.0.0.1:1997;
server 127.0.0.1:1998;
}
server{
listen 80;
server_name localhost;
...
#路由到网关集群
location /{
proxy_pass http://gateway;
}
...
}
...
}
任务管理器关闭 Nginx,然后双击 nginx.exe 重启 Nginx。
13.1.2、总结
一个请求过来,首先经过Nginx的一层负载,到达网关,然后由网关负载到真实后端,若后端有问题,网关会进行重试访问,多次访问后仍返回失败,可以通过熔断或服务降级立即返回结果。而且,由于是负载均衡,网关重试时不一定会访问到出错的后端。
14、Spring Cloud Gateway 解决跨域问题
关于跨域的理论百度上已经有很多,网关到其他服务主要是通过注册中心去找的服务名在进行转发,所以不存在跨域,主要是解决nginx到网关的跨域问题
- 网关配置类
在网关模块注入跨域配置
@Configuration
public class GlobalCorsConfig {
/**
* 为了安全,建议只放行需要的地址(可以再yaml中定义进行映射方便扩展)
*/
private List<String> sourceCors = Arrays.asList("http://localhost:8001", "http://localhost:8002", "http://localhost:8003");
private List<String> methods = Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS");
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
// 放行原始域
if (CollectionUtils.isEmpty(sourceCors)) {
config.addAllowedOrigin("*"); // 放行所有
} else {
for (String sourceCor : sourceCors) {
config.addAllowedOrigin(sourceCor);
}
}
// 放行请求头
if (CollectionUtils.isEmpty(methods)) {
config.addAllowedHeader("*"); // 放行所有
} else {
for (String method : methods) {
config.addAllowedHeader(method);
}
}
config.setAllowCredentials(true); // 是否发送cookie
config.addAllowedMethod("*"); // 放行请求方式
config.addExposedHeader("*"); // 暴露头部信息
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
- Gateway yaml 配置
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求 allowedOrigins: “*” 允许所有网站
- "https://localhost:8001"
- "https://localhost:8002"
- "https://localhost:8003"
allowedMethods: # 允许的跨域ajax的请求方式 “*” 允许所有
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期