最近学习了限流与RateLimiter
前言
分布式环境下应对高并发保证服务稳定几招,按照个人理解,优先级从高到低分别为缓存、限流、降级、熔断,每招都有它的作用,本文重点就讲讲限流这部分。
坦白讲,其实上面的说法也不准确,因为服务降级、熔断本身也是限流的一种,因为它们本质上也是阻断了流量进来,但是本文希望大家可以把限流当做一个单纯的名词来理解,看一下对请求做流控的几种算法及具体实现方式。
为什么要限流
其实很好理解的一个问题,为什么要限流,自然就流量过大了呗,一个对外服务有很多场景都会流量增大:
- 业务用户量不断攀升
- 各种促销
- 网络爬虫
- 恶意刷单
注意这个"大",1000QPS大吗?5000QPS大吗?10000QPS大么?没有答案,因为没有标准,因此,"大"一定是和正常流量相比的大。流量一大,服务器扛不住,扛不住就挂了,挂了没法提供对外服务导致业务直接熔断。怎么办,最直接的办法就是从源头把流量限制下来,例如服务器只有支撑1000QPS的处理能力,那就每秒放1000个请求,自然保证了服务器的稳定,这就是限流。
下面看一下常见的两种限流算法。
漏桶算法
漏桶算法的原理比较简单,水(请求)先进入到漏桶里,人为设置一个最大出水速率,漏桶以<=出水速率的速度出水,当水流入速度过大会直接溢出(拒绝服务):
因此,这个算法的核心为:
- 存下请求
- 匀速处理
- 多于丢弃
因此这是一种强行限制请求速率的方式,但是缺点非常明显,主要有两点:
- 无法面对突发的大流量----比如请求处理速率为1000,容量为5000,来了一波2000/s的请求持续10s,那么后5s的请求将全部直接被丢弃,服务器拒绝服务,但是实际上网络中突发一波大流量尤其是短时间的大流量是非常正常的,超过容量就拒绝,非常简单粗暴
- 无法有效利用网络资源----比如虽然服务器的处理能力是1000/s,但这不是绝对的,这个1000只是一个宏观服务器处理能力的数字,实际上一共5秒,每秒请求量分别为1200、1300、1200、500、800,平均下来qps也是1000/s,但是这个量对服务器来说完全是可以接受的,但是因为限制了速率是1000/s,因此前面的三秒,每秒只能处理掉1000个请求而一共打回了700个请求,白白浪费了服务器资源
所以,通常来说利用漏桶算法来限流,实际场景下用得不多。
令牌桶算法
令牌桶算法是网络流量整形(Traffic Shaping)和限流(Rate Limiting)中最常使用的一种算法,它可用于控制发送到网络上数据的数量并允许突发数据的发送。
从某种意义上来说,令牌桶算法是对漏桶算法的一种改进,主要在于令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用,来看下令牌桶算法的实现原理:
整个的过程是这样的:
- 系统以恒定的速率产生令牌,然后将令牌放入令牌桶中
- 令牌桶有一个容量,当令牌桶满了的时候,再向其中放入的令牌就会被丢弃
- 每次一个请求过来,需要从令牌桶中获取一个令牌,假设有令牌,那么提供服务;假设没有令牌,那么拒绝服务
那么,我们再看一下,为什么令牌桶算法可以防止一定程度的突发流量呢?可以这么理解,假设我们想要的速率是1000QPS,那么往桶中放令牌的速度就是1000个/s,假设第1秒只有800个请求,那意味着第2秒可以容许1200个请求,这就是一定程度突发流量的意思,反之我们看漏桶算法,第一秒只有800个请求,那么全部放过,第二秒这1200个请求将会被打回200个。
注意上面多次提到一定程度这四个字,这也是我认为令牌桶算法最需要注意的一个点。假设还是1000QPS的速率,那么5秒钟放1000个令牌,第1秒钟800个请求过来,第2~4秒没有请求,那么按照令牌桶算法,第5秒钟可以接受4200个请求,但是实际上这已经远远超出了系统的承载能力,因此使用令牌桶算法特别注意设置桶中令牌的上限即可。
总而言之,作为对漏桶算法的改进,令牌桶算法在限流场景下被使用更加广泛。
RateLimiter使用
上面说了令牌桶算法在限流场景下被使用更加广泛,接下来我们看一下代码示例,模拟一下每秒最多过五个请求:
public class RateLimiterTest { private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static final int THREAD_COUNT = 25; @Test
public void testRateLimiter1() {
RateLimiter rateLimiter = RateLimiter.create(5); Thread[] ts = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
ts[i] = new Thread(new RateLimiterThread(rateLimiter), "RateLimiterThread-" + i);
} for (int i = 0; i < THREAD_COUNT; i++) {
ts[i].start();
} for (;;);
} public class RateLimiterThread implements Runnable { private RateLimiter rateLimiter; public RateLimiterThread(RateLimiter rateLimiter) {
this.rateLimiter = rateLimiter;
} @Override
public void run() {
rateLimiter.acquire(1); System.out.println(Thread.currentThread().getName() + "获取到了令牌,时间 = " + FORMATTER.format(new Date()));
} } }
利用RateLimiter.create这个构造方法可以指定每秒向桶中放几个令牌,比方说上面的代码create(5),那么每秒放置5个令牌,即200ms会向令牌桶中放置一个令牌。这边代码写了一条线程模拟实际场景,拿到令牌那么就能执行下面逻辑,看一下代码执行结果:
RateLimiterThread-0获取到了令牌,时间 = 2019-08-25 20:58:53
RateLimiterThread-23获取到了令牌,时间 = 2019-08-25 20:58:54
RateLimiterThread-21获取到了令牌,时间 = 2019-08-25 20:58:54
RateLimiterThread-19获取到了令牌,时间 = 2019-08-25 20:58:54
RateLimiterThread-17获取到了令牌,时间 = 2019-08-25 20:58:54
RateLimiterThread-13获取到了令牌,时间 = 2019-08-25 20:58:54
RateLimiterThread-9获取到了令牌,时间 = 2019-08-25 20:58:55
RateLimiterThread-15获取到了令牌,时间 = 2019-08-25 20:58:55
RateLimiterThread-5获取到了令牌,时间 = 2019-08-25 20:58:55
RateLimiterThread-1获取到了令牌,时间 = 2019-08-25 20:58:55
RateLimiterThread-11获取到了令牌,时间 = 2019-08-25 20:58:55
RateLimiterThread-7获取到了令牌,时间 = 2019-08-25 20:58:56
RateLimiterThread-3获取到了令牌,时间 = 2019-08-25 20:58:56
RateLimiterThread-4获取到了令牌,时间 = 2019-08-25 20:58:56
RateLimiterThread-8获取到了令牌,时间 = 2019-08-25 20:58:56
RateLimiterThread-12获取到了令牌,时间 = 2019-08-25 20:58:56
RateLimiterThread-16获取到了令牌,时间 = 2019-08-25 20:58:57
RateLimiterThread-20获取到了令牌,时间 = 2019-08-25 20:58:57
RateLimiterThread-24获取到了令牌,时间 = 2019-08-25 20:58:57
RateLimiterThread-2获取到了令牌,时间 = 2019-08-25 20:58:57
RateLimiterThread-6获取到了令牌,时间 = 2019-08-25 20:58:57
RateLimiterThread-10获取到了令牌,时间 = 2019-08-25 20:58:58
RateLimiterThread-14获取到了令牌,时间 = 2019-08-25 20:58:58
RateLimiterThread-18获取到了令牌,时间 = 2019-08-25 20:58:58
RateLimiterThread-22获取到了令牌,时间 = 2019-08-25 20:58:58
看到,非常标准,在每次消耗一个令牌的情况下,RateLimiter可以保证每一秒内最多只有5个线程获取到令牌,使用这种方式可以很好的做单机对请求的QPS数控制。
至于为什么2019-08-25 20:58:53这个时间点只有1条线程获取到了令牌而不是有5条线程获取到令牌,因为RateLimiter是按照秒计数的,可能第一个线程是2019-08-25 20:58:53.999秒来的,算在2019-08-25 20:58:53这一秒内;下一个线程2019-08-25 20:58:54.001秒来,自然就算到2019-08-25 20:58:54这一秒去了。
上面的写法是RateLimiter最常用的写法,注意:
- acquire是阻塞的且会一直等待到获取令牌为止,它有一个返回值为double型,意思是从阻塞开始到获取到令牌的等待时间,单位为秒
- tryAcquire是另外一个方法,它可以指定超时时间,返回值为boolean型,即假设线程等待了指定时间后仍然没有获取到令牌,那么就会返回给客户端false,客户端根据自身情况是打回给前台错误还是定时重试
RateLimiter预消费
处理请求,每次来一个请求就acquire一把是RateLimiter最常见的用法,但是我们看acquire还有个acquire(int permits)的重载方法,即允许每次获取多个令牌数。这也是有可能的,请求数是一个大维度每次扣减1,有可能服务器按照字节数来进行限流,例如每秒最多处理10000字节的数据,那每次扣减的就不止1了。
接着我们再看一段代码示例:
@Test
public void testRateLimiter2() {
RateLimiter rateLimiter = RateLimiter.create(1); System.out.println("获取1个令牌开始,时间为" + FORMATTER.format(new Date()));
double cost = rateLimiter.acquire(1);
System.out.println("获取1个令牌结束,时间为" + FORMATTER.format(new Date()) + ", 耗时" + cost + "ms");
System.out.println("获取5个令牌开始,时间为" + FORMATTER.format(new Date()));
cost = rateLimiter.acquire(5);
System.out.println("获取5个令牌结束,时间为" + FORMATTER.format(new Date()) + ", 耗时" + cost + "ms");
System.out.println("获取3个令牌开始,时间为" + FORMATTER.format(new Date()));
cost = rateLimiter.acquire(3);
System.out.println("获取3个令牌结束,时间为" + FORMATTER.format(new Date()) + ", 耗时" + cost + "ms");
}
代码运行结果为:
获取1个令牌开始,时间为2019-08-25 21:21:09.973
获取1个令牌结束,时间为2019-08-25 21:21:09.976, 耗时0.0ms
获取5个令牌开始,时间为2019-08-25 21:21:09.976
获取5个令牌结束,时间为2019-08-25 21:21:10.974, 耗时0.997237ms
获取3个令牌开始,时间为2019-08-25 21:21:10.976
获取3个令牌结束,时间为2019-08-25 21:21:15.974, 耗时4.996529ms
看到这就是标题所说的预消费能力,也是RateLimiter中允许一定程度突发流量的实现方式。第二次需要获取5个令牌,指定的是每秒放1个令牌到桶中,我们发现实际上并没有等5秒钟等桶中积累了5个令牌才能让第二次acquire成功,而是直接等了1秒钟就成功了。我们可以捋一捋这个逻辑:
- 第一次请求过来需要获取1个令牌,直接拿到
- RateLimiter在1秒钟后放一个令牌,第一次请求预支的1个令牌还上了
- 1秒钟之后第二次请求过来需要获得5个令牌,直接拿到
- RateLimiter在花了5秒钟放了5个令牌,还上了第二次请求预支的5个令牌
- 第三个请求在5秒钟之后拿到3个令牌
也就是说,前面的请求如果流量大于每秒放置令牌的数量,那么允许处理,但是带来的结果就是后面的请求延后处理,从而在整体上达到一个平衡整体处理速率的效果。
突发流量的处理,在令牌桶算法中有两种方式,一种是有足够的令牌才能消费,一种是先消费后还令牌。后者就像我们0首付买车似的,30万的车很少有等攒到30万才全款买的,先签了相关合同把车子给你,然后贷款慢慢还,这样就爽了。RateLimiter也是同样的道理,先让请求得到处理,再慢慢还上预支的令牌,客户端同样也爽了,否则我假设预支60个令牌,1分钟之后才能处理我的请求,不合理也不人性化。
RateLimiter的限制
特别注意RateLimiter是单机的,也就是说它无法跨JVM使用,设置的1000QPS,那也在单机中保证平均1000QPS的流量。
假设集群中部署了10台服务器,想要保证集群1000QPS的接口调用量,那么RateLimiter就不适用了,集群流控最常见的方法是使用强大的Redis:
- 一种是固定窗口的计数,例如当前是2019/8/26 20:05:00,就往这个"2019/8/26 20:05:00"这个key进行incr,当前是2019/8/26 20:05:01,就往"2019/8/26 20:05:01"这个key进行incr,incr后的结果只要大于我们设定的值,那么就打回去,小于就相当于获取到了执行权限
- 一种是结合lua脚本,实现分布式的令牌桶算法,网上实现还是比较多的,可以参考https://blog.csdn.net/sunlihuo/article/details/79700225这篇文章
总得来说,集群限流的实现也比较简单。
总结
本文主要写了常见的两种限流算法漏桶算法与令牌桶算法,并且演示了Guava中RateLimiter的实现,相信看到这里的朋友一定都懂了,恭喜你们!
令牌桶算法是最常用的限流算法,它最大的特点就是容许一定程度的突发流量。
漏桶算法同样也有自己的应用之处,例如Nginx的限流模块就是基于漏桶算法的,它最大的特点就是强行限制流量按照指定的比例下发,适合那种对流量有绝对要求的场景,就是流量可以容许在我指定的值之下,可以被多次打回,但是无论如何决不能超过指定的。
虽然令牌桶算法相对更好,但是还是我经常说的,使用哪种完全就看大家各自的场景,适合的才是最好的。
最近学习了限流与RateLimiter的更多相关文章
- guava的限流工具RateLimiter使用
guava限流工具使用 非常详细的一篇使用博客:https://www.cnblogs.com/yeyinfu/p/7316972.html 1,原理:Guava RateLimiter基于令牌桶算法 ...
- Guava限流工具RateLimiter使用
公司最近在推一个限流工具接入,提供的功能有单机限流.集群限流等.想了解一下限流的原理和设计,看了一下wiki里面有提到用了guava的ratelimiter工具,查了一些资料了解了一下 主要的限流算法 ...
- 限流 - Guava RateLimiter
2019独角兽企业重金招聘Python工程师标准>>> 限流 限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦并发访问/请求达到限制速率或者 ...
- 超详细的Guava RateLimiter限流原理解析
超详细的Guava RateLimiter限流原理解析 mp.weixin.qq.com 点击上方“方志朋”,选择“置顶或者星标” 你的关注意义重大! 限流是保护高并发系统的三把利器之一,另外两个是 ...
- SpringBoot进阶教程(六十七)RateLimiter限流
在上一篇文章nginx限流配置中,我们介绍了如何使用nginx限流,这篇文章介绍另外一种限流方式---RateLimiter. v限流背景 在早期的计算机领域,限流技术(time limiting)被 ...
- 【高并发】亿级流量场景下如何为HTTP接口限流?看完我懂了!!
写在前面 在互联网应用中,高并发系统会面临一个重大的挑战,那就是大量流高并发访问,比如:天猫的双十一.京东618.秒杀.抢购促销等,这些都是典型的大流量高并发场景.关于秒杀,小伙伴们可以参见我的另一篇 ...
- SpringCloud系列——限流、熔断、降级
前言 分布式环境下,服务直接相互调用,一个复杂的业务可能要调用多个服务,例如A -> B -> C -> D,当某个服务出现异常(调用超时.调用失败等)将导致整个流程阻塞崩溃,严重的 ...
- Sentinel限流实现原理
Sentinel限流的神秘面纱: 之前我们学习过限流比较主流的三种算法:漏桶,令牌桶,滑动窗口.而Sentinel采用的是最后一种,滑动窗口来实现限流的. 通过对Sentinel基础Api的使用,我们 ...
- Spark Streaming数据限流简述
Spark Streaming对实时数据流进行分析处理,源源不断的从数据源接收数据切割成一个个时间间隔进行处理: 流处理与批处理有明显区别,批处理中的数据有明显的边界.数据规模已知:而流处理数 ...
随机推荐
- JavaSE总结(二)
一.Java Number .65f;byte c =0x4a; 然而,在实际开发过程中,我们经常会遇到需要使用对象,而不是内置数据类型的情形.为了解决这个问题,Java 语言为每一个内置数据类型提供 ...
- 自动化部署方案CICD
自动化部署方案 由于来来也的时间不久,可能对现有的部署情况不是很了解,以下是个人对POC自动化部署的设计方案. 自动化部署优点 降低成本,提高生产力,高可用,更可靠,性能优化 与gitlab持 ...
- 序列化Serializable接口
一.序列化 1.什么是序列化? 序列化就是将对象的状态存储到特定存储介质中的过程,也就是将对象状态转换为可保持或传输格式的过程. 在序列化过程中,会将对象的公有成员.私有成员(包括类名),转换为字节流 ...
- 12. 集合类Collection和Map
*:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...
- Linux之TCPIP内核参数
/proc/sys/net目录 参考1.Linux之TCPIP内核参数优化 所有的TCP/IP参数都位于/proc/sys/net目录下(请注意,对/proc/sys/net目录下内容的修改都是临时的 ...
- 关于报错:The Microsoft.ACE. Oledb.12.0 provider was not registered on the local computer
错误描述:The Microsoft.ACE. Oledb.12.0 provider was not registered on the local computer 最近在Web项目中做一个自动生 ...
- react开发中的小细节
目前开始使用react余遇到的问题还不是很多,但还是希望总结一下. react中的属性prop: 在react中组件的父子组件的通信是基于prop的,当然对于底层的东西不是特别了解,但可以说一说它的基 ...
- SWT 注意事项
一:GridData (1) 将 GridData 的 widthHint 设置为0,可以解决控件大小会随着这其默认值长度大小而改变的问题.
- python使用pip安装第三方库以及镜像使用豆瓣源安装第三方库
2018/8/7 在使用pip安装pynum第三方库时的随笔 所有的前提都是你成功安装了pip 首先第一步 打开命令提示符 输入pip show pip 查看当前pip版本 然后可以上官网搜索一下 ...
- ibatis 核心原理解析
最近查找一个生产问题的原因,需要深入研究 ibatis 框架的源码.虽然最后证明问题的原因与 ibatis 无关,但是这个过程加深了对 ibatis 框架原理的理解. 这篇文章主要就来讲讲 ibati ...