《数据密集型系统设计》记录
《数据密集型系统设计》记录 本书的电子版本链接:Vonng/ddia: 《Designing Data-Intensive Application》DDIA中文翻译 3. 存储与检索 本章围绕两大类存储引擎:日志结构(log-structured)的存储引擎,以及面向页面(page-oriented)的存储引擎(例如 B 树)。 数据库核心:数据结构 这里所使用的“日志”是一个更一般性的概念:一个支持 append-only 仅追加操作的记录序列。 为了高效查找数据库中特定键的值,需要用到索引。索引是从主要数据中衍生的额外结构,维护它会产生额外开销,特别是写入时。任何类型的索引通常都会减慢写入速度,因为每次写入数据时都需要更新索引。这是存储系统中一个重要的权衡:精心选择的索引加快了读查询的速度,但是每个索引都会拖慢写入速度。因为这个原因,数据库默认并不会索引所有的内容,而需要程序员或数据库管理员(DBA),基于对应用的典型查询模式的了解来手动选择索引。 SSTable 把键值对的序列按照键进行排序,这个格式称为排序字符串表(Sorted String Table, SSTable)。此时再要求每个键值在每个合并的段文件中出现一次。与使用散列索引的日志段相比,SSTable 有几个大的优势: 即使文件大于可用内存,合并段的操作仍然是简单高效: 一开始并排读取多个输入文件,查看每个文件中的第一个键,复制最低的键(根据排序顺序)到输出文件,不断重复此步骤,将产生一个新的合并段文件,而且它也是也按键排序的。 如果在几个输入段中出现相同的键,该怎么办?请记住,每个段都包含在一段时间内写入数据库的所有值。这意味着一个输入段中的所有值一定比另一个段中的所有值都更近(假设我们总是合并相邻的段)。当多个段包含相同的键时,我们可以保留最近段的值,并丢弃旧段中的值。 为了在文件中找到一个特定的键,你不再需要在内存中保存所有键的索引: 假设你正在内存中寻找键 handiwork,但是你不知道这个键在段文件中的确切偏移量。然而,你知道 handbag 和 handsome 的偏移,而且由于排序特性,你知道 handiwork 必须出现在这两者之间。这意味着你可以跳到 handbag 的偏移位置并从那里扫描,直到你找到 handiwork(或没找到,如果该文件中没有该键)。 由于读取请求无论如何都需要扫描所请求范围内的多个键值对,因此可以将这些记录分组为块(block),并在将其写入硬盘之前对其进行压缩。稀疏内存索引中的每个条目都指向压缩块的开始处。除了节省硬盘空间之外,压缩还可以减少对 I/O 带宽的使用。 那么,可以构建 SSTable 了,可以让存储引擎这样工作: 有新写入时,将其添加到内存中的平衡树数据结构(例如红黑树),这个内存树有时被称为内存表(memtable)。 当内存表大于某个阈值(通常为几兆字节)时,将其作为 SSTable 文件写入硬盘。这可以高效地完成,因为树已经维护了按键排序的键值对。新的 SSTable 文件将成为数据库中最新的段。当该 SSTable 被写入硬盘时,新的写入可以在一个新的内存表实例上继续进行。 收到读取请求时,首先尝试在内存表中找到对应的键,如果没有就在最近的硬盘段中寻找,如果还没有就在下一个较旧的段中继续寻找,以此类推。 后台进程周期性地执行合并和压缩过程,以合并段文件,并将已覆盖或已删除的值丢弃。 这样的索引结构被称为 LSM-Tree(Log-Structured Merge-Tree)。 当查找数据库中不存在的键时,LSM 树算法可能会很慢。为了优化这种访问,存储引擎通常会使用布隆过滤器。还可以使用大小分级和分层压缩。对于大小分级,较新和较小的 SSTables 相继被合并到较旧的和较大的 SSTable 中。对于分层压缩,键的范围被拆分到多个较小的 SSTables,而较旧的数据被移动到单独的层级,这使得压缩能够逐步进行并且使用较少的硬盘空间。 B-Tree 前面看到的日志结构索引将数据库分解为可变大小的段,通常是几兆字节或更大的大小,并且总是按顺序写入段。相比之下,B 树将数据库分解成固定大小的块或分页,传统上大小为 4KB(有时会更大),并且一次只能读取或写入一个页面。这种设计更接近于底层硬件,因为硬盘空间也是按固定大小的块来组织的。 在 B 树的底层中,写操作是用新数据覆写硬盘上的页面,并假定覆写不改变页面的位置。即当页面被覆写时,对该页面的所有引用保持完整。这与日志结构索引(如 LSM 树)形成鲜明对比,后者只追加到文件(并最终删除过时的文件),但从不修改文件中已有的内容。...