go rate使用令牌桶算法实现限流器(干货)

1,483 阅读10分钟

Go rate库

golang.org/x/time/rate

简要介绍:采用令牌桶算法实现限流。

令牌桶算法

描述:

有个固定大小的桶,系统会以恒定速率向桶中放入令牌,桶满则不放。请求到来时需要从桶中获取令牌才能执行,否则不能执行,要么阻塞,要么丢弃。

关键:

  1. 固定大小的桶

    桶大小决定了能支持多大的突发流量

  2. 生产者:系统会以恒定速率往桶中放token,桶满则不放。

    比如,1s放10个token,则100ms放1个token。

  3. 消费者:必须从桶中获取到token才允许事件发生,否则不允许事件发生(要么阻塞、要么直接返回报错)

    这里所说的「事件」只是一个抽象描述,具体来讲可以是「执行请求」等其他动作。

使用介绍

创建限流器

使用NewLimiter创建一个限流器Limiter

  1. r:表示速率,每秒产生r个令牌
  2. 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类方法:

  1. Wait和WaitN
  2. Allow和AllowN
  3. Reserve和ReserveN

这3类方法在token不足时会有不同的行为。(所以,有不同的应用场景)

Wait和WaitN

  1. Wait消费1个令牌
  2. 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

  1. Allow消费1个令牌
  2. 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

预订令牌

  1. Reserve消费1个令牌
  2. 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类方法的使用场景:

  1. Reserve/ReserveN:Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events.

  2. Wait/WaitN:If you need to respect a deadline or cancel the delay, use Wait instead.

  3. 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类型:

  1. 定义了事件的最大频率
  2. 表示每秒的事件数量
  3. 为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
}

说明:

  1. 一个Limiter用来控制事件被允许发生的频率

  2. 它实现了一个大小为b的令牌桶,初始时桶是满的,然后会以每秒r个令牌的速率重新填充

  3. 在任何足够大的时间间隔中,Limiter会限制速率为每秒 r 个令牌,并且「最大突发」b个事件

  4. 特殊case,当r == Inf(即无限速率),b会被忽略

  5. Limiter主要有三个方法:AllowReserveWait

    这三个方法都会消耗1个token,没有token可用时它们的行为会不同:

    1. Allow会返回false
    2. Wait会阻塞,直到可以获取到1个令牌,或者关联的context被取消
    3. Reserve返回未来令牌的预留以及调用者在使用它之前必须等待的时间。

原理

提问

  1. 怎么实现生产令牌的?

    生产和消费令牌其实就是维护token数,token数是共享资源,可能会有多线程操作,需要加锁。

    我的想法:

    起个协程,每隔一定时间(速率)尝试放token,满了就不放,不满就放。

  2. 怎么实现消费令牌的?

    我的想法是:token数减n即可。

  3. 消费时令牌不足怎么实现等待的?

    我的想法:

    发现令牌不足,就 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数

  1. CancelAt是会返还token

  2. 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
}

总结关键设计(一定要看)

  1. 每次消费token时实时计算当前桶中的最新令牌数。(有点懒加载思想

    对于「系统要以恒定速率向令牌桶生产令牌」这个点,我觉得大部分人的直觉就是「搞个定时器定时主动去累加token数」。

    这个方案固然可行,但每个限流器都需要搞个定时器,是有一点资源开销的。而go rate的这个方案与「搞个定时器定时主动累加token数」这个方案相比,性能高、资源开销小、简单。

    从这个案例能学到什么思想呢?

    要学会从「最终要达成什么目的?如何达成最终目的? 」这个角度去思考方案。

    「系统要以恒定速率向令牌桶生产令牌」的最终目的是什么?就是为了消费令牌时能拿到当前最新的令牌数,然后判断令牌是否足够,不足就要等待,足够就消费它。那为了达成这个最终目的:「消费令牌时能拿到当前最新的令牌数」,可以搞个定时器主动维护令牌数,也可以在消费时实时计算出当前最新的令牌数

  2. token数和时间的相互转换

    因为令牌生成速率是固定的,所以是可以知道:

    1. 生成x个token,需要多长时间
    2. 经过x秒,生成了多少新token

    这是「消费token时实时计算当前桶中的最新令牌数」这个方案的底层支撑。

  3. token数可为负数,负数则要等待一定时间才能拿到token

    当token不足时,如何实现等待token?多个线程都在等待时,如何实现公平等待,保证先到先得?

    关键是:token可为负数,负x,则要等待 x*interval 这么长时间(interval生成1个token的间隔)。先来等的线程,x小,等待时间短;后来等的线程,x大,等待时间长。

  4. token数是多个线程的共享资源,涉及多线程同时访问,需要加锁同步。

  5. float类型计算时的精度问题

    limit速率是float类型,注意float类型计算时的精度问题。

令牌生成速率和桶大小设置的思考,以及对突发流量的思考

桶大小决定了「突发流量」

设令牌生成速率为r,桶大小为b,假设要限制10 QPS:

  1. 若r=10, b=1,此时一定「能保证」任意1s的请求量都不会超过10,即能保证QPS为10。

  2. 若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」是有差别的。

博客参考

Golang 标准库限流器 time/rate 使用介绍

Golang 标准库限流器 time/rate 实现剖析

uber-go 漏桶限流器使用与原理分析

wiki