至关重要!万字带你实现网关GateWay的简单使用
0. 环境
- nacos版本:1.4.1
- Spring Cloud : 2020.0.2
- Spring Boot :2.4.4
- Spring Cloud alibaba: 2.2.5.RELEASE
1. 认识网关
什么是服务网关?不要给自己当头一棒。我们换个问法,为什么需要服务网关?
服务网关是跨一个或多个服务节点提供单个统一的访问入口
它的作用并不是可有可无的存在,而是至关重要。我们可以在服务网关做路由转发和过滤器的实现。优点简述如下:
- 防止内部服务关注暴露给外部客户端
- 为我们内部多个服务添加了额外的安全层
- 减低微服务访问的复杂性
根据图中内容,我们可以得出以下信息:
- 用户访问入口,统一通过网关访问到其他微服务节点
- 服务网关的功能有路由转发,API监控、权限控制、限流
而这些便是 服务网关 存在的意义!
1.1 Zuul 比较
SpringCloud Gateway 是 SpringCloud 的一个全新项目,目标是取代Netflix Zuul。它是基于 Spring5.0 + SpringBoot2.0 + WebFlux 等技术开发的,性能高于 Zuul,据官方提供的信息来看,性能是 Zuul 的1.6倍,意在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
SpringCloud Gateway 不仅提供了统一的路由方式(反向代理),并且基于 Filter 链(定义过滤器对请求过滤)提供了网关基本的功能,例如:鉴权、流量控制、熔断、路径重写、日志监控等。
其实说到 Netflix Zuul,在使用或准备使用微服务架构的小伙伴应该并不陌生,毕竟Netflix 是一个老牌微服务开源者。新秀与老牌之间的争夺,如果新秀没有点硬实力,如何让人安心转型!
这里我们可以顺带了解一下 Weflux,Webflux 的出现填补了 Spring 在响应式编程上的空白。
可能有很多小伙伴并不知道 Webflux,小菜接下来也会出一篇关于 Webflux 的讲解,实则真香!
Webflux 的响应式编程不仅仅是编程风格上的改变,而是对于一系列著名的框架都提供了响应式访问的开发包,比如 Netty、Redis(如果不知道 Netty 的实力,可以想想为什么 Nginx 可以承载那么大的并发,底层就是基于Netty)
那么说这么多,跟 Zuul 有什么关系呢?我们可以看下 Zuul 的 IO 模型
SpringCloud 中所集成的 Zuul 版本,采用的是 Tomcat 容器,使用的是传统的 Servlet IO 处理模型。Servlet 是由 Servlet Container 管理生命周期的。
但问题就在于 Servlet 是一个简单的网络IO模型,当请求进入到 ServletContainer就会为其绑定一个线程,在并发不高的场景下这种模型是没有问题的,但是一旦并发上来了,线程数量就会增加。那导致的问题就是频繁进行上下文切换,内存消耗严重,处理请求的时间就会变长。正所谓牵一发而动全身!
而 SpriingCloud Zuul 便是基于 servlet 之上的一个阻塞式处理模型,即Spring实现了处理所有 request 请求的一个 servlet(DispatcherServlet),并由该 Servlet 阻塞式处理。虽然 SpringCloud Zuul 2.0 开始,也是用了 Netty 作为并发IO框架,但是 SpringCloud 官方已经没有集成该版本的计划!
注:这里没有推崇 Gateway 的意思,具体使用依具体项目而定
2. 掌握网关
2.1 Gateway 测试环境
最关键的一步便是引入网关的依赖
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-loadbalancer
复制代码
我这里简单地创建了一个微服务项目,项目里有一个 nacos-gateway-7700 服务网关 和一个 nacos-provider-7300 服务。因为我们这篇只说明服务网关的作用,不需要太多服务提供者和消费者!
在nacos-provider-7300订单服务中只有一个控制器OrderController,里面也只有一个简单到发指的API
@RequestMapping("/provider/depart")
@RestController
public class DepartController {
@Autowired
private DepartService service;
// 服务发现客户端
@Autowired
private DiscoveryClient client;
@GetMapping("/get/{id}")
public Depart getHandle(@PathVariable("id") int id, HttpServletRequest request) {
String remoteAddr = request.getRemoteAddr();
int remotePort = request.getRemotePort();
System.out.println("remoteAddr = " + remoteAddr);
System.out.println("remotePort = " + remotePort);
return service.getDepartById(id);
}
}
复制代码
我们分别启动两个服务,然后访问提供者服务的API:
结果肯定是符合预期的,不至于翻车。7300 是提供者服务的接口,这个时候我们可以了解到,原来微服务架构每个服务独立启动,都是可以独立访问的,也就相当于传统的单体服务。
我们想想看,如果用端口来区分每个服务,是否也可以达到微服务的效果?理论上好像是可以的,但是如果成百上千个服务呢?端口爆炸,维护爆炸,治理爆炸... 不说别的,心态直接爆炸了!这个时候我们就想着如果只用统一的一个端口访问各个服务,那该多好!端口一致,路由前缀不一致,通过路由前缀来区分服务,这种方式将我们带入了服务网关的场景。是的,这里就说到了服务网关的功能之一 --- 路由转发。
2.2 网关出现
既然要用到网关,那我们上面创建的服务之一 nacos-gateway-7700 就派上用场了!怎么用?我们在配置文件做个简单的修改:
server:
port: 7700
spring:
application:
name: nacos-gateway
cloud:
# 配置路由,可以配置多个
gateway:
routes:
- id: depart-provider # id 自定义路由的id
uri: localhost:7300 # uri就是 目标服务地址
order: 1
predicates: # 断言,也就是路由条件 ,这里使用了path作为路由条件
- Path=/depart-provider/**
filters:
- StripPrefix=1 # 转发之前去掉一层路径
复制代码
不多废话,我们直接启动网关,通过访问
http://localhost:7700/depart-provider/provider/depart/get/1 看是否能获取到订单?
我们看下 URL 的组成:
能够访问到我们的服务,说明网关配置生效了,我们再来看下这么配置项是怎么一回事!
spring.cloud.gateway 这个是服务网关 Gateway 的配置前缀,没什么好说的,自己需要记住就是了。
routes 以下就是我们值得关注的了,routes 是个复数形式,那么可以肯定,这个配置项是个数组的形式,因此意味着我们可以配多个路由转发,当请求满足什么条件的时候转到哪个微服务上。
- id: 当前路由的唯一标识
- uri: 请求要转发到的地址
- order:路由的优先级,数字越小级别越高
- predicates: 路由需要满足的条件,也是个数组(这里是或的关系)
- filters: 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
了解完必要的参数,我们也高高兴兴去部署使用了,但是好景不长,我们又迎来了新的问题。我订单服务原先使用的 7300 端口,因为某些原因给其他服务使用了,这个时候小脑袋又大了,这种情况肯定不会出现 上错花轿嫁对郎 的结果!
咱们想想看这种问题要怎么解决比较合适?既然都采用微服务了,那我们能不能采用服务名的方式跳转访问,这样子无论端口怎么变,都不会影响到我们的正常业务!那如果采用服务的方式,就需要一个注册中心,这样子我们启动的服务可以同步到注册中心的 注册表 中,这样子网关就可以根据 服务名 去注册中心中寻找服务进行路由跳转了!那咱们就需要一个注册中心,这里就采用 Nacos 作为注册中心.
关于 Nacos 的了解,可以详见Nacos注册中心文章
我们分别在服务网关 和 提供者服务的配置文件都做了以下配置:
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
复制代码
启动两个服务后,我们可以在 Nacos 的控制台服务列表中看到两个服务:
这个时候可以看到 提供者服务 的服务名为:nacos-provider,那我们就可以在网关配置文件部分做以下修改:
server:
port: 7700
spring:
application:
name: nacos-gateway
cloud:
# 配置路由,可以配置多个
gateway:
routes:
- id: depart-provider # id 自定义路由的id
# uri: localhost:7300 # uri就是 目标服务地址
uri: lb://nacos-provider-depart # 微服务模式
order: 1
predicates: # 断言,也就是路由条件 ,这里使用了path作为路由条件
- Path=/depart-provider/**
filters:
- StripPrefix=1 # 转发之前去掉一层路径
复制代码
这里的配置与上述不同点之一 http 换成了 lb(lb 指的是从nacos中按照名称获取微服务,并遵循负载均衡策略),之二 端口 换成了 服务名
那我们继续访问上述URL看是否能够成功访问到订单服务:
结果依然没有翻车!这样子,不管订单服务的端口如何改变,只要我们的服务名不变,那么始终都可以访问到我们的对应的服务!
3. 掌握核心
上面已经说完了网关的简单使用,看完的小伙伴肯定已经可以上手了!接下来我们继续趁热打铁,了解下 Gateway 网关的核心。不说别的,路由转发 肯定是网关的核心!我们从上面已经了解到一个具体路由信息载体,主要定义了以下几个信息(回忆下):
- id: 路由的唯一标识,区别于其他Route
- uri: 路由指向目的地 uri,即客户端请求最终被转发到的微服务
- order: 用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高
- predicate: 用来进行条件判断,只有断言都返回真,才会真正的执行路由
- filter: 过滤器用于修改请求和响应信息
这里来梳理一下访问流程:
这张图很清楚的描述服务网关的调用流程(盲目自信)
- GatewayClient 向 GatewayServer 发出请求
- 请求首先会被 HttpWebHandlerAdapter 进行提取组转成网关上下文
- 然后网关的上下文会传递到 DispatcherHandler,它负责将请求分发给 RoutePredicateHandlerMapping
- RoutePredicateHandlerMapping负责路由查找,并更具路由断言判断路由是否可用
- 如果断言成功,由 FilteringWebHandler 创建过滤器链并调用
- 请求会一次经过 PreFilter ---> 微服务 ---> PostFilter 的方法,最终返回响应
过程了解了,我们抽取一下其中的关键!断言 和 过滤器
3.1 断言
Predicate 也就是断言,主要适用于进行条件判断,只有断言都返回真,才会真正执行路由
3.1.1 断言工厂
SpringCloud Gateway 中内置了许多断言工厂,所有的这些断言都和 HTTP 请求不同的属性相匹配,具体如下;
- 基于 Datetime 类型的断言工厂
- 该类型的断言工厂是根据时间做判断的
- AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期
- BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期
- BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内
- 基于远程地址的断言工厂 RemoteAddrRoutePredicateFactory
- 该类型的断言工厂是接收一个参数,IP 地址端,判断请求主机地址是否在地址段中。(eq:-RemoteAddr=192.168.1.1/24)
- 基于Cookie的断言工厂 CookieRoutePredicateFactory
- 该类型的断言工厂接收两个参数,Cookie 名字和一个正则表达式,判断请求 cookie 是否具有给定名称且值与正则表达式匹配。(eq:-Cookie=cbuc)
- 基于Header的断言工厂HeaderRoutePredicateFactory
- 该类型的断言工厂接收两个参数,标题名称和正则表达式。判断请求 Header 是否具有给定名称且值与正则表达式匹配。(eq:-Header=X-Request)
- 基于Host的断言工厂 HostRoutePredicateFactory
- 该类型的断言工厂接收一个参数,主机名模式。判断请求的host 是否满足匹配规则。(eq:-Host=**.cbuc.cn)
- 基于Method请求方法的断言工厂 MethodRoutePredicateFactory
- 该类型的断言工厂接收一个参数,判断请求类型是否跟指定的类型匹配。(eq:-Method=GET)
- 基于Path请求路径的断言工厂 PathRoutePredicateFactory
- 该类型的断言工厂接收一个参数,判断请求的URI部分是否满足路径规则。(-eq:-Path=/order/)
- 基于Query请求参数的断言工厂 QueryRoutePredicateFactory
- 该类型的断言工厂接收两个参数,请求 Param 和 正则表达式。判断请求参数是否具有给定名称且值与正则表达式匹配。(eq:-Query=cbuc)
- 基于路由权重的断言工厂 WeightRoutePredicateFactory
- 该类型的断言工厂接收一个[组名,权重],然后对于同一个组内的路由按照权重转发
3.1.2 使用
这么多断言工厂,这里就不一一使用演示了,我们结合几个断言工厂的使用演示一下。
我们老样子不多废话,直接上代码:
自定义
CustomPredicateRouteFactory
@Component
public class CustomRoutePredicateFactory extends AbstractRoutePredicateFactory {
private static final String CUSTOM_KEY = "name";
public CustomRoutePredicateFactory() {
super(User.class);
}
@Override
public Predicate apply(User config) {
return serverWebExchange -> {
String param = serverWebExchange.getRequest().getQueryParams().getFirst(CUSTOM_KEY);
if (StringUtils.isNotBlank(param) && StringUtils.equals(param, config.getName())) {
return true;
}
return false;
};
}
@Override
public List shortcutFieldOrder() {
return Collections.singletonList(CUSTOM_KEY);
}
@Data
public static class User {
private String name;
}
}
复制代码
配置文件
server:
port: 7700
spring:
application:
name: nacos-gateway
cloud:
# 配置路由,可以配置多个
gateway:
routes:
- id: depart-provider # id 自定义路由的id
# uri: localhost:7300 # uri就是 目标服务地址
uri: lb://nacos-provider-depart # 微服务模式
order: 1
predicates: # 断言,也就是路由条件 ,这里使用了path作为路由条件
- Path=/depart-provider/**
- Custom=hsfxuebao
filters:
- StripPrefix=1 # 转发之前去掉一层路径
复制代码
测试结果
success
fail
惊呼 Amazing 的同时,不要着急的往下看,我们回归代码,看看,为什么一个可以访问成功,一个却访问失败了。两个方面:1. 两者访问的URL有哪些不同 2. 代码哪部分对 URL 做出了处理
先养成独立思考,再去看解决方法
当你思考完后,可能部分同学已经有结果了,那让我们继续往下看!首先是一个
CustomRoutePredicateFactory类,这个类的作用有点像拦截器,在做请求转发的时候进行了拦截,我们请求的时候可以打个断点:
可以看到,确实是拦截器的功能,在每个请求发起的时候做了拦截。那问题2 的结果就出来了,原来URL处理是在 RoutePredicateFactory 中做了处理,在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息。 shortcutFieldOrder()方法也是重写的关键之一,我们需要这里返回,我们实体类中定义的属性,然后在apply()方法中才能接收到我们赋值的属性参数!
注意:如果自定义的实体中有多个属性需要判断,shortcutFieldOrder()方法中的顺序要跟配置文件中的参数顺序一致
那么当我们编写了该断言工厂后,如果让之生效?@Component 这个注解肯定必不可少了,目的就是让 Spring 容器管理。那么已经注册的断言工厂如何声明使用呢?那就得回到配置文件了!
我们这里重点看 predicates 这个配置项下的配置,分别有2个配置,一个是我们已经熟悉的 Path ,其他有点陌生,但是这里再看看 Custom 是不是又有点眼熟?是的,我们在上面好像定义了一个叫 CustomRoutePredicate 的断言工厂,两者有点相似,又好像差点什么。那我就再给你一个提示:
我们看下抽象的断言工厂有哪些自实现的类!其中是不是有 PathRoutePredicateFactory,没错,就是你想的那样!有没有一种拨开雨雾见青天的感觉!原来我们配置文件的 key 是以类名的前缀声明的,也就是说断言工厂类的格式必须是: 自定义名称+ RoutePredicateFactory 为后缀,然后在配置文件中声明。这样子举一反三,我们自然而然的就清楚了 - Before 的作用,该作用便是:限制请求时间在 xxx 之前。
而 - Custom=cbuc,这个 cbuc 便是我们限制的规则,只有 name 为 hsfxuebao 的用户才能请求成功。如果有多个参数,可以用 , 隔开,顺序需要与断言工厂中shortcutFieldOrder() 返回参数的顺序一致!
如果在自定义断言工厂的途中遇到了什么阻碍,不然看看内置的断言工厂是如何实现的。多看源码总没错!
3.2 过滤器
接下来进入第二个核心,也就是过滤器。该核心的作用也挺简单,就是在请求的传递过程中,对请求和响应做一系列的手脚。为了怕你划回去看请求流程过于麻烦,小菜贴心的再贴一遍流程图:
在 Gateway 的过滤器中又可以分为 局部过滤器 和 全局过滤器。听名称就知道其作用,局部 是用于某一个路由上的,全局 是用于所有路由上的。不过不管是 局部 还是 全局,生命周期都分为 pre 和 post。
- pre: 作用于路由到微服务之前调用。我们可以利用这种过滤器实现身份验证、在集群中选择请求的微服务,记录调试记录等
- post: 作用于路由到微服务之后执行。我们可以利用这种过滤器用来响应添加标准的 HTTP Header,收集统计信息和指标、将响应从微服务发送到客户端。
3.2.1 局部过滤器
局部过滤器是针对于单个路由的过滤器。同样 Gateway 已经内置了许多过滤器
我们选几种常用的过滤器进行说明:(下列过滤器省略后缀 GaewayFilterFactory,完整名称为 前缀+后缀)
过滤器前缀 | 作用 | 参数 |
StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断的路径数量 |
AddRequestHeader | 为原始请求添加 Header | Header 的名称及值 |
AddRequestParameter | 为原始请求添加请求参数 | 参数名称及值 |
Retry | 针对不同的响应进行重试 | reties、statuses、methods、series |
RequestSize | 设置允许接收最大请求包的大小 | 请求包大小,单位字节,默认5M |
SetPath | 修改原始请求的路径 | 修改后的路径 |
RewritePath | 重写原始的请求路径 | 原始路径正则表达式以及重写后路径的正则表达式 |
PrefixPath | 为原始请求路径添加前缀 | 前缀路径 |
RequestRateLimiter | 对请求限流,限流算法为令牌桶 | KeyResolver、reteLimiter、statusCode、denyEmptyKey |
内置的过滤器小伙伴们可以自己尝试一番,有问题欢迎提问!
3.2.2 全局过滤器
全局过滤器作用于所有路由,无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能
老样子,我们先看看 Gateway 中存在哪些全局过滤器:
相对于局部过滤器,全局过滤器的命名就没有太多约束了,毕竟不需要在配置文件中进行配置。
我们熟悉一下经典的全局过滤器
过滤器名称 | 作用 |
ForwardPathFilter / ForwardRoutingFilter | 路径转发相关过滤器 |
LoadBalanceerClientFilter | 负载均衡客户端相关过滤器 |
NettyRoutingFilter / NettyWriteResponseFilter | Http 客户端相关过滤器 |
RouteToRequestUrlFilter | 路由 URL 相关过滤器 |
WebClientHttpRoutingFilter / WebClientWriteResponseFilter | 请求 WebClient 客户端转发请求真实的URL并将响应写入到当前的请求响应中 |
WebsocketRoutingFilter | websocket 相关过滤器 |
到这里我们已经了解到了服务网关的路由转发,权限校验甚至于可以基于断言和过滤器做出粗略简单的 API监控和限流。
但其实对于 API监控和 限流,SpringCloud 中已经有了更好的组件完成这两项工作。毕竟单一原则,做得越多往往错的也越多!
原文:
https://juejin.cn/post/7170933034248732703