• 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和缓存这两个不同⻆度的分析思路,你有什么样的发现呢?
    专注事业!
  • 相关阅读:
    爬虫工具简单整理
    vue单页面处理SEO问题
    深入浅出MyBatis-快速入门
    js的匿名函数 和普通函数
    Javascript位置 body之前、后执行顺序!
    eclipse中的ctrl+H使用中的问题
    Eclipse中ctrl+shift+r与ctrl+shift+t的区别
    Java 判断字符串是否为空的四种方法、优缺点与注意事项
    eclipse 快捷键
    只缩进新加一段代码的方法
  • 原文地址:https://www.cnblogs.com/pengai/p/14775943.html
Copyright © 2020-2023  润新知