17讲案例篇:如何利⽤系统缓存优化程序的运⾏效率

上⼀节,我们学习了内存性能中 Buffer 和 Cache 的概念。简单复习⼀下,Buffer 和 Cache 的设计⽬的,是为了提升系统的
I/O 性能。它们利⽤内存,充当起慢速磁盘与快速 CPU 之间的桥梁,可以加速 I/O 的访问速度。
Buffer和Cache分别缓存的是对磁盘和⽂件系统的读写数据。
 
从写的⻆度来说,不仅可以优化磁盘和⽂件的写⼊,对应⽤程序也有好处,应⽤程序可以在数据真正落盘前,就返回去做
其他⼯作。
 
从读的⻆度来说,不仅可以提⾼那些频繁访问数据的读取速度,也降低了频繁 I/O 对磁盘的压⼒。
 
既然 Buffer 和 Cache 对系统性能有很⼤影响,那我们在软件开发的过程中,能不能利⽤这⼀点,来优化 I/O 性能,提升应⽤
程序的运⾏效率呢?
 
答案⾃然是肯定的。今天,我就⽤⼏个案例帮助你更好地理解缓存的作⽤,并学习如何充分利⽤这些缓存来提⾼程序效率。
为了⽅便你理解,Buffer和Cache我仍然⽤英⽂表示,避免跟“缓存”⼀词混淆。⽽⽂中的“缓存”,通指数据在内存中的临时存
储。
 
缓存命中率
在案例开始前,你应该习惯性地先问⾃⼰⼀个问题,你想要做成某件事情,结果应该怎么评估?⽐如说,我们想利⽤缓存来提
升程序的运⾏效率,应该怎么评估这个效果呢?换句话说,有没有哪个指标可以衡量缓存使⽤的好坏呢?
 
我估计你已经想到了,缓存的命中率。所谓缓存命中率,是指直接通过缓存获取数据的请求次数,占所有数据请求次数的百分
⽐。
 
命中率越⾼,表示使⽤缓存带来的收益越⾼,应⽤程序的性能也就越好。
 
实际上,缓存是现在所有⾼并发系统必需的核⼼模块,主要作⽤就是把经常访问的数据(也就是热点数据),提前读⼊到内存
中。这样,下次访问时就可以直接从内存读取数据,⽽不需要经过硬盘,从⽽加快应⽤程序的响应速度。
 
这些独⽴的缓存模块通常会提供查询接⼝,⽅便我们随时查看缓存的命中情况。不过 Linux 系统中并没有直接提供这些接⼝,
所以这⾥我要介绍⼀下,cachestat 和 cachetop ,它们正是查看系统缓存命中情况的⼯具。
 
cachestat 提供了整个操作系统缓存的读写命中情况。
cachetop 提供了每个进程的缓存命中情况。
 
这两个⼯具都是 bcc 软件包的⼀部分,它们基于 Linux 内核的 eBPF(extended Berkeley Packet Filters)机制,来跟踪内核
中管理的缓存,并输出缓存的使⽤和命中情况。
 
这⾥注意,eBPF 的⼯作原理不是我们今天的重点,记住这个名字即可,后⾯⽂章中我们会详细学习。今天要掌握的重点,是
这两个⼯具的使⽤⽅法。
使⽤ cachestat 和 cachetop 前,我们⾸先要安装 bcc 软件包。⽐如,在 Ubuntu 系统中,你可以运⾏下⾯的命令来安装:
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)


centos安装:
下载BCC的依赖: yum install -y elfutils-libelf-devel flex
安装bcc bcc-tools:
yum --enablerepo=elrepo-kernel install bcc bcc-tools
添加环境变量:
export PATH=$PATH:/usr/share/bcc/tools
查看:

