SpringCloud微服务网关概述
1、微服务网关概述
1.1、为什么要用微服务网关?
在学习完前面的知识后,微服务架构已经初具雏形。但还有一些问题:不同的微服务一般会有不同的网络地址,客户端在访问这些微服务时必须记住几十甚至几百个地址,这对于客户端方来说太复杂也难以维护。如下图:
如果让客户端直接与各个微服务通讯,可能会有很多问题:
- 客户端会请求多个不同的服务,需要维护不同的请求地址,增加开发难度
- 在某些场景下存在跨域请求的问题
- 加大身份认证的难度,每个微服务需要独立认证
因此,我们需要一个微服务网关,介于客户端与服务器之间的中间层,所有的外部请求都会先经过微服务网关。客户端只需要与网关交互,只知道一个网关地址即可,这样简化了开发还有以下优点:
- 易于监控
- 易于认证
- 减少了客户端与各个微服务之间的交互次数
1.2、什么是微服务网关?
API网关是一个服务器,是系统对外的唯一入口。API网关封装了系统内部架构,为每一个客户端提供了一个定制的API。API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常网关也是提供REST/HTTP的访问API接口。服务端通过API-GW注册和管理服务。
1.3、网关的作用及应用场景
网关具有的职责:
- 身份验证
- 监控
- 负载均衡(基于Ribbon)
- 缓存
- 请求分片与管理
- 静态响应处理
当然最主要的还是与外界联系。
1.4、常见的API网关实现方式
Kong
- 基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等)可以开箱即用。
- 问题:只支持HTTP协议,不易于二次开发,自由扩展困难,提供管理API,缺乏更易用的管控和配置方式。
Zuul
- Netflix开源,功能丰富,使用JAVA开发,易于二次开发;需要运行在web容器中,如Tomcat。
- 问题:缺乏管控,无法动态配置,依赖组件较多,处理HTTP请求依赖的是Web容器,性能不如Nginx。
Traefik
- Go语言开发,轻量易用,提供大多数的功能,服务路由,负载均衡等等,提供WebUI界面
- 问题:二进制文件部署,二次开发难度较大,UI更多的是监控,缺乏配置和管理能力。
SpringCloud Gateway
- SpringCloud提供的网关服务。
Nginx+lua
- 使用Nginx的反向代理和负载均衡可实现对API服务器的负载均衡及高可用。
- 问题:自身注册的问题和网关本身的扩展性。
2、基于Nginx的网关实现
2.1、Nginx介绍
2.2、Nginx正向代理
正向代理,“它代理的是客户端,代客户端发出请求”,是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端必须要进行一些特别的设置才能使用正向代理。
2.3、Nginx反向代理
多个客户端给服务器发送的请求,Nginx服务器接收到之后,按照一定的规则分发给了后端的业务处理服务器进行处理了。此时请求的来源也就是客户端是明确的,但是请求具体由哪台服务器处理并不明确,Nginx扮演的就是一个反向代理角色。客户端是无感知代理的存在的,反向代理对外都是透明的,访问者并不知道自己访问的是一个代理。因为客户端不需要任何配置就可以访问。反向代理,“它代理的是服务端,代服务端接收请求”,主要用于服务器集群分布式部署的情况下,反向代理隐藏了服务器的信息。
如果只是单纯的需要一个最基础的具备转发功能的网关,那么使用Ngnix是一个不错的选择。
2.4、准备工作
- 启动 ebuy-product 微服务,单独请求地址:http://127.0.0.1:9011/
- 启动 ebuy-order 微服务,单独请求地址: http://127.0.0.1:9013/
配置Nginx的请求转发
#ebuy-product服务代理
location /api-product {
proxy_pass http://127.0.0.1:9011/;
}
#ebuy-order服务代理
location /api-order {
proxy_pass http://127.0.0.1:9013/;
}
打开一个DOS窗口,切换至ngnix安装目录,输入命令start nginx
测试略!后续着重讲解Zuul和Gateway!
3、基于微服务网关Zuul的实现
3.1、Zuul简介
Zuul是Netflix开源的微服务网关,它可以和Eureka、Ribbon、Hystrix等组件配合使用,Zuul组件的核心是一系列的过滤器,这些过滤器可以完成一下功能:
- 动态路由: 动态的将请求路由分配到后端服务集群中
- 压力测试: 逐渐增加指向集群的流量,以了解性能
- 负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求
- 静态响应处理: 边缘位置进行响应,避免转发到内部集群
- 身份验证和安全: 识别每一个资源的验证要求,并拒绝哪些不符合的请求。SpringCloud 对Zuul进行了整合和增强
3.2、搭建Zuul网关服务器模块
(1)在ebuy-parent模块下,新建ebuy-zuul模块,并添加依赖
<!--eureka服务注册-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--Zuul网关支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
(2)编写启动类
@SpringBootApplication
@EnableZuulProxy //开启Zuul的网关功能
@EnableDiscoveryClient
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class,args);
}
}
(3)编写配置application.yml
server:
port: 9090 #端口
spring:
application:
name: ebuy-zuul #服务名称
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6001/eureka/,http://127.0.0.1:6002/eureka/
instance:
prefer-ip-address: true # 使用ip地址註冊
logging:
level:
cn.ebuy: DEBUG
(4)Zuul 中的路由转发
最直观的理解:“路由”是指根据请求URL,将请求分配到对应的处理程序。在微服务体系中,Zuul负责接收所有的请求。根据不同的URL匹配规则,将不同的请求转发到不同的微服务处理。
zuul:
routes:
ebuy-service-product: #这里是路由id,随意写
path: /ebuy-service-product/** #这里是映射路径
url: http://localhost:6501 #映射路径对应的实际url地址
sensitiveHeaders:
只需要在 application.yml文件中配置路由规则即可:
- ebuy-service-product :配置路由id,可以随意取名
- url :映射路径对应的实际url地址
- path :配置映射路径,这里将所有请求前缀为/product-service/的请求,转发到http://127.0.0.1:6501 处理
- sensitiveHeaders:默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取消默认的黑名单,如果设置了具体的头信息则不会传到下游服务
配置好Zuul路由之后启动服务。
注意:启动Zuul服务之前,先启动Eureka注册中心和商品服务ebuy-service-product并注册到注册中心,访问地址http://localhost:6001/
(5)访问地址http://localhost:9090/ebuy-service-product/product/738388
3.3、面向服务的路由
微服务一般是由几十、上百个服务组成,对于一个URL请求,最终会确认一个服务实例进行处理。如果对每个服务实例手动指定一个唯一访问地址,然后根据URL去手动实现请求匹配,这样做显然就不合理。
Zuul支持与Eureka整合开发,根据ServiceID自动的从注册中心中获取服务地址并转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例的时候不用修改Zuul的路由配置。
(1)修改application.yml文件,通过服务名获取对应的所有服务列表(集群)
ebuy-service-product:
ribbon:
ConnectTimeout: 2500 # Ribbon的连接超时时间(创建连接时间:毫秒)
ReadTimeout: 5000 # Ribbon的数据读取超时时间 (得到数据的时间)
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数 (1表示不重试自己)
#配置路由规则
zuul:
routes:
ebuy-service-product: # 这里是路由id,随意写
path: /ebuy-service-product/** # 这里是映射路径
serviceId: ebuy-service-product #配置转发的微服务名称
- serviceId: 指定需要转发的微服务实例名称
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。
(2)测试
依次启动 Eureka注册中心6001、6002
,商品微服务两台6501、6502
,Zuul API网关9090
,在浏览器上通过访问 http://localhost:9090/ebuy-service-product/product/738388
查看最终效果。
第一次访问:
第二次访问:
3.4、简化网关Zuul的路由配置
在上述的配置中,我们使用的规则:
- < route >: 是自定义的路由名id(一般会和服务名保持一致)
- path:指定映射路径
- erviceId:来指定服务名
而大多数情况下,我们的 路由名称往往和服务名会写成一样的。因此Zuul就提供了一种简化的配置语法,如下:
ebuy-service-product:
ribbon:
ConnectTimeout: 2500 # Ribbon的连接超时时间(创建连接时间:毫秒)
ReadTimeout: 5000 # Ribbon的数据读取超时时间 (得到数据的时间)
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数 (1表示不重试自己)
ebuy-service-order:
ribbon:
ConnectTimeout: 2500 # Ribbon的连接超时时间(创建连接时间:毫秒)
ReadTimeout: 5000 # Ribbon的数据读取超时时间 (得到数据的时间)
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数 (1表示不重试自己)
#配置网关路由()
zuul:
routes:
ebuy-service-product: /ebuy-service-product/** #商品微服务
ebuy-service-order: /euby-service-order/** #订单微服务
在使用Zuul的过程中,上面讲述的规则已经大大的简化了配置项。但是当服务较多时,配置也是比较繁琐的。因此Zuul就指定了默认的路由规则:
- 服务名:ebuy-service-product
- 映射路径:/ebuy-service-product/**
3.5、Zuul加入后的微服务架构
3.6、Zuul 中的过滤器
通过之前的学习,我们得知 Zuul它包含了两个核心功能:对请求的路由和过滤。其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础;而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。其实,路由功能在真正运行时,它的路由映射和请求转发同样也由几个不同的过滤器完成的。所以,过滤器可以说是Zuul实现API网关功能最为核心的部件,每一个进入Zuul的HTTP请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。
那么接下来,我们重点学习的就是Zuul的第二个核心功能:过滤器。
3.7、Zuul Filter简介
Zuul 中的过滤器跟我们之前使用的 javax.servlet.Filter 不一样,javax.servlet.Filter 只有一种类型,可以通过配置 urlPatterns 来拦截对应的请求。而 Zuul 中的过滤器总共有 4 种类型,且每种类型都有对应的使用场景。
- PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
- ERROR:在其他阶段发生错误时执行该过滤器。
Zuul提供了自定义过滤器的功能实现起来也十分简单,只需要编写一个类去实现zuul提供的接口。
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 来自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
ZuulFilter 是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法
shouldFilter :返回一个 Boolean 值,判断该过滤器是否需要执行。返回true执行,返回false
不执行。
- run :过滤器的具体业务逻辑。
- filterType :返回字符串,代表过滤器的类型。包含以下4种:
- pre :请求在被路由之前执行
- routing :在路由请求时调用
- post :在routing和errror过滤器之后调用
- error :处理请求时发生错误调用
- filterOrder :通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。
3.8、Zuul过滤器生命周期
正常流程:
- 请求到达首先会经过 pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
异常流程:
- 整个过程中, pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
- 如果是 error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
- 如果是 POST过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求
不会再到达POST过滤器了。
不同过滤器的场景:
- 请求鉴权:一般放在 pre类型,如果发现没有访问权限,直接就拦截了
- 异常处理:一般会在 error类型和post类型过滤器中结合来处理。
- 服务调用时长统计: pre和post结合使用。
3.9、Zuul网关存在的问题
在实际使用中我们会发现直接使用Zuul会存在诸多问题,包括:
性能问题:
- Zuul1x 版本本质上就是一个同步Servlet,采用多线程阻塞模型进行请求转发。简单讲,每来一个请求,Servlet容器要为该请求分配一个线程专门负责处理这个请求,直到响应返回客户端这个线程才会被释放返回容器线程池。如果后台服务调用比较耗时,那么这个线程就会被阻塞,阻塞期间线程资源被占用,不能干其它事情。我们知道Servlet容器线程池的大小是有限制的,当前端请求量大,而后台慢服务比较多时,很容易耗尽容器线程池内的线程,造成容器无法接受新的请求。
不支持任何长连接:
- 如 websocket
4、Zuul网关的替换方案GateWay
Zuul 1.x 是一个基于阻塞 IO 的 API Gateway 以及 Servlet;直到 2018 年 5 月,Zuul 2.x(基于Netty,也是非阻塞的,支持长连接)才发布,但 Spring Cloud 暂时还没有整合计划。Spring CloudGateway 比 Zuul 1.x 系列的性能和功能整体要好。
4.1、Gateway简介
Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式,统一访问接口。SpringCloud Gateway 作为 Spring Cloud 生态系中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/埋点,限流等。它是基于Nttey的响应式开发模式。
下表为Spring Cloud Gateway与Zuul的性能对比,从结果可知,Spring Cloud Gateway的RPS是Zuul的1.6倍
组件 | RPS(request per second) |
---|---|
Spring Cloud Gateway | Requests/sec: 32213.38 |
Zuul1X | Requests/sec: 20800.13 |
4.2、核心概念
- 路由(route): 路由是网关最基础的部分,路由信息由一个ID、一个目的URL、一组断言工厂和一组Filter组成。如果断言为真,则说明请求URL和配置的路由匹配。
- 断言(predicates): Java8中的断言函数,Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自Http Request中的任何信息,比如请求头和参数等。
- 过滤器(filter): 一个标准的Spring webFilter,Spring Cloud Gateway中的Filter分为两种类型,分别是Gateway Filter和Global Filter。过滤器Filter可以对请求和响应进行处理。、
4.3、搭建gateway工程案例
(1)引入pom依赖
在ebuy-gateway引入gateway依赖,注意 SpringCloud Gateway使用的web框架为webflux,和SpringMVC不兼容
,所以不可以引入spring-boot-starter-web
依赖,否者启动会报错,引入的限流组件是hystrix。redis底层不再使用jedis,而是lettuce。
<!--gateway网关启动器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<!--引入eureka客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
(2) 配置启动类
@SpringBootApplication
public class GatewayServerApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class, args);
}
}
( 3) 编写配置文件
server:
port: 9092 #服务端口
spring:
application:
name: ebuy-gateway #指定网关服务名
cloud:
gateway:
routes:
- id: ebuy-service-product #此处的id名可以自定义
uri: http://127.0.0.1:6501 #此处指定路由地址
predicates:
- Path=/product/** #开放6501服务下的product API接口
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6001/eureka/,http://127.0.0.1:6002/eureka/
instance:
instance-id: ${spring.cloud.client.ip-address}:${server.port}
prefer-ip-address: true #使用ip地址注册(在注册中心显示名字以ip地址显示)
lease-expiration-duration-in-seconds: 10 #eureka client发送心跳给eureka server服务端后,续约到期时间(默认为90秒)
lease-renewal-interval-in-seconds: 5 #发送心跳续约时间间隔
#打印日志
logging:
level:
com.ebuy: DEBUG
- 注意缩进!!!
- id :我们自定义的路由 ID,保持唯一
- uri :目标服务地址
- predicates :路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
- filters :过滤规则,暂时未使用。
上面这段配置的意思是,配置了一个id为ebuy-service-product的路由规则,当访问网关请求地址API接口以product 开头时,会自动转发到地址:http://127.0.0.1:6501
。配置完成启动项目即可在浏览器访问进行测试,当我们访问地址 http://localhost:9092/product/738388
时会展示页面展示如下:
4.4、路由规则
Spring Cloud Gateway 的功能很强大,前面我们只是使用了 predicates 进行了简单的条件匹配,其实Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。在 Spring Cloud Gateway 中 Spring 利用Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。
实例:
#路由断言之后匹配
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://xxxx.com
#路由断言之前匹配
predicates:
- After=xxxxx
#路由断言之前匹配
spring:
cloud:
gateway:
routes:
- id: before_route
uri: https://xxxxxx.com
predicates:
- Before=xxxxxxx
#路由断言之间
spring:
cloud:
gateway:
routes:
- id: between_route
uri: https://xxxx.com
predicates:
- Between=xxxx,xxxx
#路由断言Cookie匹配,此predicate匹配给定名称(chocolate)和正则表达式(ch.p)
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: https://xxxx.com
predicates:
- Cookie=chocolate, ch.p
#路由断言Header匹配,header名称匹配X-Request-Id,且正则表达式匹配\d+
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://xxxx.com
predicates:
- Header=X-Request-Id, \d+
#路由断言匹配Host匹配,匹配下面Host主机列表,**代表可变参数
spring:
cloud:
gateway:
routes:
- id: host_route
uri: https://xxxx.com
predicates:
- Host=**.somehost.org,**.anotherhost.org
#路由断言Method匹配,匹配的是请求的HTTP方法
spring:
cloud:
gateway:
routes:
- id: method_route
uri: https://xxxx.com
predicates:
- Method=GET
#路由断言匹配,{segment}为可变参数
spring:
cloud:
gateway:
routes:
- id: host_route
uri: https://xxxx.com
predicates:
- Path=/foo/{segment},/bar/{segment}
#路由断言Query匹配,将请求的参数param(baz)进行匹配,也可以进行regexp正则表达式匹配 (参数包含foo,并且foo的值匹配ba.)
spring:
cloud:
gateway:
routes:
- id: query_route
uri: https://xxxx.com
predicates:
- Query=baz 或 Query=foo,ba.
#路由断言RemoteAddr匹配,将匹配192.168.1.1~192.168.1.254之间的ip地址,其中24为子网掩码位数即255.255.255.0
spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: https://example.org
predicates:
- RemoteAddr=192.168.1.1/24
4.5、动态路由
和zuul网关类似,在SpringCloud GateWay中也支持动态路由:即自动的从注册中心中获取服务列表并访问。
上述的写法过于死板了,只能针对一个服务的一个端口及API接口,做集群比较麻烦,使用服务名实现动态调用。
(1)配置动态路由修改 application.yml 配置文件,添修改访问映射的URL为服务名称
gateway:
routes:
- id: ebuy-service-product
uri: lb://ebuy-service-product #此处指定服务地址(可自动做集群)
predicates:
- Path=/ebuy-service-product/** #开放ebuy-service-product下所有端口的服务,因为从注册中心获取ip后,就是从更路径访问请求的
- uri:以 lb: //开头(lb代表从注册中心获取服务),后面接的就是你需要转发到的服务名称
重新启动网关,这个时候我们在浏览器访问 http://localhost:9092/ebuy-service-product/product/738388
,会抛出404
。这是由于路由转发规则默认转发到商品微服务( http://localhost:9092/ebuy-service-product/product/738388)路径上,而商品微服务又没有 ebuy-service-product
对应的API
映射配置,所以会报错,在不使用重写转发路径时如何解决上述问题,直接将Path后的服务名删除,从根路径开始请求服务中所有的API映射接口:
predicates:
- Path=/** #从映射到的微服务根路径开始请求
重启gateway服务,再次请求http://localhost:9092/product/738388
,成功响应:
4.6、重写转发路径
在SpringCloud Gateway中,路由转发是直接将匹配的路由path直接拼接到映射路径(URI)之后,那么在微服务开发中往往没有那么便利。这里就可以通过RewritePath机制来进行路径重写。
#过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等
gateway:
routes:
- id: ebuy-service-product #商品微服务
uri: lb://ebuy-service-product
predicates:
- Path=/ebuy-service-product/** #此处要加上服务名
filters:
- RewritePath=/ebuy-service-product/(?<segment>.*), /$\{segment}
- id: ebuy-service-order #订单微服务
uri: lb://ebuy-service-order
predicates:
- Path=/ebuy-service-order/**
filters:
- RewritePath=/ebuy-service-order/(?<segment>.*), /$\{segment}
通过 RewritePath配置重写转发的url,将/ebuy-service-product/(?.*),重写为{segment},然后转发到商品微服务。比如在网页上请求 http://localhost:9092/ebuy-service-product/product/738388
( 值得注意的是在 yml文档中 $ 要写成 $\ )
4.7、过滤器
pring Cloud Gateway除了具备请求路由功能之外,也支持对请求的过滤。通过Zuul网关类似,也是通过过滤器的形式来实现的。那么接下来我们一起来研究一下Gateway中的过滤器。
(1)过滤器的生命周期
Spring Cloud Gateway 的 Filter 的生命周期不像 Zuul 的那么丰富,它只有两个:“pre” 和 “post”:
- PRE : 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- POST :这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
(2) 过滤器类型
Spring Cloud Gateway 的 Filter 从作用范围可分为另外两种GatewayFilter 与 GlobalFilter。
- GatewayFilter :应用到单个路由或者一个分组的路由上。
- GlobalFilter :应用到所有的路由上。
4.7.1、全局过滤器(GlobalFilter)
全局过滤器(GlobalFilter)作用于所有路由,Spring Cloud Gateway 定义了Global Filter接口,用户可以自定义实现自己的Global Filter。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能,并且全局过滤器也是程序员使用比较多的过滤器。
Spring Cloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
4.8、统一鉴权
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。
4.8.1、鉴权逻辑
开发中的鉴权逻辑:
- 当客户端第一次请求服务时,服务端对用户进行信息认证(登录);
- 认证通过,将用户信息进行加密形成 token,返回给客户端,作为登录凭证;
- 以后每次请求,客户端都携带认证的 token;
- 服务端对 token进行解密,判断是否有效。
如上图,对于验证用户是否已经登录鉴权的过程可以在网关层统一检验。检验的标准就是请求中是否携带token凭证以及token的正确性。
4.8.1、代码实现
引入相关依赖:
<!--gateway网关启动器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<!--引入eureka客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--token凭证令牌-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
下面的我们自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑。
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取请求对象
ServerHttpRequest request = exchange.getRequest();
//2.获取响应对象
ServerHttpResponse response = exchange.getResponse();
//3.判断当前的请求是否为登录请求,如果是,则直接放行
if (request.getURI().getPath().contains("/user/login")||request.getURI().getPath().contains("/user/toLogin")||request.getURI().getPath().contains("/user/index")){
//放行
return chain.filter(exchange);
}
//4.获取当前的所有请求头信息
HttpHeaders headers = request.getHeaders();
//5.获取jwt令牌信息
String token=request.getQueryParams().getFirst("token");
if(StringUtils.isEmpty(token)){
//如果不存在,则向客户端返回错误提示信息, UNAUTHORIZED= 401错误,没有权限
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
else {
//6 如果令牌合法,则放行
return chain.filter(exchange);
}
}
@Override
public int getOrder() {
return 0;
}
}
- 自定义全局过滤器需要实现 GlobalFilter和Ordered接口。
- 在 filter方法中完成过滤器的逻辑判断处理
- 在 getOrder方法指定此过滤器的优先级,返回值越大级别越低
- ServerWebExchange 就相当于当前请求和响应的上下文,存放着重要的请求-响应属性、请求实例和响应实例等等。一个请求中的request,response都可以通过 ServerWebExchange 获取
- 调用 chain.filter 继续向下游执行
重新启动ebuy-gateway网关服务,访问地址http://localhost:9091/ebuy-service-product/product/679532
:
当带上参数token,随便一个值就行,上述我们只是模拟用户请求带上token凭证,其实真正的token凭证是在通过用户登录的用户名和密码是否存在,授权中心为该用户颁发的token凭证,并且会存储在本地浏览器中,有一定的时效期:
访问地址:localhost:9091/ebuy-service-product/product/679532?token=111
4.9、网关限流
4.9.1、常见的限流算法
(1) 计数器
计数器限流算法是最简单的一种限流实现方式。其本质是通过维护一个单位时间内的计数器,每次请求计数器加1,当单位时间内计数器累加到大于设定的阈值,则之后的请求都被拒绝,直到单位时间已经过去,再将计数器重置为零。
上述算法啥意思呢?举个栗子
也就是说在一个时间段内,值允许请求多少次。
例如:从10:01-10:02这一分钟内允许10个用户请求,
那当在这一分钟内的前10秒内,10个请求名额已经用完了,
那么后50秒就不能再请求服务了,你说这能中不,肯定不能满足真实的场景需求。
( 2) 漏桶算法
漏桶算法可以很好地限制容量池的大小,从而防止流量暴增。漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。 在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。
为了更好的控制流量,漏桶算法需要通过两个变量进行控制:一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。
( 3) 令牌桶算法
令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
4.9.2、基于Filter的限流
SpringCloudGateway官方就提供了基于令牌桶
的限流支持。基于其内置的过滤器工厂
RequestRateLimiterGatewayFilterFactory
实现。在过滤器工厂中是通过Redis
和lua
脚本结合的方式进行流量控制。
(1) 环境搭建
导入 redis的依赖,首先在工程的pom文件中引入gateway的起步依赖和redis的reactive依赖,代码如下:
<!--gateway网关启动器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--redis的reactive依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifatId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
(2) 修改application.yml配置文件
在application.yml配置文件中加入限流的配置,代码如下:
server:
port: 9091 #服务端口
spring:
application:
name: ebuy-gateway #指定网关服务名
cloud:
gateway:
routes:
- id: ebuy-service-product
uri: lb://ebuy-service-product
predicates:
- Path=/ebuy-service-product/**
filters:
- name: RequestRateLimiter #请求数限流 名字不能随便写
args:
key-resolver: "#{@pathKeyResolver}"
#key-resolver: "#{@ipKeyResolver}"
#key-resolver: "#{@userKeyResolver}"
redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
- RewritePath=/ebuy-service-product/(?<segment>.*), /$\{segment}
redis:
host: localhost
port: 6379
password: 123456
在 application.yml 中添加了redis的信息,并配置了RequestRateLimiter的限流过滤器:
- burstCapacity :令牌桶总容量。
- replenishRate :令牌桶每秒填充平均速率。
- key-resolver :用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据
#{@beanName}
从 Spring 容器中获取 Bean 对象。
(3)配置KeyResolverConfiguration工具类
为了达到不同的限流效果和规则,可以通过实现 KeyResolver 接口,定义不同请求类型的限流键。
首先是基于请求路径的限流:
@Configuration
public class KeyResolverConfiguration {
/**
* 基于请求路径的限流
*/
@Bean
public KeyResolver pathKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getPath().toString()
);
}
/**
* 基于请求ip地址的限流
*/
//@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getHeaders().getFirst("X-Forwarded-For")
);
}
/**
* 基于用户的限流
*/
//@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getQueryParams().getFirst("user")
);
}
}
通过reids的MONITOR可以监听redis的执行过程。这时候Redis中会有对应的数据:
大括号中就是我们的限流 Key,这边是IP,本地的就是localhost
- timestamp: 存储的是当前时间的秒数,也就是System.currentTimeMillis() / 1000或者
Instant.now().getEpochSecond() - tokens: 存储的是当前这秒钟的对应的可用的令牌数量
Spring Cloud Gateway目前提供的限流还是相对比较简单的,在实际中我们的限流策略会有很多种情况,比如:
- 对不同接口的限流
- 被限流后的友好提示
这些可以通过自定义RedisRateLimiter来实现自己的限流策略,这里我们先不做讨论。
4.9.3、基于Sentinel的限流
Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。
从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:
- route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId
- 自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组
Sentinel 1.6.0 引入了 Sentinel API Gateway Adapter Common 模块,此模块中包含网关限流的规则和自定义 API 的实体和管理逻辑:
- GatewayFlowRule :网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
- ApiDefinition :用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api ,请求 path 模式为 /foo/** 和 /baz/** 的都归到 my_api 这个 API分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。
(1)环境搭建
在gateway的pom文件中,导入Sentinel 的响应依赖:
<!--Sentinel限流的响应依赖-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.6.3</version>
</dependency>
( 2) 编写网关配置类GatewayConfiguration
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
/**
* 配置限流过滤器
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* 配置初始化的限流参数(网关限流规则)
* 当然也可以在此处添加多个限流资源
*/
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(
new GatewayFlowRule("ebuy-service-product") //资源名称
.setCount(1) // 限流阈值,一秒访问一次
.setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
);
GatewayRuleManager.loadRules(rules);
}
}
- 基于 Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的
SentinelGatewayFilter
实例以及SentinelGatewayBlockExceptionHandler
实例即可。 @PostConstruct
定义初始化的加载方法,用于指定资源的限流规则。这里资源的名称为 ebuy-service-product,统计时间是1秒内,限流阈值是1。表示每秒只能访问一个请求。
(3)application.yml文件配置,恢复到原来的filter配置
server:
port: 9091 #服务端口
spring:
application:
name: ebuy-gateway #指定网关服务名
cloud:
gateway:
routes:
- id: ebuy-service-product
uri: lb://ebuy-service-product
predicates:
- Path=/ebuy-service-product/**
filters:
- RewritePath=/ebuy-service-product/(?<segment>.*), /$\{segment}
redis:
host: localhost
port: 6379
password: 123456
( 4)自定义异常提示处理方法
/**
* 对限流的资源做后续处理(响应一个友好的提示信息)
*/
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange
serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 001);
map.put("message", "对不起,接口限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
在没有配置限流处理器方法时,当触发限流后页面显示的是Blocked by Sentinel: FlowException
。为了展示更加友好的限流提示,Sentinel支持自定义异常处理。
您可以在 GatewayCallbackManager 注册回调进行定制:
- setBlockHandler :注册函数用于实现自定义的逻辑处理被限流的请求,对应接口为
BlockRequestHandler
。默认实现为DefaultBlockRequestHandler
,当被限流时会返回类似于下面的错误信息:Blocked by Sentinel: FlowException
。
( 5) 指定参数限流
上面的配置是针对整个路由来限流的,如果我们只想对某个路由的参数做限流,那么可以使用参数限流方式:
rules.add(new GatewayFlowRule("ebuy-service-product")
.setCount(1)
.setIntervalSec(1)
.setParamItem(new GatewayParamFlowItem()
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM).setFi
eldName("id")
)
);
通过指定参数:
PARAM_PARSE_STRATEGY_URL_PARAM
表示从url中获取参数;setFieldName
指定参数名称
(6) 自定义API分组
/**
* 自定义限流资源組(可选)
*/
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
/**
* 链式书写
*/
ApiDefinition api1 = new ApiDefinition("ebuy-api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/ebuy-service-product/product/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}}).setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/ebuy-service-order/order/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(api1);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
@PostConstruct
private void initCustomizedApis2() {
Set<ApiDefinition> definitions = new HashSet<>();
/**
* 分组书写
*/
ApiDefinition api1 = new ApiDefinition("ebuy-api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/ebuy-service-product/product/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("ebuy-api-2")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/ebuy-service-order/order/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
4.10、网关高可用
高可用HA(High Availability)
是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。我们都知道,单点是系统高可用的大敌,单点往往是系统高可用最大的风险和敌人,应该尽量在系统设计的过程中避免单点。方法论上,高可用保证的原则是“集群化”,或者叫“冗余”:只有一个单点,挂了服务会受影响;如果有冗余备份,挂了还有其他backup能够顶上。
我们实际使用 Spring Cloud Gateway 的方式如上图,不同的客户端使用不同的负载将请求分发到后端的 Gateway,Gateway 再通过HTTP调用后端服务,最后对外输出。因此为了保证 Gateway 的高可用性,前端可以同时启动多个 Gateway 实例进行负载,在 Gateway 的前端使用 Nginx 或者 F5 进行负载转发以达到高可用性。
(1) 准备两个ebuy-gateway工程,9091
和9092
server:
port: 909* #服务端口 9091、9092
spring:
application:
name: ebuy-gateway #指定网关服务名
cloud:
gateway:
routes:
- id: ebuy-service-product
uri: lb://ebuy-service-product
predicates:
- Path=/ebuy-service-product/**
filters:
- RewritePath=/ebuy-service-product/(?<segment>.*), /$\{segment}
- id: ebuy-service-order
uri: lb://ebuy-service-order
predicates:
- Path=/ebuy-service-order/**
filters:
- RewritePath=/ebuy-service-order/(?<segment>.*), /$\{segment}
redis:
host: localhost
port: 6379
password: 123456
#eureka注册中心
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6001/eureka/,http://127.0.0.1:6002/eureka/
instance:
instance-id: ${spring.cloud.client.ip-address}:${server.port}
prefer-ip-address: true #使用ip地址注册(在注册中心显示名字以ip地址显示)
lease-expiration-duration-in-seconds: 10 #eureka client发送心跳给eureka server服务端后,续约到期时间(默认为90秒)
lease-renewal-interval-in-seconds: 5 #发送心跳续约时间间隔
#打印日志
logging:
level:
com.ebuy: DEBUG
(2) 配置Ngnix
找到ngnix.conf配置文件添加负载均衡配置:
#配置多台服务器(这里只在一台服务器上的不同端口)
upstream gateway {
server 127.0.0.1:9091;
server 127.0.0.1:9092;
}
#gateway网关路由
server{
listen 80;
server_name localhost;
location / {
proxy_pass http://gateway;
}
}
在浏览器上通过访问http://localhost/ebuy-service-product/product/1816753
请求的效果和之前是一样的。这次关闭一台网关服务器,还是可以支持部分请求的访问。
(3) 执行流程分析
Spring Cloud Gateway
核心处理流程如上图所示,Gateway的客户端向 Spring Cloud Gateway 发送请求,请求首先被 HttpWebHandlerAdapter
进行提取组装成网关上下文,然后网关的上下文会传递到 DispatcherHandler
。 DispatcherHandler 是所有请求的分发处理器, DispatcherHandler 主要负责分发请求对应的处理器。比如请求分发到对应的 RoutePredicateHandlerMapping (路由断言处理映射器)
。路由断言处理映射器主要作用用于路由查找,以及找到路由后返回对应的FilterWebHandler
。 FilterWebHandler 主要负责组装Filter链并调用Filter执行一系列的Filter处理,然后再把请求转到后端对应的代理服务处理,处理完毕之后将Response返回到Gateway客户端。