一. 介绍
项目中一直在使用RateLimiter进行单机的限流,但是没有去了解他的运作原理,这里就简单记录下,为以后学习Sentinal做铺垫。
这里强烈推荐酷家乐的文章《RateLimiter解析(一) ——设计哲学与快速使用》
把定积分写入技术文档我也是第一次见,实打实的说,核心部分我也没看懂。
本文仅仅讨论SmoothBursty
的情况,SmoothWarmingUp
先放着,因为不是很理解。
二. 个人困惑点
2.1 放令牌的时间窗口
其实在阅读源码前,自己对单机限流的工具进行了脑补,想单机限流工具的实现方案。
之前了解的方法,基本就两种,一种是漏桶,一种是令牌桶。
我比较疑惑的是令牌桶方式的放令牌的时间窗口,比如说,我设置的QPS是1分钟6000。
放令牌的时候,是每隔一分钟,丢6000个进去吗?
还是把6000个平均的分布在这一分钟的跨度内,每秒放100个进去呢?
个人理解比较合适的方式是每秒放100个进去。
因为如果你每分钟就立即放6000个进去,如果在1S内立马被消耗完了,那就剩余的59S就在那儿干等。
对于服务器而言,比如说Mysql,显然是不太合理的。
这种场景下,Mysql的QPS可能达到6000/S,如果抗过去了这1S,剩余的59S一个请求也就没有。
但是如果设置的QPS是1S6000。
那么这种放令牌的方式是怎么放呢?
每隔1S就放6000个令牌,还是再下降一个计时单位到毫秒进行发放?
如果每次的放置时间窗口都下降一个单位,再小的时间单位对于计算机而言也没有意义。
2.2 放令牌的方式
往令牌桶中放令牌的方式,是单独起一个线程,每隔一段时间醒一次,往令牌桶中放令牌吗?
这种方式看起来有点太消耗内存和CPU了。
不知道RateLimiter是怎么实现的
三. 具体实现
先来看看创建方法:RateLimiter create(double permitsPerSecond)
RateLimiter的创建方法,是个double类型的值,表示1s可以产生的令牌数。
进入doSetRate
方法,注意两行代码:
1 | double stableIntervalMicros = (double)TimeUnit.SECONDS.toMicros(1L) / permitsPerSecond; |
这里的stableIntervalMicros
的值,就是以微秒为时间单位,计算多少微秒可以产生一个令牌。
所以这里基本上可以解答我的第一个疑惑,RateLimiter的时间窗口是微秒。
1s = 1,000millis = 1,000,000micros。
这里为什么是微秒,能不能是毫秒的单位?我其实还不是很懂。
有时间看看其他的限流器的实现
令牌桶与漏桶算法的区别就是在空闲时可以保存一部分的令牌,那么是多少个呢?
这个变量保存在SmoothRateLimiter::maxPermits
中,如果看调用的话,根据是SmoothBursty
还是SmoothWarmingUp
是不同的,
这里我们仅仅讨论SmoothBursty
的情况,因为SmoothWarmingUp
实在是看不懂。
在SmoothBursty.doSetRate
方法中,进行了这个值的设置,maxBurstSeconds
的默认值是1s。
1 | maxPermits = maxBurstSeconds * permitsPerSecond; |
所以这个空闲时最多保留的令牌数就是我们构造参数传入的值。
最后来看看最后一个问题,何时放置令牌以及取令牌的过程。
说到这儿其实已经知道了,RateLimiter并没有单独启一个线程去放令牌,而是使用了取时计算的方式。
而使用这种方式也是有弊端的,就是acquire
方法需要加锁:
1 | RateLimter##acquire() |
取令牌时,我们要知道目前令牌桶中存有几个令牌。计算方式也比较简单,记录下上一次获取令牌的时间t,
公式:
(now - t) / windowTime
就能计算出,空闲的这段时间内,产生了多少空闲的令牌。
同时记得要和maxPermits
取个最小值。
这个方法在方法resync
中:
方法coolDownIntervalMicros()
在SmoothBursty
就是上文提到的固定的时间窗口stableIntervalMicros
1 | void resync(long nowMicros) { |
得到了获取时,当前令牌桶中有多少令牌,最后看整体的获取逻辑:
1 | final long reserveEarliestAvailable(int requiredPermits, long nowMicros) { |
比较想要获取的令牌数和目前令牌桶中的令牌数量,得到除去令牌桶中的令牌数,有多少令牌还未产生,需要慢慢等待。
比如令牌桶中只有3个令牌,而需要获取10个令牌,则需要额外的等待7个令牌的获取时间
storedPermitsToWaitTime
在SmoothBursty
是返回的0。所以这里的
waitMicors
就是freshPermits * timeWindow
由于下面的
freshPermits
个令牌都被这个请求拿走了,所以下一次产生令牌的时间需要加上waitMicros
这里假设的是令牌不够的情况,那么如果空闲时间的令牌数足够的情况是呢?
比如令牌桶中有10个令牌,请求3个。
freshPermits
= 0waitMicros
= 0nextFreeTicketMicros
不变- 需要减去使用完的令牌数
四. 总结
看到这儿对RateLimiter有了大概的了解
- 对于存放令牌的时间窗口是微秒。
- 是完全公平的限流器,来请求的线程按照顺序等待。
并没有可扩展的其他复杂逻辑。比如排队多了,丢弃后续的请求,而不是一直卡在那儿等。