[root@test ~]# ll /usr/share/bcc/tools/
total 844
-rwxr-xr-x 1 root root 34534 Jul 31 2019 argdist
-rwxr-xr-x 1 root root 2179 Jul 31 2019 bashreadline
-rwxr-xr-x 1 root root 6229 Jul 31 2019 biolatency
-rwxr-xr-x 1 root root 5522 Jul 31 2019 biosnoop
-rwxr-xr-x 1 root root 6391 Jul 31 2019 biotop
-rwxr-xr-x 1 root root 1150 Jul 31 2019 bitesize
-rwxr-xr-x 1 root root 2451 Jul 31 2019 bpflist
-rwxr-xr-x 1 root root 6330 Apr 1 2020 btrfsdist
-rwxr-xr-x 1 root root 9581 Apr 1 2020 btrfsslower
-rwxr-xr-x 1 root root 4715 Jul 31 2019 cachestat
-rwxr-xr-x 1 root root 7300 Jul 31 2019 cachetop
-rwxr-xr-x 1 root root 6291 Jul 31 2019 capable
-rwxr-xr-x 1 root root 57 Apr 1 2020 cobjnew
-rwxr-xr-x 1 root root 5125 Apr 1 2020 cpudist
-rwxr-xr-x 1 root root 14595 Jul 31 2019 cpuunclaimed
-rwxr-xr-x 1 root root 7093 Jul 31 2019 dbslower
-rwxr-xr-x 1 root root 3778 Jul 31 2019 dbstat
-rwxr-xr-x 1 root root 3936 Jul 31 2019 dcsnoop
-rwxr-xr-x 1 root root 3918 Jul 31 2019 dcstat
-rwxr-xr-x 1 root root 19928 Jul 31 2019 deadlock
-rw-r--r-- 1 root root 7087 Jul 31 2019 deadlock.c
drwxr-xr-x 3 root root 8192 May 17 10:29 doc
-rwxr-xr-x 1 root root 6828 Jul 31 2019 drsnoop
-rwxr-xr-x 1 root root 7252 Jul 31 2019 execsnoop
-rwxr-xr-x 1 root root 6490 Apr 1 2020 ext4dist
-rwxr-xr-x 1 root root 9916 Apr 1 2020 ext4slower
-rwxr-xr-x 1 root root 3616 Jul 31 2019 filelife
-rwxr-xr-x 1 root root 7319 Apr 1 2020 fileslower
-rwxr-xr-x 1 root root 6029 Jul 31 2019 filetop
-rwxr-xr-x 1 root root 12457 Jul 31 2019 funccount
-rwxr-xr-x 1 root root 7973 Jul 31 2019 funclatency
-rwxr-xr-x 1 root root 10124 Jul 31 2019 funcslower
-rwxr-xr-x 1 root root 3802 Jul 31 2019 gethostlatency
-rwxr-xr-x 1 root root 5195 Jul 31 2019 hardirqs
-rwxr-xr-x 1 root root 59 Apr 1 2020 javacalls
-rwxr-xr-x 1 root root 58 Apr 1 2020 javaflow
-rwxr-xr-x 1 root root 56 Apr 1 2020 javagc
-rwxr-xr-x 1 root root 60 Apr 1 2020 javaobjnew
-rwxr-xr-x 1 root root 58 Apr 1 2020 javastat
-rwxr-xr-x 1 root root 61 Apr 1 2020 javathreads
-rwxr-xr-x 1 root root 3406 Jul 31 2019 killsnoop
drwxr-xr-x 2 root root 88 May 17 10:29 lib
-rwxr-xr-x 1 root root 3689 Jul 31 2019 llcstat
-rwxr-xr-x 1 root root 2061 Jul 31 2019 mdflush
-rwxr-xr-x 1 root root 19032 Apr 1 2020 memleak
-rwxr-xr-x 1 root root 12645 Apr 1 2020 mountsnoop
-rwxr-xr-x 1 root root 3054 Jul 31 2019 mysqld_qslower
-rwxr-xr-x 1 root root 4726 Jul 31 2019 nfsdist
-rwxr-xr-x 1 root root 9032 Apr 1 2020 nfsslower
-rwxr-xr-x 1 root root 56 Apr 1 2020 nodegc
-rwxr-xr-x 1 root root 58 Apr 1 2020 nodestat
-rwxr-xr-x 1 root root 11775 Apr 1 2020 offcputime
-rwxr-xr-x 1 root root 14371 Apr 1 2020 offwaketime
-rwxr-xr-x 1 root root 2107 Apr 1 2020 oomkill
-rwxr-xr-x 1 root root 7219 Jul 31 2019 opensnoop
-rwxr-xr-x 1 root root 59 Apr 1 2020 perlcalls
-rwxr-xr-x 1 root root 58 Apr 1 2020 perlflow
-rwxr-xr-x 1 root root 58 Apr 1 2020 perlstat
-rwxr-xr-x 1 root root 58 Apr 1 2020 phpcalls
-rwxr-xr-x 1 root root 57 Apr 1 2020 phpflow
-rwxr-xr-x 1 root root 57 Apr 1 2020 phpstat
-rwxr-xr-x 1 root root 1137 Jul 31 2019 pidpersec
-rwxr-xr-x 1 root root 12752 Jul 31 2019 profile
-rwxr-xr-x 1 root root 61 Apr 1 2020 pythoncalls
-rwxr-xr-x 1 root root 60 Apr 1 2020 pythonflow
-rwxr-xr-x 1 root root 58 Apr 1 2020 pythongc
-rwxr-xr-x 1 root root 60 Apr 1 2020 pythonstat
-rwxr-xr-x 1 root root 3496 Jul 31 2019 reset-trace
-rwxr-xr-x 1 root root 59 Apr 1 2020 rubycalls
-rwxr-xr-x 1 root root 58 Apr 1 2020 rubyflow
-rwxr-xr-x 1 root root 56 Apr 1 2020 rubygc
-rwxr-xr-x 1 root root 60 Apr 1 2020 rubyobjnew
-rwxr-xr-x 1 root root 58 Apr 1 2020 rubystat
-rwxr-xr-x 1 root root 8051 Apr 1 2020 runqlat
-rwxr-xr-x 1 root root 7799 Jul 31 2019 runqlen
-rwxr-xr-x 1 root root 7072 Apr 1 2020 runqslower
-rwxr-xr-x 1 root root 7983 Jul 31 2019 shmsnoop
-rwxr-xr-x 1 root root 3635 Jul 31 2019 slabratetop
-rwxr-xr-x 1 root root 8246 Jul 31 2019 sofdsnoop
-rwxr-xr-x 1 root root 4116 Jul 31 2019 softirqs
-rwxr-xr-x 1 root root 6074 Apr 1 2020 solisten
-rwxr-xr-x 1 root root 7120 Jul 31 2019 sslsniff
-rwxr-xr-x 1 root root 15924 Jul 31 2019 stackcount
-rwxr-xr-x 1 root root 4621 Jul 31 2019 statsnoop
-rwxr-xr-x 1 root root 1264 Jul 31 2019 syncsnoop
-rwxr-xr-x 1 root root 6193 Jul 31 2019 syscount
-rwxr-xr-x 1 root root 58 Apr 1 2020 tclcalls
-rwxr-xr-x 1 root root 57 Apr 1 2020 tclflow
-rwxr-xr-x 1 root root 59 Apr 1 2020 tclobjnew
-rwxr-xr-x 1 root root 57 Apr 1 2020 tclstat
-rwxr-xr-x 1 root root 7868 Jul 31 2019 tcpaccept
-rwxr-xr-x 1 root root 7382 Jul 31 2019 tcpconnect
-rwxr-xr-x 1 root root 7361 Jul 31 2019 tcpconnlat
-rwxr-xr-x 1 root root 5839 Jul 31 2019 tcpdrop
-rwxr-xr-x 1 root root 16283 Jul 31 2019 tcplife
-rwxr-xr-x 1 root root 8770 Jul 31 2019 tcpretrans
-rwxr-xr-x 1 root root 7825 Apr 1 2020 tcpsubnet
-rwxr-xr-x 1 root root 9358 Jul 31 2019 tcptop
-rwxr-xr-x 1 root root 16438 Apr 1 2020 tcptracer
-rwxr-xr-x 1 root root 4157 Jul 31 2019 tplist
-rwxr-xr-x 1 root root 37908 Jul 31 2019 trace
-rwxr-xr-x 1 root root 3014 Jul 31 2019 ttysnoop
-rwxr-xr-x 1 root root 1381 Apr 1 2020 vfscount
-rwxr-xr-x 1 root root 2634 Jul 31 2019 vfsstat
-rwxr-xr-x 1 root root 6897 Jul 31 2019 wakeuptime
-rwxr-xr-x 1 root root 4550 Apr 1 2020 xfsdist
-rwxr-xr-x 1 root root 7882 Apr 1 2020 xfsslower

