从 Go 1.12 开始就不断有人踩到监控误报的坑,原因是 Go 从 1.12 开始将 madvise
系统调用
使用的内存回收策略从 MADV_DONTNEED
改为了 MADV_FREE
。
从可查的一些文档来看,RSS 作为最常用的内存监控指标,不会反映进程中未被操作系统回收的那部分内存。
自然就会有一些说法建议将 RSS 更换为可能更妥当的指标,比如 PSS 甚至 USS。
这就导致了一些比较 tricky 的问题,PSS 和 USS 并不如 RSS 常用,文档上也没有更多的说明它们
实际能够反应的内存消耗状况,它们真的比 RSS 更合适吗?
RSS/PSS/USS 是什么
为了把问题说明清楚,总是需要解释是什么的问题。这个问题总是能搜出来一大堆的反复被复制的解释:
|
|
从这些描述上来看,给人的总体印象是 USS 比 PSS 更好,PSS 比 RSS 更好,VSS 基本上不能用: 因为 VSS 反应了当前进程申请且未归还的虚拟地址空间,RSS 包含了所谓共享库,PSS 将共享库的大小按 共享进程的比例进行了均摊,而 USS 直接没计算共享库的内存。
从这定义来看,无论是 RSS、PSS 还是 USS,它们的区别都只在共享库上,但对于像 Go 这种静态链接 来的程序,共享库并不那么常见。一个合理的怀疑就是大部分情况下:RSS == PSS == USS。
MADV_DONTNEED
vs MADV_FREE
内存消耗这类直接跟内核挂钩的功能,一个优秀的内核自然会将这些信息记录在某处供查阅。
以 Linux 为例,RSS 通常会放在 /proc/[pid]/status
中,而当运行应用想要查询自己的
消耗行为时,甚至可以用 /prof/self/status
来直接读取自己的消耗状态,比如 cat
它自己:
|
|
每个变量代表的含义可以通过 man page 查到 man proc
,比如 VmRSS
就指代了 RSS 的值,
而 VmSize 就是 VSS 的值,等等。当然,在 /proc/[pid]/status
中的内容是美化过的,
真正做程序化的话可以直接从更简洁的 /proc/[pid]/stat
统计文件中拿到这些信息。
还是以 RSS 为例:
|
|
对于 Linux 上进行内存管理的系统调用来说,mmap 加 PROT_READ
和 PROT_WRITE
得来的内存会发生缺页错误,但无论如何最终操作系统都会将这部分内存真正的分配出去给进程使用。
而使用 madvise
的 MADV_DONTNEED
策略来进行释放,和使用 MADV_FREE
两者的区别
就可以直接通过上面的 rss()
方法来度量。例如:
|
|
假设申请 10M,则可以看到这样的结果:
|
|
区别很明显,在 MADV_FREE
结束后,RSS 没有减少,而 MADV_DONTNEED
策略则全部归还。
PSS/USS vs RSS
那怎么才能拿到 PSS/USS 的值呢?更为详细的内存映射信息其实被进一步的记录在了
/proc/[pid]/smaps
中,但计算上比较麻烦,因为它是按照不同的 mmap 操作进行记录的。
但这并不妨碍我们将这个获取过程自动化:
|
|
最终可以这样使用:
|
|
嗯,把它用到前面的程序中去,表现为:
|
|
是的,没有区别。噢那要监控怎么办呢?三种手段:
GODEBUG=madvdontneed=1
,针对 1.12 至 1.16 之间的发行版runtime.ReadMemStats
定期读取上报。或者使用 expvar,又或者标准的 pprof 手段,只不过每一种方式对运行时性能都是大打折扣,因为这些查询是需要 STW 的- 升级到 Go 1.16
当然,其实还有第四种手段,那就是:不监控。
进一步阅读的相关文档
- https://man7.org/linux/man-pages/man2/mmap.2.html
- https://man7.org/linux/man-pages/man2/madvise.2.html
- https://man7.org/linux/man-pages/man2/mincore.2.html
- https://man7.org/linux/man-pages/man5/procfs.5.html
- https://unix.stackexchange.com/questions/33381/getting-information-about-a-process-memory-usage-from-proc-pid-smaps
- https://golang.org/pkg/expvar/
- https://golang.org/pkg/runtime/#MemStats
- https://golang.org/pkg/net/http/pprof/
如果对 Linux 系统调用比较了解的话,可能我们还会想到用 mincore 系统调用来检查页的缺页状态, 这虽然是一种方法但不适合 Go,因为用户代码并不知道进程消耗的地址,更查不到页。 即便能查到,但也成本极高。尽管如此,整的想查还是可以做,但前提是查询那些自己通过 mmap 申请来的内存:
|
|