I/O 性能篇:
倪朋飞 《Linux 性能优化实战》
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
Linux 的存储 I/O 栈:文件系统、通用块层、设备层 =================================================== 文件系统:文件系统是对存储设备上的文件进行组织管理的一种机制。为了支持各类不同的文件系统,Linux 在各种文件系统上,抽象了一层虚拟文件系统 VFS。 它定义了一组所有文件系统都支持的数据结构和标准接口。这样,应用程序和内核中的其他子系统,就只需要跟 VFS 提供的统一接口进行交互。 通用块层:在文件系统的下层,为了支持各种不同类型的存储设备,Linux 又在各种存储设备的基础上,抽象了一个通用块层。 通用块层,为文件系统和应用程序提供了访问块设备的标准接口;同时,为各种块设备的驱动程序提供了统一的框架。此外,通用块层还会对文件系统和应用程序发送过来的 I/O 请求进行排队,并通过重新排序、请求合并等方式,提高磁盘读写的效率。 设备层:通用块层的下一层,自然就是设备层了,包括各种块设备的驱动程序以及物理存储设备。 文件系统、通用块层以及设备层,就构成了 Linux 的存储 I/O 栈。存储系统的 I/O ,通常是整个系统中最慢的一环。所以,Linux 采用多种缓存机制,来优化 I/O 的效率,比方说 为了优化文件访问的性能,采用页缓存、索引节点缓存、目录项缓存等多种缓存机制,减少对下层块设备的直接调用。 同样的,为了优化块设备的访问效率,使用缓冲区来缓存块设备的数据。
23 | 基础篇:Linux 文件系统是怎么工作的?
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
文件系统:索引节点和目录项;文件数据存储的实现:逻辑块;超级块 ================================================================================================================ 文件系统,本身是对存储设备上的文件,进行组织管理的机制。组织方式不同,就会形成不同的文件系统。 在 Linux 中一切皆文件。不仅普通的文件和目录,就连块设备、套接字、管道等,也都要通过统一的文件系统来管理。 Linux 文件系统为每个文件都分配两个数据结构,索引节点(index node)和目录项(directory entry)。它们主要用来记录文件的元信息和目录结构。 索引节点inode: 用来记录文件的元数据,比如 inode 编号、文件大小、访问权限、修改日期、数据的位置等。 索引节点和文件一一对应,它跟文件内容一样,都会被持久化存储到磁盘中。所以记住,索引节点同样占用磁盘空间。 目录项dentry: 用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。 多个关联的目录项,就构成了文件系统的目录结构。 不过,不同于索引节点,目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存。 索引节点是每个文件的唯一标志,而目录项维护的正是文件系统的树状结构。 目录项和索引节点是多对一的关系 举个例子,通过硬链接为文件创建的别名,就会对应不同的目录项,不过这些目录项本质上还是链接同一个文件,所以,它们的索引节点相同。 ------------------------------------------------------------------------------ 文件数据存储的实现: 逻辑块:磁盘读写的最小单位是扇区,然而扇区只有 512B 大小,如果每次都读写这么小的单位,效率一定很低。 所以,文件系统又把连续的扇区组成了逻辑块,然后每次都以逻辑块为最小单元,来管理数据。 常见的逻辑块大小为 4KB,也就是由连续的 8 个扇区组成。 有两点需要注意: 第一,目录项本身就是一个内存缓存,而索引节点则是存储在磁盘中的数据。 在前面的 Buffer 和 Cache 原理中,我曾经提到过,为了协调慢速磁盘与快速 CPU 的性能差异,文件内容会缓存到页缓存 Cache 中。 这些索引节点自然也会缓存到内存中,加速文件的访问。 第二,磁盘在执行文件系统格式化时,会被分成三个存储区域,超级块、索引节点区和数据块区。 ***超级块,存储整个文件系统的状态。 索引节点区,用来存储索引节点。 数据块区,则用来存储文件数据。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
Linux 文件系统的四大基本要素:目录项、索引节点、逻辑块、超级块;虚拟文件系统 VFS;文件系统的分类 ====================================================================================================== 目录项、索引节点、逻辑块以及超级块,构成了 Linux 文件系统的四大基本要素。 目录项,记录了文件的名字,以及文件与其他目录项之间的目录关系。 目录项是一个内存缓存 索引节点,记录了文件的元数据。 逻辑块,是由连续磁盘扇区构成的最小读写单元,用来存储文件数据。 超级块,用来记录文件系统整体的状态,如索引节点和逻辑块的使用情况等。 超级块、索引节点和逻辑块,都是存储在磁盘中的持久化数据。 --------------------------------------------------------------- 虚拟文件系统 VFS(Virtual File System):为了支持各种不同的文件系统,Linux 内核在用户进程和文件系统的中间,又引入了一个抽象层,也就是虚拟文件系统 VFS(Virtual File System)。 VFS 定义了一组所有文件系统都支持的数据结构和标准接口。 这样,用户进程和内核中的其他子系统,只需要跟 VFS 提供的统一接口进行交互就可以了,而不需要再关心底层各种文件系统的实现细节。 ---------------------------------------------------------------- 在 VFS 的下方,Linux 支持各种各样的文件系统,如 Ext4、XFS、NFS 等等。按照存储位置的不同,这些文件系统可以分为三类。 第一类是基于磁盘的文件系统,也就是把数据直接存储在计算机本地挂载的磁盘中。 常见的 Ext4、XFS、OverlayFS 等,都是这类文件系统。 第二类是基于内存的文件系统,也就是我们常说的虚拟文件系统。 这类文件系统,不需要任何磁盘分配存储空间,但会占用内存。 我们经常用到的 /proc 文件系统,其实就是一种最常见的虚拟文件系统。 此外,/sys 文件系统也属于这一类,主要向用户空间导出层次化的内核对象。 第三类是网络文件系统,也就是用来访问其他计算机数据的文件系统,比如 NFS、SMB、iSCSI 等。 文件系统,要先挂载到 VFS 目录树中的某个子目录(称为挂载点),然后才能访问其中的文件。 拿第一类,也就是基于磁盘的文件系统为例,在安装系统时,要先挂载一个根目录(/),在根目录下再把其他文件系统(比如其他的磁盘分区、/proc 文件系统、/sys 文件系统、NFS 等)挂载进来。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
文件读写方式分类:缓冲与非缓冲 I/O、直接与非直接 I/O、阻塞与非阻塞 I/O、同步与异步 I/O;阻塞、非阻塞 I/O 与同步、异步 I/O 的区别和联系;直接 I/O与裸 I/O的区别 =============================================================================================================================================================== 文件系统 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。 缓冲 I/O,是指利用标准库缓存来加速文件的访问,而标准库内部再通过系统调度访问文件。 非缓冲 I/O,是指直接通过系统调用来访问文件,不再经过标准库缓存。 注意,这里所说的“缓冲”,是指标准库内部实现的缓存。比方说,你可能见到过,很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来。 无论缓冲 I/O 还是非缓冲 I/O,它们最终还是要经过系统调用来访问文件。系统调用后,还会通过页缓存,来减少磁盘的 I/O 操作。 第二,根据是否利用操作系统的页缓存,可以把文件 I/O 分为直接 I/O 与非直接 I/O。 直接 I/O,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件。 非直接 I/O 正好相反,文件读写时,先要经过系统的页缓存,然后再由内核或额外的系统调用,真正写入磁盘。 想要实现直接 I/O,需要你在系统调用中,指定 O_DIRECT 标志。如果没有设置过,默认的是非直接 I/O。 不过要注意,直接 I/O、非直接 I/O,本质上还是和文件系统交互。如果是在数据库等场景中,你还会看到,跳过文件系统读写磁盘的情况,也就是我们通常所说的裸 I/O。 第三,根据应用程序是否阻塞自身运行,可以把文件 I/O 分为阻塞 I/O 和非阻塞 I/O: 阻塞 I/O,是指应用程序执行 I/O 操作后,如果没有获得响应,就会阻塞当前线程,自然就不能执行其他任务。 非阻塞 I/O,是指应用程序执行 I/O 操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果。 比方说,访问管道或者网络套接字时,设置 O_NONBLOCK 标志,就表示用非阻塞方式访问;而如果不做任何设置,默认的就是阻塞访问。 第四,根据是否等待响应结果,可以把文件 I/O 分为同步和异步 I/O: 同步 I/O,是指应用程序执行 I/O 操作后,要一直等到整个 I/O 完成后,才能获得 I/O 响应。 异步 I/O,是指应用程序执行 I/O 操作后,不用等待完成和完成后的响应,而是继续执行就可以。等到这次 I/O 完成后,响应会用事件通知的方式,告诉应用程序。 举个例子,在操作文件时,如果你设置了 O_SYNC 或者 O_DSYNC 标志,就代表同步 I/O。 如果设置了 O_DSYNC,就要等文件数据写入磁盘后,才能返回; 而 O_SYNC,则是在 O_DSYNC 基础上,要求文件元数据也要写入磁盘后,才能返回。 再比如,在访问管道或者网络套接字时,设置了 O_ASYNC 选项后,相应的 I/O 就是异步 I/O。这样,内核会再通过 SIGIO 或者 SIGPOLL,来通知进程文件是否可读写。 ---------------------------------------------------------------------------------------------------------------------------------- 阻塞、非阻塞 I/O 与同步、异步 I/O 的区别和联系 阻塞 / 非阻塞和同步 / 异步,其实就是两个不同角度的 I/O 划分方式。 它们描述的对象也不同,阻塞 / 非阻塞针对的是 I/O 调用者(即应用程序), 而同步 / 异步针对的是 I/O 执行者(即系统)。 我举个例子来进一步解释下。比如在 Linux I/O 调用中, 系统调用 read 是同步读,所以,在没有得到磁盘数据前,read 不会响应应用程序。 而 aio_read 是异步读,系统收到 AIO 读请求后不等处理就返回了,而具体的 read 结果,再通过回调异步通知应用程序。 再如,在网络套接字的接口中, 使用 send() 直接向套接字发送数据时,如果套接字没有设置 O_NONBLOCK 标识,那么 send() 操作就会一直阻塞,当前线程也没法去做其他事情。 当然,如果你用了 epoll,系统会告诉你这个套接字的状态,那就可以用非阻塞的方式使用。当这个套接字不可写的时候,你可以去做其他事情,比如读写其他套接字。 那么如何理解阻塞 I/O 、非阻塞 I/O 和 同步、异步 I/O 的区别? https://blog.csdn.net/qq_45382625/article/details/121644713 ---------------------------------------------------------------------------------------------------------------------------------- 直接 I/O与裸 I/O的区别 直接 I/O、非直接 I/O,本质上还是和文件系统交互。 裸 I/O:跳过文件系统读写磁盘
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
性能观测:df 命令;缓存(页缓存和可回收 Slab 缓存)、/proc/meminfo;/proc/slabinfo =========================================================================================================== 容量 用 df 命令,就能查看文件系统的磁盘空间使用情况。 $ df -h /dev/sda1 Filesystem Size Used Avail Use% Mounted on /dev/sda1 29G 3.1G 26G 11% / df 命令加上 -i 参数,查看索引节点的使用情况 $ df -i /dev/sda1 Filesystem Inodes IUsed IFree IUse% Mounted on /dev/sda1 3870720 157460 3713260 5% / 索引节点的容量,(也就是 Inode 个数)是在格式化磁盘时设定好的,一般由格式化工具自动生成。当你发现索引节点空间不足,但磁盘空间充足时,很可能就是过多小文件导致的。 --------------------------------------------------------------------------------------------------------------- 缓存 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 ,来找到占用内存最多的缓存类型。 # 按下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 #从这个结果你可以看到,在我的系统中,目录项和索引节点占用了最多的 Slab 缓存。不过它们占用的内存其实并不大,加起来也只有 23MB 左右。 16380 15854 0% 0.59K 1260 13 10080K inode_cache #从这个结果你可以看到,在我的系统中,目录项和索引节点占用了最多的 Slab 缓存。不过它们占用的内存其实并不大,加起来也只有 23MB 左右。 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
24 | 基础篇:Linux 磁盘I/O是怎么工作的(上)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
磁盘的分类:根据存储介质分类;按照接口类型分类;按照架构进行分类; ============================================================================================================ 磁盘是可以持久化存储的设备,根据存储介质的不同,常见磁盘可以分为两类:机械磁盘和固态磁盘。 第一类,机械磁盘,也称为硬盘驱动器(Hard Disk Driver),通常缩写为 HDD。 机械磁盘主要由盘片和读写磁头组成,数据就存储在盘片的环状磁道中。 在读写数据前,需要移动读写磁头,定位到数据所在的磁道,然后才能访问数据。 如果 I/O 请求刚好连续,那就不需要磁道寻址,自然可以获得最佳性能。这其实就是我们熟悉的,连续 I/O 的工作原理。 与之相对应的,当然就是随机 I/O,它需要不停地移动磁头,来定位数据位置,所以读写速度就会比较慢。 第二类,固态磁盘(Solid State Disk),通常缩写为 SSD,由固态电子元器件组成。 固态磁盘不需要磁道寻址,所以,不管是连续 I/O,还是随机 I/O 的性能,都比机械磁盘要好得多。 无论机械磁盘,还是固态磁盘,相同磁盘的随机 I/O 都要比连续 I/O 慢很多,原因也很明显。 对机械磁盘来说,我们刚刚提到过的,由于随机 I/O 需要更多的磁头寻道和盘片旋转,它的性能自然要比连续 I/O 慢。 对固态磁盘来说,虽然它的随机性能比机械硬盘好很多,但同样存在“先擦除再写入”的限制。随机读写会导致大量的垃圾回收,所以相对应的,随机 I/O 的性能比起连续 I/O 来,也还是差了很多。 此外,连续 I/O 还可以通过预读的方式,来减少 I/O 请求的次数,这也是其性能优异的一个原因。很多性能优化的方案,也都会从这个角度出发,来优化 I/O 性能。 机械磁盘和固态磁盘分别有一个最小的读写单位。 机械磁盘的最小读写单位是扇区,一般大小为 512 字节。 固态磁盘的最小读写单位是页,通常大小是 4KB、8KB 等。 逻辑块:文件系统会把连续的扇区或页,组成逻辑块,然后以逻辑块作为最小单元来管理数据。 常见的逻辑块的大小是 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 等的字母顺序来编号。 ----------------------------------------------------------------------------------- 当你把磁盘接入服务器后,按照不同的使用方式,又可以把它们划分为多种不同的架构。 1.直接作为独立磁盘设备来使用。这些磁盘,往往还会根据需要,划分为不同的逻辑分区,每个分区再用数字编号。比如我们前面多次用到的 /dev/sda ,还可以分成两个分区 /dev/sda1 和 /dev/sda2。 2.把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列,也就是 RAID(Redundant Array of Independent Disks),从而可以提高数据访问的性能,并且增强数据存储的可靠性。 根据容量、性能和可靠性需求的不同,RAID 一般可以划分为多个级别,如 RAID0、RAID1、RAID5、RAID10 等。 3.把这些磁盘组合成一个网络存储集群,再通过 NFS、SMB、iSCSI 等网络存储协议,暴露给服务器使用。 ----------------------------------------------------------------------------------- 在 Linux 中,磁盘实际上是作为一个块设备来管理的,也就是以块为单位读写数据,并且支持随机读写。 每个块设备都会被赋予两个设备号,分别是主、次设备号。主设备号用在驱动程序中,用来区分设备类型;而次设备号则是用来给多个同类设备编号。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
通用块层 通用块层:为了减小不同块设备的差异带来的影响,Linux 通过一个统一的通用块层,来管理各种不同的块设备。 通用块层,其实是处在文件系统和磁盘驱动中间的一个块设备抽象层。它主要有两个功能 。 1.跟虚拟文件系统的功能类似。 向上,为文件系统和应用程序,提供访问块设备的标准接口; 向下,把各种异构的磁盘设备抽象为统一的块设备,并提供统一框架来管理这些设备的驱动程序。 2.通用块层还会给文件系统和应用程序发来的 I/O 请求排队,并通过重新排序、请求合并等方式,提高磁盘读写的效率。 其中,对 I/O 请求排序的过程,也就是我们熟悉的 I/O 调度。 Linux 内核支持四种 I/O 调度算法,分别是 NONE、NOOP、CFQ 以及 DeadLine。 1.NONE ,更确切来说,并不能算 I/O 调度算法。因为它完全不使用任何 I/O 调度器,对文件系统和应用程序的 I/O 其实不做任何处理,常用在虚拟机中(此时磁盘 I/O 调度完全由物理机负责)。 2.NOOP ,是最简单的一种 I/O 调度算法。它实际上是一个先入先出的队列,只做一些最基本的请求合并,常用于 SSD 磁盘。 3.CFQ(Completely Fair Scheduler),也被称为完全公平调度器,是现在很多发行版的默认 I/O 调度器,它为每个进程维护了一个 I/O 调度队列,并按照时间片来均匀分布每个进程的 I/O 请求。 类似于进程 CPU 调度,CFQ 还支持进程 I/O 的优先级调度,所以它适用于运行大量进程的系统,像是桌面环境、多媒体应用等。 4.DeadLine 调度算法,分别为读、写请求创建了不同的 I/O 队列,可以提高机械磁盘的吞吐量,并确保达到最终期限(deadline)的请求被优先处理。DeadLine 调度算法,多用在 I/O 压力比较重的场景,比如数据库等。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
I/O 栈:Linux 存储系统的 I/O 栈全景图 Linux 存储系统的 I/O 栈,由上到下分为三个层次,分别是文件系统层、通用块层和设备层。这三个 I/O 层的关系如下图所示,这其实也是 Linux 存储系统的 I/O 栈全景图。 文件系统层,包括虚拟文件系统和其他各种文件系统的具体实现。它为上层的应用程序,提供标准的文件访问接口;对下会通过通用块层,来存储和管理磁盘数据。 通用块层,包括块设备 I/O 队列和 I/O 调度器。它会对文件系统的 I/O 请求进行排队,再通过重新排序和请求合并,然后才要发送给下一级的设备层。 设备层,包括存储设备和相应的驱动程序,负责最终物理设备的 I/O 操作。 ------------------------------------------------------------------ 存储系统的 I/O ,通常是整个系统中最慢的一环。所以, Linux 通过多种缓存机制来优化 I/O 效率。 1.为了优化文件访问的性能,会使用页缓存、索引节点缓存、目录项缓存等多种缓存机制,以减少对下层块设备的直接调用。 2.为了优化块设备的访问效率,会使用缓冲区,来缓存块设备的数据。
25 | 基础篇:Linux 磁盘I/O是怎么工作的(下)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
磁盘性能指标:使用率、饱和度、IOPS、吞吐量、响应时间;性能测试工具 fio ============================================================================================= 磁盘性能指标 使用率,是指磁盘处理 I/O 的时间百分比。过高的使用率(比如超过 80%),通常意味着磁盘 I/O 存在性能瓶颈。 注意:使用率只考虑有没有 I/O,而不考虑 I/O 的大小。换句话说,当使用率是 100% 的时候,磁盘依然有可能接受新的 I/O 请求。 饱和度,是指磁盘处理 I/O 的繁忙程度。过高的饱和度,意味着磁盘存在严重的性能瓶颈。当饱和度为 100% 时,磁盘无法接受新的 I/O 请求。 IOPS(Input/Output Per Second),是指每秒的 I/O 请求数。 吞吐量,是指每秒的 I/O 请求大小。 响应时间,是指 I/O 请求从发出到收到响应的间隔时间。 不要孤立地去比较某一指标,而要结合读写比例、I/O 类型(随机还是连续)以及 I/O 的大小,综合来分析。 举个例子,在数据库、大量小文件等这类随机读写比较多的场景中,IOPS 更能反映系统的整体性能;而在多媒体等顺序读写较多的场景中,吞吐量才更能反映系统的整体性能。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
磁盘 I/O 观测:iostat工具 ================================================================================================================================================ iostat 是最常用的磁盘 I/O 性能观测工具,它提供了每个磁盘的使用率、IOPS、吞吐量等各种常见的性能指标,当然,这些指标实际上来自 /proc/diskstats。 参数 详解 -c 只显示cpu相关统计信息(默认是同时显示cpu和磁盘信息) -d 只显示磁盘统计信息(默认是同时显示cpu和磁盘信息) -h 使用NFS的输出报告更加友好可读。 -j { ID | LABEL | PATH | UUID | … } 磁盘列表的Device列要用什么维度来描述磁盘 -k 默认情况下,iostat的输出是以block作为计量单位,加上这个参数可以以kb作为计量单位显示。(该参数仅在linux内核版本2.4以后数据才是准确的) -m 默认情况下,iostat的输出是以block作为计量单位,加上这个参数可以以mb作为计量单位显示。(该参数仅在linux内核版本2.4以后数据才是准确的) -N 显示磁盘阵列(LVM) 信息 -n 显示NFS相关统计数据(network fileSystem)。(该参数只在linux内核版本2.6.17之后有用) -p [ { device [,…] | ALL } ] 显示磁盘分区的相关统计信息(默认粒度只到磁盘,没有显示具体的逻辑分区) -t 显示终端和CPU的信息,每次输出报告时显示系统时间。 -V 显示当前iostat的版本信息 -x 显示更详细的磁盘报告信息,默认只显示六列,加上该参数后会显示更详细的信息。(该参数需要在内核版本2.4之后才能使用) -y 跳过不显示第一次报告的数据,因为iostat使用的是采样统计,所以iostat的第一次输出的数据是自系统启动以来累计的数据 -z 只显示在采样周期内有活动的磁盘 -------------------------------------------------------------------------- # -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 第一列的 Device 表示磁盘设备的名字 r/s: 每秒完成的读 I/O 设备次数,即 delta(rio)/s 。 w/s: 每秒完成的写 I/O 设备次数,即 delta(wio)/s 。 rkB/s: 每秒读K字节数,是 rsect/s 的一半,因为每扇区大小为512字节.(需要计算)。 wkB/s: 每秒写K字节数,是 wsect/s 的一半.(需要计算)。 rrqm/s:每秒进行 merge 的读操作数目,即每秒这个设备相关的读取请求有多少被Merge了,也即 delta(rmerge)/s。当系统调用需要读取数据的时候,VFS将请求发到各个FS,如果FS发现不同的读取请求读取的是相同Block的数据,FS会将这个请求合并Merge。 wrqm/s:每秒进行 merge 的写操作数目,即每秒这个设备相关的写入请求有多少被Merge了,也即 delta(wmerge)/s。 r_await:每个读操作平均所需的时间;不仅包括硬盘设备读操作的时间,还包括了在kernel队列中等待的时间。 w_await:每个写操作平均所需的时间;不仅包括硬盘设备写操作的时间,还包括了在kernel队列中等待的时间。 aqu-sz(旧版为avgrq-sz): 平均每次设备I/O操作的数据大小 (扇区),即delta(rsect+wsect)/delta(rio+wio)。 avgqu-sz: 平均I/O队列长度,即 delta(aveq)/s/1000 (因为aveq的单位为毫秒)。 await:平均每次设备I/O操作的等待时间 (毫秒),即 delta(ruse+wuse)/delta(rio+wio)。这里可以理解为IO的响应时间,一般地系统IO响应时间应该低于5ms,如果大于10ms就比较大了。 svctm: 平均每次设备I/O操作的服务时间 (毫秒),即 delta(use)/delta(rio+wio)。 %util:代表磁盘繁忙程度。100% 表示磁盘繁忙, 0%表示磁盘空闲。%util为在统计时间内所有处理IO时间,除以总共统计时间。例如,如果统计间隔1秒【本次案例是2秒】,该设备有0.8秒在处理IO,而0.2秒闲置,那么该设备的%util = 0.8/1 = 80%,所以该参数暗示了设备的繁忙程度。一般地,如果该参数是100%表示设备已经接近满负荷运行了(当然如果是多磁盘,即使%util是100%,因为磁盘的并发能力,所以磁盘使用未必就到了瓶颈)。 这些指标中,你要注意: %util ,就是我们前面提到的磁盘 I/O 使用率; r/s+ w/s ,就是 IOPS; rkB/s+wkB/s ,就是吞吐量; r_await+w_await ,就是响应时间。 在观测指标时,也别忘了结合请求的大小( rareq-sz 和 wareq-sz)一起分析。 你可能注意到,从 iostat 并不能直接得到磁盘饱和度。事实上,饱和度通常也没有其他简单的观测方法,不过,你可以把观测到的,平均请求队列长度或者读写请求完成的等待时间,跟基准测试的结果(比如通过 fio)进行对比,综合评估磁盘的饱和情况。 -------------------------------------------------------------------------- # # iostat -c 2 3 Linux 3.10.0-1062.el7.x86_64 (ruioracle43) 02/23/2022 _x86_64_ (8 CPU) avg-cpu: %user %nice %system %iowait %steal %idle 0.12 0.00 0.10 0.00 0.00 99.77 %user:CPU处在用户模式下的时间百分比。 %nice:CPU处在带NICE值的用户模式下的时间百分比。 %system:CPU处在系统模式下的时间百分比。 %iowait:CPU等待输入输出完成时间的百分比。如果%iowait的值过高,表示硬盘存在I/O瓶颈。 %steal:管理程序维护另一个虚拟处理器时,虚拟CPU的无意识等待时间百分比。 %idle:CPU空闲时间百分比。 * 如果%idle值高,表示CPU较空闲。 * 如果%idle值高但系统响应慢时,可能是CPU等待分配内存,应加大内存容量。 * 如果%idle值持续低于10,表明CPU处理能力相对较低,系统中最需要解决的资源是CPU。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
进程 I/O 观测:pidstat -d; iotop =============================================================================================================== $ 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 情况,包括下面这些内容。 用户 ID(UID)和进程 ID(PID) 。 每秒读取的数据大小(kB_rd/s) ,单位是 KB。 每秒发出的写请求数据大小(kB_wr/s) ,单位是 KB。 每秒取消的写请求数据大小(kB_ccwr/s) ,单位是 KB。 块 I/O 延迟(iodelay),包括等待同步块 I/O 和换入块 I/O 结束的时间,单位是时钟周期。 -------------------------------------------------------------------------------------------------------------- iotop。它是一个类似于 top 的工具,你可以按照 I/O 大小对进程排序,然后找到 I/O 较大的那些进程。 $ 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 的时钟百分比等。
26 | 案例篇:如何找出狂打日志的“内鬼”?
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
案例篇:如何找出狂打日志的“内鬼”? ============================================================================================================ 小结: 首先,我们用 top、iostat,分析了系统的 CPU 和磁盘使用情况。我们发现了磁盘 I/O 瓶颈,也知道了这个瓶颈是案例应用导致的。 接着,我们试着照搬上一节案例的方法,用 strace 来观察进程的系统调用,不过这次很不走运,没找到任何 write 系统调用。 于是,我们又用了新的工具,借助动态追踪工具包 bcc 中的 filetop 和 opensnoop ,找出了案例应用的问题,发现这个根源是大量读写临时文件。 找出问题后,优化方法就相对比较简单了。如果内存充足时,最简单的方法,就是把数据都放在速度更快的内存中,这样就没有磁盘 I/O 的瓶颈了。当然,再进一步,你可以还可以利用 Trie 树等各种算法,进一步优化单词处理的效率。 ----------------------------------------------------------------------------------------------------------- $ docker run -v /tmp:/tmp --name=app -itd feisky/logapp $ ps -ef | grep /app.py #在终端中运行 ps 命令,确认案例应用正常启动。如果操作无误,你应该可以在 ps 的输出中,看到一个 app.py 的进程 ----------------------------------------------------------------------------------------------------------- 排查过程: 先用 top ,来观察 CPU 和内存的使用情况;然后再用 iostat ,来观察磁盘的 I/O 情况。 $ top # 按1切换到每个CPU的使用情况 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 #CPU0 的使用率非常高,它的系统 CPU 使用率(sys%)为 6%,而 iowait 超过了 90%。 %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 #说明 CPU0 上,可能正在运行 I/O 密集型的进程。 KiB Mem : 8169308 total, 747684 free, 741336 used, 6680288 buff/cache #总内存 8G,剩余内存只有 730 MB,而 Buffer/Cache 占用内存高达 6GB 之多,这说明内存主要被缓存占用。 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 #python 进程的 CPU 使用率已经达到了 6%,而其余进程的 CPU 使用率都比较低,不超过 0.3%。看起来 python 是个可疑进程。 1312 root 20 0 236532 24116 9648 S 0.3 0.3 9:29.80 python3 $ iostat -x -d 1 # -d表示显示I/O性能指标,-x表示显示扩展统计(即所有I/O指标) 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 磁盘 sda 的 I/O 使用率已经高达 99%,很可能已经接近 I/O 饱和。 再看前面的各个指标,每秒写磁盘请求数是 64 ,写大小是 32 MB,写请求的响应时间为 7 秒,而请求队列长度则达到了 1100。 #超慢的响应时间和特长的请求队列长度,进一步验证了 I/O 已经饱和的猜想。此时,sda 磁盘已经遇到了严重的性能瓶颈。 $ pidstat -d 1 #使用 pidstat 加上 -d 参数,就可以显示每个进程的 I/O 情况 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 的结果还要大。 #现象:再看 iodelay 项。虽然只有 python 在大量写数据,但你应该注意到了,有两个进程 (kworker 和 jbd2 )的延迟,居然比 python 进程还大很多。 #kworker 是一个内核线程,而 jbd2 是 ext4 文件系统中,用来保证数据完整性的内核线程。他们都是保证文件系统基本功能的内核线程,所以具体细节暂时就不用管了,我们只需要明白,它们延迟的根源还是大量 I/O。 $ strace -p 18940 #运行 strace 命令,并通过 -p 18940 指定 python 进程的 PID 号 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 #从 write() 系统调用上,我们可以看到,进程向文件描述符编号为 3 的文件中,写入了 300MB 的数据。 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 # stat() 调用正在获取 /tmp/logtest.txt.1 的状态。 这种“点 + 数字格式”的文件,在日志回滚中非常常见。我们可以猜测,这是第一个日志回滚文件,而正在写的日志文件路径,则是 /tmp/logtest.txt。 工具 lsof。它专门用来查看进程打开文件列表,不过,这里的“文件”不只有普通文件,还包括了目录、块设备、动态库、网络套接字等。 $ 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 #进程打开了文件 /tmp/logtest.txt,并且它的文件描述符是 3 号,而 3 后面的 w ,表示以写的方式打开。 FD 表示文件描述符号,TYPE 表示文件类型,NAME 表示文件路径。 找出了问题根源,接下来按照惯例,就该查看源代码,然后分析为什么这个进程会狂打日志了。 运行 docker cp 命令,把案例应用的源代码拷贝出来,然后查看它的内容。 #拷贝案例应用源代码到当前目录 $ 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) 一般来说,生产系统的应用程序,应该有动态调整日志级别的功能。继续查看源码,你会发现,这个程序也可以调整日志级别。如果你给它发送 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) $ kill -SIGUSR2 18940 $ top $ iostat -d -x 1 观察 top 和 iostat 的输出,你会发现,稍等一段时间后,iowait 会变成 0,而 sda 磁盘的 I/O 使用率也会逐渐减少到 0。
27 | 案例篇:为什么我的磁盘I/O延迟很高?
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
案例篇:为什么我的磁盘I/O延迟很高? ============================================================================================================ server端运行 $ docker run --name=app -p 10000:80 -itd feisky/word-pop client端测试 $ curl http://192.168.1.160:10000/ #正常 $ curl http://192.168.1.160:10000/popularity/word #响应很慢,看起来就像hold住了。。 $ while true; do time curl http://192.168.1.160:10000/popularity/word; sleep 1; done ------------------------------------ 开始分析 $ 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 #两个 CPU 的 iowait 都非常高。特别是 CPU0, iowait 已经高达 94 %,而剩余内存还有 3GB,看起来也是充足的。 %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 #进程部分有一个 python 进程的 CPU 使用率稍微有点高,达到了 14%。虽然 14% 并不能成为性能瓶颈,不过有点嫌疑——可能跟 iowait 的升高有关。 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 $ 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 $ iostat -d -x 1 #-d 选项是指显示出 I/O 的性能指标;-x 选项是指显示出扩展统计信息(即显示所有 I/O 指标)。 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 #磁盘 sda 的 I/O 使用率已经达到 98% ,接近饱和了 #写请求的响应时间高达 18 秒,每秒的写数据为 32 MB,显然写磁盘碰到了瓶颈。 $ 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 -p 12280 $ strace -p 12280 2>&1 | grep write #从 strace 中,你可以看到大量的 stat 系统调用,并且大都为 python 的文件,但是,请注意,这里并没有任何 write 系统调用。 原因:写文件是由子线程执行的,所以直接strace跟踪进程没有看到write系统调用,可以通过pstree查看进程的线程信息,再用strace跟踪。或者,通过strace -fp pid 跟踪所有线程。 ------------------------------------------------------ 我们只好综合 strace、pidstat 和 iostat 这三个结果来分析了。很明显,你应该发现了这里的矛盾: iostat 已经证明磁盘 I/O 有性能瓶颈,而 pidstat 也证明了,这个瓶颈是由 12280 号进程导致的, 但 strace 跟踪这个进程,却没有找到任何 write 系统调用。 文件写,明明应该有相应的 write 系统调用,但用现有工具却找不到痕迹,这时就该想想换工具的问题了。 一个新工具, filetop。它是 bcc 软件包的一部分,基于 Linux 内核的 eBPF(extended Berkeley Packet Filters)机制,主要跟踪内核中文件的读写情况,并输出线程 ID(TID)、读写大小、读写类型以及文件名称。 $ cd /usr/share/bcc/tools # 切换到工具目录 $ ./filetop -C # -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)、文件类型以及读写的文件名称。 发现,每隔一段时间,线程号为 514 的 python 应用就会先写入大量的 txt 文件,再大量地读。 $ ps -efT | grep 514 root 12280 514 14626 33 14:47 pts/0 00:00:05 /usr/local/bin/python /app.py 514线程正是案例应用 12280 的线程。 再介绍一个好用的工具,opensnoop 。它同属于 bcc 软件包,可以动态跟踪内核中的 open 系统调用。 $ 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 @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 的性能问题。 time curl http://192.168.1.160:10000/popularity/word time curl http://192.168.1.160:10000/popular/word #优化前后的2个API的效果对比
28 | 案例篇:一个SQL查询要15秒,这是怎么回事?
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
案例篇:一个SQL查询要15秒,这是怎么回事? =========================================================================================================== 案例总共由三个容器组成,包括一个 MySQL 数据库应用、一个商品搜索应用以及一个数据处理的应用。其中,商品搜索应用以 HTTP 的形式提供了一个接口: /:返回 Index Page; /db/insert/products/:插入指定数量的商品信息; /products/:查询指定商品的信息,并返回处理时间。 在第一个终端中执行下面命令,拉取本次案例所需脚本 $ 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 docker logs 命令,查看数据库的启动过程(MySQL 数据库的启动过程,需要做一些初始化工作,这通常需要花费几分钟时间) 当你看到下面这个输出时,说明 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) 执行下面的命令,确认它也已经正常运行 $ 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 我们切换到client端,访问一下商品搜索的接口,看看能不能找到想要的商品。执行如下的 curl 命令: $ curl http://192.168.0.10:10000/products/geektime Got data: () in 15.364538192749023 sec #这个接口返回的是空数据,而且处理时间超过 15 秒。这么慢的响应速度让人无法忍受,到底出了什么问题呢? ------------------------------------------- 排查过程: client端 $ while true; do curl http://192.168.0.10:10000/products/geektime; sleep 5; done server端开始排查 $ top #执行 top 命令,分析系统的 CPU 使用情况 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 #两个 CPU 的 iowait 都比较高,特别是 CPU0,iowait 已经超过 60%。 %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 #而具体到各个进程, CPU 使用率并不高,最高的也只有 1.7%。 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 $ 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 #磁盘 sda 每秒的读数据为 32 MB, 而 I/O 使用率高达 97% ,接近饱和,这说明,磁盘 sda 的读取确实碰到了性能瓶颈。 $ pidstat -d 1 ## -d选项表示展示进程的I/O情况 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 #mysqld 进程正在进行大量的读,而且读取速度是 32 MB/s,跟刚才 iostat 的发现一致。 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 接下来是分析mysqld进行大量读动作的原因 慢查询的现象大多是 CPU 使用率高(比如 100% ),但这里看到的却是 I/O 问题。看来,这并不是一个单纯的慢查询问题,我们有必要分析一下 MySQL 读取的数据。 #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。 $ lsof -p 28014 #lsof 并没有给出任何输出 $ echo $? #如果你查看 lsof 命令的返回值,就会发现,这个命令的执行失败了。 失败的原因在于28014是线程ID,而 lsof -p要求输入进程ID $ pstree -t -a -p 27458 ## -t表示显示线程,-a表示显示命令行参数 mysqld,27458 --log_bin=on --sync_binlog=1 ... ├─{mysqld},27922 ├─{mysqld},27923 └─{mysqld},28014 $ 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 mysqld 进程确实打开了大量文件,而根据文件描述符(FD)的编号,描述符为 38 的是一个路径为 /var/lib/mysql/test/products.MYD 的文件。 38 后面的 u 表示, mysqld 以读写的方式访问文件。 /var/lib/mysql/test/products.MYD MYD 文件,是 MyISAM 引擎用来存储表数据的文件; 文件名就是数据表的名字; 而这个文件的父目录,也就是数据库的名字。 结论:mysqld 在读取数据库 test 中的 products 表。 既然已经找出了数据库和表,接下来要做的,就是弄清楚数据库中正在执行什么样的 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> 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) #多执行几次 show full processlist 命令,你可看到 select * from products where productName=‘geektime’ 这条 SQL 语句的执行时间比较长。 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) type 表示查询类型,这里的 ALL 表示全表查询,但索引查询应该是 index 类型才对; possible_keys 表示可能选用的索引,这里是 NULL; key 表示确切会使用的索引,这里也是 NULL; 根据这些信息,我们可以确定,这条查询语句压根儿没有使用索引,所以查询时,会扫描全表,并且扫描行数高达 10000 行。响应速度那么慢也就难怪了。 优化方法:建立索引 优化步骤:略 -------------------------------- 另一个优化方法: 停止 DataService 后,商品搜索应用的处理时间,从 15 秒缩短到了 0.1 秒 原因: DataService 实际上是在读写一个仅包括 “data” 字符串的小文件。不过在读取文件前,它会先把 /proc/sys/vm/drop_caches 改成 1。 echo 1>/proc/sys/vm/drop_caches表示释放pagecache,也就是文件缓存,而mysql读书的数据就是文件缓存,dataservice不停的释放文件缓存,就导致MySQL都无法利用磁盘缓存。
29 | 案例篇:Redis响应严重延迟,如何解决?
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
案例篇:Redis响应严重延迟,如何解决? ============================================================================================================ # 注意下面的随机字符串是容器ID,每次运行均会不同,并且你不需要关注它 $ docker run --name=redis -itd -p 10000:80 feisky/redis-server $ docker run --name=app --network=container:redis -itd feisky/redis-app $ docker ps #确认两个容器都处于运行(Up)状态 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 $ curl http://192.168.1.160:10000/ hello redis # 案例插入5000条数据,在实践时可以根据磁盘的类型适当调整,比如使用SSD时可以调大,而HDD可以适当调小 $ curl http://192.168.1.160:10000/init/50000 {"elapsed_seconds":30.26814079284668,"keys_initialized":50000} #访问应用的缓存查询接口。如果一切正常,你会看到如下输出: $ curl http://192.168.1.160:10000/get_cache {"count":1677,"data":["d97662fa-06ac-11e9-92c7-0242ac110002",...],"elapsed_seconds":10.545469760894775,"type":"good"} #发现问题:这个接口调用居然要花 10 秒!这么长的响应时间,显然不能满足实际的应用需求。 --------------------------------------------------- 案例排查过程: client端 $ while true; do curl http://192.168.1.160:10000/get_cache; done server端开始排查 $ 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 #CPU0 的 iowait 比较高,已经达到了 84%; %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 #总内存 8GB,剩余内存还有 7GB 多,显然内存也没啥问题。 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 #各个进程的 CPU 使用率都不太高,最高的 python 和 redis-server ,也分别只有 8% 和 5%。 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 的信息,最有嫌疑的就是 iowait。 $ 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 本身并没有性能瓶颈,但这里的磁盘写也是比较奇怪的。为什么会有磁盘写呢?那我们就得知道,到底是哪个进程在写磁盘。 $ 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,并且它也刚好是在写磁盘。 再用 strace+lsof 组合,看看 redis-server 到底在写什么。 # -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 -p 9085 redis-ser 9085 systemd-network 3r FIFO 0,12 0t0 15447970 pipe #描述符编号为 3 的是一个 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] #5 号是 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 #7 号是一个普通文件 redis-ser 9085 systemd-network 8u sock 0,9 0t0 15448709 protocol: TCP #8 号是一个 TCP socket #结合磁盘写的现象,我们知道,只有 7 号普通文件才会产生磁盘写,而它操作的文件路径是 /data/appendonly.aof,相应的系统调用包括 write 和 fdatasync。 如果你对 Redis 的持久化配置比较熟,看到这个文件路径以及 fdatasync 的系统调用,你应该能想到,这对应着正是 Redis 持久化配置中的 appendonly 和 appendfsync 选项。很可能是因为它们的配置不合理,导致磁盘写比较多。 通过 Redis 的命令行工具,查询这两个选项的配置。 $ docker exec -it redis redis-cli config get 'append*' 1) "appendfsync" 2) "always" #appendfsync 配置的是 always,而 appendonly 配置的是 yes。 3) "appendonly" 4) "yes" #appendfsync 配置的是 always,而 appendonly 配置的是 yes。 刚刚看到的配置,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。 --------------------------------------------------------------------- 为什么查询时会有磁盘写呢?按理来说不应该只有数据的读取吗?这就需要我们再来审查一下 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 读写,是一个标准的“请求 - 响应”格式,即: 从 socket 读取 GET uuid:53522908-… 后,响应 good; 再从 socket 读取 SADD good 535… 后,响应 1。 对 Redis 来说,SADD 是一个写操作,所以 Redis 还会把它保存到用于持久化的 appendonly.aof 文件中。 观察更多的 strace 结果,你会发现,每当 GET 返回 good 时,随后都会有一个 SADD 操作,这也就导致了,明明是查询接口,Redis 却有大量的磁盘写。 lsof 命令加上 -i 选项,找出 TCP socket 对应的 TCP 连接信息。不过,由于 Redis 和 Python 应用都在容器中运行,我们需要进入容器的网络命名空间内部,才能看到完整的 TCP 连接。 # 由于这两个容器共享同一个网络命名空间,所以我们只需要进入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 提供了两种数据持久化的方式,分别是快照和追加文件。 快照方式,会按照指定的时间间隔,生成数据的快照,并且保存到磁盘文件中。为了避免阻塞主进程,Redis 还会 fork 出一个子进程,来负责快照的保存。这种方式的性能好,无论是备份还是恢复,都比追加文件好很多。 不过,它的缺点也很明显。在数据量大时,fork 子进程需要用到比较大的内存,保存数据也很耗时。所以,你需要设置一个比较长的时间间隔来应对,比如至少 5 分钟。这样,如果发生故障,你丢失的就是几分钟的数据。 追加文件,则是用在文件末尾追加记录的方式,对 Redis 写入的数据,依次进行持久化,所以它的持久化也更安全。 此外,它还提供了一个用 appendfsync 选项设置 fsync 的策略,确保写入的数据都落到磁盘中,具体选项包括 always、everysec、no 等。 always 表示,每个操作都会执行一次 fsync,是最为安全的方式; everysec 表示,每秒钟调用一次 fsync ,这样可以保证即使是最坏情况下,也只丢失 1 秒的数据; 而 no 表示交给操作系统来处理。
30 | 套路篇:如何迅速分析出系统I/O的瓶颈在哪里?
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
案例排查过程 第一,在文件系统的原理中,我介绍了查看文件系统容量的工具 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 的方式,从而减少网络通信次数,解决性能问题。
性能指标
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
文件系统 I/O 性能指标;磁盘 I/O 性能指标 ======================================================================================================================= 文件系统 I/O 性能指标 1.存储空间 存储空间的使用情况,包括容量、使用量以及剩余空间等。我们通常也称这些为磁盘空间的使用量,因为文件系统的数据最终还是存储在磁盘上。 不过要注意,这些只是文件系统向外展示的空间使用,而非在磁盘空间的真实用量,因为文件系统的元数据也会占用磁盘空间。 如果你配置了 RAID,从文件系统看到的使用量跟实际磁盘的占用空间,也会因为 RAID 级别的不同而不一样。比方说,配置 RAID10 后,你从文件系统最多也只能看到所有磁盘容量的一半。 2.索引节点 索引节点的使用情况,它也包括容量、使用量以及剩余量等三个指标。如果文件系统中存储过多的小文件,就可能碰到索引节点容量已满的问题。 3.缓存 缓存使用情况,包括页缓存、目录项缓存、索引节点缓存以及各个具体文件系统(如 ext4、XFS 等)的缓存。 这些缓存会使用速度更快的内存,用来临时存储文件数据或者文件系统的元数据,从而可以减少访问慢速磁盘的次数。 4.文件 I/O 文件 I/O 也是很重要的性能指标,包括 IOPS(包括 r/s 和 w/s)、响应时间(延迟)以及吞吐量(B/s)等。 在考察这类指标时,通常还要考虑实际文件的读写情况。比如,结合文件大小、文件数量、I/O 类型等,综合分析文件 I/O 的性能。 这些性能指标非常重要,但不幸的是,Linux 文件系统并没提供,直接查看这些指标的方法。我们只能通过系统调用、动态跟踪或者基准测试等方法,间接进行观察、评估。 --------------------------------------------------------------------------------------------------------------------------- 磁盘 I/O 性能指标 1.使用率,是指磁盘忙处理 I/O 请求的百分比。过高的使用率(比如超过 60%)通常意味着磁盘 I/O 存在性能瓶颈。 2.IOPS(Input/Output Per Second),是指每秒的 I/O 请求数。 3.吞吐量,是指每秒的 I/O 请求大小。 4.响应时间,是指从发出 I/O 请求到收到响应的间隔时间。 4.缓冲区(Buffer)也是要重点掌握的指标,它经常出现在内存和磁盘问题的分析中。 考察这些指标时,一定要注意综合 I/O 的具体场景来分析,比如读写类型(顺序还是随机)、读写比例、读写大小、存储类型(有无 RAID 以及 RAID 级别、本地存储还是网络存储)等。 有个大忌,就是把不同场景的 I/O 性能指标,直接进行分析对比。这是很常见的一个误区,你一定要避免。
性能工具
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
如何迅速分析 I/O 的性能瓶颈 ------------------------------------------------- 为了缩小排查范围,我通常会先运行那几个支持指标较多的工具,如 iostat、vmstat、pidstat 等。 然后再根据观察到的现象,结合系统和应用程序的原理,寻找下一步的分析方向。 最开始的分析思路基本上类似,都是: 1.先用 iostat 发现磁盘 I/O 性能瓶颈; 2.再借助 pidstat ,定位出导致瓶颈的进程; 3.随后分析进程的 I/O 行为; 4.最后,结合应用程序的原理,分析这些 I/O 的来源。
31 | 套路篇:磁盘 I/O 性能优化的几个思路
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
I/O 基准测试:fio工具 ==================================================================================== 为了更客观合理地评估优化效果,我们首先应该对磁盘和文件系统进行基准测试,得到文件系统或者磁盘 I/O 的极限性能。 fio(Flexible I/O Tester)正是最常用的文件系统和磁盘 I/O 性能基准测试工具。它提供了大量的可定制化选项,可以用来测试,裸盘或者文件系统在各种场景下的 I/O 性能,包括了不同块大小、不同 I/O 引擎以及是否使用缓存等场景。 # Ubuntu apt-get install -y fio # CentOS yum install -y 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 参数: direct,表示是否跳过系统缓存。上面示例中,我设置的 1 ,就表示跳过系统缓存。 iodepth,表示使用异步 I/O(asynchronous I/O,简称 AIO)时,同时发出的 I/O 请求上限。在上面的示例中,我设置的是 64。 rw,表示 I/O 模式。我的示例中, read/write 分别表示顺序读 / 写,而 randread/randwrite 则分别表示随机读 / 写。 ioengine,表示 I/O 引擎,它支持同步(sync)、异步(libaio)、内存映射(mmap)、网络(net)等各种 I/O 引擎。上面示例中,我设置的 libaio 表示使用异步 I/O。 bs,表示 I/O 的大小。示例中,我设置成了 4K(这也是默认值)。 filename,表示文件路径,当然,它可以是磁盘路径(测试磁盘性能),也可以是文件路径(测试文件系统性能)。示例中,我把它设置成了磁盘 /dev/sdb。不过注意,用磁盘路径测试写,会破坏这个磁盘中的文件系统,所以在使用前,你一定要事先做好数据备份。 使用 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)。不同之处在于: slat ,是指从 I/O 提交到实际执行 I/O 的时长(Submission latency); clat ,是指从 I/O 提交到 I/O 完成的时长(Completion latency); 而 lat ,指的是从 fio 创建 I/O 到 I/O 完成的总时长。 这里需要注意的是,对同步 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 的记录。
I/O 性能优化(未总结)
应用程序优化
文件系统优化
磁盘优化