首页 >> 大全

linux内核异步内存回收的另一个思路:基于冷热文件的冷热区域精准的回收冷文件页

2023-09-19 大全 36 作者:考证青年

本文介绍的针对文件缓存的内存回收方案与现有的思路有很大不同:内存回收的单位是一个个文件,再把文件的分成一个个小区域(或者叫小单元),一个区域由4个索引连续的文件页page组成,比如把索引是0~3的文件页page组成一个区域,索引是4~7的文件页page再组成一个区域,其他区域类推。然后,提前判断出文件的哪些区域是进程频繁访问的(即热区域,就是该区域的文件页page频繁被读写),哪些区域是进程很少访问的(即冷区域,就是该区域的文件页page很少被读写)。异步内存回收线程工作时,一个个遍历指定数目的文件,再把每个文件的冷区域找出来,最后回收掉该冷区域对应的文件页page。

当然,会首先遍历产生数量很多的文件,因为测试证明能从这种文件回收掉更多的冷文件页page,内存回收效率比较高效!还有一点,对于内存回收中让人头疼的问题,本方案是准确识别出 page,单独处理,保证在一段时间内不再回收这种page。最后,还有很多其他改进点,下文一一介绍。

该方案已做成内核ko,不用编译内核,目前已经在红帽8和9系列的 8.3(内核版本4.18.0-240)和rocky 9.2(内核版本5.14.0-284.11.1)实现异步内存回收功能(其他内核发行版,如阿里龙蜥OS、腾讯 OS、高内核版本的安卓手机等内核,理论上适配后也可以不修改编译内核的前提下使用该内核ko)。源码见 - -stack/: linux内核内存回收的另一个思路探索——基于冷热文件的冷热区域而更精准的回收冷page 。

需要说明一下,近期火热的MGLRU内存回收新方案,已经合入了6.1内核。作为全新的内存回收方案,回收效果是挺好的,但是基本抛弃内核原有lru内存回收方案,对内核做了大幅改动,比较担忧它的稳定性,主流操作系统发行版短时间内估计很难会用到!

并且,内存回收大部分场景针对的是文件的,针对的内存回收不一定得大幅改动内核吧?如果能把管理文件的页高速缓存管理起来,根据每个文件页page访问频率,找出冷page,然后回收掉,不就可以实现的内存回收了?这就是本文的异步内存回收方案基本思路,重点是不用修改内核、做成了内核ko。目前已经实现了基本功能,性能损耗正常,下文详细介绍这个方案。

1:内存回收的现状

线上经常遇到 200G~300G且free内存很少的场景,此时进程频繁因分配内存失败而内存回收,容易造成阻塞、性能抖动!此时大概率进程也在疯狂回收内存!并且,网卡软中断里分配skb有关数据结构的page时因指定了标记而禁止内存回收时休眠,导致内存分配时进入slow分支而频繁触发告警信息,严重的还会导致内核crash。

这200G+的内存如果能提前异步回收,就没那么多事了!可是内核只能在内存不足时才会按需回收一小部分内存!对了,还遇到一个 7.6内核bug,触发后容器里内存回收就io hung。遇到这么多内存回收问题,在2021年就迫切想做一个异步内存回收的内核ko工具,灵活方便,关键是不用修改编译内核!

Linux内核原生的内存回收方案简单说下:内存page分为匿名页page和文件页page两类,分别存入/ anon、/ file lru链表。文件页page主要来自,内存回收大部分情况下也是从里回收的。注意,本文讨论是针对的文件页page内存回收,不讨论匿名页的内存回收。

内核原生内存回收方案比较被动:必须要等到进程分配时,当前内存zone的可直接分配的内存page数小于内存zone的min/low/high的内存水位值+内核预留内存时,才会进行直接内存回收或唤醒线程回收内存。这对于敏感业务,很容易因分配不出内存而性能抖动!

并且,保存在 lru链表的page一般是最近多次访问的(热page),保存在 lru链表的page一般是最近没访问过的(冷page)。内存回收时是从 lru链表尾扫描一些page,尝试内存回收,这个策略貌似看着是合理的,因为 lru链表上的page是最近没访问过的,内存回收就应该回收掉不经常访问的page,而经常访问的page不能回收。但是却经常有如下情况:

