【译】Go GC:优先考虑低延迟和简单性

298 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情

原文:Go GC: Prioritizing low latency and simplicity

作者:Richard Hudson

发表时间:2015年8月31日

链接:golang.google.cn/blog/go15gc

前言

Go 正在构建一个垃圾收集器 (GC) ,不仅适用于2015年,也适用于2025年及以后:这个 GC 将支持当今的软件开发,并在未来十年内与新的软件和硬件一起扩展。在这样的未来中不应该存在由 GC 的 STW 机制引起的线程停顿,这已经成为安全、可靠的编程语言(如 Go)能够被更广泛使用的阻碍。

Go 1.5 是对这个未来的第一个预览,它实现的 GC 延迟远低于我们一年前设定的10毫秒的目标。我们在 Gophercon 的一次演讲中展示了一些令人印象深刻的数字。延迟的改进已经引起了很多关注;Robin Verlangen 的博客文章 Billions of requests per day meet Go 1.5 通过端到端的结果验证了我们的方向。我们也特别喜欢 Alan Shreve 的生产服务器图表和他的“神圣的85%减少”的评论。

如今 16G 的 RAM 价格为100美元,并且 CPU 有许多内核,每个内核都有多个硬件线程。在十年内,这种硬件将会变得看起来很古怪,但是今天正在使用 Go 构建的软件将需要扩展以满足不断扩大的需求和下一个大变化。考虑到硬件将提供增加吞吐量的能力,Go 的垃圾收集器被设计为支持低延迟和仅通过一个旋钮knob)进行调优。Go 1.5 是这条道路上的第一大步,这些第一步将永远影响 Go 及其最佳支持的应用程序。这篇博客文章对我们为 Go 1.5 垃圾收集器所做的工作进行了高层次的概述。

概述

为了创建未来十年的垃圾收集器,我们转向了几十年前的算法。Go 的新垃圾收集器是一个并发的、三色的、标记-清除(mark-sweep)收集器,这是 Dijkstra 在1978年首次提出的想法。这是一个经过深思熟虑后与当今(2015)大多数“企业级”垃圾收集器有分歧的想法,我们认为这种想法非常适合现代硬件的特性以及现代软件对延迟的要求。

在三色收集器中,每个对象都是白色、灰色或黑色中的一种,我们将堆视为一个由相互连接的对象组成的图。在 GC 循环开始时,所有对象都是白色的。GC 访问所有的根对象,这些根对象是应用程序可以直接访问的对象,比如全局变量和堆栈上的内容,然后将它们变成灰色。然后 GC 选择一个灰色对象,将其变为黑色,然后扫描该对象以寻找指向其他对象的指针。当扫描发现一个指向白色对象的指针时,它将该对象变为灰色。这个过程不断重复,直到没有更多的灰色对象。此时,剩下的白色对象就是不可访问的并且可以回收重用。

这一切都与应用程序(称为修改器 mutator)同时发生,在收集器运行时更改指针。因此,修改器必须保持不变性,即不会有黑对象指向白对象,以免垃圾收集器与设置在它已经访问过的堆的一部分中的对象失去跟踪。维护这个不变性是写屏障write barrier)的工作,写屏障是一个小型函数,每当堆中的指针被修改时,修改器就会运行它。Go 的写屏障将当前可到达的白色对象涂成灰色,以确保垃圾收集器最终将扫描它来寻找指针。

决定找到所有灰色对象的工作何时完成是微妙的,并且如果我们想要避免阻塞修改器,这也会是开销大和复杂的。为了保持简单,Go 1.5 尽可能多地同时工作,然后短暂地 STW 检查所有灰色对象的潜在来源。在这个最后的 STW 所需的时间和这个 GC 所完成的总工作量之间找到最佳平衡点,是 Go 1.6 的主要可交付成果。

当然,细节决定成败。我们什么时候开始 GC 循环?我们用什么指标来做决定?GC 应该如何与 Go 调度程序交互?我们如何暂停一个修改器线程足够长的时间来扫描它的堆栈?我们如何表示白色,灰色和黑色,以便我们可以有效地发现和扫描灰色对象?我们怎么知道根对象在哪里?我们如何知道对象指针的位置?如何最小化内存碎片?我们如何处理缓存性能问题?堆应该有多大?等等,有些与分配有关,有些与查找可达对象有关,有些与调度有关,但大多都与性能有关。这些领域的低层次讨论超出了这篇博客文章的范围。

在更高的层次上,解决性能问题的一种方法是添加 GC 旋钮,每个旋钮对应一个性能问题。然后程序员可以转动旋钮为他们的应用程序寻找合适的设置。不利的一面是,经过十年间每年一到两个新旋钮,你最终将选择以 《GC旋钮调优就业法案》来结束。这条路走不通。相反,我们提供一个单一的旋钮,称为 GOGC。此值控制堆相比于可访问对象的大小。默认值100意味着堆的总大小现在比上次收集后可达对象的大小要大100% (即两倍)。200意味着堆的总大小比可访问对象的大小要大200% (即3倍)。如果要降低在 GC 中花费的总时间,请增加 GOGC。如果您想用更多的 GC 时间来换取更少的内存,请降低 GOGC。

更重要的是,随着 RAM 由下一代硬件的发展而增加一倍,仅仅将 GOGC 增加一倍,GC 周期的数量就会减少一半。另一方面,由于 GOGC 是基于可到达对象大小的,因此通过将可到达对象增加一倍来使负载增加一倍不需要重新调优。应用程序会自动扩展。此外,运行时团队可以不受持续支持数十个旋钮的限制,专注于根据来自真实客户应用程序的反馈改进运行时。

结语

Go 1.5的 GC 将带来这样一种未来:STW 暂停不再是转向安全和可靠语言的障碍。在未来,应用程序可以毫不费力地与硬件一起扩展,而且随着硬件变得越来越强大,GC 将不会成为更好、更具可扩展性的软件的障碍。在未来十年乃至更长时间里,这将是一个很好的地方。有关 1.5 GC 以及我们如何消除延迟问题的更多细节,请参见 Go GC: 延迟问题解决演示的演讲幻灯片