19讲案例篇:为什么系统的Swap变⾼了(上)

上⼀节,我通过⼀个斐波那契数列的案例,带你学习了内存泄漏的分析。如果在程序中直接或间接地分配了动态内存,你⼀定
要记得释放掉它们,否则就会导致内存泄漏,严重时甚⾄会耗尽系统内存。
 
不过,反过来讲,当发⽣了内存泄漏时,或者运⾏了⼤内存的应⽤程序,导致系统的内存资源紧张时,系统⼜会如何应对呢?
在内存基础篇我们已经学过,这其实会导致两种可能结果,内存回收和 OOM 杀死进程。
 
我们先来看后⼀个可能结果,内存资源紧张导致的 OOM(Out Of Memory),相对容易理解,指的是系统杀死占⽤⼤量内存
的进程,释放这些内存,再分配给其他更需要的进程。
这⼀点我们前⾯详细讲过,这⾥就不再重复了。
 
接下来再看第⼀个可能的结果,内存回收,也就是系统释放掉可以回收的内存,⽐如我前⾯讲过的缓存和缓冲区,就属于可回
收内存。它们在内存管理中,通常被叫做⽂件⻚(File-backed Page)
 
⼤部分⽂件⻚,都可以直接回收,以后有需要时,再从磁盘重新读取就可以了。⽽那些被应⽤程序修改过,并且暂时还没写⼊
磁盘的数据(也就是脏⻚),就得先写⼊磁盘,然后才能进⾏内存释放。
 
这些脏⻚,⼀般可以通过两种⽅式写⼊磁盘。
可以在应⽤程序中,通过系统调⽤ fsync ,把脏⻚同步到磁盘中;
也可以交给系统,由内核线程 pdflflush 负责这些脏⻚的刷新。
除了缓存和缓冲区,通过内存映射获取的⽂件映射⻚,也是⼀种常⻅的⽂件⻚。它也可以被释放掉,下次再访问的时候,从⽂
件重新读取。
 
除了⽂件⻚外,还有没有其他的内存可以回收呢?⽐如,应⽤程序动态分配的堆内存,也就是我们在内存管理中说到的匿名
⻚(Anonymous Page),是不是也可以回收呢?
 
我想,你肯定会说,它们很可能还要再次被访问啊,当然不能直接回收了。⾮常正确,这些内存⾃然不能直接释放。
但是,如果这些内存在分配后很少被访问,似乎也是⼀种资源浪费。是不是可以把它们暂时先存在磁盘⾥,释放内存给其他更
需要的进程?
 
其实,这正是Linux的Swap机制。Swap把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使
⽤。再次访问这些内存时,重新从磁盘读⼊内存就可以了
 
在前⼏节的案例中,我们已经分别学过缓存和OOM的原理和分析。那Swap ⼜是怎么⼯作的呢?因为内容⽐较多,接下来,
我将⽤两节课的内容,带你探索Swap的⼯作原理,以及Swap升⾼后的分析⽅法。
今天我们先来看看,Swap究竟是怎么⼯作的。
 
Swap原理
前⾯提到,Swap说⽩了就是把⼀块磁盘空间或者⼀个本地⽂件(以下讲解以磁盘为例),当成内存来使⽤。它包括换出和换
⼊两个过程。
 
所谓换出,就是把进程暂时不⽤的内存数据存储到磁盘中,并释放这些数据占⽤的内存。
换⼊,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来
 
所以你看,Swap其实是把系统的可⽤内存变⼤了。这样,即使服务器的内存不⾜,也可以运⾏⼤内存的应⽤程序。
 
还记得我最早学习Linux操作系统时,内存实在太贵了,⼀个普通学⽣根本就⽤不起⼤的内存,那会⼉我就是开启了Swap来运
⾏Linux桌⾯。当然,现在的内存便宜多了,服务器⼀般也会配置很⼤的内存,那是不是说Swap就没有⽤武之地了呢?
当然不是。事实上,内存再⼤,对应⽤程序来说,也有不够⽤的时候。
 
⼀个很典型的场景就是,即使内存不⾜时,有些应⽤程序也并不想被OOM杀死,⽽是希望能缓⼀段时间,等待⼈⼯介⼊,或
者等系统⾃动释放其他进程的内存,再分配给它。
 
除此之外,我们常⻅的笔记本电脑的休眠和快速开机的功能,也基于Swap 。休眠时,把系统的内存存⼊磁盘,这样等到再次
开机时,只要从磁盘中加载内存就可以。这样就省去了很多应⽤程序的初始化过程,加快了开机速度。
 
