您的位置:首页 > 其它

内存碎片是否拖慢了你的程序?

2015-12-16 10:44 399 查看
现象描述:近日,公司HPC平台用户频繁反应任务无法正常运行或运行一般会停止无输出,或读取或写入数据时某些数据块耗时比正常速度高几百甚至上千倍。

针对此现象,对集群反复排查了多次,未发现任何异常,日志中也无明显报错信息,从监控系统中观察出现问题节点的CPU、内存、网络、存储等相关资源使用情况,均无太高负载,占用率都非常低。所以分析应该不是存储或硬件资源瓶颈导致的。后来将服务器系统重启后再提交任务,发现运行恢复正常。但一段时间后问题又会出现,至此问题始终未能得到根本性解决,不可能没隔一段时间就把服务器进行重启。

经过反复排查测试,使用top命令观察进程状态,发现每次任务运行中断时均会出现khugepage进程,占用一个cpu核资源。我们来了解下什么是Hugepage。

Hugepage,即大内存页

网上查询大内存页的优点,解释如下:

巨大的页面可以通过减少缺页提升性能(一次缺页分配大块的内存),还可以通过减少虚拟地址到物理地址的转换成本(减少转化次数),甚至可以避免地址转换。如果处理器必须转换一个虚拟地址,它必须通过多达四层页表。处理器保持一个“翻译后备缓冲器(TLB)的缓存的转换结果。TLB的容量很小,一般是128条指令转换、32条数据转换。非常容易被填满,从而必须进行大量的地址转换。例如:2m的页大小只需要一个tlb项,而4k的页大小就需要512个tlb项。通过内核地址使用大内存页,可以减少tlb的压力。

基于以上原因,使用大内存页的程序会运行的很快。

再来分析我们程序特点。由于GPU无法直接读取磁盘上数据,必须经过内存转换,由cpu先将数据读取到内存,GPU在读取内存中数据进行计算。和开发了解到每次任务中断的位置为读取数据或写入数据阶段,而不是计算阶段。而且任务运行的特点是将一个大文件按照固定大小的文件块,划分多次进行读取,直到全部读完。再进行GPU计算,计算过程中伴随结果数据的输出。



任务的特点是频繁进行内存申请释放操作。

ok,到这里我们再来研究内存管理算法:Buddy

Buddy算法的基本思想(来自论文)

首先,把内存中的所有页面按照 2n 划分,其中 n=0~5,对一

个内存空间按 1 个页面、2 个页面、4 个页面、8 个页面、16 个页 面、32 个页面进行六次划分,划分后形成了大小不等的存储块, 称为页面块,简称页块。 包含 1 个页面的页块称为 1 页块,包含 2 个页面的称为 2 页块,依此类推。 Linux 把物理内存划分成了 1、2、4、8、16、32 六种页块。 对于每种页面块按前后顺序两两结 合成一对 Buddy”伙伴”, 按照 1 页面划分后,0 和 1 页、2 和 3 页 …是 1 页块 Buddy。 按照 2 页面划分,0-1 和 2-3、4-5 和 6-7 …是 2 页块 Buddy。

Linux 把空闲的页面按照页块大小分组进行管理, 用数组 free_area[]来管理各个空闲页块组。 在/mm/page_alloc.c 中定义如 下:

#define NR_MEM_LISTS 6
static struct free_area_struct free_area[NR_MEM_LISTS];
struct free_area_struct { struct page *next; struct page *prev; unsigned int * map;
};


当进程提出存储请求时,系统按照 Buddy 算法,根据请求的

页面数在 free_area[]对应的空闲页块组中搜索。 linux 并不是按 照要求的页块的数目去进行分配,而是将大于、等于这个数目的 最小 2n 个页块分配出去。比如,要求 3 个页块,就分配 22=4 块; 要求 16 个页块,就分配 24=16 块,如此等等。 因此,系统总是按 照进程所需连续存储块的申请数量, 到空闲区队列 free_area 中 能够满足要求的最小空闲区队列里查找。 当队列不空时,就把第 一个空闲区分配出去。 如果该队列为空,那么就继续查找下面的 队列(其空闲区的尺寸为上一个队列的 2 倍)。 当它里面有空闲 区时,就把该空闲区一分为二:一个分配出去给进程使用;余下 的一半,排到它上面的空闲区队列中去。

