linux性能优化利用缓存优化程序运行效率

Posted sysu_lluozh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux性能优化利用缓存优化程序运行效率相关的知识,希望对你有一定的参考价值。

了解了内存性能中Buffer和Cache的概念,其中Buffer和Cache的设计目的,是为了提升系统的I/O性能。它们利用内存,充当起慢速磁盘与快速CPU之间 的桥梁,可以加速I/O的访问速度

既然Buffer和Cache对系统性能有很大影响,那在软件开发的过程中能不能利用这一点优化I/O性能,提升应用程序的运行效率呢?

答案自然是肯定的
接下来用几个案例更好地理解缓存的作用,并学习如何充分利用这些缓存来提高程序效率

注:
为了方便理解,Buffer和Cache用英文表示,避免跟"缓存"一词混淆。而文中的"缓存",通指数据在内存中的临时存储

一、缓存命中率

在案例开始前,先问自己一个问题,想要做成某件事情结果应该怎么评估?
比如说,想利用缓存来提升程序的运行效率,应该怎么评估这个效果呢?换句话说,有没有哪个指标可以衡量缓存使用的好坏呢?

1.1 缓冲命中率定义

估计可以想到,缓存的命中率。所谓缓存命中率,是指直接通过缓存获取数据的请求次数占所有数据请求次数的百分比

命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好

1.2 缓冲命中率作用

实际上,缓存是现在所有高并发系统必需的核心模块,主要作用是把经常访问的数据(热点数据),提前读入到内存中

这样,下次访问时就可以直接从内存读取数据,而不需要经过硬盘,从而加快应用程序的响应速度

1.3 缓冲命中率查询

这些独立的缓存模块通常会提供查询接口,方便随时查看缓存的命中情况

不过Linux系统中并没有直接提供这些接口,所以这里要介绍一下cachestatcachetop,它们正是查看系统缓存命中情况的工具:

  • cachestat

提供整个操作系统缓存的读写命中情况

  • cachetop

提供每个进程的缓存命中情况

这两个工具都是bcc软件包的一部分,它们基于Linux内核的eBPF(extended Berkeley Packet Filters)机制来跟踪内核中管理的缓存,并输出缓存的使用和命中情况

1.4 bcc软件包安装

使用cachestatcachetop前需要安装 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
sudo apt‑get update
sudo apt‑get install ‑y bcc‑tools libbcc‑examples linux‑headers‑$(uname ‑r)

操作完这些步骤,bcc提供的所有工具就都安装到/usr/share/bcc/tools这个目录中
不过bcc软件包默认不会把这些工具配置到系统的PATH路径中,所以需要手动配置:

$ export PATH=$PATH:/usr/share/bcc/tools

配置完,可以运行cachestatcachetop命令

1.5 cachestat命令

下面就是cachestat的运行界面:

$ 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 

以1秒的时间间隔,输出了3组缓存统计数据

可以看到,cachestat的输出其实是一个表格。每行代表一组数据,而每一列代表不同的缓存统计指标
这些指标从左到右依次表示:

  • TOTAL

表示总的 I/O 次数

  • MISSES

表示缓存未命中的次数

  • HITS

表示缓存命中的次数

  • DIRTIES

表示新增到缓存中的脏页数

  • BUFFERS_MB

表示Buffers的大小,以MB为单位

  • CACHED_MB

表示Cache的大小,以MB为单位

1.6 cachetop命令

接下来再来看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

新增到缓存中的脏页数

  • READ_HIT

读缓存命中率

  • WRITE_HIT

写缓存命中率

注:HITS、MISSES和DIRTIES,跟cachestat里的含义一样

二、指定文件的缓存大小

除了缓存的命中率外,还有一个指标非常重要,那就是指定文件在内存中的缓存大小

可以使用pcstat这个工具,来查看文件在内存中的缓存大小以及缓存比例

2.1 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来查看文件的缓存情况

2.2 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 |
+‑‑‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑+

知道了缓存相应的指标和查看系统缓存的方法后,接下来就进入正式案例

三、案例一

第一个案例先看一下dd命令

3.1 dd命令

dd作为一个磁盘和文件的拷贝工具,经常被拿来测试磁盘或者文件系统的读写性能

不过既然缓存会影响到性能,如果用dd对同一个文件进行多次读取测试,测试的结果会怎么样 呢?

首先,打开两个终端连接到Ubuntu机器上,确保bcc已经安装配置成功

然后,使用dd命令生成一个临时文件,用于后面的文件读取测试:

