Linux 内核 VS 内存碎片 (下)

Stella981
• 阅读 625

Linux 内核 VS 内存碎片 (上) 我们可以看到根据迁移类型进行分组只是延缓了内存碎片,而并不是从根本解决,所以随着时间的推移,当内存碎片过多,无法满足连续物理内存需求时,将会引起性能问题。因此仅仅依靠此功能还不够,所以内核又引入了内存规整等功能。

内存规整

在内存规整引入之前,内核还使用过 lumpy reclaim 来进行反碎片化,但在我们当前最常用的 3.10 版本内核上已经不存在了,所以不做介绍,感兴趣的朋友请从文章开头整理的列表中自取,我们来看内存规整。

内存规整的算法思想在 Memory compaction 有详细描述,简单来说是从选中的 zone 底部扫描已分配的迁移类型为 MIGRATE_MOVABLE 的页面,再从此 zone 的顶部扫描空闲页面,把底部的可移动页移到顶部的空闲页,在底部形成连续的空闲页。

选中的碎片化 zone:

Linux 内核 VS 内存碎片 (下)

扫描可移动的页面:

Linux 内核 VS 内存碎片 (下)

扫描空闲的页面:

Linux 内核 VS 内存碎片 (下)

规整完成:

Linux 内核 VS 内存碎片 (下)

看上去原理比较简单,内核还提供了手动规整的接口:/proc/sys/vm/compact_memory,但实际上如前言所说(至少对我们最常用的 3.10 版本内核)无论是手动还是自动触发,内存规整并不好用,其开销过大,反而成为性能瓶颈:Memory compaction issues。不过社区并没有放弃这一功能,而是选择不断完善,比如在 v 4.6 版本引入 mm, compaction: introduce kcompactd,v4.8 版本引入 make direct compaction more deterministic 等。

对于 3.10 版本内核,内存规整的时机如下:

  1. 在分配高阶内存失败后 kswapd 线程平衡 zone;

  2. 直接内存回收来满足高阶内存需求,包括 THP 缺页异常处理路径;

  3. khugepaged 内核线程尝试 collapse 一个大页;

  4. 通过 /proc 接口手动触发内存规整;

其中和 THP 有关的路径,我在上一篇文章 我们为什么要禁用 THP 有提到其危害并建议大家关闭了,所以在这里不对 THP 路径做分析,主要关注内存分配路径:

Linux 内核 VS 内存碎片 (下)

基本流程:当申请分配页的时候,如果无法从伙伴系统的 freelist 中获得页面,则进入慢速内存分配路径,率先使用低水位线尝试分配,若失败,则说明内存稍有不足,页分配器会唤醒 kswapd 线程异步回收页,然后再尝试使用最低水位线分配页。如果分配失败,说明剩余内存严重不足,会先执行异步的内存规整,若异步规整后仍无法分配页面,则执行直接内存回收,或回收的页面数量仍不满足需求,则进行直接内存规整,若直接内存回收一个页面都未收到,则调用 oom killer 回收内存。

上述流程只是简化后的描述,实际工作流程会复杂的多,根据不同的申请的阶数和分配标志位,上述流程会有些许变化,为避免大家陷入细节,我们在本文不做展开。为方便大家定量分析直接内存回收和内存规整为每个参与的线程带来的延迟,我在 BCC 项目中提交了两个工具:drsnoopcompactsnoop,这两个工具的文档写得很详细了,但在分析时需要注意一点,为了降低 BPF 引入的开销,这里抓取的每一次对应事件的延迟,因此和申请内存的事件相比,可能存在多对一的关系,对于 3.10 这样的老内核,在一次慢速内存分配过程中会重试多少次是不确定的, 导致 oom killer 要么太早的出场,那么太晚,导致服务器上绝大部分任务长期处于 hung up 状态。 内核在 4.12 版本合入 mm: fix 100% CPU kswapd busyloop on unreclaimable nodes 限定了直接内存回收的最大次数。我们按这个最大次数 16 来看,假设平均一次直接内存回收的延迟是 10ms (对于现在百G内存的服务器来说,shrink active/inactive lru 链表是很耗时的,如果需要等待回写脏页还会有额外的延迟)。所以当某个线程在通过页面分配器申请页面时,只执行一次直接内存回收就回收了足够的内存,那么这次分配内存的增加延迟就增加了 10ms,若重试了 16 次才回收到足够的页面,则增加的延迟就不再是 10ms 而是 160 ms 了。

