为什么“并发”Go GC阶段似乎是停止世界?

时间:2016-10-18 16:52:24

标签: go garbage-collection

我正在尝试对不同数量的堆对象的最大STW GC暂停时间进行基准测试。为此,我编写了一个简单的基准测试,用于推送和弹出来自map

的消息
package main

type message []byte

type channel map[int]message

const (
    windowSize = 200000
    msgCount   = 1000000
)

func mkMessage(n int) message {
    m := make(message, 1024)
    for i := range m {
        m[i] = byte(n)
    }
    return m
}

func pushMsg(c *channel, highID int) {
    lowID := highID - windowSize
    m := mkMessage(highID)
    (*c)[highID] = m
    if lowID >= 0 {
        delete(*c, lowID)
    }
}

func main() {
    c := make(channel)
    for i := 0; i < msgCount; i++ {
        pushMsg(&c, i)
    }
}

我用GODEBUG=gctrace=1运行它,在我的机器上输出为:

gc 1 @0.004s 2%: 0.007+0.44+0.032 ms clock, 0.029+0.22/0.20/0.28+0.12 ms cpu, 4->4->3 MB, 5 MB goal, 4 P
gc 2 @0.009s 3%: 0.007+0.64+0.042 ms clock, 0.030+0/0.53/0.18+0.17 ms cpu, 7->7->7 MB, 8 MB goal, 4 P
gc 3 @0.019s 1%: 0.007+0.99+0.037 ms clock, 0.031+0/0.13/1.0+0.14 ms cpu, 13->13->13 MB, 14 MB goal, 4 P
gc 4 @0.044s 2%: 0.009+2.3+0.032 ms clock, 0.039+0/2.3/0.30+0.13 ms cpu, 25->25->25 MB, 26 MB goal, 4 P
gc 5 @0.081s 1%: 0.009+9.2+0.082 ms clock, 0.039+0/0.32/9.7+0.32 ms cpu, 49->49->48 MB, 50 MB goal, 4 P
gc 6 @0.162s 0%: 0.020+10+0.078 ms clock, 0.082+0/0.28/11+0.31 ms cpu, 93->93->91 MB, 96 MB goal, 4 P
gc 7 @0.289s 0%: 0.020+27+0.092 ms clock, 0.080+0/0.95/28+0.37 ms cpu, 178->178->173 MB, 182 MB goal, 4 P
gc 8 @0.557s 1%: 0.023+38+0.086 ms clock, 0.092+0/38/10+0.34 ms cpu, 337->339->209 MB, 346 MB goal, 4 P
gc 9 @0.844s 1%: 0.008+40+0.077 ms clock, 0.032+0/5.6/46+0.30 ms cpu, 407->409->211 MB, 418 MB goal, 4 P
gc 10 @1.100s 1%: 0.009+43+0.047 ms clock, 0.036+0/6.6/50+0.19 ms cpu, 411->414->212 MB, 422 MB goal, 4 P
gc 11 @1.378s 1%: 0.008+45+0.093 ms clock, 0.033+0/6.5/52+0.37 ms cpu, 414->417->213 MB, 425 MB goal, 4 P

有关此输出的文档,请参阅上面的链接。

我的Go版本是:

$ go version
go version go1.7.1 darwin/amd64

根据以上结果,最长挂钟STW暂停时间为0.093 ms。太好了!

然而,作为一个完整性检查,我还手动计算了message

包装创建新mkMessage所需的时间
start := time.Now()
m := mkMessage(highID)
elapsed := time.Since(start)

并打印最慢的elapsed时间。我得到的时间是38.573036ms

我当时很怀疑,因为这与所谓的并发标记/扫描阶段的挂钟时间密切相关,特别是“空闲GC时间”。

我的问题是:为什么GC的并发阶段似乎阻止了mutator?

如果我强制GC定期运行,我手动计算的暂停时间会缩短到<1ms,因此它似乎达到了非实时堆对象的某种限制。如果是这样,我不确定这个限制是什么,以及为什么它会导致GC的并发阶段似乎阻止mutator。

2 个答案:

答案 0 :(得分:1)

并发GC传递通常在一定数量的空间可用于新分配时开始。如果该空间量足以处理在传递完成之前发生的所有分配,则应用程序必须在GC上等待的时间量将是最小的。但是,如果分配数量超过可用空间,则新的分配请求必须等待GC释放更多空间。

请注意,即使程序可以在进行中运行,大多数并发GC系统也有明确的周期。 GC循环的大部分用于识别存在引用的所有对象,并且识别任何单个对象都不存在引用的唯一方法是识别其他所有存在的所有引用。因此,GC循环释放的所有内容将立即释放,并且在此期间不会释放任何内容。

答案 1 :(得分:0)

为了提供比我的评论更多的可见性(和详细信息) - 行为似乎与Go bug #9477中描述的内容相匹配,并由Go changelist 23540修正。

当一个线程分配内存时,垃圾回收器可能会给它做一些内存扫描工作;这被称为“变异助手”。但是之前,这项工作可能包括扫描一张地图,这会导致一个足够大的地图长时间停顿。与问题和评论中报告的内容一致,暂停将在发生一些任意分配时发生 - 不一定在使用地图时 - 并且不会显示在GC暂停统计中,因为它不是停止的一部分 - - 世界阶段。

对Go的修复,在我写的时候还没有发布,是为了限制单个mutator协助扫描多少内存,以便在典型的现代服务器硬件上花费不超过100微秒。如果您遇到此问题,我建议您尝试使用github.com/golang/go存储库中的最新版本(或者一旦它出来的话1.8)。 (自1.7以来也有another big pause-reduction change。)如果你仍然有问题而且找不到任何解释,那我就去疯狂了。