LWN:内核中进行更积极地内存整理(compaction)

关注了就能看到更多这么棒的文章哦~

Proactive compaction for the kernel

April 21, 2020

This article was contributed by Nitin Gupta

原文来自:https://lwn.net/Articles/817905/

主译:DeepL

许多应用场景中,如果使用huge page的话都可以看到明显的性能提升。然而,huge-page allocation往往会导致很大延迟,甚至在碎片化的内存条件下会分配失败。主动在后台进行内存压缩(memory compaction)可能为这些问题提供有效解决方案。使用了我(作者Nitin Gupta,下同)提出的proactive compaction实现方案后,一般来说huge-page allocation的延迟可以减少到原来的1/70 ~1/80,CPU开销只会稍微增长一点。

Memory compaction是Linux内核的一个功能,它可以提供更大的、物理连续的可用内存块。目前,内核使用的是on-demand compaction scheme,即按需压缩方案。每当per-node的kcompactd线程被唤醒时,它就会压缩一些内存只能能提供满足用户申请size的一个page。一旦有了这个大小的page可用,线程就会回到睡眠状态。这种compaction模式往往会导致higher-order(order是指用2的指数次来代表的page size)分配的延迟,对于需要突发分配大量huge page的场景来说,这种compaction模式会损害性能。

在系统上手动触发compaction来做了些实验,结果表明,对于一个32GB的系统来说,可以在1秒内将内存整理成为一个相当紧凑(highly compacted)的内存状态。这说明,内核中的proactive compaction主动压缩方案可以在保持较低的分配延迟的同时,将相当大比例的内存分配成huge page。

我最近的patch实现了一个proactive compaction方案。它对user space开放了唯一一个供调整的参数: /sys/kernel/mm/compaction/proactiveness,它接受的值范围是[0,100],默认值是20。这个值决定了内核在后台compact内存的积极性。这个patch重用了原来就有的per-NUMA-node kcompactd线程来进行后台压缩。这些线程中每一个都会周期性地计算出一个per-node的碎片化分数(作为内存碎片化程度的指标),并将其与阈值进行比较,这个high/low阈值就来自上面提到的用户可调的proactiveness值。

当node中的碎片化评分超过high threshold时,每个node的kcompactd线程就会开始发起(后台)proactive compact动作。compact过程一直持续进行,直到node的碎片化评分低于low threshold,或者满足其中一个back-off条件(让步条件,后面有定义)。

memory compaction涉及到将 "movable" page移动到整个区域的尾端,从而在区域的起始处可以创建更大的、物理连续的、空闲的区域。如果有 "unmovable"页面,如kernel allocation的页面其实分布在整个物理地址空间中到处都是,那么这个流程的效果就比较差。由于属于不同进程的page会被到处移动,因此compaction会对整个系统带来很大影响,这也会导致在无辜应用中出现高延迟。因此,内核不能过度compact,应该有一个良好的back-off策略。

patch在以下几种情况下会进行让步:

  • 当前node的kswapd线程处于活动状态时(为了避免干扰reclaim流程)。

  • per-node lru_lock或per-zone lock有竞争时(为了避免伤害那些非后台、对延迟敏感的进程)。

  • 当一轮compaction完成后没有任何进展时(即碎片化评分不减少)

如果达成其中任何一个让步条件,proactive compaction过程将被延后到一定时间之后再进行。

Per-node fragmentation score and threshold calculation

如前所述,这种proactive compaction方案是由一个叫做proactiveness的sysfs节点来调优的。所有需要的值,如每个node的碎片化程度评分值,还有threshold阈值,都是从这个proactiveness值得出的。

node的得分在[0,100]范围内,定义为这个node中所有的zone的评分之和。其中zone score(Sz)定义为

    Sz = (Nz / Nn) * extfrag(zone, HUGETLB_PAGE_ORDER)

其中:

  • Nz是该zone的page数。

  • Nn是该zone的父node中的page总数。

  • extfrag(zone, HUGETLB_PAGE_ORDER)是这个zone中的huge-page order的external fragmentation。

一般来说,针对任意order计算external fragmentation的共识如下:

    extfrag(zone, order) =  ((Fz - Hz) / Fz) * 100

其中:

  • Fz是该zone的free page总数。

  • Hz是符合2的order次方size连续block的所有free page总数。

