SpringCloud的Gateway网关的认识

SpringCloud的Gateway网关的认识

SpringCloud的网关配置

基础知识

SpringCloudGateway和SpringCloudZuul一样是微服务网关,不过Gateway是SpringCloud官方推出的,而Zuul是Netflix推出的。
看其他人的一些文章说是Gateway是用于取代Zuul的第二代网关,这个我在官方找不到资料说明。

  • 主要术语

default-filters: 里面可以定义一些共同的filter,对所有路由都起作用
routes:具体的路由信息,是一个数组,每一个路由基本包含部分:

  • id:这个路由的唯一id,不定义的话为一个uuid
  • uri:http请求为lb://前缀 + 服务id;ws请求为lb:ws://前缀 + 服务id;表示将请求负载到哪一个服务上
  • predicates:表示这个路由的请求匹配规则,只有符合这个规则的请求才会走这个路由。为一个数组,每个规则为并且的关系。
  • filters:请求转发前的filter,为一个数组。
  • order:这个路由的执行order

网关请求

在这里插入图片描述

SpringCloudGateway限流原理与实践

缓存、降级和限流是开发高并发系统的三把利器。缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;降级是当服务出现问题或者影响到核心流程的性能则需要暂时屏蔽,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源、写服务、频繁的复杂查询,因此需有一种手段来限制这些场景的并发/请求量,即限流。
限流的目的是通过对并发访问/请求进行限速,或对一个时间窗口内的请求进行限速来保护系统。一旦达到限制速率则可以拒绝服务、排队或等待、降级。

一般开发高并发系统常见的限流有:限制总并发数、限制瞬时并发数、限制时间窗口内的平均速率、限制远程接口的调用速率、限制MQ的消费速率,或根据网络连接数、网络流量、CPU或内存负载等来限流。
本文主要就分布式限流方法,对Spring Cloud Gateway的限流原理进行分析。
分布式限流最关键的是要将限流服务做成原子化,常见的限流算法有:令牌桶、漏桶等,Spring Cloud Gateway使用Redis+Lua技术实现高并发和高性能的限流方案。

  • 令牌桶算法

令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:
假如用户配置的平均速率为r,则每隔1/r秒一个令牌被加入到桶中;
假设桶最多可以存发b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃;
当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;
如果令牌桶中少于n个令牌,那么不会删除令牌,并且认为这个数据包在流量限制之外;
算法允许最长b个字节的突发,但从长期运行结果看,数据包的速率被限制成常量r。对于在流量限制外的数据包可以以不同的方式处理:
它们可以被丢弃;
它们可以排放在队列中以便当令牌桶中累积了足够多的令牌时再传输;
它们可以继续发送,但需要做特殊标记,网络过载的时候将这些特殊标记的包丢弃。

在这里插入图片描述

  • 漏桶算法

漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)和流量控制(Traffic Policing),漏桶算法的描述如下:
一个固定容量的漏桶,按照常量固定速率流出水滴;
如果桶是空的,则不需流出水滴;
可以以任意速率流入水滴到漏桶;
如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

在这里插入图片描述

  • 实践

SpringCloudGateway限流方案
Spring Cloud Gateway 默认实现 Redis限流,如果扩展只需要实现Ratelimter接口即可,同时也可以通过自定义KeyResolver来指定限流的Key,比如我们需要根据用户、IP、URI来做限流等等,通过exchange对象可以获取到请求信息,比如:

用户限流
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}

SpringCloudGateway默认提供的RedisRateLimter 的核心逻辑为判断是否取到令牌的实现,通过调用 META-INF/scripts/request_rate_limiter.lua 脚本实现基于令牌桶算法限流

网关实现

  1. 基础的依赖
<dependencies>
    <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-all</artifactId>
      <version>4.5.13</version>
    </dependency>

    <!--自己封装的通用返回类响应类-->
    <dependency>
      <groupId>com.pkk</groupId>
      <artifactId>components-rpc</artifactId>
    </dependency>

    <!--注册中心-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <!--网关配置【不同于zuul的网关配置】-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <!--hystrix熔断器-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>

    <!--路由重试功能【Spring异常重试框架Spring Retry】-->
    <!--<dependency>
      <groupId>org.springframework.retry</groupId>
      <artifactId>spring.retry</artifactId>
    </dependency>-->


    <!--使用redis的lua语言进行[ip,接口,用户限流操作]-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>
  </dependencies>
  1. 路由的配置
