Skip to content

限流开发手册

1.1 限流开发

限流,也称流量控制。 是指系统在面临高并发,或者 大流量请求 的情况下, 限制新的请求对系统的访问 ,从而 保证系统的稳定性 。(限流代码在jupiter项目的jupiter-gateway-cloud模块中)

1.1.1 限流配置

限流中配置的必须参数:

单位时间内最大访问次数/s : 指的是每秒内可允许访问的最大请求数

单位时间桶中新增令牌数: 指的是每秒钟可以给令牌桶中增加多少个令牌

网关现在使用的是令牌桶的限流算法,每次请求需要消耗掉令牌中的一个令牌数,当消耗完这些令牌时就会触发限流,令牌桶内的令牌数也会按照所配置的单位时间桶中的新增令牌数去增加新的令牌。

1.1.2 限流策略

a) 全局限流

此限流策略是对整个服务级的所有接口进行限流

b) 根据指定IP限流

此限流策略是对接口的调用者的IP进行限流,IP配置的是接口调用方的IP地址,配置了之后只对配置的调用IP进行限流,不影响别的接口调用者

c)根据指定接口限流

此限流策略是给对应的接口配置,配置之后只对这个接口做限流,其余接口均不做限流。

注意:由于网关上配置了按路径匹配的谓词对接口的URL进行了重写,所以这里在配置的时候直接配置接口的URL即可。

d)headers字段固定值限流

此限流策略主要给核心业务使用,配置的字段是核心业务报文中对应的字段,字段的固定值就是所要限制的核心业务请求报文中的值。

e) headers字段值范围限流

此限流策略主要给核心业务使用,配置的字段是核心业务报文中对应的字段,字段的值得范围根据核心报文中要限制的值得范围去配置。

1.1.3 限流代码

限流的代码在jupiter工程下面。

a) 全局限流

对应的resolver(分解器)类是:GlobalLimiterKeyResolver.java

java
public Mono<String> resolve(ServerWebExchange exchange) {
    return Mono.just(BEAN_NAME + "@global");
}

上述调用的 Mono.just(BEAN_NAME + "@global")是创建一个包含 BEAN_NAME + "@global" 的Mono元素。此元素会在限流存储令牌桶数目的时候被分解使用,拼接到对应限流策略令牌桶存入到redis中的键上。具体的方法在 CustomRedisRateLimiter.java类中的isAllowed (String routeId, String id) 方法中。

java
request_rate_limiter.{globalLimiterKeyResolver@global}.timestamp:存储时间戳的键
request_rate_limiter.{globalLimiterKeyResolver@global}.tokens:存储令牌的个数的键

关于令牌桶存储的代码(CustomRedisRateLimiter.java):
List<String> scriptArgs = Arrays.asList(Integer.toString(replenishRate), Integer.toString(burstCapacity), Long.toString(Instant.now().getEpochSecond()), "1");
// allowed, tokens_left = redis.eval(SCRIPT, keys, args)
ReactiveRedisTemplate reactiveRedisTemplate = this.reactiveRedisTemplate01;
Flux<List<Long>> flux = reactiveRedisTemplate.execute(this.script, keys, scriptArgs);

上述代码中使用redis存储令牌桶个数的时候使用的是ReactiveRedisTemplate对象存储的,
keys 是一个存储String类型字符串的List集合,其中存储的就是当前时间戳所对应的键和令牌桶个数所对应的键,this.
script 是所要执行的脚本文件,其位置在spring-cloud-gateway-core-2.2.5.
RELEASE.jar 中的scripts包下的request_rate_limiter.lua,内容如下:
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)

if ttl > 0 then
  redis.call("setex", tokens_key, ttl, new_tokens)
  redis.call("setex", timestamp_key, ttl, now)
end

-- return { allowed_num, new_tokens, capacity, filled_tokens, requested, new_tokens }
return { allowed_num, new_tokens }

执行reactiveRedisTemplate.execute (this.script, keys, scriptArgs) 方法时,会调用上述脚本计算出当前令牌桶中的令牌数目,并将其和对应的时间戳的值存储到redis中去。

其余限流的令牌在redis中存储的键举例:

b) 根据指定IP限流

java
request_rate_limiter.{remoteAddrKeyResolver@192.168.245.197}.timestamp
request_rate_limiter.{remoteAddrKeyResolver@192.168.245.197}.tokens

c)根据接口限流

java
request_rate_limiter.{pathKeyResolver@/findSoftwarePage}.timestamp
request_rate_limiter.{pathKeyResolver@/findSoftwarePage}.tokens

d)headers字段固定值限流

java
request_rate_limiter.{requestHeaderFixationKeyResolver@ {
    "messageType":"1400", "serviceCode":"MbsdCore", "sourceType":"MT", "messageCode":"9100"
}}.timestamp

request_rate_limiter.{requestHeaderFixationKeyResolver

@ {
    "messageType":"1400", "serviceCode":"MbsdCore", "sourceType":"MT", "messageCode":"9100"
}}.tokens

e) headers字段值范围限流

java
request_rate_limiter.{requestHeaderScopeKeyResolver@ {
    "messageType":"1400", "serviceCode":"MbsdCore", "sourceType":"MT", "messageCode":"9100"
}}.timestamp

request_rate_limiter.{requestHeaderScopeKeyResolver

@ {
    "messageType":"1400", "serviceCode":"MbsdCore", "sourceType":"MT", "messageCode":"9100"
}}.tokens

1.1.4 字段固定值限流字段扩展开发流程

(1)首先修改项目类路径下的报文体配置文件headers.json,将要修改的字段和描述修改进去

(2) 在代码中修改类SystemHeader.java 修改或添加所需要的字段即可

  1. 打包重启项目即可让修改或添加的字段生效,字段值范围限流和此修改相同

1.1.5 Gateway限流的整体开发流程和执行流程

网关中使用的限流方式:

原来网关中的限流过滤器使用的是spring-cloud-gateway-core.jar包中的 RequestRateLimiterGatewayFilterFactory 过滤器。由于此限流过滤器的返回中没有具体的报文信息,而我们要在具体限流触发之后提示具体的限流信息,所以就添加了一个过滤器 BaseRequestRateLimiterGatewayFilterFactory,其中的代码逻辑和gateway源码中的一致,加了一段如下的代码,让返回的response的body中有具体的限流提示信息。

java
//自定义一个报文体,添加到response中去
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
LimiterResponse limiterResponse = new LimiterResponse();
limiterResponse.

setCode(HttpStatus.TOO_MANY_REQUESTS.value());
String[] strings = key.split("@");
String resolverStr = strings[0];
  switch(resolverStr){
        case RemoteAddrKeyResolver.BEAN_NAME:
        limiterResponse.

setMsg("TOUCH OFF REMOTE ADDRESS  LIMITER");
      break;
              case PathKeyResolver.BEAN_NAME:
        limiterResponse.

setMsg("TOUCH OFF INTERFACE PATH LIMITER");
      break;
              case RequestHeaderScopeKeyResolver.BEAN_NAME:
        limiterResponse.

setMsg("TOUCH OFF REQUEST HEADER SCOPE LIMITER");
      break;
              case RequestHeaderFixationKeyResolver.BEAN_NAME:
        limiterResponse.

setMsg("TOUCH OFF REQUEST HEADER FIXATION LIMITER");
      break;
              case GlobalLimiterKeyResolver.BEAN_NAME:
        limiterResponse.

setMsg("TOUCH OFF GLOBAL LIMITER");
      break;
              }

String lastStr = JSON.toJSONString(limiterResponse);
byte[] uppedContent = lastStr.getBytes();
DataBuffer buffer = bufferFactory.wrap(uppedContent);
return exchange.

getResponse().

writeWith(Mono.just(buffer));

RequestRateLimiterGatewayFilterFactory这个类,适用Redis和lua脚本实现了令牌桶的方式。具体实现逻辑在 RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中

具体代码实现逻辑可自己在RequestRateLimiterGatewayFilterFactory类中查看。

配置了限流之后,每一个请求从网关进来是会创建一个KeyResolver 对象,此时每一个限流策略中都会重写如下的方法,创建一个Mono 对象,并将请求中需要限流的字段或者信息拼接进去,已接口限流为例,Mono中是用当前的KeyResolver 对象的BeanName拼接上@和具体每一个接口的url生成的。