背景

最近在实现一个随机负载均衡器的时候发现一个问题,在高并发的情况下,官方标准库 rand.Intn() 性能会急剧下降。翻了下实现以后才发现它内部居然是全局共享了同一个 globalRand 对象。

一段测试代码:

func BenchmarkGlobalRand(b *testing.B) {
   b.RunParallel(func(pb *testing.PB) {
      for pb.Next() {
         rand.Intn(100)
      }
   })
}

func BenchmarkCustomRand(b *testing.B) {
   b.RunParallel(func(pb *testing.PB) {
      rd := rand.New(rand.NewSource(time.Now().Unix()))
      for pb.Next() {
         rd.Intn(100)
      }
   })
}

输出:

BenchmarkGlobalRand
BenchmarkGlobalRand-8 18075486 66.1 ns/op
BenchmarkCustomRand
BenchmarkCustomRand-8 423686118 2.38 ns/op

解决思路

最理想对情况是可以在每个 goroutine 内创建一个私有的 rand.Rand 对象,从而实现真正的无锁。

但在很多其他场景下,我们并不能直接控制调用我们的 goroutine,又或者 goroutine 数量过多以至于无法承受这部分内存成本。

此时的一个思路是使用 sync.Pool 来为 rand.Source 创建一个池,当多线程并发读写时,优先从自己当前 P 中的 poolLocal 中获取,从而减少锁的竞争。同时由于只是用 pool 扩展了原生的 rngSource 对象,所以可以兼容其 rand.Rand 下的所有接口调用。

基于这个思路,实现了一个 fastrand 库放到了 github。

从 benchmark 中可以看到性能提升显著,在并发条件下,比原生全局 rand 快了大约 8 倍.

BenchmarkStandardRand
BenchmarkStandardRand-8                         60870416                19.1 ns/op             0 B/op          0 allocs/op
BenchmarkFastRand
BenchmarkFastRand-8                             100000000               10.7 ns/op             0 B/op          0 allocs/op
BenchmarkStandardRandWithConcurrent
BenchmarkStandardRandWithConcurrent-8           18058663                67.8 ns/op             0 B/op          0 allocs/op
BenchmarkFastRandWithConcurrent
BenchmarkFastRandWithConcurrent-8               132542940                8.79 ns/op            0 B/op          0 allocs/op