由交换空间导致的 40ms Go 垃圾回收器暂停
一个 Go 中 40 ms stop-the-world 暂停的案例,被追溯为垃圾回收器访问了已被换出的元数据,引发了对 GC 如何与操作系统内存压力相互作用的更广泛审视。评论者讨论了低延迟系统是否应彻底避免 GC 语言、禁用 swap,还是使用 `mlock`、协作式或感知 swap 的收集器,以及 on-the-fly GC 或引用计数等替代算法。整场交流凸显了吞吐量与延迟之间的权衡,并表明运行时设计与操作系统级 API 都会显著影响真实世界的暂停行为。
Go 的 GC 设计与“on-the-fly”替代方案
- Go 已经使用并发标记-清扫 GC,典型暂停时间非常短。
- 有人会问,为什么 Go 不采用经典的“on-the-fly” GC 算法(例如 DLG 风格),它们的目标是实现零 stop-the-world(STW)暂停。
- 也有人指出,Go 团队大概知道这些算法;延迟与吞吐量之间的权衡,以及栈/协程扫描的复杂性,可能解释了当前的选择。
- 文中提到 Fil-C 的替代性 on-the-fly 算法,认为它对 Go 的协程模型可能相关。
Swap、缺页异常与延迟尖峰
- 核心问题在于:当 GC 元数据或堆页面被换出时,STW 段可能累积大量缺页异常,从而把内存压力转化为巨大的延迟尖峰。
- 有人认为,如果你在意延迟,就应该禁用 swap;反方则回应说,这只会把停顿转移到代码页上,并且可能触发 OOM。
- 讨论中解释了内核如何回收匿名页与文件映射页;禁用 swap 并不会阻止分页,只是改变了被回收的对象。
- 建议的缓解措施包括:对关键区域或元数据使用
mlock/mlockall,谨慎使用 cgroups 和 swappiness,或者接受 OOM-kill 作为比 swap 风暴更可取的结果。
引用计数 vs 跟踪式 GC
- 多位发言者声称引用计数(RC)能提供更可预测的性能;也有人不同意,理由包括:
- 级联释放可能长得不可预测。
- 开销很高,尤其是在多线程程序中的原子 RC。
- 还存在循环引用以及 NUMA/锁的问题。
- 有人偏好混合式或延迟 RC 方案;也有人强调,业界几十年的经验表明,纯 RC 具有明显的权衡。
OS–GC 协作与感知 swap 的设计
- 提出的想法包括:
- 感知 swap 的 GC,在 STW 阶段预取或锁定所用页面。
- 使用现有 API,例如
mlock、madvise、mincore或触页来管理驻留性。 - 更深层的 OS–GC 协作(例如 bookmark collector、协作式内存管理、与
madvise协调的基于 region 的 GC)。
- 有人认为这是一个被错过的研究/实践机会;也有人怀疑这种深度集成短期内会广泛出现。
语言选择与延迟需求
- 一些评论把这件事视为另一条数据点:在可以接受偶发 GC 延迟的场景下使用 Go;对严格低延迟需求则选择 Rust/C++(RAII/手动控制)。
- 还提到了 Discord 的案例:即使垃圾不多,一个大型对象图加上周期性 GC 也可能导致不可接受的尾延迟,从而促使他们重写为非 GC 语言。