The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
felix021

踩坑记: go 服务内存暴涨

  felix021 ·
felix021 · Apr 26, 2020 · 24548 views
This topic created in 2292 days ago, the information mentioned may be changed or developed.

我发现最近每篇文章都是 收藏数 < 回复数,希望各位可以多讨论,我觉得有时提出问题比解决问题更重要~


这周换换口味,记录一下去年踩的一个大坑。


大概是去年 8 月份,那会儿我们还在用着 64GB 的“小内存”机器。

由于升级一次版本需要较长的时间( 1~2 小时),因此我们每天只发一次车,由值班的同学负责,发布所有已 merge 的 commit 。

当天负责值班的我正开着车,突然收到 Bytedance-System 的夺命连环 call,打开 Lark 一看:

[ 规则 ]:机器资源报警 [ 报警上下文 ]:  host: 10.x.x.x 内存使用率: 0.944 [ 报警方式 ]:电话&Lark

打开 ganglia 一看,更令人害怕:好多机器的内存都在暴涨

memory.jpg

吓得我都瘫了


这看起来像是典型的内存泄漏 case,那就按正常套路排查:

一方面,通知车上的同学 review 自己的 commit,看看是否有代码疑似内存泄漏,或者新增大量内存占用的逻辑;

另一方面,我们的 go 服务都默认开启了 pprof,于是找了一台机器恢复到原版本,用来对比内存占用情况:

$ go tool pprof http://$IP:$PORT/debug/pprof/heap 
(pprof) top 10
Showing top 10 nodes out of 125
      flat  flat%   sum%        cum   cum%
 2925.01MB 17.93% 17.93%  3262.03MB 19.99%  **[此处打码]**
 2384.37MB 14.61% 32.54%  4817.78MB 29.52%  **[此处打码]**
 2142.40MB 13.13% 45.67%  2142.40MB 13.13%  **[此处打码]**
 ...

就这样,一顿操作猛如虎,~~涨跌全靠 Te 朗普~~,最终结果是,一方面没看出啥问题,另一方面也没看出啥问题。

我也很绝望.jpg

正在一筹莫展、准备回滚之际,内存它自己稳了:

虽然占用率仍然很高,但是没有继续上升,也没有出现 OOM 的情况

难道我上了灵车?


排查过程中,我们还发现一个现象:并不是所有机器的内存都涨。

memory2.jpg

这些机器的硬件都是一致的,但是用 uname -a 可以看到,内存异常的机器版本是 4.14 ,比内存正常机器的 3.16 高很多:

<异常机器>$ uname -a #
Linux 4.14.81.xxx ...
<正常机器>$ uname -a
`Linux 3.16.104.xxx ...`

说明两个 kernel 版本的某些差别是原因之一,但并不足以解释前述问题:毕竟发车之前也是这些机器。

此外,Y 同学提到,他把编译服务指定的 go 版本从 1.10 升级到了 1.12 。

当时 go 1.12 已经发布半年,Y 同学在开发环境编译和运行正常,在线上灰度机器也运行了一段时间,看着没毛病,所以就决定升级了。

既然其他可能性都排查过了,那就先降回来看看吧。

我们用 go 1.10 重新编译了 master,发布到几台内存异常的机器上。

于是问题解决了。

taiguan.jpg

什么鬼?


为什么 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;内核会在需要时回收这些内存。

这两段话每个字都认识,合到一起就……

当个垃圾感觉挺好的

不过都写到这了,我还是试着解释下,借用 C 语言的 malloc 和 free ( Go 的内存分配逻辑也类似):

  • 内存分配

在 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 》(文末参考链接)。


转²

至此前述内存暴涨问题也算是收尾了,但 Y 同学仍然有点不放心:是不是有可能某个 bug 在 Go 1.12 才会出现、导致内存泄漏?

这问题有点轴,但是好像很有道理,毕竟前面那么一大段,光说不练,像假把式。

再说一遍?

但这要如何才能实锤呢?

前面提到 go 1.12 用 MADV_FREE,内核会在需要的时候才回收这些页面。

