05.07 Go 垃圾回收:从群聊扇出到存活集与时延
返回第五卷 · 对象前置:05.06 · 调度前置:05.05 · RSS 前置:03.06 · 分布前置:11.02
05.07 Go 垃圾回收:从群聊扇出到存活集与时延
DeepTutor 初稿经技术与教学审阅后的静态课程。群成员数、内存速率、GC 目标与时延均为带假设的纸上模型;没有运行 Go、GC 调参、基准、IM 服务或站点。本章以 Go 标准
gc工具链的官方指南为依据;指南说明自身版本边界,实际优化需结合使用的 Go 版本和测量。
一、大群扇出:分配多,与活得久是两种问题
05.06 已用 c-a 的小预览讲谁还引用对象,05.05 区分 G 的运行与等待。现在考虑虚构 IM 群聊:一条消息面向 10 个目标与面向 10000 个目标,若教学实现为每个目标单独复制 1 KiB 临时数据,一次操作新增的纯数据字节分别是 10 KiB 与 10000 KiB≈9.77 MiB。这是一个明示策略的纸上计算,不表示 OpenIM 或任何真实服务一定逐目标复制。
若这些副本很快用完并失去引用,主要压力可能是分配速率:单位时间不断产生新对象,GC 得更频繁地处理它们。若慢设备的有界队列仍持有副本,它们变成存活集的一部分,GC 必须保留;即使反复强制 GC,也不能删除业务仍可达的数据。每个目标的协议、队列和对象头还有额外成本,9.77 MiB 不是进程 RSS 或消息交付成本。
| 第一遍 | 第二遍 | 交付物 |
|---|---|---|
| 根与可达、标记清扫、分配率和存活集 | 并发 GC、GOGC、软内存限制、时延与证据 | 两种群扇出情景、一张目标堆表、一张指标选型表 |
本章先建立 GC 的资源模型,不把“调一个参数”当作解决慢客户端或无界队列的办法。05.08 将设计受限并发和队列,05.10 才学习真实 profile/trace 的取得与解释。
二、追踪式标记清扫:队列仍引用,GC 就不能回收
Go 标准工具链的 GC 用根引用(如有效栈引用、全局引用)出发,沿对象里的指针找到仍可达的堆对象并标记它们;标记之后,未标记的对象在清扫阶段把占用空间变成可复用。05.06 的 queue→preview→64 KiB 底层数组 只要链不断,数组就可达;没有根能走到的一组互相引用对象则可能回收。这不是按“最近使用时间”决定的缓存淘汰,更不会读懂“这条消息业务上已经过期”的规则。
根:群发送任务、连接输出队列
→ 已排队待写的消息副本 (可达:必须保留)
已完成的临时编码缓冲,无根可达
→ 后续清扫后可复用其 Go 堆空间可复用 Go 堆空间与进程 RSS 立即下降仍是两回事,03.06 已讲分配器保留、映射和操作系统页面。Go GC 不是完全全程暂停的“扫完所有对象再让业务继续”:官方指南说其主要追踪工作与应用并发进行,但仍有短暂的全局暂停、调度竞争和分配时的辅助工作。具体时机与成本不能从上图计算到毫秒。
如果业务队列保留 10000 个 1 KiB 副本,这约 9.77 MiB 的纯数据仍属可达集合;降低 GOGC 或加大 GC 频率也不能回收它们。先解决队列容量、取消与慢设备策略,才有谈 GC 目标的基础。
三、分配速率、存活集与周期:先分清量纲
分配速率是每秒新分配多少字节,单位如 MiB/s;存活集是某次 GC 结束仍可达的堆数据规模,单位 MiB。一个服务可以分配很快但存活集平稳,例如不断创建短命群消息副本;也可以分配不快却长期保留很多缓冲,例如慢设备队列积压。两者会以不同方式影响 GC 的周期与扫描工作。
用 Go GC 指南的简化模型:上轮结束时存活堆为 8 MiB,需要计入目标计算的根扫描规模为 2 MiB(纸上可想作栈与全局各 1 MiB),GOGC=100。新分配额度的教学估计是 (8+2)×100%=10 MiB,目标总堆约 8+10=18 MiB。若稳定分配速率是 2 MiB/s,填满 10 MiB 新分配量需约 5 秒;若改为 10 MiB/s,约 1 秒。这只说明“同样存活集下分配更快,GC 周期可能更密”。真实标记与分配并发、目标和流量会变化,绝不能把 5 秒/1 秒当作精确触发时刻或服务承诺。
| 固定的纸上前提 | 低分配速率 | 高分配速率 |
|---|---|---|
| 存活堆 L | 8 MiB | 8 MiB |
| 根规模 R | 2 MiB | 2 MiB |
GOGC=100 的新增额度 | 10 MiB | 10 MiB |
| 新分配速率 | 2 MiB/s | 10 MiB/s |
| 填满额度的直觉时间 | 约 5 s | 约 1 s |
群成员从 10 扩到 10000,即使所有副本仍短命,也可显著增加某一批新分配的数据字节;要讨论“每秒分配速率”,还必须知道每秒这种群消息发生几次。若队列积压,部分副本又从短命变成长寿,L 也会增长;不要只拿一个峰值把两种变化混同。
四、GOGC:目标堆公式,不是 RSS 上限或消息延迟参数
官方指南的教学模型把目标写为:
目标堆 ≈ 上轮存活堆 + (上轮存活堆 + 根扫描规模) × GOGC / 100保持 L=8 MiB、根规模 R=2 MiB,得到:
GOGC | 目标前允许的新分配量 (8+2)×GOGC/100 | 目标总堆 8+新分配 | 直觉取舍 |
|---|---|---|---|
| 50 | 5 MiB | 13 MiB | 较早/较频繁回收,通常更多 GC 工作 |
| 100 | 10 MiB | 18 MiB | 中间纸上基准 |
| 200 | 20 MiB | 28 MiB | 更大堆空间,通常较少周期 |
这些是目标和趋势,不是“任何时刻 HeapAlloc 必≤13/18/28 MiB”、进程 RSS 上限或 HTTP P95。对象图与根数量若变,目标随之重算;大分配、并发标记、限制和实现细节也会使瞬时数据偏离理想模型。较低 GOGC 倾向以 CPU 工作换更低的堆目标,较高值反向;具体请求时延既受 GC 暂停,也受 CPU 竞争、网络和锁影响,不能单凭表格选“越大越快”。
最重要的反例:若慢设备队列把 9.77 MiB 副本全部留住,存活集 L 已经增长。调低 GOGC 不会令它们不再可达;需先定有限队列、拒绝/背压、断开与补拉合同,再用同口径实验决定是否调整 GC 参数。
五、并发 GC、短暂停顿与尾延迟不是同一个数字
Go GC 主要与应用并发工作,仍可能在阶段切换时短暂停止所有 goroutine,也可能消耗 CPU、让业务 goroutine 参与标记辅助,或增加写屏障成本。因此要分开三类观察:暂停时长、GC 参与 CPU、业务请求端到端时延。一个请求 P95 从 60 ms 升到 240 ms,不能凭这一点说“GC 暂停了 180 ms”;即使最近一次暂停很短,GC 的 CPU 竞争或别的等待也可能参与尾延迟。
| 视角 | 回答什么 | 不直接回答什么 |
|---|---|---|
PauseNs / GC 暂停直方图 | GC 的短暂全局暂停分布/近期记录 | 完整 GC 工作时间或每条 HTTP 请求时延 |
GCCPUFraction | 从进程启动以来 GC 使用的可用 CPU 时间比例 | 事故五分钟的准确分段 CPU 或单请求等待 |
NumGC | 已完成的 GC 周期数量 | 每次因何触发或每次请求被暂停多久 |
| 业务 P95/Trace | 请求慢在哪个阶段 | 单独证明 GC 是根因 |
PauseNs 是近期 GC 周期暂停统计的环形记录,不能把其中一个值当作“整轮 GC 全程停顿”;GCCPUFraction 是累计视角,不能不做时间窗对齐就与五分钟 P95 比大小。后续学习者可在自己版本环境里用 trace 观察可运行排队、网络等待与 GC 事件,并与 11.03 的脱敏 Trace/指标对应。本章没有任何实测,也不从短暂停顿直接推出业务 P99 安全。
六、GOMEMLIMIT 是运行时软限制,不是容器硬边界
Go 的 GOMEMLIMIT 或 runtime/debug.SetMemoryLimit 给Go 运行时管理的内存设置软限制;官方指南用 Sys−HeapReleased 近似说明范围。它包括运行时管理的堆、栈等,但不覆盖二进制映射、其他语言单独管理的内存或操作系统代程序持有的所有资源。因此它不是整个进程 RSS、节点内存或 cgroup 硬上限,不能保证“设置 100 MiB,容器永不超过 100 MiB”。
如果软限制低于业务仍必须保持可达的存活集,GC 不能变出更多可释放空间;运行时可能更频繁做 GC 并消耗 CPU,甚至在高压力下拖慢应用。它也不能替慢设备队列做丢弃、背压或取消。03.11 才深入容器配额与 OOM;本章只要求把限制对象和业务必须保留的数据分别列出。
| 调整项 | 能影响 | 不能替代 |
|---|---|---|
GOGC | GC CPU 与堆目标的取舍 | 业务队列容量、内存泄漏修复 |
GOMEMLIMIT | Go 运行时管理内存的软目标 | cgroup 硬限制、外部内存/RSS 总账 |
| 有限队列/取消/背压 | 应用保留的待办数量和生命周期 | GC 的对象扫描与 Go 运行时实现 |
七、选证据:短命分配与长期保留看不同视图
诊断虚构群扇出,先在同一时间窗记录每秒群消息数、目标数、队列长度、发送成功/失败、业务 P95,再选 Go 指标。TotalAlloc 是累计已分配字节,适合用两个采样点的差估算窗口分配量;HeapAlloc 是已分配堆对象的快照,可能包含尚未清扫的不可达对象,不能直接等同活跃业务数据。NumGC 的窗口差反映周期数,但不解释原因;PauseNs 与 GCCPUFraction 的口径如前表。系统 RSS 仍要单独观察,不能用 HeapAlloc 相加推算。
Go 官方 GC 指南把堆 profile 分为 alloc_space(累计分配字节热点)和 inuse_space(采样视角下仍在用的堆字节热点)。若怀疑“每次群扇出都创建很多很快消失的副本”,优先看分配热点;若怀疑“慢设备队列长期留住副本”,看在用对象和引用链。profile 通常采样,不是逐对象精确账本;05.10 再讲取得、扰动和对照实验,11 卷再处理容量与尾延迟。
| 竞争假设 | 应先核对 | 如果反例成立怎么办 |
|---|---|---|
| 短命副本造成高分配率 | 窗口 TotalAlloc 增量、alloc_space、群规模/速率 | 若分配并未增加,回查 CPU、锁、I/O |
| 队列积压扩大存活集 | 有界队列长度、inuse_space、GC 后堆水平 | 若存活不涨,检查分配速率和外部内存 |
| GC 影响请求尾部 | GC 事件与请求 Trace 的同窗重合、CPU/暂停 | 若慢段在网络/数据库,降级 GC 假设 |
看到“RSS 高”不应先改 GOGC;先判定是仍可达对象、暂时的分配峰值、分配器保留,还是 Go 外内存。所有优化都要记录可复现输入与版本,测试正确性和业务失败路径,不能只比较一个新数字。
八、交付群扇出 GC 评审与分层练习
学习者交付三张纸:群成员 10→10000 时每目标 1 KiB 副本的单次数据字节,分别画“短命”和“慢设备队列仍引用”两种对象图;手算 L=8 MiB、R=2 MiB 在三种 GOGC 下的目标堆;再列 2/10 MiB/s 分配率的周期直觉和至少两种可以反驳“GC 是 P95 根因”的证据。所有数字都是静态假设。
分层练习:先答,再展开反馈
1. 10 个目标各复制 1 KiB,单次纯数据多少?
10 KiB;没有计对象、协议和队列开销。
2. 10000 个目标各复制 1 KiB,约多少 MiB?
10000 KiB≈9.77 MiB,仍是纸上单次新数据量。
3. 单次分配 9.77 MiB 就等于分配速率 9.77 MiB/s 吗?
不等于。还需知道每秒这样的操作发生几次。
4. 副本很快不可达,主要可能推高哪一类压力?
新分配速率和 GC 周期频率,而非必然长期存活集。
5. 队列仍引用副本,反复 GC 能删掉它们吗?
不能。它们从队列根引用可达,业务须处理积压和所有权。
6. Go GC 标记从哪里开始?
从有效根引用出发,沿对象引用图找可达对象。
7. 无根的循环引用本身一定导致泄漏吗?
不一定。追踪式 GC 可回收从根不可达的循环对象。
8. 清扫后空出的 Go 堆空间一定立刻降低 RSS 吗?
不一定。分配器可保留供复用,物理页归还另有时机。
9. L=8 MiB、R=2 MiB、GOGC=100 时新增额度是多少?
(8+2)×100%=10 MiB。
10. 同样条件下目标总堆是多少?
8+10=18 MiB,是简化目标而非 RSS 上限。
11. GOGC=50 的目标总堆是多少?
8+(8+2)×50%=13 MiB。
12. GOGC=200 的目标总堆是多少?
8+(8+2)×200%=28 MiB。
13. 每秒分配 2 MiB、额度 10 MiB,纸上约多久填满?
约 5 秒;实际 GC 启停并非精确钟表。
14. 每秒分配 10 MiB、额度仍 10 MiB 呢?
约 1 秒,说明速率变大可能令 GC 更频繁。
15. 降低 GOGC 能让仍在慢设备队列中的副本消失吗?
不能。队列仍引用,它们属于存活集。
16. Go GC 是否全程停止所有业务 goroutine?
不是。主要工作与应用并发,但仍有短暂停顿和其他 CPU 成本。
17. 单次 `PauseNs` 等于完整 GC 周期耗时吗?
不等于。它记录暂停相关时长,不涵盖全部并发标记/业务竞争。
18. `GCCPUFraction` 可以直接读作事故五分钟 GC CPU 吗?
不能。它是进程启动以来的累计比例,需要匹配时间窗与其他证据。
19. `GOMEMLIMIT` 是容器 RSS 硬上限吗?
不是。它是 Go 运行时管理内存的软限制,范围不同。
20. 短命分配热点优先看 `alloc_space` 还是 `inuse_space`?
优先看 alloc_space;长期保留更关注 inuse_space 与引用链。
21. 堆 profile 可把每个对象都精确记录下来吗?
通常不能,profile 会采样,需要按其统计口径解释。
22. 历史查询 P95 变慢就能宣布 GC 是根因吗?
不能。需将 GC 事件与请求 Trace、CPU、锁、网络和存储等待对齐比较。
来源与下一步
- Go 官方 GC 指南:根追踪、标记清扫、分配率/存活集、
GOGC目标、并发 GC 和内存限制的教学模型。 - Go
runtime.MemStats、runtime/metrics和runtime/debug.SetMemoryLimit:指标与软限制的 API 口径。
按学习路线,下一章05.08 并发组合把有限工作池、按会话串行和扇出汇聚放回群聊处理链。离开本章前,应能把分配速率、存活集、GC CPU、暂停和 RSS分开说清,并说明哪项业务积压不是调 GC 参数能解决的。