这个per-zone score的范围是[0,100],当Fz = 0时,这个得分定义为0。我之所以采用这种方式来计算per-zone score,主要是为了避免浪费时间来compact类似ZONE_DMA32这样的特殊zone,而专注于在像ZONE_NORMAL这样的zone,因为这些zone管理着大部分的内存(Nz ≈Nn)。对于较小的zone来说(Nz <

这些得分都需要和low threshold(Tl)以及high threshold(Th)进行比较,这两者定义如下:

    Tl = 100 - proactiveness
    Th = min(10 + Tl, 100)

threshold也在[0,100]范围内。一旦一个zone的得分超过Th,就会进行proactive compaction,直到得分降到Tl以下。

Performance evaluation

要想真正验证memory compaction效率,第一步是对打碎系统memory,使系统内存没有huge page可以直接分配出来(即所有NUMA node的初始分数都弄到100)。系统处于这样的内存碎片化状态下,任何很依赖huge-page的workload都会凸显出内核compaction policy效果如何。在on-demand compaction的情况下,大部分的huge page分配都会进入direct-compaction path,就会看到很高的延迟。而使用proactive compaction期望能避免这些延迟,除了在compaction过程都跟不上huge-page allocation速度这种极端情况。

为了评估proactive compaction,我通过一个用户空间程序触发了很严重的external fragmentation(定义见上文),该程序几乎将所有内存都分配为huge page,然后从每个huge page对齐的位置来free了75%的page。所有测试都是在x86_64系统上进行的,该系统拥有1TB的RAM和两个NUMA node,使用内核版本5.6.0-rc4。首先测试的非常依赖huge-page的workload是一个test kernel driver,它会尽可能多地分配huge page,测量每次分配的延迟,直到分配失败为止。不管有没有我的patch,该驱动都能将几乎98%的可用内存分配成huge page。然而,使用原生内核(没有我的proactive compaction patch),95%分位的延迟是33799µs,而使用我的patch(其中proactiveness调整为20),这个延迟是429µs--减少到了1/78。

为了进一步衡量性能,我们使用了一个Java heap allocation测试,在按上述方法对内存进行强制碎片化处理后,Java heep allocation分配到了700GB的heap space。这个workload显示了减少huge page分配延迟的效果,在这种很多huge page的workload情况下。在vanilla(原生)内核上,运行时间约为27分钟,而打了patch(proactiveness=20)后,运行时间下降到大约4分钟。使用更高的proactiveness值的测试没有显示出进一步的加速或减速效果。

Vlastimil Babka提出了一些问题,他也一直都在做proactive compaction工作,并帮助review了一些patch:

按照你的描述,似乎首先是一次性的进行强制碎片化,然后是一次性的持续分配动作?kcompactd可能只做了一次proactive compact?这可以作为一个smoke test,但我认为更重要的是在更复杂的workload下行为如何,我们也应该检查vmstat compact_daemon*这些统计信息,可能还要关注kcompactd kthreads的CPU利用率。

看到这个评论后,我进一步调查了kcompactd的proactive情况下的行为。Java heap测试程序运行过程中记录了每个node的碎片化分数,以及kcompactd线程的CPU使用率。数据清楚表明,在测试程序的整个运行过程中,proactive compaction一直在起作用,并且一个kcompactd线程在活跃时100%占用了一个CPU core。

LWN:内核中进行更积极地内存整理(compaction)_第1张图片

上图显示了一个Java进程试图在一个two-node系统上分配700GB的heap区域时,每个node上碎片化评分的变化。proactiveness值设置为20,所以低阈值和高阈值分别为80和90。在测试程序运行前,系统内存是碎片化的,没有huge page可以直接分配。从Node-0开始进行heap分配,随着huge page的使用,得分也随之上升。当分数达到90分以上时,节点上会触发主动压缩,将分数拉回80分。最终,Node-0上的所有内存都用完了(运行约90秒后),此时分配从Node-1开始,同样的compaction模式重复进行。

如前所述,compaction是一个很耗时的操作,除非能够减少external fragmentation,否则我们不想付出这样的代价。为了评估back-off行为,我们创建了另一个测试,其中unmovable page分散在整个物理地址空间中。在系统处于这样的内存状态下,compaction除了让CPU发热之外,并不能起到什么作用。这个patch中实现的back-off机制可以正确识别这种情况,一轮compaction后,它看到node的得分没有下降,从而检测出来需要back-off了。当这种情况发生时,进一步的proactive compaction将被推迟一段时间。

Future work

这个补丁已经经历了几轮review,这在很多方面帮助它完善。一些内核开发者表示确实需要这样更proactive的compaction。鉴于我们测试得到的这些非常漂亮的数据,再经过几次迭代之后这个patch很有希望被接受,。

作为未来的一个方向,我正在关注如何细化per-node fragmentation score以及threshold计算,这些数字会影响background proactive compaction过程。目前,score计算时没有考虑每个node的特性,比如不同的TLB延迟,这在异构系统中会很重要。未来的patch可能会在得分和阈值计算中添加scaling factor来进行缩放,以考虑到每个node自己的特性。

全文完

LWN文章遵循CC BY-SA 4.0许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注LWN深度文章以及开源社区的各种新近言论~

你可能感兴趣的:(LWN:内核中进行更积极地内存整理(compaction))