10.1 什么是微服务网关?
单体应用拆分成多个服务后,对外需要一个统一入口,解耦客户端与内部服务。
微服务网关是整个微服务API请求的入口,主要功能如下:
- 网关核心功能是路由转发,因此不要有耗时操作在网关上处理,让请求快速转发到后端服务上
- 网关还能做统一的熔断、限流、认证、日志监控等
10.2 为什么要有微服务网关?
上述所说的横切功能(以权限校验为例)可以写在三个位置:
1)每个服务自己实现一遍
2)写到一个公共的服务中,然后其他所有服务都依赖这个服务
3)写到服务网关的前置过滤器中,所有请求过来进行权限校验
第一种,缺点太明显,基本不用;
第二种,相较于第一点好很多,代码开发不会冗余,但是有两个缺点:
1)由于每个服务引入了这个公共服务,那么相当于在每个服务中都引入了相同的权限校验的代码,使得每个服务的jar包大小无故增加了一些,尤其是对于使用docker镜像进行部署的场景,jar越小越好;
2)由于每个服务都引入了这个公共服务,那么我们后续升级这个服务可能就比较困难,而且公共服务的功能越多,升级就越难,而且假设我们改变了公共服务中的权限校验的方式,想让所有的服务都去使用新的权限校验方式,我们就需要将之前所有的服务都重新引包,编译部署。
而服务网关恰好可以解决这样的问题:
1)将权限校验的逻辑写在网关的过滤器中,后端服务不需要关注权限校验的代码,所以服务的jar包中也不会引入权限校验的逻辑,不会增加jar包大小;
2)如果想修改权限校验的逻辑,只需要修改网关中的权限校验过滤器即可,而不需要升级所有已存在的微服务。
下面是有网关时的微服务架构图
10.3 Spring Cloud Gateway简介
Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代Netflflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控和限流。
优点:
1)性能强劲:是第一代网关Zuul的1.6倍
2)功能强大:内置了很多实用的功能,例如转发、监控、限流等
3)设计优雅,容易扩展
缺点:
1)其实现依赖Netty与WebFlux,不是传统的Servlet编程模型,学习成本高
2)不能将其部署在Tomcat、Jetty等Servlet容器里,只能打成jar包执行
3)需要Spring Boot 2.0及以上的版本,才支持
gateway与zuul的简单比较:
gateway使用的是异步请求,zuul是同步请求,gateway的数据封装在ServerWebExchange里,zuul封装在RequestContext里,同步方便调式,可以把数据封装在ThreadLocal中传递。
注意:这里需要注意一下gateway使用的netty+webflux实现,不要加入web依赖(不要引用webmvc),否则初始化会报错 ,需要加入webflux依赖。
10.4 Gateway核心概念
1、路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体,由ID、目标URI、Predicate集合、Filter集合组成。主要定义了下面的几个信息:
- id,路由标识符,区别于其他 Route。
- uri,路由指向的目的地 uri,即客户端请求最终被转发到的微服务。
- order,用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
- predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。
- fifilter,过滤器用于修改请求和响应信息。
2、断言(Predicate)
Predicate(断言, 谓词) 是Java8中引入的函数式接口,提供了断言的功能,它可以匹配Http请求中的任何内容。只有断言都返回真,才会真正的执行路由。
断言就是说: 在 什么条件下 才能进行路由转发
3、过滤器(Filter)
过滤器就是在请求的传递过程中,为请求提供前置和后置的过滤
执行流程大体如下:
1)Gateway Client向Gateway Server发送请求
2)请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
3)然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给RoutePredicateHandlerMapping
4)RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用
5)如果过断言成功,由FilteringWebHandler创建过滤器链并调用
6)请求会一次经过PreFilter–微服务-PostFilter的方法,最终返回响应
10.5 代码示例
通过网关将请求转发到payment9001
第1步:创建一个 Gateway5001 的模块,导入相关依赖
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
第2步: 创建主类
@SpringBootApplication
public class GatewayMain5001
{
public static void main(String[] args) {
SpringApplication.run(GatewayMain5001.class, args);
}
}
第3步: 添加配置文件
server:
port: 5001
spring:
application:
name: springcloud-gateway
cloud:
gateway:
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:9001 #匹配后提供服务的路由地址
predicates:
- Path=/gateway/payment/nacos/** # 断言,路径相匹配的进行路由
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
第4步: 启动项目, 并通过网关去访问微服务
改造一:上述案例在配置文件中写死了转发路径的地址, 前面我们已经分析过地址写死带来的问题, 接下来我们从 注册中心获取此地址。
第1步:加入nacos依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
第2步:在主类上添加注解
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayMain5002
{
public static void main(String[] args) {
SpringApplication.run(GatewayMain5002.class, args);
}
}
第3步:修改配置文件
server:
port: 5002
spring:
application:
name: springcloud-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:9001 #匹配后提供服务的路由地址
uri: lb://nacos-payment-provider #匹配后提供服务的路由地址
predicates:
- Path=/gateway/payment/nacos/** # 断言,路径相匹配的进行路由
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
第4步:测试
10.6 断言(Predicate)
1、内置的断言工厂
SpringCloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配。具体如下:
1)基于Datetime类型的断言工厂
此类型的断言根据时间做判断,主要有三个:
- AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期
- BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期
- BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内
-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
2)基于Cookie的断言工厂
CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求cookie是否具有给定名称且值与正则表达式匹配。
-Cookie=chocolate, ch.
3)基于Header的断言工厂
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否具有给定名称且值与正则表达式匹配。
-Header=X-Request-Id, \d+
4)基于Host的断言工厂
HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。
-Host=**.testhost.org
5)基于Method请求方法的断言工厂
MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
-Method=GET
6)基于Path请求路径的断言工厂
PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
-Path=/foo/{segment}
7)基于Query请求参数的断言工厂
QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配。
-Query=baz, ba.
8)基于路由权重的断言工厂
WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
routes:
-id: weight_route1 uri: host1 predicates:
-Path=/product/**
-Weight=group3, 1
-id: weight_route2 uri: host2 predicates:
-Path=/product/**
-Weight= group3, 9
9)基于远程地址的断言工厂 RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中
-RemoteAddr=192.168.1.1/24
可以在cmd 中使用curl发送请求
发送请求:curl http://localhost:5002/gateway/payment/nacos/1
添加cookie并发送请求:curl http://localhost:5002/gateway/payment/nacos/1 --cookie “username=yili”
添加请求头并发送请求 curl http://localhost:5002/gateway/payment/nacos/1 -H “X-Request-id:1234”
添加主机地址并发送请求 curl http://localhost:5002/gateway/payment/nacos/1 -H “Host:www.yiliedu.cn”
2、自定义断言工厂
自定义场景: 假设我们的应用仅仅让购买数量count在(0,5)之间的请求通过。
第1步:配置中增加
第2步:自定义一个断言工厂,继承 AbstractRoutePredicateFactory类,实现断言方法
@Component
public class CountRoutePredicateFactory extends AbstractRoutePredicateFactory<CountRoutePredicateFactory.Config> {
//构造函数
public CountRoutePredicateFactory() {
super(CountRoutePredicateFactory.Config.class);
}
//读取配置文件的中参数值 给他赋值到配置类中的属性上
public List<String> shortcutFieldOrder() {
//这个位置的顺序必须跟配置文件中的值的顺序对应
return Arrays.asList("minAge", "maxAge");
}
//断言逻辑
public Predicate<ServerWebExchange> apply(CountRoutePredicateFactory.Config config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//1 接收前台传入的age参数
String count = serverWebExchange.getRequest().getQueryParams().getFirst("count");
//2 先判断是否为空
if (StringUtils.isNotEmpty(count)) {
//3 如果不为空,再进行路由逻辑判断
int age = Integer.parseInt(count);
if (age < config.getMinCount() && age > config.getMinCount()) {
return true;
} else {
return false;
}
}
return false;
}
};
}
//配置类,用于接收配置文件中的对应参数
@Data
@NoArgsConstructor
public static class Config {
private int minCount;//0
private int maxCount;//5
}
//这是一个自定义的路由断言工厂类,要求有两个
//1 名字必须是 配置+RoutePredicateFactory
//2 必须继承AbstractRoutePredicateFactory<配置类>
第3步:启动测试
10.7 过滤器(Filter)
在Gateway中, Filter的生命周期只有两个:“pre” 和 “post”。
1)PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
2)POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
Gateway 的Filter从作用范围可分为两种: GatewayFilter与GlobalFilter。
3)GatewayFilter:应用到单个路由上。
4)GlobalFilter:应用到所有的路由上。
1、内置局部过滤器
在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器。具体如下:
2、自定义局部过滤器
自定义局部过滤器需要实现AbstractGatewayFilterFactory(配置类)接口
自定义局部过滤器校验请求参数中的money是否在配置范围内
/自定义局部过滤器
@Component
public class MoneyGatewayFilterFactory
extends AbstractGatewayFilterFactory<MoneyGatewayFilterFactory.Config> {
//构造函数
public MoneyGatewayFilterFactory() {
super(MoneyGatewayFilterFactory.Config.class);
}
//读取配置文件中的参数 赋值到 配置类中
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("minMoney", "maxMoney");
}
//过滤器逻辑
@Override
public GatewayFilter apply(MoneyGatewayFilterFactory.Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1 接收前台传入的money参数
String money = exchange.getRequest().getQueryParams().getFirst("money");
//2 先判断是否为空
if (StringUtils.isNotEmpty(money)) {
//3 如果不为空,再进行路由逻辑判断
int moneyValue = Integer.parseInt(money);
if (moneyValue < config.getMaxMoney() && moneyValue > config.getMinMoney()) {
System.out.println("consoleLog已经开启了....");
}
}
return chain.filter(exchange);
}
};
}
//配置类 接收配置参数
@Data
@NoArgsConstructor
public static class Config {
private int minMoney;
private int maxMoney;
}
3、内置全局过滤器
4、自定义全局过滤器
对于企业开发的一些业务功能处理,更多的还是需要我们自己编写过滤器来实现的
自定义全局过滤器需要实现GlobalFilter和Ordered接口
/**
* 统一鉴权过滤器
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
//完成判断逻辑
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
//此处省略了查询用户已缓存在redis中的token,和请求参数中的token进行比对的逻辑
//需要对访问登录认证微服务的请求放行
if (!StringUtils.equals(token, "tokenFromRedis")) {
System.out.println("鉴权失败");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
//调用chain.filter继续向下游执行
return chain.filter(exchange);
}
//顺序,数值越小,优先级越高
@Override
public int getOrder() {
return 0;
}
}
10.8 网关限流
网关作为微服务架构的统一入口,在网关层面实现限流也比较广泛。
Spring Cloud Gateway整合Sentinel 实现网关限流,有两种维度的实现。
1)route 维度:即在 Spring cloud配置文件中配置的路由条目,资源名为对应的routeId
2)自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组
两个重要的API
- GatewayFlowRule:网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
- ApiDefinition:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api,请求 path 模式为 /foo/** 和 /baz/** 的都归到 my_api 这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。
其中网关限流规则 GatewayFlowRule 的字段解释如下:
- resource:资源名称,可以是网关中的 route 名称或者用户自定义的API 分组名称。
- resourceMode:规则是针对 API Gateway 的route(RESOURCE_MODE_ROUTE_ID)还是用户在 Sentinel 中定义的API 分组(RESOURCE_MODE_CUSTOM_API_NAME),默认是route。
- grade:限流指标维度,同限流规则的grade 字段。
- count:限流阈值
- intervalSec:统计时间窗口,单位是秒,默认是1 秒(目前仅对参数限流生效)。
- controlBehavior:流量整形的控制效果,同限流规则的 controlBehavior 字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。
- burst:应对突发请求时额外允许的请求数目(目前仅对参数限流生效)。
- maxQueueingTimeoutMs:匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。
- paramItem:参数限流配置。若不提供,则代表不针对参数进行限流,该网关规则将会被转换成普通流控规则;否则会转换成热点规则。