排查内存使用(泄露、耗尽)问题的一个的技巧是,区分“批发商”和“零售商”这两类不同的内存管理机制。这里的“批发商”,指的是按页面管理并分配内存的机制。而“零售商”,则是指从“批发商”那里批量获取资源,并以字节为单位,管理和分配内存的机制。
“零售商”和“批发商”的区分很重要。这是因为通过“零售商”分配出去的内存资源,在“批发商”那里或多或少都有统计。但是从“批发商”那边分配出去的内存资源,“零售商”几乎一无所知。凡是一些诡异的,“我的内存去哪里了”的问题,往往都跟这个有点有关系。
“批发商”“零售商”的例子很多,比如Windows上的一对接口VirtualAlloc和HeapAlloc。而在Linux内核中,buddy system毋庸置疑是最大内存的“批发商”,而slab则是最常用的“零售商”。今天这篇文章,我跟大家分享一个和“零售商”slab有关的真实案例。
云监控是一个好产品
今天的案例,我们从云监控说起。云监控作为一个监控工具,给客户提供了非常丰富的功能。阿里云的客户可以使用云监控来随时了解云服务的状态,而且在云服务出现异常的时候,客户可以及时地收到来自云监控的报警信息。
做为阿里云技术支持的同学,可以说我们对云监控有着一种非常“特殊”的感情。这是因为,很多时候客户提给我们的问题,会以一个云监控的截图开始,看到云监控的截图,那基本就等于我们有新的问题需要处理了。
以上的截图是云监控通过钉钉消息发送给客户的报警信息。从这个截图里来看,客户的一台ECS实例内存使用率超过了90%,而且持续了1分钟时间。这对ECS服务器来说,是相当不健康的状态。
基本套路
处理内存使用问题,有一些基本套路。首先,我们可以使用free或top命令看一下系统总体内存使用情况,如物理内存大小,有多少剩余内存,有多少被cache(这里的cache和slab机制的cache是两回事)和buff用掉了;其次,我们可以使用ps命令看看每个进程内存的使用情况;如果到这一步还没有定位到问题,进一步,我们可以通过查看/proc/meminfo文件,在更细的粒度上了解内存资源的分布与分配情况。按照这个套路打下来,一般我们都会得到一个初步的排查结果。以今天这个问题为例,系统32G内存几乎被用光了,但是cache和buff并不大,进程使用的内存也不多。最终在meminfo文件中,我发现有大量内存被slab用掉了。
零售商slab
Slab应该算是Linux内存管理机制中,最著名的“零售商”机制。写slab的文章非常多,我这里只做简单的介绍。
做为“零售商”,slab肯定需要解决两个问题,一个是怎么样从“批发商”buddy system那里“批发”内存,另外一个是怎么样管理并把这些内存“散卖”出去。为了回答这两个问题,我们首先理解一下slab机制的数据结构。Slab机制包括三个层次的数据结构:cache,slab和object。如下图,一个slab一般是一个4K大小的内存页面。当我们把slab页面,按照固定大小切分成能够被“零售”的小块的时候,我们就得到了很多的object。当把包含相同大小object的slab用“箭头”串起来的时候,就形成了cache。这里slab和object是比较容易理解的:因为把4K的页面分配给几个字节的对象使用太浪费内存了,所以就需要切开来“散卖”。而cache的存在意义其实是比较隐晦的。Cache存在的意义,其实是为了动态的管理slab,让slab可以动态的增加和减少。在object需求量大的时候,可以从“批发商”那里拿到很多的slab放进cache里,而在用完之后,又可以动态地把slab退回去。
基于cache, slab和object数据结构,slab机制要解决上边那两个问题就变得很容易了:一方面,slab机制从“批发商”buddy system那里,以slab(页面)为单位获取内存;另外一方面,slab机制以object为基本管理单元,把内存资源“零售”给系统中其他模块。
Slabtop是一个可以用来查看slab内存使用情况的工具。下边这个截图来自客户问题现场。我们可以根据cache size这一列,快速地定位到问题。很明显cache size最大的一行对应的对象是dentry。
注意:为了解释简单,这里我假设slab是一个页面,也就是4K大小。这个假设意味着,所有被管理的对象都是小于4K的。但在实际情况中,为了提供大于4K的内存块,一个slab其实可以是多个连续的物理页面。
小对象搞出大事情
目录项dentry算是一个非常小的内核数据结构。从上边的截图我们可以看到,dentry其中只有0.19K。虽然dentry很小,但是这个结构是内核中最常使用的数据结构。如下图,当进程打开文件的时候,一般都会有对应的dentry被分配出来。在Linux这样的“一切皆文件”的操作系统里,dentry结构被使用频率是不言自明的。
因为已经定位到了内核数据结构dentry,所以我选择抓一个dump来慢慢分析这个问题。抓了dump之后让客户重启机器释放内存,以免导致更严重的后果,影响业务。
Core dump掘金
使用core dump排查问题有两个优势:一是在生产环境中,客户有时候真的很难给我们机会去使用一些trace和工具一步一步的排查问题,而抓dump对业务的影响较小,抓完之后可以慢慢分析。二是core dump里包含了系统中所有正常、不正常的状态。数据量大且完整,这不是一般的数据收集方法可以比的。下边我演示一遍怎么用core dump来排查slab泄露问题。
系统基本信息
使用sys命令,我们可以快速看一下客户这台机器的基本信息。如机器名,内核版本,内存大小等。
内存概要
因为这个问题是内存使用问题,我们可以用kmem -i命令在dump里看一下内存的使用情况。我们可以看到,这个系统有32G内存,其中slab用掉27G。
Cache
接下来我们看一下slab的使用情况。命令kmem -s可以帮我们列出内核中所有的slab cache。如下图,每一行对应一个cache,每个cache负责管理一个对象,第二列是对象的名字。
Slab
既然已经发现了问题是由dentry对象引起的,那么,我们可以到slab结构内部去查看泄露的的dentry。命令kmem -S dentry可以帮我们列出包含dentry的所有slab。可以对照“零售商slab”一节第一个插图来看下边的截图。前两行给出了cache的一些基本信息,如地址,object名字,object大小等。从第三行开始,这个命令分块输出cache中所有的slab结构。每个slab里包含一列小对象,这些对象都在一个页面内部,从他们连续的地址可以看出这点。实际上这个系统分配了7百万slab页面给dentry,下图只截了前两个。
小对象dentry
这个时候问题来了。这个cache里有7百万slab,有1亿4千万dentry项,我们怎么去分析这些项呢?其实有一个很简单的技巧,就是随机挑选的一些dentry,然后看看这些dentry项里,有没有什么共同的特征,比如相同的字符串,或者共同引用的地址等。对这个问题来说,我是很幸运的。我发现在随机挑选的dentry结构里,d_iname这个字符串都包含一个子串“dOeSnotExist_.db”。
使用dOeSnotExist_.db这个子串,我们可以在公网上轻而易举地定位到下边这个问题:这是nss-softokn软件包的一个Bug。
后记
技术支持工作的一个创新点,也是这个工种的一大乐趣,是可以利用各种组合技,利用不同的方法,巧妙地组合并找出问题的答案。以今天这个问题为例,我们开始使用了一些基础的排查工具对问题进行了初步的定位;进而我们使用了core dump这种看起来不那么易用的工具,从内存中挖出了一个字符串作为线索;最后结合公网上的信息,我们拼出了这个问题的答案。