内存性能篇:物理内存与虚拟机地址空间;内存的分配和回收机制;buffer和cashe;缓存命中率;内存泄漏;swap回收原理;内存性能分析与优化
倪朋飞 《Linux 性能优化实战》
15 | 基础篇:Linux内存是怎么工作的?

物理内存与虚拟地址空间(内核空间、用户空间);内存映射、缺页异常、页表;用户空间内存的功能划分 ================================================================================================================ 物理内存也称为主存,大多数计算机用的主存都是动态随机访问内存(DRAM)。只有内核才可以直接访问物理内存。 Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。 这一点是之前看书所忽视的,对于每一个进程,都认为自己拥有了整台设备,比如32位系统,每个进程认为自己的内存都是4GB,并且划分为内核空间1G,用户空间3G等等 虚拟地址空间的内部分为内核空间和用户空间 32 位系统的内核空间占用 1G,位于最高处,剩下的 3G 是用户空间。 64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。 每个进程都有一个这么大的地址空间,那么所有进程的虚拟内存加起来,自然要比实际的物理内存大得多。 所以,并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。 -------------------------------------------------------------------------------------------------------- 内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系 页表实际上存储在 CPU 的内存管理单元 MMU 中,正常情况下,处理器就可以直接通过硬件,找出要访问的内存。 当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。 TLB(Translation Lookaside Buffer,转译后备缓冲器)会影响 CPU 的内存访问性能 TLB 其实就是 MMU 中页表的高速缓存。 由于进程的虚拟地址空间是独立的,而 TLB 的访问速度又比 MMU 快得多, 所以,通过减少进程的上下文切换,减少 TLB 的刷新次数,就可以提高 TLB 缓存的使用率,进而提高 CPU 的内存访问性能。 MMU规定了一个内存映射的最小单位,也就是页,通常是 4 KB 大小。每一次内存映射,都需要关联 4 KB 或者 4KB 整数倍的内存空间。 页的大小只有 4 KB ,导致的另一个问题就是,整个页表会变得非常大。比方说,仅 32 位系统就需要 100 多万个页表项(4GB/4KB),才可以实现整个地址空间的映射。 为了解决页表项过多的问题,Linux 提供了两种机制,也就是多级页表和大页(HugePage)。 -------------------------------------------------------------------------------------------------------- 虚拟内存空间分布 用户空间内存,从低到高分别是五种不同的内存段。 1.只读段,包括代码和常量等。 2.数据段,包括全局变量等。 3.堆,包括动态分配的内存,从低地址开始向上增长。 4.文件映射段,包括动态库、共享内存等,从高地址开始向下增长。 5.栈,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。

内存分配:malloc()、brk()、mmap();内存回收机制(回收缓存、回收不常访问的内存、OOM);内存资源紧张时的2种可能 ================================================================================================= malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即 brk() 和 mmap()。 对小块内存(小于 128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。 对大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。 优缺点比较: brk() 方式的缓存,可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。 mmap() 方式分配的内存,会在释放时直接归还系统,所以每次 mmap 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是 malloc 只对大块内存使用 mmap 的原因。 ###当这两种调用发生后,其实并没有真正分配内存。这些内存,都只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内核来分配内存。 ------------------------------------------------------------------------------------------------- 内存回收机制: 1.回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面; 2.回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中; 回收不常访问的内存时,会用到交换分区(以下简称 Swap) 把进程暂时不用的数据存储到磁盘中(这个过程称为换出),当进程访问这些内存时,再从磁盘读取这些数据到内存中(这个过程称为换入) 3.杀死进程,内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程。 OOM是内核的一种保护机制。它监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分: 一个进程消耗的内存越大,oom_score 就越大; 一个进程运行占用的 CPU 越多,oom_score 就越小。 进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死 管理员可以通过 /proc 文件系统,手动设置进程的 oom_adj ,从而调整进程的 oom_score。 oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止 OOM。 echo -16 > /proc/$(pidof sshd)/oom_adj ---------------------------------------- 缓存回收和 Swap 回收,实际上都是基于 LRU 算法,也就是优先回收不常访问的内存。LRU 回收算法,实际上维护着 active 和 inactive 两个双向链表,其中: active 记录活跃的内存页; inactive 记录非活跃的内存页。 越接近链表尾部,就表示内存页越不常访问。这样,在回收内存时,系统就可以根据活跃程度,优先回收不活跃的内存。 活跃和非活跃的内存页,按照类型的不同,又分别分为文件页和匿名页,对应着缓存回收和 Swap 回收。 $ cat /proc/meminfo | grep -i active | sort # sort表示按照字母顺序排序 # grep表示只保留包含active的指标(忽略大小写) Active(anon): 167976 kB Active(file): 971488 kB Active: 1139464 kB Inactive(anon): 720 kB Inactive(file): 2109536 kB Inactive: 2110256 kB --------------------------------------- 第三种方式,OOM 机制按照 oom_score 给进程排序。oom_score 越大,进程就越容易被系统杀死。 当系统发现内存不足以分配新的内存请求时,就会尝试直接内存回收。这种情况下,如果回收完文件页和匿名页后,内存够用了,当然皆大欢喜,把回收回来的内存分配给进程就可以了。但如果内存还是不足,OOM 就要登场了。 OOM 发生时,你可以在 dmesg 中看到 Out of memory 的信息,从而知道是哪些进程被 OOM 杀死了。比如,你可以执行下面的命令,查询 OOM 日志: $ dmesg | grep -i "Out of memory" Out of memory: Kill process 9329 (java) score 321 or sacrifice child 当然了,如果你不希望应用程序被 OOM 杀死,可以调整进程的 oom_score_adj,减小 OOM 分值,进而降低被杀死的概率。或者,你还可以开启内存的 overcommit,允许进程申请超过物理内存的虚拟内存(这儿实际上假设的是,进程不会用光申请到的虚拟内存)。 ---------------------------------------------------------------- 内存资源紧张时的2种可能: 1.内存资源紧张导致的 OOM(Out Of Memory),相对容易理解,指的是系统杀死占用大量内存的进程,释放这些内存,再分配给其他更需要的进程。 2.内存回收,也就是系统释放掉可以回收的内存,比如我前面讲过的缓存和缓冲区,就属于可回收内存。它们在内存管理中,通常被叫做文件页(File-backed Page)。 a.大部分文件页,都可以直接回收,以后有需要时,再从磁盘重新读取就可以了。 b.脏页(被应用程序修改过,并且暂时还没写入磁盘的数据)回收:先写入磁盘,然后才能进行内存释放。 这些脏页,一般可以通过两种方式写入磁盘。 a).在应用程序中,通过系统调用 fsync ,把脏页同步到磁盘中; b).交给系统,由内核线程 pdflush 负责这些脏页的刷新。 c.通过内存映射获取的文件映射页,也是一种常见的文件页。它也可以被释放掉,下次再访问的时候,从文件重新读取。 d.匿名页(Anonymous Page):应用程序动态分配的堆内存,是不会被直接释放的!!! 这些内存在分配后很少被访问,也是一种资源浪费,这时可以用到swap Linux 的 Swap 机制。Swap 把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。

查看内存使用情况:free;top ====================================================================================================================== # 注意不同版本的free输出可能会有所不同 $ free total used free shared buff/cache available Mem: 8169348 263524 6875352 668 1030472 7611064 Swap: 0 0 0 第一列,total 是总内存大小; total = used + free + buff/cache 第二列,used 是已使用内存的大小,包含了共享内存; 第三列,free 是未使用内存的大小; 第四列,shared 是共享内存的大小; 第五列,buff/cache 是缓存和缓冲区的大小; 最后一列,available 是新进程可用内存的大小。 available = free + 部分buff/cache -------------------------------------------------------------------------- # 按下M切换到内存排序 $ top ... KiB Mem : 8169348 total, 6871440 free, 267096 used, 1030812 buff/cache KiB Swap: 0 total, 0 free, 0 used. 7607492 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 430 root 19 -1 122360 35588 23748 S 0.0 0.4 0:32.17 systemd-journal 1075 root 20 0 771860 22744 11368 S 0.0 0.3 0:38.89 snapd 1048 root 20 0 170904 17292 9488 S 0.0 0.2 0:00.24 networkd-dispat 1 root 20 0 78020 9156 6644 S 0.0 0.1 0:22.92 systemd 12376 azure 20 0 76632 7456 6420 S 0.0 0.1 0:00.01 systemd 12374 root 20 0 107984 7312 6304 S 0.0 0.1 0:00.00 sshd VIRT 是进程虚拟内存的大小,只要是进程申请过的内存,即便还没有真正分配物理内存,也会计算在内。VIRT:virtual memory usage RES 是常驻内存的大小,也就是进程实际使用的物理内存大小,但不包括 Swap 和共享内存。 RES:resident memory usage 常驻内存 SHR 是共享内存的大小,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等。 SHR:shared memory %MEM 是进程使用物理内存占系统总内存的百分比。 在查看 top 输出时,你还要注意两点。 第一,虚拟内存通常并不会全部分配物理内存。从上面的输出,你可以发现每个进程的虚拟内存都比常驻内存大得多。 第二,共享内存 SHR 并不一定是共享的,比方说,程序的代码段、非共享的动态链接库,也都算在 SHR 里。 当然,SHR 也包括了进程间真正共享的内存。所以在计算多个进程的内存使用时,不要把所有进程的 SHR 直接相加得出结果。
16 | 基础篇:怎么理解内存中的Buffer和Cache?

Buffer 和 Cache;普通文件与块设备文件 =============================================================================================================== 缓存是 Buffer 和 Cache 的总和 Buffer 是缓冲区,Cache 是缓存,两者都是数据在内存中的临时存储。 man free 查看buffer和cache的区别 Buffers 是内核缓冲区用到的内存,对应的是 /proc/meminfo 中的 Buffers 值。 Cache 是内核页缓存和 Slab 用到的内存,对应的是 /proc/meminfo 中的 Cached 与 SReclaimable 之和。 man proc Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。 Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。这样,下次访问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘。 SReclaimable 是 Slab 的一部分。Slab 包括两部分,其中的可回收部分,用 SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录。 Buffer 既可以用作“将要写入磁盘数据的缓存”,也可以用作“从磁盘读取数据的缓存”。 Cache 既可以用作“从文件读取数据的页缓存”,也可以用作“写文件的页缓存”。 Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中。 从写的角度来说,不仅可以优化磁盘和文件的写入,对应用程序也有好处,应用程序可以在数据真正落盘前,就返回去做其他工作。 从读的角度来说,既可以加速读取那些需要频繁访问的数据,也降低了频繁 I/O 对磁盘的压力。 ------------------------------------------------------------------------------------------------------------------- 普通文件与块设备文件 磁盘是一个存储设备(确切地说是块设备),可以被划分为不同的磁盘分区。而在磁盘或者磁盘分区上,还可以再创建文件系统,并挂载到系统的某个目录中。这样,系统就可以通过这个挂载目录,来读写文件。 磁盘是存储数据的块设备,也是文件系统的载体。所以,文件系统确实还是要通过磁盘,来保证数据的持久化存储。 我们通常说的“文件”,其实是指普通文件。 磁盘或者分区,则是指块设备文件。 你可以执行 “ls -l < 路径 >” 查看它们的区别。如果不懂 ls 输出的含义,别忘了 man 一下就可以。执行 man ls 命令,以及 info ‘(coreutils) ls invocation’ 命令 在读写普通文件时,I/O 请求会首先经过文件系统,然后由文件系统负责,来与磁盘进行交互。 在读写块设备文件时,会跳过文件系统,直接与磁盘交互,也就是所谓的“裸 I/O”。 这两种读写方式使用的缓存不同。 文件系统管理的缓存,其实就是 Cache 的一部分。 裸磁盘的缓存,用的正是 Buffer。

文件写案例;磁盘写案例;文件读案例;磁盘读案例 ================================================================================================== 这4个案例是证明“buffer和磁盘读写有关”、“cache和文件读写有关” ----------------------------------------------------------- 场景 1:文件写案例 #输出到文件,即写入内容到文件 $ echo 3 > /proc/sys/vm/drop_caches # 清理文件页、目录项、Inodes等各种缓存 $ vmstat 1 # 每隔1秒输出1组数据,用于对比 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 7743608 1112 92168 0 0 0 0 52 152 0 1 100 0 0 0 0 0 7743608 1112 92168 0 0 0 0 36 92 0 0 100 0 0 buff 和 cache 就是我们前面看到的 Buffers 和 Cache,单位是 KB。 bi 和 bo 则分别表示块设备读取和写入的大小,单位为块 / 秒。因为 Linux 中块的大小是 1KB,所以这个单位也就等价于 KB/s。 $ dd if=/dev/urandom of=/tmp/file bs=1M count=500 #执行 dd 命令,通过读取随机设备,生成一个 500MB 大小的文件 $ vmstat 1 # 每隔1秒输出1组数据 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 7499460 1344 230484 0 0 0 0 29 145 0 0 100 0 0 1 0 0 7338088 1752 390512 0 0 488 0 39 558 0 47 53 0 0 1 0 0 7158872 1752 568800 0 0 0 4 30 376 1 50 49 0 0 1 0 0 6980308 1752 747860 0 0 0 0 24 360 0 50 50 0 0 0 0 0 6977448 1752 752072 0 0 0 0 29 138 0 0 100 0 0 0 0 0 6977440 1760 752080 0 0 0 152 42 212 0 1 99 1 0 ... 0 1 0 6977216 1768 752104 0 0 4 122880 33 234 0 1 51 49 0 0 1 0 6977440 1768 752108 0 0 0 10240 38 196 0 0 50 50 0 在 dd 命令运行时, Cache 在不停地增长,而 Buffer 基本保持不变。 在 Cache 刚开始增长时,块设备 I/O 很少,bi 只出现了一次 488 KB/s,bo 则只有一次 4KB。而过一段时间后,才会出现大量的块设备写,比如 bo 变成了 122880。 当 dd 命令结束后,Cache 不再增长,但块设备写还会持续一段时间,并且,多次 I/O 写的结果加起来,才是 dd 要写的 500M 的数据。 --------------------------------------------------------------- 场景 2:磁盘写案例 #输出到磁盘,即直接写入内容到磁盘 $ echo 3 > /proc/sys/vm/drop_caches # 首先清理缓存 ***注意不要往系统分区所在磁盘直接写入内容,这将会对你的磁盘分区造成损坏。 $ dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048 # 然后运行dd命令向磁盘分区/dev/sdb1写入2G数据 $ vmstat 1 # 每隔1秒输出1组数据 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 1 0 0 7584780 153592 97436 0 0 684 0 31 423 1 48 50 2 0 1 0 0 7418580 315384 101668 0 0 0 0 32 144 0 50 50 0 0 1 0 0 7253664 475844 106208 0 0 0 0 20 137 0 50 50 0 0 1 0 0 7093352 631800 110520 0 0 0 0 23 223 0 50 50 0 0 1 1 0 6930056 790520 114980 0 0 0 12804 23 168 0 50 42 9 0 1 0 0 6757204 949240 119396 0 0 0 183804 24 191 0 53 26 21 0 1 1 0 6591516 1107960 123840 0 0 0 77316 22 232 0 52 16 33 0 虽然同是写数据,写磁盘跟写文件的现象还是不同的。 写磁盘时(也就是 bo 大于 0 时),Buffer 和 Cache 都在增长,但显然 Buffer 的增长快得多。 这说明,写磁盘用到了大量的 Buffer --------------------------------------------------------------- 场景 3:文件读案例 $ echo 3 > /proc/sys/vm/drop_caches # 首先清理缓存 $ dd if=/tmp/file of=/dev/null # 运行dd命令读取文件数据 $ vmstat 1 # 每隔1秒输出1组数据 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 1 0 7724164 2380 110844 0 0 16576 0 62 360 2 2 76 21 0 0 1 0 7691544 2380 143472 0 0 32640 0 46 439 1 3 50 46 0 0 1 0 7658736 2380 176204 0 0 32640 0 54 407 1 4 50 46 0 0 1 0 7626052 2380 208908 0 0 32640 40 44 422 2 2 50 46 0 你会发现读取文件时(也就是 bi 大于 0 时),Buffer 保持不变,而 Cache 则在不停增长。 --------------------------------------------------------------- 场景 4:磁盘读案例 $ echo 3 > /proc/sys/vm/drop_caches # 首先清理缓存 $ dd if=/dev/sda1 of=/dev/null bs=1M count=1024 # 运行dd命令读取文件 $ vmstat 1 # 每隔1秒输出1组数据 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 7225880 2716 608184 0 0 0 0 48 159 0 0 100 0 0 0 1 0 7199420 28644 608228 0 0 25928 0 60 252 0 1 65 35 0 0 1 0 7167092 60900 608312 0 0 32256 0 54 269 0 1 50 49 0 0 1 0 7134416 93572 608376 0 0 32672 0 53 253 0 0 51 49 0 0 1 0 7101484 126320 608480 0 0 32748 0 80 414 0 1 50 49 0 读磁盘时(也就是 bi 大于 0 时),Buffer 和 Cache 都在增长,但显然 Buffer 的增长快很多。这说明读磁盘时,数据缓存到了 Buffer 中。
17 | 案例篇:如何利用系统缓存优化程序的运行效率?

缓存命中率;cachestat、cachetop;pcstat ====================================================================================================== 缓存命中率 是指直接通过缓存获取数据的请求次数,占所有数据请求次数的百分比。 命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。 实际上,缓存是现在所有高并发系统必需的核心模块,主要作用就是把经常访问的数据(也就是热点数据),提前读入到内存中。 Buffers 和 Cache 可以极大提升系统的 I/O 性能。 通常,我们用缓存命中率,来衡量缓存的使用效率。命中率越高,表示缓存被利用得越充分,应用程序的性能也就越好。 你可以用 cachestat 和 cachetop 这两个工具,观察系统和进程的缓存命中情况。其中, cachestat 提供了整个系统缓存的读写命中情况。 cachetop 提供了每个进程的缓存命中情况。 不过要注意,Buffers 和 Cache 都是操作系统来管理的,应用程序并不能直接控制这些缓存的内容和生命周期。所以,在应用程序开发中,一般要用专门的缓存组件,来进一步提升性能。 systemtap、vmtouch 也可以查看文件缓存 直接IO是跳过Buffer,裸IO是跳过文件系统(还是有buffer的) 直接IO例如:系统函数openat()设置了O_DIRECT 选项 裸IO例如直接对磁盘进行读写??? --------------------------------------------------------------- cachestat 提供了整个操作系统缓存的读写命中情况。 cachetop 提供了每个进程的缓存命中情况。 在 Ubuntu 系统中,你可以运行下面的命令来安装: sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD echo "deb https://repo.iovisor.org/apt/xenial xenial main" | sudo tee /etc/apt/sources.list.d/iovisor.list sudo apt-get update sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r) 注意:bcc-tools 需要内核版本为 4.1 或者更新的版本,如果你用的是 CentOS,那就需要手动升级内核版本后再安装。 bcc 软件包默认不会把这些工具配置到系统的 PATH 路径中 $ export PATH=$PATH:/usr/share/bcc/tools $ cachestat 1 3 TOTAL MISSES HITS DIRTIES BUFFERS_MB CACHED_MB 2 0 2 1 17 279 2 0 2 1 17 279 2 0 2 1 17 279 TOTAL ,表示总的 I/O 次数; MISSES ,表示缓存未命中的次数; HITS ,表示缓存命中的次数; DIRTIES, 表示新增到缓存中的脏页数; BUFFERS_MB 表示 Buffers 的大小,以 MB 为单位; CACHED_MB 表示 Cache 的大小,以 MB 为单位。 $ cachetop 11:58:50 Buffers MB: 258 / Cached MB: 347 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 13029 root python 1 0 0 100.0% 0.0% HITS、MISSES 和 DIRTIES ,跟 cachestat 里的含义一样,分别代表间隔时间内的缓存命中次数、未命中次数以及新增到缓存中的脏页数。 READ_HIT 和 WRITE_HIT ,分别表示读和写的缓存命中率。 在 CentOS 7 中安装 bcc-tools 的步骤(实测还是报错,猜测还是和内核版本有关。。。) 第一步,升级内核。你可以运行下面的命令来操作: # 升级系统 yum update -y # 安装ELRepo rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org rpm -Uvh https://www.elrepo.org/elrepo-release-7.0-3.el7.elrepo.noarch.rpm # 安装新内核 yum remove -y kernel-headers kernel-tools kernel-tools-libs yum --enablerepo="elrepo-kernel" install -y kernel-ml kernel-ml-devel kernel-ml-headers kernel-ml-tools kernel-ml-tools-libs kernel-ml-tools-libs-devel # 更新Grub后重启 grub2-mkconfig -o /boot/grub2/grub.cfg grub2-set-default 0 reboot # 重启后确认内核版本已升级为4.20.0-1.el7.elrepo.x86_64 uname -r 第二步,安装 bcc-tools: # 安装bcc-tools yum install -y bcc-tools # 配置PATH路径 export PATH=$PATH:/usr/share/bcc/tools # 验证安装成功 cachestat -------------------------------------------------------------------------------------- pcstat 查看文件在内存中的缓存大小以及缓存比例 pcstat 是一个基于 Go 语言开发的工具,所以安装它之前,你首先应该安装 Go 语言 安装完 Go 语言,再运行下面的命令安装 pcstat: $ export GOPATH=~/go $ export PATH=~/go/bin:$PATH $ go get golang.org/x/sys/unix $ go get github.com/tobert/pcstat/pcstat $ pcstat /bin/ls +---------+----------------+------------+-----------+---------+ | Name | Size (bytes) | Pages | Cached | Percent | |---------+----------------+------------+-----------+---------| | /bin/ls | 133792 | 33 | 0 | 000.000 | +---------+----------------+------------+-----------+---------+ Cached 就是 /bin/ls 在缓存中的大小,而 Percent 则是缓存的百分比。你看到它们都是 0,这说明 /bin/ls 并不在缓存中。 $ ls #执行一次ls命令后,再次查看,Percent为100% $ pcstat /bin/ls +---------+----------------+------------+-----------+---------+ | Name | Size (bytes) | Pages | Cached | Percent | |---------+----------------+------------+-----------+---------| | /bin/ls | 133792 | 33 | 33 | 100.000 | +---------+----------------+------------+-----------+---------+

缓存命中率案例1;缓存命中率案例2 ====================================================================================================== 案例一:读文件,缓存命中率高低对于效率的影响,说明了从磁盘读取文件的效率是多么低下(33MB对比4.5GB) $ dd if=/dev/sda1 of=file bs=1M count=512 # 生成一个512MB的临时文件 $ echo 3 > /proc/sys/vm/drop_caches # 清理缓存 $ pcstat file #文件在内存中暂未被缓存 +-------+----------------+------------+-----------+---------+ | Name | Size (bytes) | Pages | Cached | Percent | |-------+----------------+------------+-----------+---------| | file | 536870912 | 131072 | 0 | 000.000 | +-------+----------------+------------+-----------+---------+ $ cachetop 5 # 每隔5秒刷新一次数据 #第一次读取文件 $ dd if=file of=/dev/null bs=1M #运行 dd 命令测试文件的读取速度 512+0 records in 512+0 records out 536870912 bytes (537 MB, 512 MiB) copied, 16.0509 s, 33.4 MB/s 这个文件的读性能是 33.4 MB/s。 由于在 dd 命令运行前我们已经清理了缓存,所以 dd 命令读取数据时,肯定要通过文件系统从磁盘中读取。 PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% \.\.\. 3264 root dd 37077 37330 0 49.8% 50.2% 从 cachetop 的结果可以发现,并不是所有的读都落到了磁盘上,事实上读请求的缓存命中率只有 50% 。 #####缓存命中率不为0的原因为预读机制#### #####预读即在读取的起始地址连续读取多个页面(现在不需要的页面也读取了,这样以后用时就不用再读取,当一个页面用到时,大多数情况下,它周围的页面也会被用到) #第二次读取文件 $ dd if=file of=/dev/null bs=1M 512+0 records in 512+0 records out 536870912 bytes (537 MB, 512 MiB) copied, 0.118415 s, 4.5 GB/s 磁盘的读性能居然变成了 4.5 GB/s 10:45:22 Buffers MB: 4 / Cached MB: 719 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% \.\.\. 32642 root dd 131637 0 0 100.0% 0.0% 读的缓存命中率是 100.0%,也就是说这次的 dd 命令全部命中了缓存 $ pcstat file +-------+----------------+------------+-----------+---------+ | Name | Size (bytes) | Pages | Cached | Percent | |-------+----------------+------------+-----------+---------| | file | 536870912 | 131072 | 131072 | 100.000 | +-------+----------------+------------+-----------+---------+ 测试文件 file 已经被全部缓存了起来,这跟刚才观察到的缓存命中率 100% 是一致的。 ------------------------------------------------------------------------------------------------------ 案例二 cachetop 工具并不能统计到所有的IO,比如直接IO就不会被统计到。 熟悉读取次数与读取文件大小的关系与换算 本案例的基本功能比较简单,也就是每秒从磁盘分区 /dev/sda1 中读取 32MB 的数据,并打印出读取数据花费的时间。 目的在于显示直接IO的影响,以及体现文件读取次数与文件大小的关系与换算,也说明了缓存命中率的重要性 $ docker run --privileged --name=app -itd feisky/app:io-direct $ docker logs app Reading data from disk /dev/sdb1 with buffer size 33554432 Time used: 0.929935 s to read 33554432 bytes #来确认案例已经正常启动。 Time used: 0.949625 s to read 33554432 bytes 每读取 32 MB 的数据,就需要花 0.9 秒,有点慢 $ cachetop 5 # 每隔5秒刷新一次数据 16:39:18 Buffers MB: 73 / Cached MB: 281 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 21881 root app 1024 0 0 100.0% 0.0% 1024 次缓存全部命中,读的命中率是 100%,看起来全部的读请求都经过了系统缓存。 1024*4K/1024 = 4MB 每次读取的数据大小为1页,即4KB 1024次命中即读取1024次,读取的数据大小为4096KB,即4MB 5秒(cachetop采集间隔为5s)读取的缓存数据大小为4MB,那么每秒读取的缓存为0.8MB;这和实际每秒读取32MB,差距有点大 差距大的原因:cachetop没有对直接IO进行统计,所以以上输出的数据其实失真了。。。 因为容器内的应用,openat函数使用了O_DIRECT 选项,对磁盘进行了直接读取。当然是比较慢了。 # strace -p $(pgrep app) #使用strace 命令观察案例应用的系统调用情况 strace: Process 4988 attached restart_syscall(<\.\.\. resuming interrupted nanosleep \.\.\.>) = 0 openat(AT_FDCWD, "/dev/sdb1", O_RDONLY|O_DIRECT) = 4 mmap(NULL, 33558528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f448d240000 read(4, "8vq\213\314\264u\373\4\336K\224\25@\371\1\252\2\262\252q\221\n0\30\225bD\252\266@J"\.\.\., 33554432) = 33554432 write(1, "Time used: 0.948897 s to read 33"\.\.\., 45) = 45 close(4) = 0 ###从 strace 的结果可以看到,案例应用调用了 openat 来打开磁盘分区 /dev/sdb1,并且传入的参数为 O_RDONLY|O_DIRECT(中间的竖线表示或)。 O_RDONLY 表示以只读方式打开,而 O_DIRECT 则表示以直接读取的方式打开,这会绕过系统的缓存。 修改源代码,删除 O_DIRECT 选项,让应用程序使用缓存 I/O ,而不是直接 I/O $ docker rm -f app # 删除上述案例应用 $ docker run --privileged --name=app -itd feisky/app:io-cached # 运行修复后的应用 $ docker logs app Reading data from disk /dev/sdb1 with buffer size 33554432 Time used: 0.037342 s s to read 33554432 bytes Time used: 0.029676 s to read 33554432 bytes 每次只需要 0.03 秒,就可以读取 32MB 数据,明显比之前的 0.9 秒快多了 $ cachetop 5 # 每隔5秒刷新一次数据 16:40:08 Buffers MB: 73 / Cached MB: 281 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 22106 root app 40960 0 0 100.0% 0.0% 40960*4k/5/1024=32M 40960次命中,每次读取4KB,那么5秒读取的数据为40960*4KB = 163840KB,即160MB 那么每秒读取数据为32MB,这与应用设计的每秒读取32MB相符 问题1:代码修复前,为什么看不到未命中呢?从修复后的数据来看,系统每5秒进行了40960次读取动作。 因为cachetop 工具并不把直接 I/O 算进来 问题2:既然使用直接IO,那么为什么还有1024次命中呢? 暂时未知。
18 | 案例篇:内存泄漏了,我该如何定位和处理?

内存泄漏;内存泄漏相关工具 ====================================================================================================================== 内存泄漏: 数据使用完毕后,没有被free()。 内存泄漏的危害非常大,这些忘记释放的内存,不仅应用程序自己不能访问,系统也不能把它们再次分配给其他应用。内存泄漏不断累积,甚至会耗尽系统内存。 虽然,系统最终可以通过 OOM (Out of Memory)机制杀死进程,但进程在 OOM 前,可能已经引发了一连串的反应,导致严重的性能问题。 比如,其他需要内存的进程,可能无法分配新的内存;内存不足,又会触发系统的缓存回收以及 SWAP 机制,从而进一步导致 I/O 的性能问题等等。 不会产生内存泄漏问题: 栈内存由系统自动分配和管理。一旦程序运行超出了这个局部变量的作用域,栈内存就会被系统自动回收,所以不会产生内存泄漏的问题。 只读段,包括程序的代码和常量,由于是只读的,不会再去分配新的内存,所以也不会产生内存泄漏。 数据段,包括全局变量和静态变量,这些变量在定义时就已经确定了大小,所以也不会产生内存泄漏。 会产生内存泄漏问题 堆内存由应用程序自己来分配和管理。除非程序退出,这些堆内存并不会被系统自动释放,而是需要应用程序明确调用库函数 free() 来释放它们。 如果应用程序没有正确释放堆内存,就会造成内存泄漏。 内存映射段,包括动态链接库和共享内存,其中共享内存由程序动态分配和管理。所以,如果程序在分配后忘了回收,就会导致跟堆内存类似的泄漏问题。 内存泄漏相关工具: memleak要求内核4.1+ 老版本内核可以使用valgrind