1:一些page在某个时间点被频繁访问后被移入 lru链表,但是之后很长一段时间就不再被访问了。接着,有些page被少量访问了而移动到 lru链表。如果此时发生内存回收,是应该回收 lru链表长时间不被访问的page,还是回收 lru链表刚访问过的page?正常情况回收前者更合理,但是内核是回收后者。当然,内核也可以实时把 lru链表长时间不被访问的page移动到 lru链表,但是需要从 lru链表找到这种page,这个过程需要pgdat->或->加锁,损耗性能会较大。回收page的标准应该是它最近一段时间的访问频率,而不是它在处于哪个链表。

2:遍历 lru链表尾上的page,在内存回收这些page后,很快又被访问了,此时发生了现象。内核改善措施之一是增大 lru链表长度,增加 page在 lru链表停留的时间,防止再被回收掉。而更好点的方法是,对发生的page做个标记,后续内存回收尽量不回收这种page。

2:内存回收方案的改进

目前的异步内存回收方案都修改了内核,能否把异步内存回收做成一个ko,这样不用修改内核了,灵活很多。除了这点之外,有如下改进会更好:

如下图所示,这是一个4k*12大小的文件读写后产生的示意图,page0是文件地址0~4k文件数据对应的,就是索引是0的文件页page,其他类推。page0、page1、page3、page8、、访问的很频繁,是热page。剩下的page很少访问,是冷page。

这种情况其实挺常见的,如前文所说,把 page0~page3、page4~page7、page8~分别作为3个内存page单元,一起参与内存回收比较合适。分组效果如下:

每个page被访问一次则内存page单元的访问计数加1。内存回收时先扫描这3个内存page单元,根据访问计数大小判断冷热程度,然后再回收冷的内存page单元的page,这样的内存回收效率比较高。

好的,有了以上铺垫,继续深化。在“不修改内核,做成内核ko”的前提下,本文介绍的异步内存回收主要思路如下:

1:为每个文件分配一个数据结构,管理每个文件的所有。并且把每个文件的均分成若干个区域(每个区域包含4个索引连续的page),每个区域作为一个内存page单元。并为每个内存page单元分配一个数据结构,主要反应这个内存page单元page的冷热,如下图所示:

2:创建一个内核线程,负责异步内存回收工作,默认每1分钟运行一次,每次运行时先令全局age计数加1(这个age的思路参考了MGLRU的方案,作用一致)。同时,每个文件的每个内存page单元,也有一个age(注意,内存page单元的age和访问计数,都反应的冷热,但是作用有差异,3.2.3节详细介绍)。当某个内存page单元对应的文件页page被访问了,则该的age就要更新为全局age。的age就是它的文件页page最近一次被访问时的全局age!如果一个对应的文件页page长时间不被访问,它的age就很小,称为冷,内存回收时就要回收这些冷对应的文件页page。举个例子,如下图所示:

现在全局age是10,异步内存回收线程已经运行了10次(每个周期1分钟)。和的age是10,说明二者对应的文件页page最近一个周期被访问过,被判定是热。的age是2,说明已经至少有8个周期(8分钟)内其对应的文件页page没有被访问过,被判定是冷,内存回收时优先回收的4个文件页page。

3:针对消耗多的文件,内存回收时优先扫描这些文件的内存page单元,大概率能回收很多冷page。这点第3节再介绍。

每个文件的内存page单元在代码里是怎么组织起来呢?用一个内核双向链表即可:

如图,展示的一个文件的简易组织情况。这个文件的共有page0~这些文件页page(索引 0~23)。示意图中表示一个内存page单元,它包含4个索引连续的文件页page。比如 对应的是索引是0、1、2、3的文件页page0、page1、page2、page3,其他同理,这种形式下文会经常遇到。一共有6个,最初全局age是0,每个的age都是0。假设在第5个周期,全局age增加为5,此时和对应的page被访问了,则把全局age赋值给这些自己的age,如下:

热回收形式__热回收冷冻机工作原理是什么

这些的组织比较混乱,有些最近访问过,有些最近没访问过。内存回收时只想把最近没有访问过的(age很小)找出来,遍历整个链表代价太大了!于是当一个的page被访问时,就把移动到链表头(不是每次访问都移动到链表头,有一定策略),如下:

这样就好多了,我只用从链表尾遍历,遇到age很小的则一直向前遍历,如果遇到age偏大的则结束遍历。这样就避免了很多无用功,因为最近被访问过的文件页page对应的都移动到了链表头!

最后,创建一个异步内存回收线程,从这个链表尾依次扫出 、、、这些age很小的。因为这些对应的的文件页page较长时间都没有访问了,那就把这些page找出来并回收掉。

这就是本文异步内存回收的基本方案,就是想办法把长时间未被访问的文件的文件页page找出来并回收掉!难点之一是不修改内核源码的前提下(做成一个内核ko),把产生的文件组织起来,判定这些文件中,哪些文件页page是频繁访问的(热page),哪些文件页page是长时间不被访问的(冷page),最后把冷page回收掉!另外一个难点是怎么高效的组织这些文件的冷热文件页page,快速找到每个文件的冷文件页page,性能损耗还要低!当然还有很多其他细节,下文结合示意图再详细讲讲。

3:示意图详解异步内存回收机制 3.1 整体框架介绍

前文已经介绍了针对单个文件的回收思路,一个linux系统有成千上万个文件,每个文件对应一个结构,该怎么组织起来这些文件? 一个文件也会有相当多文件页page,这就导致会有相当多数目的内存page单元(默认一个对应4个索引连续的文件页page)。这些对应的文件页page有些是频繁访问的(热page);有些是很少访问(冷page)而要参与内存回收的;有些的文件页page被内存回收后短时间内又被访问了,发生现象,则这种对应的文件页page需要一段时间内禁止再内存回收,防止再发生……..情况很复杂,该怎么组织起来这些?一点一点讲解。

一个文件对应一个结构,一个文件的中,4个文件页page对应一个内存page单元,怎么与关联起来?又怎么与文件的inode、页高速缓存结构产生联系的呢?当访问一个文件的时,便会执行()函数(现在也在考虑换成等函数,效果一样,但是可以解决高版本内核 io write不执行函数的问题,阅读本方案的源码时需注意),原始定义如下:

void ( page *page)

该函数默认功能是将 lru链表上的page随着访问次数的增加而移动到 lru链表。本方案 该函数,获取到读写的文件页page指针,通过page->得到文件页高速缓存结构 。而结构体最后在红帽8和9系列内核有预留字段 long (大部分高版本的内核发行版结构体最后都有预留字段,如红帽8和9、阿里龙蜥OS、腾讯 OS、安卓手机内核)。

本方案正是把为文件分配的结构指针赋值给该文件唯一的结构体的预留字段的成员 long 。这样一来,就把文件的文件页page、文件页高速缓存、radix/ tree(保存文件页page指针)、、 串联起来了!如下示意图所示:

page、、结构体通过其成员相互指向,而默认添加在结构体成员这个链表上。那系统成百上千个文件的以及每个文件产生的属性繁多的(频繁访问的、很少访问的、发生过的)又是怎么组织起来的呢?下文重点介绍。

3.2 文件、内存page单元的组织关系 3.2.1 是怎么组织起来的?

本方案把文件分成3类,普通文件、大文件、热文件。

普通文件:一个文件最开始读写产生默认被判定为普通文件大文件:当一个文件的文件页page数量大于某个阀值(比如1G)则判定为大文件,异步内存回收时优先扫描大文件的文件页page,因为有较大概率能回收到很多冷page热文件:当一个普通文件或大文件的文件页page大部分都访问频繁则判定为热文件,异步内存回收时一定时间内不会遍历这些文件。

每个文件对应一个结构,本方案定义了一个 全局结构体,把普通文件、大文件、热文件组织起来,如下:

为了叙述方便,下文把 结构体的 、 、 链表简称为 、 、 链表。下一节开始介绍文件和内存page单元的关系。

3.2.1 和的关系

如图,结构体有各种各样的链表、、、、,分别保存不同属性的。