如果我们能想办法让内核觉得需要、去回收这些可回收的页面,就能实锤了。

熟悉虚拟化(如 xen 、kvm )的同学,可能会觉得这个问题很眼熟:如果宿主机(准确地说是 hypervisor )能够回收客户机不再使用的内存,那就可以 ~~超卖更多 VPS 赚更多钱~~ 大幅提高内存的利用率。

他们是怎么做的呢?

xen 的解决方案是:在客户机里植入一段程序,其主要工作是申请新的内存。能被它申请到的内存,就是客户机可以不用的内存(当然也不能申请得太过分,否则会导致客户机使用 swap,或其他进程 OOM )。然后宿主机就可以放心将这些内存对应的物理页挪作他用了。

这个过程就像在吹气球,把客户机里能占用的空间都占住。

所以这段程序的名字叫做:balloon driver 。

那么实锤方案就呼之欲出了:

如果我们也弄个不断膨胀的气球(申请内存),内核就会觉得需要去找其他进程回收那些被 FREE 标记的内存。

说干就干:

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main() {
    char *p = NULL;
    const int MB = 1024 * 1024;
    while (1) {
        p = malloc(100 * MB);
        memset(p, 0, 100 * MB);
        sleep(1);
    }
    return 0;
}

(注意 memset,否则内存不会实际分配)

效果如下:

3261164 xxx     20   0 52.375g 0.035t  36524 S 579.1 66.0  16711:10 [打码]       
2321665 xxx     20   0 16.358g 0.016t 569088 S  38.4  1.5   1128:45 ./test  

可以看到,虽然打码进程的 VIRT (地址空间大小)还是 52G,但是实际占用的内存已经下降到 35G,气球生效了。

深藏功与名


合²

简单汇总一下前面的内容:

  1. Go 1.12 升级能降低内存分配的延迟,但会导致进程 RSS 变高
  2. 因为 Go 1.12 用 MADV_FREE,会让内核延迟回收内存
  3. 通过在页表中做标记的方式,延迟内存的分配和回收,可以提高内存管理的效率
  4. 可以通过气球来让 OS/Hypervisor 挤占内存,另作他用

顺便一提,本文涉及的部分知识点,我在面试时偶尔会问到。

有些候选人就觉得我是在刁难他,反问我:

“你问的这些,工作中都用得到吗?”

天绝地灭般的笑声

在字节跳动真用得上,不信你来试试?

~ 投递链接 ~

网盟广告(穿山甲)-后端开发(上海) https://job.toutiao.com/s/sBAvKe

网盟广告(穿山甲)-后端开发(北京) https://job.toutiao.com/s/sBMyxk

其他地区、其他职能线 https://job.toutiao.com/s/sB9Jqk

关于字节跳动面试的详情,可参考我之前写的:

程序员面试指北:面试官视角

参考链接:

[1] Go 1.12 关于内存释放的一个改进 https://ms2008.github.io/2019/06/30/golang-madvfree/

[2] What a C programmer should know about memory
https://marek.vavrusa.com/memory/

[3] tcmalloc2.1 浅析 https://wertherzhang.com/tcmalloc2.1%E6%B5%85%E6%9E%90/


欢迎关注我的公众号

微信扫码

   ▄▄▄▄▄▄▄   ▄      ▄▄▄▄ ▄▄▄▄▄▄▄  
   █ ▄▄▄ █ ▄▀ ▄ ▀██▄ ▀█▄ █ ▄▄▄ █  
   █ ███ █  █  █  █▀▀▀█▀ █ ███ █  
   █▄▄▄▄▄█ ▄ █▀█ █▀█ ▄▀█ █▄▄▄▄▄█  
   ▄▄▄ ▄▄▄▄█  ▀▄█▀▀▀█ ▄█▄▄   ▄    
   ▄█▄▄▄▄▄▀▄▀▄██   ▀ ▄  █▀▄▄▀▄▄█  
   █ █▀▄▀▄▄▀▀█▄▀█▄▀█████▀█▀▀█ █▄  
    ▀▀  █▄██▄█▀  █ ▀█▀ ▀█▀ ▄▀▀▄█  
   █▀ ▀ ▄▄▄▄▄▄▀▄██  █ ▄████▀▀ █▄  
   ▄▀▄▄▄ ▄ ▀▀▄████▀█▀  ▀ █▄▄▄▀▄█  
   ▄▀▀██▄▄  █▀▄▀█▀▀ █▀ ▄▄▄██▀ ▀   
   ▄▄▄▄▄▄▄ █ █▀ ▀▀   ▄██ ▄ █▄▀██  
   █ ▄▄▄ █ █▄ ▀▄▀ ▀██  █▄▄▄█▄  ▀  
   █ ███ █ ▄ ███▀▀▀█▄ █▀▄ ██▄ ▀█  
   █▄▄▄▄▄█ ██ ▄█▀█  █ ▀██▄▄▄  █▄  