话说回来,既然Swap是为了回收内存,那么Linux到底在什么时候需要回收内存呢?前⾯⼀直在说内存资源紧张,⼜该怎么来
衡量内存是不是紧张呢?
 
⼀个最容易想到的场景就是,有新的⼤块内存分配请求,但是剩余内存不⾜。这个时候系统就需要回收⼀部分内存(⽐如前⾯
提到的缓存),进⽽尽可能地满⾜新内存请求。这个过程通常被称为直接内存回收
 
除了直接内存回收,还有⼀个专⻔的内核线程⽤来定期回收内存,也就是kswapd0。为了衡量内存的使⽤情况,kswapd0定
义了三个内存阈值(watermark,也称为⽔位),分别是
⻚最⼩阈值(pages_min)、⻚低阈值(pages_low)和⻚⾼阈值(pages_high)剩余内存,则使⽤ pages_free 表示
这⾥,我画了⼀张图表示它们的关系。

kswapd0定期扫描内存的使⽤情况,并根据剩余内存落在这三个阈值的空间位置,进⾏内存的回收操作。

a. 剩余内存⼩于⻚最⼩阈值,说明进程可⽤内存都耗尽了,只有内核才可以分配内存。
b. 剩余内存落在⻚最⼩阈值和⻚低阈值中间,说明内存压⼒⽐较⼤,剩余内存不多了。这时kswapd0会执⾏内存回收,直到
剩余内存⼤于⾼阈值为⽌。
c. 剩余内存落在⻚低阈值和⻚⾼阈值中间,说明内存有⼀定压⼒,但还可以满⾜新内存请求。
d. 剩余内存⼤于⻚⾼阈值,说明剩余内存⽐较多,没有内存压⼒。
 
我们可以看到,⼀旦剩余内存⼩于⻚低阈值,就会触发内存的回收。这个⻚低阈值,其实可以通过内核选项
/proc/sys/vm/min_free_kbytes 来间接设置。min_free_kbytes 设置了⻚最⼩阈值,⽽其他两个阈值,都是根据⻚最⼩阈值计
算⽣成的,计算⽅法如下 : 
pages_low = pages_min*5/4 
pages_high = pages_min*3/2
 
NUMA与Swap
很多情况下,你明明发现了 Swap 升⾼,可是在分析系统的内存使⽤时,却很可能发现,系统剩余内存还多着呢。为什么剩余
内存很多的情况下,也会发⽣ Swap 呢?
 
看到上⾯的标题,你应该已经想到了,这正是处理器的 NUMA (Non-Uniform Memory Access)架构导致的。
 
关于 NUMA,我在 CPU 模块中曾简单提到过。在 NUMA 架构下,多个处理器被划分到不同 Node 上,且每个 Node 都拥有
⾃⼰的本地内存空间。
 
⽽同⼀个 Node 内部的内存空间,实际上⼜可以进⼀步分为不同的内存域(Zone),⽐如直接内存访问区(DMA)、普通内
存区(NORMAL)、伪内存区(MOVABLE)等,如下图所示:

先不⽤特别关注这些内存域的具体含义,我们只要会查看阈值的配置,以及缓存、匿名⻚的实际使⽤情况就够了。

既然 NUMA 架构下的每个 Node 都有⾃⼰的本地内存空间,那么,在分析内存的使⽤时,我们也应该针对每个 Node 单独分
析。
 
你可以通过 numactl 命令,来查看处理器在 Node 的分布情况,以及每个 Node 的内存使⽤情况。⽐如,下⾯就是⼀个
numactl 输出的示例:
$ numactl --hardware 
available: 1 nodes (0) 
node 0 cpus: 0 1 
node 0 size: 7977 MB 
node 0 free: 4416 MB 
...
这个界⾯显示,我的系统中只有⼀个 Node,也就是Node 0 ,⽽且编号为 0 和 1 的两个 CPU, 都位于 Node 0 上。另
外,Node 0 的内存⼤⼩为 7977 MB,剩余内存为 4416 MB。
 
了解了 NUNA 的架构和 NUMA 内存的查看⽅法后,你可能就要问了这跟 Swap 有什么关系呢?
实际上,前⾯提到的三个内存阈值(⻚最⼩阈值、⻚低阈值和⻚⾼阈值),都可以通过内存域在 proc ⽂件系统中的接⼝
/proc/zoneinfo 来查看。
 
