提示:以下是本篇文章正文内容,下面案例可供参考
我们首先思考一下实际生活中有没有被限流的经历
DID:设计(10)-实现(5)-部署(2-3)考虑最大的问题,就是我们的部署成本
系统如果出现流量并发过大导致崩溃,我们如何去进行处理
系统架构的性能永远是设计出来的(做预判和评估)
主动型:
被动型:
降级系统压力的最有效的方式就是减少访问的流量,就是将用户拒之门外。可以通过一句友好提示“服务正忙,请稍后重试”,之后就将用户丢弃不管了。这种方式比较直接,对于用户体验不好。
买火车票,基本都是在流量特别大的高峰阶段,让你每次都要输入对应的验证信息的结果,这就是赤裸裸的限流方法:业务限流
对一般的限流场景来说它具有两个维度信息:
限流其实就是在某个时间窗口对资源访问做限制,比如设定每秒最多100个访问请求。但在真正的场景里,我们不止设置一种限流规则,而是限制多个限流规则共同使用,主要的几种限流规则如下:
基于QPS,可以设定基于IP纬度的限制,也可以设置基于某个服务器的限制,真实环境中通常会设置多个维度的限流规则,比如同一个IP每秒访问频率小于10,连接数小于5,再设定每台机器QPS最高1000,连接最大保持200
下载方式的传输流量限制,普通用于下载速度100k/s,会员10m/s,这背后就是用户组或用户标签下的限流逻辑
黑名单就是用来做IP访问限制的
白名单就是将IP加入后,跳出限流规则的方式
所谓的分布式限流,其实道理很简单,它将整个分布式环境中所有服务器当做一个整体来考量。比如IP的限流,1秒可以访问10次,无论这个IP落在哪个机器上,只要访问的是集群中的服务节点,都会受到限流规则的制约
从上面这个例子来看,我们必须要将限流信息保存在一个“中心化”的组件上,这样它就可以获取到集群中所有机器的访问状态,目前两个比较主流限流方案:
Google的Guava客户端组件,是Google的一款非常好用的工具包,Guava提供了一个RateLimiter的实现,主要就是进行限流支持的,既然Guava是一个客户端组件,也就说明它的作用范围仅限于当前的服务器,不能对集群内的其他服务器施加流量限制
尽管Guava不是面向分布式系统的解决方案,但作为一个简单轻量级的客户端限流组件,非常适合进行业务限流
在整个分布式系统中,服务网关,作为整个分布式链路中的第一道关卡,承接了所有用户来访问的请求,我们进行系统访问的过程中,网关限流将所有流量都在入口处进行屏蔽了
对于业务系统需要有一个类似中心节点的存储限流状态数据的地方,由这个地方进行限流的设置,而Redis就是最适合的中间件限流组件,对于限流能够提供相关请求和存储的支持
比如sentinel就提供限流的具体实施方式,很好的集成在sentinel中了
无论是Guava还是Nginx、Redis限流,到最后都是应用几种具体的限流算法
常见的限流算法:令牌桶算法、漏桶算法、滑动窗口和计数器算法来结合使用
Token Bucket令牌桶算法目前应用最为广泛的限流算法,顾名思义,他有以下两个关键角色
1、令牌:只有获得到令牌的Request请求才会被处理,其他Request请求要么排队要么被直接丢弃
2、桶:用来装令牌的地方,所有Request都从这个桶里获取令牌
对于令牌生成器来说,他会根据一个预定的速率向桶中添加令牌,发放速率是匀速的
每个访问请求到来后,必须获取到一个令牌才能执行后面的逻辑,如果访问请求过多,无法获得令牌,这个时候就可以在获取令牌前将请求消费放入MQ做缓存,如果令牌够就ACK,如过不够就NACK重归队列,通过MQ来缓存暂时没有拿到令牌的请求
漏桶算法的前半段和令牌桶类似,但操作的对象不同,令牌桶是将令牌放入桶中,而漏桶是将请求放入桶中,如果桶满了,后面来的数据包将会被丢失
漏桶算法的后半段永远只会以一个恒定的速率将数据包从桶内流出
根据各自的特点不难看出,这两种算法都有一个恒定的速率和不定的速率,令牌桶是以恒定速率创建令牌,但是访问请求获取令牌的速率不定,反正有多少令牌就发多少,令牌没了就干等。而漏桶是以恒定的速率处理请求,但是这些请求流入桶的速率是不定的
从这两个特点来讲,漏桶的天然特性决定了它不会发生突发流量,就算1000个请求到来,那么它对于后的服务输出的访问速率永远恒定,而令牌桶不一样,因为可以预存一定量的令牌,因此在应对突发流量的时候可以在短时间内消耗所有的令牌,突发流量的处理效率会比漏桶高,但是导向后台系统的压力也会相应增多
上面的大框就是时间窗口,我们设定时间窗口是5秒,随着时间向后滑动,时间盒子不断向后刷新,第1秒有5个请求,第5秒有10个请求,一共有15个请求,时间盒子向后移动了1秒,将第1秒的5个请求已经移出,将第6秒加入,还可以有20-10=10个请求加入
POM依赖
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>28.2-jreversion>
dependency>
controller
package com.icodingedu.supermall.controller;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
@Slf4j
public class LimiterController {
//允许这个限流组件每秒发放两个令牌
RateLimiter rateLimiter = RateLimiter.create(2.0);
@GetMapping("/tryAcquire")
public String tryAcquire(Integer count){
//如果每秒消耗1个令牌,一秒可以接收2次请求,消耗2个则1秒接收1个,消耗4个需要2秒
if(rateLimiter.tryAcquire(count)){
log.info("success rate is {}",rateLimiter.getRate());
return "success";
}else{
log.info("fail rate is {}",rateLimiter.getRate());
return "fail";
}
}
//增加了等待时间的方式
@GetMapping("/tryAcquirewithtimeout")
public String tryAcquire(Integer count,Integer timeout){
//如果每秒消耗1个令牌,一秒可以接收2次请求,消耗2个则1秒接收1个,消耗4个需要2秒
//timeout是指请求的时间间隔
if(rateLimiter.tryAcquire(count,timeout, TimeUnit.SECONDS)){
log.info("success rate is {}",rateLimiter.getRate());
return "success";
}else{
log.info("fail rate is {}",rateLimiter.getRate());
return "fail";
}
}
}
//使用阻塞式限流
@GetMapping("/acquire")
public String acquire(Integer count){
rateLimiter.acquire(count);
log.info("success rate is {}",rateLimiter.getRate());
return "success";
}
令牌不够则等待令牌获取够了才执行后续内容
目前还是在使用Guava RateLimiter,是单机限流,还不是分布式限流
# 首先在/etc/hosts增加一个本地域名映射
# 127.0.0.1 limit.icodingedu.com
# 根据IP地址限制访问流量
# 1.$binary_remote_addr:相当于获取来源ip的一个系统变量
# 2.zone=iplimit:20m:iplimit相当于自己开辟的存放限流数据的一个内存区域,20m是指内存大小
# 3.rate=1r/s:标示限流的频率,100r/m
limit_req_zone $binary_remote_addr zone=iplimit:20m rate=1r/s;
server {
server_name limit.icodingedu.com;
location /access-limit/ {
proxy_pass http://127.0.0.1:8080/;
# 1.zone=iplimit:引用zone中的变量
# 2.burst=2,设置一个大小为2的缓存区域,当大量请求到来时,请求超过限流频率时,放入缓冲区
# 3.nodelay:缓存区满了以后直接返回503
limit_req zone=iplimit burst=2 nodelay;
}
}
加入controller测试
@RestController
public class NginxController {
@GetMapping("/nginx-conn")
public String nginxConn(){
return "nginx-success";
}
}
springboot访问地址:http://limit.icodingedu.com:8080/nginx-conn
nginx访问地址:http://limit.icodingedu.com/access-limit/nginx-conn
# 首先在/etc/hosts增加一个本地域名映射
# 127.0.0.1 limit.icodingedu.com
# 根据IP地址限制访问流量
# 1.$binary_remote_addr:相当于获取来源ip的一个系统变量
# 2.zone=iplimit:20m:iplimit相当于自己开辟的存放限流数据的一个内存区域,20m是指内存大小
# 3.rate=1r/s:标示限流的频率,100r/m
limit_req_zone $binary_remote_addr zone=iplimit:10m rate=10r/s;
# 根据服务器级别做限流
limit_req_zone $server_name zone=serverlimit:10m rate=1r/s;
server {
server_name limit.icodingedu.com;
location /access-limit/ {
proxy_pass http://127.0.0.1:8080/;
# 1.zone=iplimit:引用zone中的变量
# 2.burst=2,设置一个大小为2的缓存区域,当大量请求到来时,请求超过限流频率时,放入缓冲区
# 3.nodelay:缓存区满了以后直接返回503
limit_req zone=iplimit burst=2 nodelay;
# 基于服务器的限流,一般来讲server级别限流是比较大的,但这里做测试就缩小来看
limit_req zone=serverlimit burst=1 nodelay;
}
}
# 首先在/etc/hosts增加一个本地域名映射
# 127.0.0.1 limit.icodingedu.com
# 根据IP地址限制访问流量
# 1.$binary_remote_addr:相当于获取来源ip的一个系统变量
# 2.zone=iplimit:20m:iplimit相当于自己开辟的存放限流数据的一个内存区域,20m是指内存大小
# 3.rate=1r/s:标示限流的频率,100r/m
limit_req_zone $binary_remote_addr zone=iplimit:10m rate=20r/s;
# 根据服务器级别做限流
limit_req_zone $server_name zone=serverlimit:10m rate=200r/s;
# 基于IP的连接数配置
limit_conn_zone $binary_remote_addr zone=perip:10m;
server {
server_name limit.icodingedu.com;
location /access-limit/ {
proxy_pass http://127.0.0.1:8080/;
# 1.zone=iplimit:引用zone中的变量
# 2.burst=2,设置一个大小为2的缓存区域,当大量请求到来时,请求超过限流频率时,放入缓冲区
# 3.nodelay:缓存区满了以后直接返回503
limit_req zone=iplimit burst=2 nodelay;
# 基于服务器的限流,一般来讲server级别限流是比较大的,但这里做测试就缩小来看
limit_req zone=serverlimit burst=1 nodelay;
# 基于IP的连接数设置
limit_conn perip 1;
}
}
controller的方法
@GetMapping("/nginx-count")
public String nginxCount(int sec){
try {
Thread.sleep(1000 * sec);
}catch(Exception ex){
ex.printStackTrace();
}
return "nginx-count success";
}