ab或wrk压测时QPS统计值偏高,因ab不控发包节奏致请求堆积,而限流器按固定速率补令牌;需用wrk --rate=10匀速压测,并通过时间戳统计单位时间通过数验证精度,容忍±0.5 QPS浮动。

用 ab 或 wrk 做压测时,为什么 QPS 统计值总比配置值高?
因为 ab 默认不控制发包节奏,它会把所有并发请求尽可能“堆”在极短时间内发出(比如 100 并发下,前 50ms 就发完),而限流器的令牌补充是连续过程——rate.NewLimiter(10, 10) 表示每秒补 10 个、桶最多存 10 个,但若 100 个请求在 200ms 内砸过来,前 10 个立刻通过,剩下 90 个全被拒,此时 ab 报出的“QPS”其实是 (成功请求数 / 总耗时),不是真实服务端处理速率。
真正要验证限流精度,得让压测工具按目标速率匀速打点:
-
wrk -t4 -c100 -d10s --rate=10 http://localhost:8080/api:强制每秒只发 10 个请求,观察成功率是否稳定在 100% - 避免用
ab -c100,它只控制并发连接数,不控频次 - 加
-H "X-Real-IP: 192.168.1.1"固定 IP,防止多 IP 分散到不同限流器实例
怎么写断言验证限流器没漏放行?
别依赖日志或肉眼数响应码。直接在测试里用 time.Now() 打时间戳,统计单位时间窗口内通过数:
func TestLimiterAccuracy(t *testing.T) {
limiter := rate.NewLimiter(5, 5) // 5 QPS,burst=5
start := time.Now()
var passed int
for i := 0; i 0.5 { // 容忍 ±0.5 QPS 浮动
t.Errorf("expected ~5 QPS, got %.2f", actualQPS)
}
}
注意点:
- 不用
time.Tick驱动循环——它本身有调度延迟,会污染测量 - sleep 时间设为
100ms是为了覆盖多个填充周期(5 QPS → 每 200ms 补 1 token),避免偶然性 - burst 参数必须 ≥ rate,否则第一次就卡死,测不出持续吞吐
并发调用 Allow() 时,原子性怎么验证?
手写限流器容易在 tokens 和 lastFillTime 更新上丢原子性,导致计数漂移。官方 rate.Limiter 内部用原子操作+纳秒级时间戳,但你仍需验证它在极端并发下不崩:
用 go test -race 跑以下代码:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
func TestConcurrentAllow(t *testing.T) {
limiter := rate.NewLimiter(100, 100)
var wg sync.WaitGroup
const total = 10000
passed := int64(0)
for i := 0; i <p>关键看两件事:<br></p>
- 竞态检测器(
-race)是否报错:如果报Read at ... by goroutine X, Previous write at ... by goroutine Y,说明内部状态没保护好 - 最终
passed值是否明显大于rate * duration + burst(比如 2 秒内超 300),那大概率漏放了
为什么本地测试准,上生产就飘?
因为生产环境有 NTP 校准、CPU 抢占、GC STW 导致 time.Now() 跳变,而限流器依赖精确时间差计算令牌增量。官方 rate.Limiter 已处理跳变(补 token 前会做 min(tokens, capacity) 截断),但你自己写的可能没这层防护。
最容易被忽略的是:
- 系统时间回拨时,
lastFillTime变小 → 下次计算elapsed变大 → 误补巨量 token - 没对
rate * elapsed.Seconds()做math.Floor,浮点误差累积几秒后多出 1 个 token - 用
time.Now().UnixMilli()算窗口,但在 100ms 窗口下误差 ±1ms,10 并发就可能集体挤进同一毫秒片
真要压准,得在容器里关掉 NTP,用 clock_gettime(CLOCK_MONOTONIC) 替代(Go 运行时已默认用它,但你的自定义逻辑别绕过)。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










