2124 字
约 7 分钟
13
S09-A22 · Go 的 RSS 不降:`GOGC` 之外的那几笔账
2024-12-31
2026-11-10

一个 Go 服务把内存从 2G 降到 800M,而 RSS 始终显示 1.8G。runtime.MemStats 里 HeapAlloc 只有 500M,HeapReleased 也在涨——于是"进程占用的内存"与"堆里的对象"之间差了一倍。

Go 的内存分三层:堆对象(HeapAlloc)、堆保留(HeapSys)、进程地址空间(RSS)。而"RSS 不降"的常见原因,这个"不是泄漏"的判断很重要,因为"追泄漏"的方向会带偏:你以为要找"谁没释放",而实际问题在"运行时什么时候把闲置内存还给 OS"——两者的修法完全不同。
是"归还与复用"的时机问题,不是"泄漏"。


1. 三个内存口径

口径 含义
HeapAlloc 存活对象占用
HeapIdle 已归还给"运行时"但未归还 OS
HeapReleased 已归还 OS
RSS 进程实际占用(含栈、代码、cgo)

第二项是关键:**Go 运行时"保留"内存用于"下次分配复用"**第二项的"动机"是"性能":向 OS 申请/归还内存要系统调用,而"保留"能让"下一次分配"免掉这一步——于是"保留"是"用内存换 CPU"的默认策略。
,于是"对象死了"不等于"内存还给了 OS"。

2. "RSS 不降"的四种原因

原因 判据
内存未归还(Go 保留) HeapIdle 高、HeapReleased 低
归还节奏慢 需要"两次 GC 或定时归还"
栈与 cgo 占用 StackSys/GCCPUFraction
内存碎片/大对象 HeapInuse 与 HeapIdle 比例

第二项还有个"步调"细节:归还由后台清扫者"按步"做,于是"刚从峰值下来"的那几秒,RSS 仍然很高——它会被"按分钟看"的监控放大。
第二项最常被忽略:Go 的"归还"不是"GC 之后立刻",而是"由后台清扫者按步调进行",且在"内存用量低"时才积极归还。

// 四个关键指标(一眼看出"账在哪")
var m runtime.MemStats
runtime.ReadMemStats(&m)
// HeapAlloc   存活对象
// HeapIdle    已空闲、未还 OS(RSS 的主要来源)
// HeapReleased 已还 OS
// HeapSys     向 OS 申请的总量
// 判据:HeapIdle 大 + HeapReleased 小 → "Go 保留着没还"

3. 三种"让 Go 还回去"的手段

手段 效果
debug.FreeOSMemory() 立刻归还(有停顿代价)
GODEBUG=madvdontneed=1 改"归还方式"(更积极)
调 GOGC/GOMEMLIMIT 间接影响"何时归还"

第一项是"手动强制":它触发一次 GC + 归还第一项的"适用"很窄:**"空闲时段"或"任务结束后"**调一次可以;"每次请求后"调会让"停顿"变成"常态延迟"。
,代价是"一次停顿"——适合"低频维护任务",不适合"每个请求"。

第三项的"差别"在于"目标":GOGC 调的是"增长倍数",GOMEMLIMIT 调的是"总量红线"——而"红线"才是容器里真正会触发 OOM 的东西。
第三项是"现代做法":GOMEMLIMIT 告诉运行时"总内存上限",于是"接近上限时"它会"更积极归还"——这比"调 GOGC"更贴近"容器配额"。

// 三种配置(按场景选)
GOGC=100                          # 默认:堆翻倍就 GC
GOMEMLIMIT=1500MiB                # 按容器配额设(推荐,Go 1.19+)
GODEBUG=madvdontneed=1            # 让"归还"用 MADV_DONTNEED(更积极)

4. "泄漏"与"不归还"的区分

指标组合 结论
HeapAlloc 持续涨 可能泄漏
HeapAlloc 稳定、RSS 涨 不归还/碎片/栈
Goroutine 数持续涨 泄漏(goroutine 持有对象)
HeapObjects 数持续涨 泄漏

第二项是"最常被误判为泄漏"的组合:对象数稳定、堆稳定,而 RSS 不降——它通常"不是 bug",而是"运行时的保留策略"。

第三项是最常见的"真泄漏":goroutine 卡住(等 channel/锁)会持有它引用的所有对象第三项的"排查"有现成工具:pprof goroutine 看"哪个栈的 goroutine 最多"——若"同一处卡了几千个",泄漏点就找到了。
——于是"堆涨"其实是"goroutine 涨"。

