0x00 前言

在上一篇笔记中我们了解到了内存本质上它的作用就是作为外部存储的缓存,而数据的持久化和未命中的数据访问等最终还是需要外部存储如磁盘,那么自然就需要频繁的 I/O 操作,这篇笔记就是介绍磁盘的工作原理和 I/O 的优化手段。

0x10 原理

老惯例,在要学习 I/O 的性能分析和优化手段之前,我们要先学习外部存储的原理。从上篇内存篇笔记中我们知道内存主要有 buff/cache 分别为磁盘 I/O 和文件系统 I/O 进行缓存,磁盘读写即为直接访问磁盘块设备,又称为裸 I/O,文件系统则是建立在磁盘上的一种文件的管理和组织方式,它是处于在块设备之上的。我们的应用程序有的是通过文件系统 I/O 进行访问,有的则是直接裸 I/O 访问的磁盘,针对不同的访问方式,我们有不同的优化手段。另外通常情况下,从上到下 I/O 栈主要分为三层:

  • 文件系统层: 包括虚拟文件系统层,为应用程序提供文件访问接口和管理磁盘数据
  • 通用块层:包括设备 I/O 队列和 I/O 调度器,对 I/O 进行管理和调度,再发给下一级设备
  • 设备层:包括存储设备和存储驱动,负责最终的 I/O

0x11 文件系统

文件系统是一种文件的组织和管理机制,而不同的管理机制就是不同文件系统,如 ext4,xfs, ntfs 等等。在文件系统中,一个文件不仅仅只是存储在磁盘上的数据,这些数据会有附带的元信息。就好像一个“人”除了本身的生理躯体外,他还有名字、出生日期、身份证号码和家庭住址等很多信息才能被称为一个人,他才能被标识和寻址。在 Linux 上,文件系统为了管理会为每个文件都分配两个数据结构来记录文件的元信息和目录结构:

  • 索引节点:inode(index node)
    1. 记录文件元数据,inode 编号,文件大小,权限,修改日志等
    2. inode 和文件一一对应,就像每个人对应一个 id card
    3. 存储在磁盘,需要占用磁盘空间
  • 目录项: dentry(directory entry)
    1. 记录文件的名字,inode
    2. 记录文件和其他文件的关联关系,就像家谱一样
    3. 由内核维护并存在内存中,也叫目录项缓缓存
    4. 和inode 是多对一的关系,可以理解为一个文件有多个别名,就好像一个人可以有多个身份,既可以是一个人的爸爸,也可以是另一个人的儿子

索引节点和目录项的关系如下图所示:

从这个图我们可以得知以下几点:

  1. 每个 dentry 就是一个数据结构,这个数据结构里面包含了 inode,文件名和其他关联的 dentry 地址等
  2. 其中 inode 是存储在磁盘的,dentry 是内存缓存
  3. 由上篇内存笔记可以推测,inode 也会缓存到内存中
  4. dentry 之间类似树形结构
  5. inode 和文件数据是存储在不同的磁盘 block 的

根据第六点,我们又可以引申出另外一个知识点,那就是给磁盘格式化文件系统的时候,会把文件系统分为三个 block:

  • 超级块: 存储整个文件系统的状态
  • inode block: 存储 inode 节点
  • 数据块:存储文件数据

而机械硬盘的最小扇区是512B,而每次读这么小效率低,所以文件系统还会把扇区组织成4KB的逻辑块,如上图左边所示,数据块被分为了一个个逻辑块

0x1100 虚拟文件系统

所有的文件系统的四大基本要素:

  • dentry
  • inode
  • 逻辑块
  • 超级块

而不同的文件系统有不同的组织方式,Linux 内核在用户进程和文件系统之间引入抽象层 VFS(Virtual File System),就好像 Java 里面的实现抽象方法一样, VFS 是抽象类,具体的文件系统则实现抽象类定义的接口。

如下图所示:

我们可以看到 Linux 支持的文件系统可分为:

  1. 磁盘文件系统
  2. 网络文件系统
  3. 内存文件系统

文件系统先要在 VFS 中找个挂载点挂载,然后用户进程就可以通过 VFS 的文件接口访问其中的文件了

0x1101 文件系统 I/O

