soul网关源码学习15-rate_limiter插件解析(下)

本文详细介绍了限流的重要性以及常见的限流算法,包括计数器固定窗口、滑动窗口、漏桶和令牌桶算法。重点解析了令牌桶算法的工作原理,强调其在处理突发流量和限制平均速率上的优势,并提供了lua脚本实现示例。文章指出,选择限流算法应根据实际场景,没有最好,只有最适合。
摘要由CSDN通过智能技术生成

soul网关源码学习15-rate_limiter插件解析(下)

目标:

  • 了解常用的限流算法
  • 接着上一讲的内容深入理解令牌桶算法

前言

分布式环境下应对高并发保证服务有稳定的几种做法,按照个人理解,优先级从高到低分别为缓存、限流、降级、熔断,其实这种说法也不完全准确,因为服务降级、熔断本身也是限流的一种,因为它们本质上也是阻断了流量进来。

一、为什么要限流

为什么要限流,其实很好理解的一个问题,就是流量过大了呗,造成流量过大的原因有很多,例如以下的几个场景:

  • 业务用户量不断攀升
  • 各种促销
  • 网络爬虫

但是这个流量的大,是有多大呢,这个其实没有一个标准。“大”一定是相对于正常的流量是要大很多,一旦超过了服务器能扛住的压力,那服务器自然就挂了,这时候服务也就熔断了。所以要在源头把流量限制下来,例如服务器只有支撑1000QPS的处理能力,那就每秒放1000个请求,自然保证了服务器的稳定,这就是限流。

二、常用的限流算法

计数器固定窗口算法

计数器固定窗口算法是最简单的限流算法,实现方式也比较简单。就是通过维护一个单位时间内的计数值,每当一个请求通过时,就将计数值加1,当计数值超过预先设定的阈值时,就拒绝单位时间内的其他请求。如果单位时间已经结束,则将计数器清零,开启下一轮的计数。
在这里插入图片描述

  • 一段时间内(不超过时间窗口)系统服务不可用。比如窗口大小为1s,限流大小为100,然后恰好在某个窗口的第1ms来了100个请求,然后第2ms-999ms的请求就都会被拒绝,这段时间用户会感觉系统服务不可用。
  • 窗口切换时可能会产生两倍于阈值流量的请求。比如窗口大小为1s,限流大小为100,然后恰好在某个窗口的第999ms来了100个请求,窗口前期没有请求,所以这100个请求都会通过。再恰好,下一个窗口的第1ms有来了100个请求,也全部通过了,那也就是在2ms之内通过了200个请求,而我们设定的阈值是100,通过的请求达到了阈值的两倍。其实这就是临界值问题,那么临界值问题要怎么解决呢?
    在这里插入图片描述

计数器滑动窗口算法

计数器滑动窗口算法是计数器固定窗口算法的改进,解决了固定窗口切换时可能会产生两倍于阈值流量请求的缺点。 滑动窗口算法在固定窗口的基础上,将一个计时窗口分成了若干个小窗口,然后每个小窗口维护一个独立的计数器。当请求的时间大于当前窗口的最大时间时,则将计时窗口向前平移一个小窗口。平移时,将第一个小窗口的数据丢弃,然后将第二个小窗口设置为第一个小窗口,同时在最后面新增一个小窗口,将新的请求放在新增的小窗口中。同时要保证整个窗口中所有小窗口的请求数目之后不能超过设定的阈值。窗口划分的越多,则限流越精准。
在这里插入图片描述

漏桶算法

漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出(拒绝服务)。
在这里插入图片描述
可以看出漏桶算法能强行限制数据的传输速率,但是缺点也非常明显:

  • 无法面对突发的大流量
    比如请求处理速率为1000,容量为5000,来了一波2000/s的请求持续10s,那么后5s的请求将全部直接被丢弃,服务器拒绝服务,但是实际上网络中突发一波大流量尤其是短时间的大流量是非常正常的,超过容量就拒绝,在很多场景中这是无法接受的。
  • 无法有效利用网络资源
    比如虽然服务器的处理能力是1000/s,但这不是绝对的,这个1000只是一个宏观服务器处理能力的数字,实际上一共5秒,每秒请求量分别为1200、1300、1200、500、800,平均下来qps也是1000/s,但是这个量对服务器来说完全是可以接受的,但是因为限制了速率是1000/s,因此前面的三秒,每秒只能处理掉1000个请求而一共打回了700个请求,白白浪费了服务器资源。

所以,其实一般都漏桶算法来限流的实际场景并不多,典型的就是我们常见的nginx,它的限流就是利用了漏桶算法。

令牌桶算法

相比漏桶算法,令牌桶算法是更为常用的一种限流算法,从某种意义上来说,令牌桶算法算是漏桶算法的一种改进,主要在于令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。

实现原理

在这里插入图片描述
整个过程是这样的:

  • 系统以恒定的速率产生令牌,然后将令牌放入令牌桶中。
  • 令牌桶有一个容量,当令牌桶满了的时候,再向其中放入的令牌就会被丢弃。
  • 每次一个请求过来,需要从令牌桶中获取一个令牌,如果有令牌,则提供服务;如果没有令牌,则拒绝服务。
