在一台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_struct
,inode
等,而这些代码一般都经过大量测试,出现问题的可能性不大。
内核IO
子系统或者驱动,比如块设备的BIO
,网络协议栈的SKB
,存储网络设备驱动。
这里最可能出现问题的地方便是存储或者网络设备的驱动,向相关研发人员询问近期内核及驱动变动情况,最终得知近期更新了X710
网卡的i40e
驱动程序,初步推断问题应该出现在网卡驱动上。
现场分析
Linux内核使用层次化内存管理的方法,每一层解决不同的问题,从下至上的关键部分如下:
物理内存管理,主要用于描述内存的布局和属性,主要有Node
、Zone
和Page
三个结构,使内存按照Page
为单位来进行管理;
Buddy
内存管理,主要解决外部碎片问题,使用get_free_pages
等函数以Page
的N
次方为单位进行申请释放;
Slab
内存管理,主要解决内部碎片问题,可以按照使用者指定的大小批量申请内存(需要先创建对象缓存池);
内核缓存对象,使用Slab预先分配一些固定大小的缓存,使用kmalloc
、vmalloc
等函数以字节为单位进行内存申请释放。
接下来,我们首先要看内存是从哪个层次上泄露的(额外说明:还有很多诸如如大页内存,页缓存,块缓存等相关内存管理技术,他们都是从这几个层次里面申请内存,不是关键,这里全部忽略掉。)。
查看Buddy内存使用情况(参考:Linux /proc/buddyinfo 理解
https://blog.csdn.net/lickylin/article/details/50726847 ):
从中我们可以看出Buddy
一共分配出去多少内存。
查看Slab
内存使用情况:
通过如上命令,我们可以确定哪些Slab
缓存占用的内存最多。
从现场数据分析,发现Buddy
分配出去了100多G的内存,Slab
只使用了几G的内存。这说明泄露的内存并不是Slab
及kmalloc
泄露出去的,是从Buddy
泄露的。Buddy
分配出去的内存可能会被Slab
,大页内存、页面缓存、块缓存、驱动,应用程序缺页映射或mmap
等需要以页为单位进行内存申请的内核代码使用。而这些部分中,最可能出现问题的依旧是驱动。
通常在高速网卡驱动中,为了实现高性能,都会直接从Buddy
中按照页的N次方为单位划分内存(大页内存也是从Buddy
里获取的,只是在向用户层映射时使用的大页表而已,这里不细区分),然后IO
映射给网卡,同时使用RingBuffer
数组或者DMA
链组成多队列。而X710
网卡是一块比较高端的网卡,应该也是具备这方面的功能的,其实现也脱离不了这些基本方法。而从Buddy
分配内存的函数主要是__get_free_pages
(不同内核版本,还有一些宏定义和变种,但都是大同小异,一定是以Pages
的N
次方分配内存,N
用参数order
输入)。
快速分析:
从下面的输出可以快速推断出,在收发数据中的确使用了直接从Buddy
分配页面函数,虽然他使用了一个”变种“函数alloc_pages_node
(这个函数肯定也是间接调用Buddy
内存分配函数,因为有order
参数,这里就不细说了)。
从下面的输出可以快速推断出,驱动使用了kmalloc
和vmalloc
函数,这都属于内核缓存对象,使用Slab
中分配出来的,而从之前的分析中可以得知Slab
是没问题的,所以这些部分理论上也不会有问题(实在想追查的话,可以实际计算出这些malloc
的内存大小,比如是100字节,那么就看上面的kmalloc-128
这个Slab
缓存是否正常即可)。
由于内存泄露一定是发生在频繁申请释放的地方,对上述疑点进行快速排查后,只有其中收发队列申请释放内存的地方是最可能出现问题的。从如下代码可见他果然没有使用内核网络协议栈的SKB
来收发数据,而是自己直接使用Buddy
的页面内存,然后映射给DMA
(这其实也是高速网卡驱动最不好写的地方之一)。
根据过往经验,再继续看下去,就得看这个网卡的Data Sheet
和Programming Guide
了,很显然短期内Intel
也没有打算开放这些资料。如果真要细心分析,难度不小,估计得一个月以上的时间,这个分析方向暂时停止。但是也基本和之前的内存分析的结论不矛盾。
接下来就去内核的Mail List
或者源码仓库上查看一番:
在驱动的发布网站上(Intel Ethernet Drivers and Utilities),他说他们修复了 Memory Leak
Bug(我们出问题的驱动是2.3.6
版本):
然后再看看这个驱动在内核Mail List
上的反馈,遇到这个问题的人很多,我们并不孤独:
至此,所有的疑点都指向这个网卡的收发队列中的内存申请释放。
由于我们无法在开发测试环境中重现问题,而并未升级网卡驱动的主机都正常。因此找了一台出现此问题的主机上把驱动降级到2.2.4
版本,其他的不变,运行两周,一切正常,问题算是被粗暴的解决了。后续可能需要继续跟进官网驱动的更新,待稳定和验证后再升级。
整个生产环境应该具备更完善的日志及监控,方便及时发现问题及故障诊断。
硬件、驱动、内核和系统等变更应该得到严格控制和验证,最好和计算组进行相应讨论和评估。
解决问题时,应该原理分析、源码分析、现场分析和测试对比等方法相结合,不要一条路走到黑。
快,关注这个公众号,一起涨姿势~