diff --git a/ch1.md b/ch1.md index c5fade515100cf10d0e42fc8bcd76bf84937c7bc..a8a4b6e8c4549fb7d12e249237cd20d6aa020317 100644 --- a/ch1.md +++ b/ch1.md @@ -64,7 +64,7 @@ ***可维护性(Maintainability)*** -​ 许多不同的人(工程师、运维)在不同的生命周期,都能在高效地在系统上工作(使系统保持现有行为,并适应新的应用场景)。(参阅”[可维护性](#可维护性)“) +​ 许多不同的人(工程师、运维)在不同的生命周期,都能高效地在系统上工作(使系统保持现有行为,并适应新的应用场景)。(参阅”[可维护性](#可维护性)“) @@ -368,11 +368,12 @@ ​ 一个应用必须满足各种需求才称得上有用。有一些**功能需求(functional requirements)**(它应该做什么,比如允许以各种方式存储,检索,搜索和处理数据)以及一些**非功能性需求(nonfunctional )**(通用属性,例如安全性,可靠性,合规性,可扩展性,兼容性和可维护性)。在本章详细讨论了可靠性,可扩展性和可维护性。 -​ **可靠性(Reliability)**意味着即使发生故障,系统也能正常工作。故障可能发生在硬件(通常是随机的和不相关的),软件(通常是系统性的Bug,很难处理),和人类(不可避免地时不时出错)。**容错技术**可以对终端用户隐藏某些类型的故障。 -​ **可扩展性(Scalability)**意味着即使在负载增加的情况下也有保持性能的策略。为了讨论可扩展性,我们首先需要定量描述负载和性能的方法。我们简要了解了推特主页时间线的例子,介绍描述负载的方法,并将响应时间百分位点作为衡量性能的一种方式。在可扩展的系统中可以添加**处理容量(processing capacity)**以在高负载下保持可靠。 +​ **可靠性(Reliability)** 意味着即使发生故障,系统也能正常工作。故障可能发生在硬件(通常是随机的和不相关的),软件(通常是系统性的Bug,很难处理),和人类(不可避免地时不时出错)。 **容错技术** 可以对终端用户隐藏某些类型的故障。 -​ **可维护性(Maintainability)**有许多方面,但实质上是关于工程师和运维团队的生活质量的。良好的抽象可以帮助降低复杂度,并使系统易于修改和适应新的应用场景。良好的可操作性意味着对系统的健康状态具有良好的可见性,并拥有有效的管理手段。 +​ **可扩展性(Scalability)** 意味着即使在负载增加的情况下也有保持性能的策略。为了讨论可扩展性,我们首先需要定量描述负载和性能的方法。我们简要了解了推特主页时间线的例子,介绍描述负载的方法,并将响应时间百分位点作为衡量性能的一种方式。在可扩展的系统中可以添加 **处理容量(processing capacity)** 以在高负载下保持可靠。 + +​ **可维护性(Maintainability)** 有许多方面,但实质上是关于工程师和运维团队的生活质量的。良好的抽象可以帮助降低复杂度,并使系统易于修改和适应新的应用场景。良好的可操作性意味着对系统的健康状态具有良好的可见性,并拥有有效的管理手段。 ​ 不幸的是,使应用可靠、可扩展或可维护并不容易。但是某些模式和技术会不断重新出现在不同的应用中。在接下来的几章中,我们将看到一些数据系统的例子,并分析它们如何实现这些目标。 diff --git a/ch10.md b/ch10.md index 5796bf909a8a687a3eb5f92f35f548e98c00ae65..b163d6b372cf2634c4f74d2ac511dd1727382551 100644 --- a/ch10.md +++ b/ch10.md @@ -143,7 +143,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5 这种方法 —— 自动化,快速原型设计,增量式迭代,对实验友好,将大型项目分解成可管理的块 —— 听起来非常像今天的敏捷开发和DevOps运动。奇怪的是,四十年来变化不大。 -​ `sort`工具是一个很好的例子。可以说它比大多数编程语言标准库中的实现(即使有很大好处,也不会溢出到磁盘或使用多线程)要更好。然而,单独使用`sort` 几乎没什么用。它只能与其他Unix工具(如`uniq`)结合使用。 +​ `sort`工具是一个很好的例子。可以说它比大多数编程语言标准库中的实现(它们不会利用磁盘或使用多线程,即使这样做有很大好处)要更好。然而,单独使用`sort` 几乎没什么用。它只能与其他Unix工具(如`uniq`)结合使用。 ​ 像 `bash`这样的Unix shell可以让我们轻松地将这些小程序组合成令人讶异的强大数据处理任务。尽管这些程序中有很多是由不同人群编写的,但它们可以灵活地结合在一起。 Unix如何实现这种可组合性? diff --git a/ch11.md b/ch11.md index 7f1ad29bb7044d43e72ac54e3ab7f612ee029b46..97309aad0092d0fb286ecd9aa1f3a285a71518ae 100644 --- a/ch11.md +++ b/ch11.md @@ -27,19 +27,19 @@ ​ 在批处理领域,作业的输入和输出是文件(也许在分布式文件系统上)。流处理领域中的等价物看上去是什么样子的? -​ 当输入是一个文件(一个字节序列),第一个处理步骤通常是将其解析为一系列记录。在流处理的上下文中,记录通常被叫做**事件(event)**,但它本质上是一样的:一个小的,自包含的,不可变的对象,包含某个时间点发生的某件事情的细节。一个事件通常包含一个来自时钟的时间戳,以指明事件发生的时间(参见“[单调钟与时钟](ch8.md#单调钟与时钟)”)。 +​ 当输入是一个文件(一个字节序列),第一个处理步骤通常是将其解析为一系列记录。在流处理的上下文中,记录通常被叫做 **事件(event)** ,但它本质上是一样的:一个小的,自包含的,不可变的对象,包含某个时间点发生的某件事情的细节。一个事件通常包含一个来自时钟的时间戳,以指明事件发生的时间(参见“[单调钟与时钟](ch8.md#单调钟与时钟)”)。 ​ 例如,发生的事件可能是用户采取的行动,例如查看页面或进行购买。它也可能来源于机器,例如对温度传感器或CPU利用率的周期性测量。在“[使用Unix工具进行批处理](ch10.md#使用Unix工具进行批处理)”的示例中,Web服务器日志的每一行都是一个事件。 ​ 事件可能被编码为文本字符串或JSON,或者某种二进制编码,如[第4章](ch4.md)所述。这种编码允许你存储一个事件,例如将其附加到一个文件,将其插入关系表,或将其写入文档数据库。它还允许你通过网络将事件发送到另一个节点以进行处理。 -​ 在批处理中,文件被写入一次,然后可能被多个作业读取。类似地,在流处理术语中,一个事件由**生产者(producer)**(也称为**发布者(publisher)**或**发送者(sender)**)生成一次,然后可能由多个**消费者(consumer)**(**订阅者(subscribers)**或**接收者(recipients)**)进行处理【3】。在文件系统中,文件名标识一组相关记录;在流式系统中,相关的事件通常被聚合为一个**主题(topic)**或**流(stream)**。 +​ 在批处理中,文件被写入一次,然后可能被多个作业读取。类似地,在流处理术语中,一个事件由 **生产者(producer)** (也称为 **发布者(publisher)** 或 **发送者(sender)** )生成一次,然后可能由多个 **消费者(consumer)** ( **订阅者(subscribers)** 或 **接收者(recipients)** )进行处理【3】。在文件系统中,文件名标识一组相关记录;在流式系统中,相关的事件通常被聚合为一个 **主题(topic)** 或 **流(stream)** 。 ​ 原则上讲,文件或数据库就足以连接生产者和消费者:生产者将其生成的每个事件写入数据存储,且每个消费者定期轮询数据存储,检查自上次运行以来新出现的事件。这实际上正是批处理在每天结束时处理当天数据时所做的事情。 ​ 但当我们想要进行低延迟的连续处理时,如果数据存储不是为这种用途专门设计的,那么轮询开销就会很大。轮询的越频繁,能返回新事件的请求比例就越低,而额外开销也就越高。相比之下,最好能在新事件出现时直接通知消费者。 -​ 数据库在传统上对这种通知机制支持的并不好,关系型数据库通常有**触发器(trigger)**,它们可以对变化作出反应(如,插入表中的一行),但是它们的功能非常有限,并且在数据库设计中有些后顾之忧【4,5】。相应的是,已经开发了专门的工具来提供事件通知。 +​ 数据库在传统上对这种通知机制支持的并不好,关系型数据库通常有 **触发器(trigger)** ,它们可以对变化作出反应(如,插入表中的一行),但是它们的功能非常有限,并且在数据库设计中有些后顾之忧【4,5】。相应的是,已经开发了专门的工具来提供事件通知。 ### 消息系统 @@ -108,7 +108,7 @@ **图11-1 (a)负载平衡:在消费者间共享消费主题;(b)扇出:将每条消息传递给多个消费者。** -​ 两种模式可以组合使用:例如,两个独立的消费者组可以每组各订阅一个主题,每一组组都共同收到所有消息,但在每一组内部,每条消息仅由单个节点处理。 +​ 两种模式可以组合使用:例如,两个独立的消费者组可以每组各订阅一个主题,每一组都共同收到所有消息,但在每一组内部,每条消息仅由单个节点处理。 #### 确认与重新交付 diff --git a/ch2.md b/ch2.md index c7a0ab001a1054b19cef6635113eac750335252c..cb3abc6091a460a3da5de72cef7c7276530ed7d8 100644 --- a/ch2.md +++ b/ch2.md @@ -685,8 +685,8 @@ WITH RECURSIVE JOIN lives_in_europe ON vertices.vertex_id = lives_in_europe.vertex_id; ``` -* 首先,查找`name`属性为`United States`的顶点,将其作为`in_use`顶点的集合的第一个元素。 -* 从`in_use`集合的顶点出发,沿着所有的`with_in`入边,将其尾顶点加入同一集合,不断递归直到所有`with_in`入边都被访问完毕。 +* 首先,查找`name`属性为`United States`的顶点,将其作为`in_usa`顶点的集合的第一个元素。 +* 从`in_usa`集合的顶点出发,沿着所有的`with_in`入边,将其尾顶点加入同一集合,不断递归直到所有`with_in`入边都被访问完毕。 * 同理,从`name`属性为`Europe`的顶点出发,建立`in_europe`顶点的集合。 * 对于`in_usa`集合中的每个顶点,根据`born_in`入边来查找出生在美国某个地方的人。 * 同样,对于`in_europe`集合中的每个顶点,根据`lives_in`入边来查找居住在欧洲的人。 @@ -843,7 +843,7 @@ SPARQL是一种很好的查询语言——哪怕语义网从未实现,它仍 [^viii]: Datomic和Cascalog使用Datalog的Clojure S表达式语法。在下面的例子中使用了一个更容易阅读的Prolog语法,但两者没有任何功能差异。 -Datalog的数据模型类似于三元组模式,但进行了一点泛化。把三元组写成**谓语**(**主语,宾语**),而不是写三元语(**主语,宾语,宾语**)。[例2-10]()显示了如何用Datalog写入我们的例子中的数据。 +Datalog的数据模型类似于三元组模式,但进行了一点泛化。把三元组写成**谓语**(**主语,宾语**),而不是写三元语(**主语,谓语,宾语**)。[例2-10]()显示了如何用Datalog写入我们的例子中的数据。 **例2-10 用Datalog来表示图2-5中的数据子集** @@ -883,7 +883,7 @@ migrated(Name, BornIn, LivingIn) :- name(Person, Name), /* Rule 3 */ ``` -Cypher和SPARQL使用SELECT立即跳转,但是Datalog一次只进行一小步。我们定义**规则**,以将新谓语告诉数据库:在这里,我们定义了两个新的谓语,`_recursive`和`migrated`。这些谓语不是存储在数据库中的三元组中,而是它们是从数据或其他规则派生而来的。规则可以引用其他规则,就像函数可以调用其他函数或者递归地调用自己一样。像这样,复杂的查询可以一次构建其中的一小块。 +Cypher和SPARQL使用SELECT立即跳转,但是Datalog一次只进行一小步。我们定义**规则**,以将新谓语告诉数据库:在这里,我们定义了两个新的谓语,`within_recursive`和`migrated`。这些谓语不是存储在数据库中的三元组中,而是它们是从数据或其他规则派生而来的。规则可以引用其他规则,就像函数可以调用其他函数或者递归地调用自己一样。像这样,复杂的查询可以一次构建其中的一小块。 在规则中,以大写字母开头的单词是变量,谓语则用Cypher和SPARQL的方式一样来匹配。例如,`name(Location, Name)`通过变量绑定`Location = namerica`和`Name ='North America'`可以匹配三元组`name(namerica, 'North America')`。 diff --git a/ch3.md b/ch3.md index 880a2e235a62836ca1d4157c376b7716e06cfbd5..d121dda7ccdce3d5001562fe337f7400dce0a93a 100644 --- a/ch3.md +++ b/ch3.md @@ -273,7 +273,7 @@ B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆 B树索引必须至少两次写入每一段数据:一次写入预先写入日志,一次写入树页面本身(也许再次分页)。即使在该页面中只有几个字节发生了变化,也需要一次编写整个页面的开销。有些存储引擎甚至会覆盖同一个页面两次,以免在电源故障的情况下导致页面部分更新【24,25】。 -由于反复压缩和合并SSTables,日志结构索引也会重写数据。这种影响 —— 在数据库的生命周期中写入数据库导致对磁盘的多次写入 —— 被称为**写放大(write amplification)**。需要特别关注的是固态硬盘,固态硬盘在磨损之前只能覆写一段时间。 +由于反复压缩和合并SSTables,日志结构索引也会重写数据。这种影响 —— 在数据库的生命周期中写入数据库导致对磁盘的多次写入 —— 被称为**写放大(write amplification)**。需要特别注意的是固态硬盘,固态硬盘的闪存寿命在覆写有限次数后就会耗尽。 在写入繁重的应用程序中,性能瓶颈可能是数据库可以写入磁盘的速度。在这种情况下,写放大会导致直接的性能代价:存储引擎写入磁盘的次数越多,可用磁盘带宽内的每秒写入次数越少。 diff --git a/ch4.md b/ch4.md index 80fc7a92c350b82f4398f764692439ede0409757..172fedb7e1bc8dd5d49e6e88f57f60b066e0e56e 100644 --- a/ch4.md +++ b/ch4.md @@ -47,7 +47,7 @@ [^i]: 除一些特殊情况外,例如某些内存映射文件或直接在压缩数据上操作(如“[列压缩](ch4.md#列压缩)”中所述)。 -所以,需要在两种表示之间进行某种类型的翻译。 从内存中表示到字节序列的转换称为**编码(Encoding)**(也称为**序列化(serialization)**或**编组(marshalling)**),反过来称为**解码(Decoding)**[^ii](**解析(Parsing)**,**反序列化(deserialization)**,**反编组() unmarshalling)**)[^译i]。 +所以,需要在两种表示之间进行某种类型的翻译。 从内存中表示到字节序列的转换称为**编码(Encoding)**(也称为**序列化(serialization)**或**编组(marshalling)**),反过来称为**解码(Decoding)**[^ii](**解析(Parsing)**,**反序列化(deserialization)**,**反编组( unmarshalling)**)[^译i]。 [^ii]: 请注意,**编码(encode)**与**加密(encryption)**无关。 本书不讨论加密。 [^译i]: Marshal与Serialization的区别:Marshal不仅传输对象的状态,而且会一起传输对象的方法(相关代码)。 @@ -350,7 +350,7 @@ Avro为静态类型编程语言提供了可选的代码生成功能,但是它 #### 在不同的时间写入不同的值 -数据库通常允许任何时候更新任何值。这意味着在一个单一的数据库中,可能有一些价值是五毫秒前写的,而一些价值是五年前写的。 +数据库通常允许任何时候更新任何值。这意味着在一个单一的数据库中,可能有一些值是五毫秒前写的,而一些值是五年前写的。 在部署应用程序的新版本(至少是服务器端应用程序)时,您可能会在几分钟内完全用新版本替换旧版本。数据库内容也是如此:五年前的数据仍然存在于原始编码中,除非您已经明确地重写了它。这种观察有时被总结为数据超出代码。 diff --git a/ch5.md b/ch5.md index 31c1ac8fa192cc6f468a3fb53bf4c80d4c457f7c..2d0f3c4a27d75dfdb1715bb286722a4281a3e90f 100644 --- a/ch5.md +++ b/ch5.md @@ -287,7 +287,7 @@ ​ 如前所述,应用程序可以提供比底层数据库更强有力的保证,例如通过主库进行某种读取。但在应用程序代码中处理这些问题是复杂的,容易出错。 -​ 如果应用程序开发人员不必担心微妙的复制问题,并可以信赖他们的数据库“做了正确的事情”,那该多好呀。这就是**事务(transaction)**存在的原因:**数据库通过事务提供强大的保证**,所以应用程序可以更假简单。 +​ 如果应用程序开发人员不必担心微妙的复制问题,并可以信赖他们的数据库“做了正确的事情”,那该多好呀。这就是**事务(transaction)**存在的原因:**数据库通过事务提供强大的保证**,所以应用程序可以更加简单。 ​ 单节点事务已经存在了很长时间。然而在走向分布式(复制和分区)数据库时,许多系统放弃了事务。声称事务在性能和可用性上的代价太高,并断言在可扩展系统中最终一致性是不可避免的。这个叙述有一些道理,但过于简单了,本书其余部分将提出更为细致的观点。第七章和第九章将回到事务的话题,并讨论一些替代机制。 @@ -433,7 +433,7 @@ ​ 有些冲突是显而易见的。在[图5-7](img/fig5-7.png)的例子中,两个写操作并发地修改了同一条记录中的同一个字段,并将其设置为两个不同的值。毫无疑问这是一个冲突。 -​ 其他类型的冲突可能更为微妙,难以发现。例如,考虑一个会议室预订系统:它记录谁腚了哪个时间段的哪个房间。应用需要确保每个房间只有一组人同时预定(即不得有相同房间的重叠预订)。在这种情况下,如果同时为同一个房间创建两个不同的预订,则可能会发生冲突。即使应用程序在允许用户进行预订之前检查可用性,如果两次预订是由两个不同的领导者进行的,则可能会有冲突。 +​ 其他类型的冲突可能更为微妙,难以发现。例如,考虑一个会议室预订系统:它记录谁订了哪个时间段的哪个房间。应用需要确保每个房间只有一组人同时预定(即不得有相同房间的重叠预订)。在这种情况下,如果同时为同一个房间创建两个不同的预订,则可能会发生冲突。即使应用程序在允许用户进行预订之前检查可用性,如果两次预订是由两个不同的领导者进行的,则可能会有冲突。 ​ 现在还没有一个现成的答案,但在接下来的章节中,我们将追溯到对这个问题有很好的理解。我们将在第7章中看到更多的冲突示例,在[第12章](ch12.md)中我们将讨论用于检测和解决复制系统中冲突的可扩展方法。 @@ -708,7 +708,7 @@ ​ [图5-13](img/fig5-13.png)中的示例只使用一个副本。当有多个副本但没有领导者时,算法如何修改? -​ [图5-13](img/fig5-13.png)使用单个版本号来捕获操作之间的依赖关系,但是当多个副本并发接受写入时,这是不够的。相反,除了对每个键使用版本号之外,还需要在**每个副本**中版本号。每个副本在处理写入时增加自己的版本号,并且跟踪从其他副本中看到的版本号。这个信息指出了要覆盖哪些值,以及保留哪些值作为兄弟。 +​ [图5-13](img/fig5-13.png)使用单个版本号来捕获操作之间的依赖关系,但是当多个副本并发接受写入时,这是不够的。相反,除了对每个键使用版本号之外,还需要在**每个副本**中使用版本号。每个副本在处理写入时增加自己的版本号,并且跟踪从其他副本中看到的版本号。这个信息指出了要覆盖哪些值,以及保留哪些值作为兄弟。 ​ 所有副本的版本号集合称为**版本向量(version vector)**【56】。这个想法的一些变体正在使用,但最有趣的可能是在Riak 2.0 【58,59】中使用的**分散版本矢量(dotted version vector)**【57】。我们不会深入细节,但是它的工作方式与我们在购物车示例中看到的非常相似。 @@ -922,4 +922,4 @@ | 上一章 | 目录 | 下一章 | | :--------------------------------: | :-----------------------------: | :--------------------: | -| [第二部分:分布式数据](part-ii.md) | [设计数据密集型应用](README.md) | [第六章:分区](ch6.md) | \ No newline at end of file +| [第二部分:分布式数据](part-ii.md) | [设计数据密集型应用](README.md) | [第六章:分区](ch6.md) | diff --git a/ch6.md b/ch6.md index 1ed2a1454e741ae2adbacd43a29c3b4e76aea5f2..5cfd838a95153ddc5ba1555b7730f432fc3f4b42 100644 --- a/ch6.md +++ b/ch6.md @@ -140,7 +140,7 @@ ​ 但是,从文档分区索引中读取需要注意:除非您对文档ID做了特别的处理,否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在[图6-4](img/fig6-4.png)中,红色汽车出现在分区0和分区1中。因此,如果要搜索红色汽车,则需要将查询发送到所有分区,并合并所有返回的结果。 -​ 这种查询分区数据库的方法有时被称为**分散/聚集(scatter/gather)**,并且可能会使二级索引上的读取查询相当昂贵。即使并行查询分区,分散/聚集也容易导致尾部延迟放大(参阅“[实践中的百分位点](ch1.md#实践中的百分位点)”)。然而,它被广泛使用:MonDBDB,Riak 【15】,Cassandra 【16】,Elasticsearch 【17】,SolrCloud 【18】和VoltDB 【19】都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案,但这并不总是可行,尤其是当在单个查询中使用多个二级索引时(例如同时需要按颜色和制造商查询)。 +​ 这种查询分区数据库的方法有时被称为**分散/聚集(scatter/gather)**,并且可能会使二级索引上的读取查询相当昂贵。即使并行查询分区,分散/聚集也容易导致尾部延迟放大(参阅“[实践中的百分位点](ch1.md#实践中的百分位点)”)。然而,它被广泛使用:MongoDB,Riak 【15】,Cassandra 【16】,Elasticsearch 【17】,SolrCloud 【18】和VoltDB 【19】都使用文档分区二级索引。大多数数据库供应商建议您构建一个能从单个分区提供二级索引查询的分区方案,但这并不总是可行,尤其是当在单个查询中使用多个二级索引时(例如同时需要按颜色和制造商查询)。 ### 根据关键词(Term)的二级索引 @@ -317,7 +317,7 @@ 我们还讨论了分区和二级索引之间的相互作用。次级索引也需要分区,有两种方法: * 按文档分区(本地索引),其中二级索引存储在与主键和值相同的分区中。这意味着只有一个分区需要在写入时更新,但是读取二级索引需要在所有分区之间进行分散/收集。 -* 按关键词分区(全局索引),其中二级索引存在不同的分区的。辅助索引中的条目可以包括来自主键的所有分区的记录。当文档写入时,需要更新多个分区中的二级索引;但是可以从单个分区中进行读取。 +* 按关键词分区(全局索引),其中二级索引存在不同的分区中。辅助索引中的条目可以包括来自主键的所有分区的记录。当文档写入时,需要更新多个分区中的二级索引;但是可以从单个分区中进行读取。 最后,我们讨论了将查询路由到适当的分区的技术,从简单的分区负载平衡到复杂的并行查询执行引擎。 diff --git a/ch8.md b/ch8.md index c31c625d2cade1e52f6d81e288f5cc6e42f58d36..bf4f843bbb24801d7422a660c2b9d4a44950bc23 100644 --- a/ch8.md +++ b/ch8.md @@ -137,7 +137,7 @@ 许多系统需要自动检测故障节点。例如: * 负载平衡器需要停止向已死亡的节点转发请求(即从**移出轮询列表(out of rotation)**)。 -* 在单主复制功能的分布式数据库中,如果主库失效,则需要将从库之一升级为新主库(参阅“[ch5.md#处理节点宕机](处理节点宕机)”)。 +* 在单主复制功能的分布式数据库中,如果主库失效,则需要将从库之一升级为新主库(参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。 不幸的是,网络的不确定性使得很难判断一个节点是否工作。在某些特定的情况下,您可能会收到一些反馈信息,明确告诉您某些事情没有成功: @@ -194,7 +194,7 @@ ​ 在这种环境下,您只能通过实验方式选择超时:测量延长的网络往返时间和多台机器的分布,以确定延迟的预期可变性。然后,考虑到应用程序的特性,可以确定**故障检测延迟**与**过早超时风险**之间的适当折衷。 -​ 更好的一种做法是,系统不是使用配置的常量超时,而是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布自动调整超时。这可以通过Phi Accrual故障检测器【30】来完成,该检测器例如在Akka和Cassandra 【31】中使用。 TCP重传超时也同样起作用【27】。 +​ 更好的一种做法是,系统不是使用配置的常量超时时间,而是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布自动调整超时时间。这可以通过Phi Accrual故障检测器【30】来完成,该检测器在例如Akka和Cassandra 【31】中使用。 TCP超时重传机制也同样起作用【27】。 ### 同步网络 vs 异步网络 @@ -467,7 +467,7 @@ while(true){ ​ 第三种情况,想象一个经历了一个长时间**停止世界垃圾收集暂停(stop-the-world GC Pause)**的节点。节点的所有线程被GC抢占并暂停一分钟,因此没有请求被处理,也没有响应被发送。其他节点等待,重试,不耐烦,并最终宣布节点死亡,并将其丢到灵车上。最后,GC完成,节点的线程继续,好像什么也没有发生。其他节点感到惊讶,因为所谓的死亡节点突然从棺材中抬起头来,身体健康,开始和旁观者高兴地聊天。GC后的节点最初甚至没有意识到已经经过了整整一分钟,而且自己已被宣告死亡。从它自己的角度来看,从最后一次与其他节点交谈以来,几乎没有经过任何时间。 -​ 这些故事的寓意是,节点不一定能相信自己对于情况的判断。分布式系统不能完全依赖单个节点,因为节点可能随时失效,可能会使系统卡死,无法恢复。相反,许多分布式算法都依赖于法定人数,即在节点之间进行投票(参阅“[读写法定人数](ch5.md#读写法定人数)”):决策需要来自多个节点的最小投票数,以减少对于某个特定节点的依赖。 +​ 这些故事的寓意是,节点不一定能相信自己对于情况的判断。分布式系统不能完全依赖单个节点,因为节点可能随时失效,可能会使系统卡死,无法恢复。相反,许多分布式算法都依赖于法定人数,即在节点之间进行投票(参阅“[读写的法定人数](ch5.md#读写的法定人数)“):决策需要来自多个节点的最小投票数,以减少对于某个特定节点的依赖。 ​ 这也包括关于宣告节点死亡的决定。如果法定数量的节点宣告另一个节点已经死亡,那么即使该节点仍感觉自己活着,它也必须被认为是死的。个体节点必须遵守法定决定并下台。 diff --git a/ch9.md b/ch9.md index 235832723f198bd719829120288a108db80d4a3d..3e119b8dd08f001a3bbe270ec9d221dfe53efd1f 100644 --- a/ch9.md +++ b/ch9.md @@ -831,7 +831,7 @@ #### 共识的局限性 -​ 共识算法对于分布式系统来说是一个巨大的突破:它为其他充满不确定性的系统带来了基础的安全属性(一致同意,完整性和有效性),然而它们还能保持容错(只要多数节点正常工作且可达,就能取得进展)。它们提供了全序广播,因此也可以它们也可以以一种容错的方式实现线性一致的原子操作(参见“[使用全序广播实现线性一致性存储](#使用全序广播实现线性一致性存储)”)。 +​ 共识算法对于分布式系统来说是一个巨大的突破:它为其他充满不确定性的系统带来了基础的安全属性(一致同意,完整性和有效性),然而它们还能保持容错(只要多数节点正常工作且可达,就能取得进展)。它们提供了全序广播,因此它们也可以以一种容错的方式实现线性一致的原子操作(参见“[使用全序广播实现线性一致性存储](#使用全序广播实现线性一致性存储)”)。 ​ 尽管如此,它们并不是在所有地方都用上了,因为好处总是有代价的。 @@ -889,7 +889,7 @@ ​ ZooKeeper,etcd和Consul也经常用于服务发现——也就是找出你需要连接到哪个IP地址才能到达特定的服务。在云数据中心环境中,虚拟机连续来去常见,你通常不会事先知道服务的IP地址。相反,你可以配置你的服务,使其在启动时注册服务注册表中的网络端点,然后可以由其他服务找到它们。 -​ 但是,服务发现是否需要达成共识还不太清楚。 DNS是查找服务名称的IP地址的传统方式,它使用多层缓存来实现良好的性能和可用性。从DNS读取是绝对不线性一致性的,如果DNS查询的结果有点陈旧,通常不会有问题【109】。 DNS对网络中断的可靠性和可靠性更为重要。 +​ 但是,服务发现是否需要达成共识还不太清楚。 DNS是查找服务名称的IP地址的传统方式,它使用多层缓存来实现良好的性能和可用性。从DNS读取是绝对不线性一致性的,如果DNS查询的结果有点陈旧,通常不会有问题【109】。 DNS的可用性和对网络中断的鲁棒性更重要。 ​ 尽管服务发现并不需要共识,但领导者选举却是如此。因此,如果你的共识系统已经知道领导是谁,那么也可以使用这些信息来帮助其他服务发现领导是谁。为此,一些共识系统支持只读缓存副本。这些副本异步接收共识算法所有决策的日志,但不主动参与投票。因此,它们能够提供不需要线性一致性的读取请求。