内容简介:这周换换口味,记录一下去年踩的一个大坑。== 起 ==
这周换换口味,记录一下去年踩的一个大坑。
== 起 ==
大概是去年8月份,那会儿我们还在用着64GB的“小内存”机器。
由于升级一次版本需要较长的时间(1~2小时),因此我们每天只发一次车,由值班的同学负责,发布所有已merge的commit。
当天负责值班的我正开着车,突然收到 Bytedance-System 的夺命连环call,打开Lark一看:
[ 规则 ]:机器资源报警
[ 报警上下文 ]:
host: 10.x.x.x
内存使用率: 0.944
[ 报警方式 ]:电话&Lark
打开ganglia一看,更令人害怕:
== 承 ==
这看起来像是典型的内存泄漏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% **[此处打码]**
...
就这样,一顿操作猛如虎, 涨跌全靠特朗普 ,最终结果是,一方面没看出啥问题,另一方面也没看出啥问题。
正在一筹莫展、准备回滚之际,内存它自己稳了:
虽然占用率仍然很高,但是没有继续上升,也没有出现OOM的情况。
== 转 ==
排查过程中,我们还发现一个现象:并不是所有机器的内存都涨。
(确实有点“灵”……)
这些机器的硬件都是一致的,但是用 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,发布到几台内存异常的机器上。
于是问题解决了。
== 合 ==
为什么 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,否则内存不会实际分配
效果如下:
可以看到,虽然打码进程的 VIRT(地址空间大小)还是52G,但是实际占用的内存已经下降到 35G,气球生效了。
== 合² ==
简单汇总一下前面的内容:
-
Go 1.12 升级能降低内存分配的延迟,但会导致进程RSS变高
-
因为 Go 1.12 用 MADV_FREE ,会让内核延迟回收内存
-
通过在页表中做标记的方式,延迟内存的分配和回收,可以提高内存管理的效率
-
可以通过气球来让 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/
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 踩坑记:Go 服务内存暴涨
- 3 月全球数据库排名:PostgreSQL 再迎暴涨
- disk io引起golang线程数暴涨的问题
- Elastic 今日在纽交所上市,股价最高暴涨122%。
- 11 月数据库榜单,SQLite 前十,PostgreSQL 连续暴涨
- 2018年被盗加密货币金额暴涨300%,行业已成黑客乐土?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
游戏编程中的人工智能技术
布克兰德 / 吴祖增 / 清华大学出版社 / 2006-5 / 39.0
《游戏编程中的人工智能技术》是人工智能游戏编程的一本指南性读物,介绍在游戏开发中怎样应用遗传算法和人工神经网络来创建电脑游戏中所需要的人工智能。书中包含了许多实用例子,所有例子的完整源码和可执行程序都能在随书附带的光盘上找到。光盘中还有不少其他方面的游戏开发资料和一个赛车游戏演示软件。 《游戏编程中的人工智能技术》适合遗传算法和人工神经网络等人工智能技术的各行业人员,特别是要实际动手做应用开......一起来看看 《游戏编程中的人工智能技术》 这本书的介绍吧!