cover_image

一次Linux内核内存泄露实例分析

程沛 gh_df02ebc3757a
2018年03月29日 04:24

问题说明

在一台CEPH存储节点上,随着运行时间的增加,可用内存越来越少。在应用程序全部退出后,释放全部缓存,可用内存依旧没有增加。重启节点后,所有内存占用恢复正常,运行一段时间后(约一周)又会出现相同情况。另外,这个问题在我们搭建的自测服务器上无法重现,只能凭借分析生产环境数据来进行无侵入性的诊断。(额外说明:由于现场调试数据并未保存,下面命令显示的数据仅供演示!)


问题确认

  • 统计所有应用程序占用的内存 U (单位是K,参考:怎样统计所有进程总共占用多少内存? http://linuxperf.com/?p=143 ):

图片

  • 查看系统内存总数 T 、空闲内存 F , 共享内存S ,缓存 C (参考: Linux 内存查看方法meminfo\maps\smaps\status 文件解析 

    https://www.cnblogs.com/jiayy/p/3458076.html ):

    图片

图片

  • 正常情况下,应该有如下公式(K 为内核占用内存,这里忽略掉Swap内存):

图片由此可以计算出内核使用的内存 K,结果发现内核占用内存异常之大。整个故障节点有128G内存,内核占用了100多G,因此可以初步推断,内核发生了内存泄露。



原理分析



根据经验,一般内存泄露并耗尽内存的代码,一定是频繁申请释放内存的部分。

内核中可能会出现频繁申请释放的内存可能有:

  • 内核管理数据结构,如task_structinode等,而这些代码一般都经过大量测试,出现问题的可能性不大。

  • 内核IO子系统或者驱动,比如块设备的BIO,网络协议栈的SKB,存储网络设备驱动。

这里最可能出现问题的地方便是存储或者网络设备的驱动,向相关研发人员询问近期内核及驱动变动情况,最终得知近期更新了X710网卡的i40e驱动程序,初步推断问题应该出现在网卡驱动上。


分析


Linux内核使用层次化内存管理的方法,每一层解决不同的问题,从下至上的关键部分如下:

  • 物理内存管理,主要用于描述内存的布局和属性,主要有NodeZonePage三个结构,使内存按照Page为单位来进行管理;

  • Buddy内存管理,主要解决外部碎片问题,使用get_free_pages等函数以PageN次方为单位进行申请释放;

  • Slab内存管理,主要解决内部碎片问题,可以按照使用者指定的大小批量申请内存(需要先创建对象缓存池);

  • 内核缓存对象,使用Slab预先分配一些固定大小的缓存,使用kmallocvmalloc等函数以字节为单位进行内存申请释放。

接下来,我们首先要看内存是从哪个层次上泄露的(额外说明:还有很多诸如如大页内存,页缓存,块缓存等相关内存管理技术,他们都是从这几个层次里面申请内存,不是关键,这里全部忽略掉。)。

  • 查看Buddy内存使用情况(参考:Linux /proc/buddyinfo 理解  

    https://blog.csdn.net/lickylin/article/details/50726847 ):

图片


从中我们可以看出Buddy一共分配出去多少内存。

  • 查看Slab内存使用情况:

图片


通过如上命令,我们可以确定哪些Slab缓存占用的内存最多。

从现场数据分析,发现Buddy分配出去了100多G的内存,Slab只使用了几G的内存。这说明泄露的内存并不是Slabkmalloc泄露出去的,是从Buddy泄露的。Buddy分配出去的内存可能会被Slab,大页内存、页面缓存、块缓存、驱动,应用程序缺页映射或mmap等需要以页为单位进行内存申请的内核代码使用。而这些部分中,最可能出现问题的依旧是驱动。


源码分析

通常在高速网卡驱动中,为了实现高性能,都会直接从Buddy中按照页的N次方为单位划分内存(大页内存也是从Buddy里获取的,只是在向用户层映射时使用的大页表而已,这里不细区分),然后IO映射给网卡,同时使用RingBuffer数组或者DMA链组成多队列。而X710网卡是一块比较高端的网卡,应该也是具备这方面的功能的,其实现也脱离不了这些基本方法。而从Buddy分配内存的函数主要是__get_free_pages(不同内核版本,还有一些宏定义和变种,但都是大同小异,一定是以PagesN次方分配内存,N用参数order输入)。

快速分析:

  • 从下面的输出可以快速推断出,在收发数据中的确使用了直接从Buddy分配页面函数,虽然他使用了一个”变种“函数alloc_pages_node(这个函数肯定也是间接调用Buddy内存分配函数,因为有order参数,这里就不细说了)。

图片

  • 从下面的输出可以快速推断出,驱动使用了kmallocvmalloc函数,这都属于内核缓存对象,使用Slab中分配出来的,而从之前的分析中可以得知Slab是没问题的,所以这些部分理论上也不会有问题(实在想追查的话,可以实际计算出这些malloc的内存大小,比如是100字节,那么就看上面的kmalloc-128这个Slab缓存是否正常即可)。

图片

  • 由于内存泄露一定是发生在频繁申请释放的地方,对上述疑点进行快速排查后,只有其中收发队列申请释放内存的地方是最可能出现问题的。从如下代码可见他果然没有使用内核网络协议栈的SKB来收发数据,而是自己直接使用Buddy的页面内存,然后映射给DMA(这其实也是高速网卡驱动最不好写的地方之一)。

图片图片

根据过往经验,再继续看下去,就得看这个网卡的Data SheetProgramming Guide了,很显然短期内Intel也没有打算开放这些资料。如果真要细心分析,难度不小,估计得一个月以上的时间,这个分析方向暂时停止。但是也基本和之前的内存分析的结论不矛盾。


资料查找

接下来就去内核的Mail List或者源码仓库上查看一番:

  • 在驱动的发布网站上(Intel Ethernet Drivers and Utilities),他说他们修复了 Memory Leak Bug(我们出问题的驱动是2.3.6版本):

图片图片图片图片图片

  • 然后再看看这个驱动在内核Mail List上的反馈,遇到这个问题的人很多,我们并不孤独:

图片

至此,所有的疑点都指向这个网卡的收发队列中的内存申请释放。


问题验证

由于我们无法在开发测试环境中重现问题,而并未升级网卡驱动的主机都正常。因此找了一台出现此问题的主机上把驱动降级到2.2.4版本,其他的不变,运行两周,一切正常,问题算是被粗暴的解决了。后续可能需要继续跟进官网驱动的更新,待稳定和验证后再升级。


总结建议

  • 整个生产环境应该具备更完善的日志及监控,方便及时发现问题及故障诊断。

  • 硬件、驱动、内核和系统等变更应该得到严格控制和验证,最好和计算组进行相应讨论和评估。

  • 解决问题时,应该原理分析、源码分析、现场分析和测试对比等方法相结合,不要一条路走到黑。



快,关注这个公众号,一起涨姿势~

图片
图片




继续滑动看下一个
未命名账号
向上滑动看下一个