虚拟内存与多级页表
对普通进程来说,能看到的其实是内核提供的虚拟内存,这些虚拟内存还需要通过页表,由系统映射为物理内存。
当进程通过 malloc() 申请虚拟内存后,系统并不会立即为其分配物理内存,而是在首次访 问时,才通过缺页异常陷入内核中分配内存。
内存缓存
为了协调 CPU 与磁盘间的性能差异,Linux 还会使用 Cache 和 Buffer ,分别把文件和磁 盘读写的数据缓存到内存中。
内存回收
一旦发现内存紧张,系统会通 过三种方式回收内存:
- 基于 LRU(Least Recently Used)算法,回收缓存;
- 基于 Swap 机制,回收不常访问的匿名页;
- 基于 OOM(Out of Memory)机制,杀掉占用大量内存的进程。
直接内存回收:缓存回收和 Swap 回收
实际上都是基于 LRU 算法,也就是优先回收不常访 问的内存。LRU 回收算法,实际上维护着 active 和 inactive 两个双向链表,其中:
- active 记录活跃的内存页;
- inactive 记录非活跃的内存页。
越接近链表尾部,就表示内存页越不常访问。这样,在回收内存时,系统就可以根据活跃程 度,优先回收不活跃的内存。活跃和非活跃的内存页,按照类型的不同,又分别分为文件页和匿名页,对应着缓存回收和 Swap 回收。当然,你可以从 /proc/meminfo 中,查询它们的大小:
OOM
OOM 机制按照 oom_score 给进程排序。oom_score 越大,进程就越容易 被系统杀死。
当系统发现内存不足以分配新的内存请求时,就会尝试直接内存回收:如果回 收完文件页和匿名页后,内存够用了,把回收回来的内存分配给进程就可以 了。
但如果内存还是不足,OOM 就要登场了。OOM 发生时,你可以在 dmesg 中看到 Out of memory 的信息,从而知道是哪些进程被 OOM 杀死了。比如,你可以执行下面的命令,查询 OOM 日志:
如果你不希望应用程序被 OOM 杀死,可以调整进程的 oom_score_adj,减小 OOM 分值,进而降低被杀死的概率。或者,你还可以开启内存的 overcommit,允许进程 申请超过物理内存的虚拟内存(这儿实际上假设的是,进程不会用光申请到的虚拟内存)。
内存泄漏
如果在程序中直接 或间接地分配了动态内存,你一定要记得释放掉它们,否则就会导致内存泄漏,严重时甚至 会耗尽系统内存。
swap
在内存资源紧张时,Linux 通过直接内存回收和定期扫描的方式,来释放文件页和匿名页, 以便把内存分配给更需要的进程使用。
- 文件页的回收比较容易理解,直接清空,或者把脏数据写回磁盘后再释放。
- 而对匿名页的回收,需要通过 Swap 换出到磁盘中,下次访问时,再从磁盘换入到内存 中。
可以设置 /proc/sys/vm/min_free_kbytes,来调整系统定期回收内存的阈值(也就是页 低阈值),还可以设置 /proc/sys/vm/swappiness,来调整文件页和匿名页的回收倾向。
在 NUMA 架构下,每个 Node 都有自己的本地内存空间,而当本地内存不足时,默认既 可以从其他 Node 寻找空闲内存,也可以从本地内存回收。
你可以设置 /proc/sys/vm/zone_reclaim_mode ,来调整 NUMA 本地内存的回收策略。
当 Swap 变高时,你可以用 sar、/proc/zoneinfo、/proc/pid/status 等方法,查看系统 和进程的内存使用情况,进而找出 Swap 升高的根源和受影响的进程。
降低 Swap 的使用,可以提高系统的整体性能:
- 禁止 Swap,现在服务器的内存足够大,所以除非有必要,禁用 Swap 就可以了。随着 云计算的普及,大部分云平台中的虚拟机都默认禁止 Swap。
- 如果实在需要用到 Swap,可以尝试降低 swappiness 的值,减少内存回收时 Swap 的 使用倾向。
- 响应延迟敏感的应用,如果它们可能在开启 Swap 的服务器中运行,你还可以用库函数 mlock() 或者 mlockall() ****锁定内存,阻止它们的内存换出。
读写普通文件(文件系统)与读写块设备文件(磁盘或分区)的区别
我们通常说的“文件”,其实是指普通文件。而磁盘或者分区,则是指块设备文件。Linux 中一切皆文件。换句话说,你可以通过相同的文件 接口,来访问磁盘和文件(比如 open、read、write、close 等)。
- 在读写普通文件时,I/O 请求会首先经过文件系统,然后由文件系统负责,来与磁盘进行交 互。
- 在读写块设备文件时,会跳过文件系统,直接与磁盘交互,也就是所谓的“裸 I/O”。
这两种读写方式使用的缓存自然不同。文件系统管理的缓存,其实就是 Cache 的一部分。 而裸磁盘的缓存,用的正是 Buffer。
磁盘是一个存储设备(确切地说是块设备),可以被划分为不同的磁盘分区。而在磁盘或者 磁盘分区上,还可以再创建文件系统,并挂载到系统的某个目录中。这样,系统就可以通过 这个挂载目录,来读写文件。
磁盘是存储数据的块设备,也是文件系统的载体,文件系统确实还是要通过磁盘,来保证数据的持久化存储。
如何统计所有进程的物理内存使用量
# 使用 grep 查找 Pss 指标后,再用 awk 计算累加值
grep Pss /proc/[1-9]*/smaps | awk '{total+=$2}; END {printf "%d kB\n", total }'
391266 kB
确定进程使用的内存大小
要确定一个进程或者容器的最小内存,最简单的方法就是让它运行起来,再通过 ps 或者 smap ,查看它的内存使用情况。不过要注意,进程刚启动时,可能还没开始处理实际 业务,一旦开始处理实际业务,就会占用更多内存。所以,要记得给内存留一定的余 量。
定位系统的内存问题
内存性能指标--衡量内存的性能
系统内存使用情况
比如已用内存、剩余内存、共享内存、可用内 存、缓存和缓冲区的用量等。
- 共享内存是通过 tmpfs 实现的,所以它的大小也就是 tmpfs 使用的内存大小。tmpfs 其 实也是一种特殊的缓存。
- 可用内存是新进程可以使用的最大内存,它包括剩余内存和可回收缓存。
- 缓存包括两部分,一部分是磁盘读取文件的页缓存,用来缓存从磁盘读取的数据,可以加快以后再次访问的速度。另一部分,则是 Slab 分配器中的可回收内存。
- 缓冲区是对原始磁盘块的临时存储,用来缓存将要写入磁盘的数据。这样,内核就可以把 分散的写集中起来,统一优化磁盘写入。
进程内存使用情况
比如进程的虚拟内存、常驻内存、共享内存以及 Swap 内存等。
- 虚拟内存,包括了进程代码段、数据段、共享内存、已经申请的堆内存和已经换出的内存 等。这里要注意,已经申请的内存,即使还没有分配物理内存,也算作虚拟内存。
- 常驻内存是进程实际使用的物理内存,不过,它不包括 Swap 和共享内存。 常驻内存一般会换算成占系统总内存的百分比,也就是进程的内存使用 率。
- 共享内存,既包括与其他进程共同使用的真实的共享内存,还包括了加载的动态链接库以及程序的代码段等。
- Swap 内存,是指通过 Swap 换出到磁盘的内存。
缺页异常
系统调用内存分配请求后,并不会立刻为其分配物理 内存,而是在请求首次访问时,通过缺页异常来分配。缺页异常又分为下面两种场景。
- 可以直接从物理内存中分配时,被称为次缺页异常。
- 需要磁盘 I/O 介入(比如 Swap)时,被称为主缺页异常。主缺页异常升高,就意味着需要磁盘 I/O,那么内存访问也会慢很多。
Swap 的使用情况
比如 Swap 的已用空 间、剩余空间、换入速度和换出速度等。
- 换入和换出速度,则表示每秒钟换入和换出内存的大小。
内存性能工具--获得这些指标
- free:查 看系统的整体内存和 Swap 使用情况。相对应的,
- top 或 ps,查看进程的内存使 用情况。
- proc 文件系统,内存指标的来源;
- vmstat,动态观察了内存的变化情况。与 free 相比,vmstat 除了可以动态查看内存 变化,还可以区分缓存和缓冲区、Swap 换入和换出的内存大小。
- cachestat ,查看整个系统缓存的读写命中情况,并用 cachetop 来观察每个进程缓存的读写命中情 况。
- memleak,确认发生了内存泄漏。通过 memleak 给出的内存分配栈,找到内存泄 漏的可疑位置。
- sar 发现了缓冲区和 Swap 升高的问题
- cachetop,找到缓冲区升高的根源;通过对比剩余内存跟 /proc/zoneinfo 的内存 阈,我们发现 Swap 升高是内存回收导致的。
- 通过 /proc 文件系统,找 出了 Swap 所影响的进程。
迅速定位内存问题
通常会先运行几个覆盖面比较大的性能工具,比如 free、top、vmstat、pidstat 等。
-
先用 free 和 top,查看系统整体的内存使用情况。
-
再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断出内存问题的类型。
-
最后进行详细分析,比如内存分配分析、缓存 / 缓冲区分析、具体进程的内存使用分析 等。
第一个例子,当你通过 free,发现大部分内存都被缓存占用后,可以使用 vmstat 或者 sar 观察一下缓存的变化趋势,确认缓存的使用是否还在继续增大。如果继续增大,则说明导致缓存升高的进程还在运行,那你就能用缓存 / 缓冲区分析工具 (比如 cachetop、slabtop 等),分析这些缓存到底被哪里占用。
第二个例子,当你 free 一下,发现系统可用内存不足时,首先要确认内存是否被缓存 / 缓 冲区占用。排除缓存 / 缓冲区后,你可以继续用 pidstat 或者 top,定位占用内存最多的进程。找出进程后,再通过进程内存空间工具(比如 pmap),分析进程地址空间中内存的使用 情况就可以了。
第三个例子,当你通过 vmstat 或者 sar 发现内存在不断增长后,可以分析中是否存在内存 泄漏的问题。比如你可以使用内存分配分析工具 memleak ,检查是否存在内存泄漏。如果存在内存泄漏 问题,memleak 会为你输出内存泄漏的进程以及调用堆栈。
内存调优总结
最重要的就是,保证应用程序的热点数据放到内存中,并尽量减少换页和交换。常见的优化思路有这么几种:
- 最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。
- 减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等。不用动态内存分配的方法,而是用数组来暂存计算结果。这样就可以由系统自动 管理这些栈内存,也不存在内存泄漏的问题了。
- 尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问。
- 使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进 程耗尽。
- 通过 /proc/pid/oom_adj ,调整核心应用的 oom_score。这样,可以保证即使内存紧 张,核心应用也不会被 OOM 杀死。