• Spring Cloud Gateway夺命连环10问,带你彻底了解gateway


    这篇文章我们重点介绍下微服务中的一个重要角色:网关,对于网关如何选择,由于阿里系暂时未出网关,当然是选择了Spring cloud Gateway,当然网关还有zuul,但是性能方面肯定远远不如gateway。

    1.为什么需要网关?

    传统的单体架构中只有一个服务开放给客户端调用,但是微服务架构中是将一个系统拆分成多个微服务,那么作为客户端如何去调用这些微服务呢?如果没有网关的存在,只能在本地记录每个微服务的调用地址
    在这里插入图片描述

    无网关的微服务架构往往存在以下问题:

    • 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性。
    • 认证复杂,每个服务都需要独立认证。
    • 存在跨域请求,在一定场景下处理相对复杂。

    2.为什么选择gateway而不是zuul

    spring-cloud-Gateway是spring-cloud的一个子项目。而zuul则是netflix公司的项目,只是spring将zuul集成在spring-cloud中使用而已。

    因为zuul2.0连续跳票zuul的性能表现不是很理想,所以催生了spring团队开发了Gateway项目。

    Zuul使用的是阻塞式的 API,不支持长连接,比如 websockets。底层是DNservletS缓存,Zuul处理的是http请求没有提供异步支持,流控等均由hystrix支持。

    依赖包spring-cloud-starter-netflix-zuul。

    Gateway:

    Spring Boot和Spring Webflux提供的Netty底层环境,不能和传统的Servlet容器一起使用,也不能打包成一个WAR包。

    依赖spring-boot-starter-webflux和spring-cloud-starter-gateway提供了异步支持,提供了抽象负载均衡,提供了抽象流控,并默认实现了RedisRateLimiter

    我们大致可以分为以下四个思路去讨论。

    2.1 相同点和不同点

    相同点:

    • 1、底层都是servlet
    • 2、两者均是web网关,处理的是http请求

    不同点:

    • 1、内部实现:
      gateway对比zuul多依赖了spring-webflux,在spring的支持下,功能更强大,内部实现了限流、负载均衡等,扩展性也更强,但同时也限制了仅适合于Spring Cloud套件
      zuul则可以扩展至其他微服务框架中,其内部没有实现限流、负载均衡等。
    • 2、是否支持异步
      zuul仅支持同步
        gateway支持异步。理论上gateway则更适合于提高系统吞吐量(但不一定能有更好的性能),最终性能还需要通过严密的压测来决定

    2.2 框架设计的角度

    gateway具有更好的扩展性,并且其已经发布了2.0.0的RELESE版本,稳定性也是非常好的

    2.3 性能

    WebFlux 模块的名称是 spring-webflux,名称中的 Flux 来源于 Reactor中的类 Flux。

    Spring webflux 有一个全新的非堵塞的函数式 Reactive Web 框架,可以用来构建异步的、非堵塞的、事件驱动的服务,在伸缩性方面表现非常好。使用非阻塞API。

    Websockets得到支持,并且由于它与Spring紧密集成,所以将会是一个更好的开发体验。

    Zuul 1.x,是一个基于阻塞io的API Gateway。Zuul已经发布了Zuul 2.x,基于Netty也是非阻塞的,支持长连接,但Spring Cloud暂时还没有整合计划。

    总的来说,在微服务架构,如果使用了Spring Cloud生态的基础组件,则Spring Cloud Gateway相比而言更加具备优势,单从流式编程+支持异步上就足以让开发者选择它了。

    对于小型微服务架构或是复杂架构(不仅包括微服务应用还有其他非Spring Cloud服务节点),zuul也是一个不错的选择。

    3.网关的基本功能?

    网关是所有微服务的门户,路由转发仅仅是最基本的功能,除此之外还有其他的一些功能,比如:认证、鉴权、熔断、限流、日志监控等等…
    在这里插入图片描述

    4.Spring Cloud Gateway几个必知的术语?

    路由(route):gateway的基本构建模块。它由ID、目标URI、断言集合和过滤器集合组成。如果聚合断言结果为真,则匹配到该路由。

    断言(Predicate ):参照Java8的新特性Predicate,允许开发人员匹配HTTP请求中的任何内容,比如头或参数。

    过滤器(filter):可以在返回请求之前或之后修改请求和响应的内容。

    5.网关如何搭建?

    在这里插入图片描述

    请大家务必严格的按照上面对应的版本,要不然会有你意想不到的bug

    6.什么是Predict(断言)?

    Predicate来自于java8的接口。Predicate接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非)。

    可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。

    Spring Cloud Gateway内置了许多Predict,这些Predict的源码在org.springframework.cloud.gateway.handler.predicate包中,有兴趣可以阅读一下。内置的一些断言如下图:
    在这里插入图片描述

    下面就以最后一种权重断言为例介绍一下如何配置。配置如下:

    spring:
      cloud:
        gateway:
          ## 路由
          routes:
            ## id只要唯一即可,名称任意
            - id: gateway-provider_1
              uri: http://localhost:9024
              ## 配置断言
              predicates:
                ## Path Route Predicate Factory断言,满足/gateway/provider/**这个请求路径的都会被路由到http://localhost:9024这个uri中
                - Path=/gateway/provider/**
                ## Weight Route Predicate Factory,同一分组按照权重进行分配流量,这里分配了80%
                ## 第一个group1是分组名,第二个参数是权重
                - Weight=group1, 8
                
            ## id必须唯一
            - id: gateway-provider_2
              ## 路由转发的uri
              uri: http://localhost:9025
              ## 配置断言
              predicates:
                ## Path Route Predicate Factory断言,满足/gateway/provider/**这个请求路径的都会被路由到http://localhost:9024这个uri中
                - Path=/gateway/provider/**
                ## Weight Route Predicate Factory,同一分组按照权重进行分配流量,这里分配了20%
                ## 第一个group1是分组名,第二个参数是权重
                - Weight=group1, 2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    routes下就是配置的路由策略,各个组件如下:

    • id:路由的唯一id,名称任意
    • uri:路由转发的uri
    • predicates:断言配置,可以配置多个

    Spring Cloud Gateway中的断言命名都是有规范的,格式:xxxRoutePredicateFactory。

    比如权重的断言:WeightRoutePredicateFactory,那么配置时直接取前面的Weight。

    默认的路由转发如果路由到了两个,则是的按照配置先后顺序转发,上面都配置了路径:Path=/gateway/provider/**,如果没有配置权重,则肯定是转发到http://localhost:9024。

    但是既然配置配置了权重并且相同的分组,则按照权重比例进行分配流量。

    8.什么是过滤器?

    过滤器这个概念很熟悉,在Spring mvc 就接触过,Gateway的过滤器的作用以及生命周期都是类似的。

    8.1 Gateway的生命周期:

    PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择 请求的微服务、记录调试信息等。

    POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。

    8.2 Gateway 的Filter作用范围

    Gateway 的Filter从作用范围可分为两种:

    • GatewayFilter:应用到单个路由或者一个分组的路由上(需要在配置文件中配置)。
    • GlobalFilter:应用到所有的路由上(无需配置,全局生效)

    GatewayFilter(局部过滤器)

    Spring Cloud Gateway中内置了许多的局部过滤器,局部过滤器需要在指定路由配置才能生效,默认是不生效的。

    以AddResponseHeaderGatewayFilterFactory这个过滤器为例,为原始响应添加Header,配置如下:

    spring:
      cloud:
        gateway:
          ## 路由
          routes:
            ## id只要唯一即可,名称任意
            - id: gateway-provider_1
              uri: http://localhost:9024
              ## 配置断言
              predicates:
                ## Path Route Predicate Factory断言,满足/gateway/provider/**这个请求路径的都会被路由到http://localhost:9024这个uri中
                - Path=/gateway/provider/**
              ## 配置过滤器(局部)
              filters:
                - AddResponseHeader=X-Response-Foo, Bar
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    注意:过滤器的名称只需要写前缀,过滤器命名必须是xxxGatewayFilterFactory(包括自定义)。

    虽说内置的过滤器能够解决很多场景,但是难免还是有些特殊需求需要定制一个过滤器,下面就来介绍一下如何自定义局部过滤器。

    场景:模拟一个授权验证的过程,如果请求头或者请求参数中携带token则放行,否则直接拦截返回401,代码如下:

    /**
     * 名称必须是xxxGatewayFilterFactory形式
     * todo:模拟授权的验证,具体逻辑根据业务完善
     */
    @Component
    @Slf4j
    public class AuthorizeGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthorizeGatewayFilterFactory.Config> {
    
        private static final String AUTHORIZE_TOKEN = "token";
    
        //构造函数,加载Config
        public AuthorizeGatewayFilterFactory() {
            //固定写法
            super(AuthorizeGatewayFilterFactory.Config.class);
            log.info("Loaded GatewayFilterFactory [Authorize]");
        }
    
        //读取配置文件中的参数 赋值到 配置类中
        @Override
        public List<String> shortcutFieldOrder() {
            //Config.enabled
            return Arrays.asList("enabled");
        }
    
        @Override
        public GatewayFilter apply(AuthorizeGatewayFilterFactory.Config config) {
            return (exchange, chain) -> {
                //判断是否开启授权验证
                if (!config.isEnabled()) {
                    return chain.filter(exchange);
                }
    
                ServerHttpRequest request = exchange.getRequest();
                HttpHeaders headers = request.getHeaders();
                //从请求头中获取token
                String token = headers.getFirst(AUTHORIZE_TOKEN);
                if (token == null) {
                    //从请求头参数中获取token
                    token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
                }
    
                ServerHttpResponse response = exchange.getResponse();
                //如果token为空,直接返回401,未授权
                if (StringUtils.isEmpty(token)) {
                    response.setStatusCode(HttpStatus.UNAUTHORIZED);
                    //处理完成,直接拦截,不再进行下去
                    return response.setComplete();
                }
                /**
                 * todo chain.filter(exchange) 之前的都是过滤器的前置处理
                 *
                 * chain.filter().then(
                 *  过滤器的后置处理...........
                 * )
                 */
                //授权正常,继续下一个过滤器链的调用
                return chain.filter(exchange);
            };
        }
    
        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        public static class Config {
            // 控制是否开启认证
            private boolean enabled;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    局部过滤器需要在路由中配置才能生效,配置如下:

    spring:
      cloud:
        gateway:
          ## 路由
          routes:
            ## id只要唯一即可,名称任意
            - id: gateway-provider_1
              uri: http://localhost:9024
              ## 配置断言
              predicates:
                ## Path Route Predicate Factory断言,满足/gateway/provider/**这个请求路径的都会被路由到http://localhost:9024这个uri中
                - Path=/gateway/provider/**
              ## 配置过滤器(局部)
              filters:
                - AddResponseHeader=X-Response-Foo, Bar
                ## AuthorizeGatewayFilterFactory自定义过滤器配置,值为true需要验证授权,false不需要
                - Authorize=true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    访问:http://localhost:8080/gateway/provider/
    在这里插入图片描述

    上述的AuthorizeGatewayFilterFactory只是涉及到了过滤器的前置处理,后置处理是在chain.filter().then()中的then()方法中完成的。
    访问:http://localhost:8080/gateway/provider?token=11111
    就会发现401无认证已经没了。

    上述的AuthorizeGatewayFilterFactory只是涉及到了过滤器的前置处理,后置处理是在chain.filter().then()中的then()方法中完成的。

    GlobalFilter(全局过滤器)

    全局过滤器应用到全部路由上,无需开发者配置,Spring Cloud Gateway也内置了一些全局过滤器,如下图:
    在这里插入图片描述

    GlobalFilter的功能其实和GatewayFilter是相同的,只是GlobalFilter的作用域是所有的路由配置,而不是绑定在指定的路由配置上。

    多个GlobalFilter可以通过@Order或者getOrder()方法指定每个GlobalFilter的执行顺序,order值越小,GlobalFilter执行的优先级越高。

    注意,由于过滤器有pre和post两种类型,pre类型过滤器如果order值越小,那么它就应该在pre过滤器链的顶层,post类型过滤器如果order值越小,那么它就应该在pre过滤器链的底层。示意图如下:
    在这里插入图片描述

    当然除了内置的全局过滤器,实际工作中还需要定制过滤器,下面来介绍一下如何自定义。

    场景:模拟Nginx的Access Log 功能,记录每次请求的相关信息。代码如下:

    /**
     * 实现GlobalFilter
     */
    @Slf4j
    @Component
    @Order(value = Integer.MIN_VALUE)
    public class AccessLogGlobalFilter implements GlobalFilter {
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            //filter的前置处理
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getPath().pathWithinApplication().value();
            InetSocketAddress remoteAddress = request.getRemoteAddress();
            return chain
                    //继续调用filter
                    .filter(exchange)
                    //filter的后置处理
                    .then(Mono.fromRunnable(() -> {
                ServerHttpResponse response = exchange.getResponse();
                HttpStatus statusCode = response.getStatusCode();
                log.info("请求路径:{},远程IP地址:{},响应码:{}", path, remoteAddress, statusCode);
            }));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    好了,全局过滤器不必在路由上配置,注入到IOC容器中即可全局生效。

    这样做有什么坏处呢?

    服务的IP的地址一旦修改了,路由配置中的uri必须修改服务集群中无法实现负载均衡。

    此时就需要集成的注册中心,使得网关能够从注册中心自动获取uri(负载均衡)。

    以上具体创建gateway微服务以及如何集成到nacos,请移步至:《SpringCloud alibaba实战–第5章》

    值得我们注意的是:路由配置中唯一不同的就是路由的uri,格式:lb://service-name,这是固定写法

    • lb:固定格式,指的是从nacos中按照名称获取微服务,并遵循负载均衡策略。
    • service-name:nacos注册中心的服务名称,这里并不是IP地址形式的

    为什么指定了lb就可以开启负载均衡,前面说过全局过滤器LoadBalancerClientFilter就是负责路由寻址负载均衡的。

    9.如何实现动态路由?

    上述例子都是将网关的一系列配置写到项目的配置文件中,一旦路由发生改变必须要重新项目,这样维护成本很高。

    其实我们可以将网关的配置存放到配置中心中,这样由配置中心统一管理,一旦路由发生改变,只需要在配置中心修改,这样便能达到一处修改,多出生效的目的

    有一篇更详细的博文可以带你一步一步的实现如何通过nacos实现动态路由。《springCloud alibaba:Nacos Config-服务配置》
    在这里插入图片描述

    10.如何自定义全局异常处理?

    通过前面的测试可以看到一个现象:一旦路由的微服务下线或者失联了,Spring Cloud Gateway直接返回了一个错误页面,如下图:
    在这里插入图片描述

    显然这种异常信息不友好,前后端分离架构中必须定制返回的异常信息。

    传统的Spring Boot 服务中都是使用@ControllerAdvice来包装全局异常处理的,但是由于服务下线,请求并没有到达。

    因此必须在网关中也要定制一层全局异常处理,这样才能更加友好的和客户端交互。

    Spring Cloud Gateway提供了多种全局处理的方式,今天我们只介绍其中一种方式,实现还算比较优雅。

    直接创建一个类GlobalErrorExceptionHandler,实现ErrorWebExceptionHandler,重写其中的handle方法,代码如下:

    /**
     * 用于网关的全局异常处理
     * @Order(-1):优先级一定要比ResponseStatusExceptionHandler低
     */
    @Slf4j
    @Order(-1)
    @Component
    @RequiredArgsConstructor
    public class GlobalErrorExceptionHandler implements ErrorWebExceptionHandler {
    
     private final ObjectMapper objectMapper;
    
     @SuppressWarnings({"rawtypes", "unchecked", "NullableProblems"})
     @Override
     public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
      ServerHttpResponse response = exchange.getResponse();
      if (response.isCommitted()) {
       return Mono.error(ex);
      }
    
      // JOSN格式返回
      response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
      if (ex instanceof ResponseStatusException) {
       response.setStatusCode(((ResponseStatusException) ex).getStatus());
      }
    
      return response.writeWith(Mono.fromSupplier(() -> {
       DataBufferFactory bufferFactory = response.bufferFactory();
       try {
        //todo 返回响应结果,根据业务需求,自己定制
        CommonResponse resultMsg = new CommonResponse("500",ex.getMessage(),null);
        return bufferFactory.wrap(objectMapper.writeValueAsBytes(resultMsg));
       }
       catch (JsonProcessingException e) {
        log.error("Error writing response", ex);
        return bufferFactory.wrap(new byte[0]);
       }
      }));
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    好了,全局异常处理已经定制完成了,在测试一下,此时正常返回JSON数据了,如下图:
    在这里插入图片描述

    总结

    Spring Cloud Gateway今天就分享到这里,主要介绍了以下几个知识点:

    • 为什么需要网关?网关的基本功能
    • 如何从零搭建一个微服务网关
    • Predict(断言)的概念
    • 过滤器的概念、Spring Cloud Gateway内置的过滤器以及如何自定义
    • 如何集成Nacos注册中心并且实现负载均衡
    • 如何集成Nacos实现动态路由,达到一处修改,多出生效的作用
    • 全局异常的处理
  • 相关阅读:
    阿里云网络、数据中心和服务器技术创新优势说明
    Codeforces Round 895 (Div. 3) C. Non-coprime Split
    清理docker 占用空间,volume挂载过大,清除镜像,容器,挂载数据
    搜索技术——遗传算法
    GeoServer:Vector Tiles-矢量瓦片制作与加载
    Spring Boot Security 角色授权示例
    QT 智能指针注意事项(备忘)
    深度学习(五)softmax 回归之:分类算法介绍,如何加载 Fashion-MINIST 数据集
    电脑监控软,电脑屏幕监控软件
    .net 6 使用 NEST 查询,时间字段传值踩坑
  • 原文地址:https://blog.csdn.net/zhiyikeji/article/details/126433406