mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
first batch of translation updates
This commit is contained in:
parent
e217d118cc
commit
a40ce46024
2
ch1.md
2
ch1.md
@ -220,7 +220,7 @@
|
||||
|
||||
即使不断重复发送同样的请求,每次得到的响应时间也都会略有不同。现实世界的系统会处理各式各样的请求,响应时间可能会有很大差异。因此我们需要将响应时间视为一个可以测量的数值**分布(distribution)**,而不是单个数值。
|
||||
|
||||
在[图1-4](img/fig1-4.png)中,每个灰条表代表一次对服务的请求,其高度表示请求花费了多长时间。大多数请求是相当快的,但偶尔会出现需要更长的时间的异常值。这也许是因为缓慢的请求实质上开销更大,例如它们可能会处理更多的数据。但即使(你认为)所有请求都花费相同时间的情况下,随机的附加延迟也会导致结果变化,例如:上下文切换到后台进程,网络数据包丢失与TCP重传,垃圾收集暂停,强制从磁盘读取的页面错误,服务器机架中的震动【18】,还有很多其他原因。
|
||||
在[图1-4](img/fig1-4.png)中,每个灰条代表一次对服务的请求,其高度表示请求花费了多长时间。大多数请求是相当快的,但偶尔会出现需要更长的时间的异常值。这也许是因为缓慢的请求实质上开销更大,例如它们可能会处理更多的数据。但即使(你认为)所有请求都花费相同时间的情况下,随机的附加延迟也会导致结果变化,例如:上下文切换到后台进程,网络数据包丢失与TCP重传,垃圾收集暂停,强制从磁盘读取的页面错误,服务器机架中的震动【18】,还有很多其他原因。
|
||||
|
||||
![](img/fig1-4.png)
|
||||
|
||||
|
6
ch2.md
6
ch2.md
@ -161,7 +161,7 @@ JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的**局部
|
||||
|
||||
***推荐***
|
||||
|
||||
假设你想添加一个新的功能:一个用户可以为另一个用户写一个推荐。在用户的简历上显示推荐,并附上推荐用户的姓名和照片。如果推荐人更新他们的照片,那他们写的任何建议都需要显示新的照片。因此,推荐应该拥有作者个人简介的引用。
|
||||
假设你想添加一个新的功能:一个用户可以为另一个用户写一个推荐。在用户的简历上显示推荐,并附上推荐用户的姓名和照片。如果推荐人更新他们的照片,那他们写的任何推荐都需要显示新的照片。因此,推荐应该拥有作者个人简介的引用。
|
||||
![](img/fig2-3.png)
|
||||
|
||||
**图2-3 公司名不仅是字符串,还是一个指向公司实体的链接(LinkedIn截图)**
|
||||
@ -188,7 +188,7 @@ IMS的设计中使用了一个相当简单的数据模型,称为**层次模型
|
||||
|
||||
#### 网络模型
|
||||
|
||||
网络模型由一个称为数据系统语言会议(CODASYL)的委员会进行了标准化,并被数个不同的数据库商实现;它也被称为CODASYL模型【16】。
|
||||
网络模型由一个称为数据系统语言会议(CODASYL)的委员会进行了标准化,并被数个不同的数据库厂商实现;它也被称为CODASYL模型【16】。
|
||||
|
||||
CODASYL模型是层次模型的推广。在层次模型的树结构中,每条记录只有一个父节点;在网络模式中,每条记录可能有多个父节点。例如,“Greater Seattle Area”地区可能是一条记录,每个居住在该地区的用户都可以与之相关联。这允许对多对一和多对多的关系进行建模。
|
||||
|
||||
@ -204,7 +204,7 @@ CODASYL中的查询是通过利用遍历记录列和跟随访问路径表在数
|
||||
|
||||
相比之下,关系模型做的就是将所有的数据放在光天化日之下:一个 **关系(表)** 只是一个 **元组(行)** 的集合,仅此而已。如果你想读取数据,它没有迷宫似的嵌套结构,也没有复杂的访问路径。你可以选中符合任意条件的行,读取表中的任何或所有行。你可以通过指定某些列作为匹配关键字来读取特定行。你可以在任何表中插入一个新的行,而不必担心与其他表的外键关系[^iv]。
|
||||
|
||||
[^iv]: 外键约束允许对修改约束,但对于关系模型这并不是必选项。即使有约束,外键连接在查询时执行,而在CODASYL中,连接在插入时高效完成。
|
||||
[^iv]: 外键约束允许对修改进行限制,但对于关系模型这并不是必选项。即使有约束,外键连接在查询时执行,而在CODASYL中,连接在插入时高效完成。
|
||||
|
||||
在关系数据库中,查询优化器自动决定查询的哪些部分以哪个顺序执行,以及使用哪些索引。这些选择实际上是“访问路径”,但最大的区别在于它们是由查询优化器自动生成的,而不是由程序员生成,所以我们很少需要考虑它们。
|
||||
|
||||
|
24
ch3.md
24
ch3.md
@ -195,7 +195,7 @@ $ cat database
|
||||
|
||||
这里描述的算法本质上是LevelDB 【6】和RocksDB 【7】中使用的键值存储引擎库,被设计嵌入到其他应用程序中。除此之外,LevelDB可以在Riak中用作Bitcask的替代品。在Cassandra和HBase中使用了类似的存储引擎【8】,这两种引擎都受到了Google的Bigtable文档【9】(引入了SSTable和memtable)的启发。
|
||||
|
||||
最初这种索引结构是由Patrick O'Neil等人描述的。在日志结构合并树(或LSM树)【10】的基础上,建立在以前的工作上日志结构的文件系统【11】。基于这种合并和压缩排序文件原理的存储引擎通常被称为LSM存储引擎。
|
||||
最初这种索引结构是由Patrick O'Neil等人描述的,且被命名为日志结构合并树(或LSM树)【10】,它是基于更早之前的日志结构文件系统【11】来构建的。基于这种合并和压缩排序文件原理的存储引擎通常被称为LSM存储引擎。
|
||||
|
||||
Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使用类似的方法来存储它的词典【12,13】。全文索引比键值索引复杂得多,但是基于类似的想法:在搜索查询中给出一个单词,找到提及单词的所有文档(网页,产品描述等)。这是通过键值结构实现的,其中键是单词(**关键词(term)**),值是包含单词(文章列表)的所有文档的ID的列表。在Lucene中,从术语到发布列表的这种映射保存在SSTable类的有序文件中,根据需要在后台合并【14】。
|
||||
|
||||
@ -203,7 +203,7 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使
|
||||
|
||||
与往常一样,大量的细节使得存储引擎在实践中表现良好。例如,当查找数据库中不存在的键时,LSM树算法可能会很慢:您必须检查内存表,然后将这些段一直回到最老的(可能必须从磁盘读取每一个),然后才能确定键不存在。为了优化这种访问,存储引擎通常使用额外的Bloom过滤器【15】。 (布隆过滤器是用于近似集合内容的内存高效数据结构,它可以告诉您数据库中是否出现键,从而为不存在的键节省许多不必要的磁盘读取操作。)
|
||||
|
||||
还有不同的策略来确定SSTables如何被压缩和合并的顺序和时间。最常见的选择是大小分层压实。 LevelDB和RocksDB使用平坦压缩(LevelDB因此得名),HBase使用大小分层,Cassandra同时支持【16】。在规模级别的调整中,更新和更小的SSTables先后被合并到更老的和更大的SSTable中。在水平压实中,关键范围被拆分成更小的SSTables,而较旧的数据被移动到单独的“水平”,这使得压缩能够更加递增地进行,并且使用更少的磁盘空间。
|
||||
还有不同的策略来确定SSTables如何被压缩和合并的顺序和时间。最常见的选择是size-tiered和leveled compaction。LevelDB和RocksDB使用leveled compaction(LevelDB因此得名),HBase使用size-tiered,Cassandra同时支持【16】。对于sized-tiered,较新和较小的SSTables相继被合并到较旧的和较大的SSTable中。对于leveled compaction,key范围被拆分到较小的SSTables,而较旧的数据被移动到单独的层级(level),这使得压缩(compaction)能够更加增量地进行,并且使用较少的磁盘空间。
|
||||
|
||||
即使有许多微妙的东西,LSM树的基本思想 —— 保存一系列在后台合并的SSTables —— 简单而有效。即使数据集比可用内存大得多,它仍能继续正常工作。由于数据按排序顺序存储,因此可以高效地执行范围查询(扫描所有高于某些最小值和最高值的所有键),并且因为磁盘写入是连续的,所以LSM树可以支持非常高的写入吞吐量。
|
||||
|
||||
@ -239,7 +239,7 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使
|
||||
|
||||
**图3-7 通过分割页面来生长B树**
|
||||
|
||||
该算法确保树保持平衡:具有 n 个键的B树总是具有 $O(log n)$ 的深度。大多数数据库可以放入一个三到四层的B树,所以你不需要遵追踪多页面引用来找到你正在查找的页面。 (分支因子为 500 的 4KB 页面的四级树可以存储多达 256TB 。)
|
||||
该算法确保树保持平衡:具有 n 个键的B树总是具有 $O(log n)$ 的深度。大多数数据库可以放入一个三到四层的B树,所以你不需要追踪多个页面引用来找到你正在查找的页面。 (分支因子为 500 的 4KB 页面的四级树可以存储多达 256TB 。)
|
||||
|
||||
#### 让B树更可靠
|
||||
|
||||
@ -247,7 +247,7 @@ B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆
|
||||
|
||||
您可以考虑将硬盘上的页面覆盖为实际的硬件操作。在磁性硬盘驱动器上,这意味着将磁头移动到正确的位置,等待旋转盘上的正确位置出现,然后用新的数据覆盖适当的扇区。在固态硬盘上,由于SSD必须一次擦除和重写相当大的存储芯片块,所以会发生更复杂的事情【19】。
|
||||
|
||||
而且,一些操作需要覆盖几个不同的页面。例如,如果因为插入导致页面过度而拆分页面,则需要编写已拆分的两个页面,并覆盖其父页面以更新对两个子页面的引用。这是一个危险的操作,因为如果数据库在仅有一些页面被写入后崩溃,那么最终将导致一个损坏的索引(例如,可能有一个孤儿页面不是任何父项的子项) 。
|
||||
而且,一些操作需要覆盖几个不同的页面。例如,如果因为插入导致页面过满而拆分页面,则需要编写已拆分的两个页面,并覆盖其父页面以更新对两个子页面的引用。这是一个危险的操作,因为如果数据库在仅有一些页面被写入后崩溃,那么最终将导致一个损坏的索引(例如,可能有一个孤儿页面不是任何父项的子项) 。
|
||||
|
||||
为了使数据库对崩溃具有韧性,B树实现通常会带有一个额外的磁盘数据结构:**预写式日志(WAL, write-ahead-log)**(也称为**重做日志(redo log)**)。这是一个仅追加的文件,每个B树修改都可以应用到树本身的页面上。当数据库在崩溃后恢复时,这个日志被用来使B树恢复到一致的状态【5,20】。
|
||||
|
||||
@ -258,34 +258,34 @@ B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆
|
||||
由于B树已经存在了这么久,许多优化已经发展了多年,这并不奇怪。仅举几例:
|
||||
|
||||
* 一些数据库(如LMDB)使用写时复制方案【21】,而不是覆盖页面并维护WAL进行崩溃恢复。修改的页面被写入到不同的位置,并且树中的父页面的新版本被创建,指向新的位置。这种方法对于并发控制也很有用,我们将在“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”中看到。
|
||||
* 我们可以通过不存储整个键来节省页面空间,但可以缩小它的大小。特别是在树内部的页面上,键只需要提供足够的信息来充当键范围之间的边界。在页面中包含更多的键允许树具有更高的分支因子,因此更少的层次
|
||||
* 通常,页面可以放置在磁盘上的任何位置;没有什么要求附近的键放在页面附近的磁盘上。如果查询需要按照排序顺序扫描大部分关键字范围,那么每个页面的布局可能会非常不方便,因为每个读取的页面都可能需要磁盘查找。因此,许多B树实现尝试布局树,使得叶子页面按顺序出现在磁盘上。但是,随着树的增长,维持这个顺序是很困难的。相比之下,由于LSM树在合并过程中一次又一次地重写存储的大部分,所以它们更容易使顺序键在磁盘上彼此靠近。
|
||||
* 我们可以通过不存储整个键,而是缩短其大小,来节省页面空间。特别是在树内部的页面上,键只需要提供足够的信息来充当键范围之间的边界。在页面中包含更多的键允许树具有更高的分支因子,因此更少的层次。
|
||||
* 通常,页面可以放置在磁盘上的任何位置;没有什么要求附近的键放在页面附近的磁盘上。如果查询需要按照排序顺序扫描大部分关键字范围,那么这种按页面存储的布局可能会效率低下,因为每个读取的页面都可能需要磁盘寻道。因此,许多B树实现尝试布局树,使得叶子页面按顺序出现在磁盘上。但是,随着树的增长,维持这个顺序是很困难的。相比之下,由于LSM树在合并过程中一次又一次地重写存储的大部分,所以它们更容易使顺序键在磁盘上彼此靠近。
|
||||
* 额外的指针已添加到树中。例如,每个叶子页面可以在左边和右边具有对其兄弟页面的引用,这允许不跳回父页面就能顺序扫描。
|
||||
* B树的变体如分形树【22】借用一些日志结构的思想来减少磁盘寻道(而且它们与分形无关)。
|
||||
|
||||
### 比较B树和LSM树
|
||||
|
||||
尽管B树实现通常比LSM树实现更成熟,但LSM树由于其性能特点也非常有趣。根据经验,通常LSM树的写入速度更快,而B树的读取速度更快【23】。 LSM树上的读取通常比较慢,因为它们必须在压缩的不同阶段检查几个不同的数据结构和SSTables。
|
||||
尽管B树实现通常比LSM树实现更成熟,但LSM树由于其性能特点也非常有趣。根据经验,通常LSM树的写入速度更快,而B树的读取速度更快【23】。 LSM树上的读取通常比较慢,因为它们必须检查几种不同的数据结构和不同压缩(Compaction)层级的SSTables。
|
||||
|
||||
然而,基准通常对工作量的细节不确定且敏感。 您需要测试具有特定工作负载的系统,以便进行有效的比较。 在本节中,我们将简要讨论一些在衡量存储引擎性能时值得考虑的事情。
|
||||
然而,基准测试通常对工作负载的细节不确定且敏感。 您需要测试具有特定工作负载的系统,以便进行有效的比较。在本节中,我们将简要讨论一些在衡量存储引擎性能时值得考虑的事情。
|
||||
|
||||
#### LSM树的优点
|
||||
|
||||
B树索引必须至少两次写入每一段数据:一次写入预先写入日志,一次写入树页面本身(也许再次分页)。即使在该页面中只有几个字节发生了变化,也需要一次编写整个页面的开销。有些存储引擎甚至会覆盖同一个页面两次,以免在电源故障的情况下导致页面部分更新【24,25】。
|
||||
|
||||
由于反复压缩和合并SSTables,日志结构索引也会重写数据。这种影响 —— 在数据库的生命周期中写入数据库导致对磁盘的多次写入 —— 被称为**写放大(write amplification)**。需要特别注意的是固态硬盘,固态硬盘的闪存寿命在覆写有限次数后就会耗尽。
|
||||
由于反复压缩和合并SSTables,日志结构索引也会重写数据。这种影响 —— 在数据库的生命周期中每次写入数据库导致对磁盘的多次写入 —— 被称为**写放大(write amplification)**。需要特别注意的是固态硬盘,固态硬盘的闪存寿命在覆写有限次数后就会耗尽。
|
||||
|
||||
在写入繁重的应用程序中,性能瓶颈可能是数据库可以写入磁盘的速度。在这种情况下,写放大会导致直接的性能代价:存储引擎写入磁盘的次数越多,可用磁盘带宽内的每秒写入次数越少。
|
||||
|
||||
而且,LSM树通常能够比B树支持更高的写入吞吐量,部分原因是它们有时具有较低的写放大(尽管这取决于存储引擎配置和工作负载),部分是因为它们顺序地写入紧凑的SSTable文件而不是必须覆盖树中的几个页面【26】。这种差异在磁性硬盘驱动器上尤其重要,顺序写入比随机写入快得多。
|
||||
|
||||
LSM树可以被压缩得更好,因此经常比B树在磁盘上产生更小的文件。 B树存储引擎会由于分割而留下一些未使用的磁盘空间:当页面被拆分或某行不能放入现有页面时,页面中的某些空间仍未被使用。由于LSM树不是面向页面的,并且定期重写SSTables以去除碎片,所以它们具有较低的存储开销,特别是当使用平坦压缩时【27】。
|
||||
LSM树可以被压缩得更好,因此经常比B树在磁盘上产生更小的文件。 B树存储引擎会由于分割而留下一些未使用的磁盘空间:当页面被拆分或某行不能放入现有页面时,页面中的某些空间仍未被使用。由于LSM树不是面向页面的,并且定期重写SSTables以去除碎片,所以它们具有较低的存储开销,特别是当使用分层压缩(leveled compaction)时【27】。
|
||||
|
||||
在许多固态硬盘上,固件内部使用日志结构化算法,将随机写入转变为顺序写入底层存储芯片,因此存储引擎写入模式的影响不太明显【19】。但是,较低的写入放大率和减少的碎片对SSD仍然有利:更紧凑地表示数据可在可用的I/O带宽内提供更多的读取和写入请求。
|
||||
|
||||
#### LSM树的缺点
|
||||
|
||||
日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试逐步执行压缩而不影响并发访问,但是磁盘资源有限,所以很容易发生请求需要等待而磁盘完成昂贵的压缩操作。对吞吐量和平均响应时间的影响通常很小,但是在更高百分比的情况下(参阅“[描述性能](ch1.md#描述性能)”),对日志结构化存储引擎的查询响应时间有时会相当长,而B树的行为则相对更具可预测性【28】。
|
||||
日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试逐步执行压缩而不影响并发访问,但是磁盘资源有限,所以很容易发生请求需要等待磁盘完成昂贵的压缩操作。对吞吐量和平均响应时间的影响通常很小,但是在更高百分比的情况下(参阅“[描述性能](ch1.md#描述性能)”),对日志结构化存储引擎的查询响应时间有时会相当长,而B树的行为则相对更具可预测性【28】。
|
||||
|
||||
压缩的另一个问题出现在高写入吞吐量:磁盘的有限写入带宽需要在初始写入(记录和刷新内存表到磁盘)和在后台运行的压缩线程之间共享。写入空数据库时,可以使用全磁盘带宽进行初始写入,但数据库越大,压缩所需的磁盘带宽就越多。
|
||||
|
||||
@ -305,7 +305,7 @@ B树在数据库体系结构中是非常根深蒂固的,为许多工作负载
|
||||
|
||||
#### 将值存储在索引中
|
||||
|
||||
索引中的关键字是查询搜索的内容,但是该值可以是以下两种情况之一:它可以是所讨论的实际行(文档,顶点),也可以是对存储在别处的行的引用。在后一种情况下,行被存储的地方被称为**堆文件(heap file)**,并且存储的数据没有特定的顺序(它可以是仅附加的,或者可以跟踪被删除的行以便用新数据覆盖它们后来)。堆文件方法很常见,因为它避免了在存在多个二级索引时复制数据:每个索引只引用堆文件中的一个位置,实际的数据保存在一个地方。
|
||||
索引中的键是查询搜索的内容,而其值可以是以下两种情况之一:它可以是所讨论的实际行(文档,顶点),也可以是对存储在别处的行的引用。在后一种情况下,行被存储的地方被称为**堆文件(heap file)**,并且存储的数据没有特定的顺序(它可以是仅追加的,或者可以跟踪被删除的行以便用新数据覆盖它们后来)。堆文件方法很常见,因为它避免了在存在多个二级索引时复制数据:每个索引只引用堆文件中的一个位置,实际的数据保存在一个地方。
|
||||
在不更改键的情况下更新值时,堆文件方法可以非常高效:只要新值不大于旧值,就可以覆盖该记录。如果新值更大,情况会更复杂,因为它可能需要移到堆中有足够空间的新位置。在这种情况下,要么所有的索引都需要更新,以指向记录的新堆位置,或者在旧堆位置留下一个转发指针【5】。
|
||||
|
||||
在某些情况下,从索引到堆文件的额外跳跃对读取来说性能损失太大,因此可能希望将索引行直接存储在索引中。这被称为聚集索引。例如,在MySQL的InnoDB存储引擎中,表的主键总是一个聚簇索引,二级索引用主键(而不是堆文件中的位置)【31】。在SQL Server中,可以为每个表指定一个聚簇索引【32】。
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
幸运的是,在技术迅速变化的背后总是存在一些持续成立的原则,无论您使用了特定工具的哪个版本。如果您理解了这些原则,就可以领会这些工具的适用场景,如何充分利用它们,以及如何避免其中的陷阱。这正是本书的初衷。
|
||||
|
||||
本书的目标是帮助您在飞速变化的数据处理和数据存储技术大观园中找到方向。本书并不是某个特定工具的教程,也不是一本充满枯燥理论的教科书。相反,我们将看到一些成功数据系统的样例:许多流行应用每天都要在生产中会满足可伸缩性、性能、以及可靠性的要求,而这些技术构成了这些应用的基础。
|
||||
本书的目标是帮助您在飞速变化的数据处理和数据存储技术大观园中找到方向。本书并不是某个特定工具的教程,也不是一本充满枯燥理论的教科书。相反,我们将看到一些成功数据系统的样例:许多流行应用每天都要在生产中满足可伸缩性、性能、以及可靠性的要求,而这些技术构成了这些应用的基础。
|
||||
|
||||
我们将深入这些系统的内部,理清它们的关键算法,讨论背后的原则和它们必须做出的权衡。在这个过程中,我们将尝试寻找**思考**数据系统的有效方式 —— 不仅关于它们**如何**工作,还包括它们**为什么**以这种方式工作,以及哪些问题是我们需要问的。
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user