package com.pkk.cloud.support.gataway.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.pkk.cloud.support.gataway.enums.code.GatewayCodeStatus;
import com.pkk.components.rpc.response.util.ResponseUtil;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import reactor.core.publisher.Mono;

/**
 * @description: 限流gateway限制
 * @author: peikunkun
 * @create: 2019-08-11 20:04
 **/
@Slf4j
public class RateCheckGatewayFilterFactory extends AbstractGatewayFilterFactory<RateCheckGatewayFilterFactory.Config> {

  private final RateLimiter defaultRateLimiter;
  private final KeyResolver defaultKeyResolver;


  public RateCheckGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) {
    //必须要这一步
    super(RateCheckGatewayFilterFactory.Config.class);
    this.defaultRateLimiter = defaultRateLimiter;
    this.defaultKeyResolver = defaultKeyResolver;
  }

  public RateLimiter getDefaultRateLimiter() {
    return defaultRateLimiter;
  }

  public KeyResolver getDefaultKeyResolver() {
    return defaultKeyResolver;
  }


  @SuppressWarnings("unchecked")
  @Override
  public GatewayFilter apply(Config config) {
    log.info("类:" + this.getClass().getSimpleName() + "今日过滤器");
    KeyResolver resolver = (config.keyResolver == null) ? defaultKeyResolver : config.keyResolver;
    RateLimiter<Object> limiter = (config.rateLimiter == null) ? defaultRateLimiter : config.rateLimiter;
    log.info("配置说明resolver:" + JSONObject.toJSONString(Stream.of(resolver).toArray()));
    log.info("配置说明limiter:" + JSONObject.toJSONString(limiter));
    return (exchange, chain) -> {
      Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
      log.info("配置说明内部route:" + JSONObject.toJSONString(route));
      return resolver.resolve(exchange).flatMap(key ->
          limiter.isAllowed(route.getId(), key).flatMap(response -> {
            log.info("配置说明内部" + route.getId() + "___" + key);
            for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
              exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
            }
            if (response.isAllowed()) {
              return chain.filter(exchange);
            }
            ServerHttpResponse rs = exchange.getResponse();
            byte[] datas = JSON.toJSONString(ResponseUtil
                .error(GatewayCodeStatus.TOO_MANY_REQUESTS))
                .getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = rs.bufferFactory().wrap(datas);
            rs.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
            rs.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
            return rs.writeWith(Mono.just(buffer));
          }));
    };
  }


  /**
   * @Description: 基础的参数配置【此配置会通过工厂进行参数绑定】
   * @Param:
   * @return:
   * @Author: peikunkun
   * @Date: 2019/8/21 0021 下午 6:10
   */
  public static class Config {

    private KeyResolver keyResolver;
    private RateLimiter rateLimiter;
    private HttpStatus statusCode = HttpStatus.TOO_MANY_REQUESTS;

    public KeyResolver getKeyResolver() {
      return keyResolver;
    }

    public Config setKeyResolver(KeyResolver keyResolver) {
      this.keyResolver = keyResolver;
      return this;
    }

    public RateLimiter getRateLimiter() {
      return rateLimiter;
    }

    public Config setRateLimiter(RateLimiter rateLimiter) {
      this.rateLimiter = rateLimiter;
      return this;
    }

    public HttpStatus getStatusCode() {
      return statusCode;
    }

    public Config setStatusCode(HttpStatus statusCode) {
      this.statusCode = statusCode;
      return this;
    }
  }
}

  1. 自定义主机地址过滤
package com.pkk.cloud.support.gataway.config.custom;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @description: 自定义主机进行限流
 * @author: peikunkun
 * @create: 2019-08-23 16:55
 **/
@Slf4j
public class CustomHostAddrKeyResolver implements KeyResolver {

  /**
   * bean的名称
   */
  public static final String KEY_RESOLVER_HOST = "customHostAddrKeyResolver";

  /**
   * @Description: 依据主机ip进行限流过滤
   * @Param: [exchange]
   * @return: reactor.core.publisher.Mono<java.lang.String>
   * @Author: peikunkun
   * @Date: 2019/8/23 0023 下午 4:57
   */
  @Override
  public Mono<String> resolve(ServerWebExchange exchange) {
    log.info(this.getClass().getSimpleName() + "resolve  is init");
    return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
  }

}

  1. 自定义请求地址的过滤
package com.pkk.cloud.support.gataway.config.custom;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @description: 自定义的路径线路解析
 * @author: peikunkun
 * @create: 2019-08-23 16:49
 **/
@Slf4j
public class CustomRequestPathKeyResolver implements KeyResolver {

  /**
   * bean的名称
   */
  public static final String KEY_RESOLVER_REQUEST_URL = "customRequestPathKeyResolver";


  /**
   * @Description: 把根据路径方式进行解析
   * @Param: [exchange]
   * @return: reactor.core.publisher.Mono<java.lang.String>
   * @Author: peikunkun
   * @Date: 2019/8/23 0023 下午 4:52
   */
  @Override
  public Mono<String> resolve(ServerWebExchange exchange) {
    log.info(this.getClass().getSimpleName() + "resolve  is init");
    return Mono.just(exchange.getRequest().getPath().value());
  }


}

  1. 使限流配置生效
package com.pkk.cloud.support.gataway.config;

import static com.pkk.cloud.support.gataway.config.custom.CustomHostAddrKeyResolver.KEY_RESOLVER_HOST;
import static com.pkk.cloud.support.gataway.config.custom.CustomRequestPathKeyResolver.KEY_RESOLVER_REQUEST_URL;

import com.pkk.cloud.support.gataway.config.custom.CustomHostAddrKeyResolver;
import com.pkk.cloud.support.gataway.config.custom.CustomRequestPathKeyResolver;
import com.pkk.cloud.support.gataway.filter.CustomGlobalFilter;
import com.pkk.cloud.support.gataway.filter.RateCheckGatewayFilterFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;

/**
 * @description: 过滤器配置类
 * @author: peikunkun
 * @create: 2019-08-23 11:23
 **/
@Slf4j
@Configuration
public class GatewayFilterConfig {


  @Bean
  public RateCheckGatewayFilterFactory rateCheckGatewayFilterFactory(RateLimiter defaultRateLimiter,
      @Qualifier("apiKeyResolver") KeyResolver defaultKeyResolver) {
    return new RateCheckGatewayFilterFactory(defaultRateLimiter, defaultKeyResolver);
  }


  /**
   * @Description: 全局过滤配置器
   * @Param: []
   * @return: com.pkk.cloud.support.gataway.filter.CustomGlobalFilter
   * @Author: peikunkun
   * @Date: 2019/8/23 0023 上午 11:28
   */
  @Bean
  public CustomGlobalFilter customGlobalFilter() {
    return new CustomGlobalFilter();
  }


  /**
   * @Description: 自定义请求路径
   * @Param: []
   * @return: com.pkk.cloud.support.gataway.config.custom.CustomRequestPathKeyResolver
   * @Author: peikunkun
   * @Date: 2019/8/23 0023 下午 4:51
   */
  @Bean(KEY_RESOLVER_REQUEST_URL)
  public CustomRequestPathKeyResolver customRequestPathKeyResolver() {
    log.info(this.getClass().getSimpleName() + " is init");
    return new CustomRequestPathKeyResolver();
  }


  /**
   * @Description:配置依据主机的方式
   * @Param: []
   * @return: com.pkk.cloud.support.gataway.config.custom.CustomHostAddrKeyResolver
   * @Author: peikunkun
   * @Date: 2019/8/23 0023 下午 4:59
   */
  @Bean(KEY_RESOLVER_HOST)
  public CustomHostAddrKeyResolver hostAddrKeyResolver() {
    log.info(this.getClass().getSimpleName() + " is init");
    return new CustomHostAddrKeyResolver();
  }


