当前位置:首页 > 技术文章 > 正文内容

至关重要!万字带你实现网关GateWay的简单使用

arlanguage3个月前 (01-31)技术文章27

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 是一个老牌微服务开源者。新秀与老牌之间的争夺,如果新秀没有点硬实力,如何让人安心转型!

这里我们可以顺带了解一下 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 的意思,具体使用依具体项目而定

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: 过滤器用于修改请求和响应信息

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

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

  1. GatewayClientGatewayServer 发出请求
  2. 请求首先会被 HttpWebHandlerAdapter 进行提取组转成网关上下文
  3. 然后网关的上下文会传递到 DispatcherHandler,它负责将请求分发给 RoutePredicateHandlerMapping
  4. RoutePredicateHandlerMapping负责路由查找,并更具路由断言判断路由是否可用
  5. 如果断言成功,由 FilteringWebHandler 创建过滤器链并调用
  6. 请求会一次经过 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

扫描二维码推送至手机访问。

版权声明:本文由AR编程网发布,如需转载请注明出处。

本文链接:http://www.arlanguage.com/post/1892.html

标签: nginx 截断
分享给朋友:

“至关重要!万字带你实现网关GateWay的简单使用” 的相关文章

Nginx热升级流程,看这篇就够了

在之前做过 Nginx 热升级的演示,他能保证nginx在不停止服务的情况下更换他的 binary 文件,这个功能非常有用,但我们在执行 Nginx 的 binary 文件升级过程中,还是会遇到很多问题,比如老的 worker 进程一直退不掉或者新的 worker 进程升级以后出现问题需要考虑回滚,...

Nginx配置七层负载均衡

Nginx 一般用于七层负载均衡,其吞吐量有一定限制。为了提升系统整体吞吐量,会在 DNS 与 Nginx 之间引入接入层,比如使用LVS(软负载均衡器)、F5(硬负载均衡器)做四层负载均衡。整体的请求流转如下图所示,即首先 DNS 解析到 LVS/F5,然后 LVS/F5 转发给 Nginx,再由...

Java 加密解密和数字签名

在做项目中,只要涉及敏感信息,或者对安全有一定要求的场景,都需要对数据进行加密。在Java中原生API即可实现对称加密与非对称加密,并支持常用的加密算法。对称加密对称加密使用单钥完成加解密,加密和解密采用相同的密钥。对称加密的速度快,常用于大量数据进行加密。主流的算法有:AES,3DES。生成3DE...

Linux下如何用nginx+ffmpeg搭建流媒体服务器

安装ffmpeg安装过程略安装完成后,检查是否安装成功。比如我这里采用向pili推流的方式,将本地的一个mp4视频推流到七牛pili。ffmpeg -re -i /tmp/ffmpeg_test.mp4 -vcodec copy -acodec copy -f flv "rtmp://pil...

nginx 解决 readv() failed (104: Connection reset by peer)

查看nginx error错误,发现上传接口报以下错:2019/10/10 19:58:25 [error] 299784#0: *5967188 readv() failed (104: Connection reset by peer) while reading upstream, clien...

Nginx实战-监控nginx.conf配置文件,配置文件修改自动重启nginx

1.1 实现目标在学习或者进行nginx测试的时候,耗费在 nginx -s reload/stop 上的命令时间很多,修改任意内容都需要重新启动或者停止启动,基本上状态就是在下面5个状态间来回切换vim nginx.conf修改nginx.conf保存nginx.conf重启nginx刷新浏览器....