golang GC profile? runtime.mallocgc 似乎是顶级的; 然后正在移动到同步.汇集解决方案



>我有一个用 Go 编写的应用程序正在做消息处理,需要以 20K/秒(可能更多)的速率从网络 (UDP) 接收消息,并且每条消息可以达到 UDP 数据包的最大长度(64KB 标头大小),程序需要解码这个传入数据包并编码为另一种格式并发送到另一个网络;

现在在 24Core + 64GB RAM 机器上,它运行正常,但偶尔会丢失一些数据包,编程模式已经使用多个 Go-例程/通道遵循管道,它占用整个机器 CPU 负载的 10%; 因此,它有可能使用更多的 CPU% 或 RAM 来处理所有 20K/s 的消息而不会丢失任何一个; 然后我开始分析, 在我发现 cpu 配置文件中的这个分析之后,runtime.mallocgc出现在顶部,即垃圾收集器运行时,我怀疑这个 GC 可能是它挂起几毫秒(或几微秒)并丢失一些数据包的罪魁祸首,一些最佳实践说切换到同步。池可能会有所帮助,但我切换到池似乎会导致更多的 CPU 争用,并丢失更多的数据包和更频繁的频率

(pprof) top20 -cum (sync|runtime)
245.99s of 458.81s total (53.61%)
Dropped 487 nodes (cum <= 22.94s)
Showing top 20 nodes out of 22 (cum >= 30.46s)
flat  flat%   sum%        cum   cum%
0     0%     0%    440.88s 96.09%  runtime.goexit
1.91s  0.42%  1.75%    244.87s 53.37%  sync.(*Pool).Get
64.42s 14.04% 15.79%    221.57s 48.29%  sync.(*Pool).getSlow
94.29s 20.55% 36.56%    125.53s 27.36%  sync.(*Mutex).Lock
1.62s  0.35% 36.91%     72.85s 15.88%  runtime.systemstack
22.43s  4.89% 41.80%     60.81s 13.25%  runtime.mallocgc
22.88s  4.99% 46.79%     51.75s 11.28%  runtime.scanobject
1.78s  0.39% 47.17%     49.15s 10.71%  runtime.newobject
26.72s  5.82% 53.00%     39.09s  8.52%  sync.(*Mutex).Unlock
0.76s  0.17% 53.16%     33.74s  7.35%  runtime.gcDrain
0     0% 53.16%     33.70s  7.35%  runtime.gcBgMarkWorker
0     0% 53.16%     33.69s  7.34%  runtime.gcBgMarkWorker.func2

使用游泳池是标准

// create this one globally at program init
var rfpool = &sync.Pool{New: func() interface{} { return new(aPrivateStruct); }}
// get
rf := rfpool.Get().(*aPrivateStruct)
// put after done processing this message
rfpool.Put(rf)

不确定我做错了吗? 或者想知道还有什么其他方法可以调整 GC 以使用更少的 CPU%?Go版本是1.8

该列表显示 pool.getSlow src 到 pool.go 中发生了很多锁争用 golang.org

(pprof) list sync.*.getSlow
Total: 7.65mins
ROUTINE ======================== sync.(*Pool).getSlow in /opt/go1.8/src/sync/pool.go
1.07mins   3.69mins (flat, cum) 48.29% of Total
.          .    144:       x = p.New()
.          .    145:   }
.          .    146:   return x
.          .    147:}
.          .    148:
80ms       80ms    149:func (p *Pool) getSlow() (x interface{}) {
.          .    150:   // See the comment in pin regarding ordering of the loads.
30ms       30ms    151:   size := atomic.LoadUintptr(&p.localSize) // load-acquire
180ms      180ms    152:   local := p.local                         // load-consume
.          .    153:   // Try to steal one element from other procs.
30ms      130ms    154:   pid := runtime_procPin()
20ms       20ms    155:   runtime_procUnpin()
730ms      730ms    156:   for i := 0; i < int(size); i++ {
51.55s     51.55s    157:       l := indexLocal(local, (pid+i+1)%int(size))
580ms   2.01mins    158:       l.Lock()
10.65s     10.65s    159:       last := len(l.shared) - 1
40ms       40ms    160:       if last >= 0 {
.          .    161:           x = l.shared[last]
.          .    162:           l.shared = l.shared[:last]
.       10ms    163:           l.Unlock()
.          .    164:           break
.          .    165:       }
490ms     37.59s    166:       l.Unlock()
.          .    167:   }
40ms       40ms    168:   return x
.          .    169:}
.          .    170:
.          .    171:// pin pins the current goroutine to P, disables preemption and returns poolLocal pool for the P.
.          .    172:// Caller must call runtime_procUnpin() when done with the pool.
.          .    173:func (p *Pool) pin() *poolLocal {

同步。池运行缓慢,并发负载较高。尝试在启动期间分配所有结构一次并多次使用它。例如,您可以在启动时创建多个 goroutine(worker),而不是在每个请求上运行新的 goroutine。我建议阅读这篇文章: https://software.intel.com/en-us/blogs/2014/05/10/debugging-performance-issues-in-go-programs .

https://golang.org/pkg/sync/#Pool

作为短期对象的一部分维护的自由列表不是 适合用于池,因为开销不能很好地摊销 那个场景。实现此类对象更有效 他们自己的免费列表

  1. 您可以尝试将 GOGC 值设置为大于 100。

https://dave.cheney.net/2015/11/29/a-whirlwind-tour-of-gos-runtime-environment-variables

  1. 或者,实现你自己的自由列表。

http://golang-jp.org/doc/effective_go.html#leaky_buffer

Go 1.13(2019 年第 4 季度)可能会改变这一点:请参阅 CL 166961。

最初的问题是问题 22950:">同步:避免清除每个 GC 上的完整池">

我发现令人惊讶的是,每个周期都会再次分配大约 1000 个。这似乎表明池正在清除每个 GC 上的全部内容。
看一眼实现似乎表明确实如此。

结果:

sync:使用受害者缓存消除 GC 上的池行为

目前,每个池在每个 GC 开始时都会完全清除。
对于池的重度用户来说,这是一个问题,因为它会在清除池后立即导致分配峰值,从而影响吞吐量和延迟。

此 CL 通过引入受害者缓存机制来解决此问题。

不是清除池,而是删除受害者缓存,主缓存 移动到受害者缓存。

因此,在稳定状态下,(大致)没有新的分配,但如果池使用率下降,对象仍将在两个 GC(而不是一个)中收集。

这种受害者缓存方法还改善了池对 GC 动态的影响。
当前的方法会导致池中的所有对象生存期较短。但是,如果应用程序处于稳定状态,并且只是要重新填充其池,则这些对象会影响实时堆大小,就像它们长期存在一样
由于池化对象在计算 GC 触发器和目标时计为生存期较短,但在实时堆中充当生存期较长的对象,因此这会导致 GC 触发过于频繁。
如果池对象是应用程序堆的重要部分,则此 增加 GC 的 CPU 开销。受害者缓存允许池化对象 将 GC 触发器和目标作为长期对象进行影响。

这对Get/Put性能没有影响,但大大降低了 发生 GC 时对池用户的影响。
PoolExpensiveNew证明了这一点,在 调用 "New" 函数。

最新更新