  /**
   * 接口限流操作
   *
   * @return
   */
  @Bean(name = "apiKeyResolver")
  public KeyResolver apiKeyResolver() {
    log.info("根据路径初始化配置:" + this.getClass().getSimpleName());
    //根据api接口来限流
    return exchange -> Mono.just(exchange.getRequest().getPath().value());
  }
}

  1. 熔断器的配置
package com.pkk.cloud.support.gataway.handle;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.pkk.components.rpc.response.CommonResponse;
import com.pkk.components.rpc.response.util.ResponseUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @description: 熔断器的简单处理
 * @author: peikunkun
 * @create: 2019-08-11 17:36
 **/
@Slf4j
@RestController
public class HystrixSimpleHandle {

  /**
   *  args:
   *   name: authHystrixCommand
   *   fallbackUri: forward:/hystrixTimeout
   *   name表示HystrixCommand代码的名称,fallbackUri表示触发断路由后的跳转请求url
   */

  /**
   * @Description: 熔断器的处理
   * @Param: []
   * @return: java.lang.String
   * @Author: peikunkun
   * @Date: 2019/8/11 0011 下午 5:38
   */
  @HystrixCommand(commandKey = "authHystrixCommand")
  public String authHystrixCommand() {
    return "进入熔断器的处理类";
  }

  /**
   * 熔断器出发类
   *
   * @return
   */
  @RequestMapping("/hystrixTimeout")
  public CommonResponse<Object> hystrixTimeout() {
    log.debug("cloud-support-gataway触发了断路由");
    return ResponseUtil.error(HttpStatus.SERVICE_UNAVAILABLE.value(), HttpStatus.SERVICE_UNAVAILABLE.getReasonPhrase());
  }

}

  1. 基本的配置
##解决注册中心和服务不在一个网段,即外网访问
#eureka.instance.ip-address = 118.25.123.16
#eureka.instance.hostname = ${eureka.instance.ip-address}
eureka.instance.prefer-ip-address = true
#eureka.instance.instance-id = ${eureka.instance.ip-address}:${spring.application.name}:${server.port}
eureka.instance.lease-renewal-interval-in-seconds = 2
eureka.instance.lease-expiration-duration-in-seconds = 5
eureka.client.service-url.defaultZone = http://118.25.123.16:8761/eureka/



#redis
spring.redis.host = 127.0.0.1
spring.redis.password = admin
spring.redis.port = 6379
spring.redis.database = 10


#服务名称
spring.application.name = cloud-support-gataway


#会为所有服务都进行转发操作,只需要在访问路径上指定要访问的服务即可,通过这种方式就不用为每个服务都去配置转发规则,当新加了服务的时候,不用去配置路由规则和重启网关
#这边设置了false,我在下面去配置路由了【discovery: locator: enabled: true   表示注册中心生效,我们可以通过注册中心的服务名进行路由转发】
spring.cloud.gateway.discovery.locator.enabled = false
spring.cloud.gateway.discovery.locator.lower-case-service-id = true

#路由配置[auth-service   表示路由的唯一id]
spring.cloud.gateway.routes[0].id = cloud-support-service
#lb表示将从服务中心去找进行转发【lb://auth-service  指向注册中心的服务,使用lb:// 加上ServiceName,当然也可以通过http://localhost:8080指向】
spring.cloud.gateway.routes[0].uri = lb://cloud-support-service
spring.cloud.gateway.routes[0].order = 0
#[- Path= /auth/**  表示path地址,根据url,以auth开头的会被转发到auth-service服务,需要注意的是后面/**和/*的区别,/**是全部的路径]
spring.cloud.gateway.routes[0].predicates[0] = Path=/cloud-support-service/**

#过滤器
#spring.cloud.gateway.routes[0].filters[0] = Request

#重试过滤器
#spring.cloud.gateway.routes[0].filters[1].name = Retry
#spring.cloud.gateway.routes[0].filters[1].args.retries = 3
#spring.cloud.gateway.routes[0].filters[1].args.status = 405
#spring.cloud.gateway.routes[0].filters[1].args.statusSeries = 500
#spring.cloud.gateway.routes[0].filters[1].args.method = GET

#路由过滤器
spring.cloud.gateway.routes[0].filters[0].name = RateCheck
#限流策略(#{@BeanName})用于限流的键的解析器的 Bean 对象名字(有些绕,看代码吧)。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。默认情况下,使用PrincipalNameKeyResolver,以请求认证的java.security.Principal作为限流键【使用SpEL按名称引用bean】
spring.cloud.gateway.routes[0].filters[0].args.KeyResolver = #{@customRequestPathKeyResolver}
#参考{https://blog.csdn.net/lalacrazy/article/details/82109685}可实现的有令牌桶模型,漏桶算法[redis-rate-limiter]
#令牌桶每秒填充平均速率【允许用户每秒处理多少个请求】
spring.cloud.gateway.routes[0].filters[0].args.redis-rate-limiter.replenishRate = 2
#令牌桶容量【令牌桶的容量,允许在一秒钟内完成的最大请求数】
spring.cloud.gateway.routes[0].filters[0].args.redis-rate-limiter.burstCapacity = 2


#全局路由过滤器【default-filters:  代表默认的过滤器,这是一个全局的过滤器,不属于任何一个route】
#[- StripPrefix= 1 看单词的意思,从前面截取一个,实际上就是截取url,本例中就是会把/cloud-support-service截掉,后面的部分才是转发的url]
spring.cloud.gateway.default-filters[0] = StripPrefix=1

#全局熔断器处理的过滤
spring.cloud.gateway.default-filters[1].name = Hystrix
spring.cloud.gateway.default-filters[1].args.name = authHystrixCommand
spring.cloud.gateway.default-filters[1].args.fallbackUri = forward:/hystrixTimeout

#全局控制过滤器
#spring.cloud.gateway.default-filters[2].name = CustomGlobalFilter


#全局路由接口限制
#spring.cloud.gateway.default-filters[2].name = RateCheck
#spring.cloud.gateway.default-filters[2].args.KeyResolver = #{@apiKeyResolver}
#spring.cloud.gateway.default-filters[2].args.redis-rate-limiter.replenishRate = 2
#spring.cloud.gateway.default-filters[2].args.redis-rate-limiter.burstCapacity = 2


#服务端口
server.port = 8081

#熔断器超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds = 3000
hystrix.metrics.enabled = true

#日志级别
logging.level.org.springframework.cloud.gateway = info

网关过滤器

  1. 源码分析[StripPrefixGatewayFilterFactory ]
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.cloud.gateway.filter.factory;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;

/**
 * 源码分析
 **/
public class StripPrefixGatewayFilterFactory extends AbstractGatewayFilterFactory<StripPrefixGatewayFilterFactory.Config> {
  public static final String PARTS_KEY = "parts";

  /**
   * 通过此方式,使StripPrefixGatewayFilterFactory.Config.class配置类进行装配,bind()方法进行配置
   */
  public StripPrefixGatewayFilterFactory() {
    super(StripPrefixGatewayFilterFactory.Config.class);
  }

  public List<String> shortcutFieldOrder() {
    return Arrays.asList("parts");
  }


  public GatewayFilter apply(StripPrefixGatewayFilterFactory.Config config) {
    return (exchange, chain) -> {
      ServerHttpRequest request = exchange.getRequest();
      //增加原始的请求地址
      ServerWebExchangeUtils.addOriginalRequestUrl(exchange, request.getURI());
      String path = request.getURI().getRawPath();

      //根据配置去掉前缀的路径,根据配置的[config.parts]
      String newPath = "/" + (String)Arrays.stream(StringUtils.tokenizeToStringArray(path, "/")).skip((long)config.parts).collect(Collectors.joining("/"));

      //改变新的请求的路径为去掉相应的前缀个数的路径
      ServerHttpRequest newRequest = request.mutate().path(newPath).build();
      exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, newRequest.getURI());
      return chain.filter(exchange.mutate().request(newRequest).build());
    };
  }

  /**
   * 配置类
   */
  public static class Config {
    private int parts;

    public Config() {
    }

    public int getParts() {
      return this.parts;
    }

    public void setParts(int parts) {
      this.parts = parts;
    }
  }
}

网关接口限流主要配置

  • 限流源码