文件的 链表:默认添加到链表。异步内存回收线程只会遍历链表上!如图该文件的文件页page8~、~对应的内存page单元、就是在链表。文件的 链表:如果对应的文件页page被频繁访问,被判定是热,于是把该热移动到链表。异步内存回收线程在较长一段时间都不会遍历链表上的,因为这些最近大概率还会被访问。如图该文件的文件页page0~page3、~因为频繁访问,则把对应的内存page单元、移动到链表。文件的 链表:如果对应的文件页page被内存回收后,短时间又被访问,则被判定是 。

于是把该移动到 链表,这些在较长一段时间不会再参与内存回收,即便age与全局相差较大!如图该文件的文件页~在内存回收后,短时间内这几个索引的page又被访问了,发生现象,于是把~对应的内存page单元移动到链表。文件的 链表:内存回收时,把链表尾的冷移动到链表。内存回收时正是遍历每个文件的链表上的对应的文件页page。如图,~、page4~page7 正在被内存回收,对应的内存page单元、正是在链表。文件的 链表:内存回收后的要从链表移动到链表,如果该对应的文件页page还是长时间不被访问则释放掉结构。如果又被访问了则要把移动回链表。如图~在内存回收后,对应的则被移动到链表。

下文为了叙述方便,把文件的 链表下文简称->, 下文简称->, 下文简称->, 下文简称->, 下文简称->。

3.2.3 的冷热判断

前文多次提到热,也就是频繁访问的,反应到代码层面是怎么界定的?先看下结构体的主要成员,主要有两个:

就是的age。异步内存回收周期默认1分钟,每过一个周期令全局age加1,当对应的文件页page被访问,则把全局age赋值给的成员。因此,的age就是最近一次被访问时的全局age。的age与全局age差值越小,说明对应的文件页page最近被访问过;的age与全局age差值很大,说明这个对应的文件页page很长时间没有被访问了(冷)。内存回收时正是优先遍历冷对应的文件页page(这些page是冷page)。

是在一个周期的访问计数,对应的文件页page每访问一次则加1。如果一个周期内对应的文件页page被访问的总次数大于阀值,被判定为热,则把该移动到该文件的->链表。异步内存回收线程在较长一段时间都不会遍历->链表上的,因为这些最近大概率还会被访问。

的age和都与的冷热有关系,但用途不一样。在一个内存回收周期内,如果的大于阀值,则立即判定该是热的,然后移动到->链表,禁止一段时间内被异步内存回收线程遍历到。的age表示广义的冷热程度,如果异步内存回收线程遍历到某个的较小,但是的age与全局age差值较小,也被判定是热,这种不参与内存回收,只是热的程度比不上很大的。

好的,文件及内存page单元的组织关系已经介绍过了。下文举个例子,介绍下一个文件的文件页page()的回收过程。

3.3 异步内存回收举例

假设有个进程访问test文件,下文演示下怎么识别出哪些文件页page是频繁访问的,哪些是不经常访问,简单说就是识别出冷热文件页page,然后只回收冷文件页page。注意,这个演示与实际代码实现有差异,为了演示方便简化了很多。

在第1个内存回收周期,全部读取98304大小test文件。于是先为该文件创建一个结构并添加到 链表。接着为该文件的98304大小的分配6个内存page单元~。表示索引是0~3的文件页page0~page3,其他类推。~最初添加到该文件->链表。如下图:

接着来到第2个内存回收周期,全局age加1为1。test文件的索引是16、20的文件页、被读取了两次,则对应的内存page单元、的age被赋值为全局age,且增加到2。接着还要把、移动到->链表头,因为被访问过的都要移动到链表头!效果如下图:

注意,这里设定只有的age与全局age的差值大于阀值N(假设N=5)时,才被判定是冷,然后才会回收该对应的文件页page。显然此时还没有达到内存回收条件的。

接着来到第3个内存回收周期,全局age加1为2。这个周期内,该文件的文件页、连续被访问了6次,对应的和的被赋值6。二者因为访问次数超过了阀值M(假设M=5),则和判定为热。于是把和移动到->链表,age也被赋值为2。同时该文件的文件页page5、page8也被访问了一次,则对应的、被移动到->链表头,且age被赋值2,赋值1,最终效果如图所示:

