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对同一个文件进行多次读取测试,测试的结果会怎么样呢?

  1. 使用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 |
    |----------+----------------+------------+-----------+---------|
    
    
  2. 终端一,运行cachetop命令,每隔5秒刷新一次数据

    [root@local_sa_192-168-1-6 ~]# cachetop 5
    
    
  3. 终端二,运行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命令读取数据时,肯定要通过文件系统从磁盘中读取
    
    
  4. 回到第一个终端, 查看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%
    
    
  5. 切换到第二个终端,再次执行刚才的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,比第一次的结果明显高了太多
    
    
  6. 回到第一个终端,看看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命令全部命中了缓存,所以才会看到那么高的性能
    
    
  7. 回到第二个终端,再次执行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%是一致的
    
    
  8. 结论

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




案例二

这个案例的基本功能比较简单,就是每秒从磁盘分区/dev/sda1中读取32MB的数据,并打印出读取数据花费的时间

直接启动docker镜像即可

-d选项,设置要读取的磁盘或分区路径,默认是查找前缀为/dev/sd或者/dev/xvd的磁盘

-s选项,设置每次读取的数据量大小,单位为字节,默认为33554432(也就是 32MB)

  1. 在第一个终端中运行cachetop命令

    # 每隔5秒刷新一次数据
    [root@local_sa_192-168-1-6 ~]# cachetop 5
    
    
  2. 到第二个终端,执行下面的命令运行案例

    [root@local_sa_192-168-1-6 ~]# docker run --privileged --name=app -itd feisky/app:io-direct
    
    
  3. 如果正常启动, 可以看到类似下面的输出

    [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秒,太慢了
    
    
  4. 回到第一个终端,从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的标志,就可以绕过系统缓存
    
    
  5. 要判断应用程序是否用了直接 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的数据就都要那么久了
    # 直接从磁盘读写的速度,自然远慢于对缓存的读写
    
    
  6. 修复

    # 删除上述案例应用
    [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秒快多了
    # 所以,这次应该用了系统缓存
    
    
  7. 回到第一个终端,查看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)
    
    
  8. 小结

    这个案例说明,在进行I/O操作时,充分利用系统缓存可以极大地提升性能
    在观察缓存命中率时,还要注意结合应用程序实际的I/O大小,综合分析缓存的使用情况
    
    为什么优化前,通过cachetop只能看到很少一部分数据的全部命中,而没有观察到大量数据的未命中情况呢?
    这是因为,cachetop工具并不把直接I/O算进来
    


总结

Buffers和Cache可以极大提升系统的I/O性能
通常,我们用缓存命中率,来衡量缓存的使用效率
命中率越高,表示缓存被利用得越充分,应用程序的性能也就越好

不过要注意,Buffers和Cache都是操作系统来管理的,应用程序并不能直接控制这些缓存的内容和生命周期
所以,在应用程序开发中,一般要用专门的缓存组件,来进一步提升性能
比如,程序内部可以使用堆或者栈明确声明内存空间,来存储需要缓存的数据
再或者, 使用Redis这类外部缓存服务,优化数据的访问效率


posted @ 2021-12-03 17:27  李成果  阅读(122)  评论(0编辑  收藏  举报