1. 内存回收⚓
1.1 内存回收⚓
应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。
当应用程序读写了这块虚拟内存,CPU 就会去访问这个虚拟内存,这时会发现这个虚拟内存没有映射到物理内存,CPU 就会产生缺页中断(没有访问的话就不会去映射,也就不会产生缺页中断),进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler
(缺页中断函数)处理。
缺页中断处理函数会看是否有空闲的物理内存,如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
如果没有空闲的物理内存,那么内核就会开始进行回收内存的工作:
- 后台内存回收(kswapd):在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程是异步的,不会阻塞进程的执行
- 直接内存回收(direct reclaim):如果通过后台异步回收跟不上内存申请的速度,就会直接开始回收,这个回收内存的过程是同步的,会阻塞进程的执行。
- OOM(Out of Memory)机制:如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请需求,就会触发此机制。OOM Killer 机制会根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer 会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。
1.2 可以回收的内存⚓
主要有两类内存可以被回收:
- 文件页:内核缓存的磁盘数据(Buffer)和文件数据(Cache)都叫作文件页。回收干净页的方式是直接释放内存,以后有需要再从磁盘读取就行了;回收脏页则需要先写回磁盘后再释放缓存。
- 匿名页:因为这部分内存没有实际载体(文件缓存有硬盘文件这样的载体),存储的是堆、栈数据等。所以不能直接释放,需要通过 Linux swap 机制,把不常访问的内存写入到磁盘中,然后再释放,再次访问这些内存时,重新从磁盘读入内存就可以了。
这两种回收方式都是基于 LRU 算法,也就是优先回收不常访问的内存。LRU 回收算法,实际上维护着 active
和 inactive
两个双向链表,其中:
active_list
活跃内存页链表,这里存放的是最近被访问过(活跃)的内存页;inactive_list
不活跃内存页链表,这里存放的是很少被访问(非活跃)的内存页;
之所以要分成两个链表,是为了解决预读失效的问题。参考《预读失效和缓存污染》或《MySQL Buffer Pool》。
活跃和非活跃的内存页,按照类型的不同,又分别分为文件页和匿名页。可以从 /proc/meminfo
中,查询它们的大小,比如:
# grep表示只保留包含active的指标(忽略大小写)
# sort表示按照字母顺序排序
[root@xiaolin ~]# cat /proc/meminfo | grep -i active | sort
Active: 901456 kB
Active(anon): 227252 kB
Active(file): 674204 kB
Inactive: 226232 kB
Inactive(anon): 41948 kB
Inactive(file): 184284 kB
1.3 性能影响⚓
回收内存的两种方式中,直接内存回收是同步回收的,会阻塞进程,这样就会造成很长时间的延迟,以及系统的 CPU 利用率会升高,最终引起系统负荷飙高。
可被回收的内存类型有文件页和匿名页:
- 文件页的回收:对于干净页是直接释放内存,这个操作不会影响性能,而对于脏页会先写回到磁盘再释放内存,这个操作会发生磁盘 I/O 的,这个操作是会影响系统性能的。
- 匿名页的回收:如果开启了 Swap 机制,那么 Swap 机制会将不常访问的匿名页换出到磁盘中,下次访问时,再从磁盘换入到内存中,这个操作是会影响系统性能的。
1.3.1 解决方式⚓
1.3.1.1 调整文件页和匿名页的回收倾向⚓
Linux 提供了一个 /proc/sys/vm/swappiness
选项,用来调整文件页和匿名页的回收倾向。
swappiness 的范围是 0~100
,数值越大,越积极使用 Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页。
一般建议 swappiness 设置为 0(默认值是 60),这样在回收内存的时候,会更倾向于文件页的回收,但是并不代表不会回收匿名页。
1.3.1.2 尽早触发 kswapd⚓
1.3.1.2.1 查看回收的指标⚓
使用 sar -B 1
命令:
后台内存回收和直接内存回收的指标:
pgscank/s
: kswapd 每秒扫描的 page 个数,也就是后台回收的个数。pgscand/s
: 应用程序在内存申请过程中每秒直接扫描的 page 个数,也就是直接回收的个数。pgsteal/s
: 扫描的 page 中每秒被回收的个数(pgscank+pgscand),即两者之和。
如果系统时不时发生抖动,并且在抖动的时间段里如果通过 sar -B
观察到 pgscand
数值很大,那大概率是因为「直接内存回收」导致的。
针对这个问题,解决的办法就是,可以通过尽早的触发「后台内存回收」来避免应用程序进行直接内存回收。
1.3.1.2.2 触发 kswapd⚓
内核定义了三个内存阈值(watermark,也称为水位),用来衡量当前剩余内存(pages_free)是否充裕或者紧张,分别是:
kswapd 会定期扫描内存的使用情况,根据剩余内存(pages_free)的情况来进行内存回收的工作。
- 如果 剩余内存 (pages_free) 大于 页高阈值 (pages_high),说明剩余内存是充足的;
- pages_free 在 pages_high ~ pages_low 之间,说明内存有一定压力,但还可以满足应用程序申请内存的请求;
- pages_free 在 pages_high ~ pages_min 之间,说明内存压力比较大。此时 kswapd0 会执行内存回收,直到 pages_free 的值大于 pages_high 为止。
- pages_free 小于 pages_min ,说明用户可用内存都耗尽了。此时就会触发直接内存回收,这时应用程序就会被阻塞,因为两者关系是同步的。
页低阈值(pages_low)可以通过内核选项 /proc/sys/vm/min_free_kbytes
(该参数代表系统所保留空闲内存的最低限)来间接设置。页高阈值(pages_high)和页低阈值(pages_low)都是根据页最小阈值(pages_min)计算生成的:
pages_min = min_free_kbytes
pages_low = pages_min*5/4
pages_high = pages_min*3/2
增大了 min_free_kbytes
配置后,这会使得系统预留过多的空闲内存,从而在一定程度上降低了应用程序可使用的内存量,这在一定程度上浪费了内存。极端情况下设置 min_free_kbytes 接近实际物理内存大小时,留给应用程序的内存就会太少而可能会频繁地导致 OOM 的发生。
所以在调整 min_free_kbytes 之前,需要先思考一下,应用程序更加关注什么,如果关注延迟那就适当地增大 min_free_kbytes,如果关注内存的使用量那就适当地调小 min_free_kbytes。
1.3.1.3 NUMA 架构下的内存回收策略⚓
1.3.1.3.1 SMP 架构⚓
对称多处理器 SMP (Symmetric Multi-Processing)指的是一种多个 CPU 处理器共享资源的电脑硬件架构,也就是说每个 CPU 地位平等,它们共享相同的物理资源,包括总线、内存、IO、操作系统等。每个 CPU 访问内存所用时间都是相同的,因此,这种系统也被称为一致存储访问结构(UMA,Uniform Memory Access)
。
随着 CPU 处理器核数的增多,多个 CPU 都通过一个总线访问内存,这样总线的带宽压力会越来越大,同时每个 CPU 可用带宽会减少,这也就是 SMP 架构的问题。
1.3.1.3.2 NUMA 架构⚓
为了解决 SMP 架构的问题,就研制出了 NUMA 结构,即非一致存储访问结构(Non-uniform memory access,NUMA)
。
NUMA 架构将每个 CPU 进行了分组,每一组 CPU 用 Node 来表示,一个 Node 可能包含多个 CPU。
每个 Node 有自己独立的资源,包括内存、IO 等,每个 Node 之间可以通过互联模块总线(QPI)
进行通信,所以,也就意味着每个 Node 上的 CPU 都可以访问到整个系统中的所有内存。但是,访问远端 Node 的内存比访问本地内存要耗时很多。
在 NUMA 架构下,当某个 Node 内存不足时,可以通过 /proc/sys/vm/zone_reclaim_mode
来控制回收策略:
0
(默认值):在回收本地内存之前,在其他 Node 寻找空闲内存;1
:只回收本地内存;2
:只回收本地内存,在本地回收内存时,可以将文件页中的脏页写回硬盘,以回收内存。4
:只回收本地内存,在本地回收内存时,可以用 swap 方式回收内存。
在使用 NUMA 架构的服务器,如果系统出现还有一半内存的时候,却发现系统频繁触发「直接内存回收」,导致了影响了系统性能,那么大概率是因为 zone_reclaim_mode
没有设置为 0,导致当本地内存不足的时候,只选择回收本地内存的方式,而不去使用其他 Node 的空闲内存。
1.4 如何保护一个进程不被 OOM 杀掉⚓
Linux 内核里有一个 oom_badness()
函数,它会把系统中可以被杀掉的进程扫描一遍,并对每个进程打分,得分最高的进程就会被首先杀掉。
进程得分的结果受下面这两个方面影响:
- 进程使用的物理内存页面数
- 每个进程的 OOM 校准值
/proc/[pid]/oom_score_adj
,调整范围是-1000~1000
函数 oom_badness() 里的最终计算方法是这样的:
// points 代表打分的结果
// process_pages 代表进程已经使用的物理内存页面数
// oom_score_adj 代表 OOM 校准值
// totalpages 代表系统总的可用页面数
points = process_pages + oom_score_adj*totalpages/1000
每个进程的 oom_score_adj
默认值都为 0,所以,消耗的内存越大越容易被杀掉。
- 如果你不想某个进程被首先杀掉,那你可以调整该进程的 oom_score_adj,从而改变这个进程的得分结果,降低该进程被 OOM 杀死的概率。
- 如果你想某个进程无论如何都不能被杀掉,那你可以将 oom_score_adj 配置为
-1000
。
最好将一些很重要的系统服务的 oom_score_adj 配置为 -1000,比如 sshd,因为这些系统服务一旦被杀掉,我们就很难再登陆进系统了。
但是,不建议将我们自己的业务程序的 oom_score_adj 设置为 -1000,因为业务程序一旦发生了内存泄漏,而它又不能被杀掉,这就会导致随着它的内存开销变大,OOM killer 不停地被唤醒,从而把其他进程一个个给杀掉。
1.5 内存超分⚓
- 在 32 位操作系统,因为进程理论上最大能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败。
- 在 64 位 位操作系统,因为进程理论上最大能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区:
- 如果没有 Swap 分区,因为物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出);
- 如果有 Swap 分区,即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行;
即使有了 swap 分区,也不能无限制的分配虚拟内存。
因为申请虚拟内存的过程中,还是使用到了物理内存(比如内核保存虚拟内存的数据结构,也是占用物理内存的)。
当系统多次尝试回收内存,如果还是无法满足所需使用的内存大小,进程就会被系统 kill 掉了。
1.5.1 swap⚓
Swap 就是把一块磁盘空间或者本地文件,当成内存来使用,它包含换出和换入两个过程:
- 换出(Swap Out) ,是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存;
- 换入(Swap In),是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来;
Linux 中的 Swap 机制会在内存不足和内存闲置的场景下触发:
- 内存不足:当系统需要的内存超过了可用的物理内存时,内核会将内存中不常使用的内存页交换到磁盘上为当前进程让出内存,保证正在执行的进程的可用性,这个内存回收的过程是强制的直接内存回收(Direct Page Reclaim)。直接内存回收是同步的过程,会阻塞当前申请内存的进程。
- 内存闲置:应用程序在启动阶段使用的大量内存在启动后往往都不会使用,通过后台运行的守护进程(kSwapd),我们可以将这部分只使用一次的内存交换到磁盘上为其他内存的申请预留空间。kSwapd 是 Linux 负责页面置换(Page replacement)的守护进程,它也是负责交换闲置内存的主要进程,它会在空闲内存低于一定水位时,回收内存页中的空闲内存保证系统中的其他进程可以尽快获得申请的内存。kSwapd 是后台进程,所以回收内存的过程是异步的,不会阻塞当前申请内存的进程。个人感觉本质上还是内存不足导致的。
Linux 提供了两种不同的方法启用 Swap,分别是 Swap 分区(Swap Partition)和 Swap 文件(Swapfile),开启方法可以看这个资料:
Swap 分区
是硬盘上的独立区域,该区域只会用于交换分区,其他的文件不能存储在该区域上,我们可以使用 swapon -s 命令查看当前系统上的交换分区;Swap 文件
是文件系统中的特殊文件,它与文件系统中的其他文件也没有太多的区别;