突发流量

为什么令牌桶算法可以防止一定程度的突发流量呢?可以这么理解,假设我们想要的速率是1000QPS,那么往桶中放令牌的速度就是1000个/s,假设第1秒只有800个请求,那意味着第2秒可以容许1200个请求(假设令牌桶容量大于等于1200的情况下),这就是一定程度上突发流量的意思,反之我们看漏桶算法,第一秒只有800个请求,那么全部放过,第二秒这1200个请求将会被打回200个。

令牌桶容量

还有一个特别需要注意的点就是令牌桶容量的设置。假设还是1000QPS的速率,那么连续的5秒每秒钟放1000个令牌,第1秒钟800个请求过来,第2~4秒没有请求,那么按照令牌桶算法,第5秒钟可以接受4200个请求,但是实际上这可能已经远远超出了系统的承载能力,因此使用令牌桶算法特别注意设置桶中令牌的上限。

总而言之,作为对漏桶算法的改进,令牌桶算法在限流场景下被使用更加广泛。

三、令牌桶算法解析

整个令牌桶算法的流程如下:
在这里插入图片描述

  • 获取上一次请求之后剩余的令牌数,若令牌数不存在,有可能令牌桶是新的,也有可能是里面的令牌都已经过期了,则把剩余令牌数设置为令牌桶的容量。(注意这里是令牌数不存在而不是令牌数为0,因为令牌数为0时是指令牌已经用完,这时要拒绝服务了
  • 获取上一次请求的时间,如果不存在,或者过期了,则设置为0。
  • 计算填充后的令牌数,用当前时间戳减去上次请求的时间戳,差值乘以填充令牌的速率,得出来的值是需要填充的令牌数,再加上剩余的令牌数就等于填充后的令牌数。若最后这个值大于令牌桶的容量,则填充后的令牌数就是令牌桶的容量。
  • 判断填充后的令牌数是否大于等于1,若是,放行请求,否则,拒绝服务。

lua脚本实现

--上一次请求之后剩余令牌数的key
local tokens_key = KEYS[1]
--上一次请求时间的key
local timestamp_key = KEYS[2]
--放令牌的速度
local rate = tonumber(ARGV[1])
--令牌桶的容量,即一秒内允许的最大请求数
local capacity = tonumber(ARGV[2])
--Instant.now().getEpochSecond()当前时间戳(单位:秒)
local now = tonumber(ARGV[3])
--当前请求数,默认1
local requested = tonumber(ARGV[4])
--填满令牌桶的时间
local fill_time = capacity/rate
--令牌的有效时间,等于填满令牌桶的两倍时间
--这里我的理解是保证在填满令牌桶的时候所有的令牌桶都是有效的
local ttl = math.floor(fill_time*2)
--上次请求之后剩余的令牌数
local last_tokens = tonumber(redis.call("get", tokens_key))
--如果不存在,有可能桶是新的,也有可能是里面的令牌都已经过期了
--初始化为桶的容量
if last_tokens == nil then
  last_tokens = capacity
end
--上一次请求时间
local last_refreshed = tonumber(redis.call("get", timestamp_key))
--如果不存在,或者过期了,设置为0
if last_refreshed == nil then
  last_refreshed = 0
end
--当前时间和上次请求的时间的差值
local delta = math.max(0, now-last_refreshed)
--当前剩余的令牌数+上次请求到当前请求期间放入桶中的令牌数
--如果计算出来的填充令牌数大于桶的容量,则取桶的容量
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
--当前填充的令牌数是否大于1
local allowed = filled_tokens >= requested
--设置最新桶中的令牌数
local new_tokens = filled_tokens
--定义一个允许的请求数
local allowed_num = 0
--如果上面的allowed 为true,也就是剩余的令牌数大于1
if allowed then
  --当前请求后剩余的令牌数
  new_tokens = filled_tokens - requested
  --允许访问
  allowed_num = 1
end

--缓存请求后令牌数
redis.call("setex", tokens_key, ttl, new_tokens)
--缓存当期时间
redis.call("setex", timestamp_key, ttl, now)

--返回是否允许和当前剩余的令牌数
return { allowed_num, new_tokens }

四、小结

  • 计数器固定窗口算法
    实现简单,容易理解,但是在窗口切换时可能会产生两倍于阈值流量的请求。
  • 计数器滑动窗口算法
    作为计数器固定窗口算法的一种改进,有效解决了窗口切换时可能会产生两倍于阈值流量请求的问题
  • 漏桶算法
    能够对流量起到整流的作用,让随机不稳定的流量以固定的速率流出,但是不能解决流量突发的问题。
  • 令牌桶算法
    作为漏桶算法的一种改进,除了能够起到平滑流量的作用,还允许一定程度的流量突发。

五、总结

总结一句就是:没有最好的算法,只有最适合的算法,主要还是看使用场景。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值