// routeId也就是我们的fsh-house,id就是限流的key,也就是localhost。
public Mono<Response> isAllowed(String routeId, String id) {
    // 会判断RedisRateLimiter是否初始化了
	if (!this.initialized.get()) {
		throw new IllegalStateException("RedisRateLimiter is not initialized");
	}
    // 获取routeId对应的限流配置
    Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig);

    if (routeConfig == null) {
		throw new IllegalArgumentException("No Configuration found for route " + routeId);
    }

    // 允许用户每秒做多少次请求
    int replenishRate = routeConfig.getReplenishRate();

    // 令牌桶的容量,允许在一秒钟内完成的最大请求数
    int burstCapacity = routeConfig.getBurstCapacity();

	try {
        // 限流key的名称(request_rate_limiter.{localhost}.timestamp,request_rate_limiter.{localhost}.tokens)
		List<String> keys = getKeys(id);


		// The arguments to the LUA script. time() returns unixtime in seconds.
		List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",
				Instant.now().getEpochSecond() + "", "1");
		// allowed, tokens_left = redis.eval(SCRIPT, keys, args)
        // 执行LUA脚本
		Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);
				// .log("redisratelimiter", Level.FINER);
		return flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))
				.reduce(new ArrayList<Long>(), (longs, l) -> {
					longs.addAll(l);
					return longs;
				}) .map(results -> {
					boolean allowed = results.get(0) == 1L;
					Long tokensLeft = results.get(1);

					Response response = new Response(allowed, getHeaders(routeConfig, tokensLeft));

					if (log.isDebugEnabled()) {
						log.debug("response: " + response);
					}
					return response;
				});
	}
	catch (Exception e) {
		log.error("Error determining if user allowed from redis", e);
	}
	return Mono.just(new Response(true, getHeaders(routeConfig, -1L)));
}

Spring Cloud Gateway目前提供的限流还是相对比较简单的,在实际中我们的限流策略会有很多种情况,比如:

  • 每个接口的限流数量不同,可以通过配置中心动态调整
  • 超过的流量被拒绝后可以返回固定的格式给调用方
  • 对某个服务进行整体限流(这个大家可以思考下用Spring Cloud Gateway如何实现,其实很简单)

大括号中就是我们的限流Key,这边是IP,本地的就是localhost

  • timestamp:存储的是当前时间的秒数,也就是System.currentTimeMillis() / 1000或者Instant.now().getEpochSecond()
  • tokens:存储的是当前这秒钟的对应的可用的令牌数量

LUA脚本

local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)

local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)

--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)

local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
  last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)

local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
  last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)

local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end

--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)

redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)

return { allowed_num, new_tokens }

限流的配置

#路由过滤器
spring.cloud.gateway.routes[0].filters[0].name = RateCheck
#限流策略(#{@BeanName})用于限流的键的解析器的 Bean 对象名字(有些绕,看代码吧)。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。默认情况下,使用PrincipalNameKeyResolver,以请求认证的java.security.Principal作为限流键【使用SpEL按名称引用bean】
spring.cloud.gateway.routes[0].filters[0].args.KeyResolver = #{@customRequestPathKeyResolver}
#参考{https://blog.csdn.net/lalacrazy/article/details/82109685}可实现的有令牌桶模型,漏桶算法[redis-rate-limiter]
#令牌桶每秒填充平均速率【允许用户每秒处理多少个请求】
spring.cloud.gateway.routes[0].filters[0].args.redis-rate-limiter.replenishRate = 2
#令牌桶容量【令牌桶的容量,允许在一秒钟内完成的最大请求数】
spring.cloud.gateway.routes[0].filters[0].args.redis-rate-limiter.burstCapacity = 2

一些内置的基础配置案例

server:
  port: 8888
spring:
  profiles:
    active: path-route      #使用哪个配置文件
  application:
    name: HELLO-GATEWAY     #服务名

