go服务内存管理

https://mp.weixin.qq.com/s/xe8KXD39YlJdDG4cLT0veA
https://segmentfault.com/a/1190000022472459?utm_source=tag-newest



为什么 go 1.12 会导致内存异常上涨呢?



查查 Go 1.12 Release Notes,可以找到一点线索:



Runtime
Go 1.12 significantly improves the performance of sweeping when a large fraction of the heap remains live. This reduces allocation latency immediately following a garbage collection.
(中间省略2段不太相关的内容)
On Linux, the runtime now uses MADV_FREE to release unused memory. This is more efficient but may result in higher reported RSS. The kernel will reclaim the unused data when it is needed.



golang.org/doc/go1.12



翻译一下:



在堆内存大部分活跃的情况下,go 1.12 可以显著提高清理性能,降低 [紧随某次gc的内存分配] 的延迟。
在Linux上,Go Runtime现在使用 MADV_FREE 来释放未使用的内存。这样效率更高,但是可能导致更高的 RSS;内核会在需要时回收这些内存。

内存分配
在Linux下,malloc 需要在其管理的内存不够用时,调用 brk 或 mmap 系统调用(syscall)找内核扩充其可用地址空间,这些地址空间对应前述的堆内存(heap)。



注意,是“扩充地址空间”:因为有些地址空间可能不会立即用到,甚至可能永远不会用到,为了提高效率,内核并不会立刻给进程分配这些内存,而只是在进程的页表中做好标记(可用、但未分配)。



注:OS用页表来管理进程的地址空间,其中记录了页的状态、对应的物理页地址等信息;一页通常是 4KB。



当进程读/写尚未分配的页面时,会触发一个缺页中断(page fault),这时内核才会分配页面,在页表中标记为已分配,然后再恢复进程的执行(在进程看来似乎什么都没发生)。



注:类似的策略还用在很多其他地方,包括被swap到磁盘的页面(“虚拟内存”),以及 fork 后的 cow 机制。



内存回收
当我们不用内存时,调用 free(ptr) 释放内存。



对应的,当 free 觉得有必要的时候,会调用 sbrk 或 munmap 缩小地址空间:这是针对一整段地址空间都空出来的情况。



但更多的时候,free 可能只释放了其中一部分内容(例如连续的 ABCDE 5个页面中只释放了C和D),并不需要(也不能)把地址空间缩小



这时最简单的策略是:什么也不干。



但这种占着茅坑不拉屎的行为,会导致内核无法将空闲页面分配给其他进程。



所以 free 可以通过 madvise 告诉内存“这一段我不用了”。



madvise
通过 madvise(addr, length, advise) 这个系统调用,告诉内核可以如何处理从 addr 开始的 length 字节。



在 Linux Kernel 4.5 之前,只支持 MADV_DONTNEED(上面提到 go 1.11 及以前的默认advise),内核会在进程的页表中将这些页标记为“未分配”,从而进程的 RSS 就会变小。OS后续可以将对应的物理页分配给其他进程。



注:RSS 是 Resident Set Size(常驻内存集)的缩写,是进程在物理内存中实际占用的内存大小(也就是页表中实际分配、且未被换出到swap的内存页总大小)。我们在 ps 命令中会看到它,在 top 命令里对应的是 REZ(man top有更多惊喜)。



被 madvise 标记的这段地址空间,该进程仍然可以访问(不会segment fault),但是当读/写其中某一页时(例如malloc分配新的内存,或 Go 创建新的对象),内核会 重新分配 一个 用全0填充 的新页面。



如果进程大量读写这段地址空间(即 release notes 说的 “a large fraction of the heap remains live”,堆空间大部分活跃),内核需要频繁分配页面、并且将页面内容清零,这会导致分配的延迟变高。



go 1.12 的改进
从 kernel 4.5 开始,Linux 支持了 MADV_FREE (go 1.12 默认使用的advise),内核只会在页表中将这些进程页面标记为可回收,在需要的时候才回收这些页面。



如果赶在内核回收前,进程读写了这段空间,就可以继续使用原页面,相比 DONTNEED 模式,减少了重新分配内存、数据清零所需的时间,这对应 Release Notes 里写的 “reduces allocation latency immediately following a garbage collection”,因为在 gc 以后立即分配内存,对应的页面大概率还没有被 OS 回收。



但其代价是 “may result in higher reported RSS”,由于页面没有被OS回收,仍被计入进程的 RSS ,因此看起来进程的内存占用会比较大。



差不多就解释到这里吧,建议再重读一遍:



在堆内存大部分活跃的情况下,go 1.12 可以显著提高清理性能,降低 [紧随某次gc的内存分配] 的延迟。
在Linux上,Go Runtime现在使用 MADV_FREE 来释放未使用的内存。这样效率更高,但是可能导致更高的 RSS;内核会在需要时回收这些内存。



如果仍然有不理解的地方,可以留言探讨。



对更多细节感兴趣的同学,推荐阅读《What Every Programmer Should Know About Memory》(TL; DR),或者它的精简版《What a C programmer should know about memory》(文末参考链接)。



Category golang