linux性能评估-内存案例实战篇

1.内存泄漏,该如何定位和处理

机器配置:2 CPU,4GB 内存

预先安装 sysstat、Docker 以及 bcc 软件包,比如:

# install sysstat docker
sudo apt-get install -y sysstat docker.io
 
# Install bcc
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD
echo "deb https://repo.iovisor.org/apt/bionic bionic 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,它提供了一系列的 Linux 性能分析工具,常用来动态追踪进程和内核的行为。更多工作原理你先不用深究,后面学习我们会逐步接触。这里你只需要记住,按照上面步骤安装完后,它提供的所有工具都位于 /usr/share/bcc/tools 这个目录中。

注意:bcc-tools 需要内核版本为 4.1 或者更高,如果你使用的是 CentOS7,或者其他内核版本比较旧的系统,那么你需要手动升级内核版本后再安装。

同以前的案例一样,下面的所有命令都默认以 root 用户运行,如果你是用普通用户身份登陆系统,请运行 sudo su root 命令切换到 root 用户。

安装完成后,再执行下面的命令来运行案例:

$ docker run --name=app -itd feisky/app:mem-leak

案例成功运行后,你需要输入下面的命令,确认案例应用已经正常启动。如果一切正常,你应该可以看到下面这个界面:

$ docker logs app
2th => 1
3th => 2
4th => 3
5th => 5
6th => 8
7th => 13

从输出中,我们可以发现,这个案例会输出斐波那契数列的一系列数值。实际上,这些数值每隔 1 秒输出一次。

知道了这些,我们应该怎么检查内存情况,判断有没有泄漏发生呢?你首先想到的可能是 top 工具,不过,top 虽然能观察系统和进程的内存占用情况,但今天的案例并不适合。内存泄漏问题,我们更应该关注内存使用的变化趋势。

运行下面的 vmstat ,等待一段时间,观察内存的变化情况。如果忘了 vmstat 里各指标的含义,记得复习前面内容,或者执行 man vmstat 查询。

root@ubuntu:/home/xhong# vmstat 3
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 392324 2100404  23480 791260   13   41 19871   104  615  619  4 23 72  1  0
 0  0 392324 2100272  23480 791296    0    0     0    52  207  386  1  1 99  0  0
 0  0 392324 2100148  23488 791296    0    0     0     5  186  371  0  0 99  0  0
 0  0 392324 2100180  23520 791332    0    0     0   373  297  456  1  1 99  0  0
 0  0 392324 2100056  23520 791332    0    0     0     0  150  342  1  0 99  0  0

从输出中你可以看到,内存的 free 列在不停的变化,并且是下降趋势;而 buffer 和 cache 基本保持不变。

未使用内存在逐渐减小,而 buffer 和 cache 基本不变,这说明,系统中使用的内存一直在升高。但这并不能说明有内存泄漏,因为应用程序运行中需要的内存也可能会增大。比如说,程序中如果用了一个动态增长的数组来缓存计算结果,占用内存自然会增长。

那怎么确定是不是内存泄漏呢?或者换句话说,有没有简单方法找出让内存增长的进程,并定位增长内存用在哪儿呢?

根据前面内容,你应该想到了用 top 或 ps 来观察进程的内存使用情况,然后找出内存使用一直增长的进程,最后再通过 pmap 查看进程的内存分布。

但这种方法并不太好用,因为要判断内存的变化情况,还需要你写一个脚本,来处理 top 或者 ps 的输出。

这里,我介绍一个专门用来检测内存泄漏的工具,memleak。memleak 可以跟踪系统或指定进程的内存分配、释放请求,然后定期输出一个未释放内存和相应调用栈的汇总情况(默认 5 秒)。

当然,memleak 是 bcc 软件包中的一个工具,我们一开始就装好了,执行 /usr/share/bcc/tools/memleak 就可以运行它。比如,我们运行下面的命令:

# -a 表示显示每个内存分配请求的大小以及地址
# -p 指定案例应用的 PID 号
$ /usr/share/bcc/tools/memleak -a -p $(pidof app)
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]
        [unknown] [app]
        start_thread+0xdb [libpthread-2.27.so]

从 memleak 的输出可以看到,案例应用在不停地分配内存,并且这些分配的地址没有被回收。

这里有一个问题,Couldn’t find .text section in /app,所以调用栈不能正常输出,最后的调用栈部分只能看到 [unknown] 的标志。