---         #三个横线表示再创建一个配置文件
spring:
  profiles: path-route            #配置文件名 和 spring.profiles.active 相对应
  cloud:
    #设置路由规则
    gateway:
      routes:
      - id: gateway
        uri: lb://MICROSERVICE01        #代表从注册中心获取服务,且以lb(load-balance)负载均衡方式转发
        predicates:                     #断言
        - Path=/service01/**            #表示将以/service01/**开头的请求转发到uri为lb://MICROSERVICE01的地址上
        - After=2019-06-20T00:00:00.789-07:00[America/Denver] #表示在该时间点之后的时间,发出的请求会被路由到uri
#        filters:
#        - StripPrefix=1                #表示将Path的路径/service01在转发前去掉,如果设置StripPrefix=2,表示将/service01/*去掉 以此类推... 同时将spring.cloud.gateway.discovery.locator.enabled改为false,如果不改的话,之前的localhost:8799/client01/test01这样的请求地址也能正常访问,因为这时为每个服务创建了2个router

      discovery:
        locator:
          #表示gateway开启服务注册和发现功能,
          #并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router,这个router将以服务名开头的请求路径转发到对应的服务
          enabled: true
          #表示将请求路径的服务名配置改成小写  因为服务注册的时候,向注册中心注册时将服务名转成大写的了
          lower-case-service-id: true
          
---
spring:
  profiles: after-route
  cloud:
    gateway:
      routes:
      - id: gateway
        uri: lb://MICROSERVICE01
        predicates:
        #表示在该时间点之后的时间,发出的请求会被路由到uri
        - After=2019-06-20T00:00:00.789-07:00[America/Denver]
#        - After=1561098916602  也可以用long类型的时间戳格式

---
spring:
  profiles: before-route
  cloud:
    gateway:
      routes:
      - id: gateway
        uri: lb://MICROSERVICE01
        predicates:
        #表示在该时间点之前的时间,发出的请求会被路由到uri
        - Before=2019-12-20T00:00:00.789-07:00[America/Denver]

---
spring:
  profiles: between-route
  cloud:
    gateway:
      routes:
      - id: gateway
        uri: lb://MICROSERVICE01
        predicates:
        #表示在该时间点之间的时间,发出的请求会被路由到uri
        - Between=2019-02-20T00:00:00.789-07:00[America/Denver],2019-12-20T00:00:00.789-07:00[America/Denver]

---
spring:
  profiles: header-route
  cloud:
    gateway:
      routes:
      - id: gateway
        uri: lb://MICROSERVICE01
        predicates:
        #表示当请求的请求头中有 key=Hello,value=World,发出的请求会被路由到uri
        - Header=Hello, World
        #可以是正则表达式 例如 - Header=Hello, \d+

---
spring:
  profiles: cookie-route
  cloud:
    gateway:
      routes:
      - id: gateway
        uri: lb://MICROSERVICE01
        predicates:
        #表示当请求带有名为Hello,值为World的Cookie时,发出的请求会被路由到uri
        - Cookie=Hello, World

---
spring:
  profiles: host-route
  cloud:
    gateway:
      routes:
      - id: gateway
        uri: lb://MICROSERVICE01
        predicates:
        #表示当请求带有host为**.host.test时,发出的请求会被路由到uri
        - Host=**.host.test

---
spring:
  profiles: method-route
  cloud:
    gateway:
      routes:
      - id: gateway
        uri: lb://MICROSERVICE01
        predicates:
        #表示GET请求,都会被路由到uri
        - Method=GET

---
spring:
  profiles: query-route
  cloud:
    gateway:
      routes:
      - id: gateway
        uri: lb://MICROSERVICE01
        predicates:
        #表示请求带有参数key=a, value=b时,该请求会被路由到uri
        - Query=a, b

动态路由

@RestController
@RequestMapping("/route")
public class RouteController {
 
    @Autowired
    private DynamicRouteServiceImpl dynamicRouteService;
 
    /**
     * 增加路由
     * @param gwdefinition
     * @return
     */
    @PostMapping("/add")
    public String add(@RequestBody GatewayRouteDefinition gwdefinition) {
        try {
            RouteDefinition definition = assembleRouteDefinition(gwdefinition);
            return this.dynamicRouteService.add(definition);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "succss";
    }
 
    @GetMapping("/delete/{id}")
    public String delete(@PathVariable String id) {
        return this.dynamicRouteService.delete(id);
    }
 
    @PostMapping("/update")
    public String update(@RequestBody GatewayRouteDefinition gwdefinition) {
        RouteDefinition definition = assembleRouteDefinition(gwdefinition);
        return this.dynamicRouteService.update(definition);
    }
 
    private RouteDefinition assembleRouteDefinition(GatewayRouteDefinition gwdefinition) {
        RouteDefinition definition = new RouteDefinition();
        List<PredicateDefinition> pdList=new ArrayList<>();
        definition.setId(gwdefinition.getId());
        List<GatewayPredicateDefinition> gatewayPredicateDefinitionList=gwdefinition.getPredicates();
        for (GatewayPredicateDefinition gpDefinition: gatewayPredicateDefinitionList) {
            PredicateDefinition predicate = new PredicateDefinition();
            predicate.setArgs(gpDefinition.getArgs());
            predicate.setName(gpDefinition.getName());
            pdList.add(predicate);
        }
 
        List<GatewayFilterDefinition> gatewayFilterDefinitions = gwdefinition.getFilters();
        List<FilterDefinition> filterList = new ArrayList<>();
        if (!CollectionUtils.isEmpty(gatewayFilterDefinitions)) {
            for (GatewayFilterDefinition gatewayFilterDefinition : gatewayFilterDefinitions) {
                FilterDefinition filterDefinition = new FilterDefinition();
                filterDefinition.setName(gatewayFilterDefinition.getName());
                filterDefinition.setArgs(gatewayFilterDefinition.getArgs());
                filterList.add(filterDefinition);
            }
        }
        definition.setPredicates(pdList);
        definition.setFilters(filterList);
        URI uri = UriComponentsBuilder.fromHttpUrl(gwdefinition.getUri()).build().toUri();
        definition.setUri(uri);
        return definition;
    }
}

