fastrand 高性能伪随机数生成器:原理、基准测试与在 VictoriaMetrics 中的实战应用
【免费下载链接】VictoriaMetricsVictoriaMetrics: fast, cost-effective monitoring solution and time series database项目地址: https://gitcode.com/GitHub_Trending/vi/VictoriaMetrics
fastrand 是 VictoriaMetrics 项目通过vendor/github.com/valyala/fastrand引入的一个高性能伪随机数生成器(PRNG)库,由 valyala 维护,当前仓库锁定版本为v1.1.0(见 go.mod)。它以极低的分配开销和出色的多核扩展性著称,被广泛用于 VictoriaMetrics 中各类定时任务与连接管理的"随机抖动(jitter)"场景。读完本文,你将掌握 fastrand 的设计原理(基于sync.Pool的无锁取用模型)、核心 API 的底层实现细节、官方基准测试数据,以及它在该仓库中防止"惊群效应(Thundering Herd)"的真实工程用法。
什么是 fastrand
fastrand 是一个"快速伪随机数生成器",其官方定位非常明确:针对速度进行极致优化,并且在多 CPU 环境下性能可线性扩展。它不提供密码学强度保证,README 与源码注释均明确提示:如果需要生成加密安全的随机数,请使用 Go 标准库的crypto/rand。
与 Go 标准库math/rand相比,fastrand 通过两方面的设计实现性能优势:
- 避免全局锁竞争:标准库
math/rand的顶层函数rand.Int31n等会经过一个全局锁保护的共享源,多核高并发下锁会成为瓶颈; - 复用而非分配:通过
sync.Pool复用 PRNG 实例,避免高并发下的对象分配与 GC 压力。
核心特性
按照 fastrand 官方 README 的表述,其特性只有两条,但都直指高并发性能场景的本质:
- Optimized for speed(为速度优化):在单核(
GOMAXPROCS=1)下与标准库性能相当,在多核下显著更快; - Performance scales on multiple CPUs(多核性能可扩展):并发调用不会因为锁竞争而退化,吞吐随 CPU 数增长。
工作原理:用 sync.Pool"伪装"per-CPU 生成器
README 中对其工作原理的描述非常直白:它"滥用"了sync.Pool来维护"per-CPU"的伪随机数生成器。作者在 README 中还留下了一个 TODO:未来希望改用真正的 per-CPU 生成器(注意这里的引号,说明它只是近似 per-CPU,并非操作系统级的 CPU 亲和绑定)。
sync.Pool是 Go 标准库提供的对象池,其设计巧妙之处在于:每个 P(Processor,Go 调度器中的逻辑处理器)都维护一份本地缓存,Get/Put 操作在无竞争时不加锁,因此天然契合"每个 CPU 各取各的生成器"的诉求。
顶层 API:Uint32 与 Uint32n
从 fastrand.go 的源码可以看到其完整的并发安全顶层函数实现:
// Uint32 returns pseudorandom uint32. // // It is safe calling this function from concurrent goroutines. func Uint32() uint32 { v := rngPool.Get() if v == nil { v = &RNG{} } r := v.(*RNG) x := r.Uint32() rngPool.Put(r) return x } var rngPool sync.Pool // Uint32n returns pseudorandom uint32 in the range [0..maxN). // // It is safe calling this function from concurrent goroutines. func Uint32n(maxN uint32) uint32 { x := Uint32() // See http://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ return uint32((uint64(x) * uint64(maxN)) >> 32) }关键设计点:
Uint32()从全局rngPool取一个*RNG,用完后立刻放回,整个生命周期不发生堆分配(池命中时);Uint32n(maxN)返回[0, maxN)区间内的随机数,但没有使用取模运算,而是采用 Daniel Lemire 提出的"无模约简"技巧:uint32((uint64(x) * uint64(maxN)) >> 32)。取模指令在 CPU 上开销较大,而乘加移位只需 2~3 条指令,这是其单次调用仅数个纳秒的关键之一;- 两个函数都标注为并发安全,可直接从多个 goroutine 同时调用。
RNG 实例:xorshift 算法
RNG是一个极简结构体,内部仅保存一个uint32状态,算法为经典的xorshift32(见 fastrand.go):
// RNG is a pseudorandom number generator. // // It is unsafe to call RNG methods from concurrent goroutines. type RNG struct { x uint32 } // Uint32 returns pseudorandom uint32. func (r *RNG) Uint32() uint32 { for r.x == 0 { r.x = getRandomUint32() } // See https://en.wikipedia.org/wiki/Xorshift x := r.x x ^= x << 13 x ^= x >> 17 x ^= x << 5 r.x = x return x } // Seed sets the r state to n. func (r *RNG) Seed(n uint32) { r.x = n }实现要点:
- 状态极小:整个生成器只有 4 字节状态,完全内联于调用方,无任何指针追逐和缓存行跨越;
- 自纠错初始化:xorshift 状态不能为 0(否则永远输出 0),因此
Uint32开头用for r.x == 0循环兜底,从时间纳秒派生初始值; - 固定移位参数:
13 / 17 / 5是 xorshift32 的经典参数组合,周期为 2³²−1,统计特性满足一般工程用途; - Seed 手动播种:
RNG.Seed(n)允许调用方直接设置状态,方便测试复现确定性序列; - 方法级非并发安全:
RNG的方法明确标注"unsafe to call from concurrent goroutines",因为 xorshift 的读-改-写序列不具备原子性。若你持有独立的RNG实例并需要并发使用,必须自行加锁(这也正是官方基准中BenchmarkRNGUint32nWithLock存在的意义)。
初始化种子来源
当池中状态为零时,由getRandomUint32生成初始状态(见 fastrand.go):
func getRandomUint32() uint32 { x := time.Now().UnixNano() return uint32((x >> 32) ^ x) }它取time.Now().UnixNano()的高 32 位与低 32 位异或,作为初始状态。这种方式无需调用系统熵源,几乎零成本,也解释了为何 README 强调它"不是加密安全的随机数"。
官方基准测试结果
README 提供了三组完整的基准数据,分别在GOMAXPROCS=1、2、4下运行,对比了fastrand.Uint32n(顶层函数)与标准库math/rand.Int31n(顶层函数),以及各自基于独立 RNG 实例(含/不含锁)的变体:
GOMAXPROCS=1(单核)
$ GOMAXPROCS=1 go test -bench=. github.com/valyala/fastrand goos: linux goarch: amd64 pkg: github.com/valyala/fastrand BenchmarkUint32n 50000000 29.7 ns/op BenchmarkRNGUint32n 200000000 6.50 ns/op BenchmarkRNGUint32nWithLock 100000000 21.5 ns/op BenchmarkMathRandInt31n 50000000 31.8 ns/op BenchmarkMathRandRNGInt31n 100000000 17.9 ns/op BenchmarkMathRandRNGInt31nWithLock 50000000 30.2 ns/op PASS ok github.com/valyala/fastrand 10.634sGOMAXPROCS=2(双核)
$ GOMAXPROCS=2 go test -bench=. github.com/valyala/fastrand goos: linux goarch: amd64 pkg: github.com/valyala/fastrand BenchmarkUint32n-2 100000000 17.6 ns/op BenchmarkRNGUint32n-2 500000000 3.36 ns/op BenchmarkRNGUint32nWithLock-2 50000000 32.0 ns/op BenchmarkMathRandInt31n-2 20000000 51.2 ns/op BenchmarkMathRandRNGInt31n-2 100000000 11.0 ns/op BenchmarkMathRandRNGInt31nWithLock-2 20000000 91.0 ns/op PASS ok github.com/valyala/fastrand 9.543sGOMAXPROCS=4(四核)
$ GOMAXPROCS=4 go test -bench=. github.com/valyala/fastrand goos: linux goarch: amd64 pkg: github.com/valyala/fastrand BenchmarkUint32n-4 100000000 14.2 ns/op BenchmarkRNGUint32n-4 500000000 3.30 ns/op BenchmarkRNGUint32nWithLock-4 20000000 88.7 ns/op BenchmarkMathRandInt31n-4 10000000 145 ns/op BenchmarkMathRandRNGInt31n-4 200000000 8.35 ns/op BenchmarkMathRandRNGInt31nWithLock-4 20000000 102 ns/op PASS ok github.com/valyala/fastrand 11.534s以上数据直接引自 fastrand README 的 benchmark 章节,测试环境为 linux/amd64,Go 版本以该库发布当时为准;不同 Go 版本、硬件与 Go 版本(Go 1.20+ 的
math/rand全局源已自动加锁并同样基于无锁设计)下数字会有变化,建议以你本地go test -bench=. github.com/valyala/fastrand的实际输出为准。
关键结论:多核可扩展性
README 给出的结论非常清晰:
- 在
GOMAXPROCS=1时,fastrand.Uint32n与rand.Int31n性能相当(29.7 ns vs 31.8 ns); - 在
GOMAXPROCS=2时,fastrand.Uint32n比rand.Int31n快约 3 倍(17.6 ns vs 51.2 ns); - 在
GOMAXPROCS=4时,fastrand.Uint32n比rand.Int31n快约 10 倍(14.2 ns vs 145 ns)。
同时可以观察到:fastrand.Uint32n在核数从 1 增加到 4 的过程中,单次调用耗时从 29.7 ns 降到 14.2 ns(并发吞吐提升),而rand.Int31n却从 31.8 ns 恶化到 145 ns——这正是"顶层函数带全局锁"与"sync.Pool无锁化取用"两种设计在并发放大后的直接体现。另外BenchmarkRNGUint32n(持有一个RNG实例、无锁、仅单 goroutine 使用)稳定在 3.3~6.5 ns 级别,展示了 xorshift 本身的极致速度;一旦加上锁(RNGUint32nWithLock),性能随核数增长而明显劣化,印证了"方法级非并发安全"的注释是有意为之的设计取舍。
在 VictoriaMetrics 中的真实工程应用
fastrand 在 VictoriaMetrics 中并不是"为用而用",而是精准服务于两类高频/高并发场景:连接超时抖动与定时任务抖动,核心目的都是避免"惊群效应"——当大量 goroutine 在同一时刻醒来或建立连接时,瞬时打爆后端资源。
场景一:HTTP 连接超时抖动(lib/httpserver)
在 lib/httpserver/httpserver.go 中,VictoriaMetrics 为每个连接计算一个带抖动的超时截止时间:
if *connTimeout > 0 { s.s.ConnContext = func(ctx context.Context, _ net.Conn) context.Context { timeoutSec := connTimeout.Seconds() // Add a jitter for connection timeout in order to prevent Thundering herd problem // when all the connections are established at the same time. jitterSec := fastrand.Uint32n(uint32(timeoutSec / 10)) deadline := fasttime.UnixTimestamp() + uint64(timeoutSec) + uint64(jitterSec) return context.WithValue(ctx, connDeadlineTimeKey, &deadline) } }这里的fastrand.Uint32n(uint32(timeoutSec / 10))会在[0, timeoutSec/10)秒内随机取一个抖动值,叠加到基础超时上。当大量客户端同时建连时,每个连接的超时点被随机分散,避免"所有连接在同一时刻超时重连"造成的雪崩。
场景二:数据去重调度抖动(lib/storage/table.go)
在 lib/storage/table.go 中,定时去重任务在每轮调度前加上 25% 的随机抖动:
// adds 25% jitter in order to prevent thundering herd problem addJitter := func(d time.Duration) time.Duration { dv := d / 4 p := float64(fastrand.Uint32()) / (1 << 32) return d + time.Duration(p*float64(dv)) } d := addJitter(finalDedupScheduleInterval) t := time.NewTicker(d)这里用到的是fastrand.Uint32()配合(1 << 32)归一化出[0, 1)浮点随机数,再乘以d/4得到 0~25% 的抖动。将原始定时器周期放大到原来的 1.0~1.25 倍,使集群中多个存储节点/分区的去重扫描不在同一时刻扎堆。
场景三:通用时长抖动工具(lib/timeutil)
VictoriaMetrics 将这一模式封装成了通用工具函数 lib/timeutil/timeutil.go:
// AddJitterToDuration adds up to 10% random jitter to d and returns the resulting duration. // // The maximum jitter is limited by 10 seconds. func AddJitterToDuration(d time.Duration) time.Duration { dv := min(d/10, 10*time.Second) p := float64(fastrand.Uint32()) / (1 << 32) return d + time.Duration(p*float64(dv)) }该函数被仓库中大量周期性任务复用,例如:
- lib/blockcache/blockcache.go:缓存清理任务以 1 分钟/3 分钟为基准加抖动;
- lib/bytesutil/internstring.go:字符串驻留缓存过期清理;
- lib/lrucache/lrucache.go:LRU 缓存过期清理;
- lib/mergeset/table.go:mergeset 表刷盘回调;
- lib/storage/metric_id_cache.go 与 lib/storage/date_metric_id_cache.go:metric ID 缓存轮换;
- lib/storage/partition.go:分区维护任务;
- lib/storage/metricsmetadata/storage.go:指标元数据缓存过期;
- lib/promscrape/discovery/kubernetes/api_watcher.go:Kubernetes 服务发现 API watcher 的重连退避。
此外,lib/uint64set/uint64set_timing_test.go 的基准测试中还演示了如何在单 goroutine 内持有fastrand.RNG实例以获得最高吞吐的典型用法,可作为性能敏感路径的参考模板。
使用建议与注意事项
基于源码与仓库实践,可以总结出以下使用准则:
- 绝不用于加密/安全场景:fastrand 的种子仅来自时间戳,xorshift 序列可预测,必须改用
crypto/rand; - 顶层函数
Uint32/Uint32n是并发安全的:适合被多个 goroutine 共享调用,靠sync.Pool摊薄锁竞争; RNG实例的方法不是并发安全的:若在多个 goroutine 间共享同一个RNG实例,需要外部加锁(加锁后性能会显著下降,参见官方WithLock基准),更推荐的做法是每个 goroutine 各自持有一个实例;Seed用于确定性复现:测试中可通过Seed(n)固定序列,保证可重复断言;- 在 VictoriaMetrics 中的通用范式:周期性任务统一通过
timeutil.AddJitterToDuration(上限 10% 或 10 秒)打散执行时机;连接/调度类场景则直接用fastrand.Uint32n按比例生成抖动,从而在不引入锁和额外依赖的前提下稳定规避惊群效应。
小结
fastrand 以不到 80 行的实现,同时做到了"顶层 API 并发安全、零分配、多核可扩展",其背后是三条朴素但高效的工程决策:用sync.Pool近似 per-CPU 隔离消除锁竞争、用 xorshift32 极小状态换极致速度、用 Lemire 无模约简替代昂贵的取模指令。在 VictoriaMetrics 中,它被用于连接超时与各类周期任务的随机抖动,是保证大规模部署下时序数据写入与合并稳定的基础设施之一。若你需要在高并发 Go 服务中生成大量非加密随机数,可直接参考 fastrand.go 的实现,并结合官方 README 中的基准方法在自己的硬件上复测。
【免费下载链接】VictoriaMetricsVictoriaMetrics: fast, cost-effective monitoring solution and time series database项目地址: https://gitcode.com/GitHub_Trending/vi/VictoriaMetrics
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考