高可用利器-限流

限流基本概念

限流是**流量限速(Rate Limit)**的简称,是一种常见的系统流量控制方式,它通过限制系统的请求或流量,来保证系统的稳定性和可靠性。对于Server而言,限流保证一部分的请求流量可以得到正常的响应,总好过全部的请求都不能得到响应,甚至导致系统雪崩。

熔断降级限流这三者经常会混淆,在这里介绍一下:

  • 熔断:在分布式系统中,下游应用会调用上游提供的服务,如果下游出现问题,下游还是盲目的调用上层的服务,这样会产生两个问题:① 增加整个链路的请求时间;② 下游出现问题,不断请求上层服务会加重系统问题,恢复困难。这个时候需要使用熔断,上游服务为了保护系统整体的可用性,可以暂时切断下游服务的调用
  • 降级:在高并发场景下,由于资源是有限的而请求是无限的,如果不做服务降级处理,一方面肯定会影响整体服务的性能,严重的话可能会导致宕机某些重要的服务不可用。服务降级是指当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心业务正常运作或高效运作

看到了一个很形象的比喻:

  1. 熔断:相当于你的一颗卒被围死了,就不要利用其它棋去救它了,弃卒保帅,否则救他的棋也可能被拖死。
  2. 降级:相当于尽量不要走用处不大的棋了,浪费走棋机会(资源),使已经过河的棋有更多的走棋机会(资源)发挥最大作用。
  3. 限流: 相当于尽量避免同时和两三个人同时下。

为什么需要限流?

因为目前互联网系统通常都要面对高并发的场景,在突发情况下(最常见的场景就是秒杀、抢购),瞬时大流量会直接将系统打垮,无法对外提供服务。为了防止出现这种情况最常见的解决方案之一就是限流,当请求达到一定的并发数或速率,就进行等待、排队、降级、拒绝服务等。

常见的限流的方式

虽然绝大多数情况下限流都是发生在服务端的,但也并不是只有在服务端可以发生限流:

  1. 客户端限流:可以通过在客户端设置请求的速率等限制参数,或者在客户端缓存一定量的数据,减轻服务器的压力。这种方式当达到阈值时遍不会请求服务端,避免服务端产生额外的资源消耗。但这种方式在遇到客户端数量增加或减少的情况就需要重新计算每个客户端的限流阈值,且当下游服务较多时,每个服务的不同API有不同的限流配额,会增加客户端限流的复杂性
  2. 服务端限流:可以通过限制QPS、并发量或连接数等参数进行限流。这种方式不会因客户端数量增加或减少而改变方便对不同上游服务进行不同阈值的限流策略。但这种方式通常只针对QPS限流,而不考虑连接数(服务在建连过程中也会产生一些资源消耗,而这些压力往往可能会成为瓶颈),如果对连接数进行限制,会造成因某个业务或服务的连接过多而其他服务的连接被限制。
  3. 网关限流:可以通过设置访问速率和黑名单等参数进行限流。这种方式可以很好的保护整个集群的负载压力,服务端数量增加或减少,则网关进行相应的阈值调整即可对不同的上游业务的服务设置不同的限流配额和不同的限流策略。但这种方式需要消耗网关资源,且要保证网关本身具有高可用性

常见的限流粒度

通常根据场景的不同,又可以选择不同的限流粒度:

  1. 服务粒度:一个服务提供一个统一的限流的策略。虽然非常简单,但很容易造成限流失效,无法保护服务本身及下游(下游请求过多导致下游崩溃)
  2. API粒度:不同的API进行不同的限流策略,这种方式相对复杂些,但是更为合理,也能很好的保护服务。需要注意的是:增加或减少API,则限流策略要做相应的调整若请求处理实现发生改变则需要重新对限流阈值进行调整,避免因增加业务逻辑导致服务本身或者校友服务过载
  3. API参数粒度:针对一些特殊场景,比如对于热点数据的访问频繁访问引起限流导致其他商品服务无法完成。

常见的限流后的处理方法

如果流量达到了设置的阈值,我们还需要对请求进行处理,常见的处理方法有:

  1. 返回错误信息:对于被限制的请求,可以返回适当的错误信息,告知用户该请求被限流了,并提供一些相关的提示或建议,例如稍后再试或减少请求频率等。
  2. 排队等待处理:对于需要等待处理的请求,可以将它们排队,并逐个处理。这样可以避免请求过载导致系统崩溃,并保证每个请求都得到及时处理。排队等待处理可以使用队列、消息中间件等技术实现。
  3. 降级处理:对于一些不是必须的请求,可以进行降级处理,例如在高峰期暂停某些功能、使用缓存等方式。这样可以降低系统的负载,并保证核心功能的正常运行。
  4. 重试处理:对于因为限流而被拒绝的请求,可以尝试重新发送。这样可以提高请求的成功率,并减少因为限流而导致的不必要的请求失败。