注意:bcc-tools需要内核版本为4.1或者更新的版本,如果你⽤的是CentOS,那就需要⼿动升级内核版本后再安装。
操作完这些步骤,bcc 提供的所有⼯具就都安装到 /usr/share/bcc/tools 这个⽬录中了。不过这⾥提醒你,bcc 软件包默认不会
把这些⼯具配置到系统的 PATH 路径中,所以你得⾃⼰⼿动配置:
$ export PATH=$PATH:/usr/share/bcc/tools
配置完,你就可以运⾏ cachestat 和 cachetop 命令了。⽐如,下⾯就是⼀个 cachestat 的运⾏界⾯,它以1秒的时间间隔,输
出了3组缓存统计数据:
$ 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
你可以看到,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 语⾔,你可以点击这⾥下载安装。
安装完 Go 语⾔,再运⾏下⾯的命令安装 pcstat:
$ export GOPATH=~/go 
$ export PATH=~/go/bin:$PATH 
$ go get golang.org/x/sys/unix 
$ go get github.com/tobert/pcstat/pcstat

备注:
  pcstat安装参考:https://www.cnblogs.com/Courage129/p/14282282.html
全部安装完成后,你就可以运⾏ pcstat 来查看⽂件的缓存情况了。⽐如,下⾯就是⼀个 pcstat 运⾏的示例,它展示了 /bin/ls
这个⽂件的缓存情况:
$ pcstat /bin/ls 
+---------+----------------+------------+-----------+---------+ 
| Name    | Size (bytes) | Pages | Cached | Percent | 
|---------+----------------+------------+-----------+---------| 
| /bin/ls | 133792       | 33    | 0      | 000.000 | 
+---------+----------------+------------+-----------+---------+
这个输出中,Cached 就是 /bin/ls 在缓存中的⼤⼩,⽽ Percent 则是缓存的百分⽐。你看到它们都是 0,这说明 /bin/ls 并不在
缓存中。
接着,如果你执⾏⼀下 ls 命令,再运⾏相同的命令来查看的话,就会发现 /bin/ls 都在缓存中了:
$ ls 
$ pcstat /bin/ls 
+---------+----------------+------------+-----------+---------+ 
| Name    | Size (bytes) | Pages | Cached | Percent | 
|---------+----------------+------------+-----------+---------| 
| /bin/ls | 133792      | 33     | 33    | 100.000 | 
+---------+----------------+------------+-----------+---------+
知道了缓存相应的指标和查看系统缓存的⽅法后,接下来,我们就进⼊今天的正式案例。
跟前⾯的案例⼀样,今天的案例也是基于 Ubuntu 18.04,当然同样适⽤于其他的 Linux 系统。
机器配置:2 CPU,8GB 内存。
预先按照上⾯的步骤安装 bcc 和 pcstat 软件包,并把这些⼯具的安装路径添加到到 PATH 环境变量中。
预先安装 Docker 软件包,⽐如 apt-get install docker.io
 