DynamicRouteServiceImpl

package org.gateway.route;
 
@Service
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
 
    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;
 
    private ApplicationEventPublisher publisher;
 
 
    /**
     * 增加路由
     * @param definition
     * @return
     */
    public String add(RouteDefinition definition) {
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return "success";
    }
 
 
    /**
     * 更新路由
     * @param definition
     * @return
     */
    public String update(RouteDefinition definition) {
        try {
            this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
        } catch (Exception e) {
            return "update fail,not find route  routeId: "+definition.getId();
        }
        try {
            routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "success";
        } catch (Exception e) {
            return "update route  fail";
        }
 
 
    }
    /**
     * 删除路由
     * @param id
     * @return
     */
    public String delete(String id) {
        try {
            this.routeDefinitionWriter.delete(Mono.just(id));
            return "delete success";
        } catch (Exception e) {
            e.printStackTrace();
            return "delete fail";
        }
 
    }
 
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }
}

上面 routeDefinitionWriter的实现默认是InMemoryRouteDefinitionRepository,将路由存在内存中,我们可以自己实现一个将路由存在redis中的repository。
this.publisher.publishEvent(new RefreshRoutesEvent(this));则会将CachingRouteLocator中的路由缓存清空

网关总结

  1. Gateway过滤器的执行顺序
全局过滤器与其他2类过滤器相比,永远是最后执行的;它的优先级只对其他全局过滤器起作用
当默认过滤器与自定义过滤器的优先级一样时,优先出发默认过滤器,然后才是自定义过滤器;同类型的过滤器,出发顺序与他们在配置文件中声明的顺序一致
默认过滤器与自定义过滤器使用同样的order顺序空间,即他们会按照各自的顺序来进行排序

参考文献

  • Gateway执行顺序 https://www.cnblogs.com/westlin/p/10911945.html
  • SpringGateway的原理解析 https://blog.csdn.net/lalacrazy/article/details/82109685
  • spring cloud gateway 之限流篇 【Forezp】 https://segmentfault.com/a/1190000017421460
  • 最简单易懂的SpringCloud入门学习(SpringCloud整合Gateway实现网关服务)【凉凉的西瓜】 https://blog.csdn.net/qq_42815754/article/details/94622244
  • SpringCloud Finchley基础教程:3,spring cloud gateway网关 【卓小洛o】 https://blog.csdn.net/ifrozen/article/details/80016566###
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值