Supplement 1  ·  Apr 26, 2020
补充:

对于大部分应用场景,其实 Go 1.12 的这个“改进”,并不会导致内存保障引起监控报警。

实在是因为,我们的场景比较特殊,对内存的消耗很大。

那既想用 Go 1.12+,又不想触发报警怎么办呢?

Release Notes 里给了解决方案:

设置一个环节变量就好了

```
To revert to the Go 1.11 behavior (MADV_DONTNEED), set the environment variable GODEBUG=madvdontneed=1.
```
154 replies    2020-04-29 16:09:03 +08:00
1  2  
hallDrawnel
    101
hallDrawnel  
   Apr 27, 2020
我看有字节的同学已经用 Grafana 呀
rrfeng
    102
rrfeng  
   Apr 27, 2020
直接回退到旧版内存管理不是一劳永逸的办法。还是需要再想一下更好的办法。

例如:为什么程序要用这么多内存?是重复使用吗?比如系统内存给足,看看到底最终占用多少?然后分析程序内的内存分配行为是不是有不合理的情况?
felix021
    103
felix021  
OP
   Apr 27, 2020
@hallDrawnel @ms2008 旧的 Ganglia 、新的 openfalcon+grafana,我们都在用,看场景和习惯
felix021
    104
felix021  
OP
   Apr 27, 2020
@rrfeng 嗯,老架构,业务快速跑,很多问题就暴露出来了,我们正在重构
superbai
    105
superbai  
   Apr 27, 2020
歪个楼问一下大佬,字节在上海都有哪些部门,Python/Java/Golang 语言都有岗位吗
felix021
    106
felix021  
OP
   Apr 27, 2020 via Android
@superbai 部门相当多,广告,ailab,飞书,基础架构等,我没有完整 list; 这些岗位都有的,文末附的链接(所有职位)你可以点开看一下,我们组这边需要 go 和 python
superbai
    107
superbai  
   Apr 27, 2020
@felix021 #106 感谢~我看看
whatisnew
    108
whatisnew  
   Apr 27, 2020
码农们的自救:没有困难创造困难
qieqie
    109
qieqie  
   Apr 27, 2020
最后一段气球程序无法说明上文的结论,至少要加入 MADV_DONTNEED 的对照
Linux page reclaim 的过程比较复杂,除了 anonymous memory (通常就是你程序申请的内存),还有 page cache (文件的读写缓存),
我猜你在 golang 1.10 做同样的实验,可能结果不会差太多
qieqie
    110
qieqie  
   Apr 27, 2020
网上也有说 rss 不包括 file cache 的,
不过从 /proc/$pid/status 来看,rss 是分成 RssAnon RssFile RssShmem 的,
当然我也不是特别确定,欢迎指正
fyibmsd
    111
fyibmsd  
   Apr 27, 2020
建议技术文章还是少放点无关图片
Gea
    112
Gea  
   Apr 27, 2020
有点难搞啊,我们的业务还真的遇不到这种问题,面试问这样的问题真不知道怎么回答了

我们当时 1.9 升 1.11 都无痛升级了,1.11 到 1.13 也无痛
felix021
    113
felix021  
OP
   Apr 27, 2020
@monsterxx03 提个小建议,如果检测到输出不是 tty (例如重定向到文件)的情况,就不要用 \x1b 做高亮,不然 vim 打开就变这样了:

