后台工程师来讲讲限流的思路

Posted yzlit

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了后台工程师来讲讲限流的思路相关的知识,希望对你有一定的参考价值。


一 ”两窗两桶“限流算法

1、固定窗口

固定窗口指的是为一定时间段的流量设置一个阈值,超过则触发限流策略(丢弃或者停留),然后直到下一个时间段重新置零开始计数,

这种策略很明显一个缺点是这个时间段的间隔要设置得好,否则会存在一个问题,就是无法“削峰填谷”,当时间间隔设为10秒100个请求量时,本来是应该正常每秒10个请求,但是突然第一秒就来了100个请求,在第一秒就变成100个请求每秒了,所以是没法达到限流缓冲的效果,所以这种策略比较粗暴简单。

2、滑动窗口

滑动窗口算法 类似于我们学过TCP的滑动窗口,对固定窗口的一种优化,

 

二、做「限流」的最佳实践

1、四种策略该如何选择?

首先,固定窗口。一般来说,如非时间紧迫,不建议选择这个方案,太过生硬。但是,为了能快速止损眼前的问题可以作为临时应急的方案。

其次,滑动窗口。这个方案适用于对异常结果「高容忍」的场景,毕竟相比“两窗”少了一个缓冲区。但是,胜在实现简单。

然后,漏桶。个人觉得这个方案最适合作为一个通用方案。虽说资源的利用率上不是极致,但是「宽进严出」的思路在保护系统的同时还留有一些余地,使得它的适用场景更广。

最后,令牌桶。当你需要尽可能的压榨程序的性能(此时桶的最大容量必然会大于等于程序的最大并发能力),并且所处的场景流量进入波动不是很大(不至于一瞬间取完令牌,压垮后端系统)。

 

三、如何做分布式限流

如果要分布式限流,那么单机限流的策略其实只需要移植一下,用新的工具如redis+lua 或者nginx+lua 又或者用zookeeper也是可以实现的,下面先具体讲讲redis+lua的分布式限流的思路和代码例子,其他有空再补上。

redis做限流的思路是先设置一个key=service1的记录,值为流量,和时间戳。这些用lua来主要是因为redis不支持事务,而lua脚本是作为一个原子操作一起执行的,lua脚本也是会存在reids内存中作为经常执行的命令串。

下面是固定窗口算法的lua脚本

 1 local key = KEYS[1]             
 2 local limit = tonumber(KEYS[2])   ## 限流
 3 local current = tonumber(redis.call(get, key) or 0)
 4 ?
 5 if current + 1 > limit then
 6     return 0
 7 else
 8     redis.call("INCRBY", key,"1")    ## 对key 加1操作
 9     redis.call("EXPIRE", key,"10")   ## 过期时间设为10秒
10     return 1
11 end

 

 

下面是令牌桶算法的lua脚本

 1 --- 获取令牌
 2 --- 返回码
 3 --- 0 没有令牌桶配置
 4 --- -1 表示取令牌失败,也就是桶里没有令牌
 5 --- 1 表示取令牌成功
 6 --- @param key 令牌(资源)的唯一标识
 7 --- @param permits 请求令牌数量
 8 --- @param curr_mill_second 当前毫秒数
 9 --- @param context 使用令牌的应用标识
10 ?
11 ?
12 local function acquire(key, permits, curr_mill_second, context)
13 redis.pcall("HSET", key, "max_permits", "5")
14 local rate_limit_info = redis.pcall("HMGET", key, "last_mill_second", "curr_permits", "max_permits", "rate", "apps")
15 local last_mill_second = rate_limit_info[1]
16 local curr_permits = tonumber(rate_limit_info[2])
17 local max_permits = tonumber(rate_limit_info[3])
18 local rate = rate_limit_info[4]
19 local apps = rate_limit_info[5]
20 --- 标识没有配置令牌桶
21 if type(apps) == boolean or apps == nil or string.find(apps, context, 1)~=nil  then
22 redis.pcall("HSET", key, "apps", context)
23 apps=context
24 end
25 if type(rate) == boolean or rate == nil then
26 redis.pcall("HSET", key, "rate", "0.2")
27 rate=0.2
28 end
29 if type(curr_permits) == boolean or curr_permits == nil then
30 redis.pcall("HSET", key, "curr_permits", "0")
31 end
32 local local_curr_permits = max_permits;
33 --- 令牌桶刚刚创建,上一次获取令牌的毫秒数为空
34 --- 根据和上一次向桶里添加令牌的时间和当前时间差,触发式往桶里添加令牌
35 --- 并且更新上一次向桶里添加令牌的时间
36 --- 如果向桶里添加的令牌数不足一个,则不更新上一次向桶里添加令牌的时间
37 if (type(last_mill_second) ~= boolean and last_mill_second ~= false and last_mill_second ~= nil) then
38 local reverse_permits = math.floor(((curr_mill_second - last_mill_second) / 1000) * rate)
39 local expect_curr_permits = reverse_permits + curr_permits;
40 local_curr_permits = math.min(expect_curr_permits, max_permits);
41 --- 大于0表示不是第一次获取令牌,也没有向桶里添加令牌
42 if (reverse_permits > 0) then
43 redis.pcall("HSET", key, "last_mill_second", curr_mill_second)
44 end
45 else
46 redis.pcall("HSET", key, "last_mill_second", curr_mill_second)
47 end
48 local result = -1
49 if (local_curr_permits - permits >= 0) then
50 result = 1
51 redis.pcall("HSET", key, "curr_permits", local_curr_permits - permits)
52 else
53 redis.pcall("HSET", key, "curr_permits", local_curr_permits)
54 end
55 return result
56 end
57 ?
58 ?
59 return acquire(KEYS[1],tonumber(KEYS[2]),tonumber(KEYS[3]),KEYS[4])

 

以上是关于后台工程师来讲讲限流的思路的主要内容,如果未能解决你的问题,请参考以下文章

架构设计之「服务限流」

如何计算服务限流的配额

流量激增不宕机,服务限流系统架构解密

分布式服务限流实战,已经为你排好坑了

SpringCloud升级之路2020.0.x版-31. FeignClient 实现断路器以及线程隔离限流的思路

SpringCloud升级之路2020.0.x版-31. FeignClient 实现断路器以及线程隔离限流的思路