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

Posted ShuSheng007

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微服务实践之网关(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 #排队顺序

原理

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

  • 定义

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

  • 初始化

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

protected void initStrategies(ApplicationContext context) 
   //获取HandlerMapping
   Map<String, HandlerMapping> mappingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
         context, HandlerMapping.class, true, false);
   this.handlerMappings = Collections.unmodifiableList(mappings);
  //获取HandlerAdapter
   Map<String, HandlerAdapter> adapterBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
         context, HandlerAdapter.class, true, false);
   this.handlerAdapters = new ArrayList<>(adapterBeans.values());

   //获取结果处理器
   Map<String, HandlerResultHandler> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
         context, HandlerResultHandler.class, true, false);
   this.resultHandlers = new ArrayList<>(beans.values());

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

  • 分发

接着调用DispatcherHandlerhandle方法。

@Override
public Mono<Void> 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));

这个是分发流程,具体的逻辑就隐藏在操作符里面的那几个函数调用。其中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中使用<dependencyManagement>加入spring-clound与spring-clound-alibaba的依赖声明,然后需要什么组件就引入那个组件的依赖。例如我们这里要集成nacos的配置功能,所以我们引入了spring-cloud-starter-alibaba-nacos-config

<dependencies>
    <!-- SpringCloud Ailibaba Nacos Config -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    
    <!-- 网关依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

</dependencies>


<dependencyManagement>
    <dependencies>
        <!--            SpringCloud依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>$spring-cloud.version</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--            SpringCloud Alibaba依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>$spring.cloud.alibaba.version</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
  • 配置

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

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

第一:在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的服务注册功能,所以需要引入相关的依赖。

<dependencies>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
</dependencies>
  • 配置

还是在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

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

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

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

@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication 
    public static void main(String[] args) 
        SpringApplication.run(ApiGatewayApplication.class, args);
    

如何使用

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

我觉着初学者第一次看到这玩意应该是较懵逼的,不怕你笑话我第一次看到就很懵逼,也许有的同学天资聪颖,一看就懂吧。上面的代码定义了一个叫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`

只能使用复杂模式

- name: VipCustomer
  args:
    vipKey: vip-key
    vipValue: i-am-vip

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

@Slf4j
public class VipCustomerRoutePredicateFactory extends AbstractRoutePredicateFactory<VipCustomerRoutePredicateFactory.Config> 
    public static final String VIP_KEY = "vipKey";
    public static final String VIP_VALUE = "vipValue";

    public VipCustomerRoutePredicateFactory() 
        super(Config.class);
    


    //实现了这个在application.yml中配置的时候可以使用简写
    @Override
    public List<String> shortcutFieldOrder() 
        return Arrays.asList(VIP_KEY,VIP_VALUE);
    

    @Override
    public Predicate<ServerWebExchange> 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;
        
    

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

过滤器 filter

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

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

public class StripPrefixGatewayFilterFactory
      extends AbstractGatewayFilterFactory<StripPrefixGatewayFilterFactory.Config> 

   //这个值要和下面Config类里面声明的属性名称一致,这里是parts
   public static final String PARTS_KEY = "parts";

   public StripPrefixGatewayFilterFactory() 
      super(Config.class);
   

   //覆写了这个方法就可以在配置的时候使用简写模式了
   @Override
   public List<String> shortcutFieldOrder() 
      return Arrays.asList(PARTS_KEY);
   

   @Override
   public GatewayFilter apply(Config config) 
      return new GatewayFilter() 
         @Override
         public Mono<Void> 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;
      

   


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

简写

filters:
  - StripPrefix=1

完整写法:

filters:
  - name: StripPrefix
     args:
      parts: 1

多一点

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

网关限流

实现

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

@ConfigurationProperties("spring.cloud.gateway.filter.request-rate-limiter")
public class RequestRateLimiterGatewayFilterFactory
      extends AbstractGatewayFilterFactory<RequestRateLimiterGatewayFilterFactory.Config>
     ...
     
    public static class Config implements HasRouteId 
       //限流接口
       private RateLimiter rateLimiter;
    

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

下面我们看如何使用RedisRateLimiter

  • 引入redis相关依赖并配置

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

<!--        由于sc gateway 基于webflux,所以需要reactive版本-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>

<!--        使用redis连接池-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

配置redis连接

spring:
  redis:
    #redis数据库,其有16个数据库
    database: 0
    #redis服务器地址
    host: localhost
    #redis服务器端口号
    port: 6379
    #连接池配置
    lettuce:
      pool:
        enabled: true
        max-active: 8
        max-wait: 10s
  • 实现KeyResolver

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

@Configuration
public class GatewayConfig 
    @Bean
    public KeyResolver pathKeyResolver() 
        return new KeyResolver() 
            @Override
            public Mono<String> resolve(ServerWebExchange exchange) 
//                Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
                String path = exchange.getRequest().getURI().getPath();
                return Mono.just(path);
            
        ;
    


  • 配置路由

最后一步就是将这个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"

里面有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<SpringCloudCircuitBreakerFilterFactory.Config> 

   //这个就是你要在yml文件中配置filter的名称
   public static final String NAME = "CircuitBreaker";
   

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

public class SpringCloudCircuitBreakerResilience4JFilterFactory extends SpringCloudCircuitBreakerFilterFactory 


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

  • 引入Resilience4j依赖
<!--        由于sc gateway 基于webflux,所以需要reactive版本-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
  • 构建Resilience4J的配置

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

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

  • TimeLimiterConfig 设置请求超时打开断路器
  • CircuitBreakerConfig 设置断路器各种参数,包括状态的转换等,这块需要仔细研究一下,可以单独写一篇断路器的文章时再说。
@Configuration
public class MsCircuitBreakerConfig 

    //对Resilience4J的配置
    @Bean
    public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() 
        return new Customizer<ReactiveResilience4JCircuitBreakerFactory>() 
            @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();
                );
            
        ;
    

这里只是在演示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<String> 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");

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

ok
delay
delay
delay
delay
ok
ok
ok
delay
ok

发起请求后,结果为:

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

总结

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

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

源码

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

参考文章

# Circuit Breaking In Spring Cloud Gateway With Resilience4J

以上是关于微服务实践之网关(Spring Cloud Gateway)详解-SpringCloud(2021.0.x)-3的主要内容,如果未能解决你的问题,请参考以下文章

微服务实践之负载均衡(Spring Cloud Load Balancer)-SpringCloud(2020.0.x)-2

微服务实践之负载均衡(Spring Cloud Load Balancer)-SpringCloud(2020.0.x)-2

spring cloud微服务实践一

spring cloud微服务实践四

Spring Cloud微服务实践之路- Eureka Server 中的第一个异常

爱油科技基于Docker和Spring Cloud的微服务实践