一次艰难的线上游戏服务器内存排查经历

TL;DR: 透明大页导致的。

我们的游戏服务器程序是采用Go程序编写的,后面在经过各种努力之后,终于将启动内存从350M降低到150M左右。

但是上线之后,奇怪的事情发生了。

由于一些原因,我们使用了两个云服务商来部署服务器。

一个空的游戏服进程, 在其中一个服务商的ECS上表现正常,即是正常的150+M内存, 但是另一个云服务商的ECS上,则启动内存有240+M之多,整整差了80M。

这我觉得非常诡异,我先是使用Go语自带的pprof拉了一下heap数据。两个进程的内存分配信息虽然不能说完全相同,但是也相关不大。

当然我知道,pprof是建立在应用程序的基础上进行的,也就是说内存管理那一块,其实是没法暴露出来的。

比如,在malloc之后,虽然调用了free, 但是你去top这个进程,一般来讲RES列不会立即变小,这和内存归还策略有关。

比如,每个操作系统线程也占了4M栈内存, 但是两个进程的线程数量并没有显著不同。

再比如,由于goroutine的调度问题,导致分配和释放顺序的差异造成的内存碎片率不一致,内存管理库有可能因为碎片的原因,无法归还大块内存。我查看了一下两台机器的CPU和内存,果然不一样。但是我仔细分析了一下,进程在启动阶段开启会分配内存的goroutine只有2~3个,理论上来讲就算对碎片率有影响,那也是有限,不可能会造成如此显著的差异。

于是我想,也许是进程打开了很多数据表文件,加载之后没有释放。由于操作系统页调度的原因,其中一个平台的操作系统将其从内存中移除了,所以表现的是150M。

我去查看了两台机器上进程的/proc/pid/smaps,发现并没有任何数据表文件的映射,我把别的文件映射大小全统计了一下,也不过几M而已。

最后,我其实已经没有办法了,去找ChatGPT找找灵感,他让我使用getconf PAGESIZE来查看一下系统的页大小。

当我看到这个答案时,我几乎觉得他就是正确答案,失望再次随之而来,两个系统的页大小均为4K

于是我将目光投向了claude, 再次问出同样的问题,并说出两个机器的页大小一致。

claude给了一个思路,让我将使用pmap -x <pid>来分别打印出来两个机器上的服务器进程,以期望能找出点什么规律。

我太懒了,所以我将pmap的输出结果直接丢给了claude让其对比,claude输出如下:

通过对比两个进程的内存信息,我发现了一些关键差异:

虚拟内存大小差异:

进程1: VmSize = 1762456 kB
进程2: VmSize = 1688404 kB
差异约 74MB,这与你观察到的 80MB 接近

实际内存使用差异:

进程1: VmRSS = 248608 kB

RssAnon: 191344 kB
RssFile: 57264 kB

进程2: VmRSS = 161064 kB

RssAnon: 101188 kB
RssFile: 59876 kB

主要差异来自:

匿名内存(RssAnon)相差约 90MB:191MB vs 101MB
线程数不同:进程1有12个线程,进程2有11个线程
数据段大小(VmData)略有差异:

进程1: 336904 kB
进程2: 328260 kB

然而这个信息仅能说明内存的差异真的是来自于内存分配造成的,而不是别的什么原因。

最惨的是,这时我的免费次数用完了,于是我又切回了ChatGPT

这一次我将两个进程的/proc/pid/smaps的输出喂给了ChatGPT

ChatGPT发现了一个有意思的结论:

这74M的匿名内存块的映射大小不一致,一台机器上的部分内存块竟然是另一台机器上的两倍左右(并不是所有内存块都这样,由于这是前几天的事情,具体数值我忘记了)。

然而事情也仅仅到此了,再继续询问ChatGPT,他也只会车轱辘话来回说,说明他已经黔驴技穷了。


两个小时之后,运维同学说,怀疑是透明大页导致, 两个云服务商的机器,一个开了透明大页, 另一个没开。

而开了透明大页的机器刚好就是进程内存占用过多的机器。

运维同学将机器配置改完之后,重启进程果然内存变得一致了。

事实上,这次的排查我费了很大的力气,从早上9:30就开始了,一直查到11:00多。

但是翻来覆去,一直将目光锁定在操作系统之内。而没有怀疑是操作系统本身的机制引起的。

甚至都没往PAGESIZE上面想,更别说透明大页了,这次的排查经历给了我很大的警醒,这几年我太聚焦业务之上的代码分析了,已经很少会将其和内核的一些特殊机制去联想了。后期需要改进一下。

发表评论

ninety five − = 90