第一章 磁盘I/O基础和常见优化策略及性能指标(Innodb原理)

393 阅读18分钟

1、磁盘的结构

磁盘主要由盘片、旋转电机、转轴、机械臂和磁头组成,它们相互配合进行数据的读取和存储,其中盘片是数据存储的载体,一般一块磁盘都会有多个盘片,由通过它们圆心的转轴固定而垂直叠落在一起,旋转电机通过转轴带动所有盘片同步转动。机械臂则承载着磁头,使磁头可以沿着盘片的半径方向前后移动,值的注意的是,类似转轴带动盘片转动一样,机械臂对磁头的移动也是同步的,所有的磁头同步移动。

我们这里只介绍机械硬盘的结构和基本原理。虽然SSD固态硬盘已经相当常见,但机械硬盘因其价格低、容量大和安全性高等特点仍然承担着服务器端存储设备的主流角色。

image.png

  • 磁道
    盘片上同心环即为磁道,之所以是同心环,那是因为磁道是有宽度的。

  • 扇区
    取磁道上的一小段圆弧即为扇区。扇区存储数据的大小是固定的,是磁盘原子性读写数据的最小单位,一般为512B,也就是说,磁盘每次至少要读取或者写入512B大小的数据。读数据的时候,如果目标数据未能占满最后一个扇区,也会读取整个扇区,多余的部分将被丢弃;写数据的时候,如果目标数据未能占满最后一个扇区,也会写整个扇区大小的数据,只是在多余的空间写入零,也就是说扇区的剩余空间是不能被利用的。

友情提示:细心的读者可能会发现,磁道的周长是不一样的,越靠近外围的磁道的周长越长,这是否意味着磁道上的扇区也会越多呢?答案是不一定,这要看不同的实现标准,有的标准是越往外的磁道拥有的扇区数量越多;有的标准是一样多,这就意味着外围的磁道上的扇区长度会变长,磁介质会越稀疏,显然这是比较浪费磁盘空间。早期磁盘多为第二种实现标准,现代磁盘则为第一种实现标准。

  • 柱面
    一般磁盘会有多个盘片,所有盘片叠落在一起形成一个圆柱体,而不同盘片相同半径的磁道就在垂直方向形成了一个柱面。每个盘片都会对应一个磁头,不过同一时间只能有一个磁头进行读写操作。数据的读写是按照柱面进行的,即先读写完一个盘片的一个磁道然后接着读写下一个盘片的相同的磁道,这样做主要是为了减少寻道时间,因为对不同盘片相同磁道的读取只需要通过电信号选择不同的磁头即可,减少了机械臂前后移动磁头而跳到不同磁道的机械操作(这个时间成本相对比较大)。

  • 寻道
    机械臂将磁头移动到指定磁道上即为寻道。更准确的说法是,将所有盘片对应的磁头都移动到相同的磁道上(即柱面)。前面我们已经说过所有的磁头的移动都是同步的。

  • 旋转延迟
    通过寻道找到对应的磁道后,盘片将最多旋转一周把要读取数据对应的扇区旋转到磁头下,以备后续开始读取,这个操作是需要花费一定时间的,我们称之为旋转延迟。

友情提示:磁头读取扇区的数据后需要一定的处理时间,早期硬盘因为磁盘的处理时间和盘片的转速存在不匹配的情况,在读取一个物理上连续的多个扇区时,读取了一个扇区之后,在处理读取到的数据的这段时间内,下一个扇区可能已经从磁头转过,因此磁头就需要再等待最多一周的时间才能读取到这个扇区,这无疑大大的增加了旋转延迟的时间。为了解决这个问题,扇区编号采用了交错编号的方式,即扇区在盘面的编号不再是连续的1,2,3,4,5...,而是1,3,2,5,4...这样中间间隔N个编号(即交错因子)的交错编号,也就是说编号连续的扇区在物理位置上不再是连续的,而是中间间隔N个扇区。合理的调整交错因子的值,就可以使得读取并处理完编号为1的扇区的数据后,紧接着就可以读取到编号为2的扇区,从而极大地减少了旋转延迟时间。现代磁盘由于内部处理速度的提升,交错因子一般为1,即在物理上是连续的。

  • 寻址
    寻道和旋转延迟的目的是为了找到数据的起始扇区,即找到数据存储的地址。我们把这个操作叫做磁盘寻址。