案例⼀
第⼀个案例,我们先来看⼀下上⼀节提到的 dd 命令。
dd 作为⼀个磁盘和⽂件的拷⻉⼯具,经常被拿来测试磁盘或者⽂件系统的读写性能。不过,既然缓存会影响到性能,如果⽤
dd对同⼀个⽂件进⾏多次读取测试,测试的结果会怎么样呢?
我们来动⼿试试。⾸先,打开两个终端,连接到 Ubuntu 机器上,确保 bcc 已经安装配置成功。
然后,使⽤ dd 命令⽣成⼀个临时⽂件,⽤于后⾯的⽂件读取测试:
# ⽣成⼀个512MB的临时⽂件 
$ dd if=/dev/sda1 of=file bs=1M count=512 
# 清理缓存 
$ echo 3 > /proc/sys/vm/drop_caches

备注:
   dd命令也⽀持直接IO的 有选项oflflag和iflflag 所以dd也可以⽤来绕过cache buff做测试
继续在第⼀个终端,运⾏ pcstat 命令,确认刚刚⽣成的⽂件不在缓存中。如果⼀切正常,你会看到 Cached 和 Percent 都是
0:
$ pcstat file 
+-------+----------------+------------+-----------+---------+ 
| Name | Size (bytes) | Pages  | Cached | Percent | 
|-------+----------------+------------+-----------+---------| 
| file | 536870912    | 131072 | 0      | 000.000 | 
+-------+----------------+------------+-----------+---------+
还是在第⼀个终端中,现在运⾏ cachetop 命令:
# 每隔5秒刷新⼀次数据 
$ cachetop 5
这次是第⼆个终端,运⾏ dd 命令测试⽂件的读取速度:
$ 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
从 dd 的结果可以看出,这个⽂件的读性能是 33.4 MB/s。由于在 dd 命令运⾏前我们已经清理了缓存,所以 dd 命令读取数据
时,肯定要通过⽂件系统从磁盘中读取。
不过,这是不是意味着, dd 所有的读请求都能直接发送到磁盘呢?
我们再回到第⼀个终端, 查看 cachetop 界⾯的缓存命中情况:
PID   UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 
\.\.\.
3264 root dd 37077 37330 0 49.8% 50.2%
从 cachetop 的结果可以发现,并不是所有的读都落到了磁盘上,事实上读请求的缓存命中率只有 50% 。
接下来,我们继续尝试相同的测试命令。先切换到第⼆个终端,再次执⾏刚才的 dd 命令:
$ dd if=file of=/dev/null bs=1M 
512+0 records in 
512+0 records out 
536870912 bytes (537 MB, 512 MiB) copied, 0.118415 s, 4.5 GB/s
看到这次的结果,有没有点⼩惊讶?磁盘的读性能居然变成了 4.5 GB/s,⽐第⼀次的结果明显⾼了太多。为什么这次的结果
这么好呢?
不妨再回到第⼀个终端,看看 cachetop 的情况:
10:45:22 Buffers MB: 4 / Cached MB: 719 / Sort: HITS / Order: ascending 
PID   UID CMD HITS   MISSES DIRTIES READ_HIT% WRITE_HIT%
 \.\.\. 
