常见限流方案设计

概述

限流在日常生活中很常见,比如在节假日期间去一个旅游景点,为了防止人流量过大,景点管理方通常会在入口处设置拦截,限制进入景点的人数,等有游客出去后才放新游客进去。对应到计算机中,比如要办活动、秒杀等,由于系统有自己的最大服务能力,即在达到某个临界点之前,系统都可以正常提供服务。为了保证系统在面临临界流量时仍然可以对外提供服务,这时就需要限流技术。

限流可以分为技术层面的限流和业务层面的限流。技术层面的限流比较通用,各个业务场景都可以用到。业务层面的限流则需要根据具体的业务场景做开发。

技术层面的限流

技术层面的限流有两种方案:

  • 限制并发数,也就是根据系统的最大资源量进行限制,比如数据库连接池、线程池、Nginx的limit_conn模块。
  • 限制速率(QPS),比如Guava的RateLimiter、Nginx的limit_req模块。

限制速率这种方式对于服务的接口调用非常有用。比如通过压力测试知道服务的QPS是2000,那么就可以限流2000QPS。当调用方的并发量超过了这个数字会直接拒绝提供服务,虽然部分请求被拒绝了,但是保证了其他请求可以正常处理。

业务层面的限流

比如在秒杀系统中,一个商品的库存只有100件,有2万人抢购,没有必要放2万个人进来,可以只放前500个,后面的人直接返回已售完即可。

针对这种业务场景,可以做一个限流系统(或者叫票据系统),票据系统中存放着500张票据,每进来一个人领一张票据,领到票据的人再进入后面的业务系统进行抢购。对于领不到票据的人,则返回已售完。

具体实现上,可以使用Redis或Nginx+Lua脚本实现。

限流算法

常用的限流算法有计数器算法、漏桶算法、令牌桶算法和滑动时间窗口算法。

计数器算法

计数器算法“简单粗暴”,该算法会维护一个counter,规定在单位时间内counter的大小不能超过最大值,每隔固定时间将counter值归零。如果counter值大于设定的阈值,那么系统就开始拒绝请求以保护系统的负载。

漏桶算法

如下图所示,在漏桶算法中会维护一个固定容量的桶,这个桶按照指定速率漏水。如果桶空了就停止漏水。

请求到达系统就类似于水加到桶中,这个速度可以是匀速的也可以瞬间的,如果桶满了就忽略后面来的请求,直到桶可以存放多余的水。

漏桶算法的好处是可以将系统的处理能力维持在一个比较平稳的水平,缺点是在瞬间流量过来时会拒绝后续的请求流量。

一般来说,代码实现时会使用一个队列实现“漏桶”效果,当请求过多时,队列中的请求开始积压,当队列满了系统就开始拒绝请求。

令牌桶算法

令牌桶与漏桶算法的效果一致,但是原理相反:如下图所示,随着时间的流逝,系统会按照指定速率往令牌桶中添加token,每来一个新请求就从桶中拿走一个token,没有token就拒绝服务。这种算法的好处是便于控制系统的处理速度,甚至可以通过统计信息实时优化令牌桶的大小。

漏桶算法和令牌桶算法一个是保持流出速率恒定,另一个是保持流入速率恒定。两者用途有一些差别,令牌桶限制的是平均流入速率而不是瞬时速率,因为可能出现一段时间没有请求进来,令牌桶中存满了令牌,然后短时间内突发流量过来,一瞬间从桶中拿令牌出来;漏桶有点类似消息队列,起到削峰的作用,平滑了突发流入速率。

Guava中的concurrent包中提供了限流工具类RateLimiter,使用了令牌桶算法,它支持两种令牌获取接口:获取不到就一直阻塞;在指定时间内获取不到就阻塞,超过这个时间就返回获取失败。

滑动时间窗口算法

滑动时间窗口算法就是根据当前时间获取对应的时间窗口,时间窗口保存有流量相关的统计值,根据该统计值判断是否触发流控。

一般来说,时间窗口可以循环复用,在复用时重新初始化即可。滑动时间窗口能够支持的瞬时流量最大可为该窗口上限,而令牌桶算法能够支持的瞬时流量最大为桶大小。

参考文献: