内存性能优化之套路篇

此篇文章为我个人学习笔记,出自倪朋飞老师的Linux性能优化实战,强烈建议购买:https://time.geekbang.org/column/intro/140

前言

内存,在计算机里面必不可少,也是明白其重要性,但是内存里面的一些原因,对于运维同学很是非常不理解,如虚拟内存、TLB、文件页、匿名页、主要缺页异常等,此文从linux系统来讲解下内存的原理。以及内存性能优化的方法。

虚拟内存、MMU与TLB

虚拟地址与MMU的由来

我们知道,CPU是通过寻址来访问内存的。32位CPU的寻址宽度是 0~0xFFFFFFFF ,计算后得到的大小是4G,也就是说可支持的物理内存最大是4G。

但在实践过程中,碰到了这样的问题,程序需要使用4G内存,而可用物理内存小于4G,导致程序不得不降低内存占用。为了解决此类问题,现代CPU引入了 MMU(Memory Management Unit 内存管理单元)。MMU 的核心思想是利用虚拟地址替代物理地址,即CPU寻址时使用虚址,由 MMU 负责将虚址映射为物理地址。 MMU的引入,解决了对物理内存的限制,对程序来说,就像自己在使用4G内存一样。所以linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的,进程就可以很方便地访问内存,更确切地说是访问虚拟内存。也就是说应用程序申请的内存只是虚拟内存,内核是不会分配物理内存给应用程序的,只有应用程序在使用的时候才会申请实际的物理内存,看上去内核是有一点小气的~

虚拟地址空间的内部又被分为内核空间和用户空间两部分,32 位系统的内核空间占用 1G,位于最高处,剩下的 3G 是用户空间。而 64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。

进程在用户态时,只能访问用户空间内存;只有进入内核态后,才可以访问内核空间内存。虽然每个进程的地址空间都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。

既然每个进程都有一个这么大的地址空间,那么所有进程的虚拟内存加起来,自然要比实际的物理内存大得多。所以,并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的

内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系

image

页表实际上存储在 CPU 的内存管理单元 MMU 中,这样,正常情况下,处理器就可以直接通过硬件,找出要访问的内存。

MMU与TLB

MMU包括了TLB和Translation Table Walk。

从虚拟地址到物理地址的转换过程可知:使用一级页表进行地址转换时,每次读/写数据需要访问两次内存,第一次访问一级页表获得物理地址,第二次才是真正的读/写数据;使用两级页表时,每次读/写数据需要访问三次内存,访问两次页表(一级页表和二级页表)获得物理地址,第三次才是真正的读/写数据。

上述的地址转换过程打打降低了CPU的性能,有没有办法改进呢?程序执行过程中,所用到的指令、数据的地址往往集中在一个很小的范围内,其中的地址、数据经常多次使用,这称为程序访问的局部性。由此,通过使用一个高速、容量相对较小的存储器来存储近期用到的页表条目(段/大页/小页/极小页描述符),以避免每次地址转换时都到主存去查找,这样可以大幅度地提高性能。这个存储器用来帮助快速地进行地址转换,称为“转译查找缓存”(TLB)

img

如上图,当CPU发出一个虚拟地址时,MMU首先访问TLB,如果TLB中含有能转换这个虚拟地址的描述符,则直接利用此描述符进行地址转换和权限检查;否则MMU访问页表找到描述符后再进行地址转换和权限检查,并将这个描述符填入TLB中(如果TLB已满,则利用round-robin算法找到一个条目,然后覆盖它),下次再使用这个虚拟地址时就可以直接使用TLB中的地址描述符了。

由于进程的虚拟地址空间是独立的,而 TLB 的访问速度又比 MMU 快得多,所以,通过减少进程的上下文切换,减少 TLB 的刷新次数,就可以提高 TLB 缓存的使用率,进而提高 CPU 的内存访问性能

MMU 并不以字节为单位来管理内存,而是规定了一个内存映射的最小单位,也就是页表,通常是 4 KB 大小。这样,每一次内存映射,都需要关联 4 KB 或者 4KB 整数倍的内存空间。

引入分页模式的好处,可以大致概括为两个方面:

  • 允许虚存空间远大于实际物理内存大小的情况。这是因为,分页之后,操作系统读入磁盘的文件时,无需以文件为单位全部读入,而可以以内存页为单位,分片读入。同时,考虑到 CPU 不可能一次性需要使用整个内存中的数据,因此可以交由特定的算法,进行内存调度:将长时间不用的页帧内的数据暂存到磁盘上。
  • 减少了内存碎片的产生。这是因为,引入分页之后,内存的分配管理都是以页大小(通常是 4KiB,扩展分页模式下是 4MiB)为单位的;虚拟内存中的页总是对应物理内存中实际的页帧。这样一来,在虚拟内存空间中,页内连续的内存在物理内存上也一定是连续的,不会产生碎片。

缺页异常

当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

缺页错误可以分为两类:硬缺页错误(Hard Page Fault)和软缺页错误(Soft Page Fault)。这里,前者又称为主要缺页错误(Major Page Fault);后者又称为次要缺页错误(Minor Page Fault)。当缺页中断发生后,Page Fault Handler 会判断缺页的类型,进而处理缺页错误,最终将控制权交给用户态代码。

若是此时物理内存里,已经有一个页帧正是此时 CPU 请求的内存页,那么这是一个软缺页错误;于是,Page Fault Hander 会指示 MMU 建立相应的页帧到页的映射关系。这一操作的实质是进程间共享内存——比如动态库(共享对象),比如 mmap 的文件。

若是此时物理内存中,没有相应的页帧,那么这就是一个硬缺页错误;于是 Page Fault Hander 会指示 CPU,从已经打开的磁盘文件中读取相应的内容到物理内存,而后交由 MMU 建立这份页帧到页的映射关系。

不难发现,软缺页错误只是在内核态里轻轻地走了一遭,而硬缺页错误则涉及到磁盘 I/O。因此,处理起来,硬缺页错误要比软缺页错误耗时长得多。这就是为什么我们要求高性能程序必须在对外提供服务时,尽可能少地发生硬缺页错误。

大页

现代的计算机系统,都支持非常大的虚拟地址空间(2^32~2^64)。在这样的环境下,页表就变得非常庞大。例如,假设页大小为4K,对占用40G内存的程序来说,页表大小为10M,而且还要求空间是连续的。为了解决空间连续问题,可以引入二级或者三级页表。但是这更加影响性能,因为如果快表缺失,访问页表的次数由两次变为三次或者四次。由于程序可以访问的内存空间很大,如果程序的访存局部性不好,则会导致快表一直缺失,从而严重影响性能。

此外,由于页表项有10M之多,而TLB快表只能缓存几百页,即使程序的访存性能很好,在大内存耗费情况下,快表缺失的概率也很大。那么,有什么好的方法解决快表缺失吗?大页内存!假设我们将页大小变为1G,40G内存的页表项也只有40,快表完全不会缺失!即使缺失,由于表项很少,可以采用一级页表,缺失只会导致两次访存。这就是大页内存可以优化程序性能的根本原因—快表几乎不缺失!

Linux下的大页分为两种类型:标准大页(Huge Pages)和透明大页(Transparent Huge Pages)。Huge Pages有时候也翻译成大页/标准大页/传统大页,它们都是Huge Pages的不同中文翻译名而已,顺带提一下这个,免得有人被这些名词给混淆、误导了。Huge Pages是从Linux Kernel 2.6后被引入的。目的是使用更大的内存页面(memory page size) 以适应越来越大的系统内存,让操作系统可以支持现代硬件架构的大页面容量功能。

透明大页(Transparent Huge Pages)缩写为THP,这个是RHEL 6(其它分支版本SUSE Linux Enterprise Server 11, and Oracle Linux 6 with earlier releases of Oracle Linux Unbreakable Enterprise Kernel 2 (UEK2))开始引入的一个功能。具体可以参考官方文档。这两者有啥区别呢?这两者的区别在于大页的分配机制,标准大页管理是预分配的方式,而透明大页管理则是动态分配的方式。相信有不少人将Huge Page和Transparent Huge Pages混为一谈。目前透明大页与传统HugePages联用会出现一些问题,导致性能问题和系统重启。