内存泄漏案例(memleak工具使用) $ docker run --name=app -itd feisky/app:mem-leak $ docker logs app #看到不断输出斐波那契数列,该容器即成功运行 top 虽然能观察系统和进程的内存占用情况,但今天的案例并不适合。内存泄漏问题,我们更应该关注内存使用的变化趋势。 $ vmstat 3 # 每隔3秒输出一组数据 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 6601824 97620 1098784 0 0 0 0 62 322 0 0 100 0 0 0 0 0 6601700 97620 1098788 0 0 0 0 57 251 0 0 100 0 0 0 0 0 6601320 97620 1098788 0 0 0 3 52 306 0 0 100 0 0 0 0 0 6601452 97628 1098788 0 0 0 27 63 326 0 0 100 0 0 2 0 0 6601328 97628 1098788 0 0 0 44 52 299 0 0 100 0 0 0 0 0 6601080 97628 1098792 0 0 0 0 56 285 0 0 100 0 0 内存的 free 列在不停的变化,并且是下降趋势;而 buffer 和 cache 基本保持不变。 未使用内存在逐渐减小,而 buffer 和 cache 基本不变,这说明,系统中使用的内存一直在升高。 但这并不能说明有内存泄漏,因为应用程序运行中需要的内存也可能会增大。 memleak 可以跟踪系统或指定进程的内存分配、释放请求,然后定期输出一个未释放内存和相应调用栈的汇总情况(默认 5 秒)。 使用memleak锁定可能存在内存泄露的进程 #这里直接锁定了app进程?我理解应该是top、pidstat -r 等命令锁定几个可疑进程,然后使用memleak工具确认 #后续有回复,memleak是可以去掉进程号选项的! $ /usr/share/bcc/tools/memleak -a -p $(pidof app) # -a 表示显示每个内存分配请求的大小以及地址 # -p 指定案例应用的PID号 WARNING: Couldn't find .text section in /app WARNING: BCC can't handle sym look ups for /app addr = 7f8f704732b0 size = 8192 addr = 7f8f704772d0 size = 8192 addr = 7f8f704712a0 size = 8192 addr = 7f8f704752c0 size = 8192 32768 bytes in 4 allocations from stack [unknown] [app] #因为案例应用运行在容器中,memleak 工具运行在容器之外,并不能直接访问进程路径 /app。 [unknown] [app] start_thread+0xdb [libpthread-2.27.so] $ docker cp app:/app /app $ /usr/share/bcc/tools/memleak -p $(pidof app) -a Attaching to pid 12512, Ctrl+C to quit. [03:00:41] Top 10 stacks with outstanding allocations: addr = 7f8f70863220 size = 8192 addr = 7f8f70861210 size = 8192 addr = 7f8f7085b1e0 size = 8192 addr = 7f8f7085f200 size = 8192 addr = 7f8f7085d1f0 size = 8192 40960 bytes in 5 allocations from stack fibonacci+0x1f [app] #查看源码中的这2个函数,最终锁定问题。 child+0x4f [app] #查看源码中的这2个函数,最终锁定问题。 start_thread+0xdb [libpthread-2.27.so] 修复后重新验证 $ docker rm -f app # 清理原来的案例应用 $ docker run --name=app -itd feisky/app:mem-leak-fix # 运行修复后的应用 $ /usr/share/bcc/tools/memleak -a -p $(pidof app) # 重新执行 memleak工具检查内存泄漏情况 Attaching to pid 18808, Ctrl+C to quit. [10:23:18] Top 10 stacks with outstanding allocations: [10:23:23] Top 10 stacks with outstanding allocations:
19 | 案例篇:为什么系统的Swap变高了(上)
20 | 案例篇:为什么系统的Swap变高了?(下)

