golang 标准库 time/rate 介绍

89

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

golang官方库中有一个rate包,实现了令牌桶算法。仓库地址:https://github.com/golang/time

  1. 生成一个Limiter对象
func NewLimiter(r Limit, b int) *Limiter {
	return &Limiter{
		limit:  r,
		burst:  b,
	}
}

如上所示,其中r是往令牌桶中放令牌的速率,b是令牌桶的大小。

  1. 尝试获取token

例如:

func (lim *Limiter) Allow() bool {
	return lim.AllowN(time.Now(), 1)
}

API很多,最终都是依靠 reserveN 来确定

func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation {
	lim.mu.Lock()

	if lim.limit == Inf {
		lim.mu.Unlock()
		return Reservation{
			ok:        true,
			lim:       lim,
			tokens:    n,
			timeToAct: now,
		}
	}

	now, last, tokens := lim.advance(now)

	// Calculate the remaining number of tokens resulting from the request.
	tokens -= float64(n)

	// Calculate the wait duration
	var waitDuration time.Duration
	if tokens < 0 {
		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 = now.Add(waitDuration)
	} else {
		panic(10)
	}

	// Update state
	if ok {
		lim.last = now
		lim.tokens = tokens
		lim.lastEvent = r.timeToAct
	} else {
		lim.last = last
	}

	lim.mu.Unlock()
	return r
}
``

首先用一个常量来判断Limiter是否是无限大,上限是 math.MaxFloat64。

然后就是计算当前时间、上一次的时间和桶里剩余的token数量,这里在一种极端情况下,存在超出预期的情况,后面会介绍。然后就是计算是否是当前token不够,需要等待一段时间来获取令牌。

其中最值得关注的是 advance 方法

func (lim *Limiter) advance(now time.Time) (newNow time.Time, newLast time.Time, newTokens float64) {
	last := lim.last
	if now.Before(last) {
		last = now
	}

	// Avoid making delta overflow below when last is very old.
	maxElapsed := lim.limit.durationFromTokens(float64(lim.burst) - lim.tokens)
	elapsed := now.Sub(last)
	if elapsed > maxElapsed {
		elapsed = maxElapsed
	}

	// Calculate the new number of tokens, due to time that passed.
	delta := lim.limit.tokensFromDuration(elapsed)
	tokens := lim.tokens + delta
	if burst := float64(lim.burst); tokens > burst {
		tokens = burst
	}

	return now, last, tokens
}

实现也很清晰,根据令牌桶里保存的上一次取令牌的时间,计算出两次时间的时间差,这里有个点是使用 durationFromTokens 方法来计算此时填满令牌桶需要的时间。取二者最小,避免delta过大,变量tokens溢出。

durationFromTokens是用来转换token生成的时间的工具方法,比如输入n个token 返回值是生成这n个token需要多少时间。

tokensFromDuration也是工具方法,用来转换一段时间能生成多少个token。

极端情况

当使用这个令牌桶时,如果burst为1,Limite为int32的最大值(2147483647)的时候,并发情况,会出现预期之外的情况。

在这个时候,上面两个工具函数的输入输出对应情况:

生成的token数量需要的时间
00
10
20
31ns
41ns
52ns
62ns
73ns

耗时生成的token数量
0ns0
1ns2.147483647
2ns4.294967294
3ns6.442450941

可以留意到,如果是limit比较大,但是不是足够打,而burst足够小,会有一种情况,导致桶中的令牌会被迅速取完。

因为在rate包中burst代表的其实是令牌桶的大小。

当burst也就是桶的size很小,假设burst为1,在请求并发的时候,假设很多个请求拿到的time的纳秒时间都是相同,就会导致桶里的令牌被迅速取走,而时间戳相同,所以不会往桶里加令牌,这样后续的请求,即使在limit设置很高的情况下,也会返回false,需要等待至少1ns的时间。

这时候,可以增大burst,减少这种请求取令牌的时候time相同时迅速取完桶里的令牌的情况。