通过文件接口可以对文件进行读写等操作,读写方式的差异导致 I/O 有很多分类。主要有四种:

  • 是否标准库缓冲(缓冲由标准库内部实现):
    1. 缓冲 I/O:使用标准库缓存加速文件访问
    2. 非缓冲 I/O:直接通过系统调用访问
  • 直接/非直接 I/O:
    1. 直接 I/O 跳过操作系统的页缓存(cache),直接访问文件系统。使用 O_DIRECT标志
    2. 非直接 I/O 使用 cache
  • 阻塞/非阻塞 I/O(调用者视角,关注是否等待调用结果):
    1. 调用者执行 I/O 操作,如果被调用者没有响应,则调用者线程阻塞,不能干别的事了。
    2. 调用者执行 I/O 操作,如果被调用者没有响应,调用者线程也不会被阻塞,可以去干别的事。后续调用者可以通过轮询(调用者过段时间就去看下好了没)或则事件通知(被调用者好了后主动通知调用者)的方式获取到响应。
  • 同步/异步 I/O(被调用者视角,关注的是消息的通知机制):
    1. 同步是被调用者直到 I/O 操作执行完才会返回结果给调用者。如果调用者是阻塞的,那么调用者就只能一直等着被调用者执行完了;如果调用者是非阻塞的,调用者就先跑了,他可能过段时间再来看下是不是好了(轮询)或者打电话给他(事件通知)
    2. 异步是被调用者马上返回 I/O 操作给调用者,让调用者先去干别的事,好了再通知他。

0x1102 文件系统性能观察

  • 容量: df 命令
  • 缓存:
    • 查看/proc/memeinfoSlab,/proc/slabinfo文件查看 dentry 和 inode 缓存
    • slabtop 工具

0x12 磁盘 I/O

磁盘主要分为机械磁盘和固态磁盘,磁盘的连续读写速度比随机读写速度更快,在 Linux 中,磁盘是作为块设备使用的。为了减小不同磁盘设备带来的差异,Linux 通过通用块层来管理不同的块设备。通用块层的主要功能如下:

  • 承上启下,为文件系统提供统一的标准接口和提供统一框架管理块设备的驱动程序
  • 对上层的 I/O 请求排队并进行调度,提高磁盘读写效率,Linux 支持 4 种调度算法
    • NONE: 不使用任何调度,常用于 vm
    • NOOP: 先入先出的队列,常用于 ssd
    • CFQ(Completely Fair Scheduler): 完全公平调度器,基于时间片段来分布每个进程的请求,支持调度优先级,适合大量进程的系统,如 desktop,media
    • DeadLine: 读写使用不同的 I/O 队列,多用于 I/O 压力比较重的场景,如数据库

0x1200 磁盘性能指标

  • 使用率:处理 I/O 的时间百分比(只考虑 I/O 数量,不考虑 I/O 大小)
  • 饱和度:处理 I/O 的繁忙程度,100%时无法接受 I/O 请求
  • IOPS(Input/Output Per Second): 每秒接受的 I/O 请求
  • 吞吐量:每秒的 I/O 请求大小
  • 响应时间:I/O 请求发出到响应的时间

0X1201 性能观测

  • 整体:iostat
  • 单个进程:pidstat,iostop
  • 磁盘性能度量:fio

0x20 性能调优

在了解了 I/O 栈的原理后,我们分析性能瓶颈的路数大概都是

  1. iostat 观察整体的磁盘情况,分析各项指标
  2. pidstat 观察具体进程的情况,分析各项指标
  3. strace分析进程的 I/O 行为
  4. 结合应用程序,分析I/O 来自哪里,是否正常

其中文件系统和 I/O 指标和磁盘的 I/O 指标需要分开来分析,另外在分析这些指标的时候,需要根据具体的 I/O 场景来进行分析,除了分析 I/O 指标,内存缓存也是需要分析的。

如果要对 I/O 进行调优,则每层都有不同的手段:

  • 应用层

    • 连续读写代替随机读写
    • 多使用系统缓存
    • 使用外部缓存,如 MySQL 前放 Redis
    • 读写同一块磁盘空间的时候,使用 mmap(内存文件映射)
    • 同步写的时候,请求合并,减少访问 I/O 次数
    • 如果有多个进程时,可以用 cgroup 限制 I/O
    • 在 CFQ 中使用 ionice 调整进程的 I/O 优先级
  • 文件系统层

    • 使用合适的文件系统
    • 优化文件系统配置选项
    • 优化文件系统缓存
    • 不需要持久化可使用 tmpfs
  • 磁盘

    • 用 SSD
    • 组 RAID
    • 选择合适的 I/O 调度算法
    • 不同进程使用不同的磁盘
    • 针对顺序读场景增大预读数据
    • 优化内核,如调整 I/O 队列长度