Swap 原理;swap回收内存场景;NUMA 与 Swap;/proc/zoneinfo;/proc/sys/vm/swappiness =========================================================================================================== Swap 原理 Swap 就是把一块磁盘空间或者一个本地文件,当成内存来使用。它包括换出和换入两个过程。 换出,就是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存。 换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来。 我们常见的笔记本电脑的休眠和快速开机的功能,也基于 Swap 。休眠时,把系统的内存存入磁盘,这样等到再次开机时,只要从磁盘中加载内存就可以。这样就省去了很多应用程序的初始化过程,加快了开机速度。 -------------------------------------------------------------------------------------------------------- swap回收内存场景: 1.直接内存回收:有新的大块内存分配请求,但是剩余内存不足。这个时候系统就需要回收一部分内存(比如前面提到的缓存),进而尽可能地满足新内存请求。 2.一个专门的内核线程 kswapd0 用来定期回收内存: kswapd0 定义了三个内存阈值(watermark,也称为水位),分别是页最小阈值(pages_min)、页低阈值(pages_low)和页高阈值(pages_high)。剩余内存,则使用 pages_free 表示。 当pages_free<pages_min:进程可用内存都耗尽了,只有内核才可以分配内存 当pages_min<pages_free<pages_low:***内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止。*** 当pages_low<pages_free<pages_high:内存有一定压力,但还可以满足新内存请求。 当pages_free>pages_high:剩余内存比较多,没有内存压力。 ##一旦剩余内存小于页低阈值,就会触发内存的回收。## 页低阈值,其实可以通过内核选项 /proc/sys/vm/min_free_kbytes 来间接设置。min_free_kbytes 设置了页最小阈值,而其他两个阈值,都是根据页最小阈值计算生成的,计算方法如下 : pages_low = pages_min*5/4 pages_high = pages_min*3/2 -------------------------------------------------------------------------------------------------------- NUMA 与 Swap 处理器的 NUMA (Non-Uniform Memory Access)架构 NUMA架构下多Node时,可能会发生一个现象:系统剩余内存还很多的情况下,却发生了swap 在 NUMA 架构下,多个处理器被划分到不同 Node 上,且每个 Node 都拥有自己的本地内存空间。 同一个 Node 内部的内存空间,实际上又可以进一步分为不同的内存域(Zone),比如直接内存访问区(DMA)、普通内存区(NORMAL)、伪内存区(MOVABLE)等 NUMA 架构下的每个 Node 都有自己的本地内存空间,那么,在分析内存的使用时,我们也应该针对每个 Node 单独分析。 $ numactl --hardware #numactl 命令,来查看处理器在 Node 的分布情况,以及每个 Node 的内存使用情况。 available: 1 nodes (0) #只有一个 Node,也就是 Node 0 node 0 cpus: 0 1 #编号为 0 和 1 的两个 CPU, 都位于 Node 0 上 node 0 size: 7977 MB node 0 free: 4416 MB ... -------------------------------------------------------------------------------------------------------- $ cat /proc/zoneinfo ... Node 0, zone Normal pages free 227894 min 14896 #pages_min low 18620 #pages_low high 22344 #pages_high ... nr_free_pages 227894 #剩余内存页数;剩余内存远大于页高阈值,所以此时的 kswapd0 不会回收内存。 nr_zone_inactive_anon 11082 #非活跃的匿名页数。 nr_zone_active_anon 14024 #活跃的匿名页数。 nr_zone_inactive_file 539024 #非活跃的文件页数。 nr_zone_active_file 923986 #活跃的文件页数。 ... 某个 Node 内存不足时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。 具体选哪种模式,你可以通过 /proc/sys/vm/zone_reclaim_mode 来调整。 默认的 0 ,也就是刚刚提到的模式,表示既可以从其他 Node 寻找空闲内存,也可以从本地回收内存。 1、2、4 都表示只回收本地内存,2 表示可以回写脏数据回收内存,4 表示可以用 Swap 方式回收内存。 -------------------------------------------------------------------------------------------------------- swappiness 回收的内存既包括了文件页,又包括了匿名页。 对文件页的回收,当然就是直接回收缓存,或者把脏页写回磁盘后再回收。(文件缓存) 对匿名页的回收,其实就是通过 Swap 机制,把它们写入磁盘后再释放内存。(malloc()函数申请的空间---匿名页) Linux 提供了一个 /proc/sys/vm/swappiness 选项,用来调整使用 Swap 的积极程度。 swappiness 的范围是 0-100,数值越大,越积极使用 Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页。 swappiness 的范围是 0-100,不过要注意,这并不是内存的百分比,而是调整 Swap 积极程度的权重,即使你把它设置成 0,当剩余内存 + 文件页小于页高阈值时,还是会发生 Swap。

