哪些内存可以被回收
- 文件页(File-backed Page):内核缓存的磁盘数据(Buffer)和内核缓存的文件数据(Cache)都叫作文件页。大部分文件页,都可以直接释放内存,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。所以,回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存。这些脏页有两种方式写入磁盘:
- 在应用程序中,通过系统调用 fsync ,把脏页同步到磁盘中。
- 交给系统,由内核线程 pdflush 负责这些脏页的刷新。
- 匿名页(Anonymous Page):这部分内存没有实际载体,不像文件缓存有硬盘文件这样一个载体,比如堆、栈数据等。这部分内存很可能还要再次被访问,所以不能直接释放内存,它们回收的方式是通过 Linux 的 Swap 机制,Swap 会把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。
如果操作系统没有开启swap分区,那这些占用的内存就只能是常驻内存了,不能被回收。
文件页和匿名页的回收都是基于 LRU 算法,也就是优先回收不常访问的内存。LRU 回收算法,实际上维护着 active 和 inactive 两个双向链表,其中:
- active_list 活跃内存页链表,这里存放的是最近被访问过(活跃)的内存页。
- inactive_list 不活跃内存页链表,这里存放的是很少被访问(非活跃)的内存页。
越接近链表尾部,就表示内存页越不常访问。这样,在回收内存时,系统就可以根据活跃程度,优先回收不活跃的内存。
活跃和非活跃的内存页,按照类型的不同,又分别分为文件页和匿名页。可以从 /proc/meminfo 中,查询它们的大小。
Swap原理
Swap说白了就是把一块磁盘空间或者一个本地文件,当成内存来使用。它包括换出和换入两个过程。
- 换出,就是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存。
- 换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来。
典型场景
- 即使内存不足时,有些应用程序也并不想被 OOM 杀死,而是希望能缓一段时间,等待人工介入,或者等系统自动释放其他进程的内存,再分配给它。
- 我们常见的笔记本电脑的休眠和快速开机的功能,也基于Swap。休眠时,把系统的内存存入磁盘,这样等到再次开机时,只要从磁盘中加载内存就可以。这样就省去了很多应用程序的初始化过程,加快了开机速度。
内存回收
直接内存回收(direct reclaim)
有新的大块内存分配请求,但是剩余内存不足。这个时候系统就需要回收一部分内存(比如前面提到的缓存),进而尽可能地满足新内存请求。这个过程通常被称为直接内存回收。
这个回收内存的过程是同步的,会阻塞进程的执行。
内核线程回收内存(kswapd0)
这个回收内存的过程异步的,不会阻塞进程的执行。
为了衡量内存的使用情况,kswapd0 定义了三个内存阈值(watermark,也称为水位),分别是:
- 页最小阈值(pages_min)
- 页低阈值(pages_low)
- 页高阈值(pages_high)
kswapd0 定期扫描内存的使用情况,并根据剩余内存落在这三个阈值的空间位置,进行内存的回收操作。
- 剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力。
- 剩余内存落在页低阈值和页高阈值中间,说明内存有一定压力,但还可以满足新内存请求。
- 剩余内存落在页最小阈值和页低阈值中间,说明内存压力比较大,剩余内存不多了。这时kswapd0会执行内存回收,直到剩余内存大于高阈值为止。
- 剩余内存小于页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存,这时会触发直接内存回收。
OOM(Out of Memory)机制
如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请。OOM Killer机制会根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。
每个进程的 OOM 校准值 oom_score_adj。它是可以通过 /proc/[pid]/oom_score_adj 来配置的。我们可以在设置 -1000 到 1000 之间的任意一个数值,调整进程被 OOM Kill 的几率。
如果你不想某个进程被首先杀掉,那你可以调整该进程的 oom_score_adj,从而改变这个进程的得分结果,降低该进程被 OOM 杀死的概率。
当发生OOM时,可以通过dmesg | grep -i "Out of memory"
这个命令查看是哪个进程被系统OOM kill了。
内存回收带来的性能影响
在前面我们知道了回收内存有两种方式。
- 内核线程回收内存,也就是唤醒 kswapd 内核线程,这种方式是异步回收的,不会阻塞进程。
- 直接内存回收,这种方式是同步回收的,会阻塞进程,这样就会造成很长时间的延迟,以及系统的 CPU 利用率会升高,最终引起系统负荷飙高。
可被回收的内存类型有文件页和匿名页:
- 文件页的回收:对于干净页是直接释放内存,这个操作不会影响性能,而对于脏页会先写回到磁盘再释放内存,这个操作会发生磁盘 I/O 的,这个操作是会影响系统性能的。
- 匿名页的回收:如果开启了 Swap 机制,那么 Swap 机制会将不常访问的匿名页换出到磁盘中,下次访问时,再从磁盘换入到内存中,这个操作是会影响系统性能的。
可以看到,回收内存的操作基本都会发生磁盘 I/O 的,如果回收内存的操作很频繁,意味着磁盘 I/O 次数会很多,这个过程势必会影响系统的性能,整个系统延时会很大。
内存回收性能优化
- 调整文件页和匿名页的回收倾向
从文件页和匿名页的回收操作来看,文件页的回收操作对系统的影响相比匿名页的回收操作会少一点,因为文件页对于干净页回收是不会发生磁盘 I/O 的,而匿名页的 Swap 换入换出这两个操作都会发生磁盘 I/O。
Linux 提供了一个 /proc/sys/vm/swappiness 选项,用来调整文件页和匿名页的回收倾向。
swappiness 的范围是 0-100,数值越大,越积极使用Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页。
- 尽早触发kswapd内核线程异步回收内存
页低阈值(pages_low)可以通过内核选项 /proc/sys/vm/min_free_kbytes (该参数代表系统所保留空闲内存的最低限)来间接设置。
其他两个阈值,都是根据页最小阈值计算生成的,计算方法如下
pages_min = min_free_kbytes
pages_low = pages_min*5/4
pages_high = pages_min*3/2
NUMA架构下的内存回收策略
先简单了解下。
SMP架构,共享相同的物理资源,包括总线、内存、IO、操作系统等。
NUMA架构,将每个CPU进行了分组,每一组CPU用Node来表示,一个Node可能包含多个CPU。每个 Node 有自己独立的资源,包括内存、IO等。
在 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。