goroutines:

^[[33m1^[[0m ^[[33mwaiting^[[0m ^[[33m^[[0m ^[[33mmain (/usr/local/go/src/runtime/proc.go:109)^[[0m
^[[33m2^[[0m ^[[33mwaiting^[[0m ^[[33m^[[0m ^[[33mforcegchelper (/usr/local/go/src/runtime/proc.go:240)^[[0m
felix021
    114
felix021  
OP
   Apr 27, 2020
@Gea 面试不会问到这么坑的,关于字节跳动的面试细节,你可以参考我文末我贴的那篇链接
felix021
    115
felix021  
OP
   Apr 27, 2020
@qieqie 嗯 严谨一点的话应该把这个对照也放出来。不过差别确实很多,否则也不会因为 RSS 报警了。

`man ps` 对 rss 的定义比较粗:

RSS resident set size, the non-swapped physical memory that a task has used (in kiloBytes)

按这个定义,是包含页表中所有已分配、未写到 swap 的内存。

对比下 `man top` ,有更多精细的指标,把内存分成了 4 个象限。
felix021
    116
felix021  
OP
   Apr 27, 2020   ❤️ 1
@fyibmsd 多谢建议,其实我就是讲个故事,加点图故事性更好。
ForeverYoung123
    117
ForeverYoung123  
   Apr 27, 2020
学习了。
monsterxx03
    118
monsterxx03  
   Apr 27, 2020
@felix021 可以啊,我明天改下
monsterxx03
    119
monsterxx03  
   Apr 27, 2020
@felix021 想起 summary 命令加过个 --no-color 选项
HangoX
    120
HangoX  
   Apr 27, 2020
其实想说,你们升级真浪。。。直接用新版本编译,不做对照测试的么。。
SevenJ
    121
SevenJ  
   Apr 27, 2020
学习了
felix021
    122
felix021  
OP
   Apr 27, 2020
@HangoX 对照了呀,线上都灰度了,应该是正好挑到了 3.16 内核的机器,没踩坑
OakScript
    123
OakScript  
   Apr 27, 2020
诶,为什么不发 byte tech
chiuan
    124
chiuan  
   Apr 27, 2020
我表示一直用最新版本貌似也没啥问题呀。。
saberlong
    125
saberlong  
   Apr 27, 2020 via Android
@monsterxx03 分析对象没用才麻烦。已经逐步缩减范围了,发现接受请求才会触发。但是没能明确位置。触发后停止,然后执行 gc,对象都已经释放了。pprof 已经没有 inuse_object 了。但是有一项 inuse_space,显示的是汇编代码 CALL DI 时,systemstack 的占用。最后在往 go 程扩栈方向找问题时,不能再现了
liuxu
    126
liuxu  
   Apr 27, 2020
好文,图有点多,干扰注意力。。

按照这个原理,你不需要处理这个问题啊,反而应该把所以内核升级到 4.5 以上,go 都使用 1.12

首先,资源是用来用的,既然不是内存泄露,内存用完是好事啊,说明程序资源利用率高,是为了性能才做的优化,并且内核需要内存的时候会自动收回

这里好比数据库建立的连接池,虽然占用了系统的文件描述符数量,但是可以避免总是重新建立连接,加快了访问,而且其他程序需要建立连接而系统不够的时候,还能释放连接池不用的连接然后给新程序分配连接,很好的一个设计啊
felix021
    127
felix021  
OP
   Apr 27, 2020
@liuxu 理论上是这样的,但是因为监控链路没有更新,无法区分 [是真的内存要挂了] 还是 [暂时没被回收] ,如果升级了,结果就是,要么不停报警到常态忽略直到某天突然 gg,要么不报警直到某天突然 gg 。
liuxu
    128
liuxu  
   Apr 27, 2020
@felix021 这么一看感觉是内核的锅,没给个标记统计数据出来么

你这个问题我查了下,go1.12.5 好像修复了

https://github.com/prometheus/prometheus/issues/5524

https://github.com/golang/go/commit/fd3676302e8436ec9243ae0695582bbb3f31dc3a
bazingaterry
    129
bazingaterry  
   Apr 27, 2020
Bytedance 还在用 1.12 那岂不是 go mod 都还没用上?我司都 1.14 了……
liuxu
    130
liuxu  
   Apr 27, 2020   ❤️ 1
我帮你翻了一下 MADV_FREE 相关的文章,linux 内核有给出统计输出,你看看 /proc/${pid}/smaps,有 LazyFree 项,这项应该就是某进程被标记但是没释放的内存,你们的监控系统可以监控统计这一项

这是 linux 内核的 commit 邮件: https://lore.kernel.org/patchwork/patch/760452/
这个是参数说明: http://man7.org/linux/man-pages/man5/proc.5.html

下面是我系统的输出,我 ubuntu20.04

root@liuxu-TM1612:/proc/46591# uname -a
Linux liuxu-TM1612 5.4.0-26-generic #30-Ubuntu SMP Mon Apr 20 16:58:30 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux


root@liuxu-TM1612:/proc/46591# cat smaps | grep -i lazy
LazyFree: 0 kB
LazyFree: 0 kB
LazyFree: 0 kB
LazyFree: 0 kB
LazyFree: 0 kB
LazyFree: 1392 kB
LazyFree: 0 kB
LazyFree: 0 kB
LazyFree: 0 kB
LazyFree: 0 kB
LazyFree: 0 kB
LazyFree: 0 kB


root@liuxu-TM1612:/proc/46591# cat smaps
00400000-00b52000 r-xp 00000000 08:02 4456749 /home/liuxu/.config/qv2ray/vcore/v2ray
Size: 7496 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 6216 kB
Pss: 6216 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 6216 kB
Private_Dirty: 0 kB
Referenced: 6216 kB
Anonymous: 0 kB
LazyFree: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
FilePmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 0 kB
THPeligible: 0
VmFlags: rd ex mr mw me dw sd
......
......
c000200000-c000800000 rw-p 00000000 00:00 0
Size: 6144 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 5352 kB
Pss: 5352 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 1392 kB
Private_Dirty: 3960 kB
Referenced: 4764 kB
Anonymous: 5352 kB
LazyFree: 1392 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
FilePmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 0 kB
THPeligible: 0
VmFlags: rd wr mr mw me ac sd nh
felix021
    131
felix021  
OP
   Apr 27, 2020
@liuxu 这个事情比你估计的还复杂一点,读取 smaps,其实是让内核遍历进程的页表,这个操作需要对页表加锁,如果期间出现了 page fault 之类需要修改页表的操作会增加业务延迟。

在监控做这个事情的话,影响会比较大,除非系统在每次 madvise 都更新统计信息 ,每次直接读取这个统计信息,才能降低对业务的影响。
felix021
    132
felix021  
OP
   Apr 27, 2020
@bazingaterry 看具体的项目,有的还在用 govendor,有些升级到 go mod 了。
wangxiyu191
    133
wangxiyu191  
   Apr 27, 2020
@felix021 读 vmstat 里面的 pglazyfreed 呢?话说感觉用 balloon 的方式来解这个问题很诡异啊,我比较赞同前面楼里说的这个本质问题不在 MADV_FREE 而是在监控不正确。
felix021
    134
felix021  
OP
   Apr 27, 2020
@OakScript 说的很有道理,刚看到技术学院征文,赶紧去占个坑
felix021
    135
felix021  
OP
   Apr 27, 2020
@wangxiyu191 balloon 不是用来[解决],是用来[验证];

看起来 pglazyfree 是这个意思(不是 pglazyfreed ),不过它统计的是 OS 级别的数据,实际生产中很可能需要进程级别的数据
wangxiyu191
    136
wangxiyu191  
   Apr 27, 2020
@felix021 哦哦验证可以的。pglazyfree 是指的啥?我看我系统和这个 patch ( https://lwn.net/Articles/594847/)里面都是 pglazyfreed 。进程级别的找内核组的帮忙加个吧 23333,应该不难的。
noogler67
    137
noogler67  
   Apr 28, 2020 via iPhone
其实只要 google 一下 go1.12 memory surge 第一个结果就是。而且楼主好像就是参(摘)考(抄)人家的段落
noogler67
    139
noogler67  
   Apr 28, 2020 via iPhone
楼主可以给大家举一个你觉得不刁难人的但又具有考察意义的和此 bug 相关的问题。
noogler67
    140
noogler67  
   Apr 28, 2020 via iPhone
看错了 楼主说 8 月份,请忽略前面的回复
nuk
    141
nuk  
   Apr 28, 2020
madvise 这个玩意有 bug 的,我们的古董内核 3.10 上用 jemalloc,调用参数 MADV_DONTNEED,结果完全不会释放内存,但是 glic 里的 mmap 和 munmap 倒是很正常,后来只好自己弄了个内存池。
nuk
    142
nuk  
   Apr 28, 2020
不过这个气球的做法,根据 commit 策略的不同,可能会让其他程序卡死在 page fault 里面。
hatebugs
    143
hatebugs  
   Apr 28, 2020 via Android
喜欢大标题
felix021
    144
felix021  
OP
   Apr 28, 2020
@nuk 这是 jemalloc 的锅(我们这边还有因为 jemalloc 的其他 bug 导致 core 的 case ),glibc 也用 madv_dontneed 的;另外气球只是用来验证,不是用来解决问题的。
felix021
    145
felix021  
OP
   Apr 28, 2020
@noogler67 哈哈哈 天下文章一大抄,但我抄的有放参考链接了
felix021
    146
felix021  
OP
   Apr 28, 2020
@wangxiyu191 看起来 pglazyfree 是可以延迟回收的页面,pglazyfreed 是可以延迟回收且回收了的页面
sariya
    147
sariya  
   Apr 28, 2020 via Android   ❤️ 1
总结下就是:通过延迟回收来减少内存操作,结果进程占用的物理内存页一直居高不下,虽然里面有很大的水分。只要整台机器上仍有空闲内存,os 就会优先分配空闲内存而不会回收 MADV_FREE 的内存吧。
felix021
    148
felix021  
OP
   Apr 28, 2020
@sariya 对于其他进程,肯定是优先分配已经回收的页面;对于该进程,如果写入的是没被回收页面,那就不用额外分配。
sariya
    149
sariya  
   Apr 28, 2020 via Android
@felix021 也就是说如果这个进程自己需要就会重用 MADV_FREE 页面;如果它自己一直不需要那 os 就会优先把这些页分配给其他需要的进程;没有任何进程需要分配内存就保持现状?
felix021
    150
felix021  
OP
   Apr 28, 2020
@sariya 不是很准确。

比如某个页面 A 对应的地址空间是 0x66660000,被 madvise(free)标记了,如果 A 尚未被 kernel 回收,且进程需要写入 0x66660000,就可以继续用 A ;如果进程需要写入其他新的地址空间,应该还是需要重新分配的(这是我的推测,没验证过)。

对于另一个进程,新申请页面的时候,如果 OS 有可用的,那么优先用可用的,效率更高,不够的时候,再去回收 A 。
sariya
    151
sariya  
   Apr 28, 2020 via Android
@felix021 嗯是的,我 #149 说的有点问题
Cyshall
    152
Cyshall  
   Apr 29, 2020
“所以 free 可以通过 madvise 告诉内存“这一段我不用了”。

楼主这段话该怎么理解啊? 是 free()的实现里面调用了 madvise()还是说在在调用 free()之后在调用一次 madvise()呢?

因为我在 free()的实现里面并没有发现调用了 madvise() 所以比较疑惑。
wysnylc
    153
wysnylc  
   Apr 29, 2020
qv2ray???
felix021
    154
felix021  
OP
   Apr 29, 2020
@Cyshall

__libc_free()
_int_free()
heap_trim()
shrink_heap()
__madvise ((char *) h + new_size, diff, MADV_DONTNEED)
1  2  
About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   5819 Online   Highest 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 100ms · UTC 02:13 · PVG 10:13 · LAX 19:13 · JFK 22:13
♥ Do have faith in what you're doing.