17-案例篇:如何利用系统缓存优化程序的运行效率?
缓存命中率
缓存的命中率:所谓缓存命中率,是指直接通过缓存获取数据的请求次数,占所有数据请求次数的百分比
命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好
实际上,缓存是现在所有高并发系统必需的核心模块,主要作用就是把经常访问的数据(也就是热点数据),提前读入到内存中
下次访问时就可以直接从内存读取数据,而不需要经过硬盘,从而加快应用程序的响应速度
这些独立的缓存模块通常会提供查询接口,方便查看缓存的命中情况
不过Linux系统中并没有直接提供这些接口,需要使用cachestat和cachetop工具查看系统缓存命中情况
cachestat提供了整个操作系统缓存的读写命中情况
cachetop提供了每个进程的缓存命中情况
安装cachestat和cachetop命令
# 使用cachestat和cachetop前,我们首先要安装bcc软件包
# 参考这篇博客:https://www.cnblogs.com/lichengguo/p/15668561.html
# 1.安装包
[root@local_sa_192-168-1-6 tools]# yum install -y bcc-tools
# 2.配置环境变量
[root@local_sa_192-168-1-6 ~]# vi /etc/profile
export PATH=$PATH:/usr/share/bcc/tools
# 3.cachestat 以1秒的时间间隔,输出了3组缓存统计数据 提供了整个操作系统缓存的读写命中情况
[root@local_sa_192-168-1-6 ~]# cachestat 1 3
HITS MISSES DIRTIES HITRATIO BUFFERS_MB CACHED_MB
0 0 0 0.00% 2 261
0 0 0 0.00% 2 261
0 0 0 0.00% 2 261
# HITS 表示缓存命中的次数
# MISSES 表示缓存未命中的次数
# DIRTIES 表示新增到缓存中的脏页数
# BUFFERS_MB 表示Buffers的大小,以MB为单位
# CACHED_MB 表示Cache的大小,以MB为单位
# 4.cachetop提供了每个进程的缓存命中情况
[root@local_sa_192-168-1-6 ~]# cachetop
14:47:21 Buffers MB: 2 / Cached MB: 196 / Sort: HITS / Order: ascending
PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT%
1540 root cachetop 3 0 0 100.0% 0.0%
# PID 进程ID
# UID 用户ID
# CMD 执行的指令
# HITS 表示缓存命中的次数
# MISSES 表示缓存未命中的次数
# DIRTIES 表示新增到缓存中的脏页数
# READ_HIT% 读的缓存命中率
# WRITE_HIT% 写的缓存命中率
查看文件的缓存大小
pcstat工具可以查看文件在内存中的缓存大小,以及缓存比例
# pcstat工具
# 下载地址:https://alnk-blog-pictures.oss-cn-shenzhen.aliyuncs.com/blog-pictures/pcstat.x86_64
# 安装
[root@local_sa_192-168-1-6 bin]# pwd
/usr/bin
# 上传命令包到服务器
[root@local_sa_192-168-1-6 bin]# rz
# 授权和改名
[root@local_sa_192-168-1-6 bin]# chmod +x pcstat.x86_64
[root@local_sa_192-168-1-6 bin]# mv pcstat.x86_64 pcstat
# 使用
[root@local_sa_192-168-1-6 ~]# 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@local_sa_192-168-1-6 ~]# ls
[root@local_sa_192-168-1-6 ~]# pcstat /bin/ls
|----------+----------------+------------+-----------+---------|
| Name | Size | Pages | Cached | Percent |
|----------+----------------+------------+-----------+---------|
| /bin/ls | 117608 | 29 | 29 | 100.000 |
|----------+----------------+------------+-----------+---------|
案例一
dd作为一个磁盘和文件的拷贝工具,经常被拿来测试磁盘或者文件系统的读写性能
不过,既然缓存会影响到性能,如果用dd对同一个文件进行多次读取测试,测试的结果会怎么样呢?
-
使用dd命令生成一个临时文件,用于后面的文件读取测试
# 生成一个512MB的临时文件 [root@local_sa_192-168-1-6 ~]# dd if=/dev/sda1 of=file bs=1M count=512 # 清理缓存 [root@local_sa_192-168-1-6 ~]# echo 3 > /proc/sys/vm/drop_caches # 运行pcstat命令,确认刚刚生成的文件不在缓存中 [root@local_sa_192-168-1-6 ~]# pcstat file |----------+----------------+------------+-----------+---------| | Name | Size | Pages | Cached | Percent | |----------+----------------+------------+-----------+---------| | file | 536870912 | 131072 | 0 | 000.000 | |----------+----------------+------------+-----------+---------|
-
终端一,运行cachetop命令,每隔5秒刷新一次数据
[root@local_sa_192-168-1-6 ~]# cachetop 5
-
终端二,运行dd命令测试文件的读取速度
[root@local_sa_192-168-1-6 ~]# dd if=file of=/dev/null bs=1M 记录了512+0 的读入 记录了512+0 的写出 536870912字节(537 MB)已复制,1.87234 秒,287 MB/秒 # 这个文件的读性能是287MB/秒 由于在dd命令运行前已经清理了缓存 # 所以dd命令读取数据时,肯定要通过文件系统从磁盘中读取
-
回到第一个终端, 查看cachetop界面的缓存命中情况
15:35:13 Buffers MB: 0 / Cached MB: 631 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 2500 root dd 131279 131448 0 50.0% 50.0% # 从cachetop的结果可以发现,并不是所有的读都落到了磁盘上,事实上读请求的缓存命中率只有50%
-
切换到第二个终端,再次执行刚才的dd命令
[root@local_sa_192-168-1-6 ~]# dd if=file of=/dev/null bs=1M 记录了512+0 的读入 记录了512+0 的写出 536870912字节(537 MB)已复制,0.120005 秒,4.5GB/秒 # 磁盘的读性能居然变成了4.5GB/s,比第一次的结果明显高了太多
-
回到第一个终端,看看cachetop的情况
15:35:53 Buffers MB: 0 / Cached MB: 631 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 2501 root dd 131279 0 0 100.0% 0.0% # 读的缓存命中率是100.0%,也就是说这次的dd命令全部命中了缓存,所以才会看到那么高的性能
-
回到第二个终端,再次执行pcstat查看文件 file 的缓存情况
[root@local_sa_192-168-1-6 ~]# pcstat file |----------+----------------+------------+-----------+---------| | Name | Size | Pages | Cached | Percent | |----------+----------------+------------+-----------+---------| | file | 536870912 | 131072 | 131072 | 100.000 | |----------+----------------+------------+-----------+---------| # 从pcstat的结果可以发现,测试文件file已经被全部缓存了起来,这跟刚才观察到的缓存命中率100%是一致的
-
结论
两次结果说明,系统缓存对第二次dd操作有明显的加速效果,可以大大提高文件读取的性能
但同时也要注意,如果把dd当成测试文件系统性能的工具,由于缓存的存在,就会导致测试结果严重失真
案例二
这个案例的基本功能比较简单,就是每秒从磁盘分区/dev/sda1中读取32MB的数据,并打印出读取数据花费的时间
直接启动docker镜像即可
-d选项,设置要读取的磁盘或分区路径,默认是查找前缀为/dev/sd或者/dev/xvd的磁盘
-s选项,设置每次读取的数据量大小,单位为字节,默认为33554432(也就是 32MB)
-
在第一个终端中运行cachetop命令
# 每隔5秒刷新一次数据 [root@local_sa_192-168-1-6 ~]# cachetop 5
-
到第二个终端,执行下面的命令运行案例
[root@local_sa_192-168-1-6 ~]# docker run --privileged --name=app -itd feisky/app:io-direct
-
如果正常启动, 可以看到类似下面的输出
[root@local_sa_192-168-1-6 ~]# 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 # 可以看到,每读取32MB的数据,就需要花0.9秒,太慢了
-
回到第一个终端,从cachetop的输出里找到案例进程app的缓存使用情况
[root@local_sa_192-168-1-6 ~]# cachetop 5 16:22:59 Buffers MB: 36 / Cached MB: 1064 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 3035 root app 2048 0 0 100.0% 0.0% # 2048次缓存全部命中,读的命中率是100%,看起来全部的读请求都经过了系统缓存 # 如果真的都是缓存I/O,那么读取速度不应该这么慢 # 计算每秒实际读取的数据大小 # HITS代表缓存的命中次数,那么每次命中能读取多少数据呢?自然是一页 # 内存以页为单位进行管理,而每个页的大小是4KB # 所以,在5秒的时间间隔里,命中的缓存为2048*4K/1024=8MB,再除以5秒,可以得到每秒读的缓存是1.6MB,显然跟案例应用的32MB/s相差太多 # 这个案例估计没有充分利用系统缓存 # 其实前面我们遇到过类似的问题,如果为系统调用设置直接I/O的标志,就可以绕过系统缓存
-
要判断应用程序是否用了直接 I/O,最简单的方法当然是观察它的系统调用
[root@local_sa_192-168-1-6 ~]# strace -p $(pgrep app) strace: Process 3035 attached restart_syscall(<... resuming interrupted read ...>) = 0 openat(AT_FDCWD, "/dev/sda3", O_RDONLY|O_DIRECT) = 4 mmap(NULL, 33558528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f508c348000 gettimeofday({tv_sec=1638520052, tv_usec=468572}, NULL) = 0 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 gettimeofday({tv_sec=1638520052, tv_usec=655666}, NULL) = 0 write(1, "Time used: 0.187094 s to read 33"..., 45) = 45 close(4) = 0 # 从strace的结果可以看到,案例应用调用了openat来打开磁盘分区/dev/sda3,并且传入的参数为O_RDONLY|O_DIRECT # O_RDONLY表示以只读方式打开,而O_DIRECT则表示以直接读取的方式打开,这会绕过系统的缓存 # 验证了这一点,就很容易理解为什么读32MB的数据就都要那么久了 # 直接从磁盘读写的速度,自然远慢于对缓存的读写
-
修复
# 删除上述案例应用 [root@local_sa_192-168-1-6 ~]# docker rm -f app # 运行修复后的应用 [root@local_sa_192-168-1-6 ~]# docker run --privileged --name=app -itd feisky/app:io-cached [root@local_sa_192-168-1-6 ~]# docker logs app Reading data from disk /dev/sda3 with buffer size 33554432 Time used: 0.011737 s to read 33554432 bytes Time used: 0.011706 s to read 33554432 bytes Time used: 0.011743 s to read 33554432 bytes # 现在,每次只需要0.01秒,就可以读取32MB数据,明显比之前的0.9秒快多了 # 所以,这次应该用了系统缓存
-
回到第一个终端,查看cachetop的输出来确认一下
[root@local_sa_192-168-1-6 ~]# cachetop 5 6:31:04 Buffers MB: 36 / Cached MB: 1066 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 3134 root app 40960 0 0 100.0% 0.0% # 读的命中率还是100%,HITS却变成了40960 # 同样的方法计算一下,换算成每秒字节数正好是32MB(40960*4k/5/1024=32M)
-
小结
这个案例说明,在进行I/O操作时,充分利用系统缓存可以极大地提升性能 在观察缓存命中率时,还要注意结合应用程序实际的I/O大小,综合分析缓存的使用情况 为什么优化前,通过cachetop只能看到很少一部分数据的全部命中,而没有观察到大量数据的未命中情况呢? 这是因为,cachetop工具并不把直接I/O算进来
总结
Buffers和Cache可以极大提升系统的I/O性能
通常,我们用缓存命中率,来衡量缓存的使用效率
命中率越高,表示缓存被利用得越充分,应用程序的性能也就越好
不过要注意,Buffers和Cache都是操作系统来管理的,应用程序并不能直接控制这些缓存的内容和生命周期
所以,在应用程序开发中,一般要用专门的缓存组件,来进一步提升性能
比如,程序内部可以使用堆或者栈明确声明内存空间,来存储需要缓存的数据
再或者, 使用Redis这类外部缓存服务,优化数据的访问效率