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

本文介绍的针对文件缓存的内存回收方案与现有的思路有很大不同:内存回收的单位是一个个文件,再把文件的分成一个个小区域(或者叫小单元),一个区域由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不能回收 。但是却经常有如下情况: