bigywater.tech
Go · 51

GOMEMLIMIT:软内存上限怎么改变 GC 的节奏

GOGC 只看存活堆的倍数,不知道你的容器还剩多少内存。GOMEMLIMIT 给运行时一个软上限:接近上限时 GC 会更频繁,甚至在 GOGC=off 时也生效。这篇用同一个分配负载在五种设置下对比内存、GC 次数与吞吐,并验证它为什么是“软”的。

299 MB
GOMEMLIMIT=300MiB 时总内存的峰值(不设限制时是 609 MB)
109.6 → 44.7 M/s
用限制换来的内存上界,代价是吞吐下降
273 MB
限制 150MiB(低于存活堆)时的峰值——软上限不会杀进程,也不保证不超

限制怎么起作用

实测 · 本机执行输出
  A 不设限制,GOGC=100(默认):
    cycles=56 ops_per_s=109.6M peak_total_MB=609 maxrss_MB=605 gc_cpu_pct=28.5 limiter_last_cycle=0 goal_MB=562
    (stderr)SetMemoryLimit(-1) 读到当前限制:9223372036854775807
  B GOMEMLIMIT=300MiB,GOGC=100:
    cycles=119 ops_per_s=44.7M peak_total_MB=299 maxrss_MB=293 gc_cpu_pct=51.9 limiter_last_cycle=0 goal_MB=268
  C GOMEMLIMIT=300MiB,GOGC=off:
    cycles=121 ops_per_s=45.5M peak_total_MB=299 maxrss_MB=293 gc_cpu_pct=53.4 limiter_last_cycle=0 goal_MB=268
  D GOMEMLIMIT=240MiB,GOGC=off(只比存活堆多一点):
    cycles=152 ops_per_s=11.2M peak_total_MB=245 maxrss_MB=241 gc_cpu_pct=65.2 limiter_last_cycle=0 goal_MB=213
  E GOMEMLIMIT=150MiB,GOGC=100(低于存活堆):
    cycles=129 ops_per_s=19.9M peak_total_MB=273 maxrss_MB=267 gc_cpu_pct=57.6 limiter_last_cycle=0 goal_MB=222
    (stderr)SetMemoryLimit(-1) 读到当前限制:157286400

文档(go doc runtime/debug.SetMemoryLimit)说限制针对 MemStats.Sys - HeapReleased(即 /memory/classes/total 减去 /memory/classes/heap/released),不包括二进制本身与 C 分配的内存。实验里限制 300MiB 把峰值压在 299 MB,GC 比不设限制多出一倍(119 轮对 56 轮),GC 的 CPU 占比从 28.5% 升到约 52%,吞吐掉到 41%。GOGC=off 加限制得到几乎相同的结果,说明限制自己就能驱动 GC。

写法与默认值

实测 · 本机执行输出
  GOMEMLIMIT=512MiB     → SetMemoryLimit(-1) 读到当前限制:536870912
  GOMEMLIMIT=1GiB       → SetMemoryLimit(-1) 读到当前限制:1073741824
  GOMEMLIMIT=1.5GiB     → fatal error: malformed GOMEMLIMIT; see `go doc runtime/debug.SetMemoryLimit`
  GOMEMLIMIT=100MB      → fatal error: malformed GOMEMLIMIT; see `go doc runtime/debug.SetMemoryLimit`
  GOMEMLIMIT=123456789  → SetMemoryLimit(-1) 读到当前限制:123456789
  GOMEMLIMIT=off        → SetMemoryLimit(-1) 读到当前限制:9223372036854775807
  不设置 → SetMemoryLimit(-1) 读到当前限制:9223372036854775807(math.MaxInt64 = 9223372036854775807,即没有限制)

默认没有限制(math.MaxInt64)。GOMEMLIMIT 只接受整数加可选的 B、KiB、MiB、GiB、TiB 后缀,1.5GiB 或 100MB 会让程序启动失败。要用小数就换成更小的单位(1536MiB)。debug.SetMemoryLimit(-1) 可以读当前值。

“软”的含义

实测 · 本机执行输出
  A 不设限制,GOGC=100(默认):cycles=56 ops_per_s=109.6M peak_total_MB=609 maxrss_MB=605 gc_cpu_pct=28.5 limiter_last_cycle=0 goal_MB=562
  E GOMEMLIMIT=150MiB,GOGC=100(低于存活堆):cycles=129 ops_per_s=19.9M peak_total_MB=273 maxrss_MB=267 gc_cpu_pct=57.6 limiter_last_cycle=0 goal_MB=222

限制低于存活堆时,程序不会被杀,也不保证内存不超:实验里 273 MB 超过了 157 MB 的限制,吞吐降到 18%,GC 轮数翻倍。文档的说法是这时 GC 可能“almost continuously”运行但应用仍能前进。实验里 /gc/limiter/last-enabled 读到 0,GC 占 CPU 57.6%,我没有观察到 GC 的 CPU 被限制在某个上限的证据(运行时是否有这样的限流,这次没有验证)。实践上的结论:限制应当设在存活堆之上留足余量的位置,而不是接近或低于它。

分步演示

GOMEMLIMIT
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。存活堆是 N 个 56 字节节点组成的图(runtime.GC() 之后约 200 MiB);4 个 goroutine 不停地 new 小对象并丢弃;总内存用 /memory/classes/total:bytes 每 3 ms 取样的峰值,GC CPU 占比来自 /cpu/classes/gc/total:cpu-seconds 与 /cpu/classes/total:cpu-seconds 的差值;maxrss 来自 getrusage;每种设置只跑一次
    • 没有验证:GC 限流器是否触发(指标读到 0)、cgroup 容器里用 GOMEMLIMIT 的推荐比例(只是常见做法)、归还内存给操作系统(scavenger)的行为、amd64 上的数字、与 GOGC 同时设置的更复杂组合、多次重复运行的离散度
    京ICP备2026067502号-1京公网安备11010502063487号