Go rate库
golang.org/x/time/rate
简要介绍:采用令牌桶算法实现限流。
令牌桶算法
描述:
有个固定大小的桶,系统会以恒定速率向桶中放入令牌,桶满则不放。请求到来时需要从桶中获取令牌才能执行,否则不能执行,要么阻塞,要么丢弃。
关键:
-
固定大小的桶
桶大小决定了能支持多大的突发流量
-
生产者:系统会以恒定速率往桶中放token,桶满则不放。
比如,1s放10个token,则100ms放1个token。
-
消费者:必须从桶中获取到token才允许事件发生,否则不允许事件发生(要么阻塞、要么直接返回报错)
这里所说的「事件」只是一个抽象描述,具体来讲可以是「执行请求」等其他动作。
使用介绍
创建限流器
使用NewLimiter
创建一个限流器Limiter
:
- r:表示速率,每秒产生r个令牌
- b:表示桶大小,最大突发b个事件
示例代码:
如下表示限制10 QPS,突发1
limiter := NewLimiter(10, 1);
源码:
func NewLimiter(r Limit, b int) *Limiter {
return &Limiter{
limit: r,
burst: b,
}
}
可以使用Every
方法来指定速率,如下:
表示100ms产生1个令牌。
limit := Every(100 * time.Millisecond);
limiter := NewLimiter(limit, 1);
Every
源码:
// Every converts a minimum time interval between events to a Limit.
func Every(interval time.Duration) Limit {
if interval <= 0 {
return Inf
}
// 转成Limit,可以看到Limit代表1秒多少次
return 1 / Limit(interval.Seconds())
}
消费token
有3类方法:
- Wait和WaitN
- Allow和AllowN
- Reserve和ReserveN
这3类方法在token不足时会有不同的行为。(所以,有不同的应用场景)
Wait和WaitN
- Wait消费1个令牌
- WaitN消费N个令牌。
令牌不足时,它们都会阻塞,直到令牌足够。
可以通过ctx来设置阻塞等待的时长。
源码:
// Wait is shorthand for WaitN(ctx, 1).
func (lim *Limiter) Wait(ctx context.Context) (err error) {
return lim.WaitN(ctx, 1)
}
// WaitN blocks until lim permits n events to happen.
// It returns an error if n exceeds the Limiter's burst size, the Context is
// canceled, or the expected wait time exceeds the Context's Deadline.
// The burst limit is ignored if the rate limit is Inf.
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) {
// The test code calls lim.wait with a fake timer generator.
// This is the real timer generator.
newTimer := func(d time.Duration) (<-chan time.Time, func() bool, func()) {
timer := time.NewTimer(d)
return timer.C, timer.Stop, func() {}
}
return lim.wait(ctx, n, time.Now(), newTimer)
}
Allow和AllowN
- Allow消费1个令牌
- AllowN消费N个令牌
在令牌不足时,不会阻塞,会返回false。
源码:
// Allow reports whether an event may happen now.
func (lim *Limiter) Allow() bool {
return lim.AllowN(time.Now(), 1)
}
// AllowN reports whether n events may happen at time t.
// Use this method if you intend to drop / skip events that exceed the rate limit.
// Otherwise use Reserve or Wait.
func (lim *Limiter) AllowN(t time.Time, n int) bool {
return lim.reserveN(t, n, 0).ok
}
Reserve和ReserveN
预订令牌
- Reserve消费1个令牌
- ReserveN消费N个令牌
无论令牌是否足够,它们都会返回 *Reservation
对象。
然后,可以调用该对象的**Delay
方法获取要等待多久才能执行动作**(0表示不用等待,可以立即执行;InfDuration
意味着无限期等待,表示Limiter
无法在最大等待时间内授予请求的令牌)。
如果不想等待,可以调用**Cancel
**方法取消,它会返还token。
使用示例:
r := lim.ReserveN(time.Now(), 1)
if !r.OK() {
// Not allowed to act! Did you remember to set lim.burst to be > 0 ?
return
}
time.Sleep(r.Delay())
Act()
源码:
// Reserve is shorthand for ReserveN(time.Now(), 1).
func (lim *Limiter) Reserve() *Reservation {
return lim.ReserveN(time.Now(), 1)
}
// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen.
// The Limiter takes this Reservation into account when allowing future events.
// The returned Reservation’s OK() method returns false if n exceeds the Limiter's burst size.
// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events.
// If you need to respect a deadline or cancel the delay, use Wait instead.
// To drop or skip events exceeding rate limit, use Allow instead.
func (lim *Limiter) ReserveN(t time.Time, n int) *Reservation {
r := lim.reserveN(t, n, InfDuration)
return &r
}
注释中的重点是如下这部分,介绍了各方法的使用场景:
Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events. If you need to respect a deadline or cancel the delay, use Wait instead. To drop or skip events exceeding rate limit, use Allow instead.
Reservation源码
// A Reservation holds information about events that are permitted by a Limiter to happen after a delay.
// A Reservation may be canceled, which may enable the Limiter to permit additional events.
type Reservation struct {
ok bool
lim *Limiter
tokens int
timeToAct time.Time
// This is the Limit at reservation time, it can change later.
limit Limit
}
// OK returns whether the limiter can provide the requested number of tokens within the maximum wait time.
// If OK is false, Delay returns InfDuration, and
// Cancel does nothing.
func (r *Reservation) OK() bool {
return r.ok
}
// Delay is shorthand for DelayFrom(time.Now()).
func (r *Reservation) Delay() time.Duration {
return r.DelayFrom(time.Now())
}
// InfDuration is the duration returned by Delay when a Reservation is not OK.
const InfDuration = time.Duration(math.MaxInt64)
// DelayFrom returns the duration for which the reservation holder must wait
// before taking the reserved action. Zero duration means act immediately.
// InfDuration means the limiter cannot grant the tokens requested in this
// Reservation within the maximum wait time.
func (r *Reservation) DelayFrom(t time.Time) time.Duration {
if !r.ok {
return InfDuration
}
delay := r.timeToAct.Sub(t)
if delay < 0 {
return 0
}
return delay
}
// Cancel is shorthand for CancelAt(time.Now()).
func (r *Reservation) Cancel() {
r.CancelAt(time.Now())
}
// CancelAt indicates that the reservation holder will not perform the reserved action
// and reverses the effects of this Reservation on the rate limit as much as possible,
// considering that other reservations may have already been made.
func (r *Reservation) CancelAt(t time.Time) {
if !r.ok {
return
}
r.lim.mu.Lock()
defer r.lim.mu.Unlock()
if r.lim.limit == Inf || r.tokens == 0 || r.timeToAct.Before(t) {
return
}
// calculate tokens to restore
// The duration between lim.lastEvent and r.timeToAct tells us how many tokens were reserved
// after r was obtained. These tokens should not be restored.
restoreTokens := float64(r.tokens) - r.limit.tokensFromDuration(r.lim.lastEvent.Sub(r.timeToAct))
if restoreTokens <= 0 {
return
}
// advance time to now
t, tokens := r.lim.advance(t)
// calculate new number of tokens
tokens += restoreTokens
if burst := float64(r.lim.burst); tokens > burst {
tokens = burst
}
// update state
r.lim.last = t
r.lim.tokens = tokens
if r.timeToAct == r.lim.lastEvent {
prevEvent := r.timeToAct.Add(r.limit.durationFromTokens(float64(-r.tokens)))
if !prevEvent.Before(t) {
r.lim.lastEvent = prevEvent
}
}
}
总结
3类方法的使用场景:
-
Reserve/ReserveN:Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events.
-
Wait/WaitN:If you need to respect a deadline or cancel the delay, use Wait instead.
-
Allow/AllowN:To drop or skip events exceeding rate limit, use Allow instead.
动态调整Limiter
可以动态调整Limiter的速率和桶大小:
// SetLimit is shorthand for SetLimitAt(time.Now(), newLimit).
func (lim *Limiter) SetLimit(newLimit Limit) {
lim.SetLimitAt(time.Now(), newLimit)
}
// SetBurst is shorthand for SetBurstAt(time.Now(), newBurst).
func (lim *Limiter) SetBurst(newBurst int) {
lim.SetBurstAt(time.Now(), newBurst)
}
源码
基础
Limit类型说明
Limit
类型:
- 定义了事件的最大频率
- 表示每秒的事件数量
- 为0则不允许事件发生
// Limit defines the maximum frequency of some events.
// Limit is represented as number of events per second.
// A zero Limit allows no events.
type Limit float64
Every
方法将两个事件之间的最小时间间隔转换为Limit
类型值,如下:
// Every converts a minimum time interval between events to a Limit.
func Every(interval time.Duration) Limit {
if interval <= 0 {
return Inf
}
// 可以看到Limit代表1秒多少次
return 1 / Limit(interval.Seconds())
}
综上,Limit
类型值的含义就是代表每秒多少个。
Limiter说明
type Limiter struct {
// 互斥锁,防并发
mu sync.Mutex
limit Limit
burst int
tokens float64
// last is the last time the limiter's tokens field was updated
last time.Time
// lastEvent is the latest time of a rate-limited event (past or future)
lastEvent time.Time
}
说明:
-
一个
Limiter
用来控制事件被允许发生的频率。 -
它实现了一个大小为
b
的令牌桶,初始时桶是满的,然后会以每秒r
个令牌的速率重新填充 -
在任何足够大的时间间隔中,
Limiter
会限制速率为每秒r
个令牌,并且「最大突发」b个事件 -
特殊case,当
r == Inf
(即无限速率),b会被忽略 -
Limiter
主要有三个方法:Allow
、Reserve
、Wait
。这三个方法都会消耗1个token,没有token可用时它们的行为会不同:
Allow
会返回falseWait
会阻塞,直到可以获取到1个令牌,或者关联的context被取消Reserve
返回未来令牌的预留以及调用者在使用它之前必须等待的时间。
原理
提问
-
怎么实现生产令牌的?
生产和消费令牌其实就是维护token数,token数是共享资源,可能会有多线程操作,需要加锁。
我的想法:
起个协程,每隔一定时间(速率)尝试放token,满了就不放,不满就放。
-
怎么实现消费令牌的?
我的想法是:token数减n即可。
-
消费时令牌不足怎么实现等待的?
我的想法:
发现令牌不足,就
sleep delta*interval
(delta是缺多少个令牌,interval生产1个令牌的间隔),醒来再判断,再不足再sleep。问题:
能保证公平吗,能保证先等的线程先获取到令牌? 比如:桶里只有1个令牌时,A先来了,要获取3个token,发现要等待,然后B来了,要获取1个token,发现桶里有足够的,B会不会把桶里的直接拿走?
我的想法是:
无论token够不够,都直接消费token,token不足则token数变成负数,比如:A先来了,直接消费3个令牌,此时token数变成-2,A需要等待
2*interval
;B来了,要消费1个,就变成-3,需要等待3*interval
。
生产和消费令牌
其实就是维护token数,可以看到只有如下方法会改变token数:
-
CancelAt
是会返还token -
reserveN
是消费token
生产令牌
TokensAt
方法返回t时刻的token数,可以在这个方法中生产令牌。
// TokensAt returns the number of tokens available at time t.
func (lim *Limiter) TokensAt(t time.Time) float64 {
lim.mu.Lock()
_, tokens := lim.advance(t) // does not mutate lim
lim.mu.Unlock()
return tokens
}
// Tokens returns the number of tokens available now.
func (lim *Limiter) Tokens() float64 {
return lim.TokensAt(time.Now())
}
advance
方法:
// advance calculates and returns an updated state for lim resulting from the passage of time.
// lim is not changed.
// advance requires that lim.mu is held.
func (lim *Limiter) advance(t time.Time) (newT time.Time, newTokens float64) {
last := lim.last
if t.Before(last) {
last = t
}
// Calculate the new number of tokens, due to time that passed.
// 过去了多久
elapsed := t.Sub(last)
// elapsed乘以令牌速率就是增量令牌数
delta := lim.limit.tokensFromDuration(elapsed)
// 当前总共有多少个令牌
tokens := lim.tokens + delta
if burst := float64(lim.burst); tokens > burst {
// 令牌数不能超过桶大小
tokens = burst
}
return t, tokens
}
advance方法不会改变Limiter的token数,只会返回t时刻桶中的令牌数。
核心思想是:
随着时间的流逝,因为生产令牌的速率是固定的,所以可以算出流逝的这段时间内生成的增量令牌数。方法如下:
// tokensFromDuration is a unit conversion function from a time duration to the number of tokens
// which could be accumulated during that duration at a rate of limit tokens per second.
func (limit Limit) tokensFromDuration(d time.Duration) float64 {
if limit <= 0 {
return 0
}
// 增量令牌数=流逝的时间 * 令牌产生的速率(个/秒)
return d.Seconds() * float64(limit)
}
Q&A
Q:为啥一开始桶就会被填满呢?
A:对着如下代码看,一开始last
是0,所以elapsed
就是t
的时间戳,那肯定是过了很多秒的,然后再乘以速率(非0的话)得到的delta
(即增量token数)就很大,然后**tokens>burst
** 就为 true
,最后执行 tokens=burst
,所以一开始桶就是满的。
func (lim *Limiter) advance(t time.Time) (newT time.Time, newTokens float64) {
last := lim.last
if t.Before(last) {
last = t
}
// 过去了多久
elapsed := t.Sub(last)
// elapsed乘以令牌速率就是增量令牌数
delta := lim.limit.tokensFromDuration(elapsed)
// 当前总共有多少个令牌
tokens := lim.tokens + delta
if burst := float64(lim.burst); tokens > burst {
// 令牌数不能超过桶大小
tokens = burst
}
return t, tokens
}
消费令牌
核心是reserveN
方法:
三类消费令牌的方法(Wait/WaitN、Allow/AllowN、Reserve/RserveN)底层实现都是
reserveN
方法。关键是要懂
Reservation
的设计,表示「预订」。
// reserveN is a helper method for AllowN, ReserveN, and WaitN.
// maxFutureReserve specifies the maximum reservation wait duration allowed.
// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN.
func (lim *Limiter) reserveN(t time.Time, n int, maxFutureReserve time.Duration) Reservation {
lim.mu.Lock()
defer lim.mu.Unlock()
if lim.limit == Inf {
return Reservation{
ok: true,
lim: lim,
tokens: n,
timeToAct: t,
}
} else if lim.limit == 0 {
var ok bool
if lim.burst >= n {
ok = true
lim.burst -= n
}
return Reservation{
ok: ok,
lim: lim,
tokens: lim.burst,
timeToAct: t,
}
}
// 获取t时刻的令牌数
t, tokens := lim.advance(t)
// Calculate the remaining number of tokens resulting from the request.
// 无论令牌数够不够,直接消费n个令牌
tokens -= float64(n)
// Calculate the wait duration
var waitDuration time.Duration
if tokens < 0 {
// 负数,说明令牌不足,需要等待waitDuration这么久,能实现公平等待
waitDuration = lim.limit.durationFromTokens(-tokens)
}
// Decide result
ok := n <= lim.burst && waitDuration <= maxFutureReserve
// Prepare reservation
r := Reservation{
ok: ok,
lim: lim,
limit: lim.limit,
}
if ok {
r.tokens = n
// 计算执行动作的时机
r.timeToAct = t.Add(waitDuration)
// Update state 更新Limiter状态(此时还持有锁)
lim.last = t
// 可为负数
lim.tokens = tokens
lim.lastEvent = r.timeToAct
}
return r
}
Q&A
Q:如何计算要等待多久?
A:如下lim.limit.durationFromTokens(-tokens)
func (lim *Limiter) reserveN(t time.Time, n int, maxFutureReserve time.Duration) Reservation {
......
// Calculate the wait duration
var waitDuration time.Duration
if tokens < 0 {
waitDuration = lim.limit.durationFromTokens(-tokens)
}
......
}
// durationFromTokens is a unit conversion function from the number of tokens to the duration
// of time it takes to accumulate them at a rate of limit tokens per second.
func (limit Limit) durationFromTokens(tokens float64) time.Duration {
if limit <= 0 {
return InfDuration
}
seconds := tokens / float64(limit)
return time.Duration(float64(time.Second) * seconds)
}
Q:wait怎么实现阻塞等待令牌的?
A:如下代码中reserveN
方法返回的**Reservation
** 对象会包含要等待多长时间,如果需要等待就**newTimer
(delay)
,然后使用** select
阻塞等待。
// wait is the internal implementation of WaitN.
func (lim *Limiter) wait(ctx context.Context, n int, t time.Time, newTimer func(d time.Duration) (<-chan time.Time, func() bool, func())) error {
lim.mu.Lock()
burst := lim.burst
limit := lim.limit
lim.mu.Unlock()
if n > burst && limit != Inf {
return fmt.Errorf("rate: Wait(n=%d) exceeds limiter's burst %d", n, burst)
}
// Check if ctx is already cancelled
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Determine wait limit
waitLimit := InfDuration
if deadline, ok := ctx.Deadline(); ok {
// 可通过ctx控制最大等待时长
waitLimit = deadline.Sub(t)
}
// Reserve
r := lim.reserveN(t, n, waitLimit)
if !r.ok {
return fmt.Errorf("rate: Wait(n=%d) would exceed context deadline", n)
}
// Wait if necessary
delay := r.DelayFrom(t)
if delay == 0 {
return nil
}
// 如果要等待,就阻塞等待
ch, stop, advance := newTimer(delay)
defer stop()
advance() // only has an effect when testing
select {
case <-ch:
// We can proceed.
return nil
case <-ctx.Done():
// Context was canceled before we could proceed. Cancel the
// reservation, which may permit other events to proceed sooner.
r.Cancel()
return ctx.Err()
}
}
Q:AllowN方法如何实现令牌不足时就返回false的?
A:看如下代码是依赖reserveN
方法返回的Reservation
对象的**ok
** 字段。再看看reserveN
方法怎么判断ok
的:
关键是**ok := n <= lim.burst && waitDuration <= maxFutureReserve
** 代码,表示如果在规定时间内 maxFutureReserve
(AllowN则为0)能获取到指定数量的token则返回true。
func (lim *Limiter) AllowN(t time.Time, n int) bool {
return lim.reserveN(t, n, 0).ok
}
func (lim *Limiter) reserveN(t time.Time, n int, maxFutureReserve time.Duration) Reservation {
......
if lim.limit == Inf {
return Reservation{
ok: true, // 无限,则默认true
lim: lim,
tokens: n,
timeToAct: t,
}
} else if lim.limit == 0 {
var ok bool
// burst决定了突发事件的最大个数,就算limit为0,只要burst不为0也是能发生事件的,直到burst耗尽
if lim.burst >= n {
ok = true // 设为true
lim.burst -= n
}
return Reservation{
ok: ok,
lim: lim,
tokens: lim.burst,
timeToAct: t,
}
}
......
// Decide result 如果在规定时间maxFutureReserve内能获取到指定数量的令牌就返回true
ok := n <= lim.burst && waitDuration <= maxFutureReserve
// Prepare reservation
r := Reservation{
ok: ok,
lim: lim,
limit: lim.limit,
}
......
return r
}
总结关键设计(一定要看)
-
每次消费token时实时计算当前桶中的最新令牌数。(有点懒加载思想)
对于「系统要以恒定速率向令牌桶生产令牌」这个点,我觉得大部分人的直觉就是「搞个定时器定时主动去累加token数」。
这个方案固然可行,但每个限流器都需要搞个定时器,是有一点资源开销的。而
go rate
的这个方案与「搞个定时器定时主动累加token数」这个方案相比,性能高、资源开销小、简单。从这个案例能学到什么思想呢?
要学会从「最终要达成什么目的?如何达成最终目的? 」这个角度去思考方案。
「系统要以恒定速率向令牌桶生产令牌」的最终目的是什么?就是为了消费令牌时能拿到当前最新的令牌数,然后判断令牌是否足够,不足就要等待,足够就消费它。那为了达成这个最终目的:「消费令牌时能拿到当前最新的令牌数」,可以搞个定时器主动维护令牌数,也可以在消费时实时计算出当前最新的令牌数。
-
token数和时间的相互转换
因为令牌生成速率是固定的,所以是可以知道:
- 生成x个token,需要多长时间
- 经过x秒,生成了多少新token
这是「消费token时实时计算当前桶中的最新令牌数」这个方案的底层支撑。
-
token数可为负数,负数则要等待一定时间才能拿到token
当token不足时,如何实现等待token?多个线程都在等待时,如何实现公平等待,保证先到先得?
关键是:token可为负数,负x,则要等待
x*interval
这么长时间(interval生成1个token的间隔)。先来等的线程,x小,等待时间短;后来等的线程,x大,等待时间长。 -
锁
token数是多个线程的共享资源,涉及多线程同时访问,需要加锁同步。
-
float
类型计算时的精度问题limit
速率是float
类型,注意float
类型计算时的精度问题。
令牌生成速率和桶大小设置的思考,以及对突发流量的思考
桶大小决定了「突发流量」。
设令牌生成速率为r
,桶大小为b
,假设要限制10 QPS:
-
若r=10, b=1,此时一定「能保证」任意1s的请求量都不会超过10,即能保证QPS为10。
-
若r=10, b>1,此时「不能保证」任意1s的请求量都不会超过10,但是从平均来看确实是不超过10。
比如,r=10,b=10时,某时刻桶满了,那接下来的一秒是能放行最多20个请求的。
所以,想要精确限流(保证任意1s内的流量不超过阈值)就意味着不能支持突发流量。
最佳实践
使用令牌桶算法给服务限流的话,桶一般会设置buffer,桶大小一般设置为QPS的1.5倍到2倍。
因为服务的流量不是绝对均匀的。
QPS设为10不是说严格保证任意1s不超过10,只要拉长时间看它平均是10就行。
「平均qps」和「最高瞬时qps」是有差别的。