32642 root dd 131637   0     0       100.0%    0.0%
显然,cachetop也有了不⼩的变化。你可以发现,这次的读的缓存命中率是100.0%,也就是说这次的 dd 命令全部命中了缓
存,所以才会看到那么⾼的性能。
然后,回到第⼆个终端,再次执⾏ pcstat 查看⽂件 fifile 的缓存情况:
$ pcstat file 
+-------+----------------+------------+-----------+---------+ 
| Name | Size (bytes) | Pages  | Cached | Percent | 
|-------+----------------+------------+-----------+---------| 
| file | 536870912    | 131072 | 131072 | 100.000 | 
+-------+----------------+------------+-----------+---------+
从 pcstat 的结果你可以发现,测试⽂件 fifile 已经被全部缓存了起来,这跟刚才观察到的缓存命中率 100% 是⼀致的。
这两次结果说明,系统缓存对第⼆次 dd 操作有明显的加速效果,可以⼤⼤提⾼⽂件读取的性能。
但同时也要注意,如果我们把 dd 当成测试⽂件系统性能的⼯具,由于缓存的存在,就会导致测试结果严重失真。
 
案例⼆
接下来,我们再来看⼀个⽂件读写的案例。这个案例类似于前⾯学过的不可中断状态进程的例⼦。它的基本功能⽐较简单,也
就是每秒从磁盘分区 /dev/sda1 中读取 32MB 的数据,并打印出读取数据花费的时间。
 
