V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
felix021
V2EX  ›  Go 编程语言

踩坑记: go 服务内存暴涨

  felix021 ·
felix021 · 2020-04-26 14:06:10 +08:00 · 21852 次点击
这是一个创建于 1664 天前的主题,其中的信息可能已经有所发展或是发生改变。

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


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


大概是去年 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/


欢迎关注我的公众号

微信扫码

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

第 1 条附言  ·  2020-04-26 15:41:42 +08:00
补充:

对于大部分应用场景,其实 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 条回复    2020-04-29 16:09:03 +08:00
1  2  
hallDrawnel
    101
hallDrawnel  
   2020-04-27 14:49:31 +08:00
我看有字节的同学已经用 Grafana 呀
rrfeng
    102
rrfeng  
   2020-04-27 14:57:38 +08:00
直接回退到旧版内存管理不是一劳永逸的办法。还是需要再想一下更好的办法。

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

我们当时 1.9 升 1.11 都无痛升级了,1.11 到 1.13 也无痛
felix021
    113
felix021  
OP
   2020-04-27 16:47:03 +08:00
@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
   2020-04-27 16:49:08 +08:00
@Gea 面试不会问到这么坑的,关于字节跳动的面试细节,你可以参考我文末我贴的那篇链接
felix021
    115
felix021  
OP
   2020-04-27 16:53:59 +08:00
@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
   2020-04-27 16:54:54 +08:00   ❤️ 1
@fyibmsd 多谢建议,其实我就是讲个故事,加点图故事性更好。
ForeverYoung123
    117
ForeverYoung123  
   2020-04-27 17:03:37 +08:00
学习了。
monsterxx03
    118
monsterxx03  
   2020-04-27 17:03:42 +08:00
@felix021 可以啊,我明天改下
monsterxx03
    119
monsterxx03  
   2020-04-27 17:06:20 +08:00
@felix021 想起 summary 命令加过个 --no-color 选项
HangoX
    120
HangoX  
   2020-04-27 17:36:27 +08:00
其实想说,你们升级真浪。。。直接用新版本编译,不做对照测试的么。。
SevenJ
    121
SevenJ  
   2020-04-27 17:49:44 +08:00
学习了
felix021
    122
felix021  
OP
   2020-04-27 19:38:14 +08:00
@HangoX 对照了呀,线上都灰度了,应该是正好挑到了 3.16 内核的机器,没踩坑
OakScript
    123
OakScript  
   2020-04-27 19:45:25 +08:00
诶,为什么不发 byte tech
chiuan
    124
chiuan  
   2020-04-27 19:48:27 +08:00
我表示一直用最新版本貌似也没啥问题呀。。
saberlong
    125
saberlong  
   2020-04-27 20:04:20 +08:00 via Android
@monsterxx03 分析对象没用才麻烦。已经逐步缩减范围了,发现接受请求才会触发。但是没能明确位置。触发后停止,然后执行 gc,对象都已经释放了。pprof 已经没有 inuse_object 了。但是有一项 inuse_space,显示的是汇编代码 CALL DI 时,systemstack 的占用。最后在往 go 程扩栈方向找问题时,不能再现了
liuxu
    126
liuxu  
   2020-04-27 20:39:45 +08:00
好文,图有点多,干扰注意力。。

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

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

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

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

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

https://github.com/golang/go/commit/fd3676302e8436ec9243ae0695582bbb3f31dc3a
bazingaterry
    129
bazingaterry  
   2020-04-27 21:27:41 +08:00
Bytedance 还在用 1.12 那岂不是 go mod 都还没用上?我司都 1.14 了……
liuxu
    130
liuxu  
   2020-04-27 21:35:32 +08:00   ❤️ 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
   2020-04-27 21:45:19 +08:00
@liuxu 这个事情比你估计的还复杂一点,读取 smaps,其实是让内核遍历进程的页表,这个操作需要对页表加锁,如果期间出现了 page fault 之类需要修改页表的操作会增加业务延迟。

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

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

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

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

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

因为我在 free()的实现里面并没有发现调用了 madvise() 所以比较疑惑。
wysnylc
    153
wysnylc  
   2020-04-29 13:10:29 +08:00
qv2ray???
felix021
    154
felix021  
OP
   2020-04-29 16:09:03 +08:00
@Cyshall

__libc_free()
_int_free()
heap_trim()
shrink_heap()
__madvise ((char *) h + new_size, diff, MADV_DONTNEED)
1  2  
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1022 人在线   最高记录 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 26ms · UTC 22:39 · PVG 06:39 · LAX 14:39 · JFK 17:39
Developed with CodeLauncher
♥ Do have faith in what you're doing.