• 剩余内存无法满足申请时,系统会怎么做?


    前言

    当我们向操作系统申请内存时候,是否有想过一个问题:如果当前系统物理内存不足以支撑我们所需要的空间容量,操作系统会进行哪些的相关处理来保证满足我们的要求?

    答案是涉及接下来我们要讲解的几个方面

    在这里插入图片描述

    内存的分配机制, 回收可回收内存, 如何在保证性能的其拉提下满足要求,以及如何保证不被OOM机制处死

    内存的分配机制

    现代操作系统大部分都是使用虚拟内存的页或者段页式内存管理机制,当我们使用malloc等内存申请函数调用时候,所分配到的空间实际是虚拟内存,并不会分配到物理内存,当程序下次进行读取该内存时,CPU就会去访问该虚拟内存,然后发现其并没有和物理内存进行映射产生关联,CPU就会发生缺页中断,然后进程从用户态转移到内核态,交给操作系统执行PAGE_Fault(缺页中断处理函数);

    此时PAGE_FAULT会检查当前空余物理内存是否足够,若足够,则进行建立和物理内存的映射关系,若不足,则通过操作系统提供的3种常见内存回收机制进行回收内存;

    3种常见内存回收机制:

    • 后台 回收机制(异步,不阻塞)

      唤醒kswapd内核线程来回收内存,此过程异步进行,不会阻塞进程的执行.

    • 直接 回收机制(同步,阻塞)

      当后台异步回收机制跟不上进程内存申请读写的速度,就开始直接回收机制,此过程同步进行,会阻塞进程的执行.

    • OOM(out of memory)

      当直接回收机制都无法满足内存申请要求,则执行OOM Killer 机制,它会根据算法选择一个占用物理内存较高的进程,然后将其杀死以释放内存资源,如果依然不足,OOM Killer 将会继续杀死占用物理内存更高的进程,直到释放足够的内存为止.

    在这里插入图片描述

    回收可回收内存的类型

    通过上面的内存分配流程图,我们可以知道当物理内存不足以支撑需求的时候,会进行执行相关的内存回收机制,但是到底回收的是哪部分内存呢?

    主要分为两类

    • 文件页(file-backed pages) 内存:进程虚拟地址空间中的代码段,以及映射的文件一般称为文件页

      大部分文件页,都可以直接释放内存,当以后有需要再从磁盘读取.而那些被修改过且还未写入磁盘的数据(脏页),就得先写入磁盘才能进行内存释放。所以,回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存

    • 匿名页(anonymous pages) 内存:进程虚拟地址空间的堆,栈,数据段一般称为匿名页

      这部分内存一般是应用程序临时产生的,所以也不能直接释放内存,它们回收的方式是通过 Linux 的 Swap 机制,Swap 会把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了

    更多关于文件页和匿名页介绍请看这里

    文件页和匿名页的回收都是基于 LRU(Least Rencently Used) 算法,也就是回收最近最少使用的内存。该算法维护着 active 和 inactive 两个双向链表:

    • active_list 活跃内存页链表,存放最近被访问过(活跃)的内存页;
    • inactive_list 不活跃内存页链表,存放最近很少被访问(非活跃)的内存页;

    越接近链表尾部,就表示内存页越不常访问。因此,在回收内存时,系统就可以根据活跃程度,优先回收不活跃的内存。

    其中在活跃页和非活跃页内部,又分对文件页(file)和匿名页(anonymous)进行了分类,可以使用linux shell命令cat /proc/meminfo | grep -i active | sort进行查看:

    在这里插入图片描述

    如何在保证系统性能前提下回收内存

    从上面两个小节可以看出,以回收方式的角度看待,直接回收会阻塞进程;以可回收内存的类型角度看,脏页和匿名页回收会引起磁盘IO;

    也就是说无论怎么样,只要回收内存,就很容易发生IO事件,如果频繁,就会引起系统整体性能降低,即宏观感受到的就是系统卡顿;

    那么回收内存时候,怎么可以保证性能呢?,一般通过以下三个角度看待:

    可回收类型角度: 调整文件页回收倾向

    文件页主要分为干净页和脏页,其中干净页的回收不需要发生磁盘IO,因此文件页的效率相对匿名页来说,回收效率更高,我们可以提高系统回收文件页的概率(或者说倾向)高于匿名页;

    linux系统给我们提供了一个选项来进行调整

    /proc/sys/vm/swappiness 
    
    • 1

    swappiness 的范围是 0-100,数值越大,越积极使用 Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页。

    所以为了效率,一般可以将swappiness的值设为0,这样当系统回收内存时候,就更倾向于去回收文件页(注意哦,说的是倾向,不是说就一定回收文件页)

    回收的方式角度: 尽早触发kswapd内核线程

    后台回收机制通过唤醒kswapd线程进行异步回收内存,并不会影响系统性能,因此可以通过尽早触发kswapd的形式

    但是触发kswapd的条件是什么呢?

    内核定义了三个内存阈值(watermark,也称为水位线),用来衡量当前剩余内存是否充裕或者紧张,分别是:

    • 页高阈值(pages_high);

    • 页低阈值(pages_low);

    • 页最小阈值(pages_min);

    kswapd 会定期扫描内存的使用情况,根据剩余内存(pages_free)的情况来进行内存回收的工作。

    • 图中绿色部分:如果剩余内存(pages_free)大于 页高阈值(pages_high),说明剩余内存是充足的;
    • 图中蓝色部分:如果剩余内存(pages_free)在页高阈值(pages_high)和页低阈值(pages_low)之间,说明内存有一定压力,但还可以满足应用程序申请内存的请求;
    • 图中橙色部分:如果剩余内存(pages_free)在页低阈值(pages_low)和页最小阈值(pages_min)之间,说明内存压力比较大,剩余内存不多了。这时 kswapd0线程就会执行内存回收,直到剩余内存大于高阈值(pages_high)为止。虽然会触发内存回收,但是不会阻塞应用程序,因为两者关系是异步的。
    • 图中红色部分:如果剩余内存(pages_free)小于页最小阈值(pages_min),说明用户可用内存都耗尽了,此时就会触发直接内存回收,这时应用程序就会被阻塞,因为两者关系是同步的。

    也就是说,触发kswapd线程的条件就是pages_min < 剩余内存 < pages_low

    知道了这个条件后,我们就可以设置pages_min,pages_low和pages_heigh的值,进行调整触发kswapd的条件难易了

    系统给我们提供了选项

    /proc/sys/vm/min_free_kbytes
    
    • 1

    而它们三者之间的关系是:

    pages_min = min_free_kbytes
    pages_low = pages_min*5/4
    pages_high = pages_min*3/2
    
    • 1
    • 2
    • 3

    当设置好我们想要的参数以后,可以通过sar -B 1命令查看系统直接回收和后台回收的指标变化了

    在这里插入图片描述

    • pgscank/s : kswapd(后台回收线程) 每秒扫描的 page 个数。
    • pgscand/s: 应用程序在内存申请过程中每秒直接扫描的 page 个数。
    • pgsteal/s: 扫描的 page 中每秒被回收的个数(pgscank+pgscand)。

    倘若在该指标变化中,发现pgscand/s的值很大,就可能是系统采用了直接回收内存的方式,因此我们可以继续采用调整min_free_bytes的值大小方式进行尽早触发kswapd内核线程

    从计算机CPU架构角度: 采用NUMA

    什么是UMA结构?

    每个 CPU 地位平等,它们共享相同的物理资源,包括总线、内存、IO、操作系统等,每个 CPU 访问内存所用时间都是相同的,因此,这种系统被称为一致存储访问结构(UMA,Uniform Memory Access)。

    在这里插入图片描述

    而随着 CPU 处理器核数的增多,多个 CPU 都通过一个总线访问内存,这样总线的带宽压力会越来越大,同时每个 CPU 可用带宽会减少,为了解决此问题,便提出了NUMA结构,即非一致存储访问结构,NUMA结构将每个 CPU 进行了分组,每一组 CPU 用 Node 来表示,一个 Node 可能包含多个 CPU ,每个 Node 有自己独立的资源,包括内存、IO 等,每个 Node 之间可以通过互联模块总线(QPI)进行通信,所以,也就意味着每个 Node 上的 CPU 都可以访问到整个系统中的所有内存。但是,访问远端 Node 的内存比访问本地内存要耗时很多。

    在这里插入图片描述

    在 NUMA 架构下,当某个 Node 内存不足时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。

    具体选哪种模式,可以通过 /proc/sys/vm/zone_reclaim_mode 来控制。它支持以下几个选项:

    • 0 (默认值):在回收本地内存之前,在其他 Node 寻找空闲内存;
    • 1:只回收本地内存;
    • 2:只回收本地内存,在本地回收内存时,可以将文件页中的脏页写回硬盘,以回收内存。
    • 4:只回收本地内存,在本地回收内存时,可以用 swap 方式回收内存。

    在使用 NUMA 架构的服务器,如果系统出现还有一半内存的时候,却发现系统频繁触发「直接内存回收」,导致了影响了系统性能,那么大概率是因为 zone_reclaim_mode 没有设置为 0 ,导致当本地内存不足的时候,只选择回收本地内存的方式,而不去使用其他 Node 的空闲内存。

    虽然说访问远端 Node 的内存比访问本地内存要耗时很多,但是相比内存回收的危害而言,访问远端 Node 的内存带来的性能影响还是比较小的。因此,zone_reclaim_mode 一般建议设置为 0

    如何保护进程不被OOM杀掉

    在系统空闲内存不足的情况,进程申请了一个很大的内存,如果直接内存回收都无法回收出足够大的空闲内存,那么就会触发 OOM 机制,内核就会根据算法选择一个进程杀掉。

    Linux 到底是根据什么标准来选择被杀的进程呢?这就要提到一个在 Linux 内核里有一个 oom_badness() 函数,它会把系统中可以被杀掉的进程扫描一遍,并对每个进程打分,得分最高的进程就会被首先杀掉。

    进程得分的结果受下面这两个方面影响:

    • 第一,进程已经使用的物理内存页面数。
    • 第二,每个进程的 OOM 校准值 oom_score_adj。它是可以通过 /proc/[pid]/oom_score_adj 来配置的。我们可以在设置 -1000 到 1000 之间的任意一个数值,调整进程被 OOM Kill 的几率。

    函数 oom_badness() 里的最终计算方法是这样的:

    // points 代表打分的结果
    // process_pages 代表进程已经使用的物理内存页面数
    // oom_score_adj 代表 OOM 校准值
    // totalpages 代表系统总的可用页面数
    points = process_pages + oom_score_adj*totalpages/1000
    
    • 1
    • 2
    • 3
    • 4
    • 5

    用「系统总的可用页面数」乘以 「OOM 校准值 oom_score_adj」再除以 1000,最后再加上进程已经使用的物理页面数,计算出来的值越大,那么这个进程被 OOM Kill 的几率也就越大

    每个进程的 oom_score_adj 默认值都为 0,所以最终得分跟进程自身消耗的内存有关,消耗的内存越大越容易被杀掉。我们可以通过调整 oom_score_adj 的数值,来改成进程的得分结果:

    • 如果你不想某个进程被首先杀掉,那你可以调整该进程的 oom_score_adj,从而改变这个进程的得分结果,降低该进程被 OOM 杀死的概率。
    • 如果你想某个进程无论如何都不能被杀掉,那你可以将 oom_score_adj 配置为 -1000。

    我们最好将一些很重要的系统服务的 oom_score_adj 配置为 -1000,比如 sshd,因为这些系统服务一旦被杀掉,我们就很难再登陆进系统了。

    但是,不建议将我们自己的业务程序的 oom_score_adj 设置为 -1000,因为业务程序一旦发生了内存泄漏,而它又不能被杀掉,这就会导致随着它的内存开销变大,OOM killer 不停地被唤醒,从而把其他进程一个个给杀掉。

    总结

    内核在给应用程序分配物理内存的时候,如果空闲物理内存不够,那么就会进行内存回收的工作,主要有两种方式:

    • 后台内存回收:异步
    • 直接内存回收:同步

    而可以回收的类型也有两种:

    • 文件页
    • 匿名页

    针对回收内存导致的性能影响,常见的解决方式。

    • 调整文件页和匿名页的回收倾向: 设置 /proc/sys/vm/swappiness 的值
    • 调整 kswapd 内核线程异步回收内存的时机: 设置 /proc/sys/vm/min_free_kbytes的值
    • 调整 NUMA 架构下内存回收策略: 设置 /proc/sys/vm/zone_reclaim_mod的值

    当这三个性能都不能完成目标内存的申请时,就会采用OOM机制,杀掉一些根据系统评分出来的最高得分的进程,直到满足申请要求为止;

  • 相关阅读:
    Django系列7-员工管理系统实战--项目准备
    短短 146 天就成为 Apache APISIX Committer,我是怎么做到的?
    CheckBox/RadioButton切换动效实现
    用于割草机器人,商用服务型机器人的陀螺仪
    携带参数的退出功能
    JVM 执行引擎部分 (编译器、解释器)
    matplotlib教程二
    Notepad++插件 Hex-Edit
    操作系统实训题目
    this指向
  • 原文地址:https://blog.csdn.net/m0_51723227/article/details/128158826