swap回收小结 在内存资源紧张时,Linux 通过直接内存回收和定期扫描的方式,来释放文件页和匿名页,以便把内存分配给更需要的进程使用。 文件页的回收比较容易理解,直接清空,或者把脏数据写回磁盘后再释放。 而对匿名页的回收,需要通过 Swap 换出到磁盘中,下次访问时,再从磁盘换入到内存中。 你可以设置 /proc/sys/vm/min_free_kbytes,来调整系统定期回收内存的阈值(也就是页低阈值),还可以设置 /proc/sys/vm/swappiness,来调整文件页和匿名页的回收倾向。 在 NUMA 架构下,每个 Node 都有自己的本地内存空间,而当本地内存不足时,默认既可以从其他 Node 寻找空闲内存,也可以从本地内存回收。 你可以设置 /proc/sys/vm/zone_reclaim_mode ,来调整 NUMA 本地内存的回收策略。 -------------------------- 在内存资源紧张时,Linux 会通过 Swap ,把不常访问的匿名页换出到磁盘中,下次访问的时候再从磁盘换入到内存中来。 你可以设置 /proc/sys/vm/min_free_kbytes,来调整系统定期回收内存的阈值;也可以设置 /proc/sys/vm/swappiness,来调整文件页和匿名页的回收倾向。 当 Swap 变高时,你可以用 sar、/proc/zoneinfo、/proc/pid/status 等方法,查看系统和进程的内存使用情况,进而找出 Swap 升高的根源和受影响的进程。 通常,降低 Swap 的使用,可以提高系统的整体性能,几种常见的降低方法: 1.禁止 Swap,现在服务器的内存足够大,所以除非有必要,禁用 Swap 就可以了。随着云计算的普及,大部分云平台中的虚拟机都默认禁止 Swap。 2.如果实在需要用到 Swap,可以尝试降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。 3.响应延迟敏感的应用,如果它们可能在开启 Swap 的服务器中运行,你还可以用库函数 mlock() 或者 mlockall() 锁定内存,阻止它们的内存换出。

swap回收案例(free、sar、cachetop命令) ============================================================================================================= $ free #在终端中运行 free 命令,查看 Swap 的使用情况 total used free shared buff/cache available Mem: 8169348 331668 6715972 696 1121708 7522896 Swap: 8388604 0 8388604 $ dd if=/dev/sda1 of=/dev/null bs=1G count=2048 ## 写入空设备,实际上只有磁盘的读请求,#模拟大文件的读取 $ sar -r -S 1 # -r表示显示内存使用情况,-S表示显示Swap使用情况 04:39:56 kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty 04:39:57 6249676 6839824 1919632 23.50 740512 67316 1691736 10.22 815156 841868 4 04:39:56 kbswpfree kbswpused %swpused kbswpcad %swpcad 04:39:57 8388604 0 0.00 0 0.00 04:39:57 kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty 04:39:58 6184472 6807064 1984836 24.30 772768 67380 1691736 10.22 847932 874224 20 04:39:57 kbswpfree kbswpused %swpused kbswpcad %swpcad 04:39:58 8388604 0 0.00 0 0.00 … 04:44:06 kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty 04:44:07 152780 6525716 8016528 98.13 6530440 51316 1691736 10.22 867124 6869332 0 #总的内存使用率(%memused)在不断增长,从开始的 23% 一直长到了 98%,并且主要内存都被缓冲区(kbbuffers)占用。 04:44:06 kbswpfree kbswpused %swpused kbswpcad %swpcad 04:44:07 8384508 4096 0.05 52 1.27 kbcommit,表示当前系统负载需要的内存。它实际上是为了保证系统内存不溢出,对需要内存的估计值。 %commit,就是这个值相对总内存的百分比。 kbactive,表示活跃内存,也就是最近使用过的内存,一般不会被系统回收。 kbinact,表示非活跃内存,也就是不常访问的内存,有可能会被系统回收。 总的内存使用率(%memused) sar命令观察到2个现象: 1.刚开始,剩余内存(kbmemfree)不断减少,而缓冲区(kbbuffers)则不断增大,由此可知,剩余内存不断分配给了缓冲区。 2.一段时间后,剩余内存已经很小,而缓冲区占用了大部分内存。这时候,Swap 的使用开始逐渐增大,缓冲区和剩余内存则只在小范围内波动。 $ cachetop 5 #看进程缓存的情况。 12:28:28 Buffers MB: 6349 / Cached MB: 87 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 18280 root python 22 0 0 100.0% 0.0% 18279 root dd 41088 41022 0 50.0% 50.0% dd 进程的读写请求只有 50% 的命中率,并且未命中的缓存页数(MISSES)为 41022(单位是页)。这说明,正是案例开始时运行的 dd,导致了缓冲区使用升高。 疑问:为什么 Swap 也跟着升高了呢?直观来说,缓冲区占了系统绝大部分内存,还属于可回收内存,内存不够用时,不应该先回收缓冲区吗? $ watch -d grep -A 15 'Normal' /proc/zoneinfo # -d 表示高亮变化的字段 # -A 表示仅显示Normal行以及之后的15行输出 Node 0, zone Normal pages free 21328 #剩余内存(pages_free)在一个小范围内不停地波动,当它小于页低阈值(pages_low) 时,又会突然增大到一个大于页高阈值(pages_high)的值。 min 14896 low 18620 #再结合刚刚用 sar 看到的剩余内存和缓冲区的变化情况,我们可以推导出,剩余内存和缓冲区的波动变化,正是由于内存回收和缓存再次分配的循环往复。 high 22344 spanned 1835008 present 1835008 managed 1796710 protection: (0, 0, 0, 0, 0) nr_free_pages 21328 #剩余内存(pages_free)在一个小范围内不停地波动,当它小于页低阈值(pages_low) 时,又会突然增大到一个大于页高阈值(pages_high)的值。 nr_zone_inactive_anon 79776 nr_zone_active_anon 206854 nr_zone_inactive_file 918561 nr_zone_active_file 496695 nr_zone_unevictable 2251 nr_zone_write_pending 0 推测: 当剩余内存小于页低阈值时,系统会回收一些缓存和匿名内存,使剩余内存增大。其中,缓存的回收导致 sar 中的缓冲区减小,而匿名内存的回收导致了 Swap 的使用增大。 紧接着,由于 dd 还在继续,剩余内存又会重新分配给缓存,导致剩余内存减少,缓冲区增大。 还有一个有趣的现象,如果多次运行 dd 和 sar,你可能会发现,在多次的循环重复中,有时候是 Swap 用得比较多,有时候 Swap 很少,反而缓冲区的波动更大。 换句话说,系统回收内存时,有时候会回收更多的文件页,有时候又回收了更多的匿名页。 这时因为swappiness,调整不同类型内存回收的配置选项 $ cat /proc/sys/vm/swappiness 60 查看进程 Swap 换出的虚拟内存大小,它保存在/proc/pid/status 中的 VmSwap 中(推荐你执行 man proc 来查询其他字段的含义) $ for file in /proc/*/status ; do awk '/VmSwap|Name|^Pid/{printf $2 " " $3}END{ print ""}' $file; done | sort -k 3 -n -r | head # 按VmSwap使用量对进程排序,输出进程名称、进程ID以及SWAP用量 dockerd 2226 10728 kB #从这里你可以看到,使用 Swap 比较多的是 dockerd 和 docker-containe 进程,所以,当 dockerd 再次访问这些换出到磁盘的内存时,也会比较慢。 docker-containe 2251 8516 kB snapd 936 4020 kB networkd-dispat 911 836 kB polkitd 1004 44 kB 这也说明了一点,虽然缓存属于可回收内存,但在类似大文件拷贝这类场景下,系统还是会用 Swap 机制来回收匿名内存,而不仅仅是回收占用绝大部分内存的文件页。
21 | 套路篇:如何“快准狠”找到系统内存的问题?