常见的单机限流算法

实现单机限流的算法有多种,比如固定窗口算法、滑动窗口算法、令牌桶算法、漏桶算法,下面依次详细介绍。

固定窗口算法

固定窗口算法是将我们的每秒钟分成固定大小的时间窗口,并在每个窗口时间内限制请求的数量。

固定窗口算法的实现很简单,我们只需要记录一个计数器,并在每个时间窗口结束时将其重置为零。每当请求进入系统时,我们都会将计数器加一,并检查它是否超过了限制的阈值。如果超过了限制的阈值,则拒绝该请求。

固定窗口算法

这种方式的缺点是:窗口是固定的,且在两个窗口边界可能会有突发流量问题。具体而言,时间窗口为1s,1s的限制阈值是4,如果一个恶意用户在1s的后500ms内发送了3个请求,又在下一秒的前500ms内发送了3个请求,相当于1s内接收了6请求,会出现超过流量限制的情况。

固定窗口算法 窗口边界流量突发的问题

滑动窗口算法

滑动窗口算法是对固定窗口算法的改进,解决了两个窗口边界可能会有突发流量问题。

滑动窗口算法的基本思想是在固定窗口算法的基础之上,将一个窗口分为若干个等份的小窗口,每个小窗口对应不同的时间点,拥有独立的计数器,当请求的时间点大于当前窗口的最大时间点时,则将窗口向前平移一个小窗口。如果请求数量超过了小窗口的限制的阈值,则拒绝该请求。

如下图所示,对1s的窗口划分为2个等长的小窗口,1s的限制阈值是4,那每个小窗口的限制阈值为2。

滑动窗口算法

令牌桶算法

与滑动窗口算法不同,令牌桶算法可以更加精细地控制请求速率。

令牌桶算法的基本思想是维护一个令牌桶,其中每个令牌表示可以处理的一个请求。令牌桶以固定速率生成令牌,并将其放入桶中。每当一个新的请求进入系统时,我们尝试从令牌桶中获取一个令牌。如果令牌桶为空,则拒绝该请求;否则,将令牌从令牌桶中移除,并处理该请求。

令牌桶算法

详细参考Guava的RateLimiter源码阅读。

漏桶算法

漏桶算法用于平滑限制请求速率。漏桶算法的基本思想是,维护一个固定大小的漏桶,并在漏桶中存储请求。每当一个新的请求进入系统时,我们将其放入漏桶中。如果漏桶已满,则拒绝该请求;否则,允许该请求进入漏桶,并以固定的速率将请求从漏桶中移除。这样,可以平滑地限制请求速率。

漏桶算法

算法比较

固定窗口算法 VS 滑动窗口算法

固定窗口算法是最为简单的限流算法,但是它存在边界可能会有突发流量问题。滑动窗口算法对固定窗口算法的改进,如果滑动窗口的精度越高,需要的存储空间就越大

令牌桶算法 VS 漏桶算法
  • 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝。
  • 令牌桶限制的是平均流入速率,允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌;漏桶限制的是常量流出速率,即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2,从而平滑突发流入速率
  • 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流出速率。

关于漏桶不适合应对突发情况的解释:这是因为漏桶以固定速率取出请求的。

举个例子,漏桶的容量为10,请求处理速率为2/s,那么可以容纳8个突发请求放入桶中,其他等待或者丢弃。

常见的分布式限流方案

单节点限流最大的问题是当服务节点动态添加或减少后,每个服务的限流配额也要跟随动态改变。而分布式限流则避免了这种问题,通过像Redis集群或发票服务器这种取号的方式来限制某个资源的流量。

Redis限流

基于Redis的单线程及原子操作特性来实现限流功能,这种方式可以实现简单的分布式限流。但是Redis本身也容易成为瓶颈,且Redis不管是主从结构还是其Cluster模式,都存在主节点故障问题。

方案一:固定窗口计数器