2、磁盘的读写过程

当读写请求到达磁盘后,机械臂先将磁头移动到相应的柱面(即寻道),然后盘片高速旋转,把要读写数据对应扇区在磁道上的起始位置旋转到磁头下(即旋转延迟),然后盘片继续旋转,进行数据读写,若读写数据的长度超过一个盘片磁道的大小,则进行下一个盘片相同磁道的读写,若同一柱面的磁道都读写完成,则进行下一个柱面的读写,依次往复,直到读写完成。

3、磁盘读写性能消耗

磁盘进行一次读写的操作需要历经寻道、旋转延迟、数据读取和数据传输(即将读取的数据读取到内存),其中数据读取和传输的时间可以忽略不计,主要的性能消耗在寻道和旋转延迟上,又以寻道为主。

3.1、何为磁盘的一次I/O操作?

磁盘进行一次I/O操作只能写入或读取一个或多个连续的扇区,如果要读取的数据位于多个不连续的扇区,那么磁盘就需要进行多次IO操作。

上层模块往往会帮我们屏蔽掉磁盘调用的细节,如应用程序读写一个文件,文件的分布可能不是连续的,但是上层模块帮我们屏蔽了对磁盘进行多次I/O调用的细节。

3.2、磁盘一次I/O具体时间消耗

下面列举了一次磁盘I/O操作具体的时间消耗,这个时间是有时间局限性的,随着磁盘技术的发展,下面各项的值可能会有所变化。

操作时间影响
寻道5ms到15ms主要
旋转延迟0ms到6ms次要
读写传输--相对忽略不计

磁盘一次I/O时间 = 寻址时间【寻道时间 + 旋转延迟时间】 + 读写传输时间(可忽略)

4、磁盘I/O优化基本原则

既然磁盘进行一次读写操作的主要性能消耗在寻址上,那么我们在进行磁盘读写的时候,就可以针对性的进行优化,以提高磁盘I/O的效率。

  • 随机I/O
    是指读写一段逻辑上连续但存储地址不连续数据时发生的读写操作。我们知道扇区是磁盘存储数据的最小单位,当一段数据由多个扇区组成且这些扇区在磁盘上是随机分布的时候,读写这段数据就需要进行多次寻址。随机I/O的特点是小而乱,每次请求的数据体积小,所有请求地址整体杂乱。

  • 顺序I/O
    与随机I/O相反,组成数据的多个扇区在磁盘上的地址是连续的。那么读写这段数据就只需要一次寻址找到起始扇区,然后依次读写即可。我们在这里再把限制放宽一些,如果要读取的数据的扇区是顺序排列的而且它们位置相距不远,也被认为是顺序I/O,原因是位置相距不远可以很大程度上减少磁道的来回切换(即多次寻道),同时还有预读机制(这个将在后面详细介绍)的存在,预读的数据中可能就包含了后面要读取的数据。顺序I/O的特点是大而少,每次请求的数据量大,请求的总体次数少。

企业微信截图_1665649153380.png

相关资料显示,磁盘顺序I/O和随机I/O之间有几个数量级的差距。也就是说我们应该尽可能的让我们的I/O操作是顺序I/O。

友情提示:SSD固态硬盘包括内存在读写数据的时候都需要进行寻址,他们在“随机I/O”和“顺序I/O”之间也存在着性能差异。对于SSD固态硬盘和内存来说并不存在传统机械硬盘所谓的“顺序/随机”的概念,我们这里只是借用了这种说法。

要想提高I/O的效率,重点在减少寻道和旋转延迟的次数,一个更直接的方式就是干脆减少I/O的次数,自然寻道和旋转延迟的次数就少了。

由此我们得到了I/O应用优化的两个方法,这也是众多I/O密集型应用系统优化的两个基本原则。

  • 原则一:尽可能的减少I/O的次数。

  • 原则二:尽可能的使用顺序I/O或将随机I/O转化为顺序I/O。

5、磁盘I/O优化常见策略

5.1、磁盘预读(Read-Ahead)

