前言
为了提升我们的软件性能,我们有多种方法,如合理的数据结构、优秀的算法,还有非常重要的一点就是:依据软件所依附的硬件自身特性,设计能最大限度发挥硬件性能的软件。根据计算机内存快但无法持久化,硬盘可以持久化但是慢的特性,我们设计了「缓存」这一策略来提升性能;根据硬盘随机读写慢但顺序读写快这一特性,Kafka 用自己独特的方式来实现高吞吐量的队列。如今,由于 SSD 出色的性能以及逐渐降低的价格,有逐步代替 HDD(传统硬盘)的趋势,而我们之前的大部分程序是基于 HDD 开发的,并不能最好的发挥 SSD 的性能,因此,我们有必要了解 SSD 的特性,以及如何写更适合于 SSD 的程序。
SSD 介绍
SSD 是用固态电子存储芯片阵列而制成的硬盘,由控制单元和存储单元(FLASH芯片、DRAM芯片)组成。SSD 的每个数据位保存在由浮栅晶体管制成的闪存单元里。SSD整个都是由电子组件制成的,没有像硬盘那样的移动或者机械的部分。SSD 的内部架构如下:
1. 使用寿命
SSD 的数据保存在浮栅晶体管中,浮栅晶体管使用电压来实现每个位的读写和擦除。写晶体管有两个方法:NOR 闪存和 NAND 闪存。目前大多数制造商都采用 NAND 闪存,NAND 闪存模块的一个重要特征是,他们的闪存单元是损耗性的,因此它们有一个寿命。衡量寿命的单位是 PE周期(program/erase cycles)。
2. 性能
下图是 SSD 与其他存储介质的性能对比:
其中 SLC、MLC、TLC 可以理解为不同类型的 SSD,下面会有介绍。
2. 存储结构
SSD 存储结构分为单元、页、块三层,下面依次介绍:
-
单元(cell):单元是 SSD 存储的最小单位,由于采用的闪存类型不同,各类闪存中的单元能存储的数据也不同,目前有三种闪存单元类型:
- 单层单元(SLC),这种的晶体管只能存储 1 个比特但寿命很长。
- 多层单元(MLC),这种的晶体管可以存储 2 个比特,但是会导致增 加延迟时间和相对于SLC减少寿命。
- 三层单元(TLC),这种的晶体管可以保存 3 个比特,但是会有更高的延迟时间和更短的寿命。
页(page):页由许多单元组成,是我们读写 SSD 的最小单位,大多数硬盘的页大小是 2KB、4KB、8 KB 或 16 KB。当我们读 SSD 的数据时,最少读取一页(就算我们只需要 1 字节的数据);当我们写数据时,就算只需要写 1 字节,也会写一页,因此存在写入放大的问题。页不能被重复写。
块(block):块由许多页组成,是数据擦除的最小单元,大多数SSD每个块有128或256个页。这即表示一个块的大小可能在 256 KB 到 4 MB 之间。之前我们说过,页不能被重复写,要修改页的数据,必须先将页所在的块擦除,然后再重新写入新值(块中其他叶需要缓存然后再写入)。
3. 数据更新
由于 SSD 不支持数据覆盖写入,因此对于数据的更新,只能将老的数据标记为过期,在另外一个空闲的地方写入更新后的值。具体操作如下图:
由上图我们可知,更新一个已有数据的流程如下:
- 将老的数据所在的页(PPN=0)标记为过期
- 在空闲页(PPN=3)写入新值 x'
- 当垃圾回收程序检测到该块(Block 1000)中存在过期数据(PPN=0),准备将其回收。而数据的擦除是以块为单位的,因此,需要先将 Block 1000 中有效的数据拷贝到另外一个空闲块(Block 2000),然后再将 Block 1000 擦除。
4. 损耗均衡
由于 SSD 存储单元的擦除次数是有限的,而擦除的最小单位是块,所以需要有一个机制来平衡各块的擦除次数,从而使整个 SSD 的各部分损耗更加均衡,这一机制称为损耗均衡。通过该技术,存储数据会在不同的块之间移动,以避免对同一块的频繁擦除。
5. 内部并行
因为物理限制的存在,异步 NAND 闪存 I/O 总线无法提供32-40 MB/s以上的带宽。由于一块 SSD 是由多个存储芯片组成的,因此我们可以通过并行读写多个存储芯片的方式来提升 SSD I/O 的性能。
SSD 内部将不同芯片中的多个块称为一个簇(clustered block),一次数据写入可以并行的写入到簇中的不同块中。如下图中,一次数据写入可以拆封成多个并行写入的任务,写入到黄色虚线框的簇中。
6. 垃圾回收
上面我们已经讲到,SSD 中的数据是不能被重复写的,必须先擦除然后再写,而擦除的速度非常慢(通常为毫秒级),SSD控制芯片会执行垃圾回收操作,即回收使用过的块,确保后续写操作能够快速分配到可用的块。
设计 SSD 友好的程序
针对上述介绍的 SSD 特性,在开发程序时我们可以做一些针对性的设计,以提高 SSD 的性能,还有延长 SSD 的使用寿命。
1. 对齐写入
SSD 数据写入的最小单元是页,因此,如果我们写入的数据小于页大小,会有两个危害:
- 空间浪费:如果想再次利用页中的空闲部分,必须擦除所处的整个块
- 读取效率低:一次读取读到的有效数据少,读相同数据需要更多次读操作
对齐写入指一次数据写入是页大小的整数倍,如果数据不够足,可以先缓冲在内存中,例如 Twitter 的 fatcache 就是凑够了1MB 才会写 SSD。
2. 相关的数据一起写
相关的数据一起写指的是对于经常被一起访问的数据,写的时候尽量一次同时写入,这样做有两个层面的好处:
- 由于读写的最小单位是页,相关的数据写在一页上,数据读的时候效率高
- 由于 SSD 存在内部并行的特性,一次将大量(簇大小的整数倍)相关的数据同时写入,内部会优化并行写入到各个存储芯片,读的时候也能并行读取,可以提升读的性能
3. 将冷热数据分开写
将冷热数据分开写主要是防止由于冷热数据范围频率不同而导致的写入放大,如果将冷热数据写一起,将会有两个层面的缺点:
- 如果冷热数据在同一个页上:数据块小于16KB或不对齐16KB时,更新热数据不得不读改写冷数据。
- 如果冷热数据在同一个块上:垃圾回收热数据时,不得不搬移并重写冷数据。
4. 缓存热数据
我们知道 SSD 对于数据更新,只能通过擦除-写入的方式,对于非常高频变化的数据,要对数据所在块进行频繁的擦除-写入,对整体性能会有较大影响。所以建议对热数据进行程序的缓存,而不是写入磁盘。
5. 读写分离
混合的小读写交叉的工作负载会妨碍内部缓存和预读取机制正常工作,并会导致吞吐量下降。最好的办法是避免同时发生的读操作和写操作,并将其以一个接一个的大数据块的形式实现,数据块大小推荐和簇大小相同。举个例子,如果要更新1000个文件,你可以遍历文件逐一读写,但会很慢。如果一次读取1000个文件然后一次写回1000个文件将会好很多。
6. 避免长时间大数据写入
SSD 内部存在异步垃圾回收进程,一般情况下不会影响正常的 I/O,但是如果长时间大数据写入,可能存在垃圾回收速度跟不上写入速度的情况,导致写入请求需要等待垃圾回收进程擦除块,影响性能。
7. 避免就地(in-place)更新优化
由于HDD在查找数据时又寻道时间,为了避免寻道产生的延迟,应用程序常常被优化成就地更新。但这个优化对于 SSD 并不适用,因为 SSD 没有寻道时间,且不支持覆盖写,更新数据必须先擦除,再写入,所以就地更新反而会造成性能下降,下图是 SSD 与 HDD 就地更新的性能对比:
8. 对于大的(大于簇的大小)读写操作,单线程比多线程好
造成这个现象的原因就是我们上面说到过的 SSD 存在内部并行的机制。
- 对于大的写入,由于内部并行机制的存在,单线程也能达到多线程的性能,并且,一个大的单线程写入比并发写入延迟时间更短。因此,在可用的时候,使用单线程大写入是最好的。
- 对于大的读取,单线程读不仅可以依靠内部并行机制达到多线程读同样的性能,而且可以更好的使用预读取机制,因此比多线程并发读更优。
9. 对于小数据的读写,多线程比单线程好
如果数据太小且没办法进行合并后读写,就无法使用其内部并行机制,因此多线程更优。
总结
本文分析了一些 SSD 的特性,以及针对这些特性,我们应该如何设计更适合于 SSD 的程序。SSD 是提升磁盘性能的利器,目前看也大有替代传统 HDD 的趋势,因此建议广大工程师都对其有所了解。当然,SSD 目前也在快速的发展之中,可能有些特性会随着发展而改变,希望大家能及时同步最新知识。