目录
接口限流
在面临高并发的请购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大的压力。尤其是对于下单的接口,过多的请求打到数据库会对系统的稳定性造成影响。能够做的就是尽量让后台系统稳定优雅的处理大量请求,所以很有必要做一个接口限流。
限流的目的
- 流量远比想象的要多
- 系统活着比挂了要好
- 宁愿只让少数人能用,也不要让所有人不能用
行业内是如何限流的?
行业内限流的主流方案是限制TPS或者限制QPS
TPS主要用来衡量对数据库产生写操作、Transaction操作的一个容量指标
QPS主要是查询数量每秒的指标
对应的行业内的解决算法分别是:
1)令牌桶算法(限制TPS)
2)漏桶算法(限制TPS)
漏桶算法是用来平滑流量以固定速率流入的操作;漏桶算法是没有办法应对突发流量的。
令牌桶算法是限制某一秒的流量的最大值,这样就可以应对一些突发流量;所以在实际项目中应用更多;本项目的限流也是使用令牌桶算法。
限流力度
1)接口维度
有一个对应的接口用来做下单限流,即便有了秒杀令牌、秒杀大闸、秒杀队列以及对应的验证码方案,还是要有最后一道保护接口维度的能力,具体就是在接口维度引入令牌桶算法,限制TPS
2)总维度
假设每个接口可承受5TPS流量,有10个接口,理论上可以承受50TPS流量,但是系统真的能承受50TPS的流量嘛,往往是不能的,因为每一个接口都到达极限值那么对应的整个系统的CPU容量就会达到极限值,因此需要一个总维度的限流,一般总维度的限流要比接口限流总和小20%左右
限流范围
集群限流:依赖redis或其他的中间件技术做统一计数器,往往会产生性能瓶颈
单击限流:负载均衡的前提下单机平均限流效果更好
项目中,不能将计数器变成系统性能的瓶颈,大部分情况下使用单机限流,但前提是负载均衡度要足够好;本项目使用单机限流。
令牌桶算法介绍(项目使用):
令牌桶算法最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。
如果我们需要在一秒内限制访问次数为 N 次,那么就每隔 1/N 的时间,往桶内放入一个令牌;在处理请求之前先要从桶中获得一个令牌,如果桶中已经没有了令牌,那么就需要等待新的令牌或者直接拒绝服务;桶中的令牌总数也要有一个限制,如果超过了限制就不能向桶中再增加新的令牌了。这样可以限制令牌的总数,一定程度上可以避免瞬时流量高峰的问题。
使用令牌桶算法就需要存储令牌的数量,如果是单机上实现限流的话,可以在进程中使用一个变量来存储;但是如果在分布式环境下,不同的机器之间无法共享进程中的变量,一般会使用 Redis 来存储这个令牌的数量。这样的话,每次请求的时候都需要请求一次 Redis 来获取一个令牌,会增加几毫秒的延迟,性能上会有一些损耗。因此,一个折中的思路是: 可以在每次取令牌的时候,不再只获取一个令牌,而是获取一批令牌,这样可以尽量减少请求 Redis的次数。
限流策略很难在实际中确认限流的阈值是多少,设置的小了容易误伤正常的请求,设置的大了则达不到限流的目的。所以,一般在实际项目中,会把阈值放置在配置中心中方便动态调整;同时,我们可以通过定期地压力测试得到整体系统以及每个微服务的实际承载能力,然后再依据这个压测出来的值设置合适的阈值,本项目中测试得到的限流的阈值是300;并且是单机限流。
项目中限流的代码实现:
使用Guava的RateLimiter实现令牌桶限流接口;Guava是Google开源的Java工具类,也提供了限流工具类RateLimiter,该类里面实现了令牌桶算法。
秒杀开始后所有请求进入了处理流程,但是被限流成每秒处理300个请求。
初始化完成后的实现:
Google 的RateLimiter的内部限流实现原理
源码方法官方解释:
需要拿一个令牌,如果拿不到就立马返回失败,最后,依靠boolean型的返回值决定是否可以拿到对应的令牌
源码方法中最后会调用acquire():
看一下acquire()内部源代码的实现:
microsToWait返回值的作用:
如果说当前这一个秒对应的permits被消耗完(即300个permits被消耗完的话),需要将自己的线程sleep多少毫秒才可以让用户继续做对应的操作
看一下reserve()方法源码:
再进入到reserveAndGetWaitLength
reserveAndGetWaitLength():
如果说当前秒还有对应的令牌,就直接返回,若没有对应的令牌,则去计算下一秒是否有对应的令牌,也就是说会有一个计算下一秒的提前量,并且将下一秒对应的令牌扣掉以使得下一秒的请求过来的时候,仍然不需要重复计算,因此Google的这种方式其实是一种比较超前的设计思维
将整个的时间轴归一化到数组内,看对应的这一秒如果不够了,先预支把下一秒下标内的令牌先拿走让当前线程睡眠,如果说当前线程睡眠成功了,自然而然下一秒被唤醒的时候令牌也被扣掉,而且程序也做到了对应的限流
这里的一个疑惑点:把下一秒的令牌扣掉是什么意思?还有计算下一秒的提前量以便于下一秒不需要重新计算该怎么理解?每次当前时间内没有对应的令牌的话,都会提前计算下一面的令牌数量
答: 下一秒的意思是这一秒令牌没了他等待一秒后优先抢占下一秒放回去的令牌
源码分析 - Guava RateLimiter源码解析_个人文章 - SegmentFault 思否----值得参考
限流与队列泄洪(流量削峰)的再理解:
假设一台机器的极限tps是400,那我们限流到300tps,如果这300tps全部是去请求createOrder这个方法,那么这个时候我们如果不用队列泄洪,那么在这1秒内需要处理300个请求,便是有300个线程,导致cpu将会在这个300线程中来回切换,使cpu的消耗加大,所以为了更好的处理300个线程,减小cpu的切换时间开销,减小cpu处理者300个请求的时间,所以我们引入队列泄洪,减少cpu在线程间切换的时间,从而提高相应速度。
最后呢,我觉得,我们如果不使用队列泄洪,其实系统应该也可以解决,但是响应时间会增加。但是我们如果只使用队列泄洪,就只考虑createOrder这个接口应该也是可以解决的,但是有可能,会导致这个order类的tps过大,导致系统处理不过来。
所以,限流应该和队列泄洪是相辅相成的,只用限流可以解决流量过大的问题,但是可能会导致并发量过大,增加cpu的处理时间,所以引入队列泄洪来减少cpu处理300个请求的时间。