首先我们来认识一下局部性原理。程序在执行时呈现出局部性规律,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。 具体可以分为以下三个部分。

  • 时间局部性原理
    被引用过一次的存储器位置,在未来很可能会再被多次引用。磁盘缓存的理论依据。

  • 空间局部性原理
    如果一个存储器位置被引用,那么将来它附近的位置也可能被引用。

  • 顺序局部性原理
    在典型程序中,除转移类指令外,大部分指令是顺序进行的。顺序执行和非顺序执行的比例大致是5:1。

尽管磁盘读写的最小单位可以精确到512B,但是操作系统却不是以512B为最小的操作单元,而是一般以4K为单位进行操作。这主要有两个原因,一是操作系统很难去管理以512B为单位的大量的地址,二是由局部性原理可知,如果读取了一个扇区,那么它临近的几个扇区的数据也极有可能需要读取,所以倒不如直接读取4K的数据更划算。

以上是操作系统通过预读对磁盘I/O的优化,实际上很多应用系统,还会进行自己的预读,比如将预读的范围扩大,每次至少读取16K,或者使用异步线程进行更大规模的预读等。我们日常刷抖音就是一个很好的例子,我们在看当前视频的时候,抖音APP实际已经预先加载了接下来的几个视频,无非这里的I/O由磁盘I/O变为了网络I/O而已。

剧透:MySQL的Innodb存储引擎就利用了预读来提高查询性能,我们将在以后的章节中介绍。

5.2、使用缓存(Using-Cache)

通常,内存读写的速度相较磁盘的速度要快很多,特别是在随机读写的情况下。因此在一定场景下,我们可以借助内存来缓冲对磁盘的直接操作,来减少直接的磁盘I/O次数,从而提高读写效率。

  • 读缓存
    我们可以通过把从磁盘读取的数据缓存在内存中,来减少再次从磁盘读取数据而带来的I/O开销,进而提高读取数据的效率,这是我们最习以为常的优化措施,即读缓存。

  • 写缓存
    实际上,我们也可以把对数据的修改进行缓存,在每次修改数据的时候,只修改磁盘原数据在内存中的拷贝,从而避免每次直接对磁盘中的原数据进行修改,进而避免每次修改数据都要去操作磁盘的I/O开销,然后通过一定的机制把内存中被多次修改后的数据同步到磁盘中,即写缓存。

剧透:MySQL的Innodb存储引擎就是通过buffer pool来实现读写缓存的。我们后续的章节会详细介绍。

5.3、预写日志(Write-Ahead-Log)

设想这样一个场景,如果磁盘中存储了大量随机的数据,应用程序每次都要修改这些数据中的部分数据,那么势必造成大量的随机I/O。如何对此进行优化呢?首先我们想到的就是上面所说的利用读写缓存,缓存对磁盘的直接I/O,这确实可以极大的提高性能,但是却引进了一个新的问题,如果还没来得及把内存中修改过的数据同步回磁盘,系统崩溃了或者断电了,那么这些还没来得及同步回磁盘的修改就丢失了。

一般地,我们选择在应用程序的一次业务逻辑结束时将内存中的数据同步到磁盘。只有同步成功了,我们才认为这次业务逻辑是成功的,否则就从磁盘重新读取数据进行下一次业务逻辑,就相当于本次业务逻辑没有发生过,这很有数据库事务的味道。的确,这既可以利用缓存对同一个数据的多次修改进行缓冲,又兼顾了数据的完整性。到这里,每次业务逻辑的I/O次数得到了减少,应用程序的性能得到了提升,但仔细分析我们会发现,将内存中的数据同步到磁盘的操作中,仍然包含了大量的随机I/O,因为要同步到磁盘的数据仍然是随机分布的。

现在的主要问题就是如何把要同步回磁盘的数据的写操作由随机I/O转化为顺序I/O。我们可以把一次业务逻辑的每一个修改操作都以日志的形式记录下来,首先记录在内存中,当一次业务逻辑结束时,一次性的将这些日志以顺序I/O的形式存入磁盘,日志写入磁盘成功,我们才认为本次业务操作成功。这就需要事先在磁盘中申请一片连续的存储空间,专门用于存储这些日志。对于缓存中被修改的数据,则选择以后台任务的方式以一定的频率同步回磁盘。当系统程序崩溃或者断电时,则用磁盘中的日志对数据进行恢复。这就是预写日志的主要原理。

image.png