我们回过来看内存规整,内存规整核心逻辑主要分为 4 步:

  1. 判断内存 zone 是否适合进行内存规整;

  2. 设置扫描的起始页帧号;

  3. 隔离 MIGRATE_MOVABLE 属性页面;

  4. 迁移 MIGRATE_MOVABLE 属性页面到 zone 顶部;

执行一次迁移后,若还需要继续规整,则循环执行 3 4 直到规整完成。因此会消耗大量的 CPU 资源,从监控上经常看到 sys cpu 被打满。

页面迁移也是一个大话题,除了内存规整外,还有其他场景也会使用内存迁移,因此我们不在此展开。我们看下如何判断 zone 是否适合进行内存规整:

  1. 对于通过 /proc 接口强制要求规整的情况来说,没啥说的,服从命令听指挥;

  2. 利用申请的阶数判断 zone 是否有足够剩余内存可供规整 (不同版本内核算法细节上有差异),计算碎片指数,当指数趋近 0 则表示内存分配将因内存不足而失败,所以此时不宜做内存规整而是做内存回收。当指数趋近 1000 时则表示内存分配将因外部碎片过多导致失败,所以不适合做内存回收而是做内存规整,在这里规整和回收的分界线由外部碎片阈值决定:/proc/sys/vm/extfrag_threshold;

内核开发者也为我们提供观察内存指数的接口:

通过执行 cat /sys/kernel/debug/extfrag/extfrag_index 可以观测到 (这里存在小数点是因为除了 1000)。

Linux 内核 VS 内存碎片 (下)

结语

本文简述了为什么外部内存碎片会引起性能问题,以及社区多年来在反碎片化方面做的努力,重点介绍了 3.10 版本内核反碎片的原理和定量、定性观察方法。期待对大家有所帮助。

在描述内存规整的时候捎带提到了直接内存回收的原因是,直接内存回收不仅会出现在内存严重不足的情况,在真正的场景中也会内存碎片原因导致触发内存直接回收,二者在一段时间内可能是混合出现的。

本文同时也介绍了基于 /proc 文件系统的监控接口和基于内核事件的工具,二者相辅相成,基于 /proc 的监控接口用起来简单,但存在无法定量分析和采样周期过大等问题,基于内核事件的工具可以解决这些问题,但需要对内核相关子系统的工作原理要有一定理解,对客户的内核版本也有一定要求。

对于如何减少直接内存回收出现的频率以及出现碎片问题后如何缓解,我的想法是对于需要大量操作 IO 的 workload 场景,由于内核在设计上照顾慢速后端设备,比如在 lru 算法的基础上实现二次机会法、Refault Distance 等,且没有提供限制 page cache 占比的能力 (一些公司为自己的内核定制了此功能并尝试过提交给上游内核社区,但上游社区一直没有接受,个人觉得可能存在导致 workingset refault 等问题)。所以对于超过百 G 大内存机器的场景,提高 vm.min_free_kbytes 变相限制 page cache 占比是个比较好的选择 (最高不要超过总内存的 5%)。虽然调大 vm.min_free_kbytes 确实会导致一些内存浪费,不过对于 256G 内存的服务器来说,我们设置成 4G,也只占了 1.5%。社区显然也注意到了这点,在 4.6 版本的内核合并了 mm: scale kswapd watermarks in proportion to memory 对此进行了优化。另外一个方法是在适当的时机执行 drop cache,但可能会给业务带来较大的抖动。

期待大家的交流与反馈!

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解2016年09月02日00:00:36 \牧野(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fme.csdn.net%2Fdcrmg) 阅读数:59593
Stella981 Stella981
2年前
Linux 内核 VS 内存碎片 (上)
(外部)内存碎片是一个历史悠久的Linux内核编程问题,随着系统的运行,页面被分配给各种任务,随着时间的推移内存会逐步碎片化,最终正常运行时间较长的繁忙系统可能只有很少的物理页面是连续的。由于Linux内核支持虚拟内存管理,物理内存碎片通常不是问题,因为在页表的帮助下,物理上分散的内存在虚拟地址空间仍然是连续的(除非使用大页),但对于需要从内核线性
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这