bigywater.tech
Go · 80

令牌桶限流:x/time/rate 的 Allow、Wait 与 Reserve

令牌桶限流器的 Allow、Wait、Reserve 有什么区别?burst 到底是什么?这篇用真实运行回答:桶能攒多少、Wait 摊开请求有多均匀、什么情况下 Wait 会立即报错、多个 goroutine 共用时总吞吐是多少,再读 rate.go 里的说明。

5
桶满或空闲很久后,burst=5 的限流器一次最多连续放行的个数
49.9–51.1 ms
20/s 的限流器 Wait 放行的相邻间隔(理论 50 ms)
1003/s
限速 1000/s 时,1、8、64 个 goroutine 共用限流器的实测吞吐

依赖说明

实测 · 本机执行输出
  golang.org/x/time v0.16.0

rate 包来自 golang.org/x/time(v0.16.0),不在标准库里。

burst 是上限

实测 · 本机执行输出
  NewLimiter(10/s, burst 5),桶是满的,连续调用 Allow() 8 次:[true true true true true false false false]
  等 105 ms 后再 Allow():true;紧接着再 Allow():false
  再空闲 1.2 秒(攒了 12 个令牌,但桶只装 5 个)后,连续 Allow() 成功了 5 次(总用时 1307 ms)

桶满时 Allow 连续放行 burst 个(5),然后被拒绝;空闲 1.2 秒本应攒 12 个令牌,桶装不下,只能一口气放行 5 个。

Wait 摊得很匀

实测 · 本机执行输出
  NewLimiter(20/s, burst 1),连续 Wait 11 次,每次放行的时刻(ms): 0 51 101 151 201 251 301 351 401 451 501
  相邻两次放行的间隔:最小 49.9 ms / 最大 51.1 ms(理论 50 ms)

20/s、burst 1 的限流器,Wait 的放行间隔在 49.9–51.1 ms 之间,几乎就是理论的 50 ms。

不空等的错误

实测 · 本机执行输出
  WaitN(ctx, 6)(超过桶容量 5):rate: Wait(n=6) exceeds limiter's burst 5
  速率 1/s,桶已空,ctx 期限 200 ms,Wait 返回:rate: Wait(n=1) would exceed context deadline(耗时 0 ms,没有真的等 1 秒)

WaitN 的 n 超过 burst 会立即报错;如果预计等待时间会超出 ctx 的期限,Wait 也立即返回 “would exceed context deadline”,不会白白等到超时。

Reserve

实测 · 本机执行输出
  桶空时 Reserve():OK=true,Delay=100 ms
  Cancel 之后再 Reserve():Delay=100 ms(取消把令牌还回去了)
  不 Cancel,连续 Reserve 3 次:Delay = 100 / 200 / 300 ms(预约一个比一个往后排)

Reserve 返回需要等待多久,不 Cancel 的话预约会占用未来的令牌:连续 3 次的延迟是 100、200、300 ms。要放弃就调 Cancel 把令牌还回去。

共用一个限流器

实测 · 本机执行输出
  限速 1000/s,1 个 goroutine 共用一个限流器各自循环 Wait,3 秒内通过 3010 次(1003 次/秒)
  限速 1000/s,8 个 goroutine 共用一个限流器各自循环 Wait,3 秒内通过 3017 次(1003 次/秒)
  限速 1000/s,64 个 goroutine 共用一个限流器各自循环 Wait,3 秒内通过 3073 次(1003 次/秒)

限速 1000/s 时,无论 1 个、8 个还是 64 个 goroutine 一起等,总吞吐都是约 1003 次/秒。限流器是整个进程的总量,不是每个 goroutine 各 1000。

自身成本

实测 · 本机执行输出
  Allow():rate.Inf 45 ns/次;有限速率(单线程)66 ns/次;有限速率(10 个 P 并行)200 ns/次

Allow 单线程 66 ns,10 个 P 争同一个限流器 200 ns;速率为 Inf 时 45 ns。对每个请求限流没问题,但每秒几百万次的热点路径要注意。

读文档

实测 · 本机执行输出
  // A Limiter controls how frequently events are allowed to happen.
  // It implements a "token bucket" of size b, initially full and refilled
  // at rate r tokens per second.
  // Informally, in any large enough time interval, the Limiter limits the
  // rate to r tokens per second, with a maximum burst size of b events.
  // As a special case, if r == Inf (the infinite rate), b is ignored.
  // See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets.
  //
  // The zero value is a valid Limiter, but it will reject all events.
  // Use NewLimiter to create non-zero Limiters.
  //
  // Limiter has three main methods, Allow, Reserve, and Wait.
  // Most callers should use Wait.
  //
  // Each of the three methods consumes a single token.
  // They differ in their behavior when no token is available.
  // If no token is available, Allow returns false.
  // If no token is available, Reserve returns a reservation for a future token
  // and the amount of time the caller must wait before using it.
  // If no token is available, Wait blocks until one can be obtained
  // or its associated context.Context is canceled.
  //
  // The methods AllowN, ReserveN, and WaitN consume n tokens.
  //
  // Limiter is safe for simultaneous use by multiple goroutines.

注释写明:令牌桶初始是满的;零值合法但拒绝所有事件;多数调用者应该用 Wait;可以被多个 goroutine 同时使用。

分步演示

令牌桶限流
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。限流器来自 golang.org/x/time v0.16.0(本机代理下载);时间用 time.Now 测,精度毫秒;吞吐实验每种 3 秒;Allow 的成本用 testing.Benchmark;源码注释摘自 rate.go
    • 没有验证:SetLimit/SetBurst 动态调整、WaitN 对大 n 的行为、长时间运行的漂移、Sometimes 等较新的辅助类型、与 HTTP 中间件的集成、其它限流算法(漏桶、滑动窗口)
    京ICP备2026067502号-1京公网安备11010502063487号