test: make limiter benchmarks measure their stated operations
- BenchmarkLimiterSweep 把「新建限流器 + 填充 5000 个 IP」的初始化成本计入了 被测区间,测得的 ~10.7ms/op 不能解释为单次 sweep 成本。改用 StopTimer/StartTimer 把初始化排除在计时之外(现 ~1.6ms/op),并新增 BenchmarkLimiterSweepWithFill 明确保留「填充+回收」的端到端口径(~16ms/op,含初始化)。 - 并行基准中每个 worker 都从 i := 0 开始,多 IP 场景实际退化为争抢同一个 IP。 改为按 worker 分配互不相同的起始偏移(乘一个与 IP 数互质的步长),使访问分布 真正覆盖多个 IP;偏移只在 worker 启动时计算一次,不增加每次请求的测试开销。 - 统一新旧对比的状态:两套基准都预填 limit+1 条记录,使双方都处于「已满→拒绝」 的稳态拒绝路径,且每-IP 记录条数一致(101)。原基准旧实现预填 limit 条,第 1 次 调用会先走放行路径(把记录推到 limit+1,增长有界);原基准新实现预填 limit 条 即已拒绝(记录数 limit)。两边的路径与记录数并不严格同态,故此前的新旧对比 不能作为「同状态对比」沿用;统一后实际数字变化不大,报告里会写明。
This commit is contained in:
1 parent
d9d2bbb850
commit
8d6781e121
1 file changed
+104
-15
+104
-15
@@ -3,10 +3,47 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 基准可比性说明(重要)
|
||||
//
|
||||
// 旧实现(allowOld)与新实现(rateLimiter)在「窗口已满」时都走拒绝路径,
|
||||
// 但两者的边界语义不同:
|
||||
// - 旧实现:判断条件为 len(entries[ip]) > limit,且判断发生在记录当前请求
|
||||
// 之前,因此窗口内实际会放行 limit+1 次(历史边界偏差)。
|
||||
// - 新实现:判断条件为 len(times) >= limit,窗口内恰好放行 limit 次。
|
||||
//
|
||||
// 为了让新旧对比处于**同一状态**(同一路径、同一每-IP 记录条数),下面两套
|
||||
// 基准统一预填 limit+1 条记录,使一次调用即落入「已满 → 拒绝」的稳态:
|
||||
// - 旧实现:len(entries[ip]) == limit+1 > limit → 返回 false,不再追加,
|
||||
// 记录数保持 limit+1。
|
||||
// - 新实现:len(times) == limit+1 >= limit → 返回 false,记录数保持 limit+1。
|
||||
// 因此两组基准测的都是 **拒绝路径的稳态成本**,且每-IP 记录条数一致(101),
|
||||
// 可直接比较。
|
||||
//
|
||||
// 做这个统一的两个理由(原基准的问题):
|
||||
// 1. 原基准旧实现预填 limit 条,第 1 次调用会先走放行路径把记录推到 limit+1,
|
||||
// 此后才进入与上面相同的稳态;即计时区间内混入了一次路径不同的调用。
|
||||
// 注意其增长是**有界的**(稳定在 limit+1),并非无界增长。
|
||||
// 2. 原基准新实现预填 limit 条即已拒绝,记录数为 limit;旧实现稳态为 limit+1。
|
||||
// 两边记录数相差 1,不是严格同态。
|
||||
// 统一后旧结果**不能直接沿用**:数字本身变化不大(见报告),但此前的对比在构造上
|
||||
// 并非同态,无法据其宣称「同状态对比」。
|
||||
//
|
||||
// 注意:这**不是**放行路径的对比。放行路径仅在记录数未达上限时出现,且对同一
|
||||
// 记录序列旧实现比新实现多放行一次(limit+1 vs limit,即历史边界偏差),两者
|
||||
// 不存在可直接对比的同一放行状态。如需放行路径数据,必须单独说明状态与记录数,
|
||||
// 不能与下表混用。
|
||||
//
|
||||
// 时钟口径:New / Old 两组都用注入的固定时间(同一 now),属于「纯算法」对比;
|
||||
// NewRealClock 用生产入口 allow()(锁内采样 time.Now),衡量真实时钟开销,
|
||||
// 与前面两组不可直接混比。
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// allowOld 复刻优化前的限流实现,仅用于基准对比:
|
||||
// 每个请求都加全局锁并遍历所有 IP,且从不删除过期 IP 键。
|
||||
func allowOld(entries map[string][]time.Time, mu *sync.Mutex, ip string, limit int64, now time.Time) bool {
|
||||
@@ -37,22 +74,40 @@ func makeIPs(n int) []string {
|
||||
return ips
|
||||
}
|
||||
|
||||
// primeRecords 是各 IP 预填的记录条数:limit+1,使新旧实现都直接进入稳态拒绝
|
||||
// 路径,且每-IP 记录条数一致(见文件头部说明)。
|
||||
func primeRecords(limit int64) int64 { return limit + 1 }
|
||||
|
||||
// workerStride 是并行基准中相邻 worker 的起始 IP 偏移步长。
|
||||
// 原实现每个 worker 都从 i := 0 开始,导致大量 worker 争抢同一个 IP,
|
||||
// 多 IP 场景实际退化成单 IP。给每个 worker 一个互不相同的起始偏移后,
|
||||
// 多 IP 场景才能真正覆盖不同 IP 的访问分布。
|
||||
// 取一个与常见 IP 数量互质的较大质数,使 worker 起始位置在 IP 空间上分散。
|
||||
const workerStride = 1000003
|
||||
|
||||
// parallelStart 返回当前 worker 的起始索引,仅在每个 worker 启动时调用一次,
|
||||
// 不进入 pb.Next() 循环,因此不引入每次请求的额外开销。
|
||||
func parallelStart(seq *atomic.Uint64, ipCount int) int {
|
||||
return int(seq.Add(1)-1) * workerStride % ipCount
|
||||
}
|
||||
|
||||
func benchNew(b *testing.B, ipCount int, limit int64) {
|
||||
l := newRateLimiter()
|
||||
l.sweepEvery = time.Hour // 排除回收开销,只衡量单请求处理成本
|
||||
ips := makeIPs(ipCount)
|
||||
now := time.Now()
|
||||
// 预填满各 IP 的窗口,使热路径处于「已满」状态。
|
||||
// 预填至 limit+1 条,使热路径处于「已满 → 拒绝」的稳态(见文件头部说明)。
|
||||
for _, ip := range ips {
|
||||
for j := int64(0); j < limit; j++ {
|
||||
for j := int64(0); j < primeRecords(limit); j++ {
|
||||
l.allowAt(ip, limit, now)
|
||||
}
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
var seq atomic.Uint64
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
i := parallelStart(&seq, ipCount)
|
||||
for pb.Next() {
|
||||
l.allowAt(ips[i%ipCount], limit, now)
|
||||
i++
|
||||
@@ -61,6 +116,7 @@ func benchNew(b *testing.B, ipCount int, limit int64) {
|
||||
}
|
||||
|
||||
// benchNewRealClock 衡量生产入口(锁内采样 time.Now)的单请求成本。
|
||||
// 时钟与 New/Old 两组不同,结果只能在这两者之间比较。
|
||||
func benchNewRealClock(b *testing.B, ipCount int, limit int64) {
|
||||
l := newRateLimiter()
|
||||
l.sweepEvery = time.Hour
|
||||
@@ -68,8 +124,9 @@ func benchNewRealClock(b *testing.B, ipCount int, limit int64) {
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
var seq atomic.Uint64
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
i := parallelStart(&seq, ipCount)
|
||||
for pb.Next() {
|
||||
l.allow(ips[i%ipCount], limit)
|
||||
i++
|
||||
@@ -82,16 +139,18 @@ func benchOld(b *testing.B, ipCount int, limit int64) {
|
||||
var mu sync.Mutex
|
||||
ips := makeIPs(ipCount)
|
||||
now := time.Now()
|
||||
// 与 benchNew 保持同一状态:预填 limit+1 条 → 稳态拒绝路径。
|
||||
for _, ip := range ips {
|
||||
for j := int64(0); j < limit; j++ {
|
||||
for j := int64(0); j < primeRecords(limit); j++ {
|
||||
entries[ip] = append(entries[ip], now)
|
||||
}
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
var seq atomic.Uint64
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
i := parallelStart(&seq, ipCount)
|
||||
for pb.Next() {
|
||||
allowOld(entries, &mu, ips[i%ipCount], limit, now)
|
||||
i++
|
||||
@@ -107,20 +166,50 @@ func BenchmarkLimiterOld_ManyIPs(b *testing.B) { benchOld(b, 1000, 100) }
|
||||
func BenchmarkLimiterNewRealClock_SingleIP(b *testing.B) { benchNewRealClock(b, 1, 100) }
|
||||
func BenchmarkLimiterNewRealClock_ManyIPs(b *testing.B) { benchNewRealClock(b, 1000, 100) }
|
||||
|
||||
// BenchmarkLimiterSweep 衡量一次回收(清理大量过期 IP)的开销。
|
||||
// fixSweepLimiter 构造一个「已填充 n 个 IP、且下一次 allow 必然触发回收」的限流器。
|
||||
// 填充工作不计入任何计时区间:调用方需在 StopTimer 状态下调用它。
|
||||
func fixSweepLimiter(n int, base time.Time) *rateLimiter {
|
||||
ips := makeIPs(n)
|
||||
l := newRateLimiter()
|
||||
l.sweepEvery = time.Hour // 填充期间不触发回收
|
||||
for _, ip := range ips {
|
||||
l.allowAt(ip, 10, base)
|
||||
}
|
||||
l.sweepEvery = 0 // 下一次 allow 立即触发 sweepLocked
|
||||
return l
|
||||
}
|
||||
|
||||
// BenchmarkLimiterSweep 只衡量**一次回收操作**本身(5000 个过期 IP 的清理)。
|
||||
//
|
||||
// 构造限流器与填充 5000 个 IP 属于初始化成本,通过 StopTimer/StartTimer 排除在
|
||||
// 计时区间之外;报告中的 ns/op、B/op、allocs/op 都只反映 sweepLocked 及其触发的
|
||||
// 那一次 allow 调用本身,可以解释为「单次 sweep 的成本」。
|
||||
func BenchmarkLimiterSweep(b *testing.B) {
|
||||
const n = 5000
|
||||
ips := makeIPs(n)
|
||||
base := time.Now()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
l := newRateLimiter()
|
||||
l.sweepEvery = time.Hour
|
||||
for _, ip := range ips {
|
||||
l.allowAt(ip, 10, base)
|
||||
}
|
||||
l.sweepEvery = 0
|
||||
l.allowAt("192.168.0.1", 10, base.Add(2*time.Minute))
|
||||
b.StopTimer()
|
||||
l := fixSweepLimiter(n, base)
|
||||
next := base.Add(2 * time.Minute)
|
||||
b.StartTimer()
|
||||
|
||||
l.allowAt("192.168.0.1", 10, next) // 触发 sweepLocked
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLimiterSweepWithFill 衡量**完整周期**:新建限流器 + 填充 5000 个 IP
|
||||
// + 触发一次回收。它包含初始化与数据填充成本,因此**不能**当作纯回收耗时解释。
|
||||
// 保留它是为了观察「填充 + 回收」整体开销随规模的变化。
|
||||
func BenchmarkLimiterSweepWithFill(b *testing.B) {
|
||||
const n = 5000
|
||||
base := time.Now()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
l := fixSweepLimiter(n, base)
|
||||
next := base.Add(2 * time.Minute)
|
||||
l.allowAt("192.168.0.1", 10, next)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user