《吃透微服务》 - 服务网关之Gateway

你们好,我是小菜。java

一个但愿可以成为 吹着牛X谈架构 的男人!若是你也想成为我想成为的人,否则点个关注作个伴,让小菜再也不孤单!web

本文主要介绍 SpringCloud之服务网关Gateway正则表达式

若有须要,能够参考算法

若有帮助,不忘 点赞spring

微信公众号已开启,小菜良记,没关注的同窗们记得关注哦!编程

前段时间与小伙伴闲聊时说到他们公司的现状,近来与未来,公司将全面把单体服务向微服务架构过渡。这里面咱们听到了关键词 --- 微服务。乍听之下,以为也很合理。互联网在不断的发展,这里不仅是行业的发展,更是系统架构的发展。如今市面上架构大体也经历了这几个阶段:数组

这是好事吗?是好事,毋庸置疑。由于不跟上时代的浪潮,总会被拍死在沙滩上。但彻底是好事吗?那也不见得。安全

咱们先要明白微服务解决了什么问题?大方面上应该就是应用层面和人层面的问题微信

  • 应用层面: 单体服务的架构很简单,项目开发和维护成本低是它无争议的优势。可是臃肿耦合又会给基础设施带来了太重的负担。若是某个应用处理资源占用了大量的CPU,就会致使其余处理资源饿死的现象,系统延迟增高,直接影响系统的可用性。
  • 人层面: 独立多语言生态 也是微服务的标签。在单体服务中,投入的人力资源越多不见得越高效,反而越容易出错。但微服务不一样,每一个服务都是独立出来的,团队能够更加容易的协做开发,甚至一套系统,多个服务,多种语言,毫无冲突。

可是咱们凡事不能被好处蒙蔽。微服务的缺点也是一直存在的:websocket

  • 须要考虑各个服务之间的容错性问题
  • 须要考虑数据一致性问题
  • 须要考虑分布式事务问题
  • ...

不少人认为微服务的核心就是在于 。服务分的越细越好,就好像平时写代码的时候丝绝不考虑的单一原则,反而在服务拆分上用到淋漓尽致。这里就须要明白一个核心概念:微服务的关键不在微,而是在于合适的大小

这句话好像很简单的样子,可是多大才算合适的大小?这可能据不一样团队,不一样项目而定。合适的大小可能取决于更少的代码仓库,更少的部署队列,更少的语言... 这里更没法一槌定音!

若是无法作到合适的大小,而无脑的拆分服务,那么微服务可能反而成为你项目的累赘。所以,有时全面转型微服务反而不是好事,你以为呢?

话题有点跑远了,我们努力扯回来。既然微服务已经成为主流,那么如何设计微服务即是咱们应该作的事,而不是谈及微服务之时,想到的只是与人如何争论如何拒用微服务。那么这篇咱们要讲的是SpringCloud之服务网关Gateway

SpringCloud之服务网关Gateway

1、认识网关

什么是服务网关?不要给本身当头一棒。咱们换个问法,为何须要服务网关?

服务网关是跨一个或多个服务节点提供单个统一的访问入口

它的做用并非无关紧要的存在,而是相当重要。咱们能够在服务网关作路由转发过滤器的实现。优势简述以下:

  • 防止内部服务关注暴露给外部客户端
  • 为咱们内部多个服务添加了额外的安全层
  • 减低微服务访问的复杂性

根据图中内容,咱们能够得出如下信息:

  • 用户访问入口,统一经过网关访问到其余微服务节点
  • 服务网关的功能有路由转发API监控权限控制限流

而这些即是 服务网关 存在的意义!

1)Zuul 比较

SpringCloud Gateway 是 SpringCloud 的一个全新项目,目标是取代Netflix Zuul。它是基于 Spring5.0 + SpringBoot2.0 + WebFlux 等技术开发的,性能高于 Zuul,据官方提供的信息来看,性能是 Zuul 的1.6倍,意在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

SpringCloud Gateway 不只提供了统一的路由方式(反向代理),而且基于 Filter 链(定义过滤器对请求过滤)提供了网关基本的功能,例如:鉴权、流量控制、熔断、路径重写、日志监控等。

其实说到 Netflix Zuul,在使用或准备使用微服务架构的小伙伴应该并不陌生,毕竟Netflix 是一个老牌微服务开源者。新秀与老牌之间的争夺,若是新秀没有点硬实力,如何让人安心转型!

这里咱们能够顺带了解一下 WefluxWebflux 的出现填补了 Spring 在响应式编程上的空白。

可能有不少小伙伴并不知道 Webflux,小菜接下来也会出一篇关于 Webflux 的讲解,实则真香!

