• 微服务实践之网关(Spring Cloud Gateway)详解-SpringCloud(2021.0.x)-3


    [版权申明] 非商业目的注明出处可自由转载
    出自:shusheng007

    系列文章

    微服务实践之服务注册与发现(Nacos)-SpringCloud(2020.0.x)-1
    微服务实践之负载均衡(Spring Cloud Load Balancer)-SpringCloud(2020.0.x)-2

    概述

    本文将介绍微服务架构中的SpringCloud Gateway这个网关组件的入门使用,观后你应该可以大体知道如网关如何工作,如何结合分布式配置,如何结合服务注册中心服务使用,如何将请求负载均衡到不同的服务实例,如何限流,如何使用断路器等实操性功能。

    编程这玩意对实践啊,理论背的天花乱坠,真用的时候还是不知道怎么下手,还是要动手实践一下…

    宏观结构

    本文是一个微服务demo的一部分,以一个简单的电商购物流程为案例,用以展示微服务架构中所要解决的问题及相应开源方案。

    网关

    网关是微服务架构中举足轻重的组件,由于其是进入微服务内部边界的门户,所以可以完成非常多具有切面性质的功能

    • 请求智能路由
    • 认证授权
    • 限流
    • 日志聚合
    • API监控

    本文使用SpringCloud Gateway,有关于它的详情可参考官网或者其他同学的博客。它是基于Webflux实现的一个非阻塞IO的组件,与我们常使用的基于线程池的阻塞IO实现的SpringMvc相比,高并发下同样的硬件资源(内存,CPU)下具有更高的吞吐量,其优势主要提现在IO密集场景下。

    SpringCloud Gateway简介

    概念

    SC Gateway最核心概念其实就是一个路由(Route)。

    一个路由可以被看做是对一个请求的智能处理,你可以把它看成是你们小区大门口的保安,我们暂且叫它阿路吧。当有一个人来你家里取东西,阿路就会根据各种情况智能帮你处理。每个保安的有一个名字,例如阿路(路由的Id)。你大姨妈来串门,由于来访人太多,阿路让她排队进入(路由order),阿路问你大姨妈找哪家业主(路由的Predicate),她说找王二狗,于是阿路告诉她左转左转再左转15号楼512 。但是进之前的给她正经做个核酸(路由的前置Filter),等到她串门要出来时,阿路又来了,她你签个名,说明何时离开的方便流调(路由的后置Filter)。

    SC Gateway既可以使用代码来写也可以使用yml来写路由,我们这里使用yml文件,例如下面这样

    - id: route_goods_service #阿路
      uri: lb://goods-service #15号楼512
      predicates:
        - Path=/goods-service/** #业主王二狗
      filters:
        - StripPrefix=1 #做个核酸
        - name: Singnature #签个名
          args:
            sign: 大姨妈
      order: 1 #排队顺序
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    原理

    原理和SpringMVC那一套挺相似的,简单过一下,有个宏观的概念

    • 定义

    先将路由(Route)转化为RouteDefinition保存起来,是不是熟悉的味道,想想SpringMVC的BeanDefinition

    • 初始化

    首次请求,调用DispatcherHandler里的initStrategies(ApplicationContext context)获取各种HandlerMappingHandlerAdapter保存起来。

    protected void initStrategies(ApplicationContext context) {
       //获取HandlerMapping
       Map mappingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
             context, HandlerMapping.class, true, false);
       this.handlerMappings = Collections.unmodifiableList(mappings);
      //获取HandlerAdapter
       Map adapterBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
             context, HandlerAdapter.class, true, false);
       this.handlerAdapters = new ArrayList<>(adapterBeans.values());
    
       //获取结果处理器
       Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
             context, HandlerResultHandler.class, true, false);
       this.resultHandlers = new ArrayList<>(beans.values());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    是不是又有一股熟悉的味道,想想SpringMvc的DispatcherSeveletinitStrategies方法,两个方法连签名都一样

    • 分发

    接着调用DispatcherHandlerhandle方法。

    @Override
    public Mono handle(ServerWebExchange exchange) {
       return Flux.fromIterable(this.handlerMappings)
             .concatMap(mapping -> mapping.getHandler(exchange))
             .next()
             .switchIfEmpty(createNotFoundError())
             .flatMap(handler -> invokeHandler(exchange, handler))
             .flatMap(result -> handleResult(exchange, result));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这个是分发流程,具体的逻辑就隐藏在操作符里面的那几个函数调用。其中HttpWebHandlerAdapterRoutePredicateHandlerMapping比较关键。但是RoutePredicateHandlerMapping的命名我比较懵逼,按说这应该是Adapter要干的事情,不知道为什么要Mapping。

    想想SpringMVC的DispatcherServletdoDispatch方法,都是一个路子。

    整体流程可以查看下图:图片来自于 SpringCloud实践:Gateway网关

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R3fD77yM-1666411221287)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9be51b758e0c48d596f4fda766f54604~tplv-k3u1fbpfcp-watermark.image?)]

    如何在微服务架构中使用

    前面的内容全当是铺垫,主要是为了后边的使用的时候容易理解。

    一个微服务架构系统中的服务几乎都是时刻准备着朝生夕死,这是微服务架构的特征,特别是进入云原生时代,在Docker与K8s的加持下,这种趋势愈发明显。它内在的思想是:你不能保证一件事100%成功,但是你的有处理失败情况的解决方案?所以我们需要服务注册中心,来时刻获取当前可用服务的坐标,以便于请求。

    此处我们使用阿里开源的Nacos,它既可以做分布式配置中心也可以做服务注册中心

    Gateway集成Nacos配置功能

    首先,得益于Alibaba的微服务组件拥抱了SpringCloud,所以现在在SrpingCloud中整合阿里的Nacos非常容易。

    • 引入依赖

    首先在pom.xml中使用加入spring-clound与spring-clound-alibaba的依赖声明,然后需要什么组件就引入那个组件的依赖。例如我们这里要集成nacos的配置功能,所以我们引入了spring-cloud-starter-alibaba-nacos-config

    
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-config
        
        
        
        
            org.springframework.cloud
            spring-cloud-starter-gateway
        
    
    
    
    
    
        
            
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
            
            
                com.alibaba.cloud
                spring-cloud-alibaba-dependencies
                ${spring.cloud.alibaba.version}
                pom
                import
            
        
    
    
    • 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
    • 配置

    resources/bootstrap.yml里进行配置,这块是集成过程中最困难的地方了,因为其涉及到了nacos自身的一些概念。

    我们先来看一下nacos中的配置文件长什么样,打开nacos管理后台。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6avMsp9H-1666411221289)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e90393ae8d1c445ba781f0c69bcd7645~tplv-k3u1fbpfcp-watermark.image?)]

    从图中红框我们可以看到3个概念:

    namespace: 这个比较好理解,例如你有两套环境,开发环境和生产环境,每套环境一个命名空间

    data-id: 每个配置文件的id,这个也比较好理解,就是你的配置文件叫个啥,随便取。

    group: 每个配置文件的group,这货最难理解,例如你有两个服务的配置文件,一个叫goods,一个叫orders,你可以把它们设置成同一个group:buy。

    只有data-id和group组合不一样才可以存在(在所有的命名空间下),意思就是同一个命名空间中data-id可以重复,只要它们属于不同的group,如下图所示。不知道为什么要这么设计,data-id起名字的时候不要重复就好了,为什么需要group呢?有知道的告诉一下

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DiqQ7th3-1666411221290)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/542eeba3204e42329d9429c6c9f03fd6~tplv-k3u1fbpfcp-watermark.image?)]

    了解了上面的概念,我们就来配置一下让springboot程序从nacos上读取配置。下图就是一个通用的配置,每个boot程序都可以用,不要被它吓到我稍微解释一下你就明白了

    spring:
      application:
        name: api-gateway
      cloud:
        nacos:
          config:
            server-addr: ${spring.cloud.nacos.server.address}
            namespace: ${spring.cloud.nacos.server.namespace}
            file-extension: ${spring.cloud.nacos.server.file-extension}
            extension-configs[0]:
              data-id: base-config.yaml
              group: ${spring.config.activate.on-profile}
              refresh: true
            extension-configs[1]:
              data-id: ${spring.application.name}.yaml
              group: ${spring.config.activate.on-profile}
              refresh: true
    
    ---
    ### 指定环境
    spring:
      config:
        activate:
          on-profile: dev
      cloud:
        nacos:
          server:
            address: 127.0.0.1:8849
            namespace: ns-dev
            file-extension: yaml
    
    • 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

    让我们来解释一下上面的配置。

    第一:在yml语法中,可以使用---将两个配置写在一个文件中,所以我们的配置文件分为两部分。

    第二:下半部分配置了当前激活的profile:dev,以及nacos server的信息:nacos服务的地址,命名空间,配置文件的扩展名。如下图所示,你创建的时候,会为配置文件选择一个扩展名。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P1BG6t9M-1666411221292)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e1083a8dc267404f9a7a1ec9ffb06d3f~tplv-k3u1fbpfcp-watermark.image?)]

    第二:上半部的配置才是nacos配置中心真正的配置。其值都是从是从下半部分读取的。就像我们开始说的,要在nacos中定位一个配置文件,需要三个要素:namespace,data-id,group。

    现在唯一注意的就是nacos支持一种类似继承的配置方式,例如你的3个服务配置文件里面都有同样的配置,那么nacos支持将其抽取出来,单独写一个配置,拉取的时候再把这两个文件的配置给合并了。这里的extension-configs[0]就是那个通用的配置文件,extension-configs[1]就是本服务的配置文件。

    至此,nacos的配置功能已经可用了

    Gateway集成Nacos服务注册功能

    在配置完nacos配置中心的功能后,服务注册中心就比较简单了,一样的路子。

    • 依赖

    这里我需要使用nacos的服务注册功能,所以需要引入相关的依赖。

    
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
        
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 配置

    还是在resources/bootstrap.yml里进行配置,在nacos标签下配置discovery即可。

    spring:
      application:
        name: api-gateway
      cloud:
        nacos:
          discovery:
            #读取下面配置的值
            server-addr: ${spring.cloud.nacos.server.address}
            namespace: ${spring.cloud.nacos.server.namespace}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可见,除了nacos的地址外,还需要配置命名空间。

    • 开启Gateway服务的服务发现功能

    服务注册功能还需要使用注解@EnableDiscoveryClient开启

    @SpringBootApplication
    @EnableDiscoveryClient
    public class ApiGatewayApplication {
        public static void main(String[] args) {
            SpringApplication.run(ApiGatewayApplication.class, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如何使用

    SrpingClout GateWay的使用也比较简单了,就是在application.yml文件中按需求配置路由,然后来拦截外界对其发起的请求。

    spring:
      cloud:
        gateway:
          routes:
            #商品服务
            - id: route_goods_service
              uri: lb://goods-service
              predicates:
                - Path=/goods-service/**
              filters:
                - StripPrefix=1
                - name: PrefixPath
                  args:
                    prefix: /goods
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我觉着初学者第一次看到这玩意应该是较懵逼的,不怕你笑话我第一次看到就很懵逼,也许有的同学天资聪颖,一看就懂吧。上面的代码定义了一个叫route_goods_service的路由,它的路由目标为goods-service这个服务,接着配置了一个predicate,两个filter。

    例如我的网关地址为http://localhost:9000,当向网关发起一个http://localhost:9000/goods-service/makeOrder请求时,就会被这个路由拦截。因为我们的请求路径匹配到了predicate的条件(存在goods-service),所以进入了下面两个filter,StripPrefix将请求路径中的第一个前缀goods-service给去掉了,而PrefixPath接着给请求路径加了一个goods前缀,所以最终的请求路径变为了http://(goods-service服务的ip+port)/goods/makeOrder

    其中个人认为初学时最难理解的就是那个predicate和filter的写法。

    断言 predicate

    关于断言的理论知识我们在前面已经介绍过了,接下来我们上点干货。

    Path=/goods-service/**这什么意思呢?这其实是断言的简写,前面是它的名称,后面跟着参数,多个参数以逗号顺序分割。那个Path其实是省略了后缀后的名称,全名为PathRoutePredicateFactory,这基本上是一个约定命名,断言都以RoutePredicateFactory为后缀,然后名称使用前缀。

    要实现一个断言非常简单,只要继承AbstractRoutePredicateFactory类即可,然后在类里面新建一个静态内部类,例如叫Config,作为泛型参数,Override里面的方法即可。

    最重要的方法是apply方法,断言的判断逻辑就在这个方法里。第二个是shortcutFieldOrder方法,这个方法是用来实现配置简写模式的,如果你不实现,那么你的predicate在用的时候就不能使用如下的简写模式:

    - VipCustomer=vip-key,i-am-vip`
    
    • 1

    只能使用复杂模式

    - name: VipCustomer
      args:
        vipKey: vip-key
        vipValue: i-am-vip
    
    • 1
    • 2
    • 3
    • 4

    下面这个自定义的predicate发现请求的header里面的存在vip-key:i-am-vip这一Header时则返回TRUE。

    @Slf4j
    public class VipCustomerRoutePredicateFactory extends AbstractRoutePredicateFactory {
        public static final String VIP_KEY = "vipKey";
        public static final String VIP_VALUE = "vipValue";
    
        public VipCustomerRoutePredicateFactory() {
            super(Config.class);
        }
    
    
        //实现了这个在application.yml中配置的时候可以使用简写
        @Override
        public List shortcutFieldOrder() {
            return Arrays.asList(VIP_KEY,VIP_VALUE);
        }
    
        @Override
        public Predicate apply(Config config) {
            return serverWebExchange -> {
                String value = serverWebExchange.getRequest().getHeaders().getFirst(config.getVipKey());
                if (!StringUtils.hasText(value) || !value.equals(config.getVipValue())) {
                    log.info("屌丝用户");
                    return false;
                }
                log.info("Vip用户");
                return true;
            };
        }
    
        public static class Config {
            private String vipKey;
            private String vipValue;
    
            public String getVipKey() {
                return vipKey;
            }
    
            public Config setVipKey(String vipKey) {
                this.vipKey = vipKey;
                return this;
            }
    
            public String getVipValue() {
                return vipValue;
            }
    
            public Config setVipValue(String vipValue) {
                this.vipValue = vipValue;
                return this;
            }
        }
    }
    
    • 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

    内置的predicate也基本都是这样的,唯一区别就是其apply方法的处理逻辑比较复杂。

    过滤器 filter

    过滤器和断言完全是一个路子,过滤器要继承的抽象类为 AbstractGatewayFilterFactory,配置的名称也是使用类的前缀,例如StripPrefixGatewayFilterFactoryyml中的名称为:StripPrefix。但也有极个别的例外,例如CircuitBreaker这个filter的全名是SpringCloudCircuitBreakerFilterFactory

    下面就是内置的StripPrefix的源码,我们稍微来看一下。

    public class StripPrefixGatewayFilterFactory
          extends AbstractGatewayFilterFactory {
    
       //这个值要和下面Config类里面声明的属性名称一致,这里是parts
       public static final String PARTS_KEY = "parts";
    
       public StripPrefixGatewayFilterFactory() {
          super(Config.class);
       }
    
       //覆写了这个方法就可以在配置的时候使用简写模式了
       @Override
       public List shortcutFieldOrder() {
          return Arrays.asList(PARTS_KEY);
       }
    
       @Override
       public GatewayFilter apply(Config config) {
          return new GatewayFilter() {
             @Override
             public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                ServerHttpRequest request = exchange.getRequest();
                ...
                return chain.filter(exchange.mutate().request(newRequest).build());
             }
          };
       }
    
       public static class Config {
    
          private int parts = 1;
    
          public int getParts() {
             return parts;
          }
    
          public void setParts(int parts) {
             this.parts = parts;
          }
    
       }
    
    }
    
    • 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

    可见这filter只有一个int型参数,参数名称为parts,所以我们在yml文件中可以按照如下配置

    简写

    filters:
      - StripPrefix=1
    
    • 1
    • 2

    完整写法:

    filters:
      - name: StripPrefix
         args:
          parts: 1
    
    • 1
    • 2
    • 3
    • 4

    多一点

    在理解了基本用法后,我们就可以实现我们最开始说的那些功能了。

    网关限流

    实现

    基于filter实现,SC gateway提供了RequestRateLimiterGatewayFilterFactory这个filter来完成限流,如下代码所示

    @ConfigurationProperties("spring.cloud.gateway.filter.request-rate-limiter")
    public class RequestRateLimiterGatewayFilterFactory
          extends AbstractGatewayFilterFactory{
         ...
         
        public static class Config implements HasRouteId {
           //限流接口
           private RateLimiter rateLimiter;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    同时其还提供了一个接口RateLimiter并提供了一个实现类RedisRateLimiter,其是基于Redis的采用令牌桶算法的限流器。 如果你不想用RedisRateLimiter,那你就可以自己基于RateLimiter接口实现一个自己的限流器,例如使用 GuavaBuket4j

    下面我们看如何使用RedisRateLimiter

    • 引入redis相关依赖并配置

    由于我们要使用Redis限流,肯定的需要连上redis

    
            
                org.springframework.boot
                spring-boot-starter-data-redis-reactive
            
    
    
            
                org.apache.commons
                commons-pool2
            
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    配置redis连接

    spring:
      redis:
        #redis数据库,其有16个数据库
        database: 0
        #redis服务器地址
        host: localhost
        #redis服务器端口号
        port: 6379
        #连接池配置
        lettuce:
          pool:
            enabled: true
            max-active: 8
            max-wait: 10s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 实现KeyResolver

      由于我们的决定基于什么维度限流,例如到底是基于访问者IP限流呢,还是基于访问的url限流呢,还是基于用户限流呢?这就是由KeyResolver决定的。下面我们写了一个基于请求路径限流的KeyResolver。

    @Configuration
    public class GatewayConfig {
        @Bean
        public KeyResolver pathKeyResolver() {
            return new KeyResolver() {
                @Override
                public Mono resolve(ServerWebExchange exchange) {
    //                Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
                    String path = exchange.getRequest().getURI().getPath();
                    return Mono.just(path);
                }
            };
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 配置路由

    最后一步就是将这个filter配置到路由里面去了。

    filters:
      #限流
      - name: RequestRateLimiter #gateway内置的一个filter
        args:
          # 令牌桶每秒填充速率
          redis-rate-limiter.replenishRate: 1
          # 令牌桶的上容量
          redis-rate-limiter.burstCapacity: 3
          # 使用SpEL表达式从Spring容器中获取KeyResolver Bean,用来确定使用什么维度限流,例如使用请求IP限流
          # 这个是我们在自己的Config文件中定义的bean
          key-resolver: "#{@pathKeyResolver}"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    里面有3个参数,注释已经说的很明白了。

    经过以上3步就成功配置了限流器。 如果对令牌填充速率和令牌桶容量的参数含义有疑问的话,那你需要去看下令牌桶限流算法,网上关于令牌桶限流算法的文章特别多,挑一篇质量好的看看就行。

    测试

    我们使用postman的批量执行功能来并发发起请求,我们配置的令牌桶容量为3,所以并发数为3,4个并发就会触发限流

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cs7g9jWm-1666411221293)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/78171c1591814fd282a06ca21aa2c695~tplv-k3u1fbpfcp-watermark.image?)]

    从图中可以看到,前3个正常执行,第4个被限流,返回429.

    断路器

    实现

    网关也是个程序,它也会崩溃,需要你的保护,不能因为下游服务太拉胯将网关给耗死,所以其内置支持了断路器。你可能又猜到了,这个又是基于filter实现的。Sc gateway提供了SpringCloudCircuitBreakerFilterFactory这个抽象类,如下所示

    public abstract class SpringCloudCircuitBreakerFilterFactory
          extends AbstractGatewayFilterFactory {
    
       //这个就是你要在yml文件中配置filter的名称
       public static final String NAME = "CircuitBreaker";
       
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    同时,Sc gateway还提供了一个继承此抽象类的实现类:SpringCloudCircuitBreakerResilience4JFilterFactory,意图很明显,这是要原生支持Resilience4J啊,其他的我也没用过,不知道集成阿里sentinel怎么弄,是否是需要继承SpringCloudCircuitBreakerFilterFactory类,这块等有机会研究一下。

    public class SpringCloudCircuitBreakerResilience4JFilterFactory extends SpringCloudCircuitBreakerFilterFactory {
    
    }
    
    • 1
    • 2
    • 3

    关于断路器,现在普遍使用的就是Resilience4J、阿里Sentinel,还有一个Netflix的Hystrix 这个不开发了,进入了维护期。今天我们这里使用Resilience4j。

    • 引入Resilience4j依赖
    
    
        org.springframework.cloud
        spring-cloud-starter-circuitbreaker-reactor-resilience4j
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 构建Resilience4J的配置

    这个其实比较困难,因为你要知道如何配置,你就要先理解断路的设计原理,不然你怎么可能会配置呢,不会配置也能起步拉,使用默认配置就好啦

    我们的目标就是要搞ReactiveResilience4JCircuitBreakerFactory,它里面有一个配置方法configureDefault需要一个Resilience4JCircuitBreakerConfiguration类型的参数。于是问题转化为搞一个这个类型的实例出来,这个类又有两个配置类TimeLimiterConfigCircuitBreakerConfig,于是问题转化为给这两个类型各搞一个实例出来。下面的代码就是在干上面描述的那些事。

    • TimeLimiterConfig 设置请求超时打开断路器
    • CircuitBreakerConfig 设置断路器各种参数,包括状态的转换等,这块需要仔细研究一下,可以单独写一篇断路器的文章时再说。
    @Configuration
    public class MsCircuitBreakerConfig {
    
        //对Resilience4J的配置
        @Bean
        public Customizer defaultCustomizer() {
            return new Customizer() {
                @Override
                public void customize(ReactiveResilience4JCircuitBreakerFactory factory) {
                    CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                            .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) // 滑动窗口的类型为请求个数
                            .slidingWindowSize(10) // 时间窗口的大小为10个
                            .minimumNumberOfCalls(1) // 在单位时间窗口内最少需要1次调用才能开始进行统计计算
                            .failureRateThreshold(50) // 在单位时间窗口内调用失败率达到50%后会启动断路器
                            .enableAutomaticTransitionFromOpenToHalfOpen() // 允许断路器自动由打开状态转换为半开状态
                            .waitDurationInOpenState(Duration.ofSeconds(2)) // 断路器打开状态转换为半开状态需要等待2秒
                            .permittedNumberOfCallsInHalfOpenState(2) // 在半开状态下允许进行正常调用的次数
                            .recordExceptions(Throwable.class) // 所有异常都当作失败来处理
                            .build();
                    TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
                            .timeoutDuration(Duration.ofMillis(200))//接口200毫秒没有响应就认为失败了
                            .build();
    
                    factory.configureDefault(id -> {
                        return new Resilience4JConfigBuilder(id)
                                .timeLimiterConfig(timeLimiterConfig)
                                .circuitBreakerConfig(circuitBreakerConfig)
                                .build();
                    });
                }
            };
        }
    }
    
    • 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

    这里只是在演示gateway如何使用断路器,没有细聊断路器自己的知识,这块有点多。这里我们简单的描述一下,帮助理解上面的代码。

    断路器有3个状态:开,半开,关。 开状态:请求被拦截,半开状态:允许尝试几个请求,关状态:请求顺利通过。他们直接的转换关系如下图所示。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dn0JdZ1Q-1666411221294)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/259a4ca32b0d41579b8a739c3e7f21f1~tplv-k3u1fbpfcp-watermark.image?)]

    测试

    仍然使用postman的批量执行功能研验证,首先把限流器开大一点。这个比较麻烦一点了。

    我们的网关会调用这个goods-service的这个方法,当参数goodsId是delay时这个方法就会延时300毫秒,就会触发网关超时,因为我们设置的网关超时是200毫秒。

    @GetMapping("/checkGoods")
    public BaseResponse getGoods(@RequestParam("goodsId") String goodsId){
        log.info("开始商品调用:{}",goodsId);
        if("delay".equals(goodsId)){
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                log.error("睡眠失败",e);
            }
        }
        log.info("结束商品调用:{}",goodsId);
    
        return ResultUtil.ok("ok");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我们发起10个请求,每个请求间隔800毫秒,我们给出的goodsId参数顺序为:

    ok
    delay
    delay
    delay
    delay
    ok
    ok
    ok
    delay
    ok
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    发起请求后,结果为:

    1  ok    200 OK                       close
    2  delay 504 Gateway Timeout          触发open
    3  delay 503 Service Unavailable      open
    4  delay 503 Service Unavailable      open
    5  delay 504 Gateway Timeout          hafe-open  由于尝试请求失败,导致断路器打开,于是请求没有被转发
    6  ok    503 Service Unavailable      open
    7  ok    503 Service Unavailable      open
    8  ok    200 OK                       half-open  半开状态下,请求成功,所以转变为cose状态
    9  delay 504 Gateway Timeout          触发open    由于请求失败,又转变为open
    10 ok    503 Service Unavailable      open
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    总结

    本文上手实践了SpringCloud Gateway,并就其核心用法、原理与功能做了解释,在整理的过程中对我自己梳理知识也有很大的帮助,如果它也帮助到了你,请不要吝惜你的赞。

    文中提到的断路器,以及限流的原理面试时候特别爱问,这块有时间可以整理一下。

    源码

    一如既往,你可以从Github上获得本文源码:master-microservice,请不要吝啬你的小星星

    参考文章

    # Circuit Breaking In Spring Cloud Gateway With Resilience4J

  • 相关阅读:
    干货 | 什么是特性团队/功能团队(FeatureTeam)
    Tauri 2.0.0 beta环境搭建
    【深度学习】详解 ViLT
    牛客小白赛60(F.被抓住的小竹)&61(E.排队)(数学+推公式)
    uniapp 之 充值 微信支付下 之 传递输入金额参数
    c++输入输出文件操作stream
    DevOps 如何解决技术债务问题
    selenium-XPATH定位
    国产开发板上打造开源ThingsBoard工业网关--基于米尔芯驰MYD-JD9X开发板
    AI 大框架基于python来实现基带处理之TensorFlow(信道估计和预测模型,信号解调和解码模型)
  • 原文地址:https://blog.csdn.net/ShuSheng0007/article/details/127460097