为了⽅便你运⾏案例,我把它打包成了⼀个 Docker 镜像。 跟前⾯案例类似,我提供了下⾯两个选项,你可以根据系统配置,
⾃⾏调整磁盘分区的路径以及 I/O 的⼤⼩。
-d 选项,设置要读取的磁盘或分区路径,默认是查找前缀为 /dev/sd 或者 /dev/xvd 的磁盘。
-s 选项,设置每次读取的数据量⼤⼩,单位为字节,默认为 33554432(也就是 32MB)。
这个案例同样需要你开启两个终端。分别 SSH 登录到机器上后,先在第⼀个终端中运⾏ cachetop 命令:
# 每隔5秒刷新⼀次数据 
$ cachetop 5
接着,再到第⼆个终端,执⾏下⾯的命令运⾏案例:
$ docker run --privileged --name=app -itd feisky/app:io-direct
案例运⾏后,我们还需要运⾏下⾯这个命令,来确认案例已经正常启动。如果⼀切正常,你应该可以看到类似下⾯的输出:
$ 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
从这⾥你可以看到,每读取 32 MB 的数据,就需要花 0.9 秒。这个时间合理吗?我想你第⼀反应就是,太慢了吧。那这是不
是没⽤系统缓存导致的呢?
我们再来检查⼀下。回到第⼀个终端,先看看 cachetop 的输出,在这⾥,我们找到案例进程 app 的缓存使⽤情况:
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 次缓存全部命中,读的命中率是 100%,看起来全部的读请求都经过了系统缓存。但是问题
⼜来了,如果真的都是缓存 I/O,读取速度不应该这么慢。
 
不过,话说回来,我们似乎忽略了另⼀个重要因素,每秒实际读取的数据⼤⼩。HITS 代表缓存的命中次数,那么每次命中能
读取多少数据呢?⾃然是⼀⻚。
 
前⾯讲过,内存以⻚为单位进⾏管理,⽽每个⻚的⼤⼩是 4KB。所以,在5秒的时间间隔⾥,命中的缓存为 1024*4K/1024 =
4MB,再除以5 秒,可以得到每秒读的缓存是 0.8MB,显然跟案例应⽤的32 MB/s 相差太多。
 
⾄于为什么只能看到 0.8 MB 的 HITS,我们后⾯再解释,这⾥你先知道怎么根据结果来分析就可以了。
 
这也进⼀步验证了我们的猜想,这个案例估计没有充分利⽤系统缓存。其实前⾯我们遇到过类似的问题,如果为系统调⽤设置
直接 I/O 的标志,就可以绕过系统缓存。
 
那么,要判断应⽤程序是否⽤了直接I/O,最简单的⽅法当然是观察它的系统调⽤,查找应⽤程序在调⽤它们时的选项。使⽤
什么⼯具来观察系统调⽤呢?⾃然还是 strace。
 
继续在终端⼆中运⾏下⾯的 strace 命令,观察案例应⽤的系统调⽤情况。注意,这⾥使⽤了 pgrep 命令来查找案例进程的
PID 号:
# 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)
从 strace 的结果可以看到,案例应⽤调⽤了 openat 来打开磁盘分区 /dev/sdb1,并且传⼊的参数为
O_RDONLY|O_DIRECT(中间的竖线表示或)。
O_RDONLY 表示以只读⽅式打开,⽽ O_DIRECT 则表示以直接读取的⽅式打开,这会绕过系统的缓存。
验证了这⼀点,就很容易理解为什么读 32 MB的数据就都要那么久了。直接从磁盘读写的速度,⾃然远慢于对缓存的读写。
这也是缓存存在的最⼤意义了。
找出问题后,我们还可以在再看看案例应⽤的源代码,再次验证⼀下:
int flags = O_RDONLY | O_LARGEFILE | O_DIRECT; 
int fd = open(disk, flags, 0755);
上⾯的代码,很清楚地告诉我们:它果然⽤了直接 I/O。
找出了磁盘读取缓慢的原因,优化磁盘读的性能⾃然不在话下。修改源代码,删除 O_DIRECT 选项,让应⽤程序使⽤缓存
I/O ,⽽不是直接 I/O,就可以加速磁盘读取速度。
app-cached.c 就是修复后的源码,我也把它打包成了⼀个容器镜像。在第⼆个终端中,按 Ctrl+C 停⽌刚才的 strace 命令,运
⾏下⾯的命令,你就可以启动它:
# 删除上述案例应⽤ 
$ docker rm -f app 