⽐如,下⾯就是⼀个 /proc/zoneinfo ⽂件的内容示例:
$ cat /proc/zoneinfo 
... 
Node 0, zone Normal 
  pages free 227894 
  min 14896 
  low 18620 
  high 22344 
... 
  nr_free_pages 227894 
  nr_zone_inactive_anon 11082 
  nr_zone_active_anon 14024 
  nr_zone_inactive_file 539024 
  nr_zone_active_file 923986 
...
这个输出中有⼤量指标,我来解释⼀下⽐较重要的⼏个。
pages处的min、low、high,就是上⾯提到的三个内存阈值,⽽free是剩余内存⻚数,它跟后⾯的 nr_free_pages 相同。
nr_zone_active_anon和nr_zone_inactive_anon,分别是活跃和⾮活跃的匿名⻚数。
nr_zone_active_fifile和nr_zone_inactive_fifile,分别是活跃和⾮活跃的⽂件⻚数
从这个输出结果可以发现,剩余内存远⼤于⻚⾼阈值,所以此时的 kswapd0 不会回收内存。
 
当然,某个 Node 内存不⾜时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。具体选哪种模式,你可
以通过 /proc/sys/vm/zone_reclaim_mode 来调整。它⽀持以下⼏个选项:
默认的 0 ,也就是刚刚提到的模式,表示既可以从其他 Node 寻找空闲内存,也可以从本地回收内存。
1、2、4 都表示只回收本地内存,1表示内存回收只会发生在本地节点内,2 表示可以回写脏数据回收内存(在本地回收内存时,可以将cache中的脏数据写回硬盘,以回收内存),4 表示可以⽤ Swap ⽅式回收内存。
 
swappiness
到这⾥,我们就可以理解内存回收的机制了。这些回收的内存既包括了⽂件⻚,⼜包括了匿名⻚
对⽂件⻚的回收,当然就是直接回收缓存,或者把脏⻚写回磁盘后再回收。
对匿名⻚的回收,其实就是通过 Swap 机制,把它们写⼊磁盘后再释放内存
 
不过,你可能还有⼀个问题。既然有两种不同的内存回收机制,那么在实际回收内存时,到底该先回收哪⼀种呢?
 
其实,Linux提供了⼀个 /proc/sys/vm/swappiness 选项,⽤来调整使⽤Swap的积极程度
swappiness的范围是0-100,数值越⼤,越积极使⽤Swap,也就是更倾向于回收匿名⻚;
数值越⼩,越消极使⽤Swap,也就是更倾向于回收⽂件⻚。
虽然 swappiness 的范围是 0-100,不过要注意,这并不是内存的百分⽐,⽽是调整 Swap 积极程度的权重,即使你把它设置
成0,当剩余内存+⽂件⻚⼩于⻚⾼阈值时,还是会发⽣Swap。
 
清楚了 Swap 原理后,当遇到 Swap 使⽤变⾼时,⼜该怎么定位、分析呢?别急,下⼀节,我们将⽤⼀个案例来探索实践。
 
⼩结
在内存资源紧张时,Linux通过直接内存回收和定期扫描的⽅式,来释放⽂件⻚和匿名⻚,以便把内存分配给更需要的进程使
⽤。
 
⽂件⻚的回收⽐较容易理解,直接清空,或者把脏数据写回磁盘后再释放。
⽽对匿名⻚的回收,需要通过Swap换出到磁盘中,下次访问时,再从磁盘换⼊到内存中。
 
可以设置/proc/sys/vm/min_free_kbytes,来调整系统定期回收内存的阈值(也就是⻚低阈值),还可以设
置/proc/sys/vm/swappiness,来调整⽂件⻚和匿名⻚的回收倾向
 
在 NUMA 架构下,每个 Node 都有⾃⼰的本地内存空间,⽽当本地内存不⾜时,默认既可以从其他 Node 寻找空闲内存,也
可以从本地内存回收。
 
你可以设置 /proc/sys/vm/zone_reclaim_mode ,来调整NUMA本地内存的回收策略。
 
思考
最后,我想请你⼀起来聊聊你理解的 SWAP。我估计你以前已经碰到过 Swap 导致的性能问题,你是怎么分析这些问题的
呢?你可以结合今天讲的 Swap 原理,记录⾃⼰的操作步骤,总结⾃⼰的解决思路。 
 
 
posted @ 2021-05-18 11:08  求其在我  阅读(87)  评论(0编辑  收藏  举报