对于一个应用系统来说一定会有极限并发/请求数,即总有一个TPS/QPS阀值,如果超了阀值则系统就会不响应用户请求或响应的非常慢,因此我们最好进行过载保护,防止大量请求涌入击垮系统。如果不知道自己应用TPS/QPS阀值的,可以看下我的另外一篇博文。
那么常用的限流方法有
容器限流
常用的有tomcat容器最大线程数,最大连接数,nginx 限制ip的链接和并发等;
tomcat参数配置
看看tomcat官网上怎么说的:
backlog:当所有可能的请求处理线程都在使用时,队列中排队的请求最大数目。默认为10,当队列已满,任何请求都将被拒绝
maxSpareThread:允许存在空闲线程的最大数目,默认值为50
maxThread:最大线程数,默认值为200
minSpareThreads:设当连接器第一次启动时创建线程的数目,确保至少有这么多的空闲线程可用,默认值为4
port:服务端套接字的TCP端口号,默认值为8089(必须)
topNoDelay:为true时,可以提高性能,默认值为true
soTimeout:超时值
nginx官网我们看下:
HttpLimit zone参数配置
limit_req-zone参数配置
这里解释一下
$binary_remote_addr 表示根据客户端ip限制;
zone=one:10m 生成10m空间 命名为one,用于储存访问频率;
rate=1r/s;的意思是限制相同ip每秒访问频次 这里是1秒1次;
limit_req zone=one burst=5;
zone=one表示使用上面的one 10m储存空间数据,计算限制;
burst=5 busrst爆发缓冲区5;超过访问频率的请求放在缓冲区;
限制总的资源数
其实tomcat nginx等容器限流的方式,都是通过限制资源数实现限流的;不过他们是在网络层面就挡了;容器的限流有个不好的地方就是用户体验十分不好;不能做到具体业务优化提示;比如一些异步请求,秒杀场景等;我们有时候可以让这类请求在业务代码层面进行限流,比如秒杀我们可以让请求加入请求队列,然后在慢慢消费,通过限制消费速率来限流;比较经典的有提高mq 异步削峰;这里其实就是漏斗算法的演变。还有比如数据库连接数资源。
限制接口的总并发数/请求数
除了限制总的资源数我们还是可以控制请求数,这点我们12306网站做的炉火纯青;
我们来看下通过验证码挡掉一些请求;
除了验证码还有其他的方法比如小库存秒杀场景里,通过随机请求控制并发数。一般这类做法前都会和后台进行一个随机数token的确认,从而避免有人恶意秒。
除了前端控制处理外,也可以在后台控制层加个多线程计数器进行限流;如 AtomicLong;但是并发量大的话 性能不怎么样,小迸发可以;
Guava实现限流
Guava是谷歌开源的Java库,里面对于封装了一些常用的类库,是对JDk的扩展延伸丰富吧。今天我们介绍他里面RateLimiter类。GuavaRateLimiter提供了令牌桶算法的实现:平滑突发限流 和 平滑预热限流 实现。
平滑突发限流:
/**
* 平滑突发限流(SmoothBursty)
*/
public class SmoothBurstyRateLimitTest {
public static void main(String[] args) {
//QPS = 5,每秒允许5个请求
RateLimiter limiter = RateLimiter.create(5);
//limiter.acquire() 返回获取token的耗时,以秒为单位
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
}
}
0.0
0.19667
0.195784
0.194672
0.195658
0.195285/**
* 平滑突发限流(smoothbursty)
*/
public class SmoothBurstyRateLimitTest02 {
public static void main(String[] args) {
//每秒允许5个请求,表示桶容量为5且每秒新增5个令牌,即每隔0.2毫秒新增一个令牌
RateLimiter limiter = RateLimiter.create(5);
//一次性消费5个令牌
System.out.println(limiter.acquire(5));
//limiter.acquire(1)将等待差不多1秒桶中才能有令牌
System.out.println(limiter.acquire(1));
//固定速率
System.out.println(limiter.acquire(1));
//固定速率
System.out.println(limiter.acquire(1));
//固定速率
System.out.println(limiter.acquire(1));
}
}
/**
* 平滑突发限流(smoothbursty)
*/
public class SmoothBurstyRateLimitTest03 {
public static void main(String[] args) {
//每秒允许5个请求,表示桶容量为5且每秒新增5个令牌,即每隔0.2毫秒新增一个令牌
RateLimiter limiter = RateLimiter.create(5);
//第一秒突发了10个请求
System.out.println(limiter.acquire(10));
//limiter.acquire(1)将等待差不多2秒桶中才能有令牌
System.out.println(limiter.acquire(1));
//固定速率
System.out.println(limiter.acquire(1));
//固定速率
System.out.println(limiter.acquire(1));
//固定速率
System.out.println(limiter.acquire(1));
}
}平滑预热限流:
/**
* 平滑预热限流(SmoothWarmingUp)
*/
public class SmoothWarmingUp {
public static void main(String[] args) {
//permitsPerSecond:每秒新增的令牌数 warmupPeriod:从冷启动速率过渡到平均速率的时间间隔
//系统冷启动后慢慢的趋于平均固定速率(即刚开始速率慢一些,然后慢慢趋于我们设置的固定速率)
RateLimiter limiter = RateLimiter.create(10, 1000, TimeUnit.MILLISECONDS);
for(int i = 0; i < 10;i++) {
//获取一个令牌
System.out.println(limiter.acquire(1));
}
}
}
guava 秒杀场景的实现
public class MiaoShaTest {
public static void main(String[] args) throws InterruptedException {
//限流,每秒允许10个请求进入秒杀
RateLimiter limiter = RateLimiter.create(10);
for (int i = 0; i < 100; i++) {
//100个线程同时抢购
new Thread(() -> {
//每个秒杀请求如果100ms以内得到令牌,就算是秒杀成功,否则就返回秒杀失败
if (limiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
System.out.println("恭喜您,秒杀成功");
} else {
System.out.println("秒杀失败,请继续努力");
}
}).start();
//等待新的令牌生成
Thread.sleep(10);
}
}
}
运行结果:恭喜您,秒杀成功
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
恭喜您,秒杀成功
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
恭喜您,秒杀成功
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
恭喜您,秒杀成功
最后附上俩在限流算法:
漏桶算法 VS 令牌桶算法
漏桶算法(Leaky Bucket)
漏桶算法(Leaky Bucket):水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:
可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。
令牌桶算法(Token Bucket)
令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务.
令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量.
漏桶算法VS令牌桶算法
令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;
漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;
漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;
令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;
两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。