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