限流详解

背景

在开发高并发系统时,有很多手段可以用来保护系统,如:缓存、降级和限流等。在某些场景下并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页)等,因此需要有一种手段来限制这些场景下的并发/请求量,这种手段就是限流。

限流的目的是通过对并发访问、请求进行限流或者一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或者等待(比如秒杀、评论、下单)、降级(返回兜底数据或者默认值,如商品详情默认有货)。在压测时,我们能找到每个系统的处理峰值,然后通过设定峰值阀值,来防止当系统过载时,通过拒绝过载的请求来保障系统可用。另外,也应根据系统的吞吐量,响应时间、可用率来动态调整限流阀值。

限流算法

常见的限流算法有:令牌桶、漏桶。计数器也可以用来进行粗暴的限流。

令牌桶算法

在 Wikipedia 上,令牌桶算法是这么描述的:

  1. 每秒会有r个令牌放入桶中,或者说,每过 1/r 秒桶中增加一个令牌
  2. 桶中最多存放 b 个令牌,如果桶满了,新放入的令牌会被丢弃
  3. 当一个n字节的数据包到达时,消耗n个令牌,然后发送该数据包
  4. 如果桶中可用令牌小于n,则该数据包将被缓存或丢弃

                                        193244_jaVw_2939155.png

                                                        图-令牌桶算法示意图

漏桶算法

  1. 一个固定容量的漏桶,按照常量固定速率流出水滴;
  2. 如果桶是空的,则不需流出水滴;
  3. 可以以任意速率流入水滴到漏桶;
  4. 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

                                                193307_Ifqq_2939155.png

                                                        图-令牌桶限流算法

令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,允许一定程度的突发流量)

漏桶限制的是常量流出速率,从而可以平滑突发流入速率

令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率。

计数器限流

                                    093341_Qk8S_2939155.png

                                                         图-计数器限流示意图

它是限流算法中最简单最容易的一种算法,比如我们要求某一个接口,1分钟内的请求不能超过10次,我们可以在开始时设置一个计数器,

每次请求,该计数器+1;如果该计数器的值大于10并且与第一次请求的时间间隔在1分钟内,那么说明请求过多;如果该请求与第一次请求的时间间隔大于1分钟,并且该计数器的值还在限流范围内,那么重置该计数器。具体代码如下:

public class CounterDemo {
    public long timeStamp = getNowTime();
    public int reqCount = 0;
    public final int limit = 100; // 时间窗口内最大请求数
    public final long interval = 1000; // 时间窗口ms
    public boolean grant() {
        long now = getNowTime();
        if (now < timeStamp + interval) {
            // 在时间窗口内
            reqCount++;
            // 判断当前时间窗口内是否超过最大请求控制数
            return reqCount <= limit;
        }
        else {
            timeStamp = now;
            // 超时后重置
            reqCount = 1;
            return true;
        }
    }
}

不过,以上代码有致命问题,当遇到恶意请求,在0:59时,瞬间请求100次,并且在1:00请求100次,那么这个用户在1秒内请求了200次,用户可以在重置节点突发请求,而瞬间超过我们设置的速率限制,用户可能通过算法漏洞击垮我们的应用。如下图,如何解决呢,看下边的滑动窗口算法。

                                    093620_ALqu_2939155.png

                                                图-计数器限流漏洞示意图

滑动窗口

                                    093650_BV0e_2939155.png

                                                    图-滑动窗口限流示意图

在上图中,整个红色矩形框是一个时间窗口,在我们的例子中,一个时间窗口就是1分钟,然后我们将时间窗口进行划分,如上图我们把滑动窗口划分为6格,所以每一格代表10秒,每超过10秒,我们的时间窗口就会向右滑动一格,每一格都有自己独立的计数器,例如:一个请求在0:35到达,那么0:30到0:39的计数器会+1,那么滑动窗口是怎么解决临界点的问题呢?如上图,0:59到达的100个请求会在灰色区域格子中,而1:00到达的请求会在红色格子中,窗口会向右滑动一格,那么此时间窗口内的总请求数共200个,超过了限定的100,所以此时能够检测出来触发了限流。回头看看计数器算法,会发现,其实计数器算法就是窗口滑动算法,只不过计数器算法没有对时间窗口进行划分,所以是一格。由此可见,当滑动窗口的格子划分越多,限流的统计就会越精确。滑动窗口算法实现

应用级限流

限制总并发/连接/请求数

应用系统一定会存在极限并发/请求数,即总有一个TPS/QPS阀值。

Tomcat,Connector配置参数:acceptCount(超出排队大小,则拒绝连接),maxConnections(瞬时最大连接数,超出的会排队等待),maxThreads(处理请求的最大线程数)

MySQL的max_connections、Redis的tcp-backlog有类似限制连接数的参数配置。

限制总资源数

稀缺资源如:数据库连接、线程可以使用池化技术限制总资源数,如连接池、线程池。

限制某个接口的总并发/请求数

某些接口可能会有突发访问情况,为避免访问量太大造成崩溃,如抢购业务,那么此时就需要限制这个接口的总并发/请求数了,超出限额,要么让用户排队,要么告诉用户没货了,这对用户来说是可以接受的。因为粒度比较细,可以为每个接口都设置相应的阀值。可以使用Java中的AtomicLong或者Semaphore进行限流。

限制某个接口的时间窗请求数

限制一个时间窗口内的请求数

平滑限流某个接口的请求数

某些场景下需要对突发请求进行整形,整形为平均速率请求处理,这个时候有两种算法满足我们的场景,令牌桶和漏桶算法。Guava框架RateLimiter类提供了令牌桶算法实现,可以直接拿来使用,可用于平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。

SmoothBursty允许一定程度的突发,会有人担心如果允许这种突发,假设突然来了很大的流量,那么系统很可能扛不住这种突发。因此,需要一种平滑速率的限流工具,从而在系统启动后慢慢趋于平均固定速率(即刚开始速率小一些,然后慢慢趋于我们设置的固定速率),SmoothWarmingUp实现了这种需求,其可以认为是漏桶算法,但是在某些特殊场景下又不太一样。参数warmupPeriod表示从冷启动速率过渡到平均速率的时间间隔。

分布式限流

分布式限流算法的是指算法可以分布式部署在多台机器上面,多台机器协同提供限流功能,可以对同一接口或者服务做限流。分布式限流算法相较于单机的限流算法,最大的区别就是接口请求计数器需要中心化存储

解决方案可以使用Redis+Lua或者Nginx+Lua技术进行实现,通过这两种技术可以实现高并发和高性能。在采用Redis做分布式限流时需要考虑的点有:

  • 数据一致性问题

接口限流过程包含读取-判断-写入三步,在并发情况下这3步CAS操作 (compare and swap) 存在race condition,但在分布式环境下引入分布式锁代价较大,可以考虑借助Redis单线程工作模式+Lua脚本完美的支持上述操作的原子性。

  • 超时问题

如果Redis访问超时,会严重影响接口的响应时间甚至导致接口响应超时,这个副作用是不能接受的。所以在我们访问 Redis 时需要设置合理的超时时间,一旦超时,判定为限流失效,继续执行接口逻辑。Redis访问超时时间的设置既不能太大也不能太小,太大可能会影响到接口的响应时间,太小可能会导致太多的限流失效。我们可以通过压测或者线上监控,获取到Redis访问时间分布情况,再结合服务接口可以容忍的限流延迟时间,权衡设置一个较合理的超时时间。

  • 性能问题

在应用分布式限流算法时,一定要考量限流算法的性能是否满足应用场景,如果微服务接口的TPS已经超过了限流框架本身的TPS,则限流功能会成为性能瓶颈影响接口本身的性能。分布式限流算法的性能瓶颈主要在中心计数器Redis,在存在瓶颈的情况下结合Redis sharding将分布式限流进行分片,或者当并发量太大时降级为应用级限流。

  • 其它问题

因为Redis的限制(Lua中有写操作不能使用带随机性质的读操作,如TIME)不能在Redis Lua中使用TIME获取时间戳,因此只好从应用获取然后传入,在某些极端情况下(机器时钟不准的情况下),限流会存在一些小问题。

如何进行分布式环境下机器时间同步?

参考ntp时间服务器 时间同步分布式系统中的时间(一)——时间的同步

接入层限流

接入层通常指请求流量的入口,该层的主要目的有:负载均衡、非法请求过滤、请求聚合、缓存、降级、限流等等。对于Nginx接入层限流可以使用Nginx自带了两个模块:连接数限流模块ngx_http_limit_conn_module和漏桶算法实现的请求限流模块ngx_http_limit_req_module。还可以使用OpenResty提供的Lua限流模块lua-resty-limit-traffic进行更复杂的限流场景。

limit_conn

limit_conn是对某个KEY对应的总的网络连接数进行限流。可以按照IP来限制IP维度的总连接数,或者按照服务域名来限制某个域名的总连接数。但是记住不是每一个请求连接都会被计数器统计,只有那些被Nginx处理的且已经读取了整个请求头的请求连接才会被计数器统计。

limit_req

limit_req是漏桶算法实现,用于对指定KEY对应的请求进行限流,比如按照IP维度限制请求速率,并有两种用法:平滑模式(delay)和允许突发模式(nodelay)。

lua-resty-limit-traffic

OpenResty提供了lua限流模块lua-resty-limit-traffic,通过它可以按照更复杂的业务逻辑进行动态限流处理。

限流思考

选择单机限流还是分布式限流

所谓单机限流是指:独立的对集群中的每台实例进行接口限流,比如限制每台实例接口访问的频率为最大 1000 次 / 秒,单机限流一般使用单机限流算法;所谓的分布式限流是指:提供服务级的限流限制对微服务集群的访问频率,比如限制A调用方每分钟最多请求1万次“用户服务”,分布式限流既可以使用单机限流算法也可以使用分布式限流算法。