查看大页的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost ~]# grep Huge /proc/meminfo
AnonHugePages: 8192 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
[root@localhost ~]# cat /sys/devices/system/node/node*/meminfo | fgrep Huge
Node 0 AnonHugePages: 0 kB
Node 0 HugePages_Total: 0
Node 0 HugePages_Free: 0
Node 0 HugePages_Surp: 0
Node 1 AnonHugePages: 8192 kB
Node 1 HugePages_Total: 0
Node 1 HugePages_Free: 0
Node 1 HugePages_Surp: 0
  • AnonHugePages: 匿名 HugePages 数量,即透明大页面的值
  • HugePages_Total: 分配的页面数目,和Hugepagesize相乘后得到所分配的内存大小
  • HugePages_Free: 从来没有被使用过的Hugepages数目。
  • HugePages_Rsvd: 已经被分配预留但是还没有使用的page数目。
  • HugePages_Surp: surplus的缩写形式,表示池中大于/proc/sys/vm/nr_hugepages 中值的 HugePages 数量。
  • Hugepagesize: 页面大小

    HugePages_Free – HugePages_Rsvd 这部分是没有被使用到的内存,HugePages_Total-HugePages_Free+HugePages_Rsvd` 就是目前实例需要的页面数量.

设置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
## 按照100G分配,大页的数据量为 51200
echo 51200 > /proc/sys/vm/nr_hugepages

## 添加能够使用大页内存的用户组 ## uid=1000(mysql) gid=1002(dba) groups=1002(dba),4(adm)
echo 1002 > /proc/sys/vm/hugetlb_shm_group

## 设置内核允许的共享内存段的大小,这个是90G
echo 96636764160 > /proc/sys/kernel/shmmax

## 设置共享内存段的数量 共享内存页的大小是4k,90G 90*1024*1024/4
echo 23592960 > /proc/sys/kernel/shmall

## 设置永远不开启透明大页
echo never > /sys/kernel/mm/transparent_hugepage/enabled

## 设置永久关闭内存碎片整理
echo never > /sys/kernel/mm/transparent_hugepage/defrag

## 创建大页内存挂接点
mount -t hugetlbfs nodev /mnt/huge

## 开机启用大页
修改/etc/default/grub.conf,加入 transparent_hugepage=never default_hugepagesz=1G hugepagesz=1G hugepages=4

虚拟内存空间分布

首先,我们需要进一步了解虚拟内存空间的分布情况。最上方的内核空间不用多讲,下方的用户空间内存,其实又被分成了多个不同的段。如下:

image

通过这张图你可以看到,用户空间内存,从低到高分别是五种不同的内存段。

  • 只读段,也叫代码段,包括代码和常量等。由于是只读的,不会再去分配新的内存,所以也不会产生内存泄漏。
  • 数据段,包括全局变量等。这些变量在定义时就已经确定了大小,所以也不会产生内存泄漏。
  • 堆,包括动态分配的内存,从低地址开始向上增长。堆内存由应用程序自己来分配和管理。除非程序退出,这些堆内存并不会被系统自动释放,而是需要应用程序明确调用库函数 free() 来释放它们。很多时候,我们事先并不知道数据大小,所以你就要用到标准库函数 malloc() , 在程序中动态分配内存。这时候,系统就会从内存空间的堆中分配内存。
  • 文件映射段,包括动态库、共享内存等,从高地址开始向下增长。共享内存由程序动态分配和管理,所以,如果程序在分配后忘了回收,就会导致跟堆内存类似的泄漏问题
  • 栈,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。栈内存由系统自动分配和管理。一旦程序运行超出了这个局部变量的作用域,栈内存就会被系统自动回收,所以不会产生内存泄漏的问题。举个例子,你在程序中定义了一个局部变量,比如一个整数数组 int data[64] ,就定义了一个可以存储 64 个整数的内存段。由于这是一个局部变量,它会从内存空间的栈中分配内存。

在这五个内存段中,堆和文件映射段的内存是动态分配的。由于是动态分配的,如果没有正常回收,则就有可能导致内存泄露。内存泄漏的危害非常大,这些忘记释放的内存,不仅应用程序自己不能访问,系统也不能把它们再次分配给其他应用。内存泄漏不断累积,甚至会耗尽系统内存。

内存分配与回收

brk与mmap

malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即 brk() 和 mmap()**。

对小块内存(小于 128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用;而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。

brk() 方式的缓存,可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。

而 mmap() 方式分配的内存,会在释放时直接归还系统,所以每次 mmap 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是 malloc 只对大块内存使用 mmap 的原因。

同时当这两种调用发生后,其实并没有真正分配内存。这些内存,都只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内核来分配内存

整体来说,Linux 使用伙伴系统来管理内存分配。前面我们提到过,这些内存在 MMU 中以页为单位进行管理,伙伴系统也一样,以页为单位来管理内存,并且会通过相邻页的合并,减少内存碎片化(比如 brk 方式造成的内存碎片)。

如果遇到比页更小的对象,比如不到 1K 的时候,该怎么分配内存呢?在用户空间,malloc 通过 brk() 分配的内存,在释放时并不立即归还系统,而是缓存起来重复利用在内核空间,Linux 则通过 slab 分配器来管理小内存。你可以把 slab 看成构建在伙伴系统上的一个缓存,主要作用就是分配并释放内核中的小对象。

对内存来说,如果只分配而不释放,就会造成内存泄漏,甚至会耗尽系统内存。所以,在应用程序用完内存后,还需要调用 free() 或 unmap() ,来释放这些不用的内存

系统自动回收内存机制

系统也不会任由某个进程用完所有内存。在发现内存紧张时,系统就会通过一系列机制来回收内存,比如下面这三种方式:

  1. 回收缓存

比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面;

  1. 回收不常访问的内存,把不常用的内存通过swap直接写到磁盘中

回收不常访问的内存时,会用到交换分区(以下简称 Swap)。Swap 其实就是把一块磁盘空间当成内存来用。它可以把进程暂时不用的数据存储到磁盘中(这个过程称为换出),当进程访问这些内存时,再从磁盘读取这些数据到内存中(这个过程称为换入)。

所以,你可以发现,Swap 把系统的可用内存变大了。不过要注意,通常只在内存不足时,才会发生 Swap 交换。并且由于磁盘读写的速度远比内存慢,Swap 会导致严重的内存性能问题。

  1. 通过OOM杀死进程

内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程。同时,可以通过dmesg |grep -E 'kill|oom|out of memory'来查看是否有出现过OOM。

OOM是内核的一种保护机制。它监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分:

一个进程消耗的内存越大,oom_score 就越大;一个进程运行占用的 CPU 越多,oom_score 就越小。这样,进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死,从而可以更好保护系统。

oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止 OOM。通过echo -17 > /proc/$(pidof sshd)/oom_adj,你就可以把 sshd 进程的 oom_adj 调小为 -17,这样, sshd 进程就不会被 OOM 杀死。

文件页与匿名页

有文件背景的页面(file-backed page)称为文件页,如缓存cache和缓冲区buffer、以及通过内存映射获取的文件映射页;没有文件背景的页面,即匿名页(anonymous page),如堆,栈,数据段等,不是以文件形式存在,因此无法和磁盘文件交换,但可以通过硬盘上划分额外的swap交换分区或使用交换文件进行交换。Swap分区可以将不活跃的页交换到硬盘中,缓解内存紧张。

脏页是linux内核中的概念,因为硬盘的读写速度远赶不上内存的速度,系统就把读写比较频繁的数据事先放到内存中,以提高读写速度,这就叫高速缓存,linux是以页作为高速缓存的单位,当进程修改了高速缓存里的数据时,该页就被内核标记为脏页,内核将会在合适的时间把脏页的数据写到磁盘中去,以保持高速缓存中的数据和磁盘中的数据是一致的。

脏页,一般可以通过两种方式写入磁盘。一是在应用程序中,通过系统调用 fsync ,把脏页同步到磁盘中;二交给系统,由内核线程 pdflush 负责这些脏页的刷新。

Swap与匿名页

swap原理

Swap 说白了就是把一块磁盘空间或者一个本地文件(以下讲解以磁盘为例),当成内存来使用。它包括换出和换入两个过程。

所谓换出,就是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存。而换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来。

有新的大块内存分配请求,但是剩余内存不足,这个时候系统就需要回收一部分内存(比如前面提到的缓存),进而尽可能地满足新内存请求。这个过程通常被称为直接内存回收。

除了直接内存回收,还有一个专门的内核线程用来定期回收内存,也就是kswapd0。

匿名页如果要回收,是通过SWAP来回收的,如果不开swap,系统就一直不会回收匿名页,除非出现了OOM或者程序自己停止了;对文件页的回收,当然就是直接回收缓存,或者把脏页写回磁盘后再回收。

kswapd0

为了衡量内存的使用情况,kswapd0 定义了三个内存阈值(watermark,也称为水位),分别是

页最小阈值(pages_min)、页低阈值(pages_low)和页高阈值(pages_high)。剩余内存,则使用 pages_free 表示。

  • 剩余内存小于页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存。
  • 剩余内存落在页最小阈值和页低阈值中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止。
  • 剩余内存落在页低阈值和页高阈值中间,说明内存有一定压力,但还可以满足新内存请求。
  • 剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力。

一旦剩余内存小于页低阈值,就会触发内存的回收。这个页低阈值,其实可以通过内核选项 /proc/sys/vm/min_free_kbytes 来间接设置。min_free_kbytes 设置了页最小阈值,而其他两个阈值,都是根据页最小阈值计算生成的,计算方法如下 :

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

如现在在使用的一台机器上面的值:

1
2
3
4
5
6
7
8
9
10
root@fdm:~# cat /proc/sys/vm/min_free_kbytes 
67584
root@fdm:~# echo 67584 |awk '{print $1/1024}' //页最小阀值
66
root@fdm:~#
root@fdm:~# echo 67584 |awk '{print $1*5/4/1024}' //页最低阀值
82.5
root@fdm:~#
root@fdm:~# echo 67584 |awk '{print $1*3/2/1024}' //页最高阀值
99

NUMA 与 Swap

在CPU NUMA 架构下,多个处理器被划分到不同 Node 上,且每个 Node 都拥有自己的本地内存空间。而同一个 Node 内部的内存空间,实际上又可以进一步分为不同的内存域(Zone),比如直接内存访问区(DMA)、普通内存区(NORMAL)、伪内存区(MOVABLE)等。

既然 NUMA 架构下的每个 Node 都有自己的本地内存空间,那么,在分析内存的使用时,我们也应该针对每个 Node 单独分析。可以通过 numactl 命令,来查看处理器在 Node 的分布情况,以及每个 Node 的内存使用情况。如下:

1
2
3
4
5
6
7
8
[root@linjx ~]# numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0
node 0 size: 369 MB
node 0 free: 15 MB
node distances:
node 0
0: 10

前面提到的三个内存阈值(页最小阈值、页低阈值和页高阈值),都可以通过内存域在 proc 文件系统中的接口 /proc/zoneinfo 来查看。可以通过查看cat /proc/zoneinfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@linjx ~]# cat /proc/sys/vm/min_free_kbytes
2387

[root@linjx ~]# cat /proc/zoneinfo |sed -n '/pages free/,/nr_zone_active_file/p' |sed '/protection/d;/spanned/d;/present/d;/managed/d' |sed '/pages free 0/,$d'
pages free 1558
min 26
low 32
high 38
spanned 4095
present 3998
managed 3977
protection: (0, 332, 332, 332, 332)
nr_free_pages 1558
nr_zone_inactive_anon 115
nr_zone_active_anon 91
nr_zone_inactive_file 243
nr_zone_active_file 1307
  • pages 处的 min、low、high,就是上面提到的三个内存阈值,而 free 是剩余内存页数,它跟后面的 nr_free_pages 相同。
  • nr_zone_active_anon 和 nr_zone_inactive_anon,分别是活跃和非活跃的匿名页数。
  • nr_zone_active_file 和 nr_zone_inactive_file,分别是活跃和非活跃的文件页数。
  • 活跃内存指最近使用过的内存;非活跃内存是指不常访问的内存,有可能会被系统回收。
  • 文件页是指有文件背景的页面(file-backed page),如缓存和缓冲区、以及通过内存映射获取的文件映射页;没有文件背景的页面,即匿名页(anonymous page),如堆,栈,数据段

从这个输出结果可以发现,剩余内存远大于页高阈值,所以此时的 kswapd0 不会回收内存。

当然,某个 Node 内存不足时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。具体选哪种模式,你可以通过 /proc/sys/vm/zone_reclaim_mode来调整。它支持以下几个选项:

  • 默认的 0 ,也就是刚刚提到的模式,表示既可以从其他 Node 寻找空闲内存,也可以从本地回收内存。
  • 1、2、4 都表示只回收本地内存,2 表示可以回写脏数据回收内存,4 表示可以用 Swap 方式回收内存。

swappiness

linux 提供了一个 /proc/sys/vm/swappiness 选项,用来调整使用 Swap 的积极程度。

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

虽然 swappiness 的范围是 0-100,不过要注意,这并不是内存的百分比,而是调整 Swap 积极程度的权重,即使你把它设置成 0,当剩余内存 + 文件页小于页高阈值时,还是会发生 Swap。参考:https://www.kernel.org/doc/Documentation/sysctl/vm.txt

如果实在需要用到 Swap,可以尝试降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。

内存性能指标

系统内存指标

  • 已用内存和剩余内存很容易理解,就是已经使用和还未使用的内存。
  • 共享内存是通过 tmpfs 实现的,所以它的大小也就是 tmpfs 使用的内存大小。tmpfs 其实也是一种特殊的缓存。
  • 可用内存是新进程可以使用的最大内存,它包括剩余内存和可回收缓存。
  • 缓存包括两部分,一部分是磁盘读取文件的页缓存,用来缓存从磁盘读取的数据,可以加快以后再次访问的速度。另一部分,则是 Slab 分配器中的可回收内存。
  • 缓冲区是对原始磁盘块的临时存储,用来缓存将要写入磁盘的数据。这样,内核就可以把分散的写集中起来,统一优化磁盘写入。

image

进程内存指标

  • 虚拟内存,包括了进程代码段、数据段、共享内存、已经申请的堆内存和已经换出的内存等。这里要注意,已经申请的内存,即使还没有分配物理内存,也算作虚拟内存。
  • 常驻内存是进程实际使用的物理内存,不过,它不包括 Swap 和共享内存。
  • 共享内存,既包括与其他进程共同使用的真实的共享内存,还包括了加载的动态链接库以及程序的代码段等。
  • Swap 内存,是指通过 Swap 换出到磁盘的内存。
  • 缺页异常:系统调用内存分配请求后,并不会立刻为其分配物理内存,而是在请求首次访问时,通过缺页异常来分配。可以直接从物理内存中分配时,被称为次缺页异常;需要磁盘 I/O 介入(比如 Swap)时,被称为主缺页异常。主缺页异常升高,就意味着需要磁盘 I/O,那么内存访问也会慢很多。

SWAP内存

比如 Swap 的已用空间、剩余空间、换入速度和换出速度等。

  • 已用空间和剩余空间很好理解,就是字面上的意思,已经使用和没有使用的内存空间。
  • 换入和换出速度,则表示每秒钟换入和换出内存的大小。

如何迅速分析内存的性能瓶颈

为了迅速定位内存问题,我通常会先运行几个覆盖面比较大的性能工具,比如 free、top、vmstat、pidstat 等。

具体的分析思路主要有这几步。

  • 先用 free 和 top,查看系统整体的内存使用情况。
  • 再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断出内存问题的类型。
  • 最后进行详细分析,比如内存分配分析、缓存 / 缓冲区分析、具体进程的内存使用分析等。

第一个例子,当你通过 free,发现大部分内存都被缓存占用后,可以使用 vmstat 或者 sar 观察一下缓存的变化趋势,确认缓存的使用是否还在继续增大。

如果继续增大,则说明导致缓存升高的进程还在运行,那你就能用缓存 / 缓冲区分析工具(比如 cachetop、slabtop 等),分析这些缓存到底被哪里占用。

第二个例子,当你 free 一下,发现系统可用内存不足时,首先要确认内存是否被缓存 / 缓冲区占用。排除缓存 / 缓冲区后,你可以继续用 pidstat 或者 top,定位占用内存最多的进程。

找出进程后,再通过进程内存空间工具(比如 pmap),分析进程地址空间中内存的使用情况就可以了。

第三个例子,当你通过 vmstat 或者 sar 发现内存在不断增长后,可以分析中是否存在内存泄漏的问题。

比如你可以使用内存分配分析工具 memleak ,检查是否存在内存泄漏。如果存在内存泄漏问题,memleak 会为你输出内存泄漏的进程以及调用堆栈。

参考链接

大页内存(HugePages)在通用程序优化中的应用

CentOS 7 关闭透明大页

程序员的自我修养(七):内存缺页错误

0%