mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 22:23:45 +08:00
mod
This commit is contained in:
282
极客时间专栏/Linux性能优化实战/I|O 性能篇/23 | 基础篇:Linux 文件系统是怎么工作的?.md
Normal file
282
极客时间专栏/Linux性能优化实战/I|O 性能篇/23 | 基础篇:Linux 文件系统是怎么工作的?.md
Normal file
@@ -0,0 +1,282 @@
|
||||
<audio id="audio" title="23 | 基础篇:Linux 文件系统是怎么工作的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8b/90/8b7e1b50b634ca14c9c204fe16fd8a90.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
通过前面CPU和内存模块的学习,我相信,你已经掌握了CPU和内存的性能分析以及优化思路。从这一节开始,我们将进入下一个重要模块——文件系统和磁盘的I/O性能。
|
||||
|
||||
同CPU、内存一样,磁盘和文件系统的管理,也是操作系统最核心的功能。
|
||||
|
||||
<li>
|
||||
磁盘为系统提供了最基本的持久化存储。
|
||||
</li>
|
||||
<li>
|
||||
文件系统则在磁盘的基础上,提供了一个用来管理文件的树状结构。
|
||||
</li>
|
||||
|
||||
那么,磁盘和文件系统是怎么工作的呢?又有哪些指标可以衡量它们的性能呢?
|
||||
|
||||
今天,我就带你先来看看,Linux文件系统的工作原理。磁盘的工作原理,我们下一节再来学习。
|
||||
|
||||
## 索引节点和目录项
|
||||
|
||||
文件系统,本身是对存储设备上的文件,进行组织管理的机制。组织方式不同,就会形成不同的文件系统。
|
||||
|
||||
你要记住最重要的一点,在Linux中一切皆文件。不仅普通的文件和目录,就连块设备、套接字、管道等,也都要通过统一的文件系统来管理。
|
||||
|
||||
为了方便管理,Linux文件系统为每个文件都分配两个数据结构,索引节点(index node)和目录项(directory entry)。它们主要用来记录文件的元信息和目录结构。
|
||||
|
||||
<li>
|
||||
索引节点,简称为inode,用来记录文件的元数据,比如inode编号、文件大小、访问权限、修改日期、数据的位置等。索引节点和文件一一对应,它跟文件内容一样,都会被持久化存储到磁盘中。所以记住,索引节点同样占用磁盘空间。
|
||||
</li>
|
||||
<li>
|
||||
目录项,简称为dentry,用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。不过,不同于索引节点,目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存。
|
||||
</li>
|
||||
|
||||
换句话说,索引节点是每个文件的唯一标志,而目录项维护的正是文件系统的树状结构。目录项和索引节点的关系是多对一,你可以简单理解为,一个文件可以有多个别名。
|
||||
|
||||
举个例子,通过硬链接为文件创建的别名,就会对应不同的目录项,不过这些目录项本质上还是链接同一个文件,所以,它们的索引节点相同。
|
||||
|
||||
索引节点和目录项纪录了文件的元数据,以及文件间的目录关系,那么具体来说,文件数据到底是怎么存储的呢?是不是直接写到磁盘中就好了呢?
|
||||
|
||||
实际上,磁盘读写的最小单位是扇区,然而扇区只有512B 大小,如果每次都读写这么小的单位,效率一定很低。所以,文件系统又把连续的扇区组成了逻辑块,然后每次都以逻辑块为最小单元,来管理数据。常见的逻辑块大小为4KB,也就是由连续的8个扇区组成。
|
||||
|
||||
为了帮助你理解目录项、索引节点以及文件数据的关系,我画了一张示意图。你可以对照着这张图,来回忆刚刚讲过的内容,把知识和细节串联起来。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/32/47/328d942a38230a973f11bae67307be47.png" alt="">
|
||||
|
||||
不过,这里有两点需要你注意。
|
||||
|
||||
第一,目录项本身就是一个内存缓存,而索引节点则是存储在磁盘中的数据。在前面的Buffer和Cache原理中,我曾经提到过,为了协调慢速磁盘与快速CPU的性能差异,文件内容会缓存到页缓存Cache中。
|
||||
|
||||
那么,你应该想到,这些索引节点自然也会缓存到内存中,加速文件的访问。
|
||||
|
||||
第二,磁盘在执行文件系统格式化时,会被分成三个存储区域,超级块、索引节点区和数据块区。其中,
|
||||
|
||||
<li>
|
||||
超级块,存储整个文件系统的状态。
|
||||
</li>
|
||||
<li>
|
||||
索引节点区,用来存储索引节点。
|
||||
</li>
|
||||
<li>
|
||||
数据块区,则用来存储文件数据。
|
||||
</li>
|
||||
|
||||
## 虚拟文件系统
|
||||
|
||||
目录项、索引节点、逻辑块以及超级块,构成了Linux文件系统的四大基本要素。不过,为了支持各种不同的文件系统,Linux内核在用户进程和文件系统的中间,又引入了一个抽象层,也就是虚拟文件系统VFS(Virtual File System)。
|
||||
|
||||
VFS 定义了一组所有文件系统都支持的数据结构和标准接口。这样,用户进程和内核中的其他子系统,只需要跟VFS 提供的统一接口进行交互就可以了,而不需要再关心底层各种文件系统的实现细节。
|
||||
|
||||
这里,我画了一张Linux文件系统的架构图,帮你更好地理解系统调用、VFS、缓存、文件系统以及块存储之间的关系。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/72/12/728b7b39252a1e23a7a223cdf4aa1612.png" alt="">
|
||||
|
||||
通过这张图,你可以看到,在VFS的下方,Linux支持各种各样的文件系统,如Ext4、XFS、NFS等等。按照存储位置的不同,这些文件系统可以分为三类。
|
||||
|
||||
<li>
|
||||
第一类是基于磁盘的文件系统,也就是把数据直接存储在计算机本地挂载的磁盘中。常见的Ext4、XFS、OverlayFS等,都是这类文件系统。
|
||||
</li>
|
||||
<li>
|
||||
第二类是基于内存的文件系统,也就是我们常说的虚拟文件系统。这类文件系统,不需要任何磁盘分配存储空间,但会占用内存。我们经常用到的 /proc 文件系统,其实就是一种最常见的虚拟文件系统。此外,/sys 文件系统也属于这一类,主要向用户空间导出层次化的内核对象。
|
||||
</li>
|
||||
<li>
|
||||
第三类是网络文件系统,也就是用来访问其他计算机数据的文件系统,比如NFS、SMB、iSCSI等。
|
||||
</li>
|
||||
|
||||
这些文件系统,要先挂载到 VFS 目录树中的某个子目录(称为挂载点),然后才能访问其中的文件。拿第一类,也就是基于磁盘的文件系统为例,在安装系统时,要先挂载一个根目录(/),在根目录下再把其他文件系统(比如其他的磁盘分区、/proc文件系统、/sys文件系统、NFS等)挂载进来。
|
||||
|
||||
## 文件系统I/O
|
||||
|
||||
把文件系统挂载到挂载点后,你就能通过挂载点,再去访问它管理的文件了。VFS 提供了一组标准的文件访问接口。这些接口以系统调用的方式,提供给应用程序使用。
|
||||
|
||||
就拿cat 命令来说,它首先调用 open() ,打开一个文件;然后调用 read() ,读取文件的内容;最后再调用 write() ,把文件内容输出到控制台的标准输出中:
|
||||
|
||||
```
|
||||
int open(const char *pathname, int flags, mode_t mode);
|
||||
ssize_t read(int fd, void *buf, size_t count);
|
||||
ssize_t write(int fd, const void *buf, size_t count);
|
||||
|
||||
```
|
||||
|
||||
文件读写方式的各种差异,导致 I/O的分类多种多样。最常见的有,缓冲与非缓冲I/O、直接与非直接I/O、阻塞与非阻塞I/O、同步与异步I/O等。 接下来,我们就详细看这四种分类。
|
||||
|
||||
第一种,根据是否利用标准库缓存,可以把文件I/O分为缓冲I/O与非缓冲I/O。
|
||||
|
||||
<li>
|
||||
缓冲I/O,是指利用标准库缓存来加速文件的访问,而标准库内部再通过系统调度访问文件。
|
||||
</li>
|
||||
<li>
|
||||
非缓冲I/O,是指直接通过系统调用来访问文件,不再经过标准库缓存。
|
||||
</li>
|
||||
|
||||
注意,这里所说的“缓冲”,是指标准库内部实现的缓存。比方说,你可能见到过,很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来。
|
||||
|
||||
无论缓冲I/O还是非缓冲I/O,它们最终还是要经过系统调用来访问文件。而根据上一节内容,我们知道,系统调用后,还会通过页缓存,来减少磁盘的I/O操作。
|
||||
|
||||
第二,根据是否利用操作系统的页缓存,可以把文件I/O分为直接I/O与非直接I/O。
|
||||
|
||||
<li>
|
||||
直接I/O,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件。
|
||||
</li>
|
||||
<li>
|
||||
非直接I/O正好相反,文件读写时,先要经过系统的页缓存,然后再由内核或额外的系统调用,真正写入磁盘。
|
||||
</li>
|
||||
|
||||
想要实现直接I/O,需要你在系统调用中,指定 O_DIRECT 标志。如果没有设置过,默认的是非直接I/O。
|
||||
|
||||
不过要注意,直接I/O、非直接I/O,本质上还是和文件系统交互。如果是在数据库等场景中,你还会看到,跳过文件系统读写磁盘的情况,也就是我们通常所说的裸I/O。
|
||||
|
||||
第三,根据应用程序是否阻塞自身运行,可以把文件I/O分为阻塞I/O和非阻塞I/O:
|
||||
|
||||
<li>
|
||||
所谓阻塞I/O,是指应用程序执行I/O操作后,如果没有获得响应,就会阻塞当前线程,自然就不能执行其他任务。
|
||||
</li>
|
||||
<li>
|
||||
所谓非阻塞I/O,是指应用程序执行I/O操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果。
|
||||
</li>
|
||||
|
||||
比方说,访问管道或者网络套接字时,设置 O_NONBLOCK 标志,就表示用非阻塞方式访问;而如果不做任何设置,默认的就是阻塞访问。
|
||||
|
||||
第四,根据是否等待响应结果,可以把文件I/O分为同步和异步I/O:
|
||||
|
||||
<li>
|
||||
所谓同步I/O,是指应用程序执行I/O操作后,要一直等到整个I/O完成后,才能获得I/O响应。
|
||||
</li>
|
||||
<li>
|
||||
所谓异步I/O,是指应用程序执行I/O操作后,不用等待完成和完成后的响应,而是继续执行就可以。等到这次 I/O完成后,响应会用事件通知的方式,告诉应用程序。
|
||||
</li>
|
||||
|
||||
举个例子,在操作文件时,如果你设置了 O_SYNC 或者 O_DSYNC 标志,就代表同步I/O。如果设置了O_DSYNC,就要等文件数据写入磁盘后,才能返回;而O_SYNC,则是在O_DSYNC基础上,要求文件元数据也要写入磁盘后,才能返回。
|
||||
|
||||
再比如,在访问管道或者网络套接字时,设置了O_ASYNC选项后,相应的I/O就是异步I/O。这样,内核会再通过SIGIO或者SIGPOLL,来通知进程文件是否可读写。
|
||||
|
||||
你可能发现了,这里的好多概念也经常出现在网络编程中。比如非阻塞I/O,通常会跟select/poll配合,用在网络套接字的I/O中。
|
||||
|
||||
你也应该可以理解,“Linux 一切皆文件”的深刻含义。无论是普通文件和块设备、还是网络套接字和管道等,它们都通过统一的VFS 接口来访问。
|
||||
|
||||
## 性能观测
|
||||
|
||||
学了这么多文件系统的原理,你估计也是迫不及待想上手,观察一下文件系统的性能情况了。
|
||||
|
||||
接下来,打开一个终端,SSH登录到服务器上,然后跟我一起来探索,如何观测文件系统的性能。
|
||||
|
||||
### 容量
|
||||
|
||||
对文件系统来说,最常见的一个问题就是空间不足。当然,你可能本身就知道,用 df 命令,就能查看文件系统的磁盘空间使用情况。比如:
|
||||
|
||||
```
|
||||
$ df /dev/sda1
|
||||
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||
/dev/sda1 30308240 3167020 27124836 11% /
|
||||
|
||||
```
|
||||
|
||||
你可以看到,我的根文件系统只使用了11%的空间。这里还要注意,总空间用1K-blocks的数量来表示,你可以给df加上-h选项,以获得更好的可读性:
|
||||
|
||||
```
|
||||
$ df -h /dev/sda1
|
||||
Filesystem Size Used Avail Use% Mounted on
|
||||
/dev/sda1 29G 3.1G 26G 11% /
|
||||
|
||||
```
|
||||
|
||||
不过有时候,明明你碰到了空间不足的问题,可是用df查看磁盘空间后,却发现剩余空间还有很多。这是怎么回事呢?
|
||||
|
||||
不知道你还记不记得,刚才我强调的一个细节。除了文件数据,索引节点也占用磁盘空间。你可以给df命令加上 -i 参数,查看索引节点的使用情况,如下所示:
|
||||
|
||||
```
|
||||
$ df -i /dev/sda1
|
||||
Filesystem Inodes IUsed IFree IUse% Mounted on
|
||||
/dev/sda1 3870720 157460 3713260 5% /
|
||||
|
||||
```
|
||||
|
||||
索引节点的容量,(也就是Inode个数)是在格式化磁盘时设定好的,一般由格式化工具自动生成。当你发现索引节点空间不足,但磁盘空间充足时,很可能就是过多小文件导致的。
|
||||
|
||||
所以,一般来说,删除这些小文件,或者把它们移动到索引节点充足的其他磁盘中,就可以解决这个问题。
|
||||
|
||||
### 缓存
|
||||
|
||||
在前面Cache案例中,我已经介绍过,可以用 free 或 vmstat,来观察页缓存的大小。复习一下,free输出的Cache,是页缓存和可回收Slab缓存的和,你可以从 /proc/meminfo ,直接得到它们的大小:
|
||||
|
||||
```
|
||||
$ cat /proc/meminfo | grep -E "SReclaimable|Cached"
|
||||
Cached: 748316 kB
|
||||
SwapCached: 0 kB
|
||||
SReclaimable: 179508 kB
|
||||
|
||||
```
|
||||
|
||||
话说回来,文件系统中的目录项和索引节点缓存,又该如何观察呢?
|
||||
|
||||
实际上,内核使用Slab机制,管理目录项和索引节点的缓存。/proc/meminfo只给出了Slab的整体大小,具体到每一种Slab缓存,还要查看/proc/slabinfo这个文件。
|
||||
|
||||
比如,运行下面的命令,你就可以得到,所有目录项和各种文件系统索引节点的缓存情况:
|
||||
|
||||
```
|
||||
$ cat /proc/slabinfo | grep -E '^#|dentry|inode'
|
||||
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
|
||||
xfs_inode 0 0 960 17 4 : tunables 0 0 0 : slabdata 0 0 0
|
||||
...
|
||||
ext4_inode_cache 32104 34590 1088 15 4 : tunables 0 0 0 : slabdata 2306 2306 0hugetlbfs_inode_cache 13 13 624 13 2 : tunables 0 0 0 : slabdata 1 1 0
|
||||
sock_inode_cache 1190 1242 704 23 4 : tunables 0 0 0 : slabdata 54 54 0
|
||||
shmem_inode_cache 1622 2139 712 23 4 : tunables 0 0 0 : slabdata 93 93 0
|
||||
proc_inode_cache 3560 4080 680 12 2 : tunables 0 0 0 : slabdata 340 340 0
|
||||
inode_cache 25172 25818 608 13 2 : tunables 0 0 0 : slabdata 1986 1986 0
|
||||
dentry 76050 121296 192 21 1 : tunables 0 0 0 : slabdata 5776 5776 0
|
||||
|
||||
```
|
||||
|
||||
这个界面中,dentry行表示目录项缓存,inode_cache行,表示VFS索引节点缓存,其余的则是各种文件系统的索引节点缓存。
|
||||
|
||||
/proc/slabinfo 的列比较多,具体含义你可以查询 man slabinfo。在实际性能分析中,我们更常使用 slabtop ,来找到占用内存最多的缓存类型。
|
||||
|
||||
比如,下面就是我运行slabtop得到的结果:
|
||||
|
||||
```
|
||||
# 按下c按照缓存大小排序,按下a按照活跃对象数排序
|
||||
$ slabtop
|
||||
Active / Total Objects (% used) : 277970 / 358914 (77.4%)
|
||||
Active / Total Slabs (% used) : 12414 / 12414 (100.0%)
|
||||
Active / Total Caches (% used) : 83 / 135 (61.5%)
|
||||
Active / Total Size (% used) : 57816.88K / 73307.70K (78.9%)
|
||||
Minimum / Average / Maximum Object : 0.01K / 0.20K / 22.88K
|
||||
|
||||
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
|
||||
69804 23094 0% 0.19K 3324 21 13296K dentry
|
||||
16380 15854 0% 0.59K 1260 13 10080K inode_cache
|
||||
58260 55397 0% 0.13K 1942 30 7768K kernfs_node_cache
|
||||
485 413 0% 5.69K 97 5 3104K task_struct
|
||||
1472 1397 0% 2.00K 92 16 2944K kmalloc-2048
|
||||
|
||||
```
|
||||
|
||||
从这个结果你可以看到,在我的系统中,目录项和索引节点占用了最多的Slab缓存。不过它们占用的内存其实并不大,加起来也只有23MB左右。
|
||||
|
||||
## 小结
|
||||
|
||||
今天,我带你梳理了Linux文件系统的工作原理。
|
||||
|
||||
文件系统,是对存储设备上的文件,进行组织管理的一种机制。为了支持各类不同的文件系统,Linux在各种文件系统实现上,抽象了一层虚拟文件系统(VFS)。
|
||||
|
||||
VFS 定义了一组所有文件系统都支持的数据结构和标准接口。这样,用户进程和内核中的其他子系统,就只需要跟 VFS 提供的统一接口进行交互。
|
||||
|
||||
为了降低慢速磁盘对性能的影响,文件系统又通过页缓存、目录项缓存以及索引节点缓存,缓和磁盘延迟对应用程序的影响。
|
||||
|
||||
在性能观测方面,今天主要讲了容量和缓存的指标。下一节,我们将会学习Linux磁盘 I/O的工作原理,并掌握磁盘I/O的性能观测方法。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,给你留一个思考题。在实际工作中,我们经常会根据文件名字,查找它所在路径,比如:
|
||||
|
||||
```
|
||||
$ find / -name file-name
|
||||
|
||||
```
|
||||
|
||||
今天的问题就是,这个命令,会不会导致系统的缓存升高呢?如果有影响,又会导致哪种类型的缓存升高呢?你可以结合今天内容,自己先去操作和分析,看看观察到的结果跟你分析的是否一样。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
152
极客时间专栏/Linux性能优化实战/I|O 性能篇/24 | 基础篇:Linux 磁盘I|O是怎么工作的(上).md
Normal file
152
极客时间专栏/Linux性能优化实战/I|O 性能篇/24 | 基础篇:Linux 磁盘I|O是怎么工作的(上).md
Normal file
@@ -0,0 +1,152 @@
|
||||
<audio id="audio" title="24 | 基础篇:Linux 磁盘I/O是怎么工作的(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/a8/75059dfe1178fb7fd7a2ff95b0e775a8.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一节,我们学习了 Linux 文件系统的工作原理。简单回顾一下,文件系统是对存储设备上的文件,进行组织管理的一种机制。而Linux 在各种文件系统实现上,又抽象了一层虚拟文件系统VFS,它定义了一组,所有文件系统都支持的,数据结构和标准接口。
|
||||
|
||||
这样,对应用程序来说,只需要跟 VFS 提供的统一接口交互,而不需要关注文件系统的具体实现;对具体的文件系统来说,只需要按照 VFS 的标准,就可以无缝支持各种应用程序。
|
||||
|
||||
VFS 内部又通过目录项、索引节点、逻辑块以及超级块等数据结构,来管理文件。
|
||||
|
||||
<li>
|
||||
目录项,记录了文件的名字,以及文件与其他目录项之间的目录关系。
|
||||
</li>
|
||||
<li>
|
||||
索引节点,记录了文件的元数据。
|
||||
</li>
|
||||
<li>
|
||||
逻辑块,是由连续磁盘扇区构成的最小读写单元,用来存储文件数据。
|
||||
</li>
|
||||
<li>
|
||||
超级块,用来记录文件系统整体的状态,如索引节点和逻辑块的使用情况等。
|
||||
</li>
|
||||
|
||||
其中,目录项是一个内存缓存;而超级块、索引节点和逻辑块,都是存储在磁盘中的持久化数据。
|
||||
|
||||
那么,进一步想,磁盘又是怎么工作的呢?又有哪些指标可以用来衡量它的性能呢?
|
||||
|
||||
接下来,我就带你一起看看, Linux 磁盘I/O的工作原理。
|
||||
|
||||
## 磁盘
|
||||
|
||||
磁盘是可以持久化存储的设备,根据存储介质的不同,常见磁盘可以分为两类:机械磁盘和固态磁盘。
|
||||
|
||||
第一类,机械磁盘,也称为硬盘驱动器(Hard Disk Driver),通常缩写为 HDD。机械磁盘主要由盘片和读写磁头组成,数据就存储在盘片的环状磁道中。在读写数据前,需要移动读写磁头,定位到数据所在的磁道,然后才能访问数据。
|
||||
|
||||
显然,如果 I/O 请求刚好连续,那就不需要磁道寻址,自然可以获得最佳性能。这其实就是我们熟悉的,连续 I/O的工作原理。与之相对应的,当然就是随机 I/O,它需要不停地移动磁头,来定位数据位置,所以读写速度就会比较慢。
|
||||
|
||||
第二类,固态磁盘(Solid State Disk),通常缩写为SSD,由固态电子元器件组成。固态磁盘不需要磁道寻址,所以,不管是连续I/O,还是随机I/O的性能,都比机械磁盘要好得多。
|
||||
|
||||
其实,无论机械磁盘,还是固态磁盘,相同磁盘的随机 I/O 都要比连续 I/O 慢很多,原因也很明显。
|
||||
|
||||
<li>
|
||||
对机械磁盘来说,我们刚刚提到过的,由于随机I/O需要更多的磁头寻道和盘片旋转,它的性能自然要比连续I/O慢。
|
||||
</li>
|
||||
<li>
|
||||
而对固态磁盘来说,虽然它的随机性能比机械硬盘好很多,但同样存在“先擦除再写入”的限制。随机读写会导致大量的垃圾回收,所以相对应的,随机I/O的性能比起连续I/O来,也还是差了很多。
|
||||
</li>
|
||||
<li>
|
||||
此外,连续I/O还可以通过预读的方式,来减少I/O请求的次数,这也是其性能优异的一个原因。很多性能优化的方案,也都会从这个角度出发,来优化I/O性能。
|
||||
</li>
|
||||
|
||||
此外,机械磁盘和固态磁盘还分别有一个最小的读写单位。
|
||||
|
||||
<li>
|
||||
机械磁盘的最小读写单位是扇区,一般大小为512字节。
|
||||
</li>
|
||||
<li>
|
||||
而固态磁盘的最小读写单位是页,通常大小是4KB、8KB等。
|
||||
</li>
|
||||
|
||||
在上一节中,我也提到过,如果每次都读写 512 字节这么小的单位的话,效率很低。所以,文件系统会把连续的扇区或页,组成逻辑块,然后以逻辑块作为最小单元来管理数据。常见的逻辑块的大小是4KB,也就是说,连续8个扇区,或者单独的一个页,都可以组成一个逻辑块。
|
||||
|
||||
除了可以按照存储介质来分类,另一个常见的分类方法,是按照接口来分类,比如可以把硬盘分为 IDE(Integrated Drive Electronics)、SCSI(Small Computer System Interface) 、SAS(Serial Attached SCSI) 、SATA(Serial ATA) 、FC(Fibre Channel) 等。
|
||||
|
||||
不同的接口,往往分配不同的设备名称。比如, IDE 设备会分配一个 hd 前缀的设备名,SCSI和SATA设备会分配一个 sd 前缀的设备名。如果是多块同类型的磁盘,就会按照a、b、c等的字母顺序来编号。
|
||||
|
||||
除了磁盘本身的分类外,当你把磁盘接入服务器后,按照不同的使用方式,又可以把它们划分为多种不同的架构。
|
||||
|
||||
最简单的,就是直接作为独立磁盘设备来使用。这些磁盘,往往还会根据需要,划分为不同的逻辑分区,每个分区再用数字编号。比如我们前面多次用到的 /dev/sda ,还可以分成两个分区 /dev/sda1和/dev/sda2。
|
||||
|
||||
另一个比较常用的架构,是把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列,也就是RAID(Redundant Array of Independent Disks),从而可以提高数据访问的性能,并且增强数据存储的可靠性。
|
||||
|
||||
根据容量、性能和可靠性需求的不同,RAID一般可以划分为多个级别,如RAID0、RAID1、RAID5、RAID10等。
|
||||
|
||||
<li>
|
||||
RAID0有最优的读写性能,但不提供数据冗余的功能。
|
||||
</li>
|
||||
<li>
|
||||
而其他级别的RAID,在提供数据冗余的基础上,对读写性能也有一定程度的优化。
|
||||
</li>
|
||||
|
||||
最后一种架构,是把这些磁盘组合成一个网络存储集群,再通过NFS、SMB、iSCSI等网络存储协议,暴露给服务器使用。
|
||||
|
||||
其实在 Linux 中,**磁盘实际上是作为一个块设备来管理的**,也就是以块为单位读写数据,并且支持随机读写。每个块设备都会被赋予两个设备号,分别是主、次设备号。主设备号用在驱动程序中,用来区分设备类型;而次设备号则是用来给多个同类设备编号。
|
||||
|
||||
## 通用块层
|
||||
|
||||
跟我们上一节讲到的虚拟文件系统VFS类似,为了减小不同块设备的差异带来的影响,Linux 通过一个统一的通用块层,来管理各种不同的块设备。
|
||||
|
||||
通用块层,其实是处在文件系统和磁盘驱动中间的一个块设备抽象层。它主要有两个功能 。
|
||||
|
||||
<li>
|
||||
第一个功能跟虚拟文件系统的功能类似。向上,为文件系统和应用程序,提供访问块设备的标准接口;向下,把各种异构的磁盘设备抽象为统一的块设备,并提供统一框架来管理这些设备的驱动程序。
|
||||
</li>
|
||||
<li>
|
||||
第二个功能,通用块层还会给文件系统和应用程序发来的 I/O 请求排队,并通过重新排序、请求合并等方式,提高磁盘读写的效率。
|
||||
</li>
|
||||
|
||||
其中,对 I/O 请求排序的过程,也就是我们熟悉的 I/O 调度。事实上,Linux 内核支持四种I/O调度算法,分别是NONE、NOOP、CFQ以及DeadLine。这里我也分别介绍一下。
|
||||
|
||||
第一种 NONE ,更确切来说,并不能算 I/O 调度算法。因为它完全不使用任何I/O调度器,对文件系统和应用程序的I/O其实不做任何处理,常用在虚拟机中(此时磁盘I/O调度完全由物理机负责)。
|
||||
|
||||
第二种 NOOP ,是最简单的一种 I/O 调度算法。它实际上是一个先入先出的队列,只做一些最基本的请求合并,常用于 SSD 磁盘。
|
||||
|
||||
第三种 CFQ(Completely Fair Scheduler),也被称为完全公平调度器,是现在很多发行版的默认 I/O 调度器,它为每个进程维护了一个 I/O 调度队列,并按照时间片来均匀分布每个进程的 I/O 请求。
|
||||
|
||||
类似于进程 CPU 调度,CFQ 还支持进程 I/O 的优先级调度,所以它适用于运行大量进程的系统,像是桌面环境、多媒体应用等。
|
||||
|
||||
最后一种 DeadLine 调度算法,分别为读、写请求创建了不同的 I/O 队列,可以提高机械磁盘的吞吐量,并确保达到最终期限(deadline)的请求被优先处理。DeadLine 调度算法,多用在 I/O 压力比较重的场景,比如数据库等。
|
||||
|
||||
## I/O栈
|
||||
|
||||
清楚了磁盘和通用块层的工作原理,再结合上一期我们讲过的文件系统原理,我们就可以整体来看Linux存储系统的 I/O 原理了。
|
||||
|
||||
我们可以把Linux 存储系统的 I/O 栈,由上到下分为三个层次,分别是文件系统层、通用块层和设备层。这三个I/O层的关系如下图所示,这其实也是 Linux 存储系统的 I/O 栈全景图。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/14/b1/14bc3d26efe093d3eada173f869146b1.png" alt=""><br>
|
||||
(图片来自 [Linux Storage Stack Diagram](https://www.thomas-krenn.com/en/wiki/Linux_Storage_Stack_Diagram) )
|
||||
|
||||
根据这张 I/O 栈的全景图,我们可以更清楚地理解,存储系统 I/O 的工作原理。
|
||||
|
||||
<li>
|
||||
文件系统层,包括虚拟文件系统和其他各种文件系统的具体实现。它为上层的应用程序,提供标准的文件访问接口;对下会通过通用块层,来存储和管理磁盘数据。
|
||||
</li>
|
||||
<li>
|
||||
通用块层,包括块设备 I/O 队列和 I/O 调度器。它会对文件系统的 I/O 请求进行排队,再通过重新排序和请求合并,然后才要发送给下一级的设备层。
|
||||
</li>
|
||||
<li>
|
||||
设备层,包括存储设备和相应的驱动程序,负责最终物理设备的I/O操作。
|
||||
</li>
|
||||
|
||||
存储系统的 I/O ,通常是整个系统中最慢的一环。所以, Linux 通过多种缓存机制来优化 I/O 效率。
|
||||
|
||||
比方说,为了优化文件访问的性能,会使用页缓存、索引节点缓存、目录项缓存等多种缓存机制,以减少对下层块设备的直接调用。
|
||||
|
||||
同样,为了优化块设备的访问效率,会使用缓冲区,来缓存块设备的数据。
|
||||
|
||||
不过,抽象的原理讲了这么多,具体操作起来,应该怎么衡量磁盘的 I/O 性能呢?我先卖个关子,下节课我们一起来看,最常用的磁盘 I/O 性能指标,以及 I/O 性能工具。
|
||||
|
||||
## 小结
|
||||
|
||||
在今天的文章中,我们梳理了 Linux 磁盘 I/O 的工作原理,并了解了由文件系统层、通用块层和设备层构成的 Linux 存储系统 I/O 栈。
|
||||
|
||||
其中,通用块层是 Linux 磁盘 I/O 的核心。向上,它为文件系统和应用程序,提供访问了块设备的标准接口;向下,把各种异构的磁盘设备,抽象为统一的块设备,并会对文件系统和应用程序发来的 I/O 请求进行重新排序、请求合并等,提高了磁盘访问的效率。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想邀请你一起来聊聊,你所理解的磁盘 I/O。我相信你很可能已经碰到过,文件或者磁盘的 I/O 性能问题,你是怎么分析这些问题的呢?你可以结合今天的磁盘 I/O 原理和上一节的文件系统原理,记录你的操作步骤,并总结出自己的思路。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
155
极客时间专栏/Linux性能优化实战/I|O 性能篇/25 | 基础篇:Linux 磁盘I|O是怎么工作的(下).md
Normal file
155
极客时间专栏/Linux性能优化实战/I|O 性能篇/25 | 基础篇:Linux 磁盘I|O是怎么工作的(下).md
Normal file
@@ -0,0 +1,155 @@
|
||||
<audio id="audio" title="25 | 基础篇:Linux 磁盘I/O是怎么工作的(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/74/14/7441a367d3701e7b5897132699baf314.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一节我们学习了 Linux 磁盘 I/O 的工作原理,并了解了由文件系统层、通用块层和设备层构成的 Linux 存储系统 I/O 栈。
|
||||
|
||||
其中,通用块层是 Linux 磁盘 I/O 的核心。向上,它为文件系统和应用程序,提供访问了块设备的标准接口;向下,把各种异构的磁盘设备,抽象为统一的块设备,并会对文件系统和应用程序发来的 I/O 请求,进行重新排序、请求合并等,提高了磁盘访问的效率。
|
||||
|
||||
掌握了磁盘 I/O 的工作原理,你估计迫不及待想知道,怎么才能衡量磁盘的 I/O 性能。
|
||||
|
||||
接下来,我们就来看看,磁盘的性能指标,以及观测这些指标的方法。
|
||||
|
||||
## 磁盘性能指标
|
||||
|
||||
说到磁盘性能的衡量标准,必须要提到五个常见指标,也就是我们经常用到的,使用率、饱和度、IOPS、吞吐量以及响应时间等。这五个指标,是衡量磁盘性能的基本指标。
|
||||
|
||||
<li>
|
||||
使用率,是指磁盘处理I/O的时间百分比。过高的使用率(比如超过80%),通常意味着磁盘 I/O 存在性能瓶颈。
|
||||
</li>
|
||||
<li>
|
||||
饱和度,是指磁盘处理 I/O 的繁忙程度。过高的饱和度,意味着磁盘存在严重的性能瓶颈。当饱和度为 100% 时,磁盘无法接受新的 I/O 请求。
|
||||
</li>
|
||||
<li>
|
||||
IOPS(Input/Output Per Second),是指每秒的 I/O 请求数。
|
||||
</li>
|
||||
<li>
|
||||
吞吐量,是指每秒的 I/O 请求大小。
|
||||
</li>
|
||||
<li>
|
||||
响应时间,是指 I/O 请求从发出到收到响应的间隔时间。
|
||||
</li>
|
||||
|
||||
这里要注意的是,使用率只考虑有没有 I/O,而不考虑 I/O 的大小。换句话说,当使用率是 100% 的时候,磁盘依然有可能接受新的 I/O 请求。
|
||||
|
||||
这些指标,很可能是你经常挂在嘴边的,一讨论磁盘性能必定提起的对象。不过我还是要强调一点,不要孤立地去比较某一指标,而要结合读写比例、I/O类型(随机还是连续)以及 I/O 的大小,综合来分析。
|
||||
|
||||
举个例子,在数据库、大量小文件等这类随机读写比较多的场景中,IOPS 更能反映系统的整体性能;而在多媒体等顺序读写较多的场景中,吞吐量才更能反映系统的整体性能。
|
||||
|
||||
一般来说,我们在为应用程序的服务器选型时,要先对磁盘的 I/O 性能进行基准测试,以便可以准确评估,磁盘性能是否可以满足应用程序的需求。
|
||||
|
||||
这一方面,我推荐用性能测试工具 fio ,来测试磁盘的IOPS、吞吐量以及响应时间等核心指标。但还是那句话,因地制宜,灵活选取。在基准测试时,一定要注意根据应用程序 I/O 的特点,来具体评估指标。
|
||||
|
||||
当然,这就需要你测试出,不同 I/O 大小(一般是 512B 至 1MB 中间的若干值)分别在随机读、顺序读、随机写、顺序写等各种场景下的性能情况。
|
||||
|
||||
用性能工具得到的这些指标,可以作为后续分析应用程序性能的依据。一旦发生性能问题,你就可以把它们作为磁盘性能的极限值,进而评估磁盘 I/O 的使用情况。
|
||||
|
||||
了解磁盘的性能指标,只是我们I/O性能测试的第一步。接下来,又该用什么方法来观测它们呢?这里,我给你介绍几个常用的I/O性能观测方法。
|
||||
|
||||
## **磁盘I/O观测**
|
||||
|
||||
第一个要观测的,是每块磁盘的使用情况。
|
||||
|
||||
iostat 是最常用的磁盘I/O性能观测工具,它提供了每个磁盘的使用率、IOPS、吞吐量等各种常见的性能指标,当然,这些指标实际上来自 /proc/diskstats。
|
||||
|
||||
iostat 的输出界面如下。
|
||||
|
||||
```
|
||||
# -d -x表示显示所有磁盘I/O的指标
|
||||
$ iostat -d -x 1
|
||||
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
|
||||
loop0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
|
||||
loop1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
|
||||
sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
|
||||
sdb 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
|
||||
|
||||
```
|
||||
|
||||
从这里你可以看到,iostat 提供了非常丰富的性能指标。第一列的 Device 表示磁盘设备的名字,其他各列指标,虽然数量较多,但是每个指标的含义都很重要。为了方便你理解,我把它们总结成了一个表格。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cf/8d/cff31e715af51c9cb8085ce1bb48318d.png" alt="">
|
||||
|
||||
这些指标中,你要注意:
|
||||
|
||||
<li>
|
||||
%util ,就是我们前面提到的磁盘I/O使用率;
|
||||
</li>
|
||||
<li>
|
||||
r/s+ w/s ,就是 IOPS;
|
||||
</li>
|
||||
<li>
|
||||
rkB/s+wkB/s ,就是吞吐量;
|
||||
</li>
|
||||
<li>
|
||||
r_await+w_await ,就是响应时间。
|
||||
</li>
|
||||
|
||||
在观测指标时,也别忘了结合请求的大小( rareq-sz 和wareq-sz)一起分析。
|
||||
|
||||
你可能注意到,从 iostat 并不能直接得到磁盘饱和度。事实上,饱和度通常也没有其他简单的观测方法,不过,你可以把观测到的,平均请求队列长度或者读写请求完成的等待时间,跟基准测试的结果(比如通过 fio)进行对比,综合评估磁盘的饱和情况。
|
||||
|
||||
## **进程I/O观测**
|
||||
|
||||
除了每块磁盘的 I/O 情况,每个进程的 I/O 情况也是我们需要关注的重点。
|
||||
|
||||
上面提到的 iostat 只提供磁盘整体的 I/O 性能数据,缺点在于,并不能知道具体是哪些进程在进行磁盘读写。要观察进程的I/O情况,你还可以使用 pidstat 和 iotop 这两个工具。
|
||||
|
||||
pidstat 是我们的老朋友了,这里我就不再啰嗦它的功能了。给它加上 -d 参数,你就可以看到进程的I/O情况,如下所示:
|
||||
|
||||
```
|
||||
$ pidstat -d 1
|
||||
13:39:51 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
13:39:52 102 916 0.00 4.00 0.00 0 rsyslogd
|
||||
|
||||
```
|
||||
|
||||
从pidstat的输出你能看到,它可以实时查看每个进程的I/O情况,包括下面这些内容。
|
||||
|
||||
<li>
|
||||
用户ID(UID)和进程ID(PID) 。
|
||||
</li>
|
||||
<li>
|
||||
每秒读取的数据大小(kB_rd/s) ,单位是 KB。
|
||||
</li>
|
||||
<li>
|
||||
每秒发出的写请求数据大小(kB_wr/s) ,单位是 KB。
|
||||
</li>
|
||||
<li>
|
||||
每秒取消的写请求数据大小(kB_ccwr/s) ,单位是 KB。
|
||||
</li>
|
||||
<li>
|
||||
块I/O延迟(iodelay),包括等待同步块I/O和换入块I/O结束的时间,单位是时钟周期。
|
||||
</li>
|
||||
|
||||
除了可以用 pidstat 实时查看,根据 I/O 大小对进程排序,也是性能分析中一个常用的方法。这一点,我推荐另一个工具, iotop。它是一个类似于 top 的工具,你可以按照 I/O 大小对进程排序,然后找到I/O较大的那些进程。
|
||||
|
||||
iotop 的输出如下所示:
|
||||
|
||||
```
|
||||
$ iotop
|
||||
Total DISK READ : 0.00 B/s | Total DISK WRITE : 7.85 K/s
|
||||
Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 0.00 B/s
|
||||
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
|
||||
15055 be/3 root 0.00 B/s 7.85 K/s 0.00 % 0.00 % systemd-journald
|
||||
|
||||
```
|
||||
|
||||
从这个输出,你可以看到,前两行分别表示,进程的磁盘读写大小总数和磁盘真实的读写大小总数。因为缓存、缓冲区、I/O合并等因素的影响,它们可能并不相等。
|
||||
|
||||
剩下的部分,则是从各个角度来分别表示进程的I/O情况,包括线程ID、I/O优先级、每秒读磁盘的大小、每秒写磁盘的大小、换入和等待I/O的时钟百分比等。
|
||||
|
||||
这两个工具,是我们分析磁盘 I/O 性能时最常用到的。你先了解它们的功能和指标含义,具体的使用方法,接下来的案例实战中我们一起学习。
|
||||
|
||||
## 小结
|
||||
|
||||
今天,我们梳理了 Linux 磁盘 I/O 的性能指标和性能工具。我们通常用IOPS、吞吐量、使用率、饱和度以及响应时间等几个指标,来评估磁盘的 I/O 性能。
|
||||
|
||||
你可以用 iostat 获得磁盘的 I/O 情况,也可以用 pidstat、iotop 等观察进程的 I/O 情况。不过在分析这些性能指标时,你要注意结合读写比例、I/O 类型以及 I/O 大小等,进行综合分析。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想请你一起来聊聊,你碰到过的磁盘 I/O 问题。在碰到磁盘 I/O 性能问题时,你是怎么分析和定位的呢?你可以结合今天学到的磁盘 I/O 指标和工具,以及上一节学过的磁盘 I/O 原理,来总结你的思路。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
299
极客时间专栏/Linux性能优化实战/I|O 性能篇/26 | 案例篇:如何找出狂打日志的“内鬼”?.md
Normal file
299
极客时间专栏/Linux性能优化实战/I|O 性能篇/26 | 案例篇:如何找出狂打日志的“内鬼”?.md
Normal file
@@ -0,0 +1,299 @@
|
||||
<audio id="audio" title="26 | 案例篇:如何找出狂打日志的“内鬼”?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/62/8a/62f4d5f6ab23e928dc35143379ddb08a.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
前两节,我们学了文件系统和磁盘的 I/O 原理,我先带你复习一下。
|
||||
|
||||
文件系统,是对存储设备上的文件进行组织管理的一种机制。为了支持各类不同的文件系统,Linux在各种文件系统上,抽象了一层虚拟文件系统VFS。
|
||||
|
||||
它定义了一组所有文件系统都支持的数据结构和标准接口。这样,应用程序和内核中的其他子系统,就只需要跟 VFS 提供的统一接口进行交互。
|
||||
|
||||
在文件系统的下层,为了支持各种不同类型的存储设备,Linux又在各种存储设备的基础上,抽象了一个通用块层。
|
||||
|
||||
通用块层,为文件系统和应用程序提供了访问块设备的标准接口;同时,为各种块设备的驱动程序提供了统一的框架。此外,通用块层还会对文件系统和应用程序发送过来的 I/O 请求进行排队,并通过重新排序、请求合并等方式,提高磁盘读写的效率。
|
||||
|
||||
通用块层的下一层,自然就是设备层了,包括各种块设备的驱动程序以及物理存储设备。
|
||||
|
||||
文件系统、通用块层以及设备层,就构成了 Linux 的存储 I/O 栈。存储系统的 I/O ,通常是整个系统中最慢的一环。所以,Linux 采用多种缓存机制,来优化 I/O 的效率,比方说,
|
||||
|
||||
<li>
|
||||
为了优化文件访问的性能,采用页缓存、索引节点缓存、目录项缓存等多种缓存机制,减少对下层块设备的直接调用。
|
||||
</li>
|
||||
<li>
|
||||
同样的,为了优化块设备的访问效率,使用缓冲区来缓存块设备的数据。
|
||||
</li>
|
||||
|
||||
不过,在碰到文件系统和磁盘的 I/O 问题时,具体应该怎么定位和分析呢?今天,我就以一个最常见的应用程序记录大量日志的案例,带你来分析这种情况。
|
||||
|
||||
## 案例准备
|
||||
|
||||
本次案例还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
|
||||
|
||||
<li>
|
||||
机器配置:2 CPU,8GB 内存
|
||||
</li>
|
||||
<li>
|
||||
预先安装 docker、sysstat 等工具,如 apt install [docker.io](http://docker.io/) sysstat
|
||||
</li>
|
||||
|
||||
这里要感谢唯品会资深运维工程师阳祥义帮忙,分担了今天的案例。这个案例,是一个用 Python 开发的小应用,为了方便运行,我把它打包成了一个 Docker 镜像。这样,你只要运行 Docker 命令,就可以启动它。
|
||||
|
||||
接下来,打开一个终端,SSH 登录到案例所用的机器中,并安装上述工具。跟以前一样,案例中所有命令,都默认以 root 用户运行。如果你是用普通用户身份登陆系统,请运行 sudo su root 命令,切换到 root 用户。
|
||||
|
||||
到这里,准备工作就完成了。接下来,我们正式进入操作环节。
|
||||
|
||||
>
|
||||
温馨提示:案例中 Python 应用的核心逻辑比较简单,你可能一眼就能看出问题,但实际生产环境中的源码就复杂多了。所以,我依旧建议,操作之前别看源码,避免先入为主,要把它当成一个黑盒来分析。这样,你可以更好把握住,怎么从系统的资源使用问题出发,分析出瓶颈所在的应用,以及瓶颈在应用中大概的位置。
|
||||
|
||||
|
||||
## 案例分析
|
||||
|
||||
首先,我们在终端中执行下面的命令,运行今天的目标应用:
|
||||
|
||||
```
|
||||
$ docker run -v /tmp:/tmp --name=app -itd feisky/logapp
|
||||
|
||||
```
|
||||
|
||||
然后,在终端中运行 ps 命令,确认案例应用正常启动。如果操作无误,你应该可以在 ps 的输出中,看到一个 app.py 的进程:
|
||||
|
||||
```
|
||||
$ ps -ef | grep /app.py
|
||||
root 18940 18921 73 14:41 pts/0 00:00:02 python /app.py
|
||||
|
||||
```
|
||||
|
||||
接着,我们来看看系统有没有性能问题。要观察哪些性能指标呢?前面文章中,我们知道 CPU、内存和磁盘 I/O 等系统资源,很容易出现资源瓶颈,这就是我们观察的方向了。我们来观察一下这些资源的使用情况。
|
||||
|
||||
当然,动手之前你应该想清楚,要用哪些工具来做,以及工具的使用顺序又是怎样的。你可以先回忆下前面的案例和思路,自己想一想,然后再继续下面的步骤。
|
||||
|
||||
我的想法是,我们可以先用 top ,来观察 CPU 和内存的使用情况;然后再用 iostat ,来观察磁盘的 I/O 情况。
|
||||
|
||||
所以,接下来,你可以在终端中运行 top 命令,观察 CPU 和内存的使用情况:
|
||||
|
||||
```
|
||||
# 按1切换到每个CPU的使用情况
|
||||
$ top
|
||||
top - 14:43:43 up 1 day, 1:39, 2 users, load average: 2.48, 1.09, 0.63
|
||||
Tasks: 130 total, 2 running, 74 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 0.7 us, 6.0 sy, 0.0 ni, 0.7 id, 92.7 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu1 : 0.0 us, 0.3 sy, 0.0 ni, 92.3 id, 7.3 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
KiB Mem : 8169308 total, 747684 free, 741336 used, 6680288 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 7113124 avail Mem
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
18940 root 20 0 656108 355740 5236 R 6.3 4.4 0:12.56 python
|
||||
1312 root 20 0 236532 24116 9648 S 0.3 0.3 9:29.80 python3
|
||||
|
||||
```
|
||||
|
||||
观察 top 的输出,你会发现,CPU0 的使用率非常高,它的系统CPU使用率(sys%)为 6%,而 iowait 超过了 90%。这说明 CPU0 上,可能正在运行 I/O 密集型的进程。不过,究竟是什么原因呢?这个疑问先保留着,我们先继续看完。
|
||||
|
||||
接着我们来看,进程部分的 CPU 使用情况。你会发现, python 进程的 CPU 使用率已经达到了 6%,而其余进程的 CPU 使用率都比较低,不超过 0.3%。看起来 python 是个可疑进程。记下 python 进程的 PID 号 18940,我们稍后分析。
|
||||
|
||||
最后再看内存的使用情况,总内存 8G,剩余内存只有 730 MB,而 Buffer/Cache占用内存高达6GB 之多,这说明内存主要被缓存占用。虽然大部分缓存可回收,我们还是得了解下缓存的去处,确认缓存使用都是合理的。
|
||||
|
||||
到这一步,你基本可以判断出,CPU 使用率中的 iowait 是一个潜在瓶颈,而内存部分的缓存占比较大,那磁盘 I/O 又是怎么样的情况呢?
|
||||
|
||||
我们在终端中按 Ctrl+C ,停止 top 命令,再运行 iostat 命令,观察 I/O 的使用情况:
|
||||
|
||||
```
|
||||
# -d表示显示I/O性能指标,-x表示显示扩展统计(即所有I/O指标)
|
||||
$ iostat -x -d 1
|
||||
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
|
||||
loop0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
|
||||
sdb 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
|
||||
sda 0.00 64.00 0.00 32768.00 0.00 0.00 0.00 0.00 0.00 7270.44 1102.18 0.00 512.00 15.50 99.20
|
||||
|
||||
```
|
||||
|
||||
还记得这些性能指标的含义吗?先自己回忆一下,如果实在想不起来,查看上一节的内容,或者用 man iostat 查询。
|
||||
|
||||
观察 iostat 的最后一列,你会看到,磁盘 sda 的 I/O 使用率已经高达 99%,很可能已经接近 I/O 饱和。
|
||||
|
||||
再看前面的各个指标,每秒写磁盘请求数是 64 ,写大小是 32 MB,写请求的响应时间为 7 秒,而请求队列长度则达到了 1100。
|
||||
|
||||
超慢的响应时间和特长的请求队列长度,进一步验证了 I/O 已经饱和的猜想。此时,sda 磁盘已经遇到了严重的性能瓶颈。
|
||||
|
||||
到这里,也就可以理解,为什么前面看到的 iowait 高达 90% 了,这正是磁盘 sda 的 I/O 瓶颈导致的。接下来的重点就是分析 I/O 性能瓶颈的根源了。那要怎么知道,这些 I/O请求相关的进程呢?
|
||||
|
||||
不知道你还记不记得,上一节我曾提到过,可以用 pidstat 或者 iotop ,观察进程的 I/O 情况。这里,我就用 pidstat 来看一下。
|
||||
|
||||
使用 pidstat 加上 -d 参数,就可以显示每个进程的 I/O 情况。所以,你可以在终端中运行如下命令来观察:
|
||||
|
||||
```
|
||||
$ pidstat -d 1
|
||||
|
||||
15:08:35 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
15:08:36 0 18940 0.00 45816.00 0.00 96 python
|
||||
|
||||
15:08:36 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
15:08:37 0 354 0.00 0.00 0.00 350 jbd2/sda1-8
|
||||
15:08:37 0 18940 0.00 46000.00 0.00 96 python
|
||||
15:08:37 0 20065 0.00 0.00 0.00 1503 kworker/u4:2
|
||||
|
||||
```
|
||||
|
||||
从 pidstat 的输出,你可以发现,只有 python 进程的写比较大,而且每秒写的数据超过 45 MB,比上面 iostat 发现的 32MB 的结果还要大。很明显,正是 python 进程导致了 I/O 瓶颈。
|
||||
|
||||
再往下看 iodelay 项。虽然只有 python 在大量写数据,但你应该注意到了,有两个进程 (kworker 和 jbd2 )的延迟,居然比 python 进程还大很多。
|
||||
|
||||
这其中,kworker 是一个内核线程,而 jbd2 是 ext4 文件系统中,用来保证数据完整性的内核线程。他们都是保证文件系统基本功能的内核线程,所以具体细节暂时就不用管了,我们只需要明白,它们延迟的根源还是大量 I/O。
|
||||
|
||||
综合pidstat的输出来看,还是python进程的嫌疑最大。接下来,我们来分析 python 进程到底在写什么。
|
||||
|
||||
首先留意一下 python 进程的 PID 号, 18940。看到 18940 ,你有没有觉得熟悉?其实前面在使用top时,我们记录过的 CPU 使用率最高的进程,也正是它。不过,虽然在top中使用率最高,也不过是 6%,并不算高。所以,以I/O问题为分析方向还是正确的。
|
||||
|
||||
知道了进程的 PID 号,具体要怎么查看写的情况呢?
|
||||
|
||||
其实,我在系统调用的案例中讲过,读写文件必须通过系统调用完成。观察系统调用情况,就可以知道进程正在写的文件。想起 strace 了吗,它正是我们分析系统调用时最常用的工具。
|
||||
|
||||
接下来,我们在终端中运行strace 命令,并通过 -p 18940 指定 python 进程的 PID 号:
|
||||
|
||||
```
|
||||
$ strace -p 18940
|
||||
strace: Process 18940 attached
|
||||
...
|
||||
mmap(NULL, 314576896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0f7aee9000
|
||||
mmap(NULL, 314576896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0f682e8000
|
||||
write(3, "2018-12-05 15:23:01,709 - __main"..., 314572844
|
||||
) = 314572844
|
||||
munmap(0x7f0f682e8000, 314576896) = 0
|
||||
write(3, "\n", 1) = 1
|
||||
munmap(0x7f0f7aee9000, 314576896) = 0
|
||||
close(3) = 0
|
||||
stat("/tmp/logtest.txt.1", {st_mode=S_IFREG|0644, st_size=943718535, ...}) = 0
|
||||
|
||||
```
|
||||
|
||||
从 write() 系统调用上,我们可以看到,进程向文件描述符编号为 3 的文件中,写入了 300MB 的数据。看来,它应该是我们要找的文件。不过,write() 调用中只能看到文件的描述符编号,文件名和路径还是未知的。
|
||||
|
||||
再观察后面的 stat() 调用,你可以看到,它正在获取 /tmp/logtest.txt.1 的状态。 这种“点+数字格式”的文件,在日志回滚中非常常见。我们可以猜测,这是第一个日志回滚文件,而正在写的日志文件路径,则是/tmp/logtest.txt。
|
||||
|
||||
当然,这只是我们的猜测,自然还需要验证。这里,我再给你介绍一个新的工具 lsof。它专门用来查看进程打开文件列表,不过,这里的“文件”不只有普通文件,还包括了目录、块设备、动态库、网络套接字等。
|
||||
|
||||
接下来,我们在终端中运行下面的 lsof 命令,看看进程 18940 都打开了哪些文件:
|
||||
|
||||
```
|
||||
$ lsof -p 18940
|
||||
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
|
||||
python 18940 root cwd DIR 0,50 4096 1549389 /
|
||||
python 18940 root rtd DIR 0,50 4096 1549389 /
|
||||
…
|
||||
python 18940 root 2u CHR 136,0 0t0 3 /dev/pts/0
|
||||
python 18940 root 3w REG 8,1 117944320 303 /tmp/logtest.txt
|
||||
|
||||
```
|
||||
|
||||
这个输出界面中,有几列我简单介绍一下,FD 表示文件描述符号,TYPE 表示文件类型,NAME 表示文件路径。这也是我们需要关注的重点。
|
||||
|
||||
再看最后一行,这说明,这个进程打开了文件 /tmp/logtest.txt,并且它的文件描述符是 3 号,而3 后面的 w ,表示以写的方式打开。
|
||||
|
||||
这跟刚才 strace 完我们猜测的结果一致,看来这就是问题的根源:进程 18940 以每次 300MB 的速度,在“疯狂”写日志,而日志文件的路径是 /tmp/logtest.txt。
|
||||
|
||||
既然找出了问题根源,接下来按照惯例,就该查看源代码,然后分析为什么这个进程会狂打日志了。
|
||||
|
||||
你可以运行 docker cp 命令,把案例应用的源代码拷贝出来,然后查看它的内容。(你也可以点击[这里](https://github.com/feiskyer/linux-perf-examples/tree/master/logging-app)查看案例应用的源码):
|
||||
|
||||
```
|
||||
#拷贝案例应用源代码到当前目录
|
||||
$ docker cp app:/app.py .
|
||||
|
||||
#查看案例应用的源代码
|
||||
$ cat app.py
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(level=logging.INFO)
|
||||
rHandler = RotatingFileHandler("/tmp/logtest.txt", maxBytes=1024 * 1024 * 1024, backupCount=1)
|
||||
rHandler.setLevel(logging.INFO)
|
||||
|
||||
def write_log(size):
|
||||
'''Write logs to file'''
|
||||
message = get_message(size)
|
||||
while True:
|
||||
logger.info(message)
|
||||
time.sleep(0.1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
msg_size = 300 * 1024 * 1024
|
||||
write_log(msg_size)
|
||||
|
||||
```
|
||||
|
||||
分析这个源码,我们发现,它的日志路径是 /tmp/logtest.txt,默认记录 INFO 级别以上的所有日志,而且每次写日志的大小是 300MB。这跟我们上面的分析结果是一致的。
|
||||
|
||||
一般来说,生产系统的应用程序,应该有动态调整日志级别的功能。继续查看源码,你会发现,这个程序也可以调整日志级别。如果你给它发送 SIGUSR1 信号,就可以把日志调整为 INFO 级;发送 SIGUSR2 信号,则会调整为 WARNING 级:
|
||||
|
||||
```
|
||||
def set_logging_info(signal_num, frame):
|
||||
'''Set loging level to INFO when receives SIGUSR1'''
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
def set_logging_warning(signal_num, frame):
|
||||
'''Set loging level to WARNING when receives SIGUSR2'''
|
||||
logger.setLevel(logging.WARNING)
|
||||
|
||||
signal.signal(signal.SIGUSR1, set_logging_info)
|
||||
signal.signal(signal.SIGUSR2, set_logging_warning)
|
||||
|
||||
```
|
||||
|
||||
根据源码中的日志调用 logger. info(message) ,我们知道,它的日志是 INFO 级,这也正是它的默认级别。那么,只要把默认级别调高到 WARNING 级,日志问题应该就解决了。
|
||||
|
||||
接下来,我们就来检查一下,刚刚的分析对不对。在终端中运行下面的 kill 命令,给进程 18940 发送 SIGUSR2 信号:
|
||||
|
||||
```
|
||||
$ kill -SIGUSR2 18940
|
||||
|
||||
```
|
||||
|
||||
然后,再执行 top 和 iostat 观察一下:
|
||||
|
||||
```
|
||||
$ top
|
||||
...
|
||||
%Cpu(s): 0.3 us, 0.2 sy, 0.0 ni, 99.5 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
$ iostat -d -x 1
|
||||
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
|
||||
loop0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
|
||||
sdb 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
|
||||
sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
|
||||
|
||||
```
|
||||
|
||||
观察top 和 iostat 的输出,你会发现,稍等一段时间后,iowait 会变成 0,而 sda 磁盘的 I/O 使用率也会逐渐减少到 0。
|
||||
|
||||
到这里,我们不仅定位了狂打日志的应用程序,并通过调高日志级别的方法,完美解决了 I/O 的性能瓶颈。
|
||||
|
||||
案例最后,当然不要忘了运行下面的命令,停止案例应用:
|
||||
|
||||
```
|
||||
$ docker rm -f app
|
||||
|
||||
```
|
||||
|
||||
## 小结
|
||||
|
||||
日志,是了解应用程序内部运行情况,最常用、也最有效的工具。无论是操作系统,还是应用程序,都会记录大量的运行日志,以便事后查看历史记录。这些日志一般按照不同级别来开启,比如,开发环境通常打开调试级别的日志,而线上环境则只记录警告和错误日志。
|
||||
|
||||
在排查应用程序问题时,我们可能需要,在线上环境临时开启应用程序的调试日志。有时候,事后一不小心就忘了调回去。没把线上的日志调高到警告级别,可能会导致 CPU 使用率、磁盘 I/O 等一系列的性能问题,严重时,甚至会影响到同一台服务器上运行的其他应用程序。
|
||||
|
||||
今后,在碰到这种“狂打日志”的场景时,你可以用 iostat、strace、lsof 等工具来定位狂打日志的进程,找出相应的日志文件,再通过应用程序的接口,调整日志级别来解决问题。
|
||||
|
||||
如果应用程序不能动态调整日志级别,你可能还需要修改应用的配置,并重启应用让配置生效。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,给你留一个思考题。
|
||||
|
||||
在今天的案例开始时,我们用 top 和 iostat 查看了系统资源的使用情况。除了 CPU 和磁盘 I/O外,剩余内存也比较少,而内存主要被 Buffer/Cache 占用。
|
||||
|
||||
那么,今天的问题就是,这些内存到底是被 Buffer 还是 Cache 占用了呢?有没有什么方法来确认你的分析结果呢?
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
390
极客时间专栏/Linux性能优化实战/I|O 性能篇/27 | 案例篇:为什么我的磁盘I|O延迟很高?.md
Normal file
390
极客时间专栏/Linux性能优化实战/I|O 性能篇/27 | 案例篇:为什么我的磁盘I|O延迟很高?.md
Normal file
@@ -0,0 +1,390 @@
|
||||
<audio id="audio" title="27 | 案例篇:为什么我的磁盘I/O延迟很高?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cd/f8/cd3d4d911c166dc81536d166a14e6df8.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一节,我们研究了一个狂打日志引发 I/O 性能问题的案例,先来简单回顾一下。
|
||||
|
||||
日志,是了解应用程序内部运行情况,最常用也是最有效的工具。日志一般会分为调试、信息、警告、错误等多个不同级别。
|
||||
|
||||
通常,生产环境只用开启警告级别的日志,这一般不会导致 I/O 问题。但在偶尔排查问题时,可能需要我们开启调试日志。调试结束后,很可能忘了把日志级别调回去。这时,大量的调试日志就可能会引发 I/O 性能问题。
|
||||
|
||||
你可以用 iostat ,确认是否有 I/O 性能瓶颈。再用 strace 和 lsof ,来定位应用程序以及它正在写入的日志文件路径。最后通过应用程序的接口调整日志级别,完美解决 I/O 问题。
|
||||
|
||||
不过,如果应用程序没有动态调整日志级别的功能,你还需要修改应用配置并重启应用,以便让配置生效。
|
||||
|
||||
今天,我们再来看一个新的案例。这次案例是一个基于 Python Flask 框架的 Web 应用,它提供了一个查询单词热度的 API,但是API 的响应速度并不让人满意。
|
||||
|
||||
非常感谢携程系统研发部资深后端工程师董国星,帮助提供了今天的案例。
|
||||
|
||||
## **案例准备**
|
||||
|
||||
本次案例还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
|
||||
|
||||
<li>
|
||||
机器配置:2 CPU,8GB 内存
|
||||
</li>
|
||||
<li>
|
||||
预先安装 docker、sysstat 等工具,如 apt install [docker.io](http://docker.io) sysstat
|
||||
</li>
|
||||
|
||||
为了方便你运行今天的案例,我把它打包成了一个 Docker 镜像。这样,你就只需要运行 Docker 命令就可以启动它。
|
||||
|
||||
今天的案例需要两台虚拟机,其中一台是案例分析的目标机器,运行 Flask 应用,它的 IP 地址是 192.168.0.10;而另一台作为客户端,请求单词的热度。我画了一张图表示它们的关系,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a8/bf/a8cc1b02b8c896380d2c53b8018bddbf.png" alt="">
|
||||
|
||||
接下来,打开两个终端,分别 SSH 登录到这两台虚拟机中,并在第一台虚拟机中,安装上述工具。
|
||||
|
||||
跟以前一样,案例中所有命令都默认以 root 用户运行,如果你是用普通用户身份登陆系统,请运行 sudo su root 命令切换到 root 用户。
|
||||
|
||||
到这里,准备工作就完成了。接下来,我们正式进入操作环节。
|
||||
|
||||
>
|
||||
温馨提示:案例中 Python 应用的核心逻辑比较简单,你可能一眼就能看出问题,但实际生产环境中的源码就复杂多了。所以,我依旧建议,操作之前别看源码,避免先入为主,而要把它当成一个黑盒来分析。这样,你可以更好把握,怎么从系统的资源使用问题出发,分析出瓶颈所在的应用,以及瓶颈在应用中大概的位置。
|
||||
|
||||
|
||||
## **案例分析**
|
||||
|
||||
首先,我们在第一个终端中执行下面的命令,运行本次案例要分析的目标应用:
|
||||
|
||||
```
|
||||
$ docker run --name=app -p 10000:80 -itd feisky/word-pop
|
||||
|
||||
```
|
||||
|
||||
然后,在第二个终端中运行 curl 命令,访问 [http://192.168.0.10:1000/](http://192.168.0.10:1000/),确认案例正常启动。你应该可以在 curl 的输出界面里,看到一个 hello world 的输出:
|
||||
|
||||
```
|
||||
$ curl http://192.168.0.10:10000/
|
||||
hello world
|
||||
|
||||
```
|
||||
|
||||
接下来,在第二个终端中,访问案例应用的单词热度接口,也就是 [http://192.168.0.10:1000/popularity/word](http://192.168.0.10:1000/popularity/word)。
|
||||
|
||||
```
|
||||
$ curl http://192.168.0.10:1000/popularity/word
|
||||
|
||||
```
|
||||
|
||||
稍等一会儿,你会发现,这个接口居然这么长时间都没响应,究竟是怎么回事呢?我们先回到终端一来分析一下。
|
||||
|
||||
我们试试在第一个终端里,随便执行一个命令,比如执行 df 命令,查看一下文件系统的使用情况。奇怪的是,这么简单的命令,居然也要等好久才有输出。
|
||||
|
||||
```
|
||||
$ df
|
||||
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||
udev 4073376 0 4073376 0% /dev
|
||||
tmpfs 816932 1188 815744 1% /run
|
||||
/dev/sda1 30308240 8713640 21578216 29% /
|
||||
|
||||
```
|
||||
|
||||
通过df我们知道,系统还有足够多的磁盘空间。那为什么响应会变慢呢?看来还是得观察一下,系统的资源使用情况,像是 CPU、内存和磁盘 I/O 等的具体使用情况。
|
||||
|
||||
这里的思路其实跟上一个案例比较类似,我们可以先用 top 来观察 CPU 和内存的使用情况,然后再用 iostat 来观察磁盘的 I/O 情况。
|
||||
|
||||
为了避免分析过程中curl 请求突然结束,我们回到终端二,按 Ctrl+C 停止刚才的应用程序;然后,把curl 命令放到一个循环里执行;这次我们还要加一个 time 命令,观察每次的执行时间:
|
||||
|
||||
```
|
||||
$ while true; do time curl http://192.168.0.10:10000/popularity/word; sleep 1; done
|
||||
|
||||
```
|
||||
|
||||
继续回到终端一来分析性能。我们在终端一中运行 top 命令,观察 CPU 和内存的使用情况:
|
||||
|
||||
```
|
||||
$ top
|
||||
top - 14:27:02 up 10:30, 1 user, load average: 1.82, 1.26, 0.76
|
||||
Tasks: 129 total, 1 running, 74 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 3.5 us, 2.1 sy, 0.0 ni, 0.0 id, 94.4 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu1 : 2.4 us, 0.7 sy, 0.0 ni, 70.4 id, 26.5 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
KiB Mem : 8169300 total, 3323248 free, 436748 used, 4409304 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 7412556 avail Mem
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
12280 root 20 0 103304 28824 7276 S 14.0 0.4 0:08.77 python
|
||||
16 root 20 0 0 0 0 S 0.3 0.0 0:09.22 ksoftirqd/1
|
||||
1549 root 20 0 236712 24480 9864 S 0.3 0.3 3:31.38 python3
|
||||
|
||||
```
|
||||
|
||||
观察 top 的输出可以发现,两个CPU的 iowait 都非常高。特别是 CPU0, iowait 已经高达 94 %,而剩余内存还有 3GB,看起来也是充足的。
|
||||
|
||||
再往下看,进程部分有一个 python 进程的CPU使用率稍微有点高,达到了 14%。虽然 14% 并不能成为性能瓶颈,不过有点嫌疑——可能跟 iowait 的升高有关。
|
||||
|
||||
那这个PID 号为 12280 的 python 进程,到底是不是我们的案例应用呢?
|
||||
|
||||
我们在第一个终端中,按下 Ctrl+C,停止 top 命令;然后执行下面的 ps 命令,查找案例应用 [app.py](http://app.py) 的 PID 号:
|
||||
|
||||
```
|
||||
$ ps aux | grep app.py
|
||||
root 12222 0.4 0.2 96064 23452 pts/0 Ss+ 14:37 0:00 python /app.py
|
||||
root 12280 13.9 0.3 102424 27904 pts/0 Sl+ 14:37 0:09 /usr/local/bin/python /app.py
|
||||
|
||||
```
|
||||
|
||||
从 ps 的输出,你可以看到,这个 CPU 使用率较高的进程,正是我们的案例应用。不过先别着急分析 CPU 问题,毕竟 iowait 已经高达 94%, I/O 问题才是我们首要解决的。
|
||||
|
||||
接下来,我们在终端一中,运行下面的 iostat 命令,其中:
|
||||
|
||||
<li>
|
||||
-d 选项是指显示出 I/O 的性能指标;
|
||||
</li>
|
||||
<li>
|
||||
-x 选项是指显示出扩展统计信息(即显示所有I/O指标)。
|
||||
</li>
|
||||
|
||||
```
|
||||
$ iostat -d -x 1
|
||||
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
|
||||
loop0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
|
||||
sda 0.00 71.00 0.00 32912.00 0.00 0.00 0.00 0.00 0.00 18118.31 241.89 0.00 463.55 13.86 98.40
|
||||
|
||||
```
|
||||
|
||||
再次看到 iostat 的输出,你还记得这个界面中的性能指标含义吗?先自己回忆一下,如果实在想不起来,一定要先查看上节内容,或者用 man iostat 查明白。
|
||||
|
||||
明白了指标含义,再来具体观察 iostat 的输出。你可以发现,磁盘 sda 的 I/O 使用率已经达到 98% ,接近饱和了。而且,写请求的响应时间高达 18 秒,每秒的写数据为 32 MB,显然写磁盘碰到了瓶颈。
|
||||
|
||||
那要怎么知道,这些 I/O请求到底是哪些进程导致的呢?我想,你已经还记得上一节我们用到的 pidstat。
|
||||
|
||||
在终端一中,运行下面的 pidstat 命令,观察进程的 I/O 情况:
|
||||
|
||||
```
|
||||
$ pidstat -d 1
|
||||
14:39:14 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
14:39:15 0 12280 0.00 335716.00 0.00 0 python
|
||||
|
||||
```
|
||||
|
||||
从 pidstat 的输出,我们再次看到了PID 号为 12280的结果。这说明,正是案例应用引发I/O 的性能瓶颈。
|
||||
|
||||
走到这一步,你估计觉得,接下来就很简单了,上一个案例不刚刚学过吗?无非就是,先用 strace 确认它是不是在写文件,再用 lsof 找出文件描述符对应的文件即可。
|
||||
|
||||
到底是不是这样呢?我们不妨来试试。还是在终端一中,执行下面的 strace 命令:
|
||||
|
||||
```
|
||||
$ strace -p 12280
|
||||
strace: Process 12280 attached
|
||||
select(0, NULL, NULL, NULL, {tv_sec=0, tv_usec=567708}) = 0 (Timeout)
|
||||
stat("/usr/local/lib/python3.7/importlib/_bootstrap.py", {st_mode=S_IFREG|0644, st_size=39278, ...}) = 0
|
||||
stat("/usr/local/lib/python3.7/importlib/_bootstrap.py", {st_mode=S_IFREG|0644, st_size=39278, ...}) = 0
|
||||
|
||||
```
|
||||
|
||||
从 strace 中,你可以看到大量的 stat 系统调用,并且大都为 python 的文件,但是,请注意,这里并没有任何 write 系统调用。
|
||||
|
||||
由于 strace 的输出比较多,我们可以用 grep ,来过滤一下 write,比如:
|
||||
|
||||
```
|
||||
$ strace -p 12280 2>&1 | grep write
|
||||
|
||||
```
|
||||
|
||||
遗憾的是,这里仍然没有任何输出。
|
||||
|
||||
难道此时已经没有性能问题了吗?重新执行刚才的 top 和 iostat 命令,你会不幸地发现,性能问题仍然存在。
|
||||
|
||||
我们只好综合 strace、pidstat 和 iostat 这三个结果来分析了。很明显,你应该发现了这里的矛盾:iostat 已经证明磁盘 I/O 有性能瓶颈,而 pidstat 也证明了,这个瓶颈是由 12280 号进程导致的,但 strace 跟踪这个进程,却没有找到任何 write 系统调用。
|
||||
|
||||
这就奇怪了。难道因为案例使用的编程语言是 Python ,而Python 是解释型的,所以找不到?还是说,因为案例运行在 Docker 中呢?这里留个悬念,你自己想想。
|
||||
|
||||
文件写,明明应该有相应的write系统调用,但用现有工具却找不到痕迹,这时就该想想换工具的问题了。怎样才能知道哪里在写文件呢?
|
||||
|
||||
这里我给你介绍一个新工具, [filetop](https://github.com/iovisor/bcc/blob/master/tools/filetop.py)。它是 [bcc](https://github.com/iovisor/bcc) 软件包的一部分,基于 Linux 内核的 eBPF(extended Berkeley Packet Filters)机制,主要跟踪内核中文件的读写情况,并输出线程ID(TID)、读写大小、读写类型以及文件名称。
|
||||
|
||||
eBPF 的工作原理,你暂时不用深究,后面内容我们会逐渐接触到,先会使用就可以了。
|
||||
|
||||
至于老朋友 bcc 的安装方法,可以参考它的 Github 网站 [https://github.com/iovisor/bcc](https://github.com/iovisor/bcc)。比如在 Ubuntu 16 以上的版本中,你可以运行下面的命令来安装它:
|
||||
|
||||
```
|
||||
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD
|
||||
echo "deb https://repo.iovisor.org/apt/$(lsb_release -cs) $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/iovisor.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install bcc-tools libbcc-examples linux-headers-$(uname -r)
|
||||
|
||||
```
|
||||
|
||||
安装后,bcc 提供的所有工具,就全部安装到了 /usr/share/bcc/tools 这个目录中。接下来我们就用这个工具,观察一下文件的读写情况。
|
||||
|
||||
首先,在终端一中运行下面的命令:
|
||||
|
||||
```
|
||||
# 切换到工具目录
|
||||
$ cd /usr/share/bcc/tools
|
||||
|
||||
# -C 选项表示输出新内容时不清空屏幕
|
||||
$ ./filetop -C
|
||||
|
||||
TID COMM READS WRITES R_Kb W_Kb T FILE
|
||||
514 python 0 1 0 2832 R 669.txt
|
||||
514 python 0 1 0 2490 R 667.txt
|
||||
514 python 0 1 0 2685 R 671.txt
|
||||
514 python 0 1 0 2392 R 670.txt
|
||||
514 python 0 1 0 2050 R 672.txt
|
||||
|
||||
...
|
||||
|
||||
TID COMM READS WRITES R_Kb W_Kb T FILE
|
||||
514 python 2 0 5957 0 R 651.txt
|
||||
514 python 2 0 5371 0 R 112.txt
|
||||
514 python 2 0 4785 0 R 861.txt
|
||||
514 python 2 0 4736 0 R 213.txt
|
||||
514 python 2 0 4443 0 R 45.txt
|
||||
|
||||
|
||||
```
|
||||
|
||||
你会看到,filetop 输出了 8 列内容,分别是线程ID、线程命令行、读写次数、读写的大小(单位KB)、文件类型以及读写的文件名称。
|
||||
|
||||
这些内容里,你可能会看到很多动态链接库,不过这不是我们的重点,暂且忽略即可。我们的重点,是一个 python 应用,所以要特别关注 python 相关的内容。
|
||||
|
||||
多观察一会儿,你就会发现,每隔一段时间,线程号为 514 的 python 应用就会先写入大量的 txt 文件,再大量地读。
|
||||
|
||||
线程号为 514 的线程,属于哪个进程呢?我们可以用 ps 命令查看。先在终端一中,按下 Ctrl+C ,停止 filetop ;然后,运行下面的 ps 命令。这个输出的第二列内容,就是我们想知道的进程号:
|
||||
|
||||
```
|
||||
$ ps -efT | grep 514
|
||||
root 12280 514 14626 33 14:47 pts/0 00:00:05 /usr/local/bin/python /app.py
|
||||
|
||||
```
|
||||
|
||||
我们看到,这个线程正是案例应用 12280的线程。终于可以先松一口气,不过还没完,filetop 只给出了文件名称,却没有文件路径,还得继续找啊。
|
||||
|
||||
我再介绍一个好用的工具,opensnoop 。它同属于 bcc 软件包,可以动态跟踪内核中的 open 系统调用。这样,我们就可以找出这些 txt 文件的路径。
|
||||
|
||||
接下来,在终端一中,运行下面的 opensnoop 命令:
|
||||
|
||||
```
|
||||
$ opensnoop
|
||||
12280 python 6 0 /tmp/9046db9e-fe25-11e8-b13f-0242ac110002/650.txt
|
||||
12280 python 6 0 /tmp/9046db9e-fe25-11e8-b13f-0242ac110002/651.txt
|
||||
12280 python 6 0 /tmp/9046db9e-fe25-11e8-b13f-0242ac110002/652.txt
|
||||
|
||||
```
|
||||
|
||||
这次,通过 opensnoop 的输出,你可以看到,这些 txt 路径位于 /tmp 目录下。你还能看到,它打开的文件数量,按照数字编号,从 0.txt 依次增大到 999.txt,这可远多于前面用 filetop 看到的数量。
|
||||
|
||||
综合 filetop 和 opensnoop ,我们就可以进一步分析了。我们可以大胆猜测,案例应用在写入 1000 个txt文件后,又把这些内容读到内存中进行处理。我们来检查一下,这个目录中是不是真的有 1000 个文件:
|
||||
|
||||
```
|
||||
$ ls /tmp/9046db9e-fe25-11e8-b13f-0242ac110002 | wc -l
|
||||
ls: cannot access '/tmp/9046db9e-fe25-11e8-b13f-0242ac110002': No such file or directory
|
||||
0
|
||||
|
||||
```
|
||||
|
||||
操作后却发现,目录居然不存在了。怎么回事呢?我们回到 opensnoop 再观察一会儿:
|
||||
|
||||
```
|
||||
$ opensnoop
|
||||
12280 python 6 0 /tmp/defee970-fe25-11e8-b13f-0242ac110002/261.txt
|
||||
12280 python 6 0 /tmp/defee970-fe25-11e8-b13f-0242ac110002/840.txt
|
||||
12280 python 6 0 /tmp/defee970-fe25-11e8-b13f-0242ac110002/136.txt
|
||||
|
||||
```
|
||||
|
||||
原来,这时的路径已经变成了另一个目录。这说明,这些目录都是应用程序动态生成的,用完就删了。
|
||||
|
||||
结合前面的所有分析,我们基本可以判断,案例应用会动态生成一批文件,用来临时存储数据,用完就会删除它们。但不幸的是,正是这些文件读写,引发了 I/O 的性能瓶颈,导致整个处理过程非常慢。
|
||||
|
||||
当然,我们还需要验证这个猜想。老办法,还是查看应用程序的源码 [app.py](https://github.com/feiskyer/linux-perf-examples/blob/master/io-latency/app.py),
|
||||
|
||||
```
|
||||
@app.route("/popularity/<word>")
|
||||
def word_popularity(word):
|
||||
dir_path = '/tmp/{}'.format(uuid.uuid1())
|
||||
count = 0
|
||||
sample_size = 1000
|
||||
|
||||
def save_to_file(file_name, content):
|
||||
with open(file_name, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
try:
|
||||
# initial directory firstly
|
||||
os.mkdir(dir_path)
|
||||
|
||||
# save article to files
|
||||
for i in range(sample_size):
|
||||
file_name = '{}/{}.txt'.format(dir_path, i)
|
||||
article = generate_article()
|
||||
save_to_file(file_name, article)
|
||||
|
||||
# count word popularity
|
||||
for root, dirs, files in os.walk(dir_path):
|
||||
for file_name in files:
|
||||
with open('{}/{}'.format(dir_path, file_name)) as f:
|
||||
if validate(word, f.read()):
|
||||
count += 1
|
||||
finally:
|
||||
# clean files
|
||||
shutil.rmtree(dir_path, ignore_errors=True)
|
||||
|
||||
return jsonify({'popularity': count / sample_size * 100, 'word': word})
|
||||
|
||||
```
|
||||
|
||||
源码中可以看到,这个案例应用,在每个请求的处理过程中,都会生成一批临时文件,然后读入内存处理,最后再把整个目录删除掉。
|
||||
|
||||
这是一种常见的利用磁盘空间处理大量数据的技巧,不过,本次案例中的 I/O 请求太重,导致磁盘 I/O 利用率过高。
|
||||
|
||||
要解决这一点,其实就是算法优化问题了。比如在内存充足时,就可以把所有数据都放到内存中处理,这样就能避免 I/O 的性能问题。
|
||||
|
||||
你可以检验一下,在终端二中分别访问 [http://192.168.0.10:10000/popularity/word](http://192.168.0.10:10000/popularity/word) 和 [http://192.168.0.10:10000/popular/word](http://192.168.0.10:10000/popular/word) ,对比前后的效果:
|
||||
|
||||
```
|
||||
$ time curl http://192.168.0.10:10000/popularity/word
|
||||
{
|
||||
"popularity": 0.0,
|
||||
"word": "word"
|
||||
}
|
||||
real 2m43.172s
|
||||
user 0m0.004s
|
||||
sys 0m0.007s
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
$ time curl http://192.168.0.10:10000/popular/word
|
||||
{
|
||||
"popularity": 0.0,
|
||||
"word": "word"
|
||||
}
|
||||
|
||||
real 0m8.810s
|
||||
user 0m0.010s
|
||||
sys 0m0.000s
|
||||
|
||||
```
|
||||
|
||||
新的接口只要8秒就可以返回,明显比一开始的 3 分钟好很多。
|
||||
|
||||
当然,这只是优化的第一步,并且方法也不算完善,还可以做进一步的优化。不过,在实际系统中,我们大都是类似的做法,先用最简单的方法,尽早解决线上问题,然后再继续思考更好的优化方法。
|
||||
|
||||
## 小结
|
||||
|
||||
今天,我们分析了一个响应过慢的单词热度案例。
|
||||
|
||||
首先,我们用 top、iostat,分析了系统的 CPU 和磁盘使用情况。我们发现了磁盘 I/O 瓶颈,也知道了这个瓶颈是案例应用导致的。
|
||||
|
||||
接着,我们试着照搬上一节案例的方法,用 strace 来观察进程的系统调用,不过这次很不走运,没找到任何 write 系统调用。
|
||||
|
||||
于是,我们又用了新的工具,借助动态追踪工具包 bcc 中的 filetop 和 opensnoop ,找出了案例应用的问题,发现这个根源是大量读写临时文件。
|
||||
|
||||
找出问题后,优化方法就相对比较简单了。如果内存充足时,最简单的方法,就是把数据都放在速度更快的内存中,这样就没有磁盘 I/O 的瓶颈了。当然,再进一步,你可以还可以利用 Trie 树等各种算法,进一步优化单词处理的效率。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,给你留一个思考题,也是我在文章中提到过的,让你思考的问题。
|
||||
|
||||
今天的案例中,iostat 已经证明,磁盘 I/O 出现了性能瓶颈, pidstat 也证明了这个瓶颈是由 12280 号进程导致的。但是,strace 跟踪这个进程,却没有发现任何 write 系统调用。
|
||||
|
||||
这究竟是怎么回事?难道是因为案例使用的编程语言 Python 本身是解释型?还是说,因为案例运行在 Docker 中呢?
|
||||
|
||||
这里我小小提示一下。当你发现性能工具的输出无法解释时,最好返回去想想,是不是分析中漏掉了什么线索,或者去翻翻工具手册,看看是不是某些默认选项导致的。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
548
极客时间专栏/Linux性能优化实战/I|O 性能篇/28 | 案例篇:一个SQL查询要15秒,这是怎么回事?.md
Normal file
548
极客时间专栏/Linux性能优化实战/I|O 性能篇/28 | 案例篇:一个SQL查询要15秒,这是怎么回事?.md
Normal file
@@ -0,0 +1,548 @@
|
||||
<audio id="audio" title="28 | 案例篇:一个SQL查询要15秒,这是怎么回事?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/04/38/04d848146b4d57a6162d172d721d4538.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一节,我们分析了一个单词热度应用响应过慢的案例。当用 top、iostat 分析了系统的 CPU 和磁盘 I/O 使用情况后,我们发现系统出现了磁盘的 I/O 瓶颈,而且正是案例应用导致的。
|
||||
|
||||
接着,在使用 strace 却没有任何发现后,我又给你介绍了两个新的工具 filetop 和 opensnoop,分析它们对系统调用 write() 和 open() 的追踪结果。
|
||||
|
||||
我们发现,案例应用正在读写大量的临时文件,因此产生了性能瓶颈。找出瓶颈后,我们又用把文件数据都放在内存的方法,解决了磁盘 I/O 的性能问题。
|
||||
|
||||
当然,你可能会说,在实际应用中,大量数据肯定是要存入数据库的,而不会直接用文本文件的方式存储。不过,数据库也不是万能的。当数据库出现性能问题时,又该如何分析和定位它的瓶颈呢?
|
||||
|
||||
今天我们就来一起分析一个数据库的案例。这是一个基于 Python Flask 的商品搜索应用,商品信息存在 MySQL 中。这个应用可以通过 MySQL 接口,根据客户端提供的商品名称,去数据库表中查询商品信息。
|
||||
|
||||
非常感谢唯品会资深运维工程师阳祥义,帮助提供了今天的案例。
|
||||
|
||||
## 案例准备
|
||||
|
||||
本次案例还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
|
||||
|
||||
<li>
|
||||
机器配置:2 CPU,8GB 内存
|
||||
</li>
|
||||
<li>
|
||||
预先安装 docker、sysstat 、git、make 等工具,如 apt install [docker.io](http://docker.io) sysstat make git
|
||||
</li>
|
||||
|
||||
其中,docker 和 sysstat 已经用过很多次,这里不再赘述;git 用来拉取本次案例所需脚本,这些脚本存储在 Github 代码仓库中;最后的 make 则是一个常用构建工具,这里用来运行今天的案例。
|
||||
|
||||
案例总共由三个容器组成,包括一个 MySQL 数据库应用、一个商品搜索应用以及一个数据处理的应用。其中,商品搜索应用以 HTTP 的形式提供了一个接口:
|
||||
|
||||
<li>
|
||||
/:返回 Index Page;
|
||||
</li>
|
||||
<li>
|
||||
/db/insert/products/<num>:插入指定数量的商品信息;</num>
|
||||
</li>
|
||||
<li>
|
||||
/products/<product>:查询指定商品的信息,并返回处理时间。</product>
|
||||
</li>
|
||||
|
||||
由于应用比较多,为了方便你运行它们,我把它们同样打包成了几个 Docker 镜像,并推送到了 Github 上。这样,你只需要运行几条命令,就可以启动了。
|
||||
|
||||
今天的案例需要两台虚拟机,其中一台作为案例分析的目标机器,运行 Flask 应用,它的 IP 地址是 192.168.0.10;另一台则是作为客户端,请求单词的热度。我画了一张图表示它们的关系。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/5d/8c954570f6e46193505c2598a06cbc5d.png" alt="">
|
||||
|
||||
接下来,打开两个终端,分别 SSH 登录到这两台虚拟机中,并在第一台虚拟机中安装上述工具。
|
||||
|
||||
跟以前一样,案例中所有命令都默认以 root 用户运行,如果你是用普通用户身份登陆系统,请运行 sudo su root命令切换到 root 用户。
|
||||
|
||||
到这里,准备工作就完成了。接下来,我们正式进入操作环节。
|
||||
|
||||
## 案例分析
|
||||
|
||||
首先,我们在第一个终端中执行下面命令,拉取本次案例所需脚本:
|
||||
|
||||
```
|
||||
$ git clone https://github.com/feiskyer/linux-perf-examples
|
||||
$ cd linux-perf-examples/mysql-slow
|
||||
|
||||
```
|
||||
|
||||
接着,执行下面的命令,运行本次的目标应用。正常情况下,你应该可以看到下面的输出:
|
||||
|
||||
```
|
||||
# 注意下面的随机字符串是容器ID,每次运行均会不同,并且你不需要关注它,因为我们只会用到名字
|
||||
$ make run
|
||||
docker run --name=mysql -itd -p 10000:80 -m 800m feisky/mysql:5.6
|
||||
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
|
||||
4156780da5be0b9026bcf27a3fa56abc15b8408e358fa327f472bcc5add4453f
|
||||
docker run --name=dataservice -itd --privileged feisky/mysql-dataservice
|
||||
f724d0816d7e47c0b2b1ff701e9a39239cb9b5ce70f597764c793b68131122bb
|
||||
docker run --name=app --network=container:mysql -itd feisky/mysql-slow
|
||||
81d3392ba25bb8436f6151662a13ff6182b6bc6f2a559fc2e9d873cd07224ab6
|
||||
|
||||
```
|
||||
|
||||
然后,再运行 docker ps 命令,确认三个容器都处在运行(Up)状态:
|
||||
|
||||
```
|
||||
$ docker ps
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
9a4e3c580963 feisky/mysql-slow "python /app.py" 42 seconds ago Up 36 seconds app
|
||||
2a47aab18082 feisky/mysql-dataservice "python /dataservice…" 46 seconds ago Up 41 seconds dataservice
|
||||
4c3ff7b24748 feisky/mysql:5.6 "docker-entrypoint.s…" 47 seconds ago Up 46 seconds 3306/tcp, 0.0.0.0:10000->80/tcp mysql
|
||||
|
||||
```
|
||||
|
||||
MySQL 数据库的启动过程,需要做一些初始化工作,这通常需要花费几分钟时间。你可以运行 docker logs 命令,查看它的启动过程。
|
||||
|
||||
当你看到下面这个输出时,说明 MySQL 初始化完成,可以接收外部请求了:
|
||||
|
||||
```
|
||||
$ docker logs -f mysql
|
||||
...
|
||||
... [Note] mysqld: ready for connections.
|
||||
Version: '5.6.42-log' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL)
|
||||
|
||||
```
|
||||
|
||||
而商品搜索应用则是在 10000 端口监听。你可以按 Ctrl+C ,停止 docker logs 命令;然后,执行下面的命令,确认它也已经正常运行。如果一切正常,你会看到 Index Page 的输出:
|
||||
|
||||
```
|
||||
$ curl http://127.0.0.1:10000/
|
||||
Index Page
|
||||
|
||||
```
|
||||
|
||||
接下来,运行 make init 命令,初始化数据库,并插入 10000 条商品信息。这个过程比较慢,比如在我的机器中,就花了十几分钟时间。耐心等待一段时间后,你会看到如下的输出:
|
||||
|
||||
```
|
||||
$ make init
|
||||
docker exec -i mysql mysql -uroot -P3306 < tables.sql
|
||||
curl http://127.0.0.1:10000/db/insert/products/10000
|
||||
insert 10000 lines
|
||||
|
||||
```
|
||||
|
||||
接着,我们切换到第二个终端,访问一下商品搜索的接口,看看能不能找到想要的商品。执行如下的 curl 命令:
|
||||
|
||||
```
|
||||
$ curl http://192.168.0.10:10000/products/geektime
|
||||
Got data: () in 15.364538192749023 sec
|
||||
|
||||
```
|
||||
|
||||
稍等一会儿,你会发现,这个接口返回的是空数据,而且处理时间超过15 秒。这么慢的响应速度让人无法忍受,到底出了什么问题呢?
|
||||
|
||||
既然今天用了 MySQL,你估计会猜到是慢查询的问题。
|
||||
|
||||
不过别急,在具体分析前,为了避免在分析过程中客户端的请求结束,我们把 curl 命令放到一个循环里执行。同时,为了避免给系统过大压力,我们设置在每次查询后,都先等待 5 秒,然后再开始新的请求。
|
||||
|
||||
所以,你可以在终端二中,继续执行下面的命令:
|
||||
|
||||
```
|
||||
$ while true; do curl http://192.168.0.10:10000/products/geektime; sleep 5; done
|
||||
|
||||
```
|
||||
|
||||
接下来,重新回到终端一中,分析接口响应速度慢的原因。不过,重回终端一后,你会发现系统响应也明显变慢了,随便执行一个命令,都得停顿一会儿才能看到输出。
|
||||
|
||||
这跟上一节的现象很类似,看来,我们还是得观察一下系统的资源使用情况,比如CPU、内存和磁盘 I/O 等的情况。
|
||||
|
||||
首先,我们在终端一执行 top 命令,分析系统的 CPU 使用情况:
|
||||
|
||||
```
|
||||
$ top
|
||||
top - 12:02:15 up 6 days, 8:05, 1 user, load average: 0.66, 0.72, 0.59
|
||||
Tasks: 137 total, 1 running, 81 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 0.7 us, 1.3 sy, 0.0 ni, 35.9 id, 62.1 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu1 : 0.3 us, 0.7 sy, 0.0 ni, 84.7 id, 14.3 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
KiB Mem : 8169300 total, 7238472 free, 546132 used, 384696 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 7316952 avail Mem
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
27458 999 20 0 833852 57968 13176 S 1.7 0.7 0:12.40 mysqld
|
||||
27617 root 20 0 24348 9216 4692 S 1.0 0.1 0:04.40 python
|
||||
1549 root 20 0 236716 24568 9864 S 0.3 0.3 51:46.57 python3
|
||||
22421 root 20 0 0 0 0 I 0.3 0.0 0:01.16 kworker/u
|
||||
|
||||
```
|
||||
|
||||
观察 top 的输出,我们发现,两个 CPU 的 iowait 都比较高,特别是 CPU0,iowait 已经超过 60%。而具体到各个进程, CPU 使用率并不高,最高的也只有 1.7%。
|
||||
|
||||
既然CPU的嫌疑不大,那问题应该还是出在了 I/O 上。我们仍然在第一个终端,按下 Ctrl+C,停止 top 命令;然后,执行下面的 iostat 命令,看看有没有 I/O 性能问题:
|
||||
|
||||
```
|
||||
$ iostat -d -x 1
|
||||
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
|
||||
...
|
||||
sda 273.00 0.00 32568.00 0.00 0.00 0.00 0.00 0.00 7.90 0.00 1.16 119.30 0.00 3.56 97.20
|
||||
|
||||
```
|
||||
|
||||
iostat 的输出你应该非常熟悉。观察这个界面,我们发现,磁盘 sda 每秒的读数据为 32 MB, 而 I/O 使用率高达 97% ,接近饱和,这说明,磁盘 sda 的读取确实碰到了性能瓶颈。
|
||||
|
||||
那要怎么知道,这些 I/O请求到底是哪些进程导致的呢?当然可以找我们的老朋友, pidstat。接下来,在终端一中,按下 Ctrl+C 停止 iostat 命令,然后运行下面的 pidstat 命令,观察进程的 I/O 情况:
|
||||
|
||||
```
|
||||
# -d选项表示展示进程的I/O情况
|
||||
$ pidstat -d 1
|
||||
12:04:11 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
12:04:12 999 27458 32640.00 0.00 0.00 0 mysqld
|
||||
12:04:12 0 27617 4.00 4.00 0.00 3 python
|
||||
12:04:12 0 27864 0.00 4.00 0.00 0 systemd-journal
|
||||
|
||||
```
|
||||
|
||||
从 pidstat 的输出可以看到,PID 为 27458 的 mysqld 进程正在进行大量的读,而且读取速度是 32 MB/s,跟刚才 iostat 的发现一致。两个结果一对比,我们自然就找到了磁盘 I/O 瓶颈的根源,即 mysqld 进程。
|
||||
|
||||
不过,这事儿还没完。我们自然要怀疑一下,为什么 mysqld 会去读取大量的磁盘数据呢?按照前面猜测,我们提到过,这有可能是个慢查询问题。
|
||||
|
||||
可是,回想一下,慢查询的现象大多是 CPU 使用率高(比如 100% ),但这里看到的却是 I/O 问题。看来,这并不是一个单纯的慢查询问题,我们有必要分析一下 MySQL 读取的数据。
|
||||
|
||||
要分析进程的数据读取,当然还要靠上一节用到过的 strace+ lsof 组合。
|
||||
|
||||
接下来,还是在终端一中,执行 strace 命令,并且指定 mysqld 的进程号 27458。我们知道,MySQL 是一个多线程的数据库应用,为了不漏掉这些线程的数据读取情况,你要记得在执行 stace 命令时,加上 -f 参数:
|
||||
|
||||
```
|
||||
$ strace -f -p 27458
|
||||
[pid 28014] read(38, "934EiwT363aak7VtqF1mHGa4LL4Dhbks"..., 131072) = 131072
|
||||
[pid 28014] read(38, "hSs7KBDepBqA6m4ce6i6iUfFTeG9Ot9z"..., 20480) = 20480
|
||||
[pid 28014] read(38, "NRhRjCSsLLBjTfdqiBRLvN9K6FRfqqLm"..., 131072) = 131072
|
||||
[pid 28014] read(38, "AKgsik4BilLb7y6OkwQUjjqGeCTQTaRl"..., 24576) = 24576
|
||||
[pid 28014] read(38, "hFMHx7FzUSqfFI22fQxWCpSnDmRjamaW"..., 131072) = 131072
|
||||
[pid 28014] read(38, "ajUzLmKqivcDJSkiw7QWf2ETLgvQIpfC"..., 20480) = 20480
|
||||
|
||||
```
|
||||
|
||||
观察一会,你会发现,线程 28014 正在读取大量数据,且读取文件的描述符编号为 38。这儿的 38 又对应着哪个文件呢?我们可以执行下面的 lsof 命令,并且指定线程号 28014 ,具体查看这个可疑线程和可疑文件:
|
||||
|
||||
```
|
||||
$ lsof -p 28014
|
||||
|
||||
```
|
||||
|
||||
奇怪的是,lsof 并没有给出任何输出。实际上,如果你查看 lsof 命令的返回值,就会发现,这个命令的执行失败了。
|
||||
|
||||
我们知道,在 SHELL 中,特殊标量 $? 表示上一条命令退出时的返回值。查看这个特殊标量,你会发现它的返回值是1。可是别忘了,在 Linux 中,返回值为 0 ,才表示命令执行成功。返回值为1,显然表明执行失败。
|
||||
|
||||
```
|
||||
$ echo $?
|
||||
1
|
||||
|
||||
```
|
||||
|
||||
为什么 lsof 命令执行失败了呢?这里希望你暂停往下,自己先思考一下原因。记住我的那句话,遇到现象解释不了,先去查查工具文档。
|
||||
|
||||
事实上,通过查询 lsof 的文档,你会发现,-p 参数需要指定进程号,而我们刚才传入的是线程号,所以 lsof 失败了。你看,任何一个细节都可能成为性能分析的“拦路虎”。
|
||||
|
||||
回过头我们看,mysqld 的进程号是 27458,而 28014 只是它的一个线程。而且,如果你观察 一下mysqld 进程的线程,你会发现,mysqld 其实还有很多正在运行的其他线程:
|
||||
|
||||
```
|
||||
# -t表示显示线程,-a表示显示命令行参数
|
||||
$ pstree -t -a -p 27458
|
||||
mysqld,27458 --log_bin=on --sync_binlog=1
|
||||
...
|
||||
├─{mysqld},27922
|
||||
├─{mysqld},27923
|
||||
└─{mysqld},28014
|
||||
|
||||
```
|
||||
|
||||
找到了原因,lsof的问题就容易解决了。把线程号换成进程号,继续执行 lsof 命令:
|
||||
|
||||
```
|
||||
$ lsof -p 27458
|
||||
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
|
||||
...
|
||||
mysqld 27458 999 38u REG 8,1 512440000 2601895 /var/lib/mysql/test/products.MYD
|
||||
|
||||
```
|
||||
|
||||
这次我们得到了lsof的输出。从输出中可以看到, mysqld 进程确实打开了大量文件,而根据文件描述符(FD)的编号,我们知道,描述符为 38 的是一个路径为 /var/lib/mysql/test/products.MYD 的文件。这里注意, 38 后面的 u 表示, mysqld 以读写的方式访问文件。
|
||||
|
||||
看到这个文件,熟悉 MySQL 的你可能笑了:
|
||||
|
||||
<li>
|
||||
MYD 文件,是 MyISAM 引擎用来存储表数据的文件;
|
||||
</li>
|
||||
<li>
|
||||
文件名就是数据表的名字;
|
||||
</li>
|
||||
<li>
|
||||
而这个文件的父目录,也就是数据库的名字。
|
||||
</li>
|
||||
|
||||
换句话说,这个文件告诉我们,mysqld 在读取数据库 test 中的 products 表。
|
||||
|
||||
实际上,你可以执行下面的命令,查看 mysqld 在管理数据库 test 时的存储文件。不过要注意,由于 MySQL 运行在容器中,你需要通过 docker exec 到容器中查看:
|
||||
|
||||
```
|
||||
$ docker exec -it mysql ls /var/lib/mysql/test/
|
||||
db.opt products.MYD products.MYI products.frm
|
||||
|
||||
```
|
||||
|
||||
从这里你可以发现,/var/lib/mysql/test/ 目录中有四个文件,每个文件的作用分别是:
|
||||
|
||||
<li>
|
||||
MYD 文件用来存储表的数据;
|
||||
</li>
|
||||
<li>
|
||||
MYI 文件用来存储表的索引;
|
||||
</li>
|
||||
<li>
|
||||
frm 文件用来存储表的元信息(比如表结构);
|
||||
</li>
|
||||
<li>
|
||||
opt 文件则用来存储数据库的元信息(比如字符集、字符校验规则等)。
|
||||
</li>
|
||||
|
||||
当然,看到这些,你可能还有一个疑问,那就是,这些文件到底是不是 mysqld 正在使用的数据库文件呢?有没有可能是不再使用的旧数据呢?其实,这个很容易确认,查一下 mysqld 配置的数据路径即可。
|
||||
|
||||
你可以在终端一中,继续执行下面的命令:
|
||||
|
||||
```
|
||||
$ docker exec -i -t mysql mysql -e 'show global variables like "%datadir%";'
|
||||
+---------------+-----------------+
|
||||
| Variable_name | Value |
|
||||
+---------------+-----------------+
|
||||
| datadir | /var/lib/mysql/ |
|
||||
+---------------+-----------------+
|
||||
|
||||
```
|
||||
|
||||
这里可以看到,/var/lib/mysql/ 确实是 mysqld 正在使用的数据存储目录。刚才分析得出的数据库 test 和数据表 products ,都是正在使用。
|
||||
|
||||
>
|
||||
注:其实 lsof 的结果已经可以确认,它们都是 mysqld 正在访问的文件。再查询 datadir ,只是想换一个思路,进一步确认一下。
|
||||
|
||||
|
||||
既然已经找出了数据库和表,接下来要做的,就是弄清楚数据库中正在执行什么样的 SQL 了。我们继续在终端一中,运行下面的 docker exec 命令,进入 MySQL 的命令行界面:
|
||||
|
||||
```
|
||||
$ docker exec -i -t mysql mysql
|
||||
...
|
||||
|
||||
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
|
||||
|
||||
mysql>
|
||||
|
||||
```
|
||||
|
||||
下一步你应该可以想到,那就是在 MySQL 命令行界面中,执行 show processlist 命令,来查看当前正在执行的 SQL 语句。
|
||||
|
||||
不过,为了保证 SQL 语句不截断,这里我们可以执行 show full processlist 命令。如果一切正常,你应该可以看到如下输出:
|
||||
|
||||
```
|
||||
mysql> show full processlist;
|
||||
+----+------+-----------------+------+---------+------+--------------+-----------------------------------------------------+
|
||||
| Id | User | Host | db | Command | Time | State | Info |
|
||||
+----+------+-----------------+------+---------+------+--------------+-----------------------------------------------------+
|
||||
| 27 | root | localhost | test | Query | 0 | init | show full processlist |
|
||||
| 28 | root | 127.0.0.1:42262 | test | Query | 1 | Sending data | select * from products where productName='geektime' |
|
||||
+----+------+-----------------+------+---------+------+--------------+-----------------------------------------------------+
|
||||
2 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
这个输出中,
|
||||
|
||||
<li>
|
||||
db 表示数据库的名字;
|
||||
</li>
|
||||
<li>
|
||||
Command 表示 SQL 类型;
|
||||
</li>
|
||||
<li>
|
||||
Time 表示执行时间;
|
||||
</li>
|
||||
<li>
|
||||
State 表示状态;
|
||||
</li>
|
||||
<li>
|
||||
而 Info 则包含了完整的 SQL 语句。
|
||||
</li>
|
||||
|
||||
多执行几次 show full processlist 命令,你可看到 select * from products where productName=‘geektime’ 这条 SQL 语句的执行时间比较长。
|
||||
|
||||
再回忆一下,案例开始时,我们在终端二查询的产品名称 [http://192.168.0.10:10000/products/geektime](http://192.168.0.10:10000/products/geektime),其中的 geektime 也符合这条查询语句的条件。
|
||||
|
||||
我们知道,MySQL 的慢查询问题,很可能是没有利用好索引导致的,那这条查询语句是不是这样呢?我们又该怎么确认,查询语句是否利用了索引呢?
|
||||
|
||||
其实,MySQL 内置的 explain 命令,就可以帮你解决这个问题。继续在 MySQL 终端中,运行下面的 explain 命令:
|
||||
|
||||
```
|
||||
# 切换到test库
|
||||
mysql> use test;
|
||||
# 执行explain命令
|
||||
mysql> explain select * from products where productName='geektime';
|
||||
+----+-------------+----------+------+---------------+------+---------+------+-------+-------------+
|
||||
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|
||||
+----+-------------+----------+------+---------------+------+---------+------+-------+-------------+
|
||||
| 1 | SIMPLE | products | ALL | NULL | NULL | NULL | NULL | 10000 | Using where |
|
||||
+----+-------------+----------+------+---------------+------+---------+------+-------+-------------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
观察这次的输出。这个界面中,有几个比较重要的字段需要你注意,我就以这个输出为例,分别解释一下:
|
||||
|
||||
<li>
|
||||
select_type 表示查询类型,而这里的SIMPLE 表示此查询不包括 UNION 查询或者子查询;
|
||||
</li>
|
||||
<li>
|
||||
table 表示数据表的名字,这里是 products;
|
||||
</li>
|
||||
<li>
|
||||
type 表示查询类型,这里的 ALL 表示全表查询,但索引查询应该是 index 类型才对;
|
||||
</li>
|
||||
<li>
|
||||
possible_keys 表示可能选用的索引,这里是 NULL;
|
||||
</li>
|
||||
<li>
|
||||
key 表示确切会使用的索引,这里也是 NULL;
|
||||
</li>
|
||||
<li>
|
||||
rows 表示查询扫描的行数,这里是 10000。
|
||||
</li>
|
||||
|
||||
根据这些信息,我们可以确定,这条查询语句压根儿没有使用索引,所以查询时,会扫描全表,并且扫描行数高达 10000 行。响应速度那么慢也就难怪了。
|
||||
|
||||
走到这一步,你应该很容易想到优化方法,没有索引那我们就自己建立,给 productName 建立索引就可以了。不过,增加索引前,你需要先弄清楚,这个表结构到底长什么样儿。
|
||||
|
||||
执行下面的 MySQL 命令,查询 products 表的结构,你会看到,它只有一个 id 主键,并不包括 productName 的索引:
|
||||
|
||||
```
|
||||
mysql> show create table products;
|
||||
...
|
||||
| products | CREATE TABLE `products` (
|
||||
`id` int(11) NOT NULL,
|
||||
`productCode` text NOT NULL COMMENT '产品代码',
|
||||
`productName` text NOT NULL COMMENT '产品名称',
|
||||
...
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC |
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
接下来,我们就可以给 productName 建立索引了,也就是执行下面的 CREATE INDEX 命令:
|
||||
|
||||
```
|
||||
mysql> CREATE INDEX products_index ON products (productName);
|
||||
ERROR 1170 (42000): BLOB/TEXT column 'productName' used in key specification without a key length
|
||||
|
||||
```
|
||||
|
||||
不过,醒目的ERROR告诉我们,这条命令运行失败了。根据错误信息,productName 是一个 BLOB/TEXT 类型,需要设置一个长度。所以,想要创建索引,就必须为 productName 指定一个前缀长度。
|
||||
|
||||
那前缀长度设置为多大比较合适呢?这里其实有专门的算法,即通过计算前缀长度的选择性,来确定索引的长度。不过,我们可以稍微简化一下,直接使用一个固定数值(比如 64),执行下面的命令创建索引:
|
||||
|
||||
```
|
||||
mysql> CREATE INDEX products_index ON products (productName(64));
|
||||
Query OK, 10000 rows affected (14.45 sec)
|
||||
Records: 10000 Duplicates: 0 Warnings: 0
|
||||
|
||||
```
|
||||
|
||||
现在可以看到,索引已经建好了。能做的都做完了,最后就该检查一下,性能问题是否已经解决了。
|
||||
|
||||
我们切换到终端二中,查看还在执行的 curl 命令的结果:
|
||||
|
||||
```
|
||||
Got data: ()in 15.383180141448975 sec
|
||||
Got data: ()in 15.384996891021729 sec
|
||||
Got data: ()in 0.0021054744720458984 sec
|
||||
Got data: ()in 0.003951072692871094 sec
|
||||
|
||||
```
|
||||
|
||||
显然,查询时间已经从 15 秒缩短到了 3 毫秒。看来,没有索引果然就是这次性能问题的罪魁祸首,解决了索引,就解决了查询慢的问题。
|
||||
|
||||
## 案例思考
|
||||
|
||||
到这里,商品搜索应用查询慢的问题已经完美解决了。但是,对于这个案例,我还有一点想说明一下。
|
||||
|
||||
不知道你还记不记得,案例开始时,我们启动的几个容器应用。除了 MySQL 和商品搜索应用外,还有一个 DataService 应用。为什么这个案例开始时,要运行一个看起来毫不相关的应用呢?
|
||||
|
||||
实际上,DataService 是一个严重影响 MySQL 性能的干扰应用。抛开上述索引优化方法不说,这个案例还有一种优化方法,也就是停止 DataService 应用。
|
||||
|
||||
接下来,我们就删除数据库索引,回到原来的状态;然后停止 DataService 应用,看看优化效果如何。
|
||||
|
||||
首先,我们在终端二中停止 curl 命令,然后回到终端一中,执行下面的命令删除索引:
|
||||
|
||||
```
|
||||
# 删除索引
|
||||
$ docker exec -i -t mysql mysql
|
||||
|
||||
mysql> use test;
|
||||
mysql> DROP INDEX products_index ON products;
|
||||
|
||||
```
|
||||
|
||||
接着,在终端二中重新运行 curl 命令。当然,这次你会发现,处理时间又变慢了:
|
||||
|
||||
```
|
||||
$ while true; do curl http://192.168.0.10:10000/products/geektime; sleep 5; done
|
||||
Got data: ()in 16.884345054626465 sec
|
||||
|
||||
```
|
||||
|
||||
接下来,再次回到终端一中,执行下面的命令,停止 DataService 应用:
|
||||
|
||||
```
|
||||
# 停止 DataService 应用
|
||||
$ docker rm -f dataservice
|
||||
|
||||
```
|
||||
|
||||
最后,我们回到终端二中,观察 curl 的结果:
|
||||
|
||||
```
|
||||
Got data: ()in 16.884345054626465 sec
|
||||
Got data: ()in 15.238174200057983 sec
|
||||
Got data: ()in 0.12604427337646484 sec
|
||||
Got data: ()in 0.1101069450378418 sec
|
||||
Got data: ()in 0.11235237121582031 sec
|
||||
|
||||
```
|
||||
|
||||
果然,停止 DataService 后,处理时间从 15 秒缩短到了 0.1 秒,虽然比不上增加索引后的 3 毫秒,但相对于 15 秒来说,优化效果还是非常明显的。
|
||||
|
||||
那么,这种情况下,还有没有 I/O 瓶颈了呢?
|
||||
|
||||
我们切换到终端一中,运行下面的 vmstat 命令(注意不是 iostat,稍后解释原因),观察 I/O 的变化情况:
|
||||
|
||||
```
|
||||
$ vmstat 1
|
||||
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
|
||||
r b swpd free buff cache si so bi bo in cs us sy id wa st
|
||||
0 1 0 6809304 1368 856744 0 0 32640 0 52 478 1 0 50 49 0
|
||||
0 1 0 6776620 1368 889456 0 0 32640 0 33 490 0 0 50 49 0
|
||||
0 0 0 6747540 1368 918576 0 0 29056 0 42 568 0 0 56 44 0
|
||||
0 0 0 6747540 1368 918576 0 0 0 0 40 141 1 0 100 0 0
|
||||
0 0 0 6747160 1368 918576 0 0 0 0 40 148 0 1 99 0 0
|
||||
|
||||
```
|
||||
|
||||
你可以看到,磁盘读(bi)和 iowait(wa)刚开始还是挺大的,但没过多久,就都变成了 0 。换句话说,I/O 瓶颈消失了。
|
||||
|
||||
这是为什么呢?原因先留个悬念,作为今天的思考题。
|
||||
|
||||
回过头来解释一下刚刚的操作,在查看 I/O 情况时,我并没用 iostat 命令,而是用了 vmstat。其实,相对于 iostat 来说,vmstat 可以同时提供 CPU、内存和 I/O 的使用情况。
|
||||
|
||||
在性能分析过程中,能够综合多个指标,并结合系统的工作原理进行分析,对解释性能现象通常会有意想不到的帮助。
|
||||
|
||||
## 小结
|
||||
|
||||
今天我们分析了一个商品搜索的应用程序。我们先是通过 top、iostat 分析了系统的 CPU 和磁盘使用情况,发现了磁盘的 I/O 瓶颈。
|
||||
|
||||
接着,我们借助 pidstat ,发现瓶颈是 mysqld 导致的。紧接着,我们又通过 strace、lsof,找出了 mysqld 正在读的文件。同时,根据文件的名字和路径,我们找出了 mysqld 正在操作的数据库和数据表。综合这些信息,我们判断,这是一个没有利用索引导致的慢查询问题。
|
||||
|
||||
于是,我们登录到 MySQL 命令行终端,用数据库分析工具进行验证,发现 MySQL 查询语句访问的字段,果然没有索引。所以,增加索引,就可以解决案例的性能问题了。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,给你留一个思考题,也是我在案例最后部分提到过的,停止 DataService 后,商品搜索应用的处理时间,从 15 秒缩短到了 0.1 秒。这是为什么呢?
|
||||
|
||||
我给个小小的提示。你可以先查看 [dataservice.py](http://dataservice.py) 的[源码](https://github.com/feiskyer/linux-perf-examples/blob/master/mysql-slow/dataservice.py),你会发现,DataService 实际上是在读写一个仅包括 “data” 字符串的小文件。不过在读取文件前,它会先把 /proc/sys/vm/drop_caches 改成 1。
|
||||
|
||||
还记得这个操作有什么作用吗?如果不记得,可以用 man 查询 proc 文件系统的文档。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
417
极客时间专栏/Linux性能优化实战/I|O 性能篇/29 | 案例篇:Redis响应严重延迟,如何解决?.md
Normal file
417
极客时间专栏/Linux性能优化实战/I|O 性能篇/29 | 案例篇:Redis响应严重延迟,如何解决?.md
Normal file
@@ -0,0 +1,417 @@
|
||||
<audio id="audio" title="29 | 案例篇:Redis响应严重延迟,如何解决?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d8/cc/d81f154d511dcc2c4927472ed3d589cc.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一节,我们一起分析了一个基于 MySQL 的商品搜索案例,先来回顾一下。
|
||||
|
||||
在访问商品搜索接口时,我们发现接口的响应特别慢。通过对系统 CPU、内存和磁盘 I/O 等资源使用情况的分析,我们发现这时出现了磁盘的 I/O 瓶颈,并且正是案例应用导致的。
|
||||
|
||||
接着,我们借助 pidstat,发现罪魁祸首是 mysqld 进程。我们又通过 strace、lsof,找出了 mysqld 正在读的文件。根据文件的名字和路径,我们找出了 mysqld 正在操作的数据库和数据表。综合这些信息,我们猜测这是一个没利用索引导致的慢查询问题。
|
||||
|
||||
为了验证猜测,我们到 MySQL 命令行终端,使用数据库分析工具发现,案例应用访问的字段果然没有索引。既然猜测是正确的,那增加索引后,问题就自然解决了。
|
||||
|
||||
从这个案例你会发现,MySQL 的 MyISAM 引擎,主要依赖系统缓存加速磁盘 I/O 的访问。可如果系统中还有其他应用同时运行, MyISAM 引擎很难充分利用系统缓存。缓存可能会被其他应用程序占用,甚至被清理掉。
|
||||
|
||||
所以,一般我并不建议,把应用程序的性能优化完全建立在系统缓存上。最好能在应用程序的内部分配内存,构建完全自主控制的缓存;或者使用第三方的缓存应用,比如 Memcached、Redis 等。
|
||||
|
||||
Redis 是最常用的键值存储系统之一,常用作数据库、高速缓存和消息队列代理等。Redis 基于内存来存储数据,不过,为了保证在服务器异常时数据不丢失,很多情况下,我们要为它配置持久化,而这就可能会引发磁盘 I/O 的性能问题。
|
||||
|
||||
今天,我就带你一起来分析一个利用 Redis 作为缓存的案例。这同样是一个基于 Python Flask 的应用程序,它提供了一个 查询缓存的接口,但接口的响应时间比较长,并不能满足线上系统的要求。
|
||||
|
||||
非常感谢携程系统研发部资深后端工程师董国星,帮助提供了今天的案例。
|
||||
|
||||
## 案例准备
|
||||
|
||||
本次案例还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
|
||||
|
||||
<li>
|
||||
机器配置:2 CPU,8GB 内存
|
||||
</li>
|
||||
<li>
|
||||
预先安装 docker、sysstat 、git、make 等工具,如 apt install [docker.io](http://docker.io) sysstat
|
||||
</li>
|
||||
|
||||
今天的案例由 Python应用+Redis 两部分组成。其中,Python 应用是一个基于 Flask 的应用,它会利用 Redis ,来管理应用程序的缓存,并对外提供三个 HTTP 接口:
|
||||
|
||||
<li>
|
||||
/:返回 hello redis;
|
||||
</li>
|
||||
<li>
|
||||
/init/<num>:插入指定数量的缓存数据,如果不指定数量,默认的是 5000 条;</num>
|
||||
</li>
|
||||
<li>
|
||||
缓存的键格式为 uuid:<uuid></uuid>
|
||||
</li>
|
||||
<li>
|
||||
缓存的值为 good、bad 或 normal 三者之一
|
||||
</li>
|
||||
<li>
|
||||
/get_cache/<type_name>:查询指定值的缓存数据,并返回处理时间。其中,type_name 参数只支持 good, bad 和 normal(也就是找出具有相同 value 的 key 列表)。
|
||||
</li>
|
||||
|
||||
由于应用比较多,为了方便你运行,我把它们打包成了两个 Docker 镜像,并推送到了 [Github](https://github.com/feiskyer/linux-perf-examples/tree/master/redis-slow) 上。这样你就只需要运行几条命令,就可以启动了。
|
||||
|
||||
今天的案例需要两台虚拟机,其中一台用作案例分析的目标机器,运行 Flask 应用,它的 IP 地址是 192.168.0.10;而另一台作为客户端,请求缓存查询接口。我画了一张图来表示它们的关系。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c8/87/c8e0ca06d70a1c7f1520d103a3edfc87.png" alt="">
|
||||
|
||||
接下来,打开两个终端,分别 SSH 登录到这两台虚拟机中,并在第一台虚拟机中安装上述工具。
|
||||
|
||||
跟以前一样,案例中所有命令都默认以 root 用户运行,如果你是用普通用户身份登陆系统,请运行 sudo su root 命令切换到 root 用户。
|
||||
|
||||
到这里,准备工作就完成了。接下来,我们正式进入操作环节。
|
||||
|
||||
## 案例分析
|
||||
|
||||
首先,我们在第一个终端中,执行下面的命令,运行本次案例要分析的目标应用。正常情况下,你应该可以看到下面的输出:
|
||||
|
||||
```
|
||||
# 注意下面的随机字符串是容器ID,每次运行均会不同,并且你不需要关注它
|
||||
$ docker run --name=redis -itd -p 10000:80 feisky/redis-server
|
||||
ec41cb9e4dd5cb7079e1d9f72b7cee7de67278dbd3bd0956b4c0846bff211803
|
||||
$ docker run --name=app --network=container:redis -itd feisky/redis-app
|
||||
2c54eb252d0552448320d9155a2618b799a1e71d7289ec7277a61e72a9de5fd0
|
||||
|
||||
```
|
||||
|
||||
然后,再运行 docker ps 命令,确认两个容器都处于运行(Up)状态:
|
||||
|
||||
```
|
||||
$ docker ps
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
2c54eb252d05 feisky/redis-app "python /app.py" 48 seconds ago Up 47 seconds app
|
||||
ec41cb9e4dd5 feisky/redis-server "docker-entrypoint.s…" 49 seconds ago Up 48 seconds 6379/tcp, 0.0.0.0:10000->80/tcp redis
|
||||
|
||||
|
||||
```
|
||||
|
||||
今天的应用在 10000 端口监听,所以你可以通过 [http://192.168.0.10:10000](http://192.168.0.10:10000) ,来访问前面提到的三个接口。
|
||||
|
||||
比如,我们切换到第二个终端,使用 curl 工具,访问应用首页。如果你看到 `hello redis` 的输出,说明应用正常启动:
|
||||
|
||||
```
|
||||
$ curl http://192.168.0.10:10000/
|
||||
hello redis
|
||||
|
||||
```
|
||||
|
||||
接下来,继续在终端二中,执行下面的 curl 命令,来调用应用的 /init 接口,初始化 Redis 缓存,并且插入 5000 条缓存信息。这个过程比较慢,比如我的机器就花了十几分钟时间。耐心等一会儿后,你会看到下面这行输出:
|
||||
|
||||
```
|
||||
# 案例插入5000条数据,在实践时可以根据磁盘的类型适当调整,比如使用SSD时可以调大,而HDD可以适当调小
|
||||
$ curl http://192.168.0.10:10000/init/5000
|
||||
{"elapsed_seconds":30.26814079284668,"keys_initialized":5000}
|
||||
|
||||
```
|
||||
|
||||
继续执行下一个命令,访问应用的缓存查询接口。如果一切正常,你会看到如下输出:
|
||||
|
||||
```
|
||||
$ curl http://192.168.0.10:10000/get_cache
|
||||
{"count":1677,"data":["d97662fa-06ac-11e9-92c7-0242ac110002",...],"elapsed_seconds":10.545469760894775,"type":"good"}
|
||||
|
||||
```
|
||||
|
||||
我们看到,这个接口调用居然要花 10 秒!这么长的响应时间,显然不能满足实际的应用需求。
|
||||
|
||||
到底出了什么问题呢?我们还是要用前面学过的性能工具和原理,来找到这个瓶颈。
|
||||
|
||||
不过别急,同样为了避免分析过程中客户端的请求结束,在进行性能分析前,我们先要把 curl 命令放到一个循环里来执行。你可以在终端二中,继续执行下面的命令:
|
||||
|
||||
```
|
||||
$ while true; do curl http://192.168.0.10:10000/get_cache; done
|
||||
|
||||
```
|
||||
|
||||
接下来,再重新回到终端一,查找接口响应慢的“病因”。
|
||||
|
||||
最近几个案例的现象都是响应很慢,这种情况下,我们自然先会怀疑,是不是系统资源出现了瓶颈。所以,先观察 CPU、内存和磁盘 I/O 等的使用情况肯定不会错。
|
||||
|
||||
我们先在终端一中执行 top 命令,分析系统的 CPU 使用情况:
|
||||
|
||||
```
|
||||
$ top
|
||||
top - 12:46:18 up 11 days, 8:49, 1 user, load average: 1.36, 1.36, 1.04
|
||||
Tasks: 137 total, 1 running, 79 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 6.0 us, 2.7 sy, 0.0 ni, 5.7 id, 84.7 wa, 0.0 hi, 1.0 si, 0.0 st
|
||||
%Cpu1 : 1.0 us, 3.0 sy, 0.0 ni, 94.7 id, 0.0 wa, 0.0 hi, 1.3 si, 0.0 st
|
||||
KiB Mem : 8169300 total, 7342244 free, 432912 used, 394144 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 7478748 avail Mem
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
9181 root 20 0 193004 27304 8716 S 8.6 0.3 0:07.15 python
|
||||
9085 systemd+ 20 0 28352 9760 1860 D 5.0 0.1 0:04.34 redis-server
|
||||
368 root 20 0 0 0 0 D 1.0 0.0 0:33.88 jbd2/sda1-8
|
||||
149 root 0 -20 0 0 0 I 0.3 0.0 0:10.63 kworker/0:1H
|
||||
1549 root 20 0 236716 24576 9864 S 0.3 0.3 91:37.30 python3
|
||||
|
||||
```
|
||||
|
||||
观察 top 的输出可以发现,CPU0 的 iowait 比较高,已经达到了 84%;而各个进程的 CPU 使用率都不太高,最高的 python 和 redis-server ,也分别只有 8% 和 5%。再看内存,总内存 8GB,剩余内存还有 7GB多,显然内存也没啥问题。
|
||||
|
||||
综合top的信息,最有嫌疑的就是 iowait。所以,接下来还是要继续分析,是不是 I/O 问题。
|
||||
|
||||
还在第一个终端中,先按下 Ctrl+C,停止 top 命令;然后,执行下面的 iostat 命令,查看有没有 I/O 性能问题:
|
||||
|
||||
```
|
||||
$ iostat -d -x 1
|
||||
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
|
||||
...
|
||||
sda 0.00 492.00 0.00 2672.00 0.00 176.00 0.00 26.35 0.00 1.76 0.00 0.00 5.43 0.00 0.00
|
||||
|
||||
```
|
||||
|
||||
观察 iostat 的输出,我们发现,磁盘 sda 每秒的写数据(wkB/s)为 2.5MB,I/O 使用率(%util)是 0。看来,虽然有些 I/O操作,但并没导致磁盘的 I/O 瓶颈。
|
||||
|
||||
排查一圈儿下来,CPU和内存使用没问题,I/O 也没有瓶颈,接下来好像就没啥分析方向了?
|
||||
|
||||
碰到这种情况,还是那句话,反思一下,是不是又漏掉什么有用线索了。你可以先自己思考一下,从分析对象(案例应用)、系统原理和性能工具这三个方向下功夫,回忆它们的特性,查找现象的异常,再继续往下走。
|
||||
|
||||
回想一下,今天的案例问题是从 Redis 缓存中查询数据慢。对查询来说,对应的 I/O 应该是磁盘的读操作,但刚才我们用 iostat 看到的却是写操作。虽说 I/O 本身并没有性能瓶颈,但这里的磁盘写也是比较奇怪的。为什么会有磁盘写呢?那我们就得知道,到底是哪个进程在写磁盘。
|
||||
|
||||
要知道 I/O请求来自哪些进程,还是要靠我们的老朋友 pidstat。在终端一中运行下面的 pidstat 命令,观察进程的 I/O 情况:
|
||||
|
||||
```
|
||||
$ pidstat -d 1
|
||||
12:49:35 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
|
||||
12:49:36 0 368 0.00 16.00 0.00 86 jbd2/sda1-8
|
||||
12:49:36 100 9085 0.00 636.00 0.00 1 redis-server
|
||||
|
||||
```
|
||||
|
||||
从 pidstat 的输出,我们看到,I/O 最多的进程是 PID 为 9085 的 redis-server,并且它也刚好是在写磁盘。这说明,确实是 redis-server 在进行磁盘写。
|
||||
|
||||
当然,光找到读写磁盘的进程还不够,我们还要再用 strace+lsof 组合,看看 redis-server 到底在写什么。
|
||||
|
||||
接下来,还是在终端一中,执行 strace 命令,并且指定 redis-server 的进程号 9085:
|
||||
|
||||
```
|
||||
# -f表示跟踪子进程和子线程,-T表示显示系统调用的时长,-tt表示显示跟踪时间
|
||||
$ strace -f -T -tt -p 9085
|
||||
[pid 9085] 14:20:16.826131 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 65, NULL, 8) = 1 <0.000055>
|
||||
[pid 9085] 14:20:16.826301 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:5b2e76cc-"..., 16384) = 61 <0.000071>
|
||||
[pid 9085] 14:20:16.826477 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000063>
|
||||
[pid 9085] 14:20:16.826645 write(8, "$3\r\nbad\r\n", 9) = 9 <0.000173>
|
||||
[pid 9085] 14:20:16.826907 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 65, NULL, 8) = 1 <0.000032>
|
||||
[pid 9085] 14:20:16.827030 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:55862ada-"..., 16384) = 61 <0.000044>
|
||||
[pid 9085] 14:20:16.827149 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000043>
|
||||
[pid 9085] 14:20:16.827285 write(8, "$3\r\nbad\r\n", 9) = 9 <0.000141>
|
||||
[pid 9085] 14:20:16.827514 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 64, NULL, 8) = 1 <0.000049>
|
||||
[pid 9085] 14:20:16.827641 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:53522908-"..., 16384) = 61 <0.000043>
|
||||
[pid 9085] 14:20:16.827784 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000034>
|
||||
[pid 9085] 14:20:16.827945 write(8, "$4\r\ngood\r\n", 10) = 10 <0.000288>
|
||||
[pid 9085] 14:20:16.828339 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 63, NULL, 8) = 1 <0.000057>
|
||||
[pid 9085] 14:20:16.828486 read(8, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 16384) = 67 <0.000040>
|
||||
[pid 9085] 14:20:16.828623 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000052>
|
||||
[pid 9085] 14:20:16.828760 write(7, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 67) = 67 <0.000060>
|
||||
[pid 9085] 14:20:16.828970 fdatasync(7) = 0 <0.005415>
|
||||
[pid 9085] 14:20:16.834493 write(8, ":1\r\n", 4) = 4 <0.000250>
|
||||
|
||||
```
|
||||
|
||||
观察一会儿,有没有发现什么有趣的现象呢?
|
||||
|
||||
事实上,从系统调用来看, epoll_pwait、read、write、fdatasync 这些系统调用都比较频繁。那么,刚才观察到的写磁盘,应该就是 write 或者 fdatasync 导致的了。
|
||||
|
||||
接着再来运行 lsof 命令,找出这些系统调用的操作对象:
|
||||
|
||||
```
|
||||
$ lsof -p 9085
|
||||
redis-ser 9085 systemd-network 3r FIFO 0,12 0t0 15447970 pipe
|
||||
redis-ser 9085 systemd-network 4w FIFO 0,12 0t0 15447970 pipe
|
||||
redis-ser 9085 systemd-network 5u a_inode 0,13 0 10179 [eventpoll]
|
||||
redis-ser 9085 systemd-network 6u sock 0,9 0t0 15447972 protocol: TCP
|
||||
redis-ser 9085 systemd-network 7w REG 8,1 8830146 2838532 /data/appendonly.aof
|
||||
redis-ser 9085 systemd-network 8u sock 0,9 0t0 15448709 protocol: TCP
|
||||
|
||||
```
|
||||
|
||||
现在你会发现,描述符编号为 3 的是一个 pipe 管道,5 号是 eventpoll,7 号是一个普通文件,而 8 号是一个 TCP socket。
|
||||
|
||||
结合磁盘写的现象,我们知道,只有 7 号普通文件才会产生磁盘写,而它操作的文件路径是 /data/appendonly.aof,相应的系统调用包括 write 和 fdatasync。
|
||||
|
||||
如果你对 Redis 的持久化配置比较熟,看到这个文件路径以及 fdatasync 的系统调用,你应该能想到,这对应着正是 Redis 持久化配置中的 appendonly 和 appendfsync 选项。很可能是因为它们的配置不合理,导致磁盘写比较多。
|
||||
|
||||
接下来就验证一下这个猜测,我们可以通过 Redis 的命令行工具,查询这两个选项的配置。
|
||||
|
||||
继续在终端一中,运行下面的命令,查询 appendonly 和 appendfsync 的配置:
|
||||
|
||||
```
|
||||
$ docker exec -it redis redis-cli config get 'append*'
|
||||
1) "appendfsync"
|
||||
2) "always"
|
||||
3) "appendonly"
|
||||
4) "yes"
|
||||
|
||||
```
|
||||
|
||||
从这个结果你可以发现,appendfsync 配置的是 always,而 appendonly 配置的是 yes。这两个选项的详细含义,你可以从 [Redis Persistence](https://redis.io/topics/persistence) 的文档中查到,这里我做一下简单介绍。
|
||||
|
||||
Redis 提供了两种数据持久化的方式,分别是快照和追加文件。
|
||||
|
||||
**快照方式**,会按照指定的时间间隔,生成数据的快照,并且保存到磁盘文件中。为了避免阻塞主进程,Redis 还会 fork 出一个子进程,来负责快照的保存。这种方式的性能好,无论是备份还是恢复,都比追加文件好很多。
|
||||
|
||||
不过,它的缺点也很明显。在数据量大时,fork子进程需要用到比较大的内存,保存数据也很耗时。所以,你需要设置一个比较长的时间间隔来应对,比如至少5分钟。这样,如果发生故障,你丢失的就是几分钟的数据。
|
||||
|
||||
**追加文件**,则是用在文件末尾追加记录的方式,对 Redis 写入的数据,依次进行持久化,所以它的持久化也更安全。
|
||||
|
||||
此外,它还提供了一个用 appendfsync 选项设置 fsync 的策略,确保写入的数据都落到磁盘中,具体选项包括 always、everysec、no 等。
|
||||
|
||||
<li>
|
||||
always表示,每个操作都会执行一次 fsync,是最为安全的方式;
|
||||
</li>
|
||||
<li>
|
||||
everysec表示,每秒钟调用一次 fsync ,这样可以保证即使是最坏情况下,也只丢失1秒的数据;
|
||||
</li>
|
||||
<li>
|
||||
而 no 表示交给操作系统来处理。
|
||||
</li>
|
||||
|
||||
回忆一下我们刚刚看到的配置,appendfsync 配置的是 always,意味着每次写数据时,都会调用一次 fsync,从而造成比较大的磁盘 I/O 压力。
|
||||
|
||||
当然,你还可以用 strace ,观察这个系统调用的执行情况。比如通过 -e 选项指定 fdatasync 后,你就会得到下面的结果:
|
||||
|
||||
```
|
||||
$ strace -f -p 9085 -T -tt -e fdatasync
|
||||
strace: Process 9085 attached with 4 threads
|
||||
[pid 9085] 14:22:52.013547 fdatasync(7) = 0 <0.007112>
|
||||
[pid 9085] 14:22:52.022467 fdatasync(7) = 0 <0.008572>
|
||||
[pid 9085] 14:22:52.032223 fdatasync(7) = 0 <0.006769>
|
||||
...
|
||||
[pid 9085] 14:22:52.139629 fdatasync(7) = 0 <0.008183>
|
||||
|
||||
```
|
||||
|
||||
从这里你可以看到,每隔 10ms 左右,就会有一次 fdatasync 调用,并且每次调用本身也要消耗 7~8ms。
|
||||
|
||||
不管哪种方式,都可以验证我们的猜想,配置确实不合理。这样,我们就找出了 Redis 正在进行写入的文件,也知道了产生大量 I/O 的原因。
|
||||
|
||||
不过,回到最初的疑问,为什么查询时会有磁盘写呢?按理来说不应该只有数据的读取吗?这就需要我们再来审查一下 strace -f -T -tt -p 9085 的结果。
|
||||
|
||||
```
|
||||
read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:53522908-"..., 16384)
|
||||
write(8, "$4\r\ngood\r\n", 10)
|
||||
read(8, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 16384)
|
||||
write(7, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 67)
|
||||
write(8, ":1\r\n", 4)
|
||||
|
||||
```
|
||||
|
||||
细心的你应该记得,根据 lsof 的分析,文件描述符编号为 7 的是一个普通文件 /data/appendonly.aof,而编号为 8 的是 TCP socket。而观察上面的内容,8 号对应的 TCP 读写,是一个标准的“请求-响应”格式,即:
|
||||
|
||||
<li>
|
||||
从 socket 读取 GET uuid:53522908-… 后,响应 good;
|
||||
</li>
|
||||
<li>
|
||||
再从 socket 读取 SADD good 535… 后,响应 1。
|
||||
</li>
|
||||
|
||||
对 Redis 来说,SADD是一个写操作,所以 Redis 还会把它保存到用于持久化的 appendonly.aof 文件中。
|
||||
|
||||
观察更多的 strace 结果,你会发现,每当 GET 返回 good 时,随后都会有一个 SADD 操作,这也就导致了,明明是查询接口,Redis 却有大量的磁盘写。
|
||||
|
||||
到这里,我们就找出了 Redis 写磁盘的原因。不过,在下最终结论前,我们还是要确认一下,8 号 TCP socket 对应的 Redis 客户端,到底是不是我们的案例应用。
|
||||
|
||||
我们可以给 lsof 命令加上 -i 选项,找出 TCP socket 对应的 TCP 连接信息。不过,由于 Redis 和 Python 应用都在容器中运行,我们需要进入容器的网络命名空间内部,才能看到完整的 TCP 连接。
|
||||
|
||||
>
|
||||
<p>注意:下面的命令用到的 [nsenter](http://man7.org/linux/man-pages/man1/nsenter.1.html) 工具,可以进入容器命名空间。如果你的系统没有安装,请运行下面命令安装 nsenter:<br>
|
||||
docker run --rm -v /usr/local/bin:/target jpetazzo/nsenter</p>
|
||||
|
||||
|
||||
还是在终端一中,运行下面的命令:
|
||||
|
||||
```
|
||||
# 由于这两个容器共享同一个网络命名空间,所以我们只需要进入app的网络命名空间即可
|
||||
$ PID=$(docker inspect --format {{.State.Pid}} app)
|
||||
# -i表示显示网络套接字信息
|
||||
$ nsenter --target $PID --net -- lsof -i
|
||||
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
|
||||
redis-ser 9085 systemd-network 6u IPv4 15447972 0t0 TCP localhost:6379 (LISTEN)
|
||||
redis-ser 9085 systemd-network 8u IPv4 15448709 0t0 TCP localhost:6379->localhost:32996 (ESTABLISHED)
|
||||
python 9181 root 3u IPv4 15448677 0t0 TCP *:http (LISTEN)
|
||||
python 9181 root 5u IPv4 15449632 0t0 TCP localhost:32996->localhost:6379 (ESTABLISHED)
|
||||
|
||||
|
||||
```
|
||||
|
||||
这次我们可以看到,redis-server 的 8 号文件描述符,对应 TCP 连接 localhost:6379->localhost:32996。其中, localhost:6379 是 redis-server 自己的监听端口,自然 localhost:32996 就是 redis 的客户端。再观察最后一行,localhost:32996 对应的,正是我们的 Python 应用程序(进程号为 9181)。
|
||||
|
||||
历经各种波折,我们总算找出了 Redis 响应延迟的潜在原因。总结一下,我们找到两个问题。
|
||||
|
||||
第一个问题,Redis 配置的 appendfsync 是 always,这就导致 Redis 每次的写操作,都会触发 fdatasync 系统调用。今天的案例,没必要用这么高频的同步写,使用默认的 1s 时间间隔,就足够了。
|
||||
|
||||
第二个问题,Python 应用在查询接口中会调用 Redis 的 SADD 命令,这很可能是不合理使用缓存导致的。
|
||||
|
||||
对于第一个配置问题,我们可以执行下面的命令,把 appendfsync 改成 everysec:
|
||||
|
||||
```
|
||||
$ docker exec -it redis redis-cli config set appendfsync everysec
|
||||
OK
|
||||
|
||||
```
|
||||
|
||||
改完后,切换到终端二中查看,你会发现,现在的请求时间,已经缩短到了 0.9s:
|
||||
|
||||
```
|
||||
{..., "elapsed_seconds":0.9368953704833984,"type":"good"}
|
||||
|
||||
```
|
||||
|
||||
而第二个问题,就要查看应用的源码了。点击 [Github](https://github.com/feiskyer/linux-perf-examples/blob/master/redis-slow/app.py) ,你就可以查看案例应用的源代码:
|
||||
|
||||
```
|
||||
def get_cache(type_name):
|
||||
'''handler for /get_cache'''
|
||||
for key in redis_client.scan_iter("uuid:*"):
|
||||
value = redis_client.get(key)
|
||||
if value == type_name:
|
||||
redis_client.sadd(type_name, key[5:])
|
||||
data = list(redis_client.smembers(type_name))
|
||||
redis_client.delete(type_name)
|
||||
return jsonify({"type": type_name, 'count': len(data), 'data': data})
|
||||
|
||||
```
|
||||
|
||||
果然,Python 应用把 Redis 当成临时空间,用来存储查询过程中找到的数据。不过我们知道,这些数据放内存中就可以了,完全没必要再通过网络调用存储到 Redis 中。
|
||||
|
||||
基于这个思路,我把修改后的代码也推送到了相同的源码文件中,你可以通过 [http://192.168.0.10:10000/get_cache_data](http://192.168.0.10:10000/get_cache_data) 这个接口来访问它。
|
||||
|
||||
我们切换到终端二,按 Ctrl+C 停止之前的 curl 命令;然后执行下面的 curl 命令,调用 [http://192.168.0.10:10000/get_cache_data](http://192.168.0.10:10000/get_cache_data) 新接口:
|
||||
|
||||
```
|
||||
$ while true; do curl http://192.168.0.10:10000/get_cache_data; done
|
||||
{...,"elapsed_seconds":0.16034674644470215,"type":"good"}
|
||||
|
||||
```
|
||||
|
||||
你可以发现,解决第二个问题后,新接口的性能又有了进一步的提升,从刚才的 0.9s ,再次缩短成了不到 0.2s。
|
||||
|
||||
当然,案例最后,不要忘记清理案例应用。你可以切换到终端一中,执行下面的命令进行清理:
|
||||
|
||||
```
|
||||
$ docker rm -f app redis
|
||||
|
||||
```
|
||||
|
||||
## 小结
|
||||
|
||||
今天我带你一起分析了一个 Redis 缓存的案例。
|
||||
|
||||
我们先用 top、iostat ,分析了系统的 CPU 、内存和磁盘使用情况,不过却发现,系统资源并没有出现瓶颈。这个时候想要进一步分析的话,该从哪个方向着手呢?
|
||||
|
||||
通过今天的案例你会发现,为了进一步分析,就需要你对系统和应用程序的工作原理有一定的了解。
|
||||
|
||||
比如,今天的案例中,虽然磁盘 I/O 并没有出现瓶颈,但从 Redis 的原理来说,查询缓存时不应该出现大量的磁盘 I/O 写操作。
|
||||
|
||||
顺着这个思路,我们继续借助 pidstat、strace、lsof、nsenter 等一系列的工具,找出了两个潜在问题,一个是 Redis 的不合理配置,另一个是 Python 应用对 Redis 的滥用。找到瓶颈后,相应的优化工作自然就比较轻松了。
|
||||
|
||||
## 思考
|
||||
|
||||
最后给你留一个思考题。从上一节 MySQL 到今天 Redis 的案例分析,你有没有发现 I/O 性能问题的分析规律呢?如果你有任何想法或心得,都可以记录下来。
|
||||
|
||||
当然,这两个案例这并不能涵盖所有的 I/O 性能问题。你在实际工作中,还碰到过哪些 I/O 性能问题吗?你又是怎么分析的呢?
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
176
极客时间专栏/Linux性能优化实战/I|O 性能篇/30 | 套路篇:如何迅速分析出系统I|O的瓶颈在哪里?.md
Normal file
176
极客时间专栏/Linux性能优化实战/I|O 性能篇/30 | 套路篇:如何迅速分析出系统I|O的瓶颈在哪里?.md
Normal file
@@ -0,0 +1,176 @@
|
||||
<audio id="audio" title="30 | 套路篇:如何迅速分析出系统I/O的瓶颈在哪里?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d1/ed/d14cff11e51ad60b59b0c0dfb13a36ed.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
前几节学习中,我们通过几个案例,分析了各种常见的 I/O 性能问题。通过这些实战操作,你应该已经熟悉了 I/O 性能问题的分析和定位思路,也掌握了很多 I/O 性能分析的工具。
|
||||
|
||||
不过,我想你可能还是会困惑,如果离开专栏,换成其他的实际工作场景,案例中提到的各种性能指标和工具,又该如何选择呢?
|
||||
|
||||
上一节最后,我留下了作业,让你自己整理思路。今天,我就带你一起复习,总结一下,如何“快准狠”定位系统的 I/O 瓶颈;并且梳理清楚,在不同场景下,指标工具怎么选,性能瓶颈又该如何定位。
|
||||
|
||||
## 性能指标
|
||||
|
||||
老规矩,我们先来回顾一下,描述 I/O 的性能指标有哪些?你可以先回想一下文件系统和磁盘 I/O 的原理,结合下面这张 Linux 系统的 I/O 栈图,凭着记忆和理解自己写一写。或者,你也可以打开前面的文章,挨个复习总结一下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9e/38/9e42aaf53ff4a544b9a7b03b6ce63f38.png" alt="">
|
||||
|
||||
学了这么久的 I/O 性能知识,一说起 I/O 指标,你应该首先会想到分类描述。我们要区分开文件系统和磁盘,分别用不同指标来描述它们的性能。
|
||||
|
||||
### 文件系统I/O性能指标
|
||||
|
||||
我们先来看文件系统的情况。
|
||||
|
||||
**首先,最容易想到的是存储空间的使用情况,包括容量、使用量以及剩余空间等**。我们通常也称这些为磁盘空间的使用量,因为文件系统的数据最终还是存储在磁盘上。
|
||||
|
||||
不过要注意,这些只是文件系统向外展示的空间使用,而非在磁盘空间的真实用量,因为文件系统的元数据也会占用磁盘空间。
|
||||
|
||||
而且,如果你配置了 RAID,从文件系统看到的使用量跟实际磁盘的占用空间,也会因为 RAID 级别的不同而不一样。比方说,配置 RAID10 后,你从文件系统最多也只能看到所有磁盘容量的一半。
|
||||
|
||||
除了数据本身的存储空间,还有一个**容易忽略的是索引节点的使用情况,它也包括容量、使用量以及剩余量等三个指标**。如果文件系统中存储过多的小文件,就可能碰到索引节点容量已满的问题。
|
||||
|
||||
**其次,你应该想到的是前面多次提到过的缓存使用情况,包括页缓存、目录项缓存、索引节点缓存以及各个具体文件系统(如 ext4、XFS 等)的缓存**。这些缓存会使用速度更快的内存,用来临时存储文件数据或者文件系统的元数据,从而可以减少访问慢速磁盘的次数。
|
||||
|
||||
除了以上这两点,文件 I/O 也是很重要的性能指标,包括 IOPS(包括 r/s 和 w/s)、响应时间(延迟)以及吞吐量(B/s)等。在考察这类指标时,通常还要考虑实际文件的读写情况。比如,结合文件大小、文件数量、I/O 类型等,综合分析文件 I/O 的性能。
|
||||
|
||||
诚然,这些性能指标非常重要,但不幸的是,Linux 文件系统并没提供,直接查看这些指标的方法。我们只能通过系统调用、动态跟踪或者基准测试等方法,间接进行观察、评估。
|
||||
|
||||
不过,实际上,这些指标在我们考察磁盘性能时更容易见到,因为 Linux 为磁盘性能提供了更详细的数据。
|
||||
|
||||
### 磁盘I/O性能指标
|
||||
|
||||
接下来,我们就来具体看看,哪些性能指标可以衡量磁盘 I/O 的性能。
|
||||
|
||||
在磁盘 I/O 原理的文章中,我曾提到过四个核心的磁盘 I/O 指标。
|
||||
|
||||
<li>
|
||||
**使用率**,是指磁盘忙处理I/O请求的百分比。过高的使用率(比如超过60%)通常意味着磁盘I/O存在性能瓶颈。
|
||||
</li>
|
||||
<li>
|
||||
**IOPS**(Input/Output Per Second),是指每秒的 I/O 请求数。
|
||||
</li>
|
||||
<li>
|
||||
**吞吐量**,是指每秒的 I/O 请求大小。
|
||||
</li>
|
||||
<li>
|
||||
**响应时间**,是指从发出 I/O 请求到收到响应的间隔时间。
|
||||
</li>
|
||||
|
||||
考察这些指标时,一定要注意综合 I/O 的具体场景来分析,比如读写类型(顺序还是随机)、读写比例、读写大小、存储类型(有无RAID以及RAID级别、本地存储还是网络存储)等。
|
||||
|
||||
不过,这里有个大忌,就是把不同场景的 I/O 性能指标,直接进行分析对比。这是很常见的一个误区,你一定要避免。
|
||||
|
||||
除了这些指标外,在前面 Cache 和 Buffer 原理的文章中,我曾多次提到,**缓冲区(Buffer)**也是要重点掌握的指标,它经常出现在内存和磁盘问题的分析中。
|
||||
|
||||
文件系统和磁盘 I/O 的这些指标都很有用,需要我们熟练掌握,所以我总结成了一张图,帮你分类和记忆。你可以保存并打印出来,方便随时查看复习,也可以把它当成 I/O 性能分析的“指标筛选”清单使用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b6/20/b6d67150e471e1340a6f3c3dc3ba0120.png" alt="">
|
||||
|
||||
## 性能工具
|
||||
|
||||
掌握文件系统和磁盘 I/O 的性能指标后,我们还要知道,怎样去获取这些指标,也就是搞明白工具的使用问题。
|
||||
|
||||
你还记得前面的基础篇和案例篇中,都分别用了哪些工具吗?我们一起回顾下这些内容。
|
||||
|
||||
第一,在文件系统的原理中,我介绍了查看文件系统容量的工具 df。它既可以查看文件系统数据的空间容量,也可以查看索引节点的容量。至于文件系统缓存,我们通过/proc/meminfo、/proc/slabinfo 以及 slabtop 等各种来源,观察页缓存、目录项缓存、索引节点缓存以及具体文件系统的缓存情况。
|
||||
|
||||
第二,在磁盘 I/O 的原理中,我们分别用 iostat 和 pidstat 观察了磁盘和进程的 I/O 情况。它们都是最常用的 I/O 性能分析工具。通过 iostat ,我们可以得到磁盘的 I/O 使用率、吞吐量、响应时间以及 IOPS 等性能指标;而通过 pidstat ,则可以观察到进程的 I/O 吞吐量以及块设备 I/O 的延迟等。
|
||||
|
||||
第三,在狂打日志的案例中,我们先用 top 查看系统的 CPU 使用情况,发现 iowait 比较高;然后,又用 iostat 发现了磁盘的 I/O 使用率瓶颈,并用 pidstat 找出了大量 I/O 的进程;最后,通过 strace 和 lsof,我们找出了问题进程正在读写的文件,并最终锁定性能问题的来源——原来是进程在狂打日志。
|
||||
|
||||
第四,在磁盘 I/O 延迟的单词热度案例中,我们同样先用 top、iostat ,发现磁盘有 I/O 瓶颈,并用 pidstat 找出了大量 I/O 的进程。可接下来,想要照搬上次操作的我们失败了。在随后的 strace 命令中,我们居然没看到 write 系统调用。于是,我们换了一个思路,用新工具 filetop 和 opensnoop ,从内核中跟踪系统调用,最终找出瓶颈的来源。
|
||||
|
||||
最后,在 MySQL 和 Redis 的案例中,同样的思路,我们先用 top、iostat 以及 pidstat ,确定并找出 I/O 性能问题的瓶颈来源,它们正是 mysqld 和 redis-server。随后,我们又用 strace+lsof 找出了它们正在读写的文件。
|
||||
|
||||
关于 MySQL 案例,根据 mysqld 正在读写的文件路径,再结合 MySQL 数据库引擎的原理,我们不仅找出了数据库和数据表的名称,还进一步发现了慢查询的问题,最终通过优化索引解决了性能瓶颈。
|
||||
|
||||
至于 Redis 案例,根据 redis-server 读写的文件,以及正在进行网络通信的 TCP Socket,再结合 Redis 的工作原理,我们发现 Redis 持久化选项配置有问题;从 TCP Socket 通信的数据中,我们还发现了客户端的不合理行为。于是,我们修改 Redis 配置选项,并优化了客户端使用Redis的方式,从而减少网络通信次数,解决性能问题。
|
||||
|
||||
一下子复习了这么多,你是不是觉得头昏脑胀,再次想感叹性能工具的繁杂呀!其实,只要把相应的系统工作原理捋明白,工具使用并不难
|
||||
|
||||
## 性能指标和工具的联系
|
||||
|
||||
同前面CPU和内存板块的学习一样,我建议从指标和工具两个不同维度出发,整理记忆。
|
||||
|
||||
<li>
|
||||
从I/O指标出发,你更容易把性能工具同系统工作原理关联起来,对性能问题有宏观的认识和把握。
|
||||
</li>
|
||||
<li>
|
||||
而从性能工具出发,可以让你更快上手使用工具,迅速找出我们想观察的性能指标。特别是在工具有限的情况下,我们更要充分利用好手头的每一个工具,少量工具也要尽力挖掘出大量信息。
|
||||
</li>
|
||||
|
||||
**第一个维度,从文件系统和磁盘 I/O 的性能指标出发。换句话说,当你想查看某个性能指标时,要清楚知道,哪些工具可以做到。**
|
||||
|
||||
根据不同的性能指标,对提供指标的性能工具进行分类和理解。这样,在实际排查性能问题时,你就可以清楚知道,什么工具可以提供你想要的指标,而不是毫无根据地挨个尝试,撞运气。
|
||||
|
||||
虽然你不需要把所有相关的工具背下来,但如果能记清楚每个指标对应的工具特性,实际操作起来,一定能更高效、灵活。
|
||||
|
||||
这里,我把提供 I/O 性能指标的工具做成了一个表格,方便你梳理关系和理解记忆。你可以把它保存并打印出来,随时记忆。当然,你也可以把它当成一个“指标工具”指南来使用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6f/98/6f26fa18a73458764fcda00212006698.png" alt="">
|
||||
|
||||
下面,我们再来看第二个维度。
|
||||
|
||||
**第二个维度,从工具出发。也就是当你已经安装了某个工具后,要知道这个工具能提供哪些指标。**
|
||||
|
||||
这在实际环境中,特别是生产环境中也是非常重要的。因为很多情况下,你并没有权限安装新的工具包,只能最大化地利用好系统已有的工具,而这就需要你对它们有足够的了解。
|
||||
|
||||
具体到每个工具的使用方法,一般都支持丰富的配置选项。不过不用担心,这些配置选项并不用背下来。你只要知道有哪些工具,以及这些工具的基本功能是什么就够了。真正要用到的时候, 通过 man 命令,查它们的使用手册就可以了。
|
||||
|
||||
同样的,我也将这些常用工具汇总成了一个表格,方便你区分和理解。自然,你也可以当成一个“工具指标”指南使用,需要时查表即可。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ee/f3/ee11664d015f034e4042b9fa4fyycff3.jpg" alt="">
|
||||
|
||||
## 如何迅速分析I/O的性能瓶颈
|
||||
|
||||
到这里,相信你对内存的性能指标已经非常熟悉,也清楚每种性能指标分别能用什么工具来获取。
|
||||
|
||||
你应该发现了,比起前两个板块,虽然文件系统和磁盘的 I/O 性能指标仍比较多,但核心的性能工具,其实就是那么几个。熟练掌握它们,再根据实际系统的现象,并配合系统和应用程序的原理, I/O 性能分析就很清晰了。
|
||||
|
||||
不过,不管怎么说,如果每次一碰到 I/O 的性能问题,就把上面提到的所有工具跑一遍,肯定是不现实的。
|
||||
|
||||
在实际生产环境中,我们希望的是,尽可能**快**地定位系统的瓶颈,然后尽可能**快**地优化性能,也就是要又快又准地解决性能问题。
|
||||
|
||||
那有没有什么方法,可以又快又准地找出系统的I/O 瓶颈呢?答案是肯定的。
|
||||
|
||||
还是那句话,找关联。多种性能指标间都有一定的关联性,不要完全孤立的看待他们。**想弄清楚性能指标的关联性,就要通晓每种性能指标的工作原理**。这也是为什么我在介绍每个性能指标时,都要穿插讲解相关的系统原理,再次希望你能记住这一点。
|
||||
|
||||
以我们前面几期的案例为例,如果你仔细对比前面的几个案例,从 I/O延迟的案例到 MySQL 和 Redis 的案例,就会发现,虽然这些问题千差万别,但从 I/O 角度来分析,最开始的分析思路基本上类似,都是:
|
||||
|
||||
<li>
|
||||
先用 iostat 发现磁盘 I/O 性能瓶颈;
|
||||
</li>
|
||||
<li>
|
||||
再借助 pidstat ,定位出导致瓶颈的进程;
|
||||
</li>
|
||||
<li>
|
||||
随后分析进程的 I/O 行为;
|
||||
</li>
|
||||
<li>
|
||||
最后,结合应用程序的原理,分析这些 I/O 的来源。
|
||||
</li>
|
||||
|
||||
**所以,为了缩小排查范围,我通常会先运行那几个支持指标较多的工具,如 iostat、vmstat、pidstat 等。**然后再根据观察到的现象,结合系统和应用程序的原理,寻找下一步的分析方向。我把这个过程画成了一张图,你可以保存下来参考使用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/18/8a/1802a35475ee2755fb45aec55ed2d98a.png" alt="">
|
||||
|
||||
图中列出了最常用的几个文件系统和磁盘 I/O 性能分析工具,以及相应的分析流程,箭头则表示分析方向。这其中,iostat、vmstat、pidstat 是最核心的几个性能工具,它们也提供了最重要的 I/O 性能指标。举几个例子你可能更容易理解。
|
||||
|
||||
例如,在前面讲过的 MySQL 和 Redis 案例中,我们就是通过 iostat 确认磁盘出现 I/O 性能瓶颈,然后用 pidstat 找出 I/O 最大的进程,接着借助 strace 找出该进程正在读写的文件,最后结合应用程序的原理,找出大量 I/O 的原因。
|
||||
|
||||
再如,当你用 iostat 发现磁盘有 I/O 性能瓶颈后,再用 pidstat 和 vmstat 检查,可能会发现 I/O 来自内核线程,如 Swap 使用大量升高。这种情况下,你就得进行内存分析了,先找出占用大量内存的进程,再设法减少内存的使用。
|
||||
|
||||
另外注意,我在这个图中只列出了最核心的几个性能工具,并没有列出前面表格中的所有工具。这么做,一方面是不想用大量的工具列表吓到你。在学习之初就接触所有核心或小众的工具,不见得是好事。另一方面,也是希望你能先把重心放在核心工具上,毕竟熟练掌握它们,就可以解决大多数问题。
|
||||
|
||||
所以,你可以保存下这张图,作为文件系统和磁盘I/O性能分析的思路图谱。从最核心的这几个工具开始,通过我提供的那些案例,自己在真实环境里实践,拿下它们。
|
||||
|
||||
## 小结
|
||||
|
||||
今天,我们一起复习了常见的文件系统和磁盘 I/O 性能指标,梳理了常见的 I/O 性能观测工具,并建立了性能指标和工具的关联。最后,我们还总结了快速分析 I/O 性能问题的思路。
|
||||
|
||||
还是那句话,虽然 I/O 的性能指标很多,相应的性能分析工具也有不少,但熟悉了各指标含义后,你就会自然找到它们的关联。顺着这个思路往下走,掌握常用的分析套路也并不难。
|
||||
|
||||
## 思考
|
||||
|
||||
专栏学习中,我只列举了几个最常见的案例,帮你理解文件系统和磁盘 I/O 性能的原理和分析方法。你肯定也碰到过不少其他 I/O 性能问题吧。我想请你一起聊聊,你碰到过哪些 I/O 性能问题呢?你又是怎么分析出它的瓶颈呢?
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
251
极客时间专栏/Linux性能优化实战/I|O 性能篇/31 | 套路篇:磁盘 I|O 性能优化的几个思路.md
Normal file
251
极客时间专栏/Linux性能优化实战/I|O 性能篇/31 | 套路篇:磁盘 I|O 性能优化的几个思路.md
Normal file
@@ -0,0 +1,251 @@
|
||||
<audio id="audio" title="31 | 套路篇:磁盘 I/O 性能优化的几个思路" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/77/c1/771462a4baa52bc952a4cc17a8875ec1.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
上一节,我们一起回顾了常见的文件系统和磁盘 I/O 性能指标,梳理了核心的 I/O 性能观测工具,最后还总结了快速分析 I/O 性能问题的思路。
|
||||
|
||||
虽然 I/O 的性能指标很多,相应的性能分析工具也有好几个,但理解了各种指标的含义后,你就会发现它们其实都有一定的关联。
|
||||
|
||||
顺着这些关系往下理解,你就会发现,掌握这些常用的瓶颈分析思路,其实并不难。
|
||||
|
||||
找出了 I/O 的性能瓶颈后,下一步要做的就是优化了,也就是如何以最快的速度完成 I/O操作,或者换个思路,减少甚至避免磁盘的 I/O 操作。
|
||||
|
||||
今天,我就来说说,优化 I/O 性能问题的思路和注意事项。
|
||||
|
||||
## I/O 基准测试
|
||||
|
||||
按照我的习惯,优化之前,我会先问自己, I/O 性能优化的目标是什么?换句话说,我们观察的这些I/O 性能指标(比如 IOPS、吞吐量、延迟等),要达到多少才合适呢?
|
||||
|
||||
事实上,I/O 性能指标的具体标准,每个人估计会有不同的答案,因为我们每个人的应用场景、使用的文件系统和物理磁盘等,都有可能不一样。
|
||||
|
||||
为了更客观合理地评估优化效果,我们首先应该对磁盘和文件系统进行基准测试,得到文件系统或者磁盘 I/O 的极限性能。
|
||||
|
||||
[fio](https://github.com/axboe/fio)(Flexible I/O Tester)正是最常用的文件系统和磁盘 I/O 性能基准测试工具。它提供了大量的可定制化选项,可以用来测试,裸盘或者文件系统在各种场景下的 I/O 性能,包括了不同块大小、不同 I/O 引擎以及是否使用缓存等场景。
|
||||
|
||||
fio 的安装比较简单,你可以执行下面的命令来安装它:
|
||||
|
||||
```
|
||||
# Ubuntu
|
||||
apt-get install -y fio
|
||||
|
||||
# CentOS
|
||||
yum install -y fio
|
||||
|
||||
```
|
||||
|
||||
安装完成后,就可以执行 man fio 查询它的使用方法。
|
||||
|
||||
fio 的选项非常多, 我会通过几个常见场景的测试方法,介绍一些最常用的选项。这些常见场景包括随机读、随机写、顺序读以及顺序写等,你可以执行下面这些命令来测试:
|
||||
|
||||
```
|
||||
# 随机读
|
||||
fio -name=randread -direct=1 -iodepth=64 -rw=randread -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
|
||||
|
||||
# 随机写
|
||||
fio -name=randwrite -direct=1 -iodepth=64 -rw=randwrite -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
|
||||
|
||||
# 顺序读
|
||||
fio -name=read -direct=1 -iodepth=64 -rw=read -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
|
||||
|
||||
# 顺序写
|
||||
fio -name=write -direct=1 -iodepth=64 -rw=write -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
|
||||
|
||||
```
|
||||
|
||||
在这其中,有几个参数需要你重点关注一下。
|
||||
|
||||
<li>
|
||||
direct,表示是否跳过系统缓存。上面示例中,我设置的 1 ,就表示跳过系统缓存。
|
||||
</li>
|
||||
<li>
|
||||
iodepth,表示使用异步 I/O(asynchronous I/O,简称AIO)时,同时发出的 I/O 请求上限。在上面的示例中,我设置的是 64。
|
||||
</li>
|
||||
<li>
|
||||
rw,表示 I/O 模式。我的示例中, read/write 分别表示顺序读/写,而 randread/randwrite 则分别表示随机读/写。
|
||||
</li>
|
||||
<li>
|
||||
ioengine,表示 I/O 引擎,它支持同步(sync)、异步(libaio)、内存映射(mmap)、网络(net)等各种 I/O 引擎。上面示例中,我设置的 libaio 表示使用异步 I/O。
|
||||
</li>
|
||||
<li>
|
||||
bs,表示 I/O 的大小。示例中,我设置成了 4K(这也是默认值)。
|
||||
</li>
|
||||
<li>
|
||||
filename,表示文件路径,当然,它可以是磁盘路径(测试磁盘性能),也可以是文件路径(测试文件系统性能)。示例中,我把它设置成了磁盘 /dev/sdb。不过注意,用磁盘路径测试写,会破坏这个磁盘中的文件系统,所以在使用前,你一定要事先做好数据备份。
|
||||
</li>
|
||||
|
||||
下面就是我使用 fio 测试顺序读的一个报告示例。
|
||||
|
||||
```
|
||||
read: (g=0): rw=read, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
|
||||
fio-3.1
|
||||
Starting 1 process
|
||||
Jobs: 1 (f=1): [R(1)][100.0%][r=16.7MiB/s,w=0KiB/s][r=4280,w=0 IOPS][eta 00m:00s]
|
||||
read: (groupid=0, jobs=1): err= 0: pid=17966: Sun Dec 30 08:31:48 2018
|
||||
read: IOPS=4257, BW=16.6MiB/s (17.4MB/s)(1024MiB/61568msec)
|
||||
slat (usec): min=2, max=2566, avg= 4.29, stdev=21.76
|
||||
clat (usec): min=228, max=407360, avg=15024.30, stdev=20524.39
|
||||
lat (usec): min=243, max=407363, avg=15029.12, stdev=20524.26
|
||||
clat percentiles (usec):
|
||||
| 1.00th=[ 498], 5.00th=[ 1020], 10.00th=[ 1319], 20.00th=[ 1713],
|
||||
| 30.00th=[ 1991], 40.00th=[ 2212], 50.00th=[ 2540], 60.00th=[ 2933],
|
||||
| 70.00th=[ 5407], 80.00th=[ 44303], 90.00th=[ 45351], 95.00th=[ 45876],
|
||||
| 99.00th=[ 46924], 99.50th=[ 46924], 99.90th=[ 48497], 99.95th=[ 49021],
|
||||
| 99.99th=[404751]
|
||||
bw ( KiB/s): min= 8208, max=18832, per=99.85%, avg=17005.35, stdev=998.94, samples=123
|
||||
iops : min= 2052, max= 4708, avg=4251.30, stdev=249.74, samples=123
|
||||
lat (usec) : 250=0.01%, 500=1.03%, 750=1.69%, 1000=2.07%
|
||||
lat (msec) : 2=25.64%, 4=37.58%, 10=2.08%, 20=0.02%, 50=29.86%
|
||||
lat (msec) : 100=0.01%, 500=0.02%
|
||||
cpu : usr=1.02%, sys=2.97%, ctx=33312, majf=0, minf=75
|
||||
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
|
||||
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
|
||||
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
|
||||
issued rwt: total=262144,0,0, short=0,0,0, dropped=0,0,0
|
||||
latency : target=0, window=0, percentile=100.00%, depth=64
|
||||
|
||||
Run status group 0 (all jobs):
|
||||
READ: bw=16.6MiB/s (17.4MB/s), 16.6MiB/s-16.6MiB/s (17.4MB/s-17.4MB/s), io=1024MiB (1074MB), run=61568-61568msec
|
||||
|
||||
Disk stats (read/write):
|
||||
sdb: ios=261897/0, merge=0/0, ticks=3912108/0, in_queue=3474336, util=90.09%
|
||||
|
||||
```
|
||||
|
||||
这个报告中,需要我们重点关注的是, slat、clat、lat ,以及 bw 和 iops 这几行。
|
||||
|
||||
先来看刚刚提到的前三个参数。事实上,slat、clat、lat 都是指 I/O 延迟(latency)。不同之处在于:
|
||||
|
||||
<li>
|
||||
slat ,是指从 I/O 提交到实际执行 I/O 的时长(Submission latency);
|
||||
</li>
|
||||
<li>
|
||||
clat ,是指从 I/O 提交到 I/O 完成的时长(Completion latency);
|
||||
</li>
|
||||
<li>
|
||||
而 lat ,指的是从fio 创建 I/O 到 I/O 完成的总时长。
|
||||
</li>
|
||||
|
||||
这里需要注意的是,对同步 I/O 来说,由于 I/O 提交和I/O完成是一个动作,所以 slat 实际上就是 I/O 完成的时间,而 clat 是 0。而从示例可以看到,使用异步 I/O(libaio)时,lat 近似等于 slat + clat之和。
|
||||
|
||||
再来看bw ,它代表吞吐量。在我上面的示例中,你可以看到,平均吞吐量大约是 16 MB(17005 KiB/1024)。
|
||||
|
||||
最后的iops ,其实就是每秒 I/O 的次数,上面示例中的平均 IOPS 为 4250。
|
||||
|
||||
通常情况下,应用程序的 I/O 都是读写并行的,而且每次的I/O大小也不一定相同。所以,刚刚说的这几种场景,并不能精确模拟应用程序的 I/O 模式。那怎么才能精确模拟应用程序的 I/O 模式呢?
|
||||
|
||||
幸运的是,fio 支持 I/O 的重放。借助前面提到过的 blktrace,再配合上 fio,就可以实现对应用程序 I/O 模式的基准测试。你需要先用 blktrace ,记录磁盘设备的 I/O 访问情况;然后使用 fio ,重放 blktrace 的记录。
|
||||
|
||||
比如你可以运行下面的命令来操作:
|
||||
|
||||
```
|
||||
# 使用blktrace跟踪磁盘I/O,注意指定应用程序正在操作的磁盘
|
||||
$ blktrace /dev/sdb
|
||||
|
||||
# 查看blktrace记录的结果
|
||||
# ls
|
||||
sdb.blktrace.0 sdb.blktrace.1
|
||||
|
||||
# 将结果转化为二进制文件
|
||||
$ blkparse sdb -d sdb.bin
|
||||
|
||||
# 使用fio重放日志
|
||||
$ fio --name=replay --filename=/dev/sdb --direct=1 --read_iolog=sdb.bin
|
||||
|
||||
```
|
||||
|
||||
这样,我们就通过 blktrace+fio 的组合使用,得到了应用程序 I/O 模式的基准测试报告。
|
||||
|
||||
## I/O 性能优化
|
||||
|
||||
得到 I/O 基准测试报告后,再用上我们上一节总结的性能分析套路,找出 I/O 的性能瓶颈并优化,就是水到渠成的事情了。当然, 想要优化I/O 性能,肯定离不开 Linux 系统的 I/O 栈图的思路辅助。你可以结合下面的 I/O 栈图再回顾一下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9e/38/9e42aaf53ff4a544b9a7b03b6ce63f38.png" alt="">
|
||||
|
||||
下面,我就带你从应用程序、文件系统以及磁盘角度,分别看看 I/O 性能优化的基本思路。
|
||||
|
||||
### 应用程序优化
|
||||
|
||||
首先,我们来看一下,从应用程序的角度有哪些优化 I/O 的思路。
|
||||
|
||||
应用程序处于整个 I/O 栈的最上端,它可以通过系统调用,来调整 I/O 模式(如顺序还是随机、同步还是异步), 同时,它也是 I/O 数据的最终来源。在我看来,可以有这么几种方式来优化应用程序的 I/O 性能。
|
||||
|
||||
第一,可以用追加写代替随机写,减少寻址开销,加快 I/O 写的速度。
|
||||
|
||||
第二,可以借助缓存 I/O ,充分利用系统缓存,降低实际 I/O 的次数。
|
||||
|
||||
第三,可以在应用程序内部构建自己的缓存,或者用 Redis 这类外部缓存系统。这样,一方面,能在应用程序内部,控制缓存的数据和生命周期;另一方面,也能降低其他应用程序使用缓存对自身的影响。
|
||||
|
||||
比如,在前面的 MySQL 案例中,我们已经见识过,只是因为一个干扰应用清理了系统缓存,就会导致 MySQL 查询有数百倍的性能差距(0.1s vs 15s)。
|
||||
|
||||
再如, C 标准库提供的 fopen、fread 等库函数,都会利用标准库的缓存,减少磁盘的操作。而你直接使用 open、read 等系统调用时,就只能利用操作系统提供的页缓存和缓冲区等,而没有库函数的缓存可用。
|
||||
|
||||
第四,在需要频繁读写同一块磁盘空间时,可以用 mmap 代替 read/write,减少内存的拷贝次数。
|
||||
|
||||
第五,在需要同步写的场景中,尽量将写请求合并,而不是让每个请求都同步写入磁盘,即可以用 fsync() 取代 O_SYNC。
|
||||
|
||||
第六,在多个应用程序共享相同磁盘时,为了保证 I/O 不被某个应用完全占用,推荐你使用 cgroups 的 I/O 子系统,来限制进程 / 进程组的 IOPS 以及吞吐量。
|
||||
|
||||
最后,在使用 CFQ 调度器时,可以用 ionice 来调整进程的 I/O 调度优先级,特别是提高核心应用的 I/O 优先级。ionice 支持三个优先级类:Idle、Best-effort 和 Realtime。其中, Best-effort 和 Realtime 还分别支持 0-7 的级别,数值越小,则表示优先级别越高。
|
||||
|
||||
### 文件系统优化
|
||||
|
||||
应用程序访问普通文件时,实际是由文件系统间接负责,文件在磁盘中的读写。所以,跟文件系统中相关的也有很多优化 I/O 性能的方式。
|
||||
|
||||
第一,你可以根据实际负载场景的不同,选择最适合的文件系统。比如Ubuntu 默认使用 ext4 文件系统,而 CentOS 7 默认使用 xfs 文件系统。
|
||||
|
||||
相比于 ext4 ,xfs 支持更大的磁盘分区和更大的文件数量,如 xfs 支持大于16TB 的磁盘。但是 xfs 文件系统的缺点在于无法收缩,而 ext4 则可以。
|
||||
|
||||
第二,在选好文件系统后,还可以进一步优化文件系统的配置选项,包括文件系统的特性(如ext_attr、dir_index)、日志模式(如journal、ordered、writeback)、挂载选项(如noatime)等等。
|
||||
|
||||
比如, 使用tune2fs 这个工具,可以调整文件系统的特性(tune2fs 也常用来查看文件系统超级块的内容)。 而通过 /etc/fstab ,或者 mount 命令行参数,我们可以调整文件系统的日志模式和挂载选项等。
|
||||
|
||||
第三,可以优化文件系统的缓存。
|
||||
|
||||
比如,你可以优化 pdflush 脏页的刷新频率(比如设置 dirty_expire_centisecs 和 dirty_writeback_centisecs)以及脏页的限额(比如调整 dirty_background_ratio 和 dirty_ratio等)。
|
||||
|
||||
再如,你还可以优化内核回收目录项缓存和索引节点缓存的倾向,即调整 vfs_cache_pressure(/proc/sys/vm/vfs_cache_pressure,默认值100),数值越大,就表示越容易回收。
|
||||
|
||||
最后,在不需要持久化时,你还可以用内存文件系统 tmpfs,以获得更好的 I/O性能 。tmpfs 把数据直接保存在内存中,而不是磁盘中。比如 /dev/shm/ ,就是大多数 Linux 默认配置的一个内存文件系统,它的大小默认为总内存的一半。
|
||||
|
||||
### 磁盘优化
|
||||
|
||||
数据的持久化存储,最终还是要落到具体的物理磁盘中,同时,磁盘也是整个 I/O 栈的最底层。从磁盘角度出发,自然也有很多有效的性能优化方法。
|
||||
|
||||
第一,最简单有效的优化方法,就是换用性能更好的磁盘,比如用 SSD 替代 HDD。
|
||||
|
||||
第二,我们可以使用 RAID ,把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列。这样做既可以提高数据的可靠性,又可以提升数据的访问性能。
|
||||
|
||||
第三,针对磁盘和应用程序 I/O 模式的特征,我们可以选择最适合的 I/O 调度算法。比方说,SSD 和虚拟机中的磁盘,通常用的是 noop 调度算法。而数据库应用,我更推荐使用 deadline 算法。
|
||||
|
||||
第四,我们可以对应用程序的数据,进行磁盘级别的隔离。比如,我们可以为日志、数据库等 I/O 压力比较重的应用,配置单独的磁盘。
|
||||
|
||||
第五,在顺序读比较多的场景中,我们可以增大磁盘的预读数据,比如,你可以通过下面两种方法,调整 /dev/sdb 的预读大小。
|
||||
|
||||
<li>
|
||||
调整内核选项 /sys/block/sdb/queue/read_ahead_kb,默认大小是 128 KB,单位为KB。
|
||||
</li>
|
||||
<li>
|
||||
使用 blockdev 工具设置,比如 blockdev --setra 8192 /dev/sdb,注意这里的单位是 512B(0.5KB),所以它的数值总是 read_ahead_kb 的两倍。
|
||||
</li>
|
||||
|
||||
第六,我们可以优化内核块设备 I/O 的选项。比如,可以调整磁盘队列的长度 /sys/block/sdb/queue/nr_requests,适当增大队列长度,可以提升磁盘的吞吐量(当然也会导致 I/O 延迟增大)。
|
||||
|
||||
最后,要注意,磁盘本身出现硬件错误,也会导致 I/O 性能急剧下降,所以发现磁盘性能急剧下降时,你还需要确认,磁盘本身是不是出现了硬件错误。
|
||||
|
||||
比如,你可以查看 dmesg 中是否有硬件 I/O 故障的日志。 还可以使用 badblocks、smartctl等工具,检测磁盘的硬件问题,或用 e2fsck 等来检测文件系统的错误。如果发现问题,你可以使用 fsck 等工具来修复。
|
||||
|
||||
## 小结
|
||||
|
||||
今天,我们一起梳理了常见的文件系统和磁盘 I/O 的性能优化思路和方法。发现 I/O 性能问题后,不要急于动手优化,而要先找出最重要的、可以最大程度提升性能的问题,然后再从 I/O 栈的不同层入手,考虑具体的优化方法。
|
||||
|
||||
记住,磁盘和文件系统的 I/O ,通常是整个系统中最慢的一个模块。所以,在优化 I/O 问题时,除了可以优化 I/O 的执行流程,还可以借助更快的内存、网络、CPU 等,减少I/O 调用。
|
||||
|
||||
比如,你可以充分利用系统提供的 Buffer、Cache ,或是应用程序内部缓存, 再或者Redis 这类的外部缓存系统。
|
||||
|
||||
## 思考
|
||||
|
||||
在整个板块的学习中,我只列举了最常见的几个 I/O 性能优化思路。除此之外,还有很多从应用程序、系统再到磁盘硬件的优化方法。我想请你一起来聊聊,你还知道哪些其他优化方法吗?
|
||||
|
||||
欢迎在留言区跟我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<audio id="audio" title="32 | 答疑(四):阻塞、非阻塞 I/O 与同步、异步 I/O 的区别和联系" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/29/aa/294a44f3338344bb8d6bbd41a8d78faa.mp3"></audio>
|
||||
|
||||
你好,我是倪朋飞。
|
||||
|
||||
专栏更新至今,四大基础模块的第三个模块——文件系统和磁盘 I/O 篇,我们就已经学完了。很开心你还没有掉队,仍然在积极学习思考和实践操作,并且热情地留言与讨论。
|
||||
|
||||
今天是性能优化的第四期。照例,我从 I/O 模块的留言中摘出了一些典型问题,作为今天的答疑内容,集中回复。同样的,为了便于你学习理解,它们并不是严格按照文章顺序排列的。
|
||||
|
||||
每个问题,我都附上了留言区提问的截屏。如果你需要回顾内容原文,可以扫描每个问题右下方的二维码查看。
|
||||
|
||||
## 问题1:阻塞、非阻塞 I/O 与同步、异步 I/O 的区别和联系
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1c/b0/1c3237118d1c55792ac0d9cc23f14bb0.png" alt="">
|
||||
|
||||
在[文件系统的工作原理](https://time.geekbang.org/column/article/76876)篇中,我曾经介绍了阻塞、非阻塞 I/O 以及同步、异步 I/O 的含义,这里我们再简单回顾一下。
|
||||
|
||||
首先我们来看阻塞和非阻塞 I/O。根据应用程序是否阻塞自身运行,可以把 I/O 分为阻塞 I/O 和非阻塞 I/O。
|
||||
|
||||
<li>
|
||||
所谓阻塞I/O,是指应用程序在执行I/O操作后,如果没有获得响应,就会阻塞当前线程,不能执行其他任务。
|
||||
</li>
|
||||
<li>
|
||||
所谓非阻塞I/O,是指应用程序在执行I/O操作后,不会阻塞当前的线程,可以继续执行其他的任务。
|
||||
</li>
|
||||
|
||||
再来看同步 I/O 和异步 I/O。根据 I/O 响应的通知方式的不同,可以把文件 I/O 分为同步 I/O 和异步 I/O。
|
||||
|
||||
<li>
|
||||
所谓同步 I/O,是指收到 I/O 请求后,系统不会立刻响应应用程序;等到处理完成,系统才会通过系统调用的方式,告诉应用程序 I/O 结果。
|
||||
</li>
|
||||
<li>
|
||||
所谓异步 I/O,是指收到 I/O 请求后,系统会先告诉应用程序 I/O 请求已经收到,随后再去异步处理;等处理完成后,系统再通过事件通知的方式,告诉应用程序结果。
|
||||
</li>
|
||||
|
||||
你可以看出,阻塞/非阻塞和同步/异步,其实就是两个不同角度的 I/O 划分方式。它们描述的对象也不同,阻塞/非阻塞针对的是 I/O 调用者(即应用程序),而同步/异步针对的是 I/O 执行者(即系统)。
|
||||
|
||||
我举个例子来进一步解释下。比如在 Linux I/O 调用中,
|
||||
|
||||
<li>
|
||||
系统调用 read 是同步读,所以,在没有得到磁盘数据前,read 不会响应应用程序。
|
||||
</li>
|
||||
<li>
|
||||
而 aio_read 是异步读,系统收到 AIO 读请求后不等处理就返回了,而具体的 read 结果,再通过回调异步通知应用程序。
|
||||
</li>
|
||||
|
||||
再如,在网络套接字的接口中,
|
||||
|
||||
<li>
|
||||
使用 send() 直接向套接字发送数据时,如果套接字没有设置 O_NONBLOCK 标识,那么 send() 操作就会一直阻塞,当前线程也没法去做其他事情。
|
||||
</li>
|
||||
<li>
|
||||
当然,如果你用了 epoll,系统会告诉你这个套接字的状态,那就可以用非阻塞的方式使用。当这个套接字不可写的时候,你可以去做其他事情,比如读写其他套接字。
|
||||
</li>
|
||||
|
||||
## 问题2:“文件系统”课后思考
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/40/a6/40c924ea4b11e12d6d34181a00f292a6.jpg" alt="">
|
||||
|
||||
在[文件系统原理](https://time.geekbang.org/column/article/76876)[文章](https://time.geekbang.org/column/article/76876)的最后,我给你留了一道思考题,那就是执行 find 命令时,会不会导致系统的缓存升高呢?如果会导致,升高的又是哪种类型的缓存呢?
|
||||
|
||||
关于这个问题,白华和 coyang 的答案已经很准确了。通过学习Linux 文件系统的原理,我们知道,文件名以及文件之间的目录关系,都放在目录项缓存中。而这是一个基于内存的数据结构,会根据需要动态构建。所以,查找文件时,Linux 就会动态构建不在缓存中的目录项结构,导致 dentry 缓存升高。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/48/c5/488110263a9c7ff801a3e04c010f0bc5.png" alt=""><img src="https://static001.geekbang.org/resource/image/57/58/57e4cf5a42a91392ebebf106f992a858.png" alt="">
|
||||
|
||||
事实上,除了目录项缓存增加,Buffer 的使用也会增加。如果你用 vmstat 观察一下,会发现 Buffer 和 Cache 都在增长:
|
||||
|
||||
```
|
||||
$ vmstat 1
|
||||
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
|
||||
r b swpd free buff cache si so bi bo in cs us sy id wa st
|
||||
0 1 0 7563744 6024 225944 0 0 3736 0 574 3249 3 5 89 3 0
|
||||
1 0 0 7542792 14736 236856 0 0 8708 0 13494 32335 8 19 66 7 0
|
||||
0 1 0 7494452 27280 272284 0 0 12544 0 4550 17084 5 15 68 13 0
|
||||
0 1 0 7475084 42380 276320 0 0 15096 0 2541 14253 2 6 78 13 0
|
||||
0 1 0 7455728 57600 280436 0 0 15220 0 2025 14518 2 6 70 22 0
|
||||
|
||||
```
|
||||
|
||||
这里,Buffer 的增长是因为,构建目录项缓存所需的元数据(比如文件名称、索引节点等),需要从文件系统中读取。
|
||||
|
||||
## 问题3:“磁盘 I/O 延迟”课后思考
|
||||
|
||||
在[磁盘 I/O 延迟案例](https://time.geekbang.org/column/article/78409)的最后,我给你留了一道思考题。
|
||||
|
||||
我们通过 iostat ,确认磁盘 I/O 已经出现了性能瓶颈,还用 pidstat 找出了大量磁盘 I/O 的进程。但是,随后使用 strace 跟踪这个进程,却找不到任何 write 系统调用。这是为什么呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/64/09/6408b3aa2aa9a98a930d1a5b2e2fef09.jpg" alt="">
|
||||
|
||||
很多同学的留言都准确回答了这个问题。比如,划时代和 jeff 的留言都指出,在这个场景中,我们需要加 -f 选项,以便跟踪多进程和多线程的系统调用情况。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e4/55/e4e9a070022f7b49cb8d5554b9a60055.png" alt=""><img src="https://static001.geekbang.org/resource/image/71/05/71a6df4144ce59d9e1a01c26453acf05.png" alt="">
|
||||
|
||||
你看,仅仅是不恰当的选项,都可能会导致性能工具“犯错”,呈现这种看起来不合逻辑的结果。非常高兴看到,这么多同学已经掌握了性能工具使用的核心思路——弄清楚工具本身的原理和问题。
|
||||
|
||||
## 问题4:“MySQL 案例”课后思考
|
||||
|
||||
在 [MySQL 案例](https://time.geekbang.org/column/article/78633)的最后,我给你留了一个思考题。
|
||||
|
||||
为什么 DataService 应用停止后,即使仍没有索引,MySQL 的查询速度还是快了很多,并且磁盘 I/O 瓶颈也消失了呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/92/78/924fbc974313b1e0fe6b8d14e7a44178.png" alt="">
|
||||
|
||||
ninuxer 的留言基本解释了这个问题,不过还不够完善。
|
||||
|
||||
事实上,当你看到 DataService 在修改 **/proc/sys/vm/drop_caches** 时,就应该想到前面学过的 Cache 的作用。
|
||||
|
||||
我们知道,案例应用访问的数据表,基于 MyISAM 引擎,而 MyISAM 的一个特点,就是只在内存中缓存索引,并不缓存数据。所以,在查询语句无法使用索引时,就需要数据表从数据库文件读入内存,然后再进行处理。
|
||||
|
||||
所以,如果你用 vmstat 工具,观察缓存和 I/O 的变化趋势,就会发现下面这样的结果:
|
||||
|
||||
```
|
||||
$ vmstat 1
|
||||
|
||||
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
|
||||
r b swpd free buff cache si so bi bo in cs us sy id wa st
|
||||
|
||||
# 备注: DataService正在运行
|
||||
0 1 0 7293416 132 366704 0 0 32516 12 36 546 1 3 49 48 0
|
||||
0 1 0 7260772 132 399256 0 0 32640 0 37 463 1 1 49 48 0
|
||||
0 1 0 7228088 132 432088 0 0 32640 0 30 477 0 1 49 49 0
|
||||
0 0 0 7306560 132 353084 0 0 20572 4 90 574 1 4 69 27 0
|
||||
0 2 0 7282300 132 368536 0 0 15468 0 32 304 0 0 79 20 0
|
||||
|
||||
# 备注:DataService从这里开始停止
|
||||
0 0 0 7241852 1360 424164 0 0 864 320 133 1266 1 1 94 5 0
|
||||
0 1 0 7228956 1368 437400 0 0 13328 0 45 366 0 0 83 17 0
|
||||
0 1 0 7196320 1368 470148 0 0 32640 0 33 413 1 1 50 49 0
|
||||
...
|
||||
0 0 0 6747540 1368 918576 0 0 29056 0 42 568 0 0 56 44 0
|
||||
0 0 0 6747540 1368 918576 0 0 0 0 40 141 1 0 100 0 0
|
||||
|
||||
```
|
||||
|
||||
在 DataService 停止前,cache 会连续增长三次后再降回去,这正是因为 DataService 每隔3秒清理一次页缓存。而 DataService 停止后,cache 就会不停地增长,直到增长为 918576 后,就不再变了。
|
||||
|
||||
这时,磁盘的读(bi)降低到 0,同时,iowait(wa)也降低到 0,这说明,此时的所有数据都已经在系统的缓存中了。我们知道,缓存是内存的一部分,它的访问速度比磁盘快得多,这也就能解释,为什么 MySQL 的查询速度变快了很多。
|
||||
|
||||
从这个案例,你会发现,MySQL 的 MyISAM 引擎,本身并不缓存数据,而要依赖系统缓存来加速磁盘 I/O 的访问。一旦系统中还有其他应用同时运行,MyISAM 引擎就很难充分利用系统缓存。因为系统缓存可能被其他应用程序占用,甚至直接被清理掉。
|
||||
|
||||
所以,一般来说,我并不建议,把应用程序的性能优化完全建立在系统缓存上。还是那句话,最好能在应用程序的内部分配内存,构建完全自主控制的缓存,比如 MySQL 的 InnoDB 引擎,就同时缓存了索引和数据;或者,可以使用第三方的缓存应用,比如 Memcached、Redis 等。
|
||||
|
||||
今天主要回答这些问题,同时也欢迎你继续在留言区写下疑问和感想,我会持续不断地解答。希望借助每一次的答疑,可以和你一起,把文章知识内化为你的能力,我们不仅在实战中演练,也要在交流中进步。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user