单机限流的初衷是防止突发流量压垮服务器,所以比较适合针对并发做限制分布式限流适合做细粒度限流或者访问配额,不同的调用方对不同的接口执行不同的限流规则,所以比较适合针对 hits per second 限流。从保证系统可用性的角度来说,单机限流更具优势,从防止某调用方过度竞争服务资源来说,分布式限流更加适合。

分布式限流与微服务之间如何部署

方式一,在接入层(api-gateway)集成限流功能,这种集成方式是在微服务架构下,有 api-gateway 的前提下,最合理的架构模式。如果 api-gateway 是单实例部署,使用单机限流算法即可。如果 api-gateway 是多实例部署,为了做到服务级别的限流就必须使用分布式限流算法。

方式二,把限流功能封装为RPC服务,这种部署架构,性能瓶颈会出现在微服务与限流服务之间的RPC通信上,一般不建议采用。

方式三,限流功能集成在微服务系统内,这种架构模式不需要再独立部署服务,减少了运维成本,但限流代码会跟业务代码有一些耦合,不过,可以将限流功能集成在切面层,尽量跟业务代码解耦。如果做服务级的分布式限流,必须使用分布式限流算法,如果是针对每台微服务实例进行单机限流,使用单机限流算法就可以。

如何选择限流算法

令牌桶和漏桶算法比较适合阻塞式限流(也可以立即返回失败,从而达到否决式限流的效果),比如一些后台job类的限流,超过了最大访问频率之后,请求并不会被拒绝,而是会被阻塞到有令牌后再继续执行,进行排队请求。对于像微服务接口这种对响应时间比较敏感的限流场景,会比较适合选择基于时间窗口的否决式限流算法,直接拒绝请求,比如返回HTTP code 429,其中滑动时间窗口限流算法空间复杂度较高,内存占用会比较多,所以对比来看,尽管固定时间窗口算法处理临界突发流量的能力较差,但实现简单,而简单带来了好的性能和不容易出错,所以固定时间窗口算法也不失是一个好的微服务接口限流算法。

如何配置合理的限流规则

限流规则包含三个部分:时间粒度,接口粒度,最大限流值。限流规则设置是否合理直接影响到限流是否合理有效。

对于限流时间粒度的选择,我们既可以选择 1 秒钟不超过 1000 次,也可以选择 10 毫秒不超过 10 次,还可以选择 1 分钟不超过 6 万次,虽然看起这几种限流规则都是等价的,但过大的时间粒度会达不到限流的效果,比如限制 1 分钟不超过 6 万次,就有可能 6 万次请求都集中在某一秒内;相反,过小的时间粒度会削足适履导致误杀很多本不应该限流的请求,因为接口访问在细时间粒度上随机性很大。所以,尽管越细的时间粒度限流整形效果越好,流量曲线越平滑,但也并不是越细越合适。

对于访问量巨大的接口限流,比如秒杀,双十一,这些场景下流量可能都集中在几秒内,TPS 会非常大,几万甚至几十万,需要选择相对小的限流时间粒度。相反,如果接口 TPS 很小,建议使用大一点的时间粒度,比如限制 1 分钟内接口的调用次数不超过 1000 次,如果换算成:一秒钟不超过 16 次,这样的限制就有点不合理,即便一秒内超过 16 次,也并没有理由就拒绝接口请求,因为对于我们系统的处理能力来说,16 次 / 秒的请求频率太微不足道了。即便 1000 次请求都集中在 1 分钟内的某一秒内,也并不会影响到系统的稳定性,所以 1 秒钟 16 次的限制意义不大。

如何评判限流功能是否正确有效

如何测试限流功能正确有效呢?尽管可以通过模拟流量或者线上流量回放等手段来测试,但是最有效的测试方法还是:通过导流的方式将流量集中到一小组机器上做真实场景的测试。对于测试结果,我们至少需要记录每个请求的如下信息:对应接口,请求时间点,限流结果 (通过还是熔断),然后根据记录的数据做对比分析。

除了事先验证之外,我们还需要时刻监控限流的工作情况,实时了解限流功能是否运行正常。一旦发生限流异常,能够在不重启服务的情况下,做到热更新限流配置:包括开启关闭限流功能,调整限流规则,更换限流算法等。

节流

有时候我们想在特定时间窗口内对重复的相同事件最多只处理一次,或者想限制多个连续相同事件最小执行时间间隔,那么可使用节流(Throttle)实现,其防止多个相同事件连续重复执行。节流主要有如下几种用法:throttleFirst、throttleLast、throttleWithTimeout。可以参考限流详解之节流

参考资料

《亿级流量网站架构核心技术——跟开涛学搭建高可用高并发系统》

微服务接口限流的设计与思考

分布式限流

关于Guava中令牌桶算法RateLimiter的理解

转载于:https://my.oschina.net/u/2939155/blog/910902

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值