「Linux」- 缓冲(Buffer)与缓存(Cache)

使用 free 查看查看内存使用情况

# free // 以字节为单位
              total        used        free      shared  buff/cache   available
Mem:       16108680    13563160      301692      731132     2243828     1478772
Swap:      31457276    16751336    14705940

// 1)total,总内存大小;
// 2)used,已使用内存的大小,包含了共享内存;
// 3)free,未使用内存的大小;
// 4)shared,共享内存的大小;
// 5)buff/cache,缓存和缓冲区的大小;
// 6)available,新进程可用内存的大小;包含未使用内存,还包括了可回收的缓存,会比未使用内存更大。但并不是所有缓存都可以回收,因为有些缓存可能正在使用。

命令 free 从 /proc/meminfo 读取数据,根据 man proc 手册:

buffer

1)Buffers,内核缓冲区用到的内存,对应的是 /proc/meminfo 中的 Buffers 值;
Buffers,对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。

cache

2)Cache,内核页缓存和 Slab 用到的内存,对应的是 /proc/meminfo 中的 Cached 与 SReclaimable 之和;
Cached,从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。
SReclaimable,Slab 的一部分。Slab 包括两部分,其中的可回收部分,用 SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录。

概念理解:Buffer and Cache

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

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

读写文件时,会经过文件系统,由文件系统负责与磁盘交互;在读写磁盘时,就会跳过文件系统,也就是所谓的“裸I/O“。这两种读写方式所使用的缓存是不同的,也就是 Cache 和 Buffer 区别。

实验探究:文件写入 与 磁盘写入

写文件时会用到 Cache 缓存数据,而写磁盘则会用到 Buffer 来缓存数据。虽然文档上只提到,Cache 是文件读的缓存,但实际上,Cache 也会缓存写文件时的数据。

文件写入:

# echo 3 > /proc/sys/vm/drop_caches
# vmstat 1 // 每隔1秒输出1组数据
# dd if=/dev/urandom of=/tmp/file bs=1M count=500
# vmstat 1 // 内存部分的 buff 和 cache ,以及 io 部分的 bi 和 bo 要重点关注

// 在命令运行时,Buffer 基本保持不变,Cache 在不停地增长,块设备 I/O 很少,bi 只出现了一次 488 KB/s,bo 则只有一次 4KB
// 而过一段时间后,才会出现大量的块设备写,比如 bo 变成 122880
// 当命令结束后,Cache 不再增长,但块设备写还会持续一段时间,并且,多次 I/O 写的结果加起来,才是 dd 要写的 500M 的数据

// 这与文档描述冲突,文档说:Cache 是磁盘读取文件时的缓存,为什么写入操作会涉及 Cache?

磁盘写入:

# echo 3 > /proc/sys/vm/drop_caches
# vmstat 1 // 每隔1秒输出1组数据
# dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048
# vmstat 1

// 写磁盘时(也就是 bo 大于 0 时),Buffer 和 Cache 都在增长,但显然 Buffer 的增长快得多

// 写磁盘用到了大量的 Buffer,这跟我们在文档中查到的定义是一样的

实验探究:文件读取 与 磁盘读取

读文件时数据会缓存到 Cache 中,而读磁盘时数据会缓存到 Buffer 中。

文件读取:

# echo 3 > /proc/sys/vm/drop_caches
# vmstat 1 // 每隔1秒输出1组数据
# dd if=/tmp/file of=/dev/null
# vmstat 1

// 读取文件时(也就是 bi 大于 0 时),Buffer 保持不变,而 Cache 则在不停增长

// 这跟我们查到的定义“Cache 是对文件读的页缓存”是一致的

磁盘读取:

# echo 3 > /proc/sys/vm/drop_caches
# vmstat 1 // 每隔1秒输出1组数据
# dd if=/dev/sda1 of=/dev/null bs=1M count=1024
# vmstat 1

// 读磁盘时(也就是 bi 大于 0 时),Buffer 和 Cache 都在增长,但显然 Buffer 的增长快很多。这说明读磁盘时,数据缓存到了 Buffer 中

// 这跟我们查到的定义“Cache 是对文件读的页缓存”是一致的

缓存命中率 – 衡量缓存使用的好坏

缓存命中率,是指直接通过缓存获取数据的请求次数所有数据请求次数的百分比。命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。

常用缓存命中率查看工具

使用如下工具,以查看系统缓存命中情况的工具,属于 bcc 软件包,基于 Linux 内核的 eBPF(extended Berkeley Packet Filters)机制,Kernel >= 4.1:
1)cachestat,整个操作系统缓存的读写命中情况
2)cachetop,每个进程的缓存命中情况

cachestat

# cachestat-bpfcc 1 3 // 以 1 秒的时间间隔,输出了 3 组缓存统计数据
   TOTAL   MISSES     HITS  DIRTIES   BUFFERS_MB  CACHED_MB
   29408       29    29379      102           21       1032
   57596    12265    45331       68           20       1035
  117526     7088   110438      316           21       1007

// TOTAL ,表示总的 I/O 次数; = MISSES + HITS
// MISSES ,表示缓存未命中的次数;
// HITS ,表示缓存命中的次数;
// DIRTIES, 表示新增到缓存中的脏页数;
// BUFFERS_MB 表示 Buffers 的大小,以 MB 为单位;
// CACHED_MB 表示 Cache 的大小,以 MB 为单位;

cachetop

08:56:03 Buffers MB: 54 / Cached MB: 1400 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
   22894 k4nz     thunderbird-bin         0       72        0       0.0%     100.0%
   23595 k4nz     Socket Thread           0      128        0       0.0%     100.0%
   12196 k4nz     chrome                  1        0        0     100.0%       0.0%
   14483 k4nz     Chrome_ChildIOT         1        0        0     100.0%       0.0%
    1841 libvirt- CPU 1/KVM               1        0        0     100.0%       0.0%
   25951 k4nz     DNS Res~r #2078         1        0        0     100.0%       0.0%

// 输出跟 top 类似,默认按照缓存的命中次数(HITS)排序,示了每个进程的缓存命中情况
// 具体到每一个指标,跟 cachestat 里的含义一样
// READ_HIT% 和 WRITE_HIT% ,分别表示读和写的缓存命中率

pcstat – 特定文件的以缓存大小

# pcstat /bin/tail
+-----------+----------------+------------+-----------+---------+
| Name      | Size (bytes)   | Pages      | Cached    | Percent |
|-----------+----------------+------------+-----------+---------|
| /bin/tail | 72608          | 18         | 0         | 000.000 |
+-----------+----------------+------------+-----------+---------+

# tail -n 1 /var/log/syslog
Aug  4 18:56:13 localhost log.example.com.sh[23480] 10.10.12.122

# pcstat /bin/tail
+-----------+----------------+------------+-----------+---------+
| Name      | Size (bytes)   | Pages      | Cached    | Percent |
|-----------+----------------+------------+-----------+---------|
| /bin/tail | 72608          | 18         | 18        | 100.000 |
+-----------+----------------+------------+-----------+---------+

实验探究:系统缓存对文件读取性能的影响

# dd if=/dev/sda1 of=file bs=1M count=512
# echo 3 > /proc/sys/vm/drop_caches

# pcstat ./file
+--------+----------------+------------+-----------+---------+
| Name   | Size (bytes)   | Pages      | Cached    | Percent |
|--------+----------------+------------+-----------+---------|
| ./file | 536870912      | 131072     | 0         | 000.000 |
+--------+----------------+------------+-----------+---------+

# dd if=file of=/dev/null bs=1M
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 4.85321 s, 111 MB/s
root@laptop /mnt/k4nz

// 当进行上述操作之后,./file 会被缓存,但是缓存的程度可能不同。
// 虽然我们已经清理了缓存,但是操作系统会预读,导致上述 dd 命令会使用缓存。这点可以通过 cachetop 观察到命中率
// 最然会绕过系统缓存,但是还存在元数据缓存,所以即使绕过缓存也会存在缓存命中率

# pcstat ./file
+--------+----------------+------------+-----------+---------+
| Name   | Size (bytes)   | Pages      | Cached    | Percent |
|--------+----------------+------------+-----------+---------|
| ./file | 536870912      | 131072     | 131072    | 100.000 |
+--------+----------------+------------+-----------+---------+

# dd if=file of=/dev/null bs=1M
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 0.171562 s, 3.1 GB/s

// 系统缓存对第二次 dd 操作有明显的加速效果,可以大大提高文件读取的性能。
// 但同时也要注意,如果我们把 dd 当成测试文件系统性能的工具,由于缓存的存在,就会导致测试结果严重失真

实验探究:直接 I/O 对文件读取性能的影响

# cachetop 5
# docker run --privileged --name=app-io-direct -itd feisky/app:io-direct // 每秒从磁盘分区 /dev/sda1 中读取 32MB 的数据
# docker logs app // 确认案例已经正常启动

// 每读取 32 MB 的数据,就需要花 0.9 秒,这个速度并不高

# cachetop 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 * 4K / 1024K/M / 5s = 0.8M/s,也就是说秒读取 0.8M 缓存
// 可以推测系统缓存没有充分利用,类似于我们之前 DIRECT_IO 例子

// 查看系统调用以验证猜想

# strace -p $(pgrep app)

// 从 strace 的结果可以看到,案例应用调用 openat 来打开磁盘分区,并且传入的参数为 O_RDONLY|O_DIRECT
// 而 O_DIRECT 这会绕过系统的缓存

// 另外,cachetop 工具并不把直接 I/O 算进来,所以通过 cachetop 只能看到很少一部分数据的全部命中
// 毕竟 iotop 计算缓存命中,而不是所有读取次数

参考文献

16 | 基础篇:怎么理解内存中的Buffer和Cache?