为什么会有这个错误呢?实际上,这是由于案例应用运行在容器中导致的。memleak 工具运行在容器之外,并不能直接访问进程路径 /app。

比方说,在终端中直接运行 ls 命令,你会发现,这个路径的确不存在:

$ ls /app
ls: cannot access '/app': No such file or directory

类似的问题,我在 CPU 模块中的perf 使用方法已经提到好几个解决思路。最简单的方法,就是在容器外部构建相同路径的文件以及依赖库。这个案例只有一个二进制文件,所以只要把案例应用的二进制文件放到 /app 路径中,就可以修复这个问题。

比如,你可以运行下面的命令,把 app 二进制文件从容器中复制出来,然后重新运行 memleak 工具:

$ 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]
        child+0x4f [app]
        start_thread+0xdb [libpthread-2.27.so]

这一次,我们终于看到了内存分配的调用栈,原来是 fibonacci() 函数分配的内存没释放。

定位了内存泄漏的来源,下一步自然就应该查看源码,想办法修复它。我们一起来看案例应用的源代码:

$ docker exec app cat /app.c
...
long long *fibonacci(long long *n0, long long *n1)
{
    // 分配 1024 个长整数空间方便观测内存的变化情况
    long long *v = (long long *) calloc(1024, sizeof(long long));
    *v = *n0 + *n1;
    return v;
}
 
 
void *child(void *arg)
{
    long long n0 = 0;
    long long n1 = 1;
    long long *v = NULL;
    for (int n = 2; n > 0; n++) {
        v = fibonacci(&n0, &n1);
        n0 = n1;
        n1 = *v;
        printf("%dth => %lld\n", n, *v);
        sleep(1);
    }
}
...

你会发现, child() 调用了 fibonacci() 函数,但并没有释放 fibonacci() 返回的内存。所以,想要修复泄漏问题,在 child() 中加一个释放函数就可以了,比如:

void *child(void *arg)
{
    ...
    for (int n = 2; n > 0; n++) {
        v = fibonacci(&n0, &n1);
        n0 = n1;
        n1 = *v;
        printf("%dth => %lld\n", n, *v);
        free(v);    // 释放内存
        sleep(1);
    }
}

修复后的代码放到了 app-fix.c,也打包成了一个 Docker 镜像。你可以运行下面的命令,验证一下内存泄漏是否修复:

# 清理原来的案例应用
$ docker rm -f app
 
# 运行修复后的应用
$ docker run --name=app -itd feisky/app:mem-leak-fix
 
# 重新执行 memleak 工具检查内存泄漏情况
$ /usr/share/bcc/tools/memleak -a -p $(pidof app)
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:

现在,我们看到,案例应用已经没有遗留内存,证明我们的修复工作成功完成。

小结:

应用程序可以访问的用户内存空间,由只读段、数据段、堆、栈以及文件映射段等组成。其中,堆内存和内存映射,需要应用程序来动态管理内存段,所以我们必须小心处理。不仅要会用标准库函数malloc() 来动态分配内存,还要记得在用完内存后,调用库函数 _free() 来 _ 释放它们。

今天的案例比较简单,只用加一个 free() 调用就能修复内存泄漏。不过,实际应用程序就复杂多了。比如说,

  • malloc() 和 free() 通常并不是成对出现,而是需要你,在每个异常处理路径和成功路径上都释放内存 。
  • 在多线程程序中,一个线程中分配的内存,可能会在另一个线程中访问和释放。
  • 更复杂的是,在第三方的库函数中,隐式分配的内存可能需要应用程序显式释放。

所以,为了避免内存泄漏,最重要的一点就是养成良好的编程习惯,比如分配内存后,一定要先写好内存释放的代码,再去开发其他逻辑。

 

 

2.内存中的Buffer 和 Cache 在不同场景下的使用情况

机器配置:2 CPU,4GB 内存。

预先安装 sysstat 包,如 apt install sysstat。

准备环节的最后一步,为了减少缓存的影响,记得在第一个终端中,运行下面的命令来清理系统缓存:

# 清理文件页、目录项、Inodes 等各种缓存
$ echo 3 > /proc/sys/vm/drop_caches

这里的 /proc/sys/vm/drop_caches ,就是通过 proc 文件系统修改内核行为的一个示例,写入 3 表示清理文件页、目录项、Inodes 等各种缓存。

场景 1:磁盘和文件写案例

1.我们先来模拟第一个场景。首先,在第一个终端,运行下面这个 vmstat 命令:

写文件:

root@ubuntu:/home/xhong# vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0 792276 760464   3900 178292    7   16  1067    35  203  429  4  2 93  0  0
 0  0 792276 760216   3900 178328    0    0     0     0  303  571  2  1 97  0  0
 0  0 792276 760216   3900 178328    0    0     0     0  155  342  2  0 98  0  0

输出界面里, 内存部分的 buff 和 cache ,以及 io 部分的 bi 和 bo 就是我们要关注的重点。

  • buff 和 cache 就是我们前面看到的 Buffers 和 Cache,单位是 KB。
  • bi 和 bo 则分别表示块设备读取和写入的大小,单位为块 / 秒。因为 Linux 中块的大小是 1KB,所以这个单位也就等价于 KB/s。

正常情况下,空闲系统中,你应该看到的是,这几个值在多次结果中一直保持不变。
2.接下来,到第二个终端执行 dd 命令,通过读取随机设备,生成一个 500MB 大小的文件:

$ dd if=/dev/urandom of=/tmp/file bs=1M count=500

3.然后再回到第一个终端,观察 Buffer 和 Cache 的变化情况:

root@ubuntu:/home/xhong# vmstat 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 792276 761200   2560 177500    7   16  1062    41  202  428  4  2 93  0  0
 0  0 792276 761200   2560 177520    0    0     0     0  115  304  1  0 99  0  0
 0  0 792276 760952   2704 177512    0    0   136    24  161  353  1  1 99  0  0
 0  0 792276 761324   2704 177520    0    0     0     0  570 1065  3  3 94  0  0
 0  0 792276 760944   2988 177520    4    0   288     0  507  803  4  2 95  0  0
 0  0 792276 760952   2992 177556    0    0     4     0  338  789  3  1 96  0  0
 0  0 792276 760952   2992 177556    0    0     0     0  174  325  1  0 99  0  0
 1  0 792276 611028   3024 326092    0    0   108 65748  680  743  1 45 54  1  0
 1  0 792276 447596   3576 489016    0    0   592 159744 1015 1085  4 53 44  0  0
 1  0 792276 285868   3580 650696    0    0     4 176128  863  861  1 54 44  1  0
 0  0 792276 234968   3588 702384    0    0    12 110592  527  471  2 37 58  4  0
 0  0 792276 234968   3588 702384    0    0     0     0  184  340  2  1 98  0  0
 0  0 792276 234968   3600 702388    0    0     0    92  200  386  2  0 99  0  0

通过观察 vmstat 的输出,我们发现,在 dd 命令运行时, Cache 在不停地增长,而 Buffer 基本保持不变。
再进一步观察 I/O 的情况,你会看到,
在 Cache 刚开始增长时,块设备 I/O 很少,bi 只出现了一次 488 KB/s,bo 则只有一次 4KB。而过一段时间后,才会出现大量的块设备写,比如 bo 变成了 159744。
当 dd 命令结束后,Cache 不再增长,但块设备写还会持续一段时间,并且,多次 I/O 写的结果加起来,才是 dd 要写的 500M 的数据。
把这个结果,跟我们刚刚了解到的 Cache 的定义做个对比,你可能会有点晕乎。为什么前面文档上说 Cache 是文件读的页缓存,怎么现在写文件也有它的份?
这个疑问,我们暂且先记下来,接着再来看另一个磁盘写的案例。两个案例结束后,我们再统一进行分析。
不过,对于接下来的案例,必须强调一点:
下面的命令对环境要求很高,需要你的系统配置多块磁盘,并且磁盘分区 /dev/sdb1 还要处于未使用状态。如果你只有一块磁盘,千万不要尝试,否则将会对你的磁盘分区造成损坏。
如果你的系统符合标准,就可以继续在第二个终端中,运行下面的命令。清理缓存后,向磁盘分区 /dev/sdb1 写入 2GB 的随机数据:

写磁盘:

# 首先清理缓存
$ echo 3 > /proc/sys/vm/drop_caches
# 然后运行 dd 命令向磁盘分区 /dev/sdb1 写入 2G 数据
$ dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048

然后,再回到终端一,观察内存和 I/O 的变化情况:

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,这跟我们在文档中查到的定义是一样的。
对比两个案例,我们发现,写文件时会用到 Cache 缓存数据,而写磁盘则会用到 Buffer 来缓存数据。所以,回到刚刚的问题,虽然文档上只提到,Cache 是文件读的缓存,但实际上,Cache 也会缓存写文件时的数据。

场景 2:磁盘和文件读案例