内存性能指标:系统内存、进程内存、Swap =========================================================================================================================================== 系统内存 已用内存和剩余内存:就是已经使用和还未使用的内存。 共享内存:通过 tmpfs 实现的,所以它的大小也就是 tmpfs 使用的内存大小。tmpfs 其实也是一种特殊的缓存。 可用内存:新进程可以使用的最大内存,它包括剩余内存和可回收缓存。 缓存包括两部分,一部分是磁盘读取文件的页缓存,用来缓存从磁盘读取的数据,可以加快以后再次访问的速度。另一部分,则是 Slab 分配器中的可回收内存。 缓冲区是对原始磁盘块的临时存储,用来缓存将要写入磁盘的数据。这样,内核就可以把分散的写集中起来,统一优化磁盘写入。 -------------------------------------------------- 进程内存 虚拟内存,包括了进程代码段、数据段、共享内存、已经申请的堆内存和已经换出的内存等。这里要注意,已经申请的内存,即使还没有分配物理内存,也算作虚拟内存。 常驻内存是进程实际使用的物理内存,不过,它不包括 Swap 和共享内存。 常驻内存一般会换算成占系统总内存的百分比,也就是进程的内存使用率。 共享内存,既包括与其他进程共同使用的真实的共享内存,还包括了加载的动态链接库以及程序的代码段等。 Swap 内存,是指通过 Swap 换出到磁盘的内存。 缺页异常,有两种场景: 1.可以直接从物理内存中分配时,被称为次缺页异常。 2.需要磁盘 I/O 介入(比如 Swap)时,被称为主缺页异常。 注意:系统内存和进程内存是2个维度,不能被直接比较的!!!!! -------------------------------------------------- Swap 的使用情况,比如 Swap 的已用空间、剩余空间、换入速度和换出速度等。 已用空间和剩余空间很好理解,就是字面上的意思,已经使用和没有使用的内存空间。 换入和换出速度,则表示每秒钟换入和换出内存的大小。
内存性能工具
工具的2个维度,从工具出发和从性能指标出发

内存性能瓶颈的分析思路;示例 ------------------------------------------ 具体的分析思路主要有这几步。 1.先用 free 和 top,查看系统整体的内存使用情况。 2.再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断出内存问题的类型。 3.最后进行详细分析,比如内存分配分析、缓存 / 缓冲区分析、具体进程的内存使用分析等。 --------------------------------------------- 举几个例子你可能会更容易理解。 第一个例子,当你通过 free,发现大部分内存都被缓存占用后,可以使用 vmstat 或者 sar 观察一下缓存的变化趋势,确认缓存的使用是否还在继续增大。 如果继续增大,则说明导致缓存升高的进程还在运行,那你就能用缓存 / 缓冲区分析工具(比如 cachetop、slabtop 等),分析这些缓存到底被哪里占用。 第二个例子,当你 free 一下,发现系统可用内存不足时,首先要确认内存是否被缓存 / 缓冲区占用。排除缓存 / 缓冲区后,你可以继续用 pidstat 或者 top,定位占用内存最多的进程。 找出进程后,再通过进程内存空间工具(比如 pmap),分析进程地址空间中内存的使用情况就可以了。 第三个例子,当你通过 vmstat 或者 sar 发现内存在不断增长后,可以分析中是否存在内存泄漏的问题。 比如你可以使用内存分配分析工具 memleak ,检查是否存在内存泄漏。如果存在内存泄漏问题,memleak 会为你输出内存泄漏的进程以及调用堆栈。

内存优化的思路 ============================================================ 内存优化: 内存调优最重要的就是,保证应用程序的热点数据放到内存中,并尽量减少换页和交换。 常见的优化思路有这么几种。 1.最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。 2.减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等。 3.尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问。 4.使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进程耗尽。 5.通过 /proc/pid/oom_adj ,调整核心应用的 oom_score。这样,可以保证即使内存紧张,核心应用也不会被 OOM 杀死。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 易语言 —— 开山篇