Linux 里Buffer和Cache的定义及使用
Buffer 和 Cache 的介绍
查看内存使用情况
1 2 3 4 5 | # 注意不同版本的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内核版本工具版本问题报错
安装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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 的运行界面:
1 2 3 4 | $ 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 语言,你可以点击这里下载安装。
1 2 3 4 5 6 7 | 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 这个文件的缓存情况:
1 2 3 4 5 6 | [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 都在缓存中了:
1 2 3 4 5 6 7 8 | [root @localhost ~]# ls anaconda-ks.cfg file [root @localhost ~]# pcstat /bin/ls |----------+----------------+------------+-----------+---------| | Name | Size | Pages | Cached | Percent | |----------+----------------+------------+-----------+---------| | /bin/ls | 117608 | 29 | 29 | 100.000 | |----------+----------------+------------+-----------+---------| |
知道了缓存相应的指标和查看系统缓存的方法后
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # 生成一个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 命令:
1 2 | # 每隔 5 秒刷新一次数据 $ cachetop 5 |
运行 dd 命令测试文件的读取速度:
1 2 3 4 | [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 界面的缓存命中情况
1 2 3 4 | 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 命令
1 2 3 4 | [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 的情况
1 2 3 4 5 | 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 的缓存情况
1 2 3 4 5 6 | [root @localhost ~]# pcstat file |----------+----------------+------------+-----------+---------| | Name | Size | Pages | Cached | Percent | |----------+----------------+------------+-----------+---------| | file | 536870912 | 131072 | 131072 | 100.000 | |----------+----------------+------------+-----------+---------| |
pcstat 的结果可以发现,测试文件 file 已经被全部缓存了起来,这跟刚才观察到的缓存命中率 100% 是一致的。
这两次结果说明,系统缓存对第二次 dd 操作有明显的加速效果,可以大大提高文件读取的性能。但同时也要注意,如果把 dd 当成测试文件系统性能的工具,由于缓存的存在,就会导致测试结果严重失真。
再来看一个文件读写的案例
开启两个终端。分别 SSH 登录到机器上后,先在第一个终端中运行 cachetop 命令:
1 2 | # 每隔 5 秒刷新一次数据 $ cachetop 5 |
接着,再到第二个终端,执行下面的命令运行案例:
1 | docker run --privileged --name=app -itd feisky/app:io-direct |
查看环境是否启动完成
1 2 3 4 5 6 7 | [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,读取速度不应该这么慢。
1 2 3 4 5 | 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | [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 , "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" ..., 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 , "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" ..., 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 的数据就都要那么久了。直接从磁盘读写的速度,自然远慢于对缓存的读写。这也是缓存存在的最大意义了。
对代码做修改重新运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | [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 的输出来确认一下
1 2 3 4 5 | 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 提供了每个进程的缓存命中情况。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