随后一直到周期7,该文件都没再被访问了,此时出现了内存回收契机。因为此时全局age增加到6,而该文件在->链表尾的内存page单元、对应的文件页page0~page3、~ 一直没有被访问,age是0,与全局age的差值大于阀值N(假设N=5),则二者被判定是冷。然后在该周期,异步内存回收线程把、移动到->链表。如图:

注意,的只表示单个周期内被访问的次数。每个周期开始,的要先清0,然后每被访问一次则加1。

接着,异步内存回收线程遍历->链表上的和,根据该文件的radix/ tree(保存文件页page指针),找到对应的page0~page3、~文件页指针。然后就尝试回收掉这8个page,具体回收流程大体模仿内核原生内存回收代码,但有很大改动。这些文件页page回收完后,把和移动到->链表。如图:

假设对应的索引是0的文件页page0在内存回收后,索引是0的page立即又被进程访问了。则发生了现象,于是把移动到-> 链表,这样后期有相当长一段时间不会再被参与内存回收,即便它的age与全局age相差较大。如图:

好的,以上把针对单个文件的文件页page的内存回收流程大体演示了一遍。系统有成百上千个文件时,每个文件的文件页page回收过程类似。但再结合普通文件、大文件、热文件的文件页page的回收,就很复杂了。下文从代码角度介绍下整体的异步内存回收流程,或许可以加深理解。

4:基于源码流程图聊聊异步内存回收

该方案的源码主要有两个流程:

1:文件页读写后,会执行到内核原生()函数。本方案 ()函数(现在也在考虑换成等函数,效果一样,但是可以解决高版本内核 io write不执行函数的问题,阅读本方案的源码时需注意),然后执行自定义()函数。

在函数()函数里:为读写的文件分配;为访问的文件页page分配对应的;把、添加到各种链表;增加的age和访问计数;判断热;普通文件在达到一定条件升级到大文件或者热文件等等。

2:异步内存回收线程的入口函数是(),默认一分钟运行一次。在()函数里:令全局age加1;遍历 大文件链表和 普通文件链表的,从这些文件-> 链表尾找出age与全局age差值大于阀值N的(这些就是冷,对应的文件页page长时间不被访问,本次就回收这些page)。

如果遍历到的文件的->和-> 链表上的长时间不被访问,则要降级移动到->链表;如果 链表上的大文件,对应文件页page减少到阀值以下,则降级为普通文件,并把移动到普通文件 链表;如果 链表上的热文件,大部分不再是热,则也要降级为普通文件,并把移动到普通文件 或大文件 链表等等。

细节还是比较复杂的,下边先看下文件被读写最后执行到的()函数的整体流程图:

可以发现,这个流程主要就是操作文件和本次访问的文件页page对应的内存page单元:更新 的age、增加计数、热文件和大文件的判定、热、 的判定和处理。下边看下异步内存回收线程入口函数()的整理流程:

可以发现,这个流程主要讲解怎么一个个扫描文件,从 ->链表尾扫出冷,然后把冷对应的文件页page回收掉。还有就是->和->链表上怎么降级到->链表,热文件、大文件怎么降级为普通文件等等。源码就不再列了,后续的文章再讲解。

5:总结

本文只是简单介绍了一下异步内存回收的大体方案,实际还有很多细节没讲解,比如:

当然,还有很多其他细节,代码开发测试过程也遇到很多棘手的bug!就不在本文介绍了,后续再单独写文章。其实有一个想法,能否把本方案的一些源码合入内核呢?比如:修改内核原来保存文件页page指针的radix/ tree,不再保存page指针,而是保存和page指针的联合体。文件页page被访问则的age跟随全局age增加,据此判断和文件页page的冷热。再开一个内核线程回收冷对应的文件页page。原理是类似的,对内核的改动也比较小,同时应该能实现不错的异步内存回收效果。

本次实践是个不错的学习内核内存回收的机会!在模仿内核原生内存回收源码,修改成适用于本方案的内存回收代码的过程中,对内存回收的理解加深了很多。目前已经解决了很多bug,但不排除还存在隐藏bug,欢迎大佬的指导和批评指正。

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了