剧透:MySQL的Innodb存储引擎就是通过预写日志的方式保证数据的安全性和将随机I/O转换为顺序I/O的,连同崩溃恢复的具体细节将在后续章节具体介绍

5.4、两次写(Double-Write)

前面我们主要介绍的都是针对性能的优化措施,下面我们来说一说针对数据完整性的优化策略。

磁盘原子性写入的数据大小是512B,假设现在要往磁盘写入1KB的数据,那么磁盘就需要进行两次原子性的写操作才能把1KB的数据写入磁盘,设想在写完第一个512B时,突然断电,那么第二个512B的数据就不会被写入磁盘,其结果就是磁盘中只写入了部分数据,数据是损坏的。原则上,我们更倾向于损坏的数据远比没有数据更糟糕(这里有些事务的韵味),首先损坏的数据完全没有意义,更严重的是对原来正常的数据造成了破坏,比如要修改某个数据,只修改了一半出现问题,那么只是被修改了一半的数据是没有用处的,更致命的是原来正常的数据也被破坏。这就需要寻找一种机制,杜绝损坏数据的出现。

一段数据由A、B和C三段组成,程序读取了B段数据,并对B段数据进行了修改,然而,当把修改后的B段数据写回磁盘的时候发生了断电,只成功写入了部分数据,那么此时磁盘中的数据段B是有问题,数据完整性早到了破坏,更甚至会导致由A、B和C做成的整个数据不可用。

image.png

最直接的,可以在修改数据之前,将原来的数据进行备份,只有备份成功了之后才能对数据进行修改,如果出现损坏的数据,可以用备份的数据进行还原,这确实是一种可行的方法,但是效率相对是比较低下的,首先备份需要读取原有数据,进行了一次读操作,其次把备份数据再写入到磁盘,又进行了一次写操作,而且如果出现损坏的数据,只能向后还原(无功而返)。

另一种更加高效的做法是,在修改原来的数据之前,先把本次数据修改后的数据写入磁盘,写入成功后,再去把修改同步到磁盘的原有数据。现在我们分析下这个机制是如何保证数据完整性的。

Step 1,将修改后的数据写入磁盘
Step 2,将修改同步回磁盘,即修改磁盘中的原数据

如果step 1失败,那么不会去修改磁盘中的原数据,保证了元数据的完整性。如果step 2失败,但step 1已经成功将修改后的数据写入了磁盘,此时可以通过这个数据对磁盘中的原数据进行向前恢复,最终也保证了数据的完整性。

剧透:MySQL的Innodb存储引擎中,就使用了两次写来保证数据页写入磁盘的完整性。Innodb的数据页大小是16KB,而磁盘原子性的写入值是512B,这就可能导致在往磁盘写入数据页时出现数据页损坏的情况。具体细节将在后续章节介绍。

6、常用磁盘性能指标

  • 利用率
    指磁盘处理I/O的时间百分比,过高的使用率,通常意味着磁盘I/O存在性能瓶颈,但是使用率只考虑有没有I/O,而不考虑I/O大小,当使用率达到100%的时候,磁盘依旧会接受新的I/O请求。

  • 饱和度
    指磁盘处理I/O的繁忙程度,过高的饱和度意味着磁盘存在严重的性能瓶颈,当饱和度打到100%时,则代表磁盘不会再接受新的I/O请求。

磁盘利用率其实并不是磁盘本身的属性表现,只是说明系统对磁盘的使用程度,如果系统对磁盘的使用程度很高,则有可能是因为磁盘读写延迟或排队导致,所以在一定程度上能够反应出磁盘的性能状况。磁盘饱和度则真正说明了磁盘的繁忙程度,一般饱和度很高,说明磁盘请求队列过长,若达到100%说明请求队列已满,则磁盘不能接收新的读写请求。磁盘饱和度一般不能直接通过系统监控工具获取到,我们可以通过观察磁盘利用率和磁盘队列的长度来估计,如果磁盘利用率100%且磁盘队列长度很大,那么说明磁盘的饱和度很高了。

  • IOPS
    指每秒的I/O请求次数。

  • 吞吐量
    指每秒的I/O请求大小。

大量随机I/O一般IOPS比较高,吞吐量比较低。大量顺序I/O一般IOPS一比较低,吞吐量比较大。

  • 响应时间
    指I/O请求从发出到收到响应的间隔时间。