将要限制的资源名+时间窗口为精度的时间戳作为Redis的key,设置略大于时间戳的超时时间,然后用Redis的incrby的原子特性来增加计数。(存放当前窗口下的流量

具体的lua脚本为:

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local acquireCount = tonumber(ARGV[2])
local current = tonumber(redis.call('get', key) or "0")
if current + acquireCount > limit then
    return 0
else
    redis.call("incrby", key, acquireCount)
    redis.call("expire", key, "2")
    return 1
end

该方法存在的问题为:

  • 要和Redis进行交互,时延较差。
  • 热点资源Redis容易成为瓶颈。
  • Redis进行主从切换会导致限流失效。
  • 服务的时钟会有误差:由于lua中有写操作就不能使用带随机性质的读操作所以不能通过Redis lua获取。
  • 属于固定窗口算法,在窗口之间容易产生突发流量问题。
方案二:令牌桶

利用Hash集合,存放最后更新令牌数的时间之前剩余的令牌数最大令牌数、以及令牌的放入速率,利用这些信息来计算下一次的令牌书等内容。

local function acquire(key, acquireTokens, currentTimeMillSecond)

    local rateLimiterInfo = redis.pcall("HMGET", key, "lastTimeMilliSecond", "availableTokens", "maxLimit", "rate")
    local lastTimeMilliSecond = rateLimiterInfo[1]
    local availableTokens = tonumber(rateLimiterInfo[2])
    local maxLimit = tonumber(rateLimiterInfo[3])
    local rate = rateLimiterInfo[4]
    local currentTokens = availableTokens;

    local result = -1

    if (type(lastTimeMilliSecond) ~= 'boolean' and lastTimeMilliSecond ~= false and lastTimeMilliSecond ~= nil) then
        local diffTime = currentTimeMillSecond - lastTimeMilliSecond
        if diffTime > 0 then
            local fillTokens = math.floor((diffTime / 1000) * rate)
            local allTokens = fillTokens + availableTokens;
            currentTokens = math.min(allTokens, maxLimit);
        end
    end

    if (currentTokens - acquireTokens >= 0) then
        result = 1
        redis.pcall("HMSET", key, "lastTimeMilliSecond", currentTimeMillSecond, "availableTokens", currentTokens - acquireTokens)
    end

    return result
end

local key = KEYS[1]
local acquireTokens = ARGV[1]
local currentTimeMillSecond = ARGV[2]

local ret = acquire(key, acquireTokens, currentTimeMillSecond)
return ret

这种方法存在的问题:

  • 要和Redis进行交互,时延较差。
  • Redis容易成为瓶颈。(可能会造成每秒发放的令牌大于限流的令牌的数量,每个访问的之间的时间间隔都小于一个令牌的时间会造成该问题,可以采用浮点数计算的方法减小误差,不确定是否可行,望指正)
  • Redis进行主从切换会导致限流失效。
  • 服务的时钟会有误差:由于lua中有写操作就不能使用带随机性质的读操作所以不能通过Redis lua获取。

发票服务器

上述Redis方案,是将Redis作为一种发票服务器,但是由于Redis这种方案本身存在可用性问题(主从切换等),控制规则也比较简单,所以对于可用性要求比较高且规则复杂的需求,都选择自己开发服务器程序来作为发票服务器,比如阿里的Sentinel。

发票服务器一般由一些服务进程组成一个或多个发票集群。服务通过RPC向发票服务器领票,成功则可以执行,否则则进入限流机制。为了减少RPC通信带来的延迟,一般可以批量获取

发票规则可以存储道一致性存储或者数据库等,发票服务器定期更新或者监听通知来获取规则的变化。也可以通过其他服务来动态调整算法和阈值,然后通知发票服务器,也可以由发票服务器自己根据负载情况来计算。

发票服务器的特点:

  • 发票服务器可用性高:通过集群模式,且可以持久化到数据库。
  • 发票服务器负载均衡:服务从发票服务集群领票要注意发票服务器负载均衡,避免造成有的发票服务器发票领完有的却有大量剩余发票。
  • 发票服务器高性能:因为发票服务器的计算和存储都基于内存,所以性能不容易成为瓶颈。
  • 发票服务器一致性:类似于ID生成器,对于极高要求的场景,可以定期将发票服务器发票的信息等进行持久化存储,故障时再从中进行恢复。

参考文献:

  1. 看完终于搞懂了限流,限流的优缺点、应用场景
  2. SpringBoot中如何实现限流----1.对接口进行限流
  3. 限流的概念,算法,分布式限流以及微服务架构下限流的难点
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值