内存性能篇:物理内存与虚拟机地址空间;内存的分配和回收机制;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 把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。
内存分配:malloc()、brk()、mmap();内存回收机制(回收缓存、回收不常访问的内存、OOM);内存资源紧张时的2种可能
复制代码
复制代码
查看内存使用情况: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 直接相加得出结果。
查看内存使用情况:free;top
复制代码

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。
Buffer 和 Cache;普通文件与块设备文件
复制代码
复制代码
文件写案例;磁盘写案例;文件读案例;磁盘读案例
==================================================================================================
这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 |
+---------+----------------+------------+-----------+---------+
    
缓存命中率;cachestat、cachetop;pcstat
复制代码
复制代码
缓存命中率案例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次命中呢?
    暂时未知。
缓存命中率案例1;缓存命中率案例2
复制代码

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:
内存泄漏案例(memleak工具使用)
复制代码

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 原理;swap回收内存场景;NUMA 与 Swap;/proc/zoneinfo;/proc/sys/vm/swappiness
复制代码
复制代码
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回收小结
复制代码
复制代码
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.0004: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 机制来回收匿名内存,而不仅仅是回收占用绝大部分内存的文件页。
swap回收案例(free、sar、cachetop命令)
复制代码

21 | 套路篇:如何“快准狠”找到系统内存的问题?

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

--------------------------------------------------
进程内存
    虚拟内存,包括了进程代码段、数据段、共享内存、已经申请的堆内存和已经换出的内存等。这里要注意,已经申请的内存,即使还没有分配物理内存,也算作虚拟内存。
    常驻内存是进程实际使用的物理内存,不过,它不包括 Swap 和共享内存。
        常驻内存一般会换算成占系统总内存的百分比,也就是进程的内存使用率。
    共享内存,既包括与其他进程共同使用的真实的共享内存,还包括了加载的动态链接库以及程序的代码段等。
    Swap 内存,是指通过 Swap 换出到磁盘的内存。
    缺页异常,有两种场景:
        1.可以直接从物理内存中分配时,被称为次缺页异常。
        2.需要磁盘 I/O 介入(比如 Swap)时,被称为主缺页异常。

    注意:系统内存和进程内存是2个维度,不能被直接比较的!!!!!
--------------------------------------------------
Swap 的使用情况,比如 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 杀死。
内存优化的思路
复制代码

 

posted @   雲淡風輕333  阅读(1113)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 易语言 —— 开山篇
点击右上角即可分享
微信分享提示