了解了磁盘和文件写的情况,我们再反过来想,磁盘和文件读的时候,又是怎样的呢?
我们回到第二个终端,运行下面的命令。清理缓存后,从文件 /tmp/file 中,读取数据写入空设备:

# 首先清理缓存
$ echo 3 > /proc/sys/vm/drop_caches
# 运行 dd 命令读取文件数据
$ dd if=/tmp/file of=/dev/null

然后,再回到终端一,观察内存和 I/O 的变化情况:

root@ubuntu:/home/xhong# vmstat 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 789972 696996  11596 223964    6   15  1029    45  198  418  4  2 93  0  0
 2  0 789972 696988  11604 223964    0    0     0     0  175  479  1  1 99  0  0
 1  0 789972 696988  11604 223964    0    0     0     0  162  388  1  1 99  0  0
 0  0 789972 696988  11604 223964    0    0     0     0  167  460  2  0 98  0  0
 1  0 789972 554504  11712 366208    0    0 142504     0 2511 2571  4 21 74  1  0
 1  0 789972 323364  11712 597380    0    0 231040     0 4501 5313 10 47 43  1  0
 0  0 789972 184452  11712 736464    0    0 138644     0 2585 2771  6 29 61  4  0
 0  0 789972 184452  11712 736464    0    0     0    48  111  281  1  1 99  0  0
 0  0 789972 184452  11712 736464    0    0     0     0  460  885  2  2 96  0  0

观察 vmstat 的输出,你会发现读取文件时(也就是 bi 大于 0 时),Buffer 保持不变,而 Cache 则在不停增长。这跟我们查到的定义“Cache 是对文件读的页缓存”是一致的。

那么,磁盘读又是什么情况呢?我们再运行第二个案例来看看。
首先,回到第二个终端,运行下面的命令。清理缓存后,从磁盘分区 /dev/sda1 中读取数据,写入空设备:

# 首先清理缓存
$ echo 3 > /proc/sys/vm/drop_caches
# 运行 dd 命令读取文件
$ dd if=/dev/sda1 of=/dev/null bs=1M count=1024

然后,再回到终端一,观察内存和 I/O 的变化情况:

root@ubuntu:/home/xhong# vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0 789972 753200   4720 178428    6   15  1030    45  198  418  4  2 93  0  0
 0  0 789972 752836   4860 178424   16    0   172     0  566  998  3  1 96  1  0
 0  0 789972 752836   4860 178420    0    0     0     0  416  773  3  2 95  0  0
 1  0 789972 752836   4860 178420    0    0     0     0  155  307  0  1 99  0  0
 0  1 789972 576872 178924 178432    0    0 174140     0 1062 1428  4 42 48  6  0
 1  0 789972 227564 527604 178460    0    0 348672    48 1179 1656  3 26 47 24  0
 2  0 790028  81060 687904 171188    0   72 390144    72 1275 1519  1 37 32 29  0
 0  0 790564  78148 694584 171224    0  332 136204   332  571  840  1 14 82  4  0
 0  0 790564  78148 694584 171224    0    0     0     0  151  305  1  1 98  0  0
 0  0 790564  78148 694584 171224    0    0     0     0  181  382  1  1 99  0  0
 0  0 790564  78148 694584 171224    0    0     0     0  166  360  1  0 98  0  0

观察 vmstat 的输出,你会发现读磁盘时(也就是 bi 大于 0 时),Buffer 和 Cache 都在增长,但显然 Buffer 的增长快很多。这说明读磁盘时,数据缓存到了 Buffer 中。

得出这个结论:读文件时数据会缓存到 Cache 中,而读磁盘时数据会缓存到 Buffer 中。
到这里你应该发现了,虽然文档提供了对 Buffer 和 Cache 的说明,但是仍不能覆盖到所有的细节。比如说,今天我们了解到的这两点:

  • Buffer 既可以用作“将要写入磁盘数据的缓存”,也可以用作“从磁盘读取数据的缓存”。
  • Cache 既可以用作“从文件读取数据的页缓存”,也可以用作“写文件的页缓存”。

简单来说,Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中
从写的角度来说,不仅可以优化磁盘和文件的写入,对应用程序也有好处,应用程序可以在数据真正落盘前,就返回去做其他工作。
从读的角度来说,不仅可以提高那些频繁访问数据的读取速度,也降低了频繁 I/O 对磁盘的压力。

posted @ 2019-07-01 15:37  小鱼儿_summer  阅读(768)  评论(0编辑  收藏  举报