14 | 内存中的Buffer和Cache

273 阅读5分钟

1. 概述

执行free命令查看

# 注意不同版本的free输出可能会有所不同
$ root@calvin:~# free
              total        used        free      shared  buff/cache   available
Mem:        8070124     6938996      366304         676      764824      881284
Swap:       4194300           0     4194300

其中对于buff/cache的解释

       buffers
              Memory used by kernel buffers (Buffers in /proc/meminfo)

       cache  Memory used by the page cache and slabs (Cached and SReclaimable in /proc/meminfo)

       buff/cache
              Sum of buffers and cache

查看man命令可以看到

  • Buffers 是内核缓冲区用到的内存,对应的是 /proc/meminfo 中的 Buffers 值。
  • Cache 是内核页缓存和 Slab 用到的内存,对应的是 /proc/meminfo 中的 Cached 与 SReclaimable 之和。

free命令的统计数据是来自于/proc/meminfo ,所以要弄清楚buffer和cache可以继续查看/proc

              Buffers %lu
                     Relatively temporary storage for raw disk blocks that shouldn't get tremendously large (20MB or so).

              Cached %lu
                     In-memory cache for files read from the disk (the page cache).  Doesn't include SwapCached.

              SwapCached %lu
                     Memory that once was swapped out, is swapped back in but still also is in the swap file.  (If memory pressure is high, these pages don't need to be swapped out again because they are already in the swap file.  This saves
                     I/O.)

中文解释如下:

  • Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。
  • Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。这样,下次访问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘。
  • SReclaimable 是 Slab 的一部分。Slab 包括两部分,其中的可回收部分,用 SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录。

针对上面的描述,有几个问题:

  • 第一个问题:Buffer 通常被认为是用于缓存将要写入磁盘的数据,但它也可能缓存从磁盘读取的数据,尤其在涉及 I/O 操作时。它不仅仅限于写操作,也会在读取过程中起到临时存储的作用。
  • 第二个问题:Cache 通常用于缓存从文件读取的数据,以提高读取速度。它通常不会缓存写入文件的数据,因为写入操作的目标是将数据持久化到磁盘,而缓存主要关注提升读取性能。不过,某些情况下,也有可能对即将写入的数据进行缓存,特别是在写操作与读取操作有相关性时。

为这里两个问题,下面做实验

2. 案例

环境准备:

  • 操作系统:Ubuntu 18.04。
  • 机器配置:2 CPU,8GB 内存。
  • 预先安装 sysstat 包,如 apt install sysstat。

测试前清空缓存

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

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

终端 1:执行vmstat,观察buff、cache字段

root@calvin:~# 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      0 999960   2464 135840    0    0     1     2    5   29  0  0 100  0  0
 0  0      0 999960   2464 135852    0    0     0     0    7   53  0  0 100  0  0
 0  0      0 999960   2464 135852    0    0     0     0   10   56  0  0 100  0  0
 0  0      0 999960   2464 135852    0    0     0     0    8   64  0  0 100  0  0
 0  0      0 999960   2464 135852    0    0     0     0   11   50  0  0 100  0  0
 0  0      0 999960   2464 135852    0    0     0     0   10   55  0  0 100  0  0
 0  0      0 999960   2472 135852    0    0     0    12   16   70  0  0 100  0  0
 0  0      0 999960   2472 135852    0    0     0     0   13   76  0  0 100  0  0
 0  0      0 999960   2472 135852    0    0     0     0   15   46  0  0 100  0  0
 0  0      0 999960   2472 135852    0    0     0     0   10   54  0  0 100  0  0
  • buff 和 cache 就是我们前面看到的 Buffers 和 Cache,单位是 KB。
  • bi 和 bo 则分别表示块设备读取和写入的大小,单位为块 / 秒。因为 Linux 中块的大小是 1KB,所以这个单位也就等价于 KB/s。

终端 2:

注意事项:

  • 命令要求系统有多块磁盘,且 /dev/sdb 磁盘分区未被使用。
  • 如果只有一块磁盘,不要尝试,以免损坏分区。
  • 满足条件后,可以在第二个终端执行命令,清理缓存并向 /dev/sdb1 写入 2GB 随机数据。
$ dd if=/dev/urandom of=/tmp/file bs=1M count=500

执行结果

通过观察 vmstat 输出,在 dd 命令运行时,我们发现 Cache 持续增长,而 Buffer 基本保持不变。初期,块设备 I/O 很少,bi(读取)只有一次 228 KB/s,bo(写入)高达1万+。当 dd 命令结束后,Cache 停止增长,但块设备写入继续一段时间,最终多次写入累计完成 500M 数据的写操作。

继续先做下一个实验

终端 2

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

执行结果

  • 写文件时,主要使用 Cache 来缓存数据。
  • 写磁盘时,主要使用 Buffer 来缓存数据,且 Buffer 增长较快。
  • 虽然文档中提到 Cache 是用于从文件读取的数据缓存,但在实际情况中,Cache 也会缓存写文件的数据。

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

终端 2

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

执行结果

  • 读取文件时(bi 大于 0),Buffer 保持不变,Cache 持续增长。
  • 这与定义中“Cache 是文件读的页缓存”一致。

磁盘读验证

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

执行结果

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

通过案例可以做个简单的总结:

  • Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中。

2.3. 场景3:文件读

