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 · 21869 次点击
这是一个创建于 1670 天前的主题,其中的信息可能已经有所发展或是发生改变。

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


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


大概是去年 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  
asAnotherJack
    1
asAnotherJack  
   2020-04-26 14:46:13 +08:00   ❤️ 1
之前也遇到了类似的情况,也以为是内存泄漏
另外可以设置 GODEBUG madvdontneed 使用之前的行为

https://godoc.org/runtime#hdr-Environment_Variables

madvdontneed: setting madvdontneed=1 will use MADV_DONTNEED
instead of MADV_FREE on Linux when returning memory to the
kernel. This is less efficient, but causes RSS numbers to drop
more quickly.
xloger
    2
xloger  
   2020-04-26 14:50:41 +08:00   ❤️ 8
收藏多于回复,对我来说的话是因为博主的大部分文章都是科普向,写得的确挺好也很有趣,但是我也就只能学习一下了,我 技术栈不是这方面 /水平不够 没法参与讨论。
rockyou12
    3
rockyou12  
   2020-04-26 14:55:47 +08:00
硬核文章,回复支持下
felix021
    4
felix021  
OP
   2020-04-26 14:58:10 +08:00
@xloger @rockyou12 顶帖都是真爱~
bluefalconjun
    5
bluefalconjun  
   2020-04-26 14:58:50 +08:00   ❤️ 1
漂亮! 这种问题追起来是最有意思的
labulaka521
    6
labulaka521  
   2020-04-26 15:01:26 +08:00
收藏改天看
dandycheung
    7
dandycheung  
   2020-04-26 15:10:00 +08:00 via iPhone
开篇说的是收藏少于回复呀?到底是什么状况?
dilu
    8
dilu  
   2020-04-26 15:14:17 +08:00
我以为是个技术分享,结果是个招聘
IMCA1024
    9
IMCA1024  
   2020-04-26 15:14:43 +08:00
这是大佬,我现在看不懂
pmispig
    10
pmispig  
   2020-04-26 15:14:44 +08:00
你这个故事有一个疑点,就是你说你开启了 pprof,问题就在这里,pprof 是不会撒谎的,它给你的信息可以看出真实在使用的内存,可被 GC 回收的,或者被系统回收的内存是可以看出来的,用 pprof 观察的话,是不会得出内存暴涨这样的结论的。因为你能明确看到 HeapAlloc 。
退一步说,就算你没有开启 pprof,那么你可以紧急上一个带有定时任务的 runtime.MemStats,每秒一次打出 HeapAlloc,也不会得出结论内存暴涨。
还请楼主指点
felix021
    11
felix021  
OP
   2020-04-26 15:20:00 +08:00
@pmispig 对,可能文中没说得很明确,pprof 没对比出差别(“另一方面也没查出啥问题”),内存暴涨是 ganglia 上看到,ps/top 也能看到的
felix021
    12
felix021  
OP
   2020-04-26 15:20:39 +08:00
@dandycheung 符号打反了...
felix021
    13
felix021  
OP
   2020-04-26 15:22:16 +08:00
@IMCA1024 哪儿不懂,提出一个问题?
pmispig
    14
pmispig  
   2020-04-26 15:23:27 +08:00
@felix021 请问有没有注意 free buff/cache 的大小,按道理来说,可被释放的内存会出现在这个里面,如果你用 available 来看的话应该也是正常的,只是 used 不好看
felix021
    15
felix021  
OP
   2020-04-26 15:28:25 +08:00
@pmispig 因为这些页面还没被 OS 回收,所以不会计入到 free 的 buff/cache ;不过如果用 rnutime.MemStats 看应该是能看到被 go runtime 使用的内存没有暴涨
rrfeng
    16
rrfeng  
   2020-04-26 15:31:11 +08:00
有一个差不多的情况,redis info memory 里会有几个值:max,used,peak,如果有大量 key 过期或者删除,peak > used 。

而 peak = 进程 rss,used = redis 里所有对象占用的,实际上 redis 并没有用到这些内存,但是不会立即释放给 OS 。
ysmood
    17
ysmood  
   2020-04-26 15:32:23 +08:00
所以还是没提算法边界条件的细节,单纯靠设置 flag 或者吹气球只是在回避问题核心?
比如写个最小 go 实例能 100% 复现这个问题。一般人真正关心的是如何在 go 代码层回避这个问题吧?
ysmood
    18
ysmood  
   2020-04-26 15:35:05 +08:00
所以说白了就是监控反应的不是真实情况,运维需要与时俱进改善对新 linux 内核的监控方式(笑
felix021
    19
felix021  
OP
   2020-04-26 15:39:07 +08:00
@ysmood 其实对于大部分的场景,dontneed 和 free 的差别 可能都不会被观测到,我们的场景比较特殊,内存占用比较大,会导致 RSS 太大触发报警。

解决问题,可以使用 go1.12 新增的 GODEBUG=madvdontneed=1 这个环境变量,切回到原来的模式。

监控反应的情况是否“真实”要看怎么定义了。。。这个未回收的页面可以如何体现,确实是个不错的问题
loalj
    20
loalj  
   2020-04-26 15:47:43 +08:00   ❤️ 1
去年也碰到过,在系统上看到最直观的指标是 heapAlloc/heapReleased, heapReleased 在 MADV_FREE 模式下不会还给 OS. 另外 MADV_FREE 对比 DONTNEED 我们当时观察到确实在 minor page fault 上优化了很多。
phpcyy
    21
phpcyy  
   2020-04-26 15:53:19 +08:00
借楼问个和楼主曾经出现过的相似的问题,很高兴能遇见大佬,希望能够给点指点:

有一次发完 golang 项目,内存占用会持续上升,表现为某个时间段持续上升,某个时间段静止,再过一段时间又会再次上升,直至内存全部用光,操作系统杀死该进程,supervisor 重新拉起来该进程。

我也是用 pprof 分析了好久,发现没有任何异常。
然后又在本地压测什么的,都没复现这个问题。

最后真的是无可奈何,只能到处看看项目。偶然去检查了构建命令,之前我复制的测试环境的命令,把 `-race` 加上了,线上环境去掉该指令后内存泄漏问题就解决了。

当时的 golang 版本是 1.13.6,请问您觉得也是这个问题导致的吗?抑或是我可以通过什么方法找到问题根源?

如能给点指导,不胜感激。

补充一点:
我搜 go race memory leak,看到相关的 issue 已经被 close 和 fix 了。

( ps: 最近面了头条,当然是不过,毕竟我这种分析水平确实不太行,这个问题也表现了自己能力的不足。)
ysmood
    22
ysmood  
   2020-04-26 15:55:47 +08:00
@ysmood 我说的 flag 就是指的这个 GODEBUG=madvdontneed=1 啊。所以理论上应该去修正监控和报警吧,虽然可能改 flag 是最现实的做法,但我觉得最好是把理论也说清楚以免看不懂的人误解。

内存报警不应该是泄漏导致需要内存的地方没有足够内存使用吗?还需要怎么定义?现在是其它程序需要内存依然能正常得到吧?

我觉得去业界看看用的多的监控系统源代码,肯定有监控系统已经修正或者打算修正这个问题了,也不是只有 go 才能触发。
egen
    23
egen  
   2020-04-26 16:10:42 +08:00
好奇这个问题追了多久
felix021
    24
felix021  
OP
   2020-04-26 16:20:34 +08:00
@phpcyy 看起来你这个 case 就是多线程竞争共享变量的处理不当导致了内存泄漏,-race 注入了很多代码,会导致性能下降( race-enabled binaries can use ten times the CPU and memory ),但是可能带来的意外收获是大幅降低了竞争冲突出现的概率。
tt67wq
    25
tt67wq  
   2020-04-26 16:23:02 +08:00
这个码为何我扫不出?
felix021
    26
felix021  
OP
   2020-04-26 16:23:32 +08:00
@ysmood 理论上是的,但是这个周期可能很长。

想要监控到这部分内存,需要 Linux 内核采集更多信息,以及更新整个监控方案的相关逻辑。

我注意到有开发者给内核提了个 patch,但是开发者前年过世了,不知道后续情况如何:

mm: add a separate RSS for MADV_FREE pages
https://lore.kernel.org/patchwork/patch/760453/
tt67wq
    27
tt67wq  
   2020-04-26 16:24:27 +08:00
原来是我沙雕浏览器的问题,复制到文本里面就能扫出了
felix021
    28
felix021  
OP
   2020-04-26 16:24:54 +08:00
@egen 当天定位了问题,几个小时吧
felix021
    29
felix021  
OP
   2020-04-26 16:25:16 +08:00
@tt67wq 下次我还是贴图吧。。感觉损失了好多转化(捂脸)
luojian666
    30
luojian666  
   2020-04-26 16:30:19 +08:00
已关注~
rafa
    31
rafa  
   2020-04-26 16:30:39 +08:00
学习了
felix021
    32
felix021  
OP
   2020-04-26 16:33:00 +08:00
@phpcyy 我好像看错了你的问题,是说测试环境用了 -race 出现内存异常,但是线上没用 -race 就正常?
felix021
    33
felix021  
OP
   2020-04-26 16:33:44 +08:00
@phpcyy 那看起来是 race runtime 的问题,细节你得看看那个 issue 做了啥
phpcyy
    34
phpcyy  
   2020-04-26 16:37:07 +08:00
@felix021 对,性能下降我是认的,但是内存泄漏我不知道是由于我的代码中存在什么问题导致的还是 go build -race 中有什么机制导致的。

我去掉了这个 flag 之后内存就没变化过,而且标准输出中不记得有 go race 检测到的并发冲突。
还有就是如果并发访问可能会导致 panic,程序也没有 panic 记录。

实际上我的那个代码非常简单,让另外一个同事一起逐行检测过,没发现任何问题。

现在就是非常疑惑什么情形下开了 race flag 会导致内存泄漏,其实这个 case 挺难遇到的,现在也算是解决了,就是不知道更细节的坑是什么。
phpcyy
    35
phpcyy  
   2020-04-26 16:40:14 +08:00
@felix021 #32,是线上原先用 -race 异常,去掉了就正常,再也没发生过内存变化。原先是 1 天以内必定大幅度上升,改了之后十几天内存都是稳定的直线。
qwefdrt
    36
qwefdrt  
   2020-04-26 16:45:04 +08:00
文章写的太有意思了,但是最后防不胜防啊
Dreamacro
    37
Dreamacro  
   2020-04-26 16:52:32 +08:00
为什么不直接 GODEBUG=madvdontneed=1 禁用 MADV_FREE
phytry
    38
phytry  
   2020-04-26 16:58:29 +08:00
顶顶,我也遇到过,以为是内存泄漏,但是过段时间就恢复正常了,我入坑的时候 go 就已经是 1.12 ,我当时以为这是 go 自身的特性,就没有继续研究了
felix021
    39
felix021  
OP
   2020-04-26 17:02:38 +08:00
@Dreamacro 升级的时候没细看 release notes,估计看了也不知道影响会这么大
gemini767
    40
gemini767  
   2020-04-26 17:15:43 +08:00   ❤️ 1
不错的 case,但是我有一个疑问,线上环境难道不是 docker 打包,为什么会有同一环境多 linux kernel 版本?
zdt3476
    41
zdt3476  
   2020-04-26 17:23:05 +08:00
@phpcyy 使用-race 参数的话,达到 1024 个 goroutine 进程就会挂掉
jinsongzhao
    42
jinsongzhao  
   2020-04-26 17:24:05 +08:00 via Android
gc 才是最大的坑,不过确实大幅度降低了程序员门槛,提高了程序员体验。连编程的妹子都越来越多了。所以,我们还是需要 gc 这个坑
ikaros
    43
ikaros  
   2020-04-26 17:30:48 +08:00
我们之前也遇到过这个问题, 也是广告点击系统, pprof 那个 cum 还是啥我记得是有列出整个程序生命中各个函数从高到低申请的内存量的, 然后依次检查每个函数内部有没有大的对象分配
songjiaxin2008
    44
songjiaxin2008  
   2020-04-26 17:35:08 +08:00
@gemini767 #40 docker 并不能改变宿主机内核版本
lewinlan
    45
lewinlan  
   2020-04-26 17:46:32 +08:00 via Android
前阵子遇到过类似问题。
docker 看内存爆炸,pprof 看毫无压力。研究了好久才知道是统计口径的问题,然后写了篇博客记录了下就没再关心了
felix021
    46
felix021  
OP
   2020-04-26 17:53:05 +08:00
@gemini767 我们这个服务内存要求比较大,目前用的是物理机,正在做架构调整
zhouwei520
    47
zhouwei520  
   2020-04-26 17:59:56 +08:00
学习到了
lucky215
    48
lucky215  
   2020-04-26 18:03:17 +08:00
学习了
logic159
    49
logic159  
   2020-04-26 18:26:52 +08:00
写得不错
gemini767
    50
gemini767  
   2020-04-26 18:27:04 +08:00
@songjiaxin2008 学习到了 docker image 使用宿主机的共享 kernel
bsidb
    51
bsidb  
   2020-04-26 18:46:21 +08:00
Linux 下内存分配那一段获益匪浅。之前用 C 语言写科学程序的时候就遇到了类似的问题。用 malloc 申请了一段非常大的内存缓冲区之后,第一次扫描该缓冲区的速度特别慢,后面再次扫描时速度就正常了。
Meltdown
    52
Meltdown  
   2020-04-26 18:48:00 +08:00 via Android
貌似用 tcmalloc 也会有类似问题?分配器申请了内存内核没有回收
felix021
    53
felix021  
OP
   2020-04-26 18:57:07 +08:00
@Meltdown 会的,一般 api 层的实现都会维护比较复杂的内存管理,找 os
分配和回收开销很大,不会每次都穿透。
sagaxu
    54
sagaxu  
   2020-04-26 18:57:33 +08:00 via Android
JVM 也有类似特性,延迟释放
felix021
    55
felix021  
OP
   2020-04-26 18:57:33 +08:00
@gemini767 可以了解一下 docker 底层的实现( cgroup ),严格来说不是共享 kernel
felix021
    56
felix021  
OP
   2020-04-26 18:58:12 +08:00
@bsidb 对,推荐看那篇 What a C programmer should know about memory,可以搜到中文翻译的版本
azh7138m
    57
azh7138m  
   2020-04-26 19:06:51 +08:00 via Android
为啥不发 ByteKM (手动狗头
felix021
    58
felix021  
OP
   2020-04-26 19:08:15 +08:00
@azh7138m 和大佬们相比,感觉写得比较水,不敢去装逼
felix021
    59
felix021  
OP
   2020-04-26 19:08:31 +08:00
@sagaxu 对,底下原理都一样的
bitdepth
    60
bitdepth  
   2020-04-26 19:28:54 +08:00 via iPad
malloc()並不會清空 page 內容吧? calloc()才有
fcoolish
    61
fcoolish  
   2020-04-26 19:31:57 +08:00
感谢分享
hushao
    62
hushao  
   2020-04-26 19:53:29 +08:00
确认过眼神,大佬是在水招聘文😂
感谢 lz 分享
felix021
    63
felix021  
OP
   2020-04-26 19:53:52 +08:00
@bitdepth 嗯,对,但是我说的不是 malloc 清空 page 内容,是说 OS 给的 page 是全零的,否则可能会暴露其他进程的数据,有安全风险。
buffzty
    64
buffzty  
   2020-04-26 20:09:06 +08:00
能不能别写的跟知乎一样?
cydian
    65
cydian  
   2020-04-26 20:24:51 +08:00
@felix021 这样的二维码如何生成呢?
felix021
    66
felix021  
OP
   2020-04-26 20:32:29 +08:00
@cydian google 搜一下 qr code ascii 有几个提供在线生成的服务。

linux 下也有个 libqrencode3 可以搞这个,google-authenticator 就是用它来生成终端下的 qrcode 的。
felix021
    67
felix021  
OP
   2020-04-26 20:32:53 +08:00
@buffzty 那你觉得写成什么样更好呢?
tkl
    68
tkl  
   2020-04-26 21:03:45 +08:00   ❤️ 3
最烦这种到处表情包,结尾公众号的。
felix021
    69
felix021  
OP
   2020-04-26 21:17:49 +08:00
@tkl 感谢看到结尾,我照顾不到所有人口味,我自己写得开心就行。
hcivincentchan
    70
hcivincentchan  
   2020-04-26 22:09:14 +08:00
你是头条的吧。。。
Arnie97
    71
Arnie97  
   2020-04-26 22:35:48 +08:00
感谢科普,tag 很灵性
gemini767
    72
gemini767  
   2020-04-26 22:40:02 +08:00
@felix021 我明白你的意思,但是我说的是共享的实现,而非共享资源,也就是我最初疑问多版本 kernel 问题
felix021
    73
felix021  
OP
   2020-04-26 22:41:48 +08:00 via Android
@Arnie97 哈哈哈 tag 确实很“灵”
felix021
    74
felix021  
OP
   2020-04-26 22:43:13 +08:00 via Android
@hcivincentchan 最后的广告还不足以说明吗🤔
mahone3297
    75
mahone3297  
   2020-04-26 23:09:48 +08:00
很不错的分享,赞
临时解决方案,应该是上面说的,加参数,仍然用老的 MADV_DONTNEED 方式
但,既然 go 升级了,默认采用了新的方式 MADV_FREE,应该是新的方案更好(事实上也确实是,gc 效率更高),那,长久方案应该是能读到正确的值( go 未归还给 os 的部分)比较好
felix021
    76
felix021  
OP
   2020-04-26 23:15:30 +08:00
@mahone3297 对,但是这需要内核提供更精细的数据,以及整个监控链路的相关改造,周期会比较长了。
k9982874
    77
k9982874  
   2020-04-26 23:25:10 +08:00 via iPad
我擦,这个周末刚碰上了这个问题,没想到逛 v2“捡到”了答案。感谢!
saberlong
    78
saberlong  
   2020-04-27 08:55:43 +08:00
saberlong
    79
saberlong  
   2020-04-27 09:17:52 +08:00
@felix021
刚才浏览器卡了下,竟然直接发出去了.
借地发一个类似的棘手的问题,至今没有找到答案。
内存随着运行不断增加。开始以为常见的泄漏问题,但并不是那么简单。
版本:go1.13.x 。去年的事 x 多少忘记了。
前提:window 下交叉编译成 linux 时会出现。 我的是 linux 系统,编译出来没问题。
调查情况: pprof 观察 inuse_space 能观测到内存占用,但是显示只有一条记录,是 runtime.systemstack 的占用,位置发生在 asm_amd64.s 的 370 位置。 功能是"call target function",执行 CALL DI 。到这以为是调用函数栈的问题,检查程序后排除。 观察 go 程,仅 pprof 所使用的 go 程、一个 tcp 监听 go 程以及 main 程等待 signal 。
通过 linux 下的分析一些分析工具没找到头绪。根据可能性猜测去看了 runtime 源码也没能定位。

最后,不知道怎么回事,它好了。原本每次必现的问题,它竟然自己好了。然后就没法追查了。
不知道这里的各位有什么头绪
zhoudaiyu
    80
zhoudaiyu  
   2020-04-27 09:51:52 +08:00
排查问题很爽,有时候觉得自己化身柯南 /doge
heyhumor
    81
heyhumor  
   2020-04-27 10:04:47 +08:00
你图真多
dreamage
    82
dreamage  
   2020-04-27 10:20:02 +08:00
硬核
mentalkiller
    83
mentalkiller  
   2020-04-27 10:22:27 +08:00
有一说一,字节的招聘帖比阿里的招聘帖高到不知道哪里去了。
至少让我不排斥。
lrh3321
    84
lrh3321  
   2020-04-27 10:27:44 +08:00 via Android
硬核招聘帖
jonnn
    85
jonnn  
   2020-04-27 10:58:15 +08:00
你三月份发我就不用重新踩一遍了
felix021
    86
felix021  
OP
   2020-04-27 11:08:01 +08:00
@saberlong 现场没有了可能是比较难查 ,如果还有现场的话,上面说的 GODEBUG=madvdontneed=1 可以试试。

另外对于很难定位的问题,我还会用尝试删掉无关的逻辑,找到可能会导致问题的最小集。
felix021
    87
felix021  
OP
   2020-04-27 11:10:00 +08:00
@jonnn 嗯,这个坑有点偏门,我们厂内去年 7 月份就已经有团队踩过了。。。
saberlong
    88
saberlong  
   2020-04-27 12:01:24 +08:00 via Android
@felix021 我也希望能复现。最小集已经在做了,只保留了描述中的几个 go 程。runtime.systemstack 和汇编的函数调用 CALL DI 的使用范围太广几乎没有帮助。因为 pprof 看不到占用的对象,去年后来是往调用函数时引起的 go 程扩栈方向去查的。同事开发中的代码,每天抽空排查的,可惜后来不能复现了
buffzty
    89
buffzty  
   2020-04-27 12:15:18 +08:00
@felix021 到处表情包,各种花里胡哨 ,简单的说明不行吗, 就跟装乎那种回答一样.明明能一两百字回答完的 非常写个两千字图文 真心不喜欢.不过你随意 我只是不想 v2 知乎化
Oysmart
    90
Oysmart  
   2020-04-27 12:22:29 +08:00
需要的就是这种分析和解决问题的方法。优秀!
felix021
    91
felix021  
OP
   2020-04-27 12:26:18 +08:00
@buffzty 这是通过少量多次的正反馈提高读者阅读兴趣的一种方法,全都是文字干巴巴的对于大多数人更容易 TLDR 。

这个问题一两百字能不能说得完,取决于读者的情况,release notes 里倒是很简洁,一次就能看懂的有多少呢。
felix021
    92
felix021  
OP
   2020-04-27 12:27:44 +08:00
@buffzty 你可以换个视角,大多数人来 v2 不是来学习,是来消遣的。
freefcw
    93
freefcw  
   2020-04-27 12:43:18 +08:00
不错不错。。。不过这个确实是有点坑
freefcw
    94
freefcw  
   2020-04-27 12:44:06 +08:00
对于内存这块的使用,一般用户都是黑盒,很有点麻烦
freefcw
    95
freefcw  
   2020-04-27 12:45:47 +08:00
c/c++这种反而是开发人员最能掌控的。。但也是最难的

go 和 java 等对这一块做了屏蔽和封装,是好事也是坏事,不过不了解底层终究是有些问题难以解决的
felix021
    96
felix021  
OP
   2020-04-27 12:52:06 +08:00
@freefcw 嗯,而且因为 runtime 更新很快,很多知识过期也很快,年纪大了就容易跟不上……
Wirbelwind
    97
Wirbelwind  
   2020-04-27 13:08:19 +08:00
学习了
Yyyye
    98
Yyyye  
   2020-04-27 13:20:02 +08:00
谢谢楼主分享这么有趣的过程!
monsterxx03
    99
monsterxx03  
   2020-04-27 13:49:51 +08:00   ❤️ 1
@saberlong 我写过一个工具, 可以 dump runtime heap 不同 size 对象的分配情况(当然还是没办法知道具体什么对象), 要能复现可以跑几次看下是什么 size 的对象分配在增长

https://github.com/monsterxx03/gospy#heap
ms2008
    100
ms2008  
   2020-04-27 13:50:05 +08:00
监控系统看起来够老的。。。
1  2  
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5892 人在线   最高记录 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 50ms · UTC 02:09 · PVG 10:09 · LAX 18:09 · JFK 21:09
Developed with CodeLauncher
♥ Do have faith in what you're doing.