mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
fix typo
This commit is contained in:
parent
03cdebe141
commit
726784be63
18
ch1.md
18
ch1.md
@ -46,7 +46,7 @@
|
||||
|
||||
**图1-1 一个可能的组合使用多个组件的数据系统架构**
|
||||
|
||||
当你将多个工具组合在一起提供服务时,服务的接口或**应用程序编程接口(API, Application Programming Interface)**通常向客户端隐藏这些实现细节。现在,你基本上已经使用较小的通用组件创建了一个全新的、专用的数据系统。这个新的复合数据系统可能会提供特定的保证,例如:缓存在写入时会作废或更新,以便外部客户端获取一致的结果。现在你不仅是应用程序开发人员,还是数据系统设计人员了。
|
||||
当你将多个工具组合在一起提供服务时,服务的接口或**应用程序编程接口(API, Application Programming Interface)** 通常向客户端隐藏这些实现细节。现在,你基本上已经使用较小的通用组件创建了一个全新的、专用的数据系统。这个新的复合数据系统可能会提供特定的保证,例如:缓存在写入时会作废或更新,以便外部客户端获取一致的结果。现在你不仅是应用程序开发人员,还是数据系统设计人员了。
|
||||
|
||||
设计数据系统或服务时可能会遇到很多棘手的问题,例如:当系统出问题时,如何确保数据的正确性和完整性?当部分系统退化降级时,如何为客户提供始终如一的良好性能?当负载增加时,如何扩容应对?什么样的API才是好的API?
|
||||
|
||||
@ -121,7 +121,7 @@
|
||||
|
||||
导致这类软件故障的BUG通常会潜伏很长时间,直到被异常情况触发为止。这种情况意味着软件对其环境做出了某种假设——虽然这种假设通常来说是正确的,但由于某种原因最后不再成立了【11】。
|
||||
|
||||
虽然软件中的系统性故障没有速效药,但我们还是有很多小办法,例如:仔细考虑系统中的假设和交互;彻底的测试;进程隔离;允许进程崩溃并重启;测量、监控并分析生产环境中的系统行为。如果系统能够提供一些保证(例如在一个消息队列中,进入与发出的消息数量相等),那么系统就可以在运行时不断自检,并在出现**差异(discrepancy)**时报警【12】。
|
||||
虽然软件中的系统性故障没有速效药,但我们还是有很多小办法,例如:仔细考虑系统中的假设和交互;彻底的测试;进程隔离;允许进程崩溃并重启;测量、监控并分析生产环境中的系统行为。如果系统能够提供一些保证(例如在一个消息队列中,进入与发出的消息数量相等),那么系统就可以在运行时不断自检,并在出现**差异(discrepancy)** 时报警【12】。
|
||||
|
||||
### 人为错误
|
||||
|
||||
@ -228,7 +228,7 @@
|
||||
|
||||
通常报表都会展示服务的平均响应时间。 (严格来讲“平均”一词并不指代任何特定公式,但实际上它通常被理解为**算术平均值(arithmetic mean)**:给定 n 个值,加起来除以 n )。然而如果你想知道“**典型(typical)**”响应时间,那么平均值并不是一个非常好的指标,因为它不能告诉你有多少用户实际上经历了这个延迟。
|
||||
|
||||
通常使用**百分位点(percentiles)**会更好。如果将响应时间列表按最快到最慢排序,那么**中位数(median)**就在正中间:举个例子,如果你的响应时间中位数是200毫秒,这意味着一半请求的返回时间少于200毫秒,另一半比这个要长。
|
||||
通常使用**百分位点(percentiles)** 会更好。如果将响应时间列表按最快到最慢排序,那么**中位数(median)** 就在正中间:举个例子,如果你的响应时间中位数是200毫秒,这意味着一半请求的返回时间少于200毫秒,另一半比这个要长。
|
||||
|
||||
如果想知道典型场景下用户需要等待多长时间,那么中位数是一个好的度量标准:一半用户请求的响应时间少于响应时间的中位数,另一半服务时间比中位数长。中位数也被称为第50百分位点,有时缩写为p50。注意中位数是关于单个请求的;如果用户同时发出几个请求(在一个会话过程中,或者由于一个页面中包含了多个资源),则至少一个请求比中位数慢的概率远大于50%。
|
||||
|
||||
@ -266,7 +266,7 @@
|
||||
|
||||
有些系统是 **弹性(elastic)** 的,这意味着可以在检测到负载增加时自动增加计算资源,而其他系统则是手动扩展(人工分析容量并决定向系统添加更多的机器)。如果负载**极难预测(highly unpredictable)**,则弹性系统可能很有用,但手动扩展系统更简单,并且意外操作可能会更少(参阅“[重新平衡分区](ch6.md#分区再平衡)”)。
|
||||
|
||||
跨多台机器部署**无状态服务(stateless services)**非常简单,但将带状态的数据系统从单节点变为分布式配置则可能引入许多额外复杂度。出于这个原因,常识告诉我们应该将数据库放在单个节点上(纵向扩展),直到扩展成本或可用性需求迫使其改为分布式。
|
||||
跨多台机器部署**无状态服务(stateless services)** 非常简单,但将带状态的数据系统从单节点变为分布式配置则可能引入许多额外复杂度。出于这个原因,常识告诉我们应该将数据库放在单个节点上(纵向扩展),直到扩展成本或可用性需求迫使其改为分布式。
|
||||
|
||||
随着分布式系统的工具和抽象越来越好,至少对于某些类型的应用而言,这种常识可能会改变。可以预见分布式数据系统将成为未来的默认设置,即使对不处理大量数据或流量的场景也如此。本书的其余部分将介绍多种分布式数据系统,不仅讨论它们在可扩展性方面的表现,还包括易用性和可维护性。
|
||||
|
||||
@ -274,7 +274,7 @@
|
||||
|
||||
举个例子,用于处理每秒十万个请求(每个大小为1 kB)的系统与用于处理每分钟3个请求(每个大小为2GB)的系统看上去会非常不一样,尽管两个系统有同样的数据吞吐量。
|
||||
|
||||
一个良好适配应用的可扩展架构,是围绕着**假设(assumption)**建立的:哪些操作是常见的?哪些操作是罕见的?这就是所谓负载参数。如果假设最终是错误的,那么为扩展所做的工程投入就白费了,最糟糕的是适得其反。在早期创业公司或非正式产品中,通常支持产品快速迭代的能力,要比可扩展至未来的假想负载要重要的多。
|
||||
一个良好适配应用的可扩展架构,是围绕着**假设(assumption)** 建立的:哪些操作是常见的?哪些操作是罕见的?这就是所谓负载参数。如果假设最终是错误的,那么为扩展所做的工程投入就白费了,最糟糕的是适得其反。在早期创业公司或非正式产品中,通常支持产品快速迭代的能力,要比可扩展至未来的假想负载要重要的多。
|
||||
|
||||
尽管这些架构是应用程序特定的,但可扩展的架构通常也是从通用的积木块搭建而成的,并以常见的模式排列。在本书中,我们将讨论这些构件和模式。
|
||||
|
||||
@ -284,7 +284,7 @@
|
||||
|
||||
众所周知,软件的大部分开销并不在最初的开发阶段,而是在持续的维护阶段,包括修复漏洞、保持系统正常运行、调查失效、适配新的平台、为新的场景进行修改、偿还技术债、添加新的功能等等。
|
||||
|
||||
不幸的是,许多从事软件系统行业的人不喜欢维护所谓的**遗留(legacy)**系统,——也许因为涉及修复其他人的错误、和过时的平台打交道,或者系统被迫使用于一些份外工作。每一个遗留系统都以自己的方式让人不爽,所以很难给出一个通用的建议来和它们打交道。
|
||||
不幸的是,许多从事软件系统行业的人不喜欢维护所谓的**遗留(legacy)** 系统,——也许因为涉及修复其他人的错误、和过时的平台打交道,或者系统被迫使用于一些份外工作。每一个遗留系统都以自己的方式让人不爽,所以很难给出一个通用的建议来和它们打交道。
|
||||
|
||||
但是我们可以,也应该以这样一种方式来设计软件:在设计之初就尽量考虑尽可能减少维护期间的痛苦,从而避免自己的软件系统变成遗留系统。为此,我们将特别关注软件系统的三个设计原则:
|
||||
|
||||
@ -339,11 +339,11 @@
|
||||
|
||||
因为复杂度导致维护困难时,预算和时间安排通常会超支。在复杂的软件中进行变更,引入错误的风险也更大:当开发人员难以理解系统时,隐藏的假设、无意的后果和意外的交互就更容易被忽略。相反,降低复杂度能极大地提高软件的可维护性,因此简单性应该是构建系统的一个关键目标。
|
||||
|
||||
简化系统并不一定意味着减少功能;它也可以意味着消除**额外的(accidental)**的复杂度。 Moseley和Marks【32】把 **额外复杂度** 定义为:由具体实现中涌现,而非(从用户视角看,系统所解决的)问题本身固有的复杂度。
|
||||
简化系统并不一定意味着减少功能;它也可以意味着消除**额外的(accidental)** 的复杂度。 Moseley和Marks【32】把 **额外复杂度** 定义为:由具体实现中涌现,而非(从用户视角看,系统所解决的)问题本身固有的复杂度。
|
||||
|
||||
用于消除**额外复杂度**的最好工具之一是**抽象(abstraction)**。一个好的抽象可以将大量实现细节隐藏在一个干净,简单易懂的外观下面。一个好的抽象也可以广泛用于各类不同应用。比起重复造很多轮子,重用抽象不仅更有效率,而且有助于开发高质量的软件。抽象组件的质量改进将使所有使用它的应用受益。
|
||||
用于消除**额外复杂度** 的最好工具之一是**抽象(abstraction)**。一个好的抽象可以将大量实现细节隐藏在一个干净,简单易懂的外观下面。一个好的抽象也可以广泛用于各类不同应用。比起重复造很多轮子,重用抽象不仅更有效率,而且有助于开发高质量的软件。抽象组件的质量改进将使所有使用它的应用受益。
|
||||
|
||||
例如,高级编程语言是一种抽象,隐藏了机器码、CPU寄存器和系统调用。 SQL也是一种抽象,隐藏了复杂的磁盘/内存数据结构、来自其他客户端的并发请求、崩溃后的不一致性。当然在用高级语言编程时,我们仍然用到了机器码;只不过没有**直接(directly)**使用罢了,正是因为编程语言的抽象,我们才不必去考虑这些实现细节。
|
||||
例如,高级编程语言是一种抽象,隐藏了机器码、CPU寄存器和系统调用。 SQL也是一种抽象,隐藏了复杂的磁盘/内存数据结构、来自其他客户端的并发请求、崩溃后的不一致性。当然在用高级语言编程时,我们仍然用到了机器码;只不过没有**直接(directly)** 使用罢了,正是因为编程语言的抽象,我们才不必去考虑这些实现细节。
|
||||
|
||||
抽象可以帮助我们将系统的复杂度控制在可管理的水平,不过,找到好的抽象是非常困难的。在分布式系统领域虽然有许多好的算法,但我们并不清楚它们应该打包成什么样抽象。
|
||||
|
||||
|
8
ch2.md
8
ch2.md
@ -531,7 +531,7 @@ db.observations.aggregate([
|
||||
|
||||
可以将那些众所周知的算法运用到这些图上:例如,汽车导航系统搜索道路网络中两点之间的最短路径,PageRank可以用在网络图上来确定网页的流行程度,从而确定该网页在搜索结果中的排名。
|
||||
|
||||
在刚刚给出的例子中,图中的所有顶点代表了相同类型的事物(人,网页或交叉路口)。不过,图并不局限于这样的同类数据:同样强大地是,图提供了一种一致的方式,用来在单个数据存储中存储完全不同类型的对象。例如,Facebook维护一个包含许多不同类型的顶点和边的单个图:顶点表示人,地点,事件,签到和用户的评论;边缘表示哪些人是彼此的朋友,哪个签到发生在何处,谁评论了哪条消息,谁参与了哪个事件,等等【35】。
|
||||
在刚刚给出的例子中,图中的所有顶点代表了相同类型的事物(人,网页或交叉路口)。不过,图并不局限于这样的同类数据:同样强大地是,图提供了一种一致的方式,用来在单个数据存储中存储完全不同类型的对象。例如,Facebook维护一个包含许多不同类型的顶点和边的单个图:顶点表示人,地点,事件,签到和用户的评论;边缘表示哪些人是彼此的朋友,哪个签到发生在何处,谁评论了哪条消息,谁参与了哪个事件,等等【35】。
|
||||
|
||||
在本节中,我们将使用[图2-5](img/fig2-5.png)所示的示例。它可以从社交网络或系谱数据库中获得:它显示了两个人,来自爱达荷州的Lucy和来自法国Beaune的Alain。他们已婚,住在伦敦。
|
||||
|
||||
@ -543,7 +543,7 @@ db.observations.aggregate([
|
||||
|
||||
### 属性图
|
||||
|
||||
在属性图模型中,每个**顶点(vertex)**包括:
|
||||
在属性图模型中,每个**顶点(vertex)** 包括:
|
||||
|
||||
* 唯一的标识符
|
||||
* 一组 **出边(outgoing edges)**
|
||||
@ -558,7 +558,7 @@ db.observations.aggregate([
|
||||
* 描述两个顶点之间关系类型的标签
|
||||
* 一组属性(键值对)
|
||||
|
||||
可以将图存储看作由两个关系表组成:一个存储顶点,另一个存储边,如[例2-2]()所示(该模式使用PostgreSQL json数据类型来存储每个顶点或每条边的属性)。头部和尾部顶点用来存储每条边;如果你想要一组顶点的输入或输出边,你可以分别通过`head_vertex`或`tail_vertex`来查询`edges`表。
|
||||
可以将图存储看作由两个关系表组成:一个存储顶点,另一个存储边,如[例2-2]()所示(该模式使用PostgreSQL JSON数据类型来存储每个顶点或每条边的属性)。头部和尾部顶点用来存储每条边;如果你想要一组顶点的输入或输出边,你可以分别通过`head_vertex`或`tail_vertex`来查询`edges`表。
|
||||
|
||||
**例2-2 使用关系模式来表示属性图**
|
||||
|
||||
@ -926,7 +926,7 @@ Cypher和SPARQL使用SELECT立即跳转,但是Datalog一次只进行一小步
|
||||
虽然我们已经覆盖了很多层面,但仍然有许多数据模型没有提到。举几个简单的例子:
|
||||
|
||||
* 使用基因组数据的研究人员通常需要执行**序列相似性搜索**,这意味着需要一个很长的字符串(代表一个DNA分子),并在一个拥有类似但不完全相同的字符串的大型数据库中寻找匹配。这里所描述的数据库都不能处理这种用法,这就是为什么研究人员编写了像GenBank这样的专门的基因组数据库软件的原因【48】。
|
||||
* 粒子物理学家数十年来一直在进行大数据类型的大规模数据分析,像大型强子对撞机(LHC)这样的项目现在可以工作在数百亿兆字节的范围内!在这样的规模下,需要定制解决方案来阻住硬件成本的失控【49】。
|
||||
* 粒子物理学家数十年来一直在进行大数据类型的大规模数据分析,像大型强子对撞机(LHC)这样的项目现在可以工作在数百亿兆字节的范围内!在这样的规模下,需要定制解决方案来阻止硬件成本的失控【49】。
|
||||
* **全文搜索**可以说是一种经常与数据库一起使用的数据模型。信息检索是一个很大的专业课题,我们不会在本书中详细介绍,但是我们将在第三章和第三章中介绍搜索索引。
|
||||
|
||||
让我们暂时将其放在一边。在[下一章](ch3.md)中,我们将讨论在**实现**本章描述的数据模型时会遇到的一些权衡。
|
||||
|
12
ch3.md
12
ch3.md
@ -71,7 +71,7 @@ $ cat database
|
||||
|
||||
为了高效查找数据库中特定键的值,我们需要一个数据结构:**索引(index)**。本章将介绍一系列的索引结构,并它们进行对比。索引背后的大致思想是,保存一些额外的元数据作为路标,帮助你找到想要的数据。如果您想在同一份数据中以几种不同的方式进行搜索,那么你也许需要不同的索引,建在数据的不同部分上。
|
||||
|
||||
索引是从主数据衍生的**附加(additional)**结构。许多数据库允许添加与删除索引,这不会影响数据的内容,它只影响查询的性能。维护额外的结构会产生开销,特别是在写入时。写入性能很难超过简单地追加写入文件,因为追加写入是最简单的写入操作。任何类型的索引通常都会减慢写入速度,因为每次写入数据时都需要更新索引。
|
||||
索引是从主数据衍生的**附加(additional)** 结构。许多数据库允许添加与删除索引,这不会影响数据的内容,它只影响查询的性能。维护额外的结构会产生开销,特别是在写入时。写入性能很难超过简单地追加写入文件,因为追加写入是最简单的写入操作。任何类型的索引通常都会减慢写入速度,因为每次写入数据时都需要更新索引。
|
||||
|
||||
这是存储系统中一个重要的权衡:精心选择的索引加快了读查询的速度,但是每个索引都会拖慢写入速度。因为这个原因,数据库默认并不会索引所有的内容,而需要你(程序员或DBA)通过对应用查询模式的了解来手动选择索引。你可以选择能为应用带来最大收益,同时又不会引入超出必要开销的索引。
|
||||
|
||||
@ -105,7 +105,7 @@ $ cat database
|
||||
|
||||
**图3-3 同时执行压缩和分段合并**
|
||||
|
||||
每个段现在都有自己的内存散列表,将键映射到文件偏移量。为了找到一个键的值,我们首先检查最近段的哈希映射;如果键不存在,我们检查第二个最近的段,依此类推。合并过程保持细分的数量,所以查找不需要检查许多哈希映射。
|
||||
每个段现在都有自己的内存散列表,将键映射到文件偏移量。为了找到一个键的值,我们首先检查最近段的哈希映射;如果键不存在,我们检查第二个最近的段,依此类推。合并过程保持细分的数量,所以查找不需要检查许多哈希映射。
|
||||
大量的细节进入实践这个简单的想法工作。简而言之,一些真正实施中重要的问题是:
|
||||
|
||||
***文件格式***
|
||||
@ -201,7 +201,7 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使
|
||||
|
||||
#### 性能优化
|
||||
|
||||
与往常一样,大量的细节使得存储引擎在实践中表现良好。例如,当查找数据库中不存在的键时,LSM树算法可能会很慢:您必须检查内存表,然后将这些段一直回到最老的(可能必须从磁盘读取每一个),然后才能确定键不存在。为了优化这种访问,存储引擎通常使用额外的Bloom过滤器【15】。 (布隆过滤器是用于近似集合内容的内存高效数据结构,它可以告诉您数据库中是否出现键,从而为不存在的键节省许多不必要的磁盘读取操作。
|
||||
与往常一样,大量的细节使得存储引擎在实践中表现良好。例如,当查找数据库中不存在的键时,LSM树算法可能会很慢:您必须检查内存表,然后将这些段一直回到最老的(可能必须从磁盘读取每一个),然后才能确定键不存在。为了优化这种访问,存储引擎通常使用额外的Bloom过滤器【15】。 (布隆过滤器是用于近似集合内容的内存高效数据结构,它可以告诉您数据库中是否出现键,从而为不存在的键节省许多不必要的磁盘读取操作。)
|
||||
|
||||
还有不同的策略来确定SSTables如何被压缩和合并的顺序和时间。最常见的选择是大小分层压实。 LevelDB和RocksDB使用平坦压缩(LevelDB因此得名),HBase使用大小分层,Cassandra同时支持【16】。在规模级别的调整中,更新和更小的SSTables先后被合并到更老的和更大的SSTable中。在水平压实中,关键范围被拆分成更小的SSTables,而较旧的数据被移动到单独的“水平”,这使得压缩能够更加递增地进行,并且使用更少的磁盘空间。
|
||||
|
||||
@ -243,7 +243,7 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使
|
||||
|
||||
#### 让B树更可靠
|
||||
|
||||
B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆盖不改变页面的位置;即,当页面被覆盖时,对该页面的所有引用保持完整。这与日志结构索引(如LSM树)形成鲜明对比,后者只附加到文件(并最终删除过时的文件),但从不修改文件。
|
||||
B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆盖不改变页面的位置:即,当页面被覆盖时,对该页面的所有引用保持完整。这与日志结构索引(如LSM树)形成鲜明对比,后者只附加到文件(并最终删除过时的文件),但从不修改文件。
|
||||
|
||||
您可以考虑将硬盘上的页面覆盖为实际的硬件操作。在磁性硬盘驱动器上,这意味着将磁头移动到正确的位置,等待旋转盘上的正确位置出现,然后用新的数据覆盖适当的扇区。在固态硬盘上,由于SSD必须一次擦除和重写相当大的存储芯片块,所以会发生更复杂的事情【19】。
|
||||
|
||||
@ -259,7 +259,7 @@ B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆
|
||||
|
||||
* 一些数据库(如LMDB)使用写时复制方案【21】,而不是覆盖页面并维护WAL进行崩溃恢复。修改的页面被写入到不同的位置,并且树中的父页面的新版本被创建,指向新的位置。这种方法对于并发控制也很有用,我们将在“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”中看到。
|
||||
* 我们可以通过不存储整个键来节省页面空间,但可以缩小它的大小。特别是在树内部的页面上,键只需要提供足够的信息来充当键范围之间的边界。在页面中包含更多的键允许树具有更高的分支因子,因此更少的层次
|
||||
* 通常,页面可以放置在磁盘上的任何位置;没有什么要求附近的键范围页面附近的磁盘上。如果查询需要按照排序顺序扫描大部分关键字范围,那么每个页面的布局可能会非常不方便,因为每个读取的页面都可能需要磁盘查找。因此,许多B树实现尝试布局树,使得叶子页面按顺序出现在磁盘上。但是,随着树的增长,维持这个顺序是很困难的。相比之下,由于LSM树在合并过程中一次又一次地重写存储的大部分,所以它们更容易使顺序键在磁盘上彼此靠近。
|
||||
* 通常,页面可以放置在磁盘上的任何位置;没有什么要求附近的键放在页面附近的磁盘上。如果查询需要按照排序顺序扫描大部分关键字范围,那么每个页面的布局可能会非常不方便,因为每个读取的页面都可能需要磁盘查找。因此,许多B树实现尝试布局树,使得叶子页面按顺序出现在磁盘上。但是,随着树的增长,维持这个顺序是很困难的。相比之下,由于LSM树在合并过程中一次又一次地重写存储的大部分,所以它们更容易使顺序键在磁盘上彼此靠近。
|
||||
* 额外的指针已添加到树中。例如,每个叶子页面可以在左边和右边具有对其兄弟页面的引用,这允许不跳回父页面就能顺序扫描。
|
||||
* B树的变体如分形树【22】借用一些日志结构的思想来减少磁盘寻道(而且它们与分形无关)。
|
||||
|
||||
@ -267,7 +267,7 @@ B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆
|
||||
|
||||
尽管B树实现通常比LSM树实现更成熟,但LSM树由于其性能特点也非常有趣。根据经验,通常LSM树的写入速度更快,而B树的读取速度更快【23】。 LSM树上的读取通常比较慢,因为它们必须在压缩的不同阶段检查几个不同的数据结构和SSTables。
|
||||
|
||||
然而,基准通常对工作量的细节不确定和敏感。 您需要测试具有特定工作负载的系统,以便进行有效的比较。 在本节中,我们将简要讨论一些在衡量存储引擎性能时值得考虑的事情。
|
||||
然而,基准通常对工作量的细节不确定且敏感。 您需要测试具有特定工作负载的系统,以便进行有效的比较。 在本节中,我们将简要讨论一些在衡量存储引擎性能时值得考虑的事情。
|
||||
|
||||
#### LSM树的优点
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user