上篇用编译信息和基准报告解释了一次分配变化。可任务服务在处理多个请求时,分配了多少、最后留下多少,以及用了多久,不能靠一个函数的 B/op 推断。本篇在同一个任务服务增加固定 HTTP handler 负载,保留 完整工程包、SHA-256和独立复跑步骤。

分配的字节不是存活堆

TestTaskGCBudget 用两个 worker、容量 1,025 的队列,提交 1,024 个相同请求。每个请求 JSON 体是 1,803 字节,文本里有 512 个单词;HTTP 适配层必须返回 202。这里使用 httptest.NewRequest 和当前服务的原 handler,没有走 TCP。收齐应答之后调用 service.Close(),等待服务拥有的 worker 退出,再逐个按 ID 核对 1,024 条记录均为 SUCCEEDED、Words=512。如果漏了任务,只有速度的数字就不能拿来讨论 GC。202 是入队应答,逐 ID 的终态检查才是这次业务完成的证据。

负载开始前后,测试从 runtime/metrics 读累计分配字节和自动 GC 轮次;分配字节除以从开始提交到 Close 返回的时间,得到这次测试期间的 MiB/s 分配速率。同一个测试在关闭之后读堆对象占用,随后显式调用 runtime.GC(),保持服务对象可达,再读一次。前一次堆值可能包含尚未回收的对象;后一次也含测试和运行时自己仍存活的对象,都不是单一服务的精确驻留量。POST p50/p95 从每次 handler 调用的墙钟延迟排序得到;completionWall 则包含提交与等待 worker 完成,不能将这两个时间混成“每个任务的处理延迟”。

GOGC 控制触发 GC 的目标比例,GOMEMLIMIT 是 runtime 管理内存的软上限。本章让后者在两组均为 64MiB,只改变 GOGC=50 和 GOGC=200。固定 Go 1.27.1 的 runtime/debug.SetMemoryLimit 文档要求观察 /memory/classes/total:bytes - /memory/classes/heap/released:bytes 才能对应软上限;测试正按这两个指标取差。进程 RSS 还包含映射、内核等账目,与它不是同一个数。这个上限在本次负载下没有被顶到,不能用数据说它触发了限制;更不能当硬性的进程内存封顶。

顺序交换的十轮观测

每组先运行五轮,再交换两种配置的运行顺序,各加五轮;完整命令、标准输出、标准错误与退出码保存在 writing-plans/go-language/evidence/33/,不是从某一轮挑最好数字。

指标,单轮范围 GOGC=50(10 轮) GOGC=200(10 轮)
负载段自动 GC 轮次 12–13 2
累计分配字节增量 约 14.4–14.7 MB 约 14.4–14.7 MB
完成后强制 GC 的堆对象字节 约 0.60–0.66 MB 约 0.60–0.66 MB
本版 runtime 管理内存近似 约 8 MB 约 14–15 MB

同样的任务数据并没有因 GOGC 变小就凭空不分配;改变的是回收节奏和这组采样点上的堆账。日志还保存每轮强制 GC 之前的堆、以 MiB/s 表示的分配速率、完成墙钟和 POST p50/p95。GOGC=50 最慢轮 completionWall 为 172ms,而倒序运行的一轮约 60ms;GOGC=200 的多轮也存在抖动。虽然这组负载中 50 的 GC 次数显著更多,仍不能用一轮的 p95 推出在任何机器上将 GOGC 改大就能加速业务,或以这个实验推断单次停顿时间。分配速率的分母包含 GC 和调度时间,速度慢时即使分配字节差不多,算出的每秒速率也会下降;它不是另一个独立的“分配更省”信号。

练习:① 14MB 分配、约 0.6MB 强制回收后堆,是否说明丢了 13MB 的任务数据?不能,累计分配是流量,完成后的记录只留词数等状态;需要逐 ID 的业务断言排除任务丢失。② 观测到 runtime 管理内存 15MB,能写进文档为“进程 RSS 15MB”吗?不能,账目定义不同,GOMEMLIMIT 也只是 runtime 软限制。③ 调整 GC 配置后 POST p95 降了,能立刻改生产默认值吗?先固定真实负载与并发,分清入队应答和任务完成,重复测量并观察负载外的内存与延迟;当前只有本机合成 handler 路径。

同一次章节验收还用真实 PostgreSQL 16.15 跑了既有 SQL 回归、两包测试、实际路径 -race、旧 CLI 正反;它们不意味着这个 GC 目标也经过 SQL 或 TCP。race 无报告不覆盖未执行路径,数据库提交也不能替代外部副作用确认。远程网络、内存限额真正压满、其他平台、含 33 的全站构建与发布仍 NOT_RUN。下一篇在 CPU、channel 与 I/O 三种等待下看 goroutine 的调度状态。

参考资料