# 运⾏修复后的应⽤ 
$ docker run --privileged --name=app -itd feisky/app:io-cached
还是第⼆个终端,再来运⾏下⾯的命令查看新应⽤的⽇志,你应该能看到下⾯这个输出:
$ docker logs app 
Reading data from disk /dev/sdb1 with buffer size 33554432 
Time used: 0.037342 s s to read 33554432 bytes 
Time used: 0.029676 s to read 33554432 bytes
现在,每次只需要 0.03秒,就可以读取 32MB 数据,明显⽐之前的 0.9 秒快多了。所以,这次应该⽤了系统缓存。
我们再回到第⼀个终端,查看 cachetop 的输出来确认⼀下:
16:40:08 Buffers MB: 73 / Cached MB: 281 / Sort: HITS / Order: ascending 
PID    UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 
22106 root app 40960 0      0       100.0%   0.0%
果然,读的命中率还是 100%,HITS (即命中数)却变成了 40960,同样的⽅法计算⼀下,换算成每秒字节数正好是 32
MB(即 40960*4k/5/1024=32M)。
 
这个案例说明,在进⾏ I/O 操作时,充分利⽤系统缓存可以极⼤地提升性能。 但在观察缓存命中率时,还要注意结合应⽤程
序实际的 I/O ⼤⼩,综合分析缓存的使⽤情况。
 
案例的最后,再回到开始的问题,为什么优化前,通过 cachetop 只能看到很少⼀部分数据的全部命中,⽽没有观察到⼤量数
据的未命中情况呢?这是因为,cachetop ⼯具并不把直接 I/O 算进来。这也⼜⼀次说明了,了解⼯具原理的重要。
cachetop 的计算⽅法涉及到 I/O 的原理以及⼀些内核的知识,如果你想了解它的原理的话,可以点击这⾥查看它的源代
 
总结
Buffers 和 Cache 可以极⼤提升系统的 I/O 性能。通常,我们⽤缓存命中率,来衡量缓存的使⽤效率。命中率越⾼,表示缓存
被利⽤得越充分,应⽤程序的性能也就越好。
 
你可以⽤ cachestat 和 cachetop 这两个⼯具,观察系统和进程的缓存命中情况。其中,
cachestat 提供了整个系统缓存的读写命中情况。
cachetop 提供了每个进程的缓存命中情况。
 
不过要注意,Buffers 和 Cache 都是操作系统来管理的,应⽤程序并不能直接控制这些缓存的内容和⽣命周期。所以,在应⽤
程序开发中,⼀般要⽤专⻔的缓存组件,来进⼀步提升性能。
⽐如,程序内部可以使⽤堆或者栈明确声明内存空间,来存储需要缓存的数据。再或者,使⽤ Redis 这类外部缓存服务,优
化数据的访问效率。
 
思考
最后,我想给你留下⼀道思考题,帮你更进⼀步了解缓存的原理。
今天的第⼆个案例你应该很眼熟,因为前⾯不可中断进程的⽂章⽤的也是直接I/O的例⼦,不过那次,我们是从CPU使⽤率和
进程状态的⻆度来分析的。对⽐CPU和缓存这两个不同⻆度的分析思路,你有什么样的发现呢?
posted @ 2021-12-17 16:29  求其在我  阅读(184)  评论(0编辑  收藏  举报