在内存页面释放时,系统将做为空闲页面看待。 然后检查 是否存在与这些页面相邻的其它空闲页块, 若存在, 则合为一 个连续的空闲区按 Buddy 算法重新分组。

2、用 Buddy 算法分配和释放空闲区

举个例子:比如系统的可分配内存总量是 1MB。 现在有一 个存储申请、 释放序列: 进程 A 申请 100KB, 进程 B 申请 256KB, 进程C申请64KB, 进程D申请256KB, 进程B释放 256KB, 进程A释放100KB, 进程E申请75KB, 进程C释放 64KB,进程 E 释放 75KB,进程 D 释放 256KB。

那么,它的过程如下图所示:

第一个请求是进程 A 需要 100KB。 按照 2 的幂次计算,应 该分配给它 128KB 的存储量。于是,先把 1MB 的区域一分为二, 划分出两个 512KB 的伙伴, 再把其中的第一个划分成两个 256KB 的伙伴,最后把这两个中的第一个划分成两个 128KB 的

伙伴。 至此,将伙伴中的第一个分配给进程 A,如图(b)所示。 接 着是进程 B 申请 256KB。由于已经有这样的存储区存在,就直接 分配给它, 如图 (c) 所示。 随之进程 C 申请 64KB, 就把原先 128KB 空闲区一分为二,出现两个 64KB 的区域,并把第一个分 配出去,如图(d)所示。 图(e)表示分配给进程 D 一个 256KB 的 区域。 图(f)表示进程 B 释放了它所占据的 256KB 区域。 这时要 注意,虽然该区域前面有一个 64KB 的空闲区,但因为他们不是 伙伴,因此不能将它们合并成一个大的空闲区。 图(g)表示进程 A 释放了它所占据的 128KB。 图(h)表示将一个 128KB 的区域 分配给进程 E。 图(i)表示进程 C 释放 64KB。 这时它的伙伴是空 闲的,所以将它们两个合并成一个 128KB 的大区域。 图(j)表示 进程 E 释放 128KB,根据伙伴原则,这个释放将引起一连串的空 闲区合并,即释放的 128KB 和它的伙伴合并成一个 256KB 的空 闲区; 两个 256KB 的空闲区又合并成一个大的 512KB 的空闲 区。 最后,进程 D 释放 256KB,又通过一连串的合并,整个区域 恢复呈现一个大的、1MB 的空闲区。

虽然伙伴系统的让内存管理变得简单,但是会造成另外一个问题,就是内存碎片。内存碎片是非常重要的问题因为如果长期占用不同大小的内存产生了内存碎片,则在申请内存的时候会引发缺页异常,但是实际上,依旧有很多的空闲的内存可供使用。



如上图所示,上图有非常多的剩余空间,但因为内存碎片的问题,当系统需要申请4个内存页的时候,就无法申请内存了,虽然我们的内存空间里剩余内存的数量远远大于4个内存页,但无法找到连续的4个内存页,则会产生一个缺页异常。

而对于理想情况下的内存而言,应该如下图中的图例所示,对于一个程序,在其可用的内存地址空间内,不应该存在大片的不连续的内存,应该说,对于应用程序看到的内存区而言,总应该是连续的。

我们谈到内存碎片的时候,大多数只涉及到内核,因为对于内核而言,内存碎片确实是一个非常大的问题。虽然大多数现代的CPU都提供了巨型的页使用,但解决内存碎片依旧对内存使用密集型的应用程序有好处。在使用更大的页的时候,地址转换后备缓冲器只需处理较少的项,降低了TLB缓存失效的可能性。但巨型页的分配依旧需要连续的空闲物理内存。

以上科普了一下内存算法和内存碎片的问题。了解了这些,基本也就明白了平台中出现的问题原因了。

最直接的解决办法,关闭Hugepage:

echo no > /sys/kernel/mm/redhat_transparent_hugepage/khugepaged/defrag
echo never > /sys/kernel/mm/redhat_transparent_hugepage/defrag


再进程测试,任务可正常运行了,但比较hugepage可提供程序运行效率,还需进一步测试一下关闭该功能后是否会降低程序运行效率。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: