介绍/概念
限流就是在某个时间窗口对资源访问做限制,比如设定每秒最多100个访问请求;
主要的几种限流规则如下:
QPS和连接数控制
QPS(query per second)
可以设定IP维度的限流,也可以设置基于单个服务器的限流。
传输速率
资源的下载速度, 比如在Nginx中限制传输速度。
黑白名单
分布式环境
所谓的分布式限流, 区别于单机限流的场景,它把整个分布式环境中所有服务器当做一个整体来考量.
比如说针对IP的限流,我们限制了1个IP每秒最多10个访问,不管来自这个IP的请求落在了哪台机器上,只要是访问了集群中的服务节点,那么都会受到限流规则的制约。
分布式环境必须将限流信息保存在一个“中心化”的组件上,两个比较主流的限流方案:
- 网关层限流: 将限流规则应用在所有流量的入口处
- 中间件限流: 将限流信息存储在分布式环境中某个中间件里(比如Redis缓存),每个组件都可以从这里获取到当前时刻的流量统计,从而决定是拒绝服务还是放行流量
限流方案
Guava
RateLimiter
Guava是一个客户端组件,也就是说它的作用范围仅限于“当前”这台服务器,不能对集群以内的其他服务器施加流量控制。
网关层限流
服务网关,作为整个分布式链路中的第一道关卡,承接了所有用户来访请求.
网关层限流的架构考量
系统流量的分布层次抽象成一个简单的漏斗模型:
为什么说它是一个漏斗模型,因为流量自上而下是逐层递减的,在网关层聚集了最多最密集的用户访问请求,其次是后台服务。然后经过后台服务的验证逻辑之后,刷掉了一部分错误请求,剩下的请求落在缓存上,如果缓存中没有数据才会请求漏斗最下方的数据库,因此数据库层面请求数量最小(相比较其他组件来说数据库往往是并发量能力最差的一环,阿里系的MySQL即便经过了大量改造,单机并发量也无法和Redis、Kafka之类的组件相比)
目前主流的网关层有以软件为代表的Nginx,还有Spring Cloud中的Gateway和Zuul这类网关层组件,也有以硬件+软件为代表的F5(F5价钱贵到你怀疑人生)
Nginx限流
Nginx里的几种核心限流模式:
- 基于IP地址和基于服务器的访问请求限流
- 并发量(连接数)限流
- 下行带宽速率限制
中间件限流
Redis简直就是为服务端限流量身打造的利器。利用Redis过期时间特性,我们可以轻松设置限流的时间跨度(比如每秒10个请求,或者每10秒10个请求)。同时Redis还有一个特殊技能--脚本编程,我们可以将限流逻辑编写成一段脚本植入到Redis中,这样就将限流的重任从服务层完全剥离出来,同时Redis强大的并发量特性以及高可用集群架构也可以很好的支持庞大集群的限流访问。
限流组件
Sentinel
从架构维度考虑限流设计
在真实的大型项目里,不会只使用一种限流手段,往往是几种方式互相搭配使用,让限流策略有一种层次感,达到资源的最大使用率。
在这个过程中,限流策略的设计也可以参考前面提到的漏斗模型,上宽下紧,漏斗不同部位的限流方案设计要尽量关注当前组件的高可用
app端的访问请求首先会经过网关,在网关层我们的限流会做的比较宽松,等到请求通过网关抵达后台的商品详情页服务之后,再利用一系列的中间件+限流组件,对服务进行更加细致的限流控制(这里面还会包含熔断降级等一系列复杂的异常处理)
限流算法
令牌桶算法 - Token Bucket
令牌桶算法是目前应用最为广泛的限流算法, 两个关键角色:
- 令牌: 获取到令牌的Request才会被处理,其他Requests要么排队要么被直接丢弃
- 桶: 用来装令牌的地方,所有Request都从这个桶里面获取令牌, 桶所能容纳的令牌数量是一个固定的数值。
图示:
令牌生成:
令牌生成器根据一个预定的速率向令牌桶中添加令牌, 由于桶的容量是有限的,如果放满了,新的令牌就会被丢弃掉。
令牌发放器就像一个水龙头,在下面接水的桶子满了,水(令牌)自然就流到了外面。
令牌获取:
每个访问请求到来后,必须获取到一个令牌才能执行后面的逻辑。
访问请求较多的情况下, 可以设置一个“缓冲队列”来暂存这些请求;
队列可以增加一些策略, 比如设置队列中请求的存活时间,或者将队列改造为PriorityQueue,根据某种优先级排序,而不是先进先出, 算法是死的,人是活的, 根据具体需求而定.
漏桶算法 - Leaky Bucket
漏桶算法的前半段和令牌桶类似, 令牌桶是将令牌放入桶里,而漏桶是将访问请求放到桶里。同样的是,如果桶满了,那么后面新来的数据包将被丢弃。
漏桶算法的后半程是有鲜明特色的,它永远只会以一个恒定的速率将数据包从桶内流出。打个比方,如果我设置了漏桶可以存放100个数据包,然后流出速度是1s一个,那么不管数据包以什么速率流入桶里,也不管桶里有多少数据包,漏桶能保证这些数据包永远以1s一个的恒定速度被处理。
漏桶 vs 令牌桶
这两种算法都有一个“恒定”的速率和“不定”的速率。令牌桶是以恒定速率创建令牌,但是访问请求获取令牌的速率“不定”。而漏桶是以“恒定”的速率处理请求,但是这些请求流入桶的速率是“不定”的。
漏桶的特性决定了它不会发生突发流量, 那么它对后台服务输出的访问速率永远恒定; 而令牌桶的特性可以“预存”一定量的令牌,因此在应对突发流量的时候可以在短时间消耗所有令牌,其突发流量处理效率会比漏桶高,但是导向后台系统的压力也会相应增多。
滑动窗口 - Rolling Window
上图中黑色的大框就是时间窗口,我们设定窗口时间为5秒,它会随着时间推移向后滑动。我们将窗口内的时间划分为五个小格子,每个格子代表1秒钟,同时这个格子还包含一个计数器,用来计算在当前时间内访问的请求数量。那么这个时间窗口内的总访问量就是所有格子计数器累加后的数值。
滑动窗口有一个显著特点,当时间窗口的跨度越长时,限流效果就越平滑。打个比方,如果当前时间窗口只有两秒,而访问请求全部集中在第一秒的时候,当时间向后滑动一秒后,当前窗口的计数量将发生较大的变化,拉长时间窗口可以降低这种情况的发生概率.
需要注意的问题
imoocJava架构师体系课:跟随千万级项目从0到100全过程高效成长第17周.分布式接口幂等性,分布式限流第2章 分布式限流2-15分布式限流要注意的问题2-15分布式限流要注意的问题.md
为什么需要匀速限流
- 提高令牌利用率
- 不匀速可能导致服务雪崩的问题,被人造流量峰值攻击
限流组件的失效应该怎么办?
参考Spring Cloud和其他限流开源方案的做法,当限流组件失效的时候,默认不启用限流服务。
其实道理很简单,拒绝外部请求所造成的损失,远大于放行请求暴露出的潜在破绽。
如何确定限流上界
限流能“卡在”系统处理能力的上限附近,那是再好不过的了。这个数值不能靠猜,而必须基于事实依据。那么事实从哪里来?压力测试!
压测不仅仅是无脑打高流量,而是在基于一个合理的“预估”访问量级之下,对系统进行全方位的摸底。执行全链路压测,它不仅包含压力测试,还有故障演练,异地多活演练(突然切断一整个机房),弹性伸缩(紧急上线新机器提高算力),服务降级(核心主链路降级演练,考察系统的最低可用性)等等复杂的流程。
因此,在确定限流上界之前,我们要根据当前业务规模预估一个合理的访问量级,再乘以一个系数(比如1.2)保证当前系统有一部分设计余量(预留少量弹性空间),通过压测找到系统瓶颈加以巩固,先确保当前系统在这个量级下的可用性。在此之上,向上打流量,反复进行多次测试后分析汇总性能指标(QPS和连接数),将限流的上界设置在指标的「平均值」或者「中位数」附近。
实战: Guava RateLimiter
Quick Guide to the Guava RateLimiter | Baeldung
只用于单体应用内;
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
// 限流: 2次/秒
RateLimiter rateLimiter = RateLimiter.create(2);
// 阻塞式限流: 一直等待到有许可;
rateLimiter.acquire(1);
// 非阻塞式限流: 询问是否有许可;
Boolean isAcquire = rateLimiter.tryAcquire(2);
// 限定时间内的非阻塞限流: 在限定时间内等待许可;
rateLimiter.tryAcquire(2, 10, TimeUnit.MILLISECONDS);
Guava RateLimiter预热模型
imoocJava架构师体系课:跟随千万级项目从0到100全过程高效成长第17周.分布式接口幂等性,分布式限流第2章 分布式限流2-7GuavaRateLimiter预热模型.pdf
预热: 动态调整令牌发放速度,让流量变化更加平滑。根据桶内的令牌数量动态控制令牌的发放速率,让忙时流量和闲时流量可以互相平滑过渡。
举一个例子:某个接口设定了100个Request每秒的限流标准,同时使用令牌桶算法做限流。假如当前时间窗口内都没有Request过来,那么令牌桶中会装满100个令牌。如果在下一秒突然涌入100个请求,这些请求会迅速消耗令牌,对服务的瞬时冲击会比较大。
横坐标表示桶中令牌的梳理, 纵坐标表示令牌发放的时间间隔; 桶里的剩余令牌多, 发放速度慢;
比如: 限流是10r/s, 桶最大数据量是100, 桶中令牌数量小于50时, 0.1s发放一个令牌; 桶中令牌数量大于50时, 0.3秒发放一个;
实现流量预热的类是SmoothWarmingUp
, 重点关注doSetRate方法:
@Override
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = maxPermits;
// maxPermits表示令牌桶内最大容量,它由我们设置的预热时间除以稳定间隔获得
// 打个比方,如果stableIntervalMicros=0.1s,而我们设置的预热时间是2s
// 那么这时候maxPermits就是2除以0.1=20
maxPermits = warmupPeriodMicros / stableIntervalMicros;
// 这句不用解释了吧,halfPermits是最大容量的一半
halfPermits = maxPermits / 2.0;
// coldIntervalMicros就是我们前面写到的3倍间隔,通过稳定间隔*3计算得出
// 稳定间隔是0.1,3倍间隔是0.2,那么平均间隔是0.2
double coldIntervalMicros = stableIntervalMicros * 3.0;
// slope的意思是斜率,也就是前面我们图中预热阶段中画出的斜线(速率从稳定间隔向3x间隔变化的斜线)
// 它的计算过程就是一个简单的求斜率公式
slope = (coldIntervalMicros - stableIntervalMicros) / halfPermits;
// 计算目前令牌桶的令牌个数
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
// 如果令牌桶最大容量是无穷大,则设置当前可用令牌数为0
// 说实话这段逻辑没什么用
storedPermits = 0.0;
} else {
storedPermits = (oldMaxPermits == 0.0)
? maxPermits // 初始化的状态是3x间隔
: storedPermits * maxPermits / oldMaxPermits;
}
}
实战: nginx限流
Nginx官方版本限制IP的连接和并发分别有两个模块:
limit_req_zone 用来限制单位时间内的请求数,即速率限制;
limit_req_conn 用来限制同一时间连接数,即并发限制;
参考:
官方文档:
nginx documentation
博客:
死磕nginx系列--nginx 限流配置 - biglittleant - 博客园
Nginx 的两种限流方式 - 开发者头条
ngx_http_limit_req_module 限制请求数
Module ngx_http_limit_req_module
采用的漏桶算法 "leaky bucket";
示例:
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
...
server {
...
location /search/ {
limit_req zone=one burst=5;
Syntax: limit_req_zone key zone=name:size rate=rate [sync];
Default: —
Context: http
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
- 第一个参数:$binary_remote_addr 表示通过remote_addr这个标识来做限制,“binary_”的目的是缩写内存占用量,是限制同一客户端ip地址。
- 第二个参数:zone=one:10m表示生成一个大小为10M,名字为one的内存区域,用来存储访问的频次信息。
- 第三个参数:rate=1r/s表示允许相同标识的客户端的访问频次,这里限制的是每秒1次,还可以有比如30r/m的。
Syntax: limit_req zone=name [burst=number] [nodelay | delay=number];
Default: —
Context: http, server, location
limit_req zone=one burst=5 nodelay;
- 第一个参数:zone=one 设置使用哪个配置区域来做限制,与上面limit_req_zone 里的name对应。
- 第二个参数:burst=5,重点说明一下这个配置,burst爆发的意思,这个配置的意思是设置一个大小为5的缓冲区, 当有大量请求(爆发)过来时,超过了访问频次限制的请求可以先放到这个缓冲区内。
- 第三个参数:nodelay,如果设置,超过访问频次而且缓冲区也满了的时候就会直接返回503,如果没有设置,则所有请求会等待排队。
演示
default.conf 新增限流配置:
limit_req_zone $binary_remote_addr zone=one:10m rate=2r/s;
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
location /ratelimit/ {
limit_req zone=one burst=5 nodelay;
proxy_pass http://localhost:80/;
}
}
jmeter匀速每秒发送5个请求:
前两秒, 成功了9个请求, 包括5个burst请求, 2s * 2r/s= 4
个限流请求;
后面每秒成功2个请求, 分别是第1个和第3个;
ngx_http_limit_conn_module 限制连接数
Module ngx_http_limit_conn_module
示例:
http {
limit_conn_zone $binary_remote_addr zone=addr:10m;
...
server {
...
location /download/ {
limit_conn addr 1;
}
Syntax: limit_conn_zone key zone=name:size;
Default: —
Context: http
Syntax: limit_conn zone number;
Default: —
Context: http, server, location
ngx_http_core_module limit_rate 限速
location /flv/ {
flv;
limit_rate_after 20m;
limit_rate 100k;
}
这个限制是针对每个请求的,表示客户端下载前20M时不限速,后续限制100kb/s。