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

3 篇文章 0 订阅
2 篇文章 0 订阅

大家好,我是小菜。

一个希望能够成为 吹着牛X谈架构 的男人!如果你也想成为我想成为的人,不然点个关注做个伴,让小菜不再孤单!

本文主要介绍 SpringCloud之服务网关Gateway

如有需要,可以参考

如有帮助,不忘 点赞

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

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

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

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

  • 应用层面: 单体服务的架构很简单,项目开发和维护成本低是它无争议的优点。但是臃肿耦合又会给基础设施带来了过重的负担。如果某个应用处理资源占用了大量的CPU,就会导致其他处理资源饿死的现象,系统延迟增高,直接影响系统的可用性。

  • 人层面:独立多语言生态 也是微服务的标签。在单体服务中,投入的人力资源越多不见得越高效,反而越容易出错。但微服务不同,每个服务都是独立出来的,团队可以更加容易的协作开发,甚至一套系统,多个服务,多种语言,毫无冲突。

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

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

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

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

如果没法做到合适的大小,而无脑的拆分服务,那么微服务可能反而成为你项目的累赘。因此,有时全面转型微服务反而不是好事,你觉得呢?

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

SpringCloud之服务网关Gateway

一、认识网关

什么是服务网关?不要给自己当头一棒。我们换个问法,为什么需要服务网关?

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

它的作用并不是可有可无的存在,而是至关重要。我们可以在服务网关做路由转发过滤器的实现。优点简述如下:

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

根据图中内容,我们可以得出以下信息:

  • 用户访问入口,统一通过网关访问到其他微服务节点
  • 服务网关的功能有路由转发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 的意思,具体使用依具体项目而定

三、掌握网关
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 的操作,但是,哪一天小菜不小心手一抖,那么~~~ 算了算了,找找看有没有什么可以偷懒的写法,终于不负小菜心,找到了一种简化版!配置如下:

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

但是方便归方便,在方便的同时也限制了很多扩展功能,因此使用需三思!不可贪懒

四、掌握核心

上面已经说完了网关的简单使用,看完的小伙伴肯定已经可以上手了!接下来我们继续趁热打铁,了解下 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 类型的断言工厂

该类型的断言工厂是根据时间做判断的

1、AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期

2、**BeforeRoutePredicateFactory:**接收一个日期参数,判断请求日期是否早于指定日期

3、**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为原始请求添加 HeaderHeader 的名称及值
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 / NettyWriteResponseFilterHttp 客户端相关过滤器
RouteToRequestUrlFilter路由 URL 相关过滤器
WebClientHttpRoutingFilter / WebClientWriteResponseFilter请求 WebClient 客户端转发请求真实的URL并将响应写入到当前的请求响应中
WebsocketRoutingFilterwebsocket 相关过滤器

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

CustomerGlobalFilter

对于全局过滤器,我们不需要在配置文件中配置,因为是作用于所有路由

测试结果

success

fail

可以看到,我们使用全局过滤器进行了鉴权处理,如果没有携带 token 将无法访问!


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

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

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

对于微服务的框架,孰好孰坏由我们自己判定。但是不管孰好孰坏,面对一门新技术的产生,我们最需要做的便是接收它,包容它,然后用好它,是骡子是马,自己溜溜就知道了。

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

看完不赞,都是坏蛋

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

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

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

ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它是基于散列的数据结构,可以支持并发的读和更新操作。它相比于普通的HashMap,在多线程环境下具有更好的性能和可靠性。 ConcurrentHashMap的主要特点如下: 1. 线程安全:ConcurrentHashMap使用了一种细粒度的锁机制来保证线程安全,允许多个线程同时进行读操作,而对写操作进行了分段加锁,不会阻塞其他读操作。 2. 分段锁:ConcurrentHashMap内部将数据分成多个段(Segment),每个段都维护了一个独立的哈希表。不同的线程可以同时访问不同的段,从而提高并发性能。 3. 原子性操作:ConcurrentHashMap提供了一些原子性的操作方法,如putIfAbsent()、remove()和replace()等。这些方法可以保证在操作期间没有其他线程对数据进行修改。 4. 无需加锁的读操作:ConcurrentHashMap允许多个线程同时进行并发读操作,因为读操作不会产生冲突,不需要加锁。 5. 高效性能:ConcurrentHashMap在大多数情况下具有比Hashtable和同步的HashMap更好的并发性能。 要充分理解和使用ConcurrentHashMap,你可以关注以下几点: - 理解其线程安全机制和锁的细粒度设计,避免出现死锁和竞态条件等问题。 - 确保在多线程环境下使用正确的并发控制方法,如使用原子性操作方法而不是先检查后执行的方式。 - 注意ConcurrentHashMap的迭代器可能不是强一致性的,如果需要强一致性的结果,可以考虑使用其他手段来处理。 - 根据具体的使用场景和需求,选择合适的并发度和初始容量。 希望以上信息对你有帮助!如果你还有其他问题,请继续提问。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值