buffer/cache对于系统的性能影响较大,那么想通过buffer/cache手段提升系统性能,如何判定?那就是缓存命中率。命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。

这里介绍两个查看缓存命中率的工具:

  • cachestat 提供了整个操作系统缓存的读写命中情况。
  • cachetop 提供了每个进程的缓存命中情况。

这两个工具都是 bcc 软件包的一部分,它们基于 Linux 内核的 eBPF(extended Berkeley Packet Filters)机制,来跟踪内核中管理的缓存,并输出缓存的使用和命中情况。

安装bcc

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 提供的所有工具就都安装到 /usr/share/bcc/tools 这个目录中了。不过还需要配置一下环境变量

$ export PATH=$PATH:/usr/share/bcc/tools

试运行代码 cachestat

$ 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

$ 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)排序,展示了每个进程的缓存命中情况。具体到每一个指标,这里的 HITS、MISSES 和 DIRTIES ,跟 cachestat 里的含义一样,分别代表间隔时间内的缓存命中次数、未命中次数以及新增到缓存中的脏页数。READ_HIT 和 WRITE_HIT ,分别表示读和写的缓存命中率。

除了缓存命中率以外, 可以使用 pcstat 工具查看指定文件在内存中的缓存大小及缓存比例。

pcstat安装

# go安装
sudo apt update && sudo apt upgrade -y
wget https://go.dev/dl/go1.23.4.linux-amd64.tar.gz #不行就用浏览器下载。
tar -C /usr/local -xzf go1.23.4.linux-amd64.tar.gz
vim ~/.bashrc

export GOROOT=/usr/local/go
export GOPATH=$HOME/go
export PATH=$PATH:/usr/local/go/bin

source ~/.bashrc
go version
go version go1.23.4 linux/amd64 #表示安装成功



$ export GOPATH=~/go
$ export PATH=~/go/bin:$PATH
$ go mod init mymodule
$ go get golang.org/x/sys/unix
$ go get github.com/tobert/pcstat/pcstat

安装出了一些小插曲,上面步骤未必准确。具体步骤忘了,回头重新整理一下。

使用pcstat

$ pcstat /bin/ls
+---------+----------------+------------+-----------+---------+
| Name    | Size (bytes)   | Pages      | Cached    | Percent |
|---------+----------------+------------+-----------+---------|
| /bin/ls | 133792         | 33         | 0         | 000.000 |
+---------+----------------+------------+-----------+---------+

字段含义

列名含义
Name进程或文件的名称,在此为 /bin/ls,表示你查询的文件(或正在运行的进程)。
Size (bytes)文件的大小(字节数)。这里显示的是 /bin/ls文件的大小,即 133792字节(大约 133 KB)。
Pages文件占用的内存页面数。每个页面通常为 4KB,因此 33页大约是 33 * 4KB = 132KB,这与文件大小非常接近。
Cached文件或进程的缓存内存(页)。这个值表示文件在内存中的缓存页面数,0表示该文件目前没有被缓存。
Percent该文件/进程占用的总内存百分比。这里的 000.000表示 /bin/ls占用的内存极少,可能是系统内存的一个微不足道部分。

终端 1:

# 生成一个512MB的临时文件
$ dd if=/dev/sda1 of=file bs=1M count=512
# 清理缓存
$ echo 3 > /proc/sys/vm/drop_caches

继续执行pcstat命令

root@calvin:~# pcstat file
+-------+----------------+------------+-----------+---------+
| Name  | Size (bytes)   | Pages      | Cached    | Percent |
|-------+----------------+------------+-----------+---------|
| file  | 1048576        | 256        | 0         |   0.000 |

继续在终端 1

# 每隔5秒刷新一次数据
$ cachetop 5

终端 2:

$ dd if=file of=/dev/null bs=1M
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 16.0509 s, 33.4 MB/s

cachetop命令输出,命中率只有65%。

可以明显感觉到第一次与第二次的执行速度。

观察cachetop发现,READ_HIT命中率这次是100%。

2.4. 场景3:文件写

为方便运行案例,借用docker程序:

  • -d 选项,设置要读取的磁盘或分区路径,默认是查找前缀为 /dev/sd 或者 /dev/xvd 的磁盘。
  • -s 选项,设置每次读取的数据量大小,单位为字节,默认为 33554432(也就是 32MB)。

终端 1

# 每隔5秒刷新一次数据
$ cachetop 5 

终端2

$ docker run --privileged --name=app -itd feisky/app:io-direct

终端2,测试;输出如下内容说明测试程序正常。

$ 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

回到终端 1

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% 

这个输出显示系统缓存的命中率为 100% ,即所有的读请求都来自缓存。然而,实际读取速度却较慢,与预期不符。关键原因在于 每次缓存命中读取的数据量较小。由于每次命中读取的数据量为一页(4KB),5 秒内共命中 1024 次,因此总读取量为:

换算到每秒的读取速度为:

从经验上来讲,大概率使用了DIO导致无法使用缓存,如何判断是否使用了O_DIRECT可以使用strace命令跟踪

# strace -p $(pgrep app)
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

找到问题原因。

3. 小结

  • cachestat 提供了整个系统缓存的读写命中情况。
  • cachetop 提供了每个进程的缓存命中情况。
  • pcstat 用于分析文件或进程的页面缓存使用情况。

附录

go install github.com/tobert/pcstat@latest

参考文献:

blog.csdn.net/weixin_4379…