// 三分钟的"是不是泄漏"判定
1 持续请求 10 分钟,看 HeapAlloc 是否单调上升(且 GC 后不回落)
2 看 goroutine 数与 HeapObjects 数是否同步上升
3 用 pprof heap(inuse_space)看"谁占着"
// 结论:1 不涨 → 不是泄漏;2 同步涨 → goroutine 泄漏优先

"内存账"可以用一个"三行 dump"贴进故障单,避免"看错口径":

// 三行 dump(HeapAlloc / HeapIdle / RSS)+ goroutine 数
HeapAlloc=512MiB HeapIdle=1100MiB HeapReleased=120MiB HeapSys=1700MiB
RSS=1.8GiB goroutine=48
// 读法:HeapIdle 大(1100MiB)说明"Go 保留着没还";
//      goroutine 只有 48 → 不是 goroutine 泄漏;HeapAlloc 稳定 → 不是对象泄漏

5. "归还"的三个误区

误区 实际
"GC 之后 RSS 就该降" 不一定(保留复用)
"调小 GOGC 就会省内存" 会更频繁 GC,RSS 未必降
"FreeOSMemory 可以每次都调" 停顿代价高

第二项的"原因"是"保留量由"峰值"决定":GC 更频繁只是"更早回收垃圾",而"曾经用到过的峰值"仍被保留——除非"主动归还"。
第二项很反直觉:GOGC 小 → GC 更频繁 → CPU 上升,而 RSS 可能不变(因为"保留量"由"峰值"决定)。

"强制归还"要"按条件触发",而不是"定时无脑调":

// 仅在"堆空闲占比高且长时间无请求"时归还
if idleRatio > 0.4 && time.Since(lastReq) > 30*time.Second {
    debug.FreeOSMemory()      // 一次 GC + 归还;代价是一次停顿
}
// 反例:每次请求后调用 → 停顿变成常态延迟,吞吐下降

6. 排错:五种"内存问题"

现象 成因 处理方向
RSS 不降但 Heap 稳定 保留/碎片 设 GOMEMLIMIT
容器 OOM 但 Heap 不高 栈/cgo/碎片 看 HeapInuse/Idle
内存单调上升 泄漏 pprof + goroutine 数
GC 频繁但内存不降 GOGC 太小 改 GOMEMLIMIT
峰值后 RSS 一直高 峰值决定保留 接受或强制归还

第五项是"容量规划"的现实:RSS 的上界由"历史峰值"决定第五项的"缓解"是"把峰值降下来":"某次大查询"用了 1.5G,之后 RSS 就按 1.5G 保留——所以"限制单次查询的内存"比"事后归还"更根本。
,于是"某次大查询"会让"之后一直占着"——除非"主动归还"或"重启"。

最后一个"观测"建议:把 HeapIdle 与 RSS 画在同一张图上,而不是只看 RSS。

// 两张图能讲的三个故事
RSS 涨、HeapIdle 涨 → 保留(不是泄漏)
RSS 涨、HeapAlloc 涨 → 泄漏(去 pprof)
RSS 稳定、HeapAlloc 涨 → 分配快、回收也快(GC 在跑,正常)
// 判据:单看 RSS 无法区分这三种,必须配一个堆内指标

7. 边界:什么情况不必追 RSS

场景 为什么 做法
容器内存充足(余量 > 50%) 不影响稳定 不追
短生命周期进程(批处理) 退出即释放 不追
RSS 高但 P99 稳定 无实际影响 不追
已设 GOMEMLIMIT 且未 OOM 运行时在管 观察即可

最后一条与"容器"有关的边界:容器的 memory limit 是"硬上限",而 Go 在 1.19 前"不知道"这个上限——于是"堆按机器内存算"、"容器按配额杀",这就是"GOMEMLIMIT 存在的原因"。
判断口径一句话:"RSS 高会不会导致 OOM 或影响 P99"。会(容器配额紧),就要"设 GOMEMLIMIT + 找峰值来源";不会(余量充足),它只是"运行时保留",不必当成 bug。


8. 自测

  1. 三个内存口径分别是什么?为什么"对象死了"不等于"内存还给 OS"?
  2. 四种"RSS 不降"的原因里,哪一种是"运行时保留"?它用什么指标判断?
  3. GOMEMLIMIT 与 GOGC 的区别是什么?为什么前者更贴近容器?
  4. 怎么区分"泄漏"与"不归还"?哪两项指标同步上涨指向"goroutine 泄漏"?
  5. 为什么"调小 GOGC 未必省内存"?
  6. 判断"是否该追 RSS"的那个问题是什么?
S09-A22 · Go 的 RSS 不降:`GOGC` 之外的那几笔账
https://www.clxhxhhr.top/posts/6612/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。