Appearance
固态硬盘与分布式数据系统
数据系统的设计始终受限于物理硬件的特性。这类系统的本质是期望提供的外部 API 与实际可用硬件资源之间的妥协。尤其令人沮丧的是,数据库或文件系统中大量代码的存在仅是为了掩盖磁盘驱动的延迟。我认为大多数人都明白固态硬盘的速度确实更快一些,但我想谈谈对于当前这批固态硬盘我们能做些什么,以及如果固态硬盘的价格和尺寸趋势继续保持下去(并非所有人都认同这一点),未来又可能会出现哪些可能性。
你需要了解的有关固态硬盘的情况:
它们消除了“查找时间”,从而使得随机读取操作的速度变得非常非常快。一款普通固态硬盘的随机读取速度可达每秒约 40,000 次,随机写入速度约为每秒 20,000 次,这大约是传统磁性硬盘预期速度的 200 倍。
线性读写吞吐量也有所提高,但提升幅度不大(约为每秒 200MB,而传统磁盘约为 50-100MB/秒)。
与传统硬盘相比,固态硬盘出现随机故障的可能性更低,因为固态硬盘并非机械装置,没有移动部件。据估计,其故障率约为传统硬盘的 20%,但这类估计通常是不准确的。
固态硬盘和硬盘一样,是按块划分的。在固态硬盘中,每个块在被写入之前只能被写入有限次数,之后就会无法使用。这个有限的写入次数被称为设备的“写入耐久性”。
固态硬盘的块大小通常为 512KB。这意味着,如果你写入 1 个字节,固态硬盘就必须擦除并重写一个完整的 512KB 块,就像你写入 512KB 一样(普通硬盘也是如此,但块大小要小得多)。这种现象被称为“写入放大”。
所以很明显,这里最大的变化在于随机请求流的延迟时间大幅降低,以及新的缺陷——有限的写入耐久性。
固态硬盘如何影响数据系统设计
系统设计的很大一部分是由不同类型的存储和网络技术的成本、延迟和吞吐量比率所决定的。这与物理定律如何限制锤子或水槽等物品的可能设计方式并无太大不同,只是在数据系统方面,物理定律会随着时间而变化。大卫·帕特森在“延迟滞后于带宽”这一演讲中对延迟和吞吐量的一些趋势进行了有趣的阐述。固态硬盘(SSD)代表了一个领域,其延迟突然大幅降低,这使得现有系统中的许多设计权衡变得无效。对于这种变化将如何影响系统设计的推测纯属猜测,但一个好的启发是假设人们最终会趋向于提供最佳性价比设计的方案。
对于分布式数据系统而言,固态硬盘带来的重大变革在于随机磁盘访问的延迟与远程网络传输的延迟之间的差异。对于传统硬盘而言,一次寻道操作的延迟成本可能比本地网络中的 TCP 请求高出 10 到 20 倍,这意味着远程缓存命中要比本地缓存未命中成本低得多。而固态硬盘则消除了这种差异,使得它们在延迟方面非常接近。其结果应该是倾向于采用每台机器存储更多数据且进行较少网络请求的设计方案。
对于数据库而言,这种极端情况就是完全放弃分区,将所有数据都存储在所有机器上。毕竟,如果写入负载较小且数据大小不会超过 1TB,那么一个不分区的、复制的 MySQL 或 PostgreSQL 就可能已经足够了。因为分区会极大地限制可以实现的查询的丰富性,所以在数据大小不会无限制增长且写入量是可以接受的情况下,不分区的设计或许是有一定道理的(因为在不分区的设计中,所有的写入操作都必须发送到所有节点)。
消除分区是减少网络跳数的一种方法。进一步遵循这一思路,可以将存储与它所服务的应用程序实际置于同一位置。这是一种彻底的架构变革,但或许有一定合理性。网络服务器通常每秒能处理约 40,000 次请求。对于一个为可能存储在传统硬盘上的磁盘驻留数据提供随机请求流服务的数据系统来说,这并非限制因素,因为每个硬盘每秒只能进行几百次随机访问。如果硬盘能够进行数十万次操作,网络请求吞吐量的限制可能会成为真正的限制因素,这时人们可能会考虑将数据和应用程序直接置于同一位置。这种做法具有意义的第二个原因是,数据系统不再是一个允许多个应用程序共享相同数据的常见集成层。当前关系型数据库管理系统的设计中隐含着这样一个事实,即多个应用程序将访问同一组表(因此需要在数据库中进行大量结构和正确性检查,以便安全地由多个应用程序共享数据,而无需在所有应用程序中验证代码)。实际上,现代应用程序之间数据共享的方式并非是直接通过访问共享数据库来实现,而是通过某种服务 API(通常为 REST)来完成的。尽管存在许多与性能无关的理由支持将数据系统保持在客户端-服务器模式(例如,允许将 CPU 密集型部分的应用程序与 I/O 密集型部分分开进行扩展),但如果性能方面的考量变得足够重要,这些理由可能就不再那么充分了。
虽然改变不会那么彻底,但固态硬盘仍可能会对缓存的使用方式产生影响。许多网络公司都安装了大量的 memcached。在处理小数据集时,memcached可以很好地提供高吞吐量和低延迟,但由于所有数据都在内存中,所以如果你受限于存储空间而非 CPU 性能的话,其实际成本会相当高。如果每台服务器配备 32GB 的缓存,那么总共 5TB 的缓存空间就需要 160 台服务器。而拥有 5 台每台配备 1TB 固态硬盘的服务器可能会是一个巨大的优势。此外,内存缓存存在一个实际问题:重启会清除整个服务器的缓存内容。如果需要频繁重启缓存服务器,或者需要在启动新的堆栈时使用完全冷启动的缓存(因为如果没有缓存,实际上可能无法运行应用程序(如果可以运行,那又何必有缓存呢?)),这会是一个令人烦恼的问题。
对于实时应用而言,固态硬盘还能实现一些访问模式,在这些模式下,在对延迟敏感的应用中,每次请求会涉及多次的寻道操作。例如,在基于磁盘驻留数据集的轻量级图遍历操作上,固态硬盘是可行的,但传统硬盘则通常无法实现。通常情况下,图数据并不适合进行干净的分区(本质上它们是相互交织的)。因此,运行在固态硬盘上的磁盘驻留块存储现在可能是一种很好的方式来实现社交图或“关注者”功能。
对于离线应用而言,延迟的降低使得随机访问再次成为可能。MapReduce 的设计初衷很大程度上是为了仅适用于线性 I/O 模式,并消除随机 I/O。这对于基于磁盘存储的数据集的批处理操作来说是一个巨大的性能提升。但要做到这一点,只需要提供一个相当有限的编程模型即可。这意味着 Hadoop 无法轻易实现类似于“哈希Join”的功能,除非哈希值能全部存放在内存中。有趣的是,如果在本地随机读写的成本很低的情况下,MapReduce 可能会有不同的设计方式。
让固态硬盘价格降低的同时又不丢失任何数据
将数据存储介质更换为固态硬盘也会对数据系统的内部结构产生影响。传统的 B+ 树或哈希结构已不再是最适合的持久数据结构。这并非是因为延迟降低,而是由于写入耐久性的问题所致。将使用传统存储引擎的数据库迁移到普通固态硬盘上可能会非常迅速,但这些固态硬盘可能在几个月后就无法正常工作了!
先介绍一下相关背景。目前固态硬盘有两种类型:企业级(SLC)和消费级(MLC)。企业级固态硬盘价格昂贵(与内存价格相当),因此对于大多数横向扩展部署来说并非可行选择。不过,对于小型部署来说,它们是个不错的选择,因为高昂的成本问题在此时不是那么重要。不同的固态硬盘在固件的复杂程度以及通过PCI总线还是SATA接口连接等方面存差异,但这些方面并不那么重要。如果你习惯了机械硬盘,那么几乎任何固态硬盘的速度都会超出你的想象。网络访问时间可能会消除不同固态硬盘类型之间任何更细微的性能差异。
例如,当我开始使用 MLC 驱动器进行存储实验时,有人警告我,消费级设备在执行各种内部压缩操作时会偶尔出现较大的延迟峰值。这是事实,99%分位数的时间确实会有偶尔出现的大峰值。尽管这些峰值在相对数值上相当巨大,但从绝对数值来看却极其微小(低于 1 毫秒)。与内存相比,这种差异非常糟糕,但与磁盘访问相比,固态硬盘的 99% 分位数则更接近硬盘未缓存的 1% 访问时间。
MLC 和 SLC 的重要区别在于它们能够处理的写入次数。我将详细介绍如何通过基本运算来模拟这一情况。固态硬盘被划分为一个个大小约为 512KB 的块,每次写入操作首先需要擦除整个 512KB 的块,然后再进行重写。每个块在被擦除之前只能被擦除一定次数,之后就会开始损坏并给出错误的数据。为了避免这种情况,固态硬盘制造商似乎会将每个块的程序擦除循环次数限制在一个固定的数值内。这意味着,在对某个特定块进行了一定次数的写入操作后,该块将不再接受新的写入操作。SLC 和 MLC 在这方面的工作方式相同,只是 SLC 在每个块上承受的写入次数大约要多一个数量级。
这里有一张表格,对比了 MLC、SLC、RAM 和 SAS 类型硬盘的价格以及每个硬盘的存储容量。这些数据是从互联网上随机收集的,您的实际体验可能会有所不同,但这张表格大致反映了截至 2012 年 5 月的市场价格情况。
存储类型 | 成本/GB | 编程-擦除次数 |
---|---|---|
RAM | $5-6 | 无限 |
15000 转/分钟的 SAS 硬盘驱动器 | $0.75 | 无限 |
MLC SSD | $1 | 5,000-10,000 |
SLC SSD | $4-6 | ~100,000 |
可以得出几个明显的结论:SLC SSD 的价格与内存大致相同。从某种意义上说,SLC SSD 比内存更出色,因为它们是持久性的,但如果将所有数据都存储在内存中听起来成本过高,那么 SLC 也会如此。而且无论如何,你都无法完全消除内存缓存,因为即使使用速度更快的 SSD,至少部分索引数据仍需要驻留在内存中。
而 MLC 存储器的价格实际上与优质硬盘的价格非常接近。虽然存在一点价格溢价,但对于大多数在线数据系统而言,这一点溢价是具有误导性的。由于大多数实时存储系统受到寻道容量的限制,而非数据大小或 CPU 的限制,增加可用的寻道次数可能会显著降低总体存储容量需求。每台机器的更多寻道次数使得每台机器能够处理更多的请求。对于我们的使用场景而言,我们发现每台机器能够轻松处理至少 5 到 10 倍更多的请求,而在此之前我们不会因磁盘空间和 CPU 的限制而遇到瓶颈。所以问题在于,是否有一种方法可以接受 MLC 设备较低的写耐久性,同时仍能获得出色的性能和成本优势?
这就是存储格式与固态硬盘之间复杂相互关系的体现。如果您的存储引擎进行的是大规模的线性写入操作(比如一个 512KB 的完整块或更大),那么计算在一块硬盘完全写满之前能够进行的写入次数就很容易了。如果该硬盘容量为 300GB,每个块可重写 5000 次,那么每块硬盘将能够支持 5000 × 300GB(约 1.4PB)的写入量。假设您在一个没有 RAID 的盒子里有 8 个这样的设备,且该盒每天 24 小时均匀地为每个硬盘提供 50MB/秒的写入量,那么这些硬盘的使用寿命约为 7.8 年。对于大多数系统来说,这应该已经足够长的时间了。但这种使用寿命仅适用于大规模的线性写入操作——这是固态硬盘写入耐久性的最佳情况。
另一种情况是,您进行的是小规模的随机写入操作,并且这些写入会立即同步到磁盘上。比如进行 100 字节的随机写入,而 SSD 内部的固件无法以某种方式将这些小块写入合并为更大的物理写入操作,那么每次 100 字节的写入就会变成一个完整的 512KB 块的程序擦除周期。在这种情况下,您预计每个块的写入量最多只能达到 5000 * 100 字节 = 500KB,之后该块就会损坏;所以一个 300GB 的驱动器,其 300GB / 512KB = 614,400 个块,总共大约只能进行 286GB 的写入操作才会报废;假设再假设有 8 个这样的驱动器,不使用 RAID 结构,且读取速度为 50MB/秒,那么其使用寿命仅为大约半天。这是 SSD 的最坏情况,显然完全不可行。
需要指出的是,只要对物理磁盘的写入操作是大规模的,那么对文件系统的写入操作大小如何并不重要。如果写入是线性的,即按照顺序写入到磁盘上,并且不会由操作系统在大量数据积累到足以满足条件时才同步到物理驱动器,那么对文件系统的多次小规模顺序写入操作将会由操作系统的 I/O 调度器合并为一次对物理设备的大型写入操作。只要没有中间调用 fsync(或等效操作),这种情况就会发生。
值得注意的是,写入文件系统的数据大小并不重要,只要对物理磁盘的写入是较大的即可。如果写入是线性的,即按照顺序写入到磁盘上,并且不是由操作系统在大量数据积累到足以满足需求时才同步到物理驱动器,那么问题就解决了。为了解决这一限制,固态硬盘试图采用某种内部预写格式,试图将随机的 I/O 转换为更大的线性写入集。然而,这种方法的效果因设备而异,我认为将数据押注于它能适用于所有工作负载这一点有点危险。同样,这还会引入一个新的读取碎片化问题,因为原本应该并行存放的更新实际上分散在不同的块中。操作系统 I/O 调度器会将对文件系统的多次小顺序写入合并为对物理设备的单个大写入,前提是没有调用 fsync(或等效操作)。
更好的选择是采用一种本身就支持线性写入的存储格式。传统的存储格式包括 B+ 树和线性哈希。这些格式是按照键将数据分组存入块中,因此写入操作会随机分布在磁盘上,除非写入顺序恰好与键的顺序一致(但除非在批量加载的情况下,否则你无法指望这一点,因为在这种情况下你可以选择更新记录的顺序)。缓冲可能会在这方面起到一定的作用,但当缓冲区被刷新时,很可能会有随机的一部分块会进行修改,且每次修改的幅度都很小。日志结构(log-structured)格式是另一种选择,它们按照数据被写入的顺序存储数据,因此总是进行线性写入。日志结构合并树和谷歌的 SSTable 变体就是这种格式的示例。各种哈希和树格式都可以以日志结构(log-structured)的方式设计。
在传统的机械硬盘中,原地存储(in-place)和日志结构存储之间的权衡关系其实并不明显。日志结构存储的写入性能要好得多,但大多数应用程序中的读取操作多于写入操作。读取性能的好坏取决于具体实现的细节(传统的日志结构合并树在读取操作中由于需要对每个未缓存的读取进行多次寻道操作,所以读取性能肯定较差,但采用哈希变体或使用布隆过滤器来避免不必要的查找的 SSTable 则无需如此)。然而,转向固态硬盘,这种状况彻底改变了。由于固态硬盘具有快速的寻道性能,按关键字分组数据就不再那么重要了。使用日志结构存储格式使得即使在高写入负载下(如果使用原地存储格式运行,这种负载会在几个月内使驱动器损坏)也能使用廉价的消费级 MLC 固态硬盘。
实现这一方案的一个关键因素在于存储引擎是否要求每次写入操作都立即将数据写入磁盘(fsync)。许多系统确实需要这样做以保证数据完整性。当然,写入磁盘(fsync)操作会需要小规模的写入操作,除非要写入的数据本身较大。在单节点系统中,避免写入磁盘(fsync)可能会导致在崩溃时丢失最后几条记录。然而,在一个设计合理的分布式系统中,这种情况通常不会发生——可以将数据复制到其他节点来代替写入磁盘操作。复制和磁盘同步有不同的故障模式(例如,在电源中断的情况下,所有节点同时失效时,复制无法提供帮助,而在磁盘损坏或整个机器死亡的情况下,写入磁盘操作(flush)通常也无济于事)。因此,在任何数据系统设计中,要确定其是否适合使用固态硬盘,需要考虑的一个重要问题是它是否要求将数据立即写入磁盘。特别是,一个良好的系统应当能够在无需等待磁盘刷新(flush)的情况下提供数据复制保障。
大约一年前,我与 LinkedIn 的Voldemort团队合作,对一些固态硬盘进行了较为详细的评估。其中一项测试是高速重放一个生产 I/O 跟踪数据,以模拟 BDB JE 上等效5 年的生产负载情况。这种磨损模型确实完全符合所设定的模式:缓冲线性写入使得能够从价格低廉的 MLC 固态硬盘中获得近十年的高写入使用量。Voldemort团队还详细记录了他们为使固态硬盘与 BDB JE 共同良好运行所做的一些工作。
如果您正在评估存储引擎,以下是对哪些引擎是采用原地更新存储格式或日志结构存储格式的简要概述。InnoDB、BDB、Toyko 和 Kyoto Cabnet 文件系统以及 MongoDB 都采用原地更新存储格式,且并不适合在高写入负载下使用廉价的固态硬盘。LevelDB、BDB-JE、Krati、Cassandra 的 SSTable 实现以及 Bitcask 都是采用日志结构存储格式。Riak 和Voldemort都支持可插拔存储引擎,并默认采用日志结构格式。Datastax 的 Cassandra 团队就如何在固态硬盘上运行 Cassandra 进行了很好的演示。
一个有趣的问题是,云托管服务提供商是否会在短期内提供配备固态硬盘的实例。由于写入耐久性问题,固态硬盘对于共享托管环境来说存在一定的局限性,因此他们可能需要采用按块擦除次数计费的方式。我听说亚马逊可能会提供这种服务,但我不清楚具体的形式、费用是多少(这是关键的细节)。