# 生成一个512MB的临时文件
$ dd if=/dev/sda1 of=file bs=1M count=512
# 清理缓存
$ echo 3 > /proc/sys/vm/drop_caches

继续在第一个终端运行 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.5GB/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查看文件file的缓存情况:

$ pcstat file
+‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑+
| Name  | Size (bytes)   | Pages      | Cached    | Percent |
|‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑|
| file  | 536870912      | 131072     | 131072    | 100.000 |
+‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑‑‑+‑‑‑‑‑‑‑‑‑+

从pcstat的结果可以发现,测试文件file已经被全部缓存了起来,这跟刚才观察到的缓存命中率 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

从这里可以看到,每读取32MB的数据需要花 0.9 秒,这个时间合理吗?有没有觉得这也太慢了吧?那这是不是没用系统缓存导致的呢?

再来检查一下,回到第一个终端先看看cachetop的输出

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% 

可以看到进程app的缓存使用情况,即1024次缓存全部命中,读的命中率是100%
看起来全部的读请求都经过了系统缓存,但是问题又来了,如果真的都是缓存I/O,读取速度不应该这么慢

不过话说回来,似乎忽略了另一个重要因素:每秒实际读取的数据大小
HITS代表缓存的命中次数,那么每次命中能读取多少数据呢?自然是一页

内存以页为单位进行管理,而每个页的大小是4KB。所以,在5秒的时间间隔里命中的缓存为4MB(=1024(HITS)*4K(每个页大小)/1024(换算成MB)),可以得到每秒读的缓存是0.8MB(=4MB/5),显然和32MB/s相差很大

这也进一步验证了猜想,估计并没有充分利用系统缓存,如果为系统调用设置直接I/O的标志就可以绕过系统缓存

那么,要判断应用程序是否用了直接I/O,最简单的方法当然是观察它的系统调用,查找应用程序在调用它们时的选项
使用什么工具来观察系统调用呢?自然还是strace

继续在终端二中运行下面的strace命令,观察案例应用的系统调用情况

# 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"\\.\\.\\., 3
write(1, "Time used: 0.948897 s to read 33"\\.\\.\\., 45) = 45
close(4)                                = 0

注意:这里使用pgrep命令来查找案例进程的PID号

从strace的结果可以看到,调用了openat来打开磁盘分区/dev/sdb1,并且传入的参数为O_RDONLY|O_DIRECT(中间的竖线表示或)

O_RDONLY表示以只读方式打开,而O_DIRECT则表示以直接读取的方式打开,这会绕过
系统的缓存

验证了这一点,就很容易理解为什么读32MB的数据就都要那么久了。直接从磁盘读写的速度自然远慢于对缓存的读写,这也是缓存存在的最大意义

找出问题后,还可以再看看案例应用的源代码,再次验证一下:

int flags = O_RDONLY | O_LARGEFILE | O_DIRECT; 
int fd = open(disk, flags, 0755);

上面的代码很清楚说明:果然用了直接I/O

找出了磁盘读取缓慢的原因,优化磁盘读的性能自然不在话下
修改源代码,删除O_DIRECT选项让应用程序使用缓存 I/O,而不是直接I/O,就可以加速磁盘读取速度

这个案例说明,在进行I/O操作时充分利用系统缓存可以极大地提升性能,但在观察缓存命中率时,还要注意结合应用程序实际的I/O大小,综合分析缓存的使用情况

案例的最后再回到开始的问题,为什么优化前通过cachetop只能看到很少一部分数据的全部命中,而没有观察到大量数据的未命中情况呢?这是因为cachetop工具并不把直接I/O算进来

五、小结

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

可以用cachestat和cachetop这两个工具观察系统和进程的缓存命中情况,其中:

  • cachestat提供整个系统缓存的读写命中情况
  • cachetop提供每个进程的缓存命中情况

不过要注意,Buffers和Cache都是操作系统来管理的,应用程序并不能直接控制这些缓存的内容和生命周期。所以在应用程序开发中,一般要用专门的缓存组件来进一步提升性能

比如:

  • 程序内部可以使用堆或者栈明确声明内存空间来存储需要缓存的数据
  • 使用Redis这类外部缓存服务优化数据的访问效率

以上是关于linux性能优化利用缓存优化程序运行效率的主要内容,如果未能解决你的问题,请参考以下文章

linux性能优化CPU上下文切换

linux性能优化动态追踪

Linux性能优化实战:如何利用系统缓存优化程序的运行效率?(17)

linux性能优化怎么评估系统的网络性能

linux性能优化系统Swap变高原因分析

linux性能优化定位分析狂打日志的问题