Webflux 的响应式编程不只仅是编程风格上的改变,而是对于一系列著名的框架都提供了响应式访问的开发包,好比 NettyRedis(若是不知道 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 的意思,具体使用依具体项目而定

3、掌握网关

1. Gateway 依赖

最关键的一步即是引入网关的依赖

<!--gateway网关-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2. 项目结构

我这里简单的建立了一个微服务项目,项目里有一个 store-gateway 服务网关 和一个 store-order 订单服务。由于咱们这篇只说明服务网关的做用,不须要太多服务提供者和消费者!

store-order订单服务中只有一个控制器OrderController,里面也只有一个简单到发指的API

@RestController
@RequestMapping("order")
public class OrderController {
    
    @GetMapping("/{id:.+}")
    public String detail(@PathVariable String id) {
        return StringUtils.join("获取到了ID为", id, "的商品");
    }
    
}

咱们分别启动两个服务,而后访问订单服务的API:

结果确定是符合预期的,不至于翻车。8001 是订单服务的接口,这个时候咱们能够了解到,原来微服务架构每一个服务独立启动,都是能够独立访问的,也就至关于传统的单体服务。

咱们想一想看,若是用端口来区分每一个服务,是否也能够达到微服务的效果?理论上好像是能够的,可是若是成百上千个服务呢?端口爆炸,维护爆炸,治理爆炸... 不说别的,心态直接爆炸了!这个时候咱们就想着若是只用统一的一个端口访问各个服务,那该多好!端口一致,路由前缀不一致,经过路由前缀来区分服务,这种方式将咱们带入了服务网关的场景。是的,这里就说到了服务网关的功能之一 --- 路由转发

3. 网关出现

既然要用到网关,那咱们上面建立的服务之一 store-gateway 就派上用场了!怎么用?咱们在配置文件作个简单的修改:

spring:
  application:
    name: store-gateway
  cloud:
    gateway:
      routes: 
        - id: store-order 
          uri: http://localhost:8001 
          order: 1
          predicates:
            - Path=/store-order/** 
          filters:
            - StripPrefix=1

很少废话,咱们直接启动网关,经过访问http://localhost:8000/store-order/order/123 看是否能获取到订单?

很顺利,咱们成功拿到了ID 为 123 的订单商品!

咱们看下 URL 的组成:

可以访问到咱们的服务,说明网关配置生效了,咱们再来看下这么配置项是怎么一回事!

spring.cloud.gateway 这个是服务网关 Gateway 的配置前缀,没什么好说的,本身须要记住就是了。

routes 如下就是咱们值得关注的了,routes 是个复数形式,那么能够确定,这个配置项是个数组的形式,所以意味着咱们能够配多个路由转发,当请求知足什么条件的时候转到哪一个微服务上。

  • id: 当前路由的惟一标识
  • uri: 请求要转发到的地址
  • order:路由的优先级,数字越小级别越高
  • predicates: 路由须要知足的条件,也是个数组(这里是的关系)
  • filters: 过滤器,请求在传递过程当中能够经过过滤器对其进行必定的修改

了解完必要的参数,咱们也高高兴兴去部署使用了,可是好景不长,咱们又迎来了新的问题。我订单服务原先使用的 8001 端口,由于某些缘由给其余服务使用了,这个时候小脑壳又大了,这种状况确定不会出现 上错花轿嫁对郎 的结果!

我们想一想看这种问题要怎么解决比较合适?既然都采用微服务了,那咱们能不能采用服务名的方式跳转访问,这样子不管端口怎么变,都不会影响到咱们的正常业务!那若是采用服务的方式,就须要一个注册中心,这样子咱们启动的服务能够同步到注册中心注册表 中,这样子网关就能够根据 服务名 去注册中心中寻找服务进行路由跳转了!那我们就须要一个注册中心,这里就采用 Nacos 做为注册中心.

关于 Nacos 的了解,能够空降 微服务新秀之Nacos,看了就会,我说的!

咱们分别在服务网关 和 订单服务的配置文件都作了如下配置:

启动两个服务后,咱们能够在 Nacos 的控制台服务列表中看到两个服务:

这个时候能够看到 订单服务 的服务名为:store-order,那咱们就能够在网关配置文件部分作如下修改:

这里的配置与上述不一样点之一 http 换成了 lblb 指的是从nacos中按照名称获取微服务,并遵循负载均衡策略),之二 端口 换成了 服务名

那咱们继续访问上述URL看是否可以成功访问到订单服务:

结果依然没有翻车!这样子,无论订单服务的端口如何改变,只要咱们的服务名不变,那么始终均可以访问到咱们的对应的服务!

日子一天一天的过去~ 服务也一点一点的增长!终于有一天小菜烦了,由于每次增长服务都得去配置文件中增长一个routes 的配置列,虽然也只是 CV 的操做,可是,哪一天小菜不当心手一抖,那么~~~ 算了算了,找找看有没有什么能够偷懒的写法,终于不负小菜心,找到了一种简化版!配置以下:

就这?是的,就这!管你服务再怎么多,我配置文件都不用修改。这样子配置的目的即是请求统一方式都是变成了 网关地址:网关端口/服务名/接口名 的方式访问。再次访问页面,依然行得通!

可是方便归方便,在方便的同时也限制了不少扩展功能,所以使用需三思!不可贪懒

4、掌握核心

上面已经说完了网关的简单使用,看完的小伙伴确定已经能够上手了!接下来咱们继续趁热打铁,了解下 Gateway 网关的核心。不说别的,路由转发 确定是网关的核心!咱们从上面已经了解到一个具体路由信息载体,主要定义了如下几个信息(回忆下):

  • id: 路由的惟一标识,区别于其余Route
  • uri: 路由指向目的地 uri,即客户端请求最终被转发到的微服务
  • order: 用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高
  • predicate: 用来进行条件判断,只有断言都返回真,才会真正的执行路由
  • filter: 过滤器用于修改请求和响应信息

这里来梳理一下访问流程:

这张图很清楚的描述服务网关的调用流程(盲目自信

  1. GatewayClientGatewayServer 发出请求
  2. 请求首先会被 HttpWebHandlerAdapter 进行提取组转成网关上下文
  3. 而后网关的上下文会传递到 DispatcherHandler,它负责将请求分发给 RoutePredicateHandlerMapping
  4. RoutePredicateHandlerMapping负责路由查找,并更具路由断言判断路由是否可用
  5. 若是断言成功,由 FilteringWebHandler 建立过滤器链并调用
  6. 请求会一次通过 PreFilter ---> 微服务 ---> PostFilter 的方法,最终返回响应

过程了解了,咱们抽取一下其中的关键!断言过滤器

1. 断言

Predicate 也就是断言,主要适用于进行条件判断,只有断言都返回真,才会真正执行路由

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

该类型的断言工厂接收一个[组名,权重],而后对于同一个组内的路由按照权重转发

2)使用

这么多断言工厂,这里就不一一使用演示了,咱们结合几个断言工厂的使用演示一下。

咱们老样子很少废话,直接上代码:

CustomPredicateRouteFactory

配置文件

测试结果

success

fail

惊呼 Amazing 的同时,不要着急的往下看,咱们回归代码,看看,为何一个能够访问成功,一个却访问失败了。两个方面:1. 二者访问的URL有哪些不一样 2. 代码哪部分对 URL 作出了处理

先养成独立思考,再去看解决方法

当你思考完后,可能部分同窗已经有结果了,那让咱们继续往下看!首先是一个 CustomRoutePredicateFactory类,这个类的做用有点像拦截器,在作请求转发的时候进行了拦截,咱们请求的时候能够打个断点:

能够看到,确实是拦截器的功能,在每一个请求发起的时候作了拦截。那问题2 的结果就出来了,原来URL处理是在 RoutePredicateFactory 中作了处理,在 apply 方法中能够经过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而能够获取到请求的参数、请求方式、请求头等信息。 shortcutFieldOrder()方法也是重写的关键之一,咱们须要这里返回,咱们实体类中定义的属性,而后在apply()方法中才能接收到咱们赋值的属性参数!

注意:若是自定义的实体中有多个属性须要判断, shortcutFieldOrder()方法中的顺序要跟配置文件中的参数顺序一致

那么当咱们编写了该断言工厂后,若是让之生效?@Component 这个注解确定必不可少了,目的就是让 Spring 容器管理。那么已经注册的断言工厂如何声明使用呢?那就得回到配置文件了!

咱们这里重点看 predicates 这个配置项下的配置,分别有三个配置,一个是咱们已经熟悉的 Path ,其余两个有点陌生,可是这里再看看 Custom 是否是又有点眼熟?是的,咱们在上面好像定义了一个叫 CustomRoutePredicate 的断言工厂,二者有点类似,又好像差点什么。那我就再给你一个提示:

咱们看下抽象的断言工厂有哪些自实现的类!其中是否是有 PathRoutePredicateFactory,没错,就是你想的那样!有没有一种拨开雨雾见青天的感受!原来咱们配置文件的 key 是以类名的前缀声明的,也就是说断言工厂类的格式必须是: 自定义名称+ RoutePredicateFactory 为后缀,而后在配置文件中声明。这样子触类旁通,咱们天然而然的就清楚了 - Before 的做用,该做用即是:限制请求时间在 xxx 以前

- Custom=cbuc,这个 cbuc 即是咱们限制的规则,只有 name 为 cbuc 的用户才能请求成功。若是有多个参数,能够用 , 隔开,顺序须要与断言工厂中shortcutFieldOrder() 返回参数的顺序一致!

若是在自定义断言工厂的途中遇到了什么阻碍,否则看看内置的断言工厂是如何实现的。 多看源码总没错!
2. 过滤器

接下来进入第二个核心,也就是过滤器。该核心的做用也挺简单,就是在请求的传递过程当中,对请求和响应作一系列的手脚。为了怕你划回去看请求流程过于麻烦,小菜贴心的再贴一遍流程图:

Gateway 的过滤器中又能够分为 局部过滤器全局过滤器。听名称就知道其做用,局部 是用于某一个路由上的,全局 是用于全部路由上的。不过无论是 局部 仍是 全局,生命周期都分为 prepost

  • pre: 做用于路由到微服务以前调用。咱们能够利用这种过滤器实现身份验证、在集群中选择请求的微服务,记录调试记录等
  • post: 做用于路由到微服务以后执行。咱们能够利用这种过滤器用来响应添加标准的 HTTP Header,收集统计信息和指标、将响应从微服务发送到客户端。
1)局部过滤器

局部过滤器是针对于单个路由的过滤器。一样 Gateway 已经内置了许多过滤器

咱们选几种经常使用的过滤器进行说明:(下列过滤器省略后缀 GaewayFilterFactory,完整名称为 前缀+后缀)

过滤器前缀 做用 参数
StripPrefix 用于截断原始请求的路径 使用数字表示要截断的路径数量
AddRequestHeader 为原始请求添加 Header Header 的名称及值
AddRequestParameter 为原始请求添加请求参数 参数名称及值
Retry 针对不一样的响应进行重试 reties、statuses、methods、series
RequestSize 设置容许接收最大请求包的大小 请求包大小,单位字节,默认5M
SetPath 修改原始请求的路径 修改后的路径
RewritePath 重写原始的请求路径 原始路径正则表达式以及重写后路径的正则表达式
PrefixPath 为原始请求路径添加前缀 前缀路径
RequestRateLimiter 对请求限流,限流算法为令牌桶 KeyResolver、reteLimiter、statusCode、denyEmptyKey
内置的过滤器小伙伴们能够本身尝试一番,有问题欢迎提问!

咱们接下来说讲如何自定义过滤器工厂。don't say so much,咱们上代码

CustomGatewayFilterFactory

配置文件

当咱们开启请求计数的时候,能够看到控制台对于请求次数做了统计:

所以咱们能够经过这种方式轻松实现局部过滤器

2)全局过滤器

全局过滤器做用于全部路由,无需配置。经过全局过滤器能够实现对权限的统一校验,安全性验证等功能

老样子,咱们先看看 Gateway 中存在哪些全局过滤器:

相对于局部过滤器,全局过滤器的命名就没有太多约束了,毕竟不须要在配置文件中进行配置。

咱们熟悉一下经典的全局过滤器

过滤器名称 做用
ForwardPathFilter / ForwardRoutingFilter 路径转发相关过滤器
LoadBalanceerClientFilter 负载均衡客户端相关过滤器
NettyRoutingFilter / NettyWriteResponseFilter Http 客户端相关过滤器
RouteToRequestUrlFilter 路由 URL 相关过滤器
WebClientHttpRoutingFilter / WebClientWriteResponseFilter 请求 WebClient 客户端转发请求真实的URL并将响应写入到当前的请求响应中
WebsocketRoutingFilter websocket 相关过滤器

了解完内置的过滤器,咱们再看看如何定义全局的过滤器!

CustomerGlobalFilter

对于全局过滤器,咱们不须要在配置文件中配置,由于是做用于全部路由

测试结果

success

fail

能够看到,咱们使用全局过滤器进行了鉴权处理,若是没有携带 token 将没法访问!


到这里咱们已经了解到了服务网关的路由转发,权限校验甚至于能够基于断言和过滤器作出粗略简单的 API监控和限流

但其实对于 API监控限流,SpringCloud 中已经有了更好的组件完成这两项工做。毕竟单一原则,作的越多每每错的也越多!

后面会继续整理关于 SpringCloud 组件的文章,敬请关注!

对于微服务的框架,孰好孰坏由咱们本身断定。可是无论孰好孰坏,面对一门新技术的产生,咱们最须要作的即是接收它,包容它,而后用好它,是骡子是马,本身溜溜就知道了。

不要空谈,不要贪懒,和小菜一块儿作个吹着牛X作架构的程序猿吧~点个关注作个伴,让小菜再也不孤单。我们下文见!

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一块儿变强的男人。 💋

微信公众号已开启,小菜良记,没关注的同窗们记得关注哦!