HeadlinesBriefing HeadlinesBriefing.com

Go GC暂停峰值:交换驱逐问题

Hacker News •
×

我之所以写这篇文章,是因为当我决定在生产环境中运行交换空间以吸收内存峰值时,我几乎像你一样想要打自己额头。我有一个cgroup中有两个进程:一个是Go进程,它调用io.ReadAll然后proto.Unmarshal,创建一个blob然后是一个图结构(这个结构被Go的分配器标记为扫描对象)。另一个进程是一个HTTP服务器,大部分时间都保持安静。每当垃圾回收器运行时,它会逐个指针读取这些扫描跨度,并决定如何处理它们。所以我想:好的,在内存压力下,内核会将页面驱逐到交换设备,但由于驱逐是按cgroup而不是按进程进行的,因此两个进程的页面都会被驱逐——所以这变成内核和垃圾回收器之间的交换进出“悲伤舞蹈”的可能性很小。我错了。在实验过程中,我发现了一个可能会伤害到我的问题:Go的垃圾回收器在停止世界暂停期间读取其元数据(位于堆外,在一个未被释放的区域),而这些元数据可能被换出到交换空间。我在Hetzner的一台机器上使用内核6.8和MGLRU启用的环境下做了一个模拟运行。中位数暂停时间约为51微秒。而将元数据放在NVMe上时,最坏的暂停时间达到了40毫秒。为了查清那40毫秒去了哪里,我编写了一个小型bpf脚本来统计停止世界期间的页面错误。这是最坏的情况:39902微秒,期间发生228次页面错误,其中39013微秒花在页面错误上。那40毫秒中的39毫秒花在了228次页面错误上。这些页面错误发生在GC的簿记工作中。这是一个潜在的故障模式。Go的GC在两个点上必须停止世界:当它执行清扫终止时,以及当它执行标记终止时。我们在30分钟内经历了312次这样的暂停。所以这就是为什么会发生这种情况:运行时分配了这些页面。它们没有被释放,但被重用。这些页面在GC周期中被读取。因为内核按年龄驱逐页面,它会将最近最少访问的页面发送到交换空间。GC运行,停止世界,尝试读取这些页面,但现在我们遇到了一次主要页面错误。内核需要读取页表项,然后调用do_swap_page,找到一个新的帧,将其计入cgroup,读取页面,提交bio,等待磁盘完成,然后将它们放回内存——仅此而已。起初那40毫秒似乎无害。但我们正在谈论一次停止世界暂停。那40毫秒意味着所有事情都停止了——在Go的术语中,每个P都停止了,因此,例如,如果一个goroutine正在等待I/O,在那个暂停期间I/O可能会返回,但没有人来处理它。40毫秒是中位数暂停的800倍。在测试过程中,每次内存峰值都会发生两到三次。这真的很多。

来源: Hacker News · 由HeadlinesBriefing整理摘要