Buffer 和 Cache 的介绍
查看内存使用情况
# 注意不同版本的free输出可能会有所不同 $ free total used free shared buff/cache available Mem: 8169348 263524 6875352 668 1030472 7611064 Swap: 0 0 0
显然,这个界面包含了物理内存 Mem 和交换分区 Swap 的具体使用情况,比如总内存、已用内存、缓存、可用内存等。其中缓存是 Buffer 和 Cache 两部分的总和 。
大部分指标都比较容易理解,但 Buffer 和 Cache 可能不太好区分。从字面上来说,Buffer 是缓冲区,而 Cache 是缓存,两者都是数据在内存中的临时存储;
Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中。
uffer 和 Cache 分别缓存的是
对磁盘和文件系统的读写数据。从写的角度来说,不仅可以优化磁盘和文件的写入,对应用程序也有好处,应用程序可以在数据真正落盘前,就返回去做其他工作。
从读的角度来说,不仅可以提高那些频繁访问数据的读取速度,也降低了频繁 I/O 对磁盘的压力。
利用缓存的命中率来优化系统。
所谓缓存命中率,是指直接通过缓存获取数据的请求次数,占所有数据请求次数的百分比。
命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。实际上,缓存是现在所有高并发系统必需的核心模块,主要作用就是把经常访问的数据(也就是热点数据),提前读入到内存中。这样,下次访问时就可以直接从内存读取数据,而不需要经过硬盘,从而加快应用程序的响应速度。
这些独立的缓存模块通常会提供查询接口,方便随时查看缓存的命中情况。不过 Linux 系统中并没有直接提供这些接口,所以这里介绍一下,cachestat 和 cachetop ,它们正是查看系统缓存命中情况的工具。
cachestat 提供了整个操作系统缓存的读写命中情况。
cachetop 提供了每个进程的缓存命中情况。
这两个工具都是 bcc 软件包的一部分,它们基于 Linux 内核的 eBPF(extended Berkeley Packet Filters)机制,来跟踪内核中管理的缓存,并输出缓存的使用和命中情况。
bcc-tools 需要内核版本为 4.1 或者更新的版本,如果你用的是 CentOS,那就需要手动升级,但我升到5.8内核版本工具版本问题报错
安装
yum update rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org && rpm -Uvh http://www.elrepo.org/elrepo-release-7.0-2.el7.elrepo.noarch.rpm uname -r yum remove kernel-headers kernel-tools kernel-tools-libs yum -y install perl yum --disablerepo="*" --enablerepo="elrepo-kernel" install kernel-lt kernel-lt-devel kernel-lt-headers kernel-lt-tools kernel-lt-tools-libs kernel-lt-tools-libs-devel 想要升级最新版本执行下面安装命令 yum --disablerepo="*" --enablerepo="elrepo-kernel" install kernel-ml kernel-ml-devel kernel-ml-headers kernel-ml-tools kernel-ml-tools-libs kernel-ml-tools-libs-devel sed -i '/GRUB_DEFAULT/s/=.*/=0/' /etc/default/grub grub2-mkconfig -o /boot/grub2/grub.cfg reboot uname -r 查看内核 4.4.233-1.el7.elrepo.x86_64 yum install -y bcc-tools 安装工具集 添加环境变量 echo 'export PATH=$PATH:/usr/share/bcc/tools' > /etc/profile.d/bcc-tools.sh exec bash [root@localhost ~]# cachestat 1 1 HITS MISSES DIRTIES HITRATIO BUFFERS_MB CACHED_MB 0 0 0 0.00% 2 302
cachestat 的输出其实是一个表格。每行代表一组数据,而每一列代表不同的缓存统计指标。这些指标从左到右依次表示:
TOTAL ,表示总的 I/O 次数;
MISSES ,表示缓存未命中的次数;
HITS ,表示缓存命中的次数;
DIRTIES, 表示新增到缓存中的脏页数;
BUFFERS_MB 表示 Buffers 的大小,以 MB 为单位;
CACHED_MB 表示 Cache 的大小,以 MB 为单位。
再来看一个 cachetop 的运行界面:
$ 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%
它的输出跟 top 类似,默认按照缓存的命中次数(HITS)排序,展示了每个进程的缓存命中情况。具体到每一个指标,这里的 HITS、MISSES 和 DIRTIES ,跟 cachestat 里的含义一样,分别代表间隔时间内的缓存命中次数、未命中次数以及新增到缓存中的脏页数。而 READ_HIT 和 WRITE_HIT ,分别表示读和写的缓存命中率。
指定文件的缓存大小
除了缓存的命中率外,还有一个指标也会很感兴趣,那就是指定文件在内存中的缓存大小。可以使用 pcstat 这个工具,来查看文件在内存中的缓存大小以及缓存比例。pcstat 是一个基于 Go 语言开发的工具,所以安装它之前,你首先应该安装 Go 语言,你可以点击这里下载安装。
cd /usr/bin if [ $(uname -m) == "x86_64" ] ; then curl -L -o pcstat https://github.com/tobert/pcstat/raw/2014-05-02-01/pcstat.x86_64 else curl -L -o pcstat https://github.com/tobert/pcstat/raw/2014-05-02-01/pcstat.x86_32 fi chmod 755 pcstat
全部安装完成后,可以运行 pcstat 来查看文件的缓存情况了。比如,下面就是一个 pcstat 运行的示例,它展示了 /bin/ls 这个文件的缓存情况:
[root@localhost ~]# pcstat /bin/ls |----------+----------------+------------+-----------+---------| | Name | Size | Pages | Cached | Percent | |----------+----------------+------------+-----------+---------| | /bin/ls | 117608 | 29 | 0 | 000.000 | |----------+----------------+------------+-----------+---------|
这个输出中,Cached 就是 /bin/ls 在缓存中的大小,而 Percent 则是缓存的百分比。如果看到它们都是 0,这说明 /bin/ls 并不在缓存中。
接着,如果执行一下 ls 命令,再运行相同的命令来查看的话,就会发现 /bin/ls 都在缓存中了:
[root@localhost ~]# ls anaconda-ks.cfg file [root@localhost ~]# pcstat /bin/ls |----------+----------------+------------+-----------+---------| | Name | Size | Pages | Cached | Percent | |----------+----------------+------------+-----------+---------| | /bin/ls | 117608 | 29 | 29 | 100.000 | |----------+----------------+------------+-----------+---------|
知道了缓存相应的指标和查看系统缓存的方法后
# 生成一个512MB的临时文件 #dd if=/dev/sda1 of=file bs=1M count=512 # 清理缓存 # echo 3 > /proc/sys/vm/drop_caches # pcstat file |----------+----------------+------------+-----------+---------| | Name | Size | Pages | Cached | Percent | |----------+----------------+------------+-----------+---------| | file | 536870912 | 131072 | 62974 | 048.045 | |----------+----------------+------------+-----------+---------| echo 3 > /proc/sys/vm/drop_caches echo 3 > /proc/sys/vm/drop_caches echo 3 > /proc/sys/vm/drop_caches # pcstat file |----------+----------------+------------+-----------+---------| | Name | Size | Pages | Cached | Percent | |----------+----------------+------------+-----------+---------| | file | 536870912 | 131072 | 0 | 000.000 | |----------+----------------+------------+-----------+---------|
运行 pcstat 命令,确认刚刚生成的文件不在缓存中。如果一切正常,看到 Cached 和 Percent 都是 0:,如果不是0多清理一下缓存
现在运行 cachetop 命令:
# 每隔5秒刷新一次数据 $ cachetop 5
运行 dd 命令测试文件的读取速度:
[root@localhost ~]# dd if=file of=/dev/null bs=1M 记录了512+0 的读入 记录了512+0 的写出 536870912字节(537 MB)已复制,20.7171 秒,25.9 MB/秒
从 dd 的结果可以看出,这个文件的读性能是 33.4 MB/s。由于在 dd 命令运行前我们已经清理了缓存,所以 dd 命令读取数据时,肯定要通过文件系统从磁盘中读取。
查看 cachetop 界面的缓存命中情况
07:57:55 Buffers MB: 0 / Cached MB: 288 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 1409 root cachetop 3 0 0 100.0% 0.0% 1435 root dd 27648 27648 0 50.0% 50.0%
从 cachetop 的结果可以发现,并不是所有的读都落到了磁盘上,事实上读请求的缓存命中率只有 50% 。
继续尝试相同的测试命令。终端2再次执行刚才的 dd 命令
[root@localhost ~]# dd if=file of=/dev/null bs=1M 记录了512+0 的读入 记录了512+0 的写出 536870912字节(537 MB)已复制,0.123877 秒,4.3 GB/秒
磁盘的读性能居然变成了 4.5 GB/s,比第一次的结果明显高了太多
看看 cachetop 的情况
08:03:35 Buffers MB: 0 / Cached MB: 635 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 1409 root cachetop 1 0 0 100.0% 0.0% 1457 root bash 277 0 0 100.0% 0.0% 1457 root dd 131644 0 0 100.0% 0.0%
cachetop 也有了不小的变化。可以发现,这次的读的缓存命中率是 100.0%,也就是说这次的 dd 命令全部命中了缓存,所以才会看到那么高的性能。
终端2再次执行 pcstat 查看文件 file 的缓存情况
[root@localhost ~]# pcstat file |----------+----------------+------------+-----------+---------| | Name | Size | Pages | Cached | Percent | |----------+----------------+------------+-----------+---------| | file | 536870912 | 131072 | 131072 | 100.000 | |----------+----------------+------------+-----------+---------|
pcstat 的结果可以发现,测试文件 file 已经被全部缓存了起来,这跟刚才观察到的缓存命中率 100% 是一致的。
这两次结果说明,系统缓存对第二次 dd 操作有明显的加速效果,可以大大提高文件读取的性能。但同时也要注意,如果把 dd 当成测试文件系统性能的工具,由于缓存的存在,就会导致测试结果严重失真。
再来看一个文件读写的案例
开启两个终端。分别 SSH 登录到机器上后,先在第一个终端中运行 cachetop 命令:
# 每隔5秒刷新一次数据 $ cachetop 5
接着,再到第二个终端,执行下面的命令运行案例:
docker run --privileged --name=app -itd feisky/app:io-direct
查看环境是否启动完成
[root@localhost ~]# docker logs app Reading data from disk /dev/sda2 with buffer size 33554432 Time used: 0.090524 s to read 33554432 bytes Time used: 0.029526 s to read 33554432 bytes Time used: 0.028942 s to read 33554432 bytes Time used: 0.028966 s to read 33554432 bytes Time used: 0.027196 s to read 33554432 bytes
可以看到,每读取 32 MB 的数据,就需要花 0.9 秒
这个输出似乎有点意思了。1024 次缓存全部命中,读的命中率是 100%,看起来全部的读请求都经过了系统缓存。但是问题又来了,如果真的都是缓存 I/O,读取速度不应该这么慢。
08:19:57 Buffers MB: 0 / Cached MB: 959 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 1409 root cachetop 3 0 0 100.0% 0.0% 1748 root dockerd 10 0 5 50.0% 0.0% 1811 root app 2560 0 0 100.0% 0.0%
每秒实际读取的数据大小。HITS 代表缓存的命中次数,那么每次命中能读取是一页数据。内存以页为单位进行管理,而每个页的大小是 4KB。所以,在 5 秒的时间间隔里,命中的缓存为 1024*4K/1024 = 4MB,再除以 5 秒,可以得到每秒读的缓存是 0.8MB,显然跟案例应用的 32 MB/s 相差太多。
如果为系统调用设置直接 I/O 的标志,就可以绕过系统缓存。那么,要判断应用程序是否用了直接 I/O,最简单的方法当然是观察它的系统调用,查找应用程序在调用它们时的选项。还是 strace。
[root@localhost ~]# strace -p $(pgrep app) strace: Process 1811 attached restart_syscall(<... resuming interrupted read ...>) = 0 openat(AT_FDCWD, "/dev/sda2", O_RDONLY|O_DIRECT) = 4 mmap(NULL, 33558528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fbb65270000 read(4, " "..., 33554432) = 33554432 write(1, "Time used: 0.039126 s to read 33"..., 45) = 45 close(4) = 0 munmap(0x7fbb65270000, 33558528) = 0 nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffd1dcc01c0) = 0 openat(AT_FDCWD, "/dev/sda2", O_RDONLY|O_DIRECT) = 4 mmap(NULL, 33558528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fbb65270000 read(4, " "..., 33554432) = 33554432 write(1, "Time used: 0.029105 s to read 33"..., 45) = 45 close(4) = 0
从 strace 的结果可以看到,案例应用调用了 openat 来打开磁盘分区 /dev/sda2,并且传入的参数为 O_RDONLY|O_DIRECT(中间的竖线表示或)。O_RDONLY 表示以只读方式打开,而 O_DIRECT 则表示以直接读取的方式打开,这会绕过系统的缓存。
验证了这一点,就很容易理解为什么读 32 MB 的数据就都要那么久了。直接从磁盘读写的速度,自然远慢于对缓存的读写。这也是缓存存在的最大意义了。
对代码做修改重新运行
[root@localhost ~]# docker rm -f app app [root@localhost ~]# docker run --privileged --name=app -itd feisky/app:io-cached Unable to find image 'feisky/app:io-cached' locally io-cached: Pulling from feisky/app 32802c0cfa4d: Already exists da1315cffa03: Already exists fa83472a3562: Already exists f85999a86bef: Already exists 2f251909225c: Retrying in 1 second a374aef23781: Downloading io-cached: Pulling from feisky/app 32802c0cfa4d: Already exists da1315cffa03: Already exists fa83472a3562: Already exists f85999a86bef: Already exists 2f251909225c: Pull complete a374aef23781: Pull complete Digest: sha256:affc2e9dd8d4cecc23b918e7b536852c747ce86291eb4daecdc8903b16c461ed Status: Downloaded newer image for feisky/app:io-cached 5843d1ee9bf07381fa81acf834d4f37dd56f77d08dab3bfd5e31d7301c6a514c [root@localhost ~]# docker logs app Reading data from disk /dev/sda2 with buffer size 33554432 Time used: 0.030117 s to read 33554432 bytes Time used: 0.027294 s to read 33554432 bytes Time used: 0.025451 s to read 33554432 bytes Time used: 0.052026 s to read 33554432 bytes Time used: 0.026425 s to read 33554432 bytes Time used: 0.019488 s to read 33554432 bytes Time used: 0.025104 s to read 33554432 bytes Time used: 0.024904 s to read 33554432 bytes Time used: 0.025110 s to read 33554432 bytes Time used: 0.025644 s to read 33554432 bytes Time used: 0.025669 s to read 33554432 bytes Time used: 0.023755 s to read 33554432 bytes Time used: 0.022087 s to read 33554432 bytes Time used: 0.023663 s to read 33554432 bytes Time used: 0.024177 s to read 33554432 bytes Time used: 0.025311 s to read 33554432 bytes Time used: 0.014088 s to read 33554432 bytes Time used: 0.021050 s to read 33554432 bytes Time used: 0.024807 s to read 33554432 bytes Time used: 0.025297 s to read 33554432 bytes Time used: 0.024300 s to read 33554432 bytes Time used: 0.024217 s to read 33554432 bytes Time used: 0.024323 s to read 33554432 bytes Time used: 0.024950 s to read 33554432 bytes
现在,每次只需要 0.03 秒,就可以读取 32MB 数据,明显比之前的 0.9 秒快多了。所以,这次应该用了系统缓存。
查看 cachetop 的输出来确认一下
08:35:49 Buffers MB: 36 / Cached MB: 1126 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 1409 root cachetop 3 0 0 100.0% 0.0% 1590 root dockerd 10 1 5 45.5% 0.0% 2164 root app 40960 0 0 100.0% 0.0%
果然,读的命中率还是 100%,HITS (即命中数)却变成了 40960,同样的方法计算一下,换算成每秒字节数正好是 32 MB(即 40960*4k/5/1024=32M)。这个说明,在进行 I/O 操作时,充分利用系统缓存可以极大地提升性能。 但在观察缓存命中率时,还要注意结合应用程序实际的 I/O 大小,综合分析缓存的使用情况。
cachestat 和 cachetop 这两个工具,观察系统和进程的缓存命中情况。
其中,cachestat 提供了整个系统缓存的读写命中情况。
cachetop 提供了每个进程的缓存命中情况。