mirror of
https://github.com/Vonng/ddia.git
synced 2025-01-05 15:30:06 +08:00
reformat indented content
This commit is contained in:
parent
ac6b23f0cc
commit
9b3bd96457
@ -149,6 +149,8 @@
|
||||
|
||||
| ISSUE & Pull Requests | USER | Title |
|
||||
| ----------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| [147](https://github.com/Vonng/ddia/pull/147) | [@ZvanYang](https://github.com/ZvanYang) | ch5: 更正一处不准确的翻译 |
|
||||
| [145](https://github.com/Vonng/ddia/pull/145) | [@Hookey](https://github.com/Hookey) | 识别了当前简繁转译过程中处理不当的地方,暂通过转换脚本规避 |
|
||||
| [144](https://github.com/Vonng/ddia/issues/144) | [@secret4233](https://github.com/secret4233) | ch7: 不翻译`next-key locking` |
|
||||
| [143](https://github.com/Vonng/ddia/issues/143) | [@imcheney](https://github.com/imcheney) | ch3: 更新残留的机翻段落 |
|
||||
| [142](https://github.com/Vonng/ddia/issues/142) | [@XIJINIAN](https://github.com/XIJINIAN) | 建议去除段首的制表符 |
|
||||
|
32
ch1.md
32
ch1.md
@ -53,17 +53,17 @@
|
||||
|
||||
本书着重讨论三个在大多数软件系统中都很重要的问题:
|
||||
|
||||
***可靠性(Reliability)***
|
||||
* 可靠性(Reliability)
|
||||
|
||||
系统在**困境(adversity)**(硬件故障、软件故障、人为错误)中仍可正常工作(正确完成功能,并能达到期望的性能水准)。请参阅“[可靠性](#可靠性)”。
|
||||
系统在**困境(adversity)**(硬件故障、软件故障、人为错误)中仍可正常工作(正确完成功能,并能达到期望的性能水准)。请参阅“[可靠性](#可靠性)”。
|
||||
|
||||
***可伸缩性(Scalability)***
|
||||
* 可伸缩性(Scalability)
|
||||
|
||||
有合理的办法应对系统的增长(数据量、流量、复杂性)。请参阅“[可伸缩性](#可伸缩性)”。
|
||||
有合理的办法应对系统的增长(数据量、流量、复杂性)。请参阅“[可伸缩性](#可伸缩性)”。
|
||||
|
||||
***可维护性(Maintainability)***
|
||||
* 可维护性(Maintainability)
|
||||
|
||||
许多不同的人(工程师、运维)在不同的生命周期,都能高效地在系统上工作(使系统保持现有行为,并适应新的应用场景)。请参阅“[可维护性](#可维护性)”。
|
||||
许多不同的人(工程师、运维)在不同的生命周期,都能高效地在系统上工作(使系统保持现有行为,并适应新的应用场景)。请参阅“[可维护性](#可维护性)”。
|
||||
|
||||
人们经常追求这些词汇,却没有清楚理解它们到底意味着什么。为了工程的严谨性,本章的剩余部分将探讨可靠性、可伸缩性、可维护性的含义。为实现这些目标而使用的各种技术,架构和算法将在后续的章节中研究。
|
||||
|
||||
@ -152,13 +152,13 @@
|
||||
|
||||
为了使这个概念更加具体,我们以推特在2012年11月发布的数据【16】为例。推特的两个主要业务是:
|
||||
|
||||
***发布推文***
|
||||
* 发布推文
|
||||
|
||||
用户可以向其粉丝发布新消息(平均 4.6k请求/秒,峰值超过 12k请求/秒)。
|
||||
用户可以向其粉丝发布新消息(平均 4.6k请求/秒,峰值超过 12k请求/秒)。
|
||||
|
||||
***主页时间线***
|
||||
* 主页时间线
|
||||
|
||||
用户可以查阅他们关注的人发布的推文(300k请求/秒)。
|
||||
用户可以查阅他们关注的人发布的推文(300k请求/秒)。
|
||||
|
||||
处理每秒12,000次写入(发推文的速率峰值)还是很简单的。然而推特的伸缩性挑战并不是主要来自推特量,而是来自**扇出(fan-out)**——每个用户关注了很多人,也被很多人关注。
|
||||
|
||||
@ -279,17 +279,17 @@
|
||||
|
||||
但是我们可以,也应该以这样一种方式来设计软件:在设计之初就尽量考虑尽可能减少维护期间的痛苦,从而避免自己的软件系统变成遗留系统。为此,我们将特别关注软件系统的三个设计原则:
|
||||
|
||||
***可操作性(Operability)***
|
||||
* 可操作性(Operability)
|
||||
|
||||
便于运维团队保持系统平稳运行。
|
||||
便于运维团队保持系统平稳运行。
|
||||
|
||||
***简单性(Simplicity)***
|
||||
* 简单性(Simplicity)
|
||||
|
||||
从系统中消除尽可能多的**复杂度(complexity)**,使新工程师也能轻松理解系统。(注意这和用户接口的简单性不一样。)
|
||||
从系统中消除尽可能多的**复杂度(complexity)**,使新工程师也能轻松理解系统。(注意这和用户接口的简单性不一样。)
|
||||
|
||||
***可演化性(evolability)***
|
||||
* 可演化性(evolability)
|
||||
|
||||
使工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配。也称为**可伸缩性(extensibility)**,**可修改性(modifiability)** 或**可塑性(plasticity)**。
|
||||
使工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配。也称为**可伸缩性(extensibility)**,**可修改性(modifiability)** 或**可塑性(plasticity)**。
|
||||
|
||||
和之前提到的可靠性、可伸缩性一样,实现这些目标也没有简单的解决方案。不过我们会试着想象具有可操作性,简单性和可演化性的系统会是什么样子。
|
||||
|
||||
|
41
ch10.md
41
ch10.md
@ -16,17 +16,17 @@
|
||||
|
||||
Web和越来越多的基于HTTP/REST的API使交互的请求/响应风格变得如此普遍,以至于很容易将其视为理所当然。但我们应该记住,这不是构建系统的唯一方式,其他方法也有其优点。我们来看看三种不同类型的系统:
|
||||
|
||||
***服务(在线系统)***
|
||||
* 服务(在线系统)
|
||||
|
||||
服务等待客户的请求或指令到达。每收到一个,服务会试图尽快处理它,并发回一个响应。响应时间通常是服务性能的主要衡量指标,可用性通常非常重要(如果客户端无法访问服务,用户可能会收到错误消息)。
|
||||
服务等待客户的请求或指令到达。每收到一个,服务会试图尽快处理它,并发回一个响应。响应时间通常是服务性能的主要衡量指标,可用性通常非常重要(如果客户端无法访问服务,用户可能会收到错误消息)。
|
||||
|
||||
***批处理系统(离线系统)***
|
||||
* 批处理系统(离线系统)
|
||||
|
||||
一个批处理系统有大量的输入数据,跑一个**作业(job)** 来处理它,并生成一些输出数据,这往往需要一段时间(从几分钟到几天),所以通常不会有用户等待作业完成。相反,批量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(处理特定大小的输入所需的时间)。本章中讨论的就是批处理。
|
||||
一个批处理系统有大量的输入数据,跑一个**作业(job)** 来处理它,并生成一些输出数据,这往往需要一段时间(从几分钟到几天),所以通常不会有用户等待作业完成。相反,批量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(处理特定大小的输入所需的时间)。本章中讨论的就是批处理。
|
||||
|
||||
***流处理系统(准实时系统)***
|
||||
* 流处理系统(准实时系统)
|
||||
|
||||
流处理介于在线和离线(批处理)之间,所以有时候被称为**准实时(near-real-time)** 或**准在线(nearline)** 处理。像批处理系统一样,流处理消费输入并产生输出(并不需要响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则需等待固定的一组输入数据。这种差异使流处理系统比起批处理系统具有更低的延迟。由于流处理基于批处理,我们将在[第十一章](ch11.md)讨论它。
|
||||
流处理介于在线和离线(批处理)之间,所以有时候被称为**准实时(near-real-time)** 或**准在线(nearline)** 处理。像批处理系统一样,流处理消费输入并产生输出(并不需要响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则需等待固定的一组输入数据。这种差异使流处理系统比起批处理系统具有更低的延迟。由于流处理基于批处理,我们将在[第十一章](ch11.md)讨论它。
|
||||
|
||||
正如我们将在本章中看到的那样,批处理是构建可靠、可伸缩和可维护应用程序的重要组成部分。例如,2004年发布的批处理算法Map-Reduce(可能被过分热情地)被称为“造就Google大规模可伸缩性的算法”【2】。随后在各种开源数据系统中得到应用,包括Hadoop,CouchDB和MongoDB。
|
||||
|
||||
@ -224,11 +224,12 @@ MapReduce是一个编程框架,你可以使用它编写代码来处理HDFS等
|
||||
|
||||
要创建MapReduce作业,你需要实现两个回调函数,Mapper和Reducer,其行为如下(请参阅“[MapReduce查询](ch2.md#MapReduce查询)”):
|
||||
|
||||
***Mapper***
|
||||
* Mapper
|
||||
|
||||
Mapper会在每条输入记录上调用一次,其工作是从输入记录中提取键值。对于每个输入,它可以生成任意数量的键值对(包括None)。它不会保留从一个输入记录到下一个记录的任何状态,因此每个记录都是独立处理的。
|
||||
Mapper会在每条输入记录上调用一次,其工作是从输入记录中提取键值。对于每个输入,它可以生成任意数量的键值对(包括None)。它不会保留从一个输入记录到下一个记录的任何状态,因此每个记录都是独立处理的。
|
||||
|
||||
* Reducer
|
||||
|
||||
***Reducer***
|
||||
MapReduce框架拉取由Mapper生成的键值对,收集属于同一个键的所有值,并在这组值上迭代调用Reducer。 Reducer可以产生输出记录(例如相同URL的出现次数)。
|
||||
|
||||
在Web服务器日志的例子中,我们在第5步中有第二个`sort`命令,它按请求数对URL进行排序。在MapReduce中,如果你需要第二个排序阶段,则可以通过编写第二个MapReduce作业并将第一个作业的输出用作第二个作业的输入来实现它。这样看来,Mapper的作用是将数据放入一个适合排序的表单中,并且Reducer的作用是处理已排序的数据。
|
||||
@ -686,30 +687,30 @@ Spark,Flink和Tez避免将中间状态写入HDFS,因此它们采取了不同
|
||||
|
||||
分布式批处理框架需要解决的两个主要问题是:
|
||||
|
||||
***分区***
|
||||
* 分区
|
||||
|
||||
在MapReduce中,Mapper根据输入文件块进行分区。Mapper的输出被重新分区、排序并合并到可配置数量的Reducer分区中。这一过程的目的是把所有的**相关**数据(例如带有相同键的所有记录)都放在同一个地方。
|
||||
在MapReduce中,Mapper根据输入文件块进行分区。Mapper的输出被重新分区、排序并合并到可配置数量的Reducer分区中。这一过程的目的是把所有的**相关**数据(例如带有相同键的所有记录)都放在同一个地方。
|
||||
|
||||
后MapReduce时代的数据流引擎若非必要会尽量避免排序,但它们也采取了大致类似的分区方法。
|
||||
后MapReduce时代的数据流引擎若非必要会尽量避免排序,但它们也采取了大致类似的分区方法。
|
||||
|
||||
***容错***
|
||||
* 容错
|
||||
|
||||
MapReduce经常写入磁盘,这使得从单个失败的任务恢复很轻松,无需重新启动整个作业,但在无故障的情况下减慢了执行速度。数据流引擎更多地将中间状态保存在内存中,更少地物化中间状态,这意味着如果节点发生故障,则需要重算更多的数据。确定性算子减少了需要重算的数据量。
|
||||
MapReduce经常写入磁盘,这使得从单个失败的任务恢复很轻松,无需重新启动整个作业,但在无故障的情况下减慢了执行速度。数据流引擎更多地将中间状态保存在内存中,更少地物化中间状态,这意味着如果节点发生故障,则需要重算更多的数据。确定性算子减少了需要重算的数据量。
|
||||
|
||||
|
||||
我们讨论了几种MapReduce的连接算法,其中大多数也在MPP数据库和数据流引擎内部使用。它们也很好地演示了分区算法是如何工作的:
|
||||
|
||||
***排序合并连接***
|
||||
* 排序合并连接
|
||||
|
||||
每个参与连接的输入都通过一个提取连接键的Mapper。通过分区、排序和合并,具有相同键的所有记录最终都会进入相同的Reducer调用。这个函数能输出连接好的记录。
|
||||
每个参与连接的输入都通过一个提取连接键的Mapper。通过分区、排序和合并,具有相同键的所有记录最终都会进入相同的Reducer调用。这个函数能输出连接好的记录。
|
||||
|
||||
***广播散列连接***
|
||||
* 广播散列连接
|
||||
|
||||
两个连接输入之一很小,所以它并没有分区,而且能被完全加载进一个哈希表中。因此,你可以为连接输入大端的每个分区启动一个Mapper,将输入小端的散列表加载到每个Mapper中,然后扫描大端,一次一条记录,并为每条记录查询散列表。
|
||||
两个连接输入之一很小,所以它并没有分区,而且能被完全加载进一个哈希表中。因此,你可以为连接输入大端的每个分区启动一个Mapper,将输入小端的散列表加载到每个Mapper中,然后扫描大端,一次一条记录,并为每条记录查询散列表。
|
||||
|
||||
***分区散列连接***
|
||||
* 分区散列连接
|
||||
|
||||
如果两个连接输入以相同的方式分区(使用相同的键,相同的散列函数和相同数量的分区),则可以独立地对每个分区应用散列表方法。
|
||||
如果两个连接输入以相同的方式分区(使用相同的键,相同的散列函数和相同数量的分区),则可以独立地对每个分区应用散列表方法。
|
||||
|
||||
分布式批处理引擎有一个刻意限制的编程模型:回调函数(比如Mapper和Reducer)被假定是无状态的,而且除了指定的输出外,必须没有任何外部可见的副作用。这一限制允许框架在其抽象下隐藏一些困难的分布式系统问题:当遇到崩溃和网络问题时,任务可以安全地重试,任何失败任务的输出都被丢弃。如果某个分区的多个任务成功,则其中只有一个能使其输出实际可见。
|
||||
|
||||
|
44
ch11.md
44
ch11.md
@ -96,13 +96,13 @@
|
||||
|
||||
当多个消费者从同一主题中读取消息时,有两种主要的消息传递模式,如[图11-1](img/fig11-1.png)所示:
|
||||
|
||||
***负载均衡(load balancing)***
|
||||
* 负载均衡(load balancing)
|
||||
|
||||
每条消息都被传递给消费者**之一**,所以处理该主题下消息的工作能被多个消费者共享。代理可以为消费者任意分配消息。当处理消息的代价高昂,希望能并行处理消息时,此模式非常有用(在AMQP中,可以通过让多个客户端从同一个队列中消费来实现负载均衡,而在JMS中则称之为**共享订阅(shared subscription)**)。
|
||||
每条消息都被传递给消费者**之一**,所以处理该主题下消息的工作能被多个消费者共享。代理可以为消费者任意分配消息。当处理消息的代价高昂,希望能并行处理消息时,此模式非常有用(在AMQP中,可以通过让多个客户端从同一个队列中消费来实现负载均衡,而在JMS中则称之为**共享订阅(shared subscription)**)。
|
||||
|
||||
***扇出(fan-out)***
|
||||
* 扇出(fan-out)
|
||||
|
||||
每条消息都被传递给**所有**消费者。扇出允许几个独立的消费者各自“收听”相同的消息广播,而不会相互影响 —— 这个流处理中的概念对应批处理中多个不同批处理作业读取同一份输入文件 (JMS中的主题订阅与AMQP中的交叉绑定提供了这一功能)。
|
||||
每条消息都被传递给**所有**消费者。扇出允许几个独立的消费者各自“收听”相同的消息广播,而不会相互影响 —— 这个流处理中的概念对应批处理中多个不同批处理作业读取同一份输入文件 (JMS中的主题订阅与AMQP中的交叉绑定提供了这一功能)。
|
||||
|
||||
![](img/fig11-1.png)
|
||||
|
||||
@ -542,21 +542,21 @@ CEP的实现包括Esper【69】,IBM InfoSphere Streams【70】,Apama,TIBCO
|
||||
|
||||
当你知道如何确定一个事件的时间戳后,下一步就是如何定义时间段的窗口。然后窗口就可以用于聚合,例如事件计数,或计算窗口内值的平均值。有几种窗口很常用【79,83】:
|
||||
|
||||
***滚动窗口(Tumbling Window)***
|
||||
* 滚动窗口(Tumbling Window)
|
||||
|
||||
滚动窗口有着固定的长度,每个事件都仅能属于一个窗口。例如,假设你有一个1分钟的滚动窗口,则所有时间戳在`10:03:00`和`10:03:59`之间的事件会被分组到一个窗口中,`10:04:00`和`10:04:59`之间的事件被分组到下一个窗口,依此类推。通过将每个事件时间戳四舍五入至最近的分钟来确定它所属的窗口,可以实现1分钟的滚动窗口。
|
||||
滚动窗口有着固定的长度,每个事件都仅能属于一个窗口。例如,假设你有一个1分钟的滚动窗口,则所有时间戳在`10:03:00`和`10:03:59`之间的事件会被分组到一个窗口中,`10:04:00`和`10:04:59`之间的事件被分组到下一个窗口,依此类推。通过将每个事件时间戳四舍五入至最近的分钟来确定它所属的窗口,可以实现1分钟的滚动窗口。
|
||||
|
||||
***跳动窗口(Hopping Window)***
|
||||
* 跳动窗口(Hopping Window)
|
||||
|
||||
跳动窗口也有着固定的长度,但允许窗口重叠以提供一些平滑。例如,一个带有1分钟跳跃步长的5分钟窗口将包含`10:03:00`至`10:07:59`之间的事件,而下一个窗口将覆盖`10:04:00`至`10:08:59`之间的事件,等等。通过首先计算1分钟的滚动窗口(tunmbling window),然后在几个相邻窗口上进行聚合,可以实现这种跳动窗口。
|
||||
跳动窗口也有着固定的长度,但允许窗口重叠以提供一些平滑。例如,一个带有1分钟跳跃步长的5分钟窗口将包含`10:03:00`至`10:07:59`之间的事件,而下一个窗口将覆盖`10:04:00`至`10:08:59`之间的事件,等等。通过首先计算1分钟的滚动窗口(tunmbling window),然后在几个相邻窗口上进行聚合,可以实现这种跳动窗口。
|
||||
|
||||
***滑动窗口(Sliding Window)***
|
||||
* 滑动窗口(Sliding Window)
|
||||
|
||||
滑动窗口包含了彼此间距在特定时长内的所有事件。例如,一个5分钟的滑动窗口应当覆盖`10:03:39`和`10:08:12`的事件,因为它们相距不超过5分钟(注意滚动窗口与步长5分钟的跳动窗口可能不会把这两个事件分组到同一个窗口中,因为它们使用固定的边界)。通过维护一个按时间排序的事件缓冲区,并不断从窗口中移除过期的旧事件,可以实现滑动窗口。
|
||||
滑动窗口包含了彼此间距在特定时长内的所有事件。例如,一个5分钟的滑动窗口应当覆盖`10:03:39`和`10:08:12`的事件,因为它们相距不超过5分钟(注意滚动窗口与步长5分钟的跳动窗口可能不会把这两个事件分组到同一个窗口中,因为它们使用固定的边界)。通过维护一个按时间排序的事件缓冲区,并不断从窗口中移除过期的旧事件,可以实现滑动窗口。
|
||||
|
||||
***会话窗口(Session window)***
|
||||
* 会话窗口(Session window)
|
||||
|
||||
与其他窗口类型不同,会话窗口没有固定的持续时间,而定义为:将同一用户出现时间相近的所有事件分组在一起,而当用户一段时间没有活动时(例如,如果30分钟内没有事件)窗口结束。会话切分是网站分析的常见需求(请参阅“[分组](ch10.md#分组)”)。
|
||||
与其他窗口类型不同,会话窗口没有固定的持续时间,而定义为:将同一用户出现时间相近的所有事件分组在一起,而当用户一段时间没有活动时(例如,如果30分钟内没有事件)窗口结束。会话切分是网站分析的常见需求(请参阅“[分组](ch10.md#分组)”)。
|
||||
|
||||
### 流连接
|
||||
|
||||
@ -684,13 +684,13 @@ Storm的Trident基于类似的想法来处理状态【78】。依赖幂等性意
|
||||
|
||||
我们花了一些时间比较两种消息代理:
|
||||
|
||||
***AMQP/JMS风格的消息代理***
|
||||
* AMQP/JMS风格的消息代理
|
||||
|
||||
代理将单条消息分配给消费者,消费者在成功处理单条消息后确认消息。消息被确认后从代理中删除。这种方法适合作为一种异步形式的RPC(另请参阅“[消息传递中的数据流](ch4.md#消息传递中的数据流)”),例如在任务队列中,消息处理的确切顺序并不重要,而且消息在处理完之后,不需要回头重新读取旧消息。
|
||||
代理将单条消息分配给消费者,消费者在成功处理单条消息后确认消息。消息被确认后从代理中删除。这种方法适合作为一种异步形式的RPC(另请参阅“[消息传递中的数据流](ch4.md#消息传递中的数据流)”),例如在任务队列中,消息处理的确切顺序并不重要,而且消息在处理完之后,不需要回头重新读取旧消息。
|
||||
|
||||
***基于日志的消息代理***
|
||||
* 基于日志的消息代理
|
||||
|
||||
代理将一个分区中的所有消息分配给同一个消费者节点,并始终以相同的顺序传递消息。并行是通过分区实现的,消费者通过存档最近处理消息的偏移量来跟踪工作进度。消息代理将消息保留在磁盘上,因此如有必要的话,可以回跳并重新读取旧消息。
|
||||
代理将一个分区中的所有消息分配给同一个消费者节点,并始终以相同的顺序传递消息。并行是通过分区实现的,消费者通过存档最近处理消息的偏移量来跟踪工作进度。消息代理将消息保留在磁盘上,因此如有必要的话,可以回跳并重新读取旧消息。
|
||||
|
||||
基于日志的方法与数据库中的复制日志(请参阅[第五章](ch5.md))和日志结构存储引擎(请参阅[第三章](ch3.md))有相似之处。我们看到,这种方法对于消费输入流,并产生衍生状态或衍生输出数据流的系统而言特别适用。
|
||||
|
||||
@ -704,17 +704,17 @@ Storm的Trident基于类似的想法来处理状态【78】。依赖幂等性意
|
||||
|
||||
我们区分了流处理中可能出现的三种连接类型:
|
||||
|
||||
***流流连接***
|
||||
* 流流连接
|
||||
|
||||
两个输入流都由活动事件组成,而连接算子在某个时间窗口内搜索相关的事件。例如,它可能会将同一个用户30分钟内进行的两个活动联系在一起。如果你想要找出一个流内的相关事件,连接的两侧输入可能实际上都是同一个流(**自连接(self-join)**)。
|
||||
两个输入流都由活动事件组成,而连接算子在某个时间窗口内搜索相关的事件。例如,它可能会将同一个用户30分钟内进行的两个活动联系在一起。如果你想要找出一个流内的相关事件,连接的两侧输入可能实际上都是同一个流(**自连接(self-join)**)。
|
||||
|
||||
***流表连接***
|
||||
* 流表连接
|
||||
|
||||
一个输入流由活动事件组成,另一个输入流是数据库变更日志。变更日志保证了数据库的本地副本是最新的。对于每个活动事件,连接算子将查询数据库,并输出一个扩展的活动事件。
|
||||
一个输入流由活动事件组成,另一个输入流是数据库变更日志。变更日志保证了数据库的本地副本是最新的。对于每个活动事件,连接算子将查询数据库,并输出一个扩展的活动事件。
|
||||
|
||||
***表表连接***
|
||||
* 表表连接
|
||||
|
||||
两个输入流都是数据库变更日志。在这种情况下,一侧的每一个变化都与另一侧的最新状态相连接。结果是两表连接所得物化视图的变更流。
|
||||
两个输入流都是数据库变更日志。在这种情况下,一侧的每一个变化都与另一侧的最新状态相连接。结果是两表连接所得物化视图的变更流。
|
||||
|
||||
最后,我们讨论了在流处理中实现容错和恰好一次语义的技术。与批处理一样,我们需要放弃任何失败任务的部分输出。然而由于流处理长时间运行并持续产生输出,所以不能简单地丢弃所有的输出。相反,可以使用更细粒度的恢复机制,基于微批次、存档点、事务或幂等写入。
|
||||
|
||||
|
12
ch12.md
12
ch12.md
@ -575,17 +575,17 @@ COMMIT;
|
||||
|
||||
更一般地来讲,我认为术语**一致性(consistency)** 这个术语混淆了两个值得分别考虑的需求:
|
||||
|
||||
***及时性(Timeliness)***
|
||||
* 及时性(Timeliness)
|
||||
|
||||
及时性意味着确保用户观察到系统的最新状态。我们之前看到,如果用户从陈旧的数据副本中读取数据,它们可能会观察到系统处于不一致的状态(请参阅“[复制延迟问题](ch5.md#复制延迟问题)”)。但这种不一致是暂时的,而最终会通过等待与重试简单地得到解决。
|
||||
及时性意味着确保用户观察到系统的最新状态。我们之前看到,如果用户从陈旧的数据副本中读取数据,它们可能会观察到系统处于不一致的状态(请参阅“[复制延迟问题](ch5.md#复制延迟问题)”)。但这种不一致是暂时的,而最终会通过等待与重试简单地得到解决。
|
||||
|
||||
CAP定理(请参阅“[线性一致性的代价](ch9.md#线性一致性的代价)”)使用**线性一致性(linearizability)** 意义上的一致性,这是实现及时性的强有力方法。像**写后读**这样及时性更弱的一致性也很有用(请参阅“[读己之写](ch5.md#读己之写)”)也很有用。
|
||||
CAP定理(请参阅“[线性一致性的代价](ch9.md#线性一致性的代价)”)使用**线性一致性(linearizability)** 意义上的一致性,这是实现及时性的强有力方法。像**写后读**这样及时性更弱的一致性也很有用(请参阅“[读己之写](ch5.md#读己之写)”)也很有用。
|
||||
|
||||
***完整性(Integrity)***
|
||||
* 完整性(Integrity)
|
||||
|
||||
完整性意味着没有损坏;即没有数据丢失,并且没有矛盾或错误的数据。尤其是如果某些衍生数据集是作为底层数据之上的视图而维护的(请参阅“[从事件日志中派生出当前状态](ch11.md#从事件日志中派生出当前状态)”),这种衍生必须是正确的。例如,数据库索引必须正确地反映数据库的内容 —— 缺失某些记录的索引并不是很有用。
|
||||
完整性意味着没有损坏;即没有数据丢失,并且没有矛盾或错误的数据。尤其是如果某些衍生数据集是作为底层数据之上的视图而维护的(请参阅“[从事件日志中派生出当前状态](ch11.md#从事件日志中派生出当前状态)”),这种衍生必须是正确的。例如,数据库索引必须正确地反映数据库的内容 —— 缺失某些记录的索引并不是很有用。
|
||||
|
||||
如果完整性被违背,这种不一致是永久的:在大多数情况下,等待与重试并不能修复数据库损坏。相反的是,需要显式地检查与修复。在ACID事务的上下文中(请参阅“[ACID的含义](ch7.md#ACID的含义)”),一致性通常被理解为某种特定于应用的完整性概念。原子性和持久性是保持完整性的重要工具。
|
||||
如果完整性被违背,这种不一致是永久的:在大多数情况下,等待与重试并不能修复数据库损坏。相反的是,需要显式地检查与修复。在ACID事务的上下文中(请参阅“[ACID的含义](ch7.md#ACID的含义)”),一致性通常被理解为某种特定于应用的完整性概念。原子性和持久性是保持完整性的重要工具。
|
||||
|
||||
|
||||
口号形式:违反及时性,“最终一致性”;违反完整性,“永无一致性”。
|
||||
|
20
ch2.md
20
ch2.md
@ -154,13 +154,13 @@ JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的**局部
|
||||
|
||||
此外,即便应用程序的最初版本适合无连接的文档模型,随着功能添加到应用程序中,数据会变得更加互联。例如,考虑一下对简历例子进行的一些修改:
|
||||
|
||||
***组织和学校作为实体***
|
||||
* 组织和学校作为实体
|
||||
|
||||
在前面的描述中,`organization`(用户工作的公司)和`school_name`(他们学习的地方)只是字符串。也许他们应该是对实体的引用呢?然后,每个组织,学校或大学都可以拥有自己的网页(标识,新闻提要等)。每个简历可以链接到它所提到的组织和学校,并且包括他们的图标和其他信息(请参阅[图2-3](img/fig2-3.png),来自LinkedIn的一个例子)。
|
||||
在前面的描述中,`organization`(用户工作的公司)和`school_name`(他们学习的地方)只是字符串。也许他们应该是对实体的引用呢?然后,每个组织,学校或大学都可以拥有自己的网页(标识,新闻提要等)。每个简历可以链接到它所提到的组织和学校,并且包括他们的图标和其他信息(请参阅[图2-3](img/fig2-3.png),来自LinkedIn的一个例子)。
|
||||
|
||||
***推荐***
|
||||
* 推荐
|
||||
|
||||
假设你想添加一个新的功能:一个用户可以为另一个用户写一个推荐。在用户的简历上显示推荐,并附上推荐用户的姓名和照片。如果推荐人更新他们的照片,那他们写的任何推荐都需要显示新的照片。因此,推荐应该拥有作者个人简介的引用。
|
||||
假设你想添加一个新的功能:一个用户可以为另一个用户写一个推荐。在用户的简历上显示推荐,并附上推荐用户的姓名和照片。如果推荐人更新他们的照片,那他们写的任何推荐都需要显示新的照片。因此,推荐应该拥有作者个人简介的引用。
|
||||
|
||||
![](img/fig2-3.png)
|
||||
|
||||
@ -515,17 +515,17 @@ db.observations.aggregate([
|
||||
|
||||
一个图由两种对象组成:**顶点(vertices)**(也称为**节点(nodes)** 或**实体(entities)**),和**边(edges)**( 也称为**关系(relationships)** 或**弧 (arcs)** )。多种数据可以被建模为一个图形。典型的例子包括:
|
||||
|
||||
***社交图谱***
|
||||
* 社交图谱
|
||||
|
||||
顶点是人,边指示哪些人彼此认识。
|
||||
顶点是人,边指示哪些人彼此认识。
|
||||
|
||||
***网络图谱***
|
||||
* 网络图谱
|
||||
|
||||
顶点是网页,边缘表示指向其他页面的HTML链接。
|
||||
顶点是网页,边缘表示指向其他页面的HTML链接。
|
||||
|
||||
***公路或铁路网络***
|
||||
* 公路或铁路网络
|
||||
|
||||
顶点是交叉路口,边线代表它们之间的道路或铁路线。
|
||||
顶点是交叉路口,边线代表它们之间的道路或铁路线。
|
||||
|
||||
可以将那些众所周知的算法运用到这些图上:例如,汽车导航系统搜索道路网络中两点之间的最短路径,PageRank可以用在网络图上来确定网页的流行程度,从而确定该网页在搜索结果中的排名。
|
||||
|
||||
|
20
ch3.md
20
ch3.md
@ -108,25 +108,25 @@ $ cat database
|
||||
|
||||
要让这个简单的想法在实际中能工作会涉及到大量的细节。简单来说,下面几点都是实现过程中需要认真考虑的问题:
|
||||
|
||||
***文件格式***
|
||||
* 文件格式
|
||||
|
||||
CSV不是日志的最佳格式。使用二进制格式更快,更简单:首先以字节为单位对字符串的长度进行编码,然后是原始的字符串(不需要转义)。
|
||||
CSV不是日志的最佳格式。使用二进制格式更快,更简单:首先以字节为单位对字符串的长度进行编码,然后是原始的字符串(不需要转义)。
|
||||
|
||||
***删除记录***
|
||||
* 删除记录
|
||||
|
||||
如果要删除一个键及其关联的值,则必须在数据文件中追加一个特殊的删除记录(有时称为逻辑删除(tombstone))。当日志段被合并时,逻辑删除告诉合并过程丢弃被删除键的任何以前的值。
|
||||
如果要删除一个键及其关联的值,则必须在数据文件中追加一个特殊的删除记录(有时称为逻辑删除(tombstone))。当日志段被合并时,逻辑删除告诉合并过程丢弃被删除键的任何以前的值。
|
||||
|
||||
***崩溃恢复***
|
||||
* 崩溃恢复
|
||||
|
||||
如果数据库重新启动,则内存散列映射将丢失。原则上,你可以通过从头到尾读取整个段文件并记录下来每个键的最近值来恢复每个段的散列映射。但是,如果段文件很大,可能需要很长时间,这会使服务的重启比较痛苦。 Bitcask 通过将每个段的散列映射的快照存储在硬盘上来加速恢复,可以使散列映射更快地加载到内存中。
|
||||
如果数据库重新启动,则内存散列映射将丢失。原则上,你可以通过从头到尾读取整个段文件并记录下来每个键的最近值来恢复每个段的散列映射。但是,如果段文件很大,可能需要很长时间,这会使服务的重启比较痛苦。 Bitcask 通过将每个段的散列映射的快照存储在硬盘上来加速恢复,可以使散列映射更快地加载到内存中。
|
||||
|
||||
***部分写入记录***
|
||||
* 部分写入记录
|
||||
|
||||
数据库随时可能崩溃,包括在将记录追加到日志的过程中。 Bitcask文件包含校验和,允许检测和忽略日志中的这些损坏部分。
|
||||
数据库随时可能崩溃,包括在将记录追加到日志的过程中。 Bitcask文件包含校验和,允许检测和忽略日志中的这些损坏部分。
|
||||
|
||||
***并发控制***
|
||||
* 并发控制
|
||||
|
||||
由于写操作是以严格的顺序追加到日志中的,所以常见的实现是只有一个写入线程。也因为数据文件段是仅追加的或者说是不可变的,所以它们可以被多个线程同时读取。
|
||||
由于写操作是以严格的顺序追加到日志中的,所以常见的实现是只有一个写入线程。也因为数据文件段是仅追加的或者说是不可变的,所以它们可以被多个线程同时读取。
|
||||
|
||||
乍一看,仅追加日志似乎很浪费:为什么不直接在文件里更新,用新值覆盖旧值?仅追加的设计之所以是个好的设计,有如下几个原因:
|
||||
|
||||
|
8
ch4.md
8
ch4.md
@ -24,13 +24,13 @@
|
||||
|
||||
这意味着,新旧版本的代码,以及新旧数据格式可能会在系统中同时共处。系统想要继续顺利运行,就需要保持**双向兼容性**:
|
||||
|
||||
***向后兼容 (backward compatibility)***
|
||||
* 向后兼容 (backward compatibility)
|
||||
|
||||
新代码可以读旧数据。
|
||||
新代码可以读旧数据。
|
||||
|
||||
***向前兼容 (forward compatibility)***
|
||||
* 向前兼容 (forward compatibility)
|
||||
|
||||
旧代码可以读新数据。
|
||||
旧代码可以读新数据。
|
||||
|
||||
向后兼容性通常并不难实现:新代码的作者当然知道由旧代码使用的数据格式,因此可以显示地处理它(最简单的办法是,保留旧代码即可读取旧数据)。
|
||||
|
||||
|
68
ch5.md
68
ch5.md
@ -315,17 +315,17 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
|
||||
|
||||
我们来比较一下在运维多个数据中心时,单主和多主的适应情况。
|
||||
|
||||
***性能***
|
||||
* 性能
|
||||
|
||||
在单主配置中,每个写入都必须穿过互联网,进入主库所在的数据中心。这可能会增加写入时间,并可能违背了设置多个数据中心的初心。在多主配置中,每个写操作都可以在本地数据中心进行处理,并与其他数据中心异步复制。因此,数据中心之间的网络延迟对用户来说是透明的,这意味着感觉到的性能可能会更好。
|
||||
在单主配置中,每个写入都必须穿过互联网,进入主库所在的数据中心。这可能会增加写入时间,并可能违背了设置多个数据中心的初心。在多主配置中,每个写操作都可以在本地数据中心进行处理,并与其他数据中心异步复制。因此,数据中心之间的网络延迟对用户来说是透明的,这意味着感觉到的性能可能会更好。
|
||||
|
||||
***容忍数据中心停机***
|
||||
* 容忍数据中心停机
|
||||
|
||||
在单主配置中,如果主库所在的数据中心发生故障,故障切换必须使另一个数据中心里的追随者成为领导者。在多主配置中,每个数据中心可以独立于其他数据中心继续运行,并且当发生故障的数据中心归队时,复制会自动赶上。
|
||||
在单主配置中,如果主库所在的数据中心发生故障,故障切换必须使另一个数据中心里的追随者成为领导者。在多主配置中,每个数据中心可以独立于其他数据中心继续运行,并且当发生故障的数据中心归队时,复制会自动赶上。
|
||||
|
||||
***容忍网络问题***
|
||||
* 容忍网络问题
|
||||
|
||||
数据中心之间的通信通常穿过公共互联网,这可能不如数据中心内的本地网络可靠。单主配置对这数据中心间的连接问题非常敏感,因为通过这个连接进行的写操作是同步的。采用异步复制功能的多主配置通常能更好地承受网络问题:临时的网络中断并不会妨碍正在处理的写入。
|
||||
数据中心之间的通信通常穿过公共互联网,这可能不如数据中心内的本地网络可靠。单主配置对这数据中心间的连接问题非常敏感,因为通过这个连接进行的写操作是同步的。采用异步复制功能的多主配置通常能更好地承受网络问题:临时的网络中断并不会妨碍正在处理的写入。
|
||||
|
||||
有些数据库默认情况下支持多主配置,但使用外部工具实现也很常见,例如用于MySQL的Tungsten Replicator 【26】,用于PostgreSQL的BDR【27】以及用于Oracle的GoldenGate 【19】。
|
||||
|
||||
@ -397,13 +397,13 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
|
||||
|
||||
作为解决冲突最合适的方法可能取决于应用程序,大多数多主复制工具允许使用应用程序代码编写冲突解决逻辑。该代码可以在写入或读取时执行:
|
||||
|
||||
***写时执行***
|
||||
* 写时执行
|
||||
|
||||
只要数据库系统检测到复制更改日志中存在冲突,就会调用冲突处理程序。例如,Bucardo允许你为此编写一段Perl代码。这个处理程序通常不能提示用户——它在后台进程中运行,并且必须快速执行。
|
||||
只要数据库系统检测到复制更改日志中存在冲突,就会调用冲突处理程序。例如,Bucardo允许你为此编写一段Perl代码。这个处理程序通常不能提示用户——它在后台进程中运行,并且必须快速执行。
|
||||
|
||||
***读时执行***
|
||||
* 读时执行
|
||||
|
||||
当检测到冲突时,所有冲突写入被存储。下一次读取数据时,会将这些多个版本的数据返回给应用程序。应用程序可能会提示用户或自动解决冲突,并将结果写回数据库。例如,CouchDB以这种方式工作。
|
||||
当检测到冲突时,所有冲突写入被存储。下一次读取数据时,会将这些多个版本的数据返回给应用程序。应用程序可能会提示用户或自动解决冲突,并将结果写回数据库。例如,CouchDB以这种方式工作。
|
||||
|
||||
请注意,冲突解决通常适用于单个行或文档层面,而不是整个事务【36】。因此,如果你有一个事务会原子性地进行几次不同的写入(请参阅[第七章](ch7.md),对于冲突解决而言,每个写入仍需分开单独考虑。
|
||||
|
||||
@ -492,13 +492,13 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
|
||||
|
||||
在Dynamo风格的数据存储中经常使用两种机制:
|
||||
|
||||
***读修复(Read repair)***
|
||||
* 读修复(Read repair)
|
||||
|
||||
当客户端并行读取多个节点时,它可以检测到任何陈旧的响应。例如,在[图5-10](img/fig5-10.png)中,用户2345获得了来自副本3的版本6值和来自副本1和2的版本7值。客户端发现副本3具有陈旧值,并将新值写回到该副本。这种方法适用于读频繁的值。
|
||||
当客户端并行读取多个节点时,它可以检测到任何陈旧的响应。例如,在[图5-10](img/fig5-10.png)中,用户2345获得了来自副本3的版本6值和来自副本1和2的版本7值。客户端发现副本3具有陈旧值,并将新值写回到该副本。这种方法适用于读频繁的值。
|
||||
|
||||
***反熵过程(Anti-entropy process)***
|
||||
* 反熵过程(Anti-entropy process)
|
||||
|
||||
此外,一些数据存储具有后台进程,该进程不断查找副本之间的数据差异,并将任何缺少的数据从一个副本复制到另一个副本。与基于领导者的复制中的复制日志不同,此反熵过程不会以任何特定的顺序复制写入,并且在复制数据之前可能会有显著的延迟。
|
||||
此外,一些数据存储具有后台进程,该进程不断查找副本之间的数据差异,并将任何缺少的数据从一个副本复制到另一个副本。与基于领导者的复制中的复制日志不同,此反熵过程不会以任何特定的顺序复制写入,并且在复制数据之前可能会有显著的延迟。
|
||||
|
||||
并不是所有的系统都实现了这两个,例如,Voldemort目前没有反熵过程。请注意,如果没有反熵过程,某些副本中很少读取的值可能会丢失,从而降低了持久性,因为只有在应用程序读取值时才执行读修复。
|
||||
|
||||
@ -713,38 +713,38 @@ LWW实现了最终收敛的目标,但以**持久性**为代价:如果同一
|
||||
|
||||
在本章中,我们考察了复制的问题。复制可以用于几个目的:
|
||||
|
||||
***高可用性***
|
||||
* 高可用性
|
||||
|
||||
即使在一台机器(或多台机器,或整个数据中心)停机的情况下也能保持系统正常运行
|
||||
即使在一台机器(或多台机器,或整个数据中心)停机的情况下也能保持系统正常运行
|
||||
|
||||
***断开连接的操作***
|
||||
* 断开连接的操作
|
||||
|
||||
允许应用程序在网络中断时继续工作
|
||||
允许应用程序在网络中断时继续工作
|
||||
|
||||
***延迟***
|
||||
* 延迟
|
||||
|
||||
将数据放置在距离用户较近的地方,以便用户能够更快地与其交互
|
||||
将数据放置在距离用户较近的地方,以便用户能够更快地与其交互
|
||||
|
||||
***可伸缩性***
|
||||
* 可伸缩性
|
||||
|
||||
通过在副本上读,能够处理比单机更大的读取量
|
||||
通过在副本上读,能够处理比单机更大的读取量
|
||||
|
||||
|
||||
尽管是一个简单的目标 - 在几台机器上保留相同数据的副本,但复制却是一个非常棘手的问题。它需要仔细考虑并发和所有可能出错的事情,并处理这些故障的后果。至少,我们需要处理不可用的节点和网络中断(这还不包括更隐蔽的故障,例如由于软件错误导致的静默数据损坏)。
|
||||
|
||||
我们讨论了复制的三种主要方法:
|
||||
|
||||
***单主复制***
|
||||
* 单主复制
|
||||
|
||||
客户端将所有写入操作发送到单个节点(领导者),该节点将数据更改事件流发送到其他副本(追随者)。读取可以在任何副本上执行,但从追随者读取可能是陈旧的。
|
||||
客户端将所有写入操作发送到单个节点(领导者),该节点将数据更改事件流发送到其他副本(追随者)。读取可以在任何副本上执行,但从追随者读取可能是陈旧的。
|
||||
|
||||
***多主复制***
|
||||
* 多主复制
|
||||
|
||||
客户端发送每个写入到几个领导节点之一,其中任何一个都可以接受写入。领导者将数据更改事件流发送给彼此以及任何跟随者节点。
|
||||
客户端发送每个写入到几个领导节点之一,其中任何一个都可以接受写入。领导者将数据更改事件流发送给彼此以及任何跟随者节点。
|
||||
|
||||
***无主复制***
|
||||
* 无主复制
|
||||
|
||||
客户端发送每个写入到几个节点,并从多个节点并行读取,以检测和纠正具有陈旧数据的节点。
|
||||
客户端发送每个写入到几个节点,并从多个节点并行读取,以检测和纠正具有陈旧数据的节点。
|
||||
|
||||
每种方法都有优点和缺点。单主复制是非常流行的,因为它很容易理解,不需要担心冲突解决。在出现故障节点,网络中断和延迟峰值的情况下,多领导者和无领导者复制可以更加稳健,但以更难以推理并仅提供非常弱的一致性保证为代价。
|
||||
|
||||
@ -752,17 +752,17 @@ LWW实现了最终收敛的目标,但以**持久性**为代价:如果同一
|
||||
|
||||
我们研究了一些可能由复制滞后引起的奇怪效应,我们也讨论了一些有助于决定应用程序在复制滞后时的行为的一致性模型:
|
||||
|
||||
***写后读***
|
||||
* 写后读
|
||||
|
||||
用户应该总是看到自己提交的数据。
|
||||
用户应该总是看到自己提交的数据。
|
||||
|
||||
***单调读***
|
||||
* 单调读
|
||||
|
||||
用户在看到某一个时间点的数据后,他们不应该再看到某个更早时间点的数据。
|
||||
用户在看到某一个时间点的数据后,他们不应该再看到某个更早时间点的数据。
|
||||
|
||||
***一致前缀读***
|
||||
* 一致前缀读
|
||||
|
||||
用户应该看到数据处于一种具有因果意义的状态:例如,按正确的顺序看到一个问题和对应的回答。
|
||||
用户应该看到数据处于一种具有因果意义的状态:例如,按正确的顺序看到一个问题和对应的回答。
|
||||
|
||||
最后,我们讨论了多领导者和无领导者复制方法所固有的并发问题:因为他们允许多个写入并发发生,这可能会导致冲突。我们研究了一个数据库可能使用的算法来确定一个操作是否发生在另一个操作之前,或者它们是否同时发生。我们还谈到了通过合并并发更新来解决冲突的方法。
|
||||
|
||||
|
13
ch6.md
13
ch6.md
@ -298,18 +298,17 @@ Couchbase不会自动重新平衡,这简化了设计。通常情况下,它
|
||||
|
||||
我们讨论了两种主要的分区方法:
|
||||
|
||||
***键范围分区***
|
||||
* 键范围分区
|
||||
|
||||
其中键是有序的,并且分区拥有从某个最小值到某个最大值的所有键。排序的优势在于可以进行有效的范围查询,但是如果应用程序经常访问相邻的键,则存在热点的风险。
|
||||
其中键是有序的,并且分区拥有从某个最小值到某个最大值的所有键。排序的优势在于可以进行有效的范围查询,但是如果应用程序经常访问相邻的键,则存在热点的风险。
|
||||
|
||||
在这种方法中,当分区变得太大时,通常将分区分成两个子分区,动态地再平衡分区。
|
||||
在这种方法中,当分区变得太大时,通常将分区分成两个子分区,动态地再平衡分区。
|
||||
|
||||
***散列分区***
|
||||
* 散列分区
|
||||
|
||||
散列函数应用于每个键,分区拥有一定范围的散列。这种方法破坏了键的排序,使得范围查询效率低下,但可以更均匀地分配负载。
|
||||
|
||||
通过散列进行分区时,通常先提前创建固定数量的分区,为每个节点分配多个分区,并在添加或删除节点时将整个分区从一个节点移动到另一个节点。也可以使用动态分区。
|
||||
散列函数应用于每个键,分区拥有一定范围的散列。这种方法破坏了键的排序,使得范围查询效率低下,但可以更均匀地分配负载。
|
||||
|
||||
通过散列进行分区时,通常先提前创建固定数量的分区,为每个节点分配多个分区,并在添加或删除节点时将整个分区从一个节点移动到另一个节点。也可以使用动态分区。
|
||||
|
||||
两种方法搭配使用也是可行的,例如使用复合主键:使用键的一部分来标识分区,而使用另一部分作为排序顺序。
|
||||
|
||||
|
92
ch7.md
92
ch7.md
@ -128,13 +128,13 @@ ACID意义上的隔离性意味着,**同时执行的事务是相互隔离的**
|
||||
|
||||
回顾一下,在ACID中,原子性和隔离性描述了客户端在同一事务中执行多次写入时,数据库应该做的事情:
|
||||
|
||||
***原子性***
|
||||
* 原子性
|
||||
|
||||
如果在一系列写操作的中途发生错误,则应中止事务处理,并丢弃当前事务的所有写入。换句话说,数据库免去了用户对部分失败的担忧——通过提供“**宁为玉碎,不为瓦全(all-or-nothing)**”的保证。
|
||||
如果在一系列写操作的中途发生错误,则应中止事务处理,并丢弃当前事务的所有写入。换句话说,数据库免去了用户对部分失败的担忧——通过提供“**宁为玉碎,不为瓦全(all-or-nothing)**”的保证。
|
||||
|
||||
***隔离性***
|
||||
* 隔离性
|
||||
|
||||
同时运行的事务不应该互相干扰。例如,如果一个事务进行多次写入,则另一个事务要么看到全部写入结果,要么什么都看不到,但不应该是一些子集。
|
||||
同时运行的事务不应该互相干扰。例如,如果一个事务进行多次写入,则另一个事务要么看到全部写入结果,要么什么都看不到,但不应该是一些子集。
|
||||
|
||||
这些定义假设你想同时修改多个对象(行,文档,记录)。通常需要**多对象事务(multi-object transaction)** 来保持多块数据同步。[图7-2](img/fig7-2.png)展示了一个来自电邮应用的例子。执行以下查询来显示用户未读邮件数量:
|
||||
|
||||
@ -301,13 +301,13 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
对于Alice的情况,这不是一个长期持续的问题。因为如果她几秒钟后刷新银行网站的页面,她很可能会看到一致的帐户余额。但是有些情况下,不能容忍这种暂时的不一致:
|
||||
|
||||
***备份***
|
||||
* 备份
|
||||
|
||||
进行备份需要复制整个数据库,对大型数据库而言可能需要花费数小时才能完成。备份进程运行时,数据库仍然会接受写入操作。因此备份可能会包含一些旧的部分和一些新的部分。如果从这样的备份中恢复,那么不一致(如消失的钱)就会变成永久的。
|
||||
进行备份需要复制整个数据库,对大型数据库而言可能需要花费数小时才能完成。备份进程运行时,数据库仍然会接受写入操作。因此备份可能会包含一些旧的部分和一些新的部分。如果从这样的备份中恢复,那么不一致(如消失的钱)就会变成永久的。
|
||||
|
||||
***分析查询和完整性检查***
|
||||
* 分析查询和完整性检查
|
||||
|
||||
有时,你可能需要运行一个查询,扫描大部分的数据库。这样的查询在分析中很常见(请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”),也可能是定期完整性检查(即监视数据损坏)的一部分。如果这些查询在不同时间点观察数据库的不同部分,则可能会返回毫无意义的结果。
|
||||
有时,你可能需要运行一个查询,扫描大部分的数据库。这样的查询在分析中很常见(请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”),也可能是定期完整性检查(即监视数据损坏)的一部分。如果这些查询在不同时间点观察数据库的不同部分,则可能会返回毫无意义的结果。
|
||||
|
||||
**快照隔离(snapshot isolation)**【28】是这个问题最常见的解决方案。想法是,每个事务都从数据库的**一致快照(consistent snapshot)** 中读取——也就是说,事务可以看到事务开始时在数据库中提交的所有数据。即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。
|
||||
|
||||
@ -511,42 +511,42 @@ COMMIT;
|
||||
|
||||
写偏差乍看像是一个深奥的问题,但一旦意识到这一点,很容易会注意到更多可能的情况。以下是一些例子:
|
||||
|
||||
***会议室预订系统***
|
||||
* 会议室预订系统
|
||||
|
||||
比如你想要规定不能在同一时间对同一个会议室进行多次的预订【43】。当有人想要预订时,首先检查是否存在相互冲突的预订(即预订时间范围重叠的同一房间),如果没有找到,则创建会议(请参阅示例7-2)[^ix]。
|
||||
比如你想要规定不能在同一时间对同一个会议室进行多次的预订【43】。当有人想要预订时,首先检查是否存在相互冲突的预订(即预订时间范围重叠的同一房间),如果没有找到,则创建会议(请参阅示例7-2)[^ix]。
|
||||
|
||||
[^ix]: 在PostgreSQL中,你可以使用范围类型优雅地执行此操作,但在其他数据库中并未得到广泛支持。
|
||||
[^ix]: 在PostgreSQL中,你可以使用范围类型优雅地执行此操作,但在其他数据库中并未得到广泛支持。
|
||||
|
||||
**例7-2 会议室预订系统试图避免重复预订(在快照隔离下不安全)**
|
||||
**例7-2 会议室预订系统试图避免重复预订(在快照隔离下不安全)**
|
||||
|
||||
```sql
|
||||
BEGIN TRANSACTION;
|
||||
```sql
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- 检查所有现存的与12:00~13:00重叠的预定
|
||||
SELECT COUNT(*) FROM bookings
|
||||
WHERE room_id = 123 AND
|
||||
-- 检查所有现存的与12:00~13:00重叠的预定
|
||||
SELECT COUNT(*) FROM bookings
|
||||
WHERE room_id = 123 AND
|
||||
end_time > '2015-01-01 12:00' AND start_time < '2015-01-01 13:00';
|
||||
|
||||
-- 如果之前的查询返回0
|
||||
INSERT INTO bookings(room_id, start_time, end_time, user_id)
|
||||
-- 如果之前的查询返回0
|
||||
INSERT INTO bookings(room_id, start_time, end_time, user_id)
|
||||
VALUES (123, '2015-01-01 12:00', '2015-01-01 13:00', 666);
|
||||
|
||||
COMMIT;
|
||||
```
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
不幸的是,快照隔离并不能防止另一个用户同时插入冲突的会议。为了确保不会遇到调度冲突,你又需要可串行化的隔离级别了。
|
||||
不幸的是,快照隔离并不能防止另一个用户同时插入冲突的会议。为了确保不会遇到调度冲突,你又需要可串行化的隔离级别了。
|
||||
|
||||
***多人游戏***
|
||||
* 多人游戏
|
||||
|
||||
在[例7-1]()中,我们使用一个锁来防止丢失更新(也就是确保两个玩家不能同时移动同一个棋子)。但是锁定并不妨碍玩家将两个不同的棋子移动到棋盘上的相同位置,或者采取其他违反游戏规则的行为。按照你正在执行的规则类型,也许可以使用唯一约束(unique constraint),否则你很容易发生写入偏差。
|
||||
在[例7-1]()中,我们使用一个锁来防止丢失更新(也就是确保两个玩家不能同时移动同一个棋子)。但是锁定并不妨碍玩家将两个不同的棋子移动到棋盘上的相同位置,或者采取其他违反游戏规则的行为。按照你正在执行的规则类型,也许可以使用唯一约束(unique constraint),否则你很容易发生写入偏差。
|
||||
|
||||
***抢注用户名***
|
||||
* 抢注用户名
|
||||
|
||||
在每个用户拥有唯一用户名的网站上,两个用户可能会尝试同时创建具有相同用户名的帐户。可以在事务检查名称是否被抢占,如果没有则使用该名称创建账户。但是像在前面的例子中那样,在快照隔离下这是不安全的。幸运的是,唯一约束是一个简单的解决办法(第二个事务在提交时会因为违反用户名唯一约束而被中止)。
|
||||
在每个用户拥有唯一用户名的网站上,两个用户可能会尝试同时创建具有相同用户名的帐户。可以在事务检查名称是否被抢占,如果没有则使用该名称创建账户。但是像在前面的例子中那样,在快照隔离下这是不安全的。幸运的是,唯一约束是一个简单的解决办法(第二个事务在提交时会因为违反用户名唯一约束而被中止)。
|
||||
|
||||
***防止双重开支***
|
||||
* 防止双重开支
|
||||
|
||||
允许用户花钱或积分的服务,需要检查用户的支付数额不超过其余额。可以通过在用户的帐户中插入一个试探性的消费项目来实现这一点,列出帐户中的所有项目,并检查总和是否为正值【44】。有了写入偏差,可能会发生两个支出项目同时插入,一起导致余额变为负值,但这两个事务都不会注意到另一个。
|
||||
允许用户花钱或积分的服务,需要检查用户的支付数额不超过其余额。可以通过在用户的帐户中插入一个试探性的消费项目来实现这一点,列出帐户中的所有项目,并检查总和是否为正值【44】。有了写入偏差,可能会发生两个支出项目同时插入,一起导致余额变为负值,但这两个事务都不会注意到另一个。
|
||||
|
||||
#### 导致写入偏差的幻读
|
||||
|
||||
@ -833,43 +833,43 @@ WHERE room_id = 123 AND
|
||||
|
||||
本章深入讨论了**并发控制**的话题。我们讨论了几个广泛使用的隔离级别,特别是**读已提交**,**快照隔离**(有时称为可重复读)和**可串行化**。并通过研究竞争条件的各种例子,来描述这些隔离等级:
|
||||
|
||||
***脏读***
|
||||
* 脏读
|
||||
|
||||
一个客户端读取到另一个客户端尚未提交的写入。**读已提交**或更强的隔离级别可以防止脏读。
|
||||
一个客户端读取到另一个客户端尚未提交的写入。**读已提交**或更强的隔离级别可以防止脏读。
|
||||
|
||||
***脏写***
|
||||
* 脏写
|
||||
|
||||
一个客户端覆盖写入了另一个客户端尚未提交的写入。几乎所有的事务实现都可以防止脏写。
|
||||
一个客户端覆盖写入了另一个客户端尚未提交的写入。几乎所有的事务实现都可以防止脏写。
|
||||
|
||||
***读取偏差(不可重复读)***
|
||||
* 读取偏差(不可重复读)
|
||||
|
||||
在同一个事务中,客户端在不同的时间点会看见数据库的不同状态。**快照隔离**经常用于解决这个问题,它允许事务从一个特定时间点的一致性快照中读取数据。快照隔离通常使用**多版本并发控制(MVCC)** 来实现。
|
||||
在同一个事务中,客户端在不同的时间点会看见数据库的不同状态。**快照隔离**经常用于解决这个问题,它允许事务从一个特定时间点的一致性快照中读取数据。快照隔离通常使用**多版本并发控制(MVCC)** 来实现。
|
||||
|
||||
***更新丢失***
|
||||
* 更新丢失
|
||||
|
||||
两个客户端同时执行**读取-修改-写入序列**。其中一个写操作,在没有合并另一个写入变更情况下,直接覆盖了另一个写操作的结果。所以导致数据丢失。快照隔离的一些实现可以自动防止这种异常,而另一些实现则需要手动锁定(`SELECT FOR UPDATE`)。
|
||||
两个客户端同时执行**读取-修改-写入序列**。其中一个写操作,在没有合并另一个写入变更情况下,直接覆盖了另一个写操作的结果。所以导致数据丢失。快照隔离的一些实现可以自动防止这种异常,而另一些实现则需要手动锁定(`SELECT FOR UPDATE`)。
|
||||
|
||||
***写偏差***
|
||||
* 写偏差
|
||||
|
||||
一个事务读取一些东西,根据它所看到的值作出决定,并将该决定写入数据库。但是,写入时,该决定的前提不再是真实的。只有可串行化的隔离才能防止这种异常。
|
||||
一个事务读取一些东西,根据它所看到的值作出决定,并将该决定写入数据库。但是,写入时,该决定的前提不再是真实的。只有可串行化的隔离才能防止这种异常。
|
||||
|
||||
***幻读***
|
||||
* 幻读
|
||||
|
||||
事务读取符合某些搜索条件的对象。另一个客户端进行写入,影响搜索结果。快照隔离可以防止直接的幻像读取,但是写入偏差上下文中的幻读需要特殊处理,例如索引范围锁定。
|
||||
事务读取符合某些搜索条件的对象。另一个客户端进行写入,影响搜索结果。快照隔离可以防止直接的幻像读取,但是写入偏差上下文中的幻读需要特殊处理,例如索引范围锁定。
|
||||
|
||||
弱隔离级别可以防止其中一些异常情况,但要求你,也就是应用程序开发人员手动处理剩余那些(例如,使用显式锁定)。只有可串行化的隔离才能防范所有这些问题。我们讨论了实现可串行化事务的三种不同方法:
|
||||
|
||||
***字面意义上的串行执行***
|
||||
* 字面意义上的串行执行
|
||||
|
||||
如果每个事务的执行速度非常快,并且事务吞吐量足够低,足以在单个CPU核上处理,这是一个简单而有效的选择。
|
||||
如果每个事务的执行速度非常快,并且事务吞吐量足够低,足以在单个CPU核上处理,这是一个简单而有效的选择。
|
||||
|
||||
***两阶段锁定***
|
||||
* 两阶段锁定
|
||||
|
||||
数十年来,两阶段锁定一直是实现可串行化的标准方式,但是许多应用出于性能问题的考虑避免使用它。
|
||||
数十年来,两阶段锁定一直是实现可串行化的标准方式,但是许多应用出于性能问题的考虑避免使用它。
|
||||
|
||||
***可串行化快照隔离(SSI)***
|
||||
* 可串行化快照隔离(SSI)
|
||||
|
||||
一个相当新的算法,避免了先前方法的大部分缺点。它使用乐观的方法,允许事务执行而无需阻塞。当一个事务想要提交时,它会进行检查,如果执行不可串行化,事务就会被中止。
|
||||
一个相当新的算法,避免了先前方法的大部分缺点。它使用乐观的方法,允许事务执行而无需阻塞。当一个事务想要提交时,它会进行检查,如果执行不可串行化,事务就会被中止。
|
||||
|
||||
本章中的示例主要是在关系数据模型的上下文中。但是,正如在讨论中,无论使用哪种数据模型,如“**[多对象事务的需求](#多对象事务的需求)**”中所讨论的,事务都是有价值的数据库功能。
|
||||
|
||||
|
36
ch8.md
36
ch8.md
@ -549,32 +549,32 @@ Web应用程序确实需要预期受终端用户控制的客户端(如Web浏
|
||||
|
||||
关于时序假设,三种系统模型是常用的:
|
||||
|
||||
***同步模型***
|
||||
* 同步模型
|
||||
|
||||
**同步模型(synchronous model)** 假设网络延迟、进程暂停和和时钟误差都是受限的。这并不意味着完全同步的时钟或零网络延迟;这只意味着你知道网络延迟、暂停和时钟漂移将永远不会超过某个固定的上限【88】。同步模型并不是大多数实际系统的现实模型,因为(如本章所讨论的)无限延迟和暂停确实会发生。
|
||||
**同步模型(synchronous model)** 假设网络延迟、进程暂停和和时钟误差都是受限的。这并不意味着完全同步的时钟或零网络延迟;这只意味着你知道网络延迟、暂停和时钟漂移将永远不会超过某个固定的上限【88】。同步模型并不是大多数实际系统的现实模型,因为(如本章所讨论的)无限延迟和暂停确实会发生。
|
||||
|
||||
***部分同步模型***
|
||||
* 部分同步模型
|
||||
|
||||
**部分同步(partial synchronous)** 意味着一个系统在大多数情况下像一个同步系统一样运行,但有时候会超出网络延迟,进程暂停和时钟漂移的界限【88】。这是很多系统的现实模型:大多数情况下,网络和进程表现良好,否则我们永远无法完成任何事情,但是我们必须承认,在任何时刻都存在时序假设偶然被破坏的事实。发生这种情况时,网络延迟、暂停和时钟错误可能会变得相当大。
|
||||
**部分同步(partial synchronous)** 意味着一个系统在大多数情况下像一个同步系统一样运行,但有时候会超出网络延迟,进程暂停和时钟漂移的界限【88】。这是很多系统的现实模型:大多数情况下,网络和进程表现良好,否则我们永远无法完成任何事情,但是我们必须承认,在任何时刻都存在时序假设偶然被破坏的事实。发生这种情况时,网络延迟、暂停和时钟错误可能会变得相当大。
|
||||
|
||||
***异步模型***
|
||||
* 异步模型
|
||||
|
||||
在这个模型中,一个算法不允许对时序做任何假设——事实上它甚至没有时钟(所以它不能使用超时)。一些算法被设计为可用于异步模型,但非常受限。
|
||||
在这个模型中,一个算法不允许对时序做任何假设——事实上它甚至没有时钟(所以它不能使用超时)。一些算法被设计为可用于异步模型,但非常受限。
|
||||
|
||||
|
||||
进一步来说,除了时序问题,我们还要考虑**节点失效**。三种最常见的节点系统模型是:
|
||||
|
||||
***崩溃-停止故障***
|
||||
* 崩溃-停止故障
|
||||
|
||||
在**崩溃停止(crash-stop)** 模型中,算法可能会假设一个节点只能以一种方式失效,即通过崩溃。这意味着节点可能在任意时刻突然停止响应,此后该节点永远消失——它永远不会回来。
|
||||
在**崩溃停止(crash-stop)** 模型中,算法可能会假设一个节点只能以一种方式失效,即通过崩溃。这意味着节点可能在任意时刻突然停止响应,此后该节点永远消失——它永远不会回来。
|
||||
|
||||
***崩溃-恢复故障***
|
||||
* 崩溃-恢复故障
|
||||
|
||||
我们假设节点可能会在任何时候崩溃,但也许会在未知的时间之后再次开始响应。在**崩溃-恢复(crash-recovery)** 模型中,假设节点具有稳定的存储(即,非易失性磁盘存储)且会在崩溃中保留,而内存中的状态会丢失。
|
||||
我们假设节点可能会在任何时候崩溃,但也许会在未知的时间之后再次开始响应。在**崩溃-恢复(crash-recovery)** 模型中,假设节点具有稳定的存储(即,非易失性磁盘存储)且会在崩溃中保留,而内存中的状态会丢失。
|
||||
|
||||
***拜占庭(任意)故障***
|
||||
* 拜占庭(任意)故障
|
||||
|
||||
节点可以做(绝对意义上的)任何事情,包括试图戏弄和欺骗其他节点,如上一节所述。
|
||||
节点可以做(绝对意义上的)任何事情,包括试图戏弄和欺骗其他节点,如上一节所述。
|
||||
|
||||
对于真实系统的建模,具有**崩溃-恢复故障(crash-recovery)** 的**部分同步模型(partial synchronous)** 通常是最有用的模型。分布式算法如何应对这种模型?
|
||||
|
||||
@ -584,17 +584,17 @@ Web应用程序确实需要预期受终端用户控制的客户端(如Web浏
|
||||
|
||||
同样,我们可以写下我们想要的分布式算法的属性来定义它的正确含义。例如,如果我们正在为一个锁生成防护令牌(请参阅“[防护令牌](#防护令牌)”),我们可能要求算法具有以下属性:
|
||||
|
||||
***唯一性(uniqueness)***
|
||||
* 唯一性(uniqueness)
|
||||
|
||||
没有两个防护令牌请求返回相同的值。
|
||||
没有两个防护令牌请求返回相同的值。
|
||||
|
||||
***单调序列(monotonic sequence)***
|
||||
* 单调序列(monotonic sequence)
|
||||
|
||||
如果请求 $x$ 返回了令牌 $t_x$,并且请求$y$返回了令牌$t_y$,并且 $x$ 在 $y$ 开始之前已经完成,那么$t_x <t_y$。
|
||||
如果请求 $x$ 返回了令牌 $t_x$,并且请求$y$返回了令牌$t_y$,并且 $x$ 在 $y$ 开始之前已经完成,那么$t_x <t_y$。
|
||||
|
||||
***可用性(availability)***
|
||||
* 可用性(availability)
|
||||
|
||||
请求防护令牌并且不会崩溃的节点,最终会收到响应。
|
||||
请求防护令牌并且不会崩溃的节点,最终会收到响应。
|
||||
|
||||
如果一个系统模型中的算法总是满足它在所有我们假设可能发生的情况下的性质,那么这个算法是正确的。但这如何有意义?如果所有的节点崩溃,或者所有的网络延迟突然变得无限长,那么没有任何算法能够完成任何事情。
|
||||
|
||||
|
113
ch9.md
113
ch9.md
@ -197,27 +197,27 @@
|
||||
|
||||
使系统容错最常用的方法是使用复制。我们再来回顾[第五章](ch5.md)中的复制方法,并比较它们是否可以满足线性一致性:
|
||||
|
||||
***单主复制(可能线性一致)***
|
||||
* 单主复制(可能线性一致)
|
||||
|
||||
在具有单主复制功能的系统中(请参阅“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能(potential)** 是线性一致性的[^iv]。然而,实际上并不是每个单主数据库都是线性一致性的,无论是因为设计的原因(例如,因为使用了快照隔离)还是因为在并发处理上存在错误【10】。
|
||||
在具有单主复制功能的系统中(请参阅“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能(potential)** 是线性一致性的[^iv]。然而,实际上并不是每个单主数据库都是线性一致性的,无论是因为设计的原因(例如,因为使用了快照隔离)还是因为在并发处理上存在错误【10】。
|
||||
|
||||
[^iv]: 对单主数据库进行分区(分片),使得每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。 交叉分区事务是一个不同的问题(请参阅“[分布式事务与共识](#分布式事务与共识)”)。
|
||||
[^iv]: 对单主数据库进行分区(分片),使得每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。 交叉分区事务是一个不同的问题(请参阅“[分布式事务与共识](#分布式事务与共识)”)。
|
||||
|
||||
从主库读取依赖一个假设,你确定地知道领导者是谁。正如在“[真相由多数所定义](ch8.md#真相由多数所定义)”中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此——如果具有错觉的领导者继续为请求提供服务,可能违反线性一致性【20】。使用异步复制,故障切换时甚至可能会丢失已提交的写入(请参阅“[处理节点宕机](ch5.md#处理节点宕机)”),这同时违反了持久性和线性一致性。
|
||||
从主库读取依赖一个假设,你确定地知道领导者是谁。正如在“[真相由多数所定义](ch8.md#真相由多数所定义)”中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此——如果具有错觉的领导者继续为请求提供服务,可能违反线性一致性【20】。使用异步复制,故障切换时甚至可能会丢失已提交的写入(请参阅“[处理节点宕机](ch5.md#处理节点宕机)”),这同时违反了持久性和线性一致性。
|
||||
|
||||
***共识算法(线性一致)***
|
||||
* 共识算法(线性一致)
|
||||
|
||||
一些在本章后面讨论的共识算法,与单领导者复制类似。然而,共识协议包含防止脑裂和陈旧副本的措施。正是由于这些细节,共识算法可以安全地实现线性一致性存储。例如,Zookeeper 【21】和etcd 【22】就是这样工作的。
|
||||
一些在本章后面讨论的共识算法,与单领导者复制类似。然而,共识协议包含防止脑裂和陈旧副本的措施。正是由于这些细节,共识算法可以安全地实现线性一致性存储。例如,Zookeeper 【21】和etcd 【22】就是这样工作的。
|
||||
|
||||
***多主复制(非线性一致)***
|
||||
* 多主复制(非线性一致)
|
||||
|
||||
具有多主程序复制的系统通常不是线性一致的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。因此,它们可能会产生需要被解决的写入冲突(请参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。这种冲突是因为缺少单一数据副本所导致的。
|
||||
具有多主程序复制的系统通常不是线性一致的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。因此,它们可能会产生需要被解决的写入冲突(请参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。这种冲突是因为缺少单一数据副本所导致的。
|
||||
|
||||
***无主复制(也许不是线性一致的)***
|
||||
* 无主复制(也许不是线性一致的)
|
||||
|
||||
对于无领导者复制的系统(Dynamo风格;请参阅“[无主复制](ch5.md#无主复制)”),有时候人们会声称通过要求法定人数读写( $w + r> n$ )可以获得“强一致性”。这取决于法定人数的具体配置,以及强一致性如何定义(通常不完全正确)。
|
||||
对于无领导者复制的系统(Dynamo风格;请参阅“[无主复制](ch5.md#无主复制)”),有时候人们会声称通过要求法定人数读写( $w + r> n$ )可以获得“强一致性”。这取决于法定人数的具体配置,以及强一致性如何定义(通常不完全正确)。
|
||||
|
||||
基于日历时钟(例如,在Cassandra中;请参阅“[依赖同步时钟](ch8.md#依赖同步时钟)”)的“最后写入胜利”冲突解决方法几乎可以确定是非线性一致的,由于时钟偏差,不能保证时钟的时间戳与实际事件顺序一致。宽松的法定人数(请参阅“[宽松的法定人数与提示移交](ch5.md#宽松的法定人数与提示移交)”)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也只是可能的,如下节所示。
|
||||
基于日历时钟(例如,在Cassandra中;请参阅“[依赖同步时钟](ch8.md#依赖同步时钟)”)的“最后写入胜利”冲突解决方法几乎可以确定是非线性一致的,由于时钟偏差,不能保证时钟的时间戳与实际事件顺序一致。宽松的法定人数(请参阅“[宽松的法定人数与提示移交](ch5.md#宽松的法定人数与提示移交)”)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也只是可能的,如下节所示。
|
||||
|
||||
#### 线性一致性和法定人数
|
||||
|
||||
@ -336,13 +336,13 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
全序和偏序之间的差异反映在不同的数据库一致性模型中:
|
||||
|
||||
***线性一致性***
|
||||
* 线性一致性
|
||||
|
||||
在线性一致的系统中,操作是全序的:如果系统表现的就好像只有一个数据副本,并且所有操作都是原子性的,这意味着对任何两个操作,我们总是能判定哪个操作先发生。这个全序在[图9-4](img/fig9-4.png)中以时间线表示。
|
||||
在线性一致的系统中,操作是全序的:如果系统表现的就好像只有一个数据副本,并且所有操作都是原子性的,这意味着对任何两个操作,我们总是能判定哪个操作先发生。这个全序在[图9-4](img/fig9-4.png)中以时间线表示。
|
||||
|
||||
***因果性***
|
||||
* 因果性
|
||||
|
||||
我们说过,如果两个操作都没有在彼此**之前发生**,那么这两个操作是并发的(请参阅[“此前发生”的关系和并发](ch5.md#“此前发生”的关系和并发))。换句话说,如果两个事件是因果相关的(一个发生在另一个事件之前),则它们之间是有序的,但如果它们是并发的,则它们之间的顺序是无法比较的。这意味着因果关系定义了一个偏序,而不是一个全序:一些操作相互之间是有顺序的,但有些则是无法比较的。
|
||||
我们说过,如果两个操作都没有在彼此**之前发生**,那么这两个操作是并发的(请参阅[“此前发生”的关系和并发](ch5.md#“此前发生”的关系和并发))。换句话说,如果两个事件是因果相关的(一个发生在另一个事件之前),则它们之间是有序的,但如果它们是并发的,则它们之间的顺序是无法比较的。这意味着因果关系定义了一个偏序,而不是一个全序:一些操作相互之间是有顺序的,但有些则是无法比较的。
|
||||
|
||||
因此,根据这个定义,在线性一致的数据存储中是不存在并发操作的:必须有且仅有一条时间线,所有的操作都在这条时间线上,构成一个全序关系。可能有几个请求在等待处理,但是数据存储确保了每个请求都是在唯一时间线上的某个时间点自动处理的,不存在任何并发。
|
||||
|
||||
@ -463,13 +463,13 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
全序广播通常被描述为在节点间交换消息的协议。 非正式地讲,它要满足两个安全属性:
|
||||
|
||||
***可靠交付(reliable delivery)***
|
||||
* 可靠交付(reliable delivery)
|
||||
|
||||
没有消息丢失:如果消息被传递到一个节点,它将被传递到所有节点。
|
||||
没有消息丢失:如果消息被传递到一个节点,它将被传递到所有节点。
|
||||
|
||||
***全序交付(totally ordered delivery)***
|
||||
* 全序交付(totally ordered delivery)
|
||||
|
||||
消息以相同的顺序传递给每个节点。
|
||||
消息以相同的顺序传递给每个节点。
|
||||
|
||||
正确的全序广播算法必须始终保证可靠性和有序性,即使节点或网络出现故障。当然在网络中断的时候,消息是传不出去的,但是算法可以不断重试,以便在网络最终修复时,消息能及时通过并送达(当然它们必须仍然按照正确的顺序传递)。
|
||||
|
||||
@ -540,16 +540,15 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
节点能达成一致,在很多场景下都非常重要,例如:
|
||||
|
||||
***领导选举***
|
||||
* 领导选举
|
||||
|
||||
在单主复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会对领导权的归属引起争议。在这种情况下,共识对于避免错误的故障切换非常重要。错误的故障切换会导致两个节点都认为自己是领导者(**脑裂**,请参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。如果有两个领导者,它们都会接受写入,它们的数据会发生分歧,从而导致不一致和数据丢失。
|
||||
在单主复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会对领导权的归属引起争议。在这种情况下,共识对于避免错误的故障切换非常重要。错误的故障切换会导致两个节点都认为自己是领导者(**脑裂**,请参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。如果有两个领导者,它们都会接受写入,它们的数据会发生分歧,从而导致不一致和数据丢失。
|
||||
|
||||
***原子提交***
|
||||
* 原子提交
|
||||
|
||||
在支持跨多节点或跨多分区事务的数据库中,一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就ACID而言,请参阅“[原子性](ch7.md#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为**原子提交(atomic commit)** 问题[^xii]。
|
||||
在支持跨多节点或跨多分区事务的数据库中,一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就ACID而言,请参阅“[原子性](ch7.md#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为**原子提交(atomic commit)** 问题[^xii]。
|
||||
|
||||
|
||||
[^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在**所有**参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 共识则允许就**任意一个**被参与者提出的候选值达成一致。 然而,原子提交和共识可以相互简化为对方【70,71】。 **非阻塞**原子提交则要比共识更为困难 —— 请参阅“[三阶段提交](#三阶段提交)”。
|
||||
[^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在**所有**参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 共识则允许就**任意一个**被参与者提出的候选值达成一致。 然而,原子提交和共识可以相互简化为对方【70,71】。 **非阻塞**原子提交则要比共识更为困难 —— 请参阅“[三阶段提交](#三阶段提交)”。
|
||||
|
||||
> ### 共识的不可能性
|
||||
>
|
||||
@ -662,13 +661,13 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
|
||||
但我们不应该直接忽视分布式事务,而应当更加仔细地审视这些事务,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明“**分布式事务**”的含义。两种截然不同的分布式事务类型经常被混淆:
|
||||
|
||||
***数据库内部的分布式事务***
|
||||
* 数据库内部的分布式事务
|
||||
|
||||
一些分布式数据库(即在其标准配置中使用复制和分区的数据库)支持数据库节点之间的内部事务。例如,VoltDB和MySQL Cluster的NDB存储引擎就有这样的内部事务支持。在这种情况下,所有参与事务的节点都运行相同的数据库软件。
|
||||
一些分布式数据库(即在其标准配置中使用复制和分区的数据库)支持数据库节点之间的内部事务。例如,VoltDB和MySQL Cluster的NDB存储引擎就有这样的内部事务支持。在这种情况下,所有参与事务的节点都运行相同的数据库软件。
|
||||
|
||||
***异构分布式事务***
|
||||
* 异构分布式事务
|
||||
|
||||
在**异构(heterogeneous)** 事务中,参与者是由两种或两种以上的不同技术组成的:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。
|
||||
在**异构(heterogeneous)** 事务中,参与者是由两种或两种以上的不同技术组成的:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。
|
||||
|
||||
数据库内部事务不必与任何其他系统兼容,因此它们可以使用任何协议,并能针对特定技术进行特定的优化。因此数据库内部的分布式事务通常工作地很好。另一方面,跨异构技术的事务则更有挑战性。
|
||||
|
||||
@ -736,21 +735,21 @@ XA事务解决了保持多个参与者(数据系统)相互一致的现实的
|
||||
|
||||
[^xiii]: 这种共识的特殊形式被称为**统一共识(uniform consensus)**,相当于在具有不可靠故障检测器的异步系统中的**常规共识(regular consensus)**【71】。学术文献通常指的是**进程(process)** 而不是节点,但我们在这里使用**节点(node)** 来与本书的其余部分保持一致。
|
||||
|
||||
***一致同意(Uniform agreement)***
|
||||
* 一致同意(Uniform agreement)
|
||||
|
||||
没有两个节点的决定不同。
|
||||
没有两个节点的决定不同。
|
||||
|
||||
***完整性(Integrity)***
|
||||
* 完整性(Integrity)
|
||||
|
||||
没有节点决定两次。
|
||||
没有节点决定两次。
|
||||
|
||||
***有效性(Validity)***
|
||||
* 有效性(Validity)
|
||||
|
||||
如果一个节点决定了值 `v` ,则 `v` 由某个节点所提议。
|
||||
如果一个节点决定了值 `v` ,则 `v` 由某个节点所提议。
|
||||
|
||||
***终止(Termination)***
|
||||
* 终止(Termination)
|
||||
|
||||
由所有未崩溃的节点来最终决定值。
|
||||
由所有未崩溃的节点来最终决定值。
|
||||
|
||||
**一致同意**和**完整性**属性定义了共识的核心思想:所有人都决定了相同的结果,一旦决定了,你就不能改变主意。**有效性**属性主要是为了排除平凡的解决方案:例如,无论提议了什么值,你都可以有一个始终决定值为`null`的算法。;该算法满足**一致同意**和**完整性**属性,但不满足**有效性**属性。
|
||||
|
||||
@ -835,21 +834,21 @@ ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据(
|
||||
|
||||
ZooKeeper模仿了Google的Chubby锁服务【14,98】,不仅实现了全序广播(因此也实现了共识),而且还构建了一组有趣的其他特性,这些特性在构建分布式系统时变得特别有用:
|
||||
|
||||
***线性一致性的原子操作***
|
||||
* 线性一致性的原子操作
|
||||
|
||||
使用原子CAS操作可以实现锁:如果多个节点同时尝试执行相同的操作,只有一个节点会成功。共识协议保证了操作的原子性和线性一致性,即使节点发生故障或网络在任意时刻中断。分布式锁通常以**租约(lease)** 的形式实现,租约有一个到期时间,以便在客户端失效的情况下最终能被释放(请参阅“[进程暂停](ch8.md#进程暂停)”)。
|
||||
使用原子CAS操作可以实现锁:如果多个节点同时尝试执行相同的操作,只有一个节点会成功。共识协议保证了操作的原子性和线性一致性,即使节点发生故障或网络在任意时刻中断。分布式锁通常以**租约(lease)** 的形式实现,租约有一个到期时间,以便在客户端失效的情况下最终能被释放(请参阅“[进程暂停](ch8.md#进程暂停)”)。
|
||||
|
||||
***操作的全序排序***
|
||||
* 操作的全序排序
|
||||
|
||||
如“[领导者和锁](ch8.md#领导者和锁)”中所述,当某个资源受到锁或租约的保护时,你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。防护令牌是每次锁被获取时单调增加的数字。 ZooKeeper通过全序化所有操作来提供这个功能,它为每个操作提供一个单调递增的事务ID(`zxid`)和版本号(`cversion`)【15】。
|
||||
如“[领导者和锁](ch8.md#领导者和锁)”中所述,当某个资源受到锁或租约的保护时,你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。防护令牌是每次锁被获取时单调增加的数字。 ZooKeeper通过全序化所有操作来提供这个功能,它为每个操作提供一个单调递增的事务ID(`zxid`)和版本号(`cversion`)【15】。
|
||||
|
||||
***失效检测***
|
||||
* 失效检测
|
||||
|
||||
客户端在ZooKeeper服务器上维护一个长期会话,客户端和服务器周期性地交换心跳包来检查节点是否还活着。即使连接暂时中断,或者ZooKeeper节点失效,会话仍保持在活跃状态。但如果心跳停止的持续时间超出会话超时,ZooKeeper会宣告该会话已死亡。当会话超时时(ZooKeeper称这些节点为**临时节点(ephemeral nodes)**),会话持有的任何锁都可以配置为自动释放。
|
||||
客户端在ZooKeeper服务器上维护一个长期会话,客户端和服务器周期性地交换心跳包来检查节点是否还活着。即使连接暂时中断,或者ZooKeeper节点失效,会话仍保持在活跃状态。但如果心跳停止的持续时间超出会话超时,ZooKeeper会宣告该会话已死亡。当会话超时时(ZooKeeper称这些节点为**临时节点(ephemeral nodes)**),会话持有的任何锁都可以配置为自动释放。
|
||||
|
||||
***变更通知***
|
||||
* 变更通知
|
||||
|
||||
客户端不仅可以读取其他客户端创建的锁和值,还可以监听它们的变更。因此,客户端可以知道另一个客户端何时加入集群(基于新客户端写入ZooKeeper的值),或发生故障(因其会话超时,而其临时节点消失)。通过订阅通知,客户端不用再通过频繁轮询的方式来找出变更。
|
||||
客户端不仅可以读取其他客户端创建的锁和值,还可以监听它们的变更。因此,客户端可以知道另一个客户端何时加入集群(基于新客户端写入ZooKeeper的值),或发生故障(因其会话超时,而其临时节点消失)。通过订阅通知,客户端不用再通过频繁轮询的方式来找出变更。
|
||||
|
||||
在这些功能中,只有线性一致的原子操作才真的需要共识。但正是这些功能的组合,使得像ZooKeeper这样的系统在分布式协调中非常有用。
|
||||
|
||||
@ -892,29 +891,29 @@ ZooKeeper和它的小伙伴们可以看作是成员资格服务(membership ser
|
||||
|
||||
我们看到,达成共识意味着以这样一种方式决定某件事:所有节点一致同意所做决定,且这一决定不可撤销。通过深入挖掘,结果我们发现很广泛的一系列问题实际上都可以归结为共识问题,并且彼此等价(从这个意义上来讲,如果你有其中之一的解决方案,就可以轻易将它转换为其他问题的解决方案)。这些等价的问题包括:
|
||||
|
||||
***线性一致性的CAS寄存器***
|
||||
* 线性一致性的CAS寄存器
|
||||
|
||||
寄存器需要基于当前值是否等于操作给出的参数,原子地**决定**是否设置新值。
|
||||
寄存器需要基于当前值是否等于操作给出的参数,原子地**决定**是否设置新值。
|
||||
|
||||
***原子事务提交***
|
||||
* 原子事务提交
|
||||
|
||||
数据库必须**决定**是否提交或中止分布式事务。
|
||||
数据库必须**决定**是否提交或中止分布式事务。
|
||||
|
||||
***全序广播***
|
||||
* 全序广播
|
||||
|
||||
消息系统必须**决定**传递消息的顺序。
|
||||
消息系统必须**决定**传递消息的顺序。
|
||||
|
||||
***锁和租约***
|
||||
* 锁和租约
|
||||
|
||||
当几个客户端争抢锁或租约时,由锁来**决定**哪个客户端成功获得锁。
|
||||
当几个客户端争抢锁或租约时,由锁来**决定**哪个客户端成功获得锁。
|
||||
|
||||
***成员/协调服务***
|
||||
* 成员/协调服务
|
||||
|
||||
给定某种故障检测器(例如超时),系统必须**决定**哪些节点活着,哪些节点因为会话超时需要被宣告死亡。
|
||||
给定某种故障检测器(例如超时),系统必须**决定**哪些节点活着,哪些节点因为会话超时需要被宣告死亡。
|
||||
|
||||
***唯一性约束***
|
||||
* 唯一性约束
|
||||
|
||||
当多个事务同时尝试使用相同的键创建冲突记录时,约束必须**决定**哪一个被允许,哪些因为违反约束而失败。
|
||||
当多个事务同时尝试使用相同的键创建冲突记录时,约束必须**决定**哪一个被允许,哪些因为违反约束而失败。
|
||||
|
||||
如果你只有一个节点,或者你愿意将决策的权能分配给单个节点,所有这些事都很简单。这就是在单领导者数据库中发生的事情:所有决策权归属于领导者,这就是为什么这样的数据库能够提供线性一致的操作,唯一性约束,完全有序的复制日志,以及更多。
|
||||
|
||||
|
20
part-ii.md
20
part-ii.md
@ -11,17 +11,17 @@
|
||||
|
||||
你可能会出于各种各样的原因,希望将数据库分布到多台机器上:
|
||||
|
||||
***可伸缩性***
|
||||
* 可伸缩性
|
||||
|
||||
如果你的数据量、读取负载、写入负载超出单台机器的处理能力,可以将负载分散到多台计算机上。
|
||||
如果你的数据量、读取负载、写入负载超出单台机器的处理能力,可以将负载分散到多台计算机上。
|
||||
|
||||
***容错/高可用性***
|
||||
* 容错/高可用性
|
||||
|
||||
如果你的应用需要在单台机器(或多台机器,网络或整个数据中心)出现故障的情况下仍然能继续工作,则可使用多台机器,以提供冗余。一台故障时,另一台可以接管。
|
||||
如果你的应用需要在单台机器(或多台机器,网络或整个数据中心)出现故障的情况下仍然能继续工作,则可使用多台机器,以提供冗余。一台故障时,另一台可以接管。
|
||||
|
||||
***延迟***
|
||||
* 延迟
|
||||
|
||||
如果在世界各地都有用户,你也许会考虑在全球范围部署多个服务器,从而每个用户可以从地理上最近的数据中心获取服务,避免了等待网络数据包穿越半个世界。
|
||||
如果在世界各地都有用户,你也许会考虑在全球范围部署多个服务器,从而每个用户可以从地理上最近的数据中心获取服务,避免了等待网络数据包穿越半个世界。
|
||||
|
||||
## 伸缩至更高的载荷
|
||||
|
||||
@ -51,13 +51,13 @@
|
||||
|
||||
数据分布在多个节点上有两种常见的方式:
|
||||
|
||||
***复制(Replication)***
|
||||
* 复制(Replication)
|
||||
|
||||
在几个不同的节点上保存数据的相同副本,可能放在不同的位置。 复制提供了冗余:如果一些节点不可用,剩余的节点仍然可以提供数据服务。 复制也有助于改善性能。 [第五章](ch5.md)将讨论复制。
|
||||
在几个不同的节点上保存数据的相同副本,可能放在不同的位置。 复制提供了冗余:如果一些节点不可用,剩余的节点仍然可以提供数据服务。 复制也有助于改善性能。 [第五章](ch5.md)将讨论复制。
|
||||
|
||||
***分区 (Partitioning)***
|
||||
* 分区 (Partitioning)
|
||||
|
||||
将一个大型数据库拆分成较小的子集(称为**分区(partitions)**),从而不同的分区可以指派给不同的**节点(node)**(亦称**分片(shard)**)。 [第六章](ch6.md)将讨论分区。
|
||||
将一个大型数据库拆分成较小的子集(称为**分区(partitions)**),从而不同的分区可以指派给不同的**节点(node)**(亦称**分片(shard)**)。 [第六章](ch6.md)将讨论分区。
|
||||
|
||||
复制和分区是不同的机制,但它们经常同时使用。如[图II-1](img/figii-1.png)所示。
|
||||
|
||||
|
@ -10,13 +10,13 @@
|
||||
|
||||
从高层次上看,存储和处理数据的系统可以分为两大类:
|
||||
|
||||
***记录系统(System of record)***
|
||||
* 记录系统(System of record)
|
||||
|
||||
**记录系统**,也被称为**真相源(source of truth)**,持有数据的权威版本。当新的数据进入时(例如,用户输入)首先会记录在这里。每个事实正正好好表示一次(表示通常是**正规化的(normalized)**)。如果其他系统和**记录系统**之间存在任何差异,那么记录系统中的值是正确的(根据定义)。
|
||||
**记录系统**,也被称为**真相源(source of truth)**,持有数据的权威版本。当新的数据进入时(例如,用户输入)首先会记录在这里。每个事实正正好好表示一次(表示通常是**正规化的(normalized)**)。如果其他系统和**记录系统**之间存在任何差异,那么记录系统中的值是正确的(根据定义)。
|
||||
|
||||
***衍生数据系统(Derived data systems)***
|
||||
* 衍生数据系统(Derived data systems)
|
||||
|
||||
**衍生系统**中的数据,通常是另一个系统中的现有数据以某种方式进行转换或处理的结果。如果丢失衍生数据,可以从原始来源重新创建。典型的例子是**缓存(cache)**:如果数据在缓存中,就可以由缓存提供服务;如果缓存不包含所需数据,则降级由底层数据库提供。非规范化的值,索引和物化视图亦属此类。在推荐系统中,预测汇总数据通常衍生自用户日志。
|
||||
**衍生系统**中的数据,通常是另一个系统中的现有数据以某种方式进行转换或处理的结果。如果丢失衍生数据,可以从原始来源重新创建。典型的例子是**缓存(cache)**:如果数据在缓存中,就可以由缓存提供服务;如果缓存不包含所需数据,则降级由底层数据库提供。非规范化的值,索引和物化视图亦属此类。在推荐系统中,预测汇总数据通常衍生自用户日志。
|
||||
|
||||
从技术上讲,衍生数据是**冗余的(redundant)**,因为它重复了已有的信息。但是衍生数据对于获得良好的只读查询性能通常是至关重要的。它通常是非规范化的。可以从单个源头衍生出多个不同的数据集,使你能从不同的“视角”洞察数据。
|
||||
|
||||
|
@ -149,6 +149,8 @@
|
||||
|
||||
| ISSUE & Pull Requests | USER | Title |
|
||||
| ----------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| [147](https://github.com/Vonng/ddia/pull/147) | [@ZvanYang](https://github.com/ZvanYang) | ch5: 更正一處不準確的翻譯 |
|
||||
| [145](https://github.com/Vonng/ddia/pull/145) | [@Hookey](https://github.com/Hookey) | 識別了當前簡繁轉譯過程中處理不當的地方,暫透過轉換指令碼規避 |
|
||||
| [144](https://github.com/Vonng/ddia/issues/144) | [@secret4233](https://github.com/secret4233) | ch7: 不翻譯`next-key locking` |
|
||||
| [143](https://github.com/Vonng/ddia/issues/143) | [@imcheney](https://github.com/imcheney) | ch3: 更新殘留的機翻段落 |
|
||||
| [142](https://github.com/Vonng/ddia/issues/142) | [@XIJINIAN](https://github.com/XIJINIAN) | 建議去除段首的製表符 |
|
||||
|
32
zh-tw/ch1.md
32
zh-tw/ch1.md
@ -53,17 +53,17 @@
|
||||
|
||||
本書著重討論三個在大多數軟體系統中都很重要的問題:
|
||||
|
||||
***可靠性(Reliability)***
|
||||
* 可靠性(Reliability)
|
||||
|
||||
系統在**困境(adversity)**(硬體故障、軟體故障、人為錯誤)中仍可正常工作(正確完成功能,並能達到期望的效能水準)。請參閱“[可靠性](#可靠性)”。
|
||||
系統在**困境(adversity)**(硬體故障、軟體故障、人為錯誤)中仍可正常工作(正確完成功能,並能達到期望的效能水準)。請參閱“[可靠性](#可靠性)”。
|
||||
|
||||
***可伸縮性(Scalability)***
|
||||
* 可伸縮性(Scalability)
|
||||
|
||||
有合理的辦法應對系統的增長(資料量、流量、複雜性)。請參閱“[可伸縮性](#可伸縮性)”。
|
||||
有合理的辦法應對系統的增長(資料量、流量、複雜性)。請參閱“[可伸縮性](#可伸縮性)”。
|
||||
|
||||
***可維護性(Maintainability)***
|
||||
* 可維護性(Maintainability)
|
||||
|
||||
許多不同的人(工程師、運維)在不同的生命週期,都能高效地在系統上工作(使系統保持現有行為,並適應新的應用場景)。請參閱“[可維護性](#可維護性)”。
|
||||
許多不同的人(工程師、運維)在不同的生命週期,都能高效地在系統上工作(使系統保持現有行為,並適應新的應用場景)。請參閱“[可維護性](#可維護性)”。
|
||||
|
||||
人們經常追求這些詞彙,卻沒有清楚理解它們到底意味著什麼。為了工程的嚴謹性,本章的剩餘部分將探討可靠性、可伸縮性、可維護性的含義。為實現這些目標而使用的各種技術,架構和演算法將在後續的章節中研究。
|
||||
|
||||
@ -152,13 +152,13 @@
|
||||
|
||||
為了使這個概念更加具體,我們以推特在2012年11月釋出的資料【16】為例。推特的兩個主要業務是:
|
||||
|
||||
***釋出推文***
|
||||
* 釋出推文
|
||||
|
||||
使用者可以向其粉絲釋出新訊息(平均 4.6k請求/秒,峰值超過 12k請求/秒)。
|
||||
使用者可以向其粉絲釋出新訊息(平均 4.6k請求/秒,峰值超過 12k請求/秒)。
|
||||
|
||||
***主頁時間線***
|
||||
* 主頁時間線
|
||||
|
||||
使用者可以查閱他們關注的人釋出的推文(300k請求/秒)。
|
||||
使用者可以查閱他們關注的人釋出的推文(300k請求/秒)。
|
||||
|
||||
處理每秒12,000次寫入(發推文的速率峰值)還是很簡單的。然而推特的伸縮性挑戰並不是主要來自推特量,而是來自**扇出(fan-out)**——每個使用者關注了很多人,也被很多人關注。
|
||||
|
||||
@ -279,17 +279,17 @@
|
||||
|
||||
但是我們可以,也應該以這樣一種方式來設計軟體:在設計之初就儘量考慮儘可能減少維護期間的痛苦,從而避免自己的軟體系統變成遺留系統。為此,我們將特別關注軟體系統的三個設計原則:
|
||||
|
||||
***可操作性(Operability)***
|
||||
* 可操作性(Operability)
|
||||
|
||||
便於運維團隊保持系統平穩執行。
|
||||
便於運維團隊保持系統平穩執行。
|
||||
|
||||
***簡單性(Simplicity)***
|
||||
* 簡單性(Simplicity)
|
||||
|
||||
從系統中消除儘可能多的**複雜度(complexity)**,使新工程師也能輕鬆理解系統。(注意這和使用者介面的簡單性不一樣。)
|
||||
從系統中消除儘可能多的**複雜度(complexity)**,使新工程師也能輕鬆理解系統。(注意這和使用者介面的簡單性不一樣。)
|
||||
|
||||
***可演化性(evolability)***
|
||||
* 可演化性(evolability)
|
||||
|
||||
使工程師在未來能輕鬆地對系統進行更改,當需求變化時為新應用場景做適配。也稱為**可伸縮性(extensibility)**,**可修改性(modifiability)** 或**可塑性(plasticity)**。
|
||||
使工程師在未來能輕鬆地對系統進行更改,當需求變化時為新應用場景做適配。也稱為**可伸縮性(extensibility)**,**可修改性(modifiability)** 或**可塑性(plasticity)**。
|
||||
|
||||
和之前提到的可靠性、可伸縮性一樣,實現這些目標也沒有簡單的解決方案。不過我們會試著想象具有可操作性,簡單性和可演化性的系統會是什麼樣子。
|
||||
|
||||
|
@ -16,17 +16,17 @@
|
||||
|
||||
Web和越來越多的基於HTTP/REST的API使互動的請求/響應風格變得如此普遍,以至於很容易將其視為理所當然。但我們應該記住,這不是構建系統的唯一方式,其他方法也有其優點。我們來看看三種不同型別的系統:
|
||||
|
||||
***服務(線上系統)***
|
||||
* 服務(線上系統)
|
||||
|
||||
服務等待客戶的請求或指令到達。每收到一個,服務會試圖儘快處理它,併發回一個響應。響應時間通常是服務效能的主要衡量指標,可用性通常非常重要(如果客戶端無法訪問服務,使用者可能會收到錯誤訊息)。
|
||||
服務等待客戶的請求或指令到達。每收到一個,服務會試圖儘快處理它,併發回一個響應。響應時間通常是服務效能的主要衡量指標,可用性通常非常重要(如果客戶端無法訪問服務,使用者可能會收到錯誤訊息)。
|
||||
|
||||
***批處理系統(離線系統)***
|
||||
* 批處理系統(離線系統)
|
||||
|
||||
一個批處理系統有大量的輸入資料,跑一個**作業(job)** 來處理它,並生成一些輸出資料,這往往需要一段時間(從幾分鐘到幾天),所以通常不會有使用者等待作業完成。相反,批次作業通常會定期執行(例如,每天一次)。批處理作業的主要效能衡量標準通常是吞吐量(處理特定大小的輸入所需的時間)。本章中討論的就是批處理。
|
||||
一個批處理系統有大量的輸入資料,跑一個**作業(job)** 來處理它,並生成一些輸出資料,這往往需要一段時間(從幾分鐘到幾天),所以通常不會有使用者等待作業完成。相反,批次作業通常會定期執行(例如,每天一次)。批處理作業的主要效能衡量標準通常是吞吐量(處理特定大小的輸入所需的時間)。本章中討論的就是批處理。
|
||||
|
||||
***流處理系統(準實時系統)***
|
||||
* 流處理系統(準實時系統)
|
||||
|
||||
流處理介於線上和離線(批處理)之間,所以有時候被稱為**準實時(near-real-time)** 或**準線上(nearline)** 處理。像批處理系統一樣,流處理消費輸入併產生輸出(並不需要響應請求)。但是,流式作業在事件發生後不久就會對事件進行操作,而批處理作業則需等待固定的一組輸入資料。這種差異使流處理系統比起批處理系統具有更低的延遲。由於流處理基於批處理,我們將在[第十一章](ch11.md)討論它。
|
||||
流處理介於線上和離線(批處理)之間,所以有時候被稱為**準實時(near-real-time)** 或**準線上(nearline)** 處理。像批處理系統一樣,流處理消費輸入併產生輸出(並不需要響應請求)。但是,流式作業在事件發生後不久就會對事件進行操作,而批處理作業則需等待固定的一組輸入資料。這種差異使流處理系統比起批處理系統具有更低的延遲。由於流處理基於批處理,我們將在[第十一章](ch11.md)討論它。
|
||||
|
||||
正如我們將在本章中看到的那樣,批處理是構建可靠、可伸縮和可維護應用程式的重要組成部分。例如,2004年釋出的批處理演算法Map-Reduce(可能被過分熱情地)被稱為“造就Google大規模可伸縮性的演算法”【2】。隨後在各種開源資料系統中得到應用,包括Hadoop,CouchDB和MongoDB。
|
||||
|
||||
@ -224,11 +224,12 @@ MapReduce是一個程式設計框架,你可以使用它編寫程式碼來處
|
||||
|
||||
要建立MapReduce作業,你需要實現兩個回撥函式,Mapper和Reducer,其行為如下(請參閱“[MapReduce查詢](ch2.md#MapReduce查詢)”):
|
||||
|
||||
***Mapper***
|
||||
* Mapper
|
||||
|
||||
Mapper會在每條輸入記錄上呼叫一次,其工作是從輸入記錄中提取鍵值。對於每個輸入,它可以生成任意數量的鍵值對(包括None)。它不會保留從一個輸入記錄到下一個記錄的任何狀態,因此每個記錄都是獨立處理的。
|
||||
Mapper會在每條輸入記錄上呼叫一次,其工作是從輸入記錄中提取鍵值。對於每個輸入,它可以生成任意數量的鍵值對(包括None)。它不會保留從一個輸入記錄到下一個記錄的任何狀態,因此每個記錄都是獨立處理的。
|
||||
|
||||
* Reducer
|
||||
|
||||
***Reducer***
|
||||
MapReduce框架拉取由Mapper生成的鍵值對,收集屬於同一個鍵的所有值,並在這組值上迭代呼叫Reducer。 Reducer可以產生輸出記錄(例如相同URL的出現次數)。
|
||||
|
||||
在Web伺服器日誌的例子中,我們在第5步中有第二個`sort`命令,它按請求數對URL進行排序。在MapReduce中,如果你需要第二個排序階段,則可以透過編寫第二個MapReduce作業並將第一個作業的輸出用作第二個作業的輸入來實現它。這樣看來,Mapper的作用是將資料放入一個適合排序的表單中,並且Reducer的作用是處理已排序的資料。
|
||||
@ -686,30 +687,30 @@ Spark,Flink和Tez避免將中間狀態寫入HDFS,因此它們採取了不同
|
||||
|
||||
分散式批處理框架需要解決的兩個主要問題是:
|
||||
|
||||
***分割槽***
|
||||
* 分割槽
|
||||
|
||||
在MapReduce中,Mapper根據輸入檔案塊進行分割槽。Mapper的輸出被重新分割槽、排序併合併到可配置數量的Reducer分割槽中。這一過程的目的是把所有的**相關**資料(例如帶有相同鍵的所有記錄)都放在同一個地方。
|
||||
在MapReduce中,Mapper根據輸入檔案塊進行分割槽。Mapper的輸出被重新分割槽、排序併合併到可配置數量的Reducer分割槽中。這一過程的目的是把所有的**相關**資料(例如帶有相同鍵的所有記錄)都放在同一個地方。
|
||||
|
||||
後MapReduce時代的資料流引擎若非必要會盡量避免排序,但它們也採取了大致類似的分割槽方法。
|
||||
後MapReduce時代的資料流引擎若非必要會盡量避免排序,但它們也採取了大致類似的分割槽方法。
|
||||
|
||||
***容錯***
|
||||
* 容錯
|
||||
|
||||
MapReduce經常寫入磁碟,這使得從單個失敗的任務恢復很輕鬆,無需重新啟動整個作業,但在無故障的情況下減慢了執行速度。資料流引擎更多地將中間狀態儲存在記憶體中,更少地物化中間狀態,這意味著如果節點發生故障,則需要重算更多的資料。確定性運算元減少了需要重算的資料量。
|
||||
MapReduce經常寫入磁碟,這使得從單個失敗的任務恢復很輕鬆,無需重新啟動整個作業,但在無故障的情況下減慢了執行速度。資料流引擎更多地將中間狀態儲存在記憶體中,更少地物化中間狀態,這意味著如果節點發生故障,則需要重算更多的資料。確定性運算元減少了需要重算的資料量。
|
||||
|
||||
|
||||
我們討論了幾種MapReduce的連線演算法,其中大多數也在MPP資料庫和資料流引擎內部使用。它們也很好地演示了分割槽演算法是如何工作的:
|
||||
|
||||
***排序合併連線***
|
||||
* 排序合併連線
|
||||
|
||||
每個參與連線的輸入都透過一個提取連線鍵的Mapper。透過分割槽、排序和合並,具有相同鍵的所有記錄最終都會進入相同的Reducer呼叫。這個函式能輸出連線好的記錄。
|
||||
每個參與連線的輸入都透過一個提取連線鍵的Mapper。透過分割槽、排序和合並,具有相同鍵的所有記錄最終都會進入相同的Reducer呼叫。這個函式能輸出連線好的記錄。
|
||||
|
||||
***廣播雜湊連線***
|
||||
* 廣播雜湊連線
|
||||
|
||||
兩個連線輸入之一很小,所以它並沒有分割槽,而且能被完全載入進一個雜湊表中。因此,你可以為連線輸入大端的每個分割槽啟動一個Mapper,將輸入小端的散列表載入到每個Mapper中,然後掃描大端,一次一條記錄,併為每條記錄查詢散列表。
|
||||
兩個連線輸入之一很小,所以它並沒有分割槽,而且能被完全載入進一個雜湊表中。因此,你可以為連線輸入大端的每個分割槽啟動一個Mapper,將輸入小端的散列表載入到每個Mapper中,然後掃描大端,一次一條記錄,併為每條記錄查詢散列表。
|
||||
|
||||
***分割槽雜湊連線***
|
||||
* 分割槽雜湊連線
|
||||
|
||||
如果兩個連線輸入以相同的方式分割槽(使用相同的鍵,相同的雜湊函式和相同數量的分割槽),則可以獨立地對每個分割槽應用散列表方法。
|
||||
如果兩個連線輸入以相同的方式分割槽(使用相同的鍵,相同的雜湊函式和相同數量的分割槽),則可以獨立地對每個分割槽應用散列表方法。
|
||||
|
||||
分散式批處理引擎有一個刻意限制的程式設計模型:回撥函式(比如Mapper和Reducer)被假定是無狀態的,而且除了指定的輸出外,必須沒有任何外部可見的副作用。這一限制允許框架在其抽象下隱藏一些困難的分散式系統問題:當遇到崩潰和網路問題時,任務可以安全地重試,任何失敗任務的輸出都被丟棄。如果某個分割槽的多個任務成功,則其中只有一個能使其輸出實際可見。
|
||||
|
||||
|
@ -96,13 +96,13 @@
|
||||
|
||||
當多個消費者從同一主題中讀取訊息時,有兩種主要的訊息傳遞模式,如[圖11-1](../img/fig11-1.png)所示:
|
||||
|
||||
***負載均衡(load balancing)***
|
||||
* 負載均衡(load balancing)
|
||||
|
||||
每條訊息都被傳遞給消費者**之一**,所以處理該主題下訊息的工作能被多個消費者共享。代理可以為消費者任意分配訊息。當處理訊息的代價高昂,希望能並行處理訊息時,此模式非常有用(在AMQP中,可以透過讓多個客戶端從同一個佇列中消費來實現負載均衡,而在JMS中則稱之為**共享訂閱(shared subscription)**)。
|
||||
每條訊息都被傳遞給消費者**之一**,所以處理該主題下訊息的工作能被多個消費者共享。代理可以為消費者任意分配訊息。當處理訊息的代價高昂,希望能並行處理訊息時,此模式非常有用(在AMQP中,可以透過讓多個客戶端從同一個佇列中消費來實現負載均衡,而在JMS中則稱之為**共享訂閱(shared subscription)**)。
|
||||
|
||||
***扇出(fan-out)***
|
||||
* 扇出(fan-out)
|
||||
|
||||
每條訊息都被傳遞給**所有**消費者。扇出允許幾個獨立的消費者各自“收聽”相同的訊息廣播,而不會相互影響 —— 這個流處理中的概念對應批處理中多個不同批處理作業讀取同一份輸入檔案 (JMS中的主題訂閱與AMQP中的交叉繫結提供了這一功能)。
|
||||
每條訊息都被傳遞給**所有**消費者。扇出允許幾個獨立的消費者各自“收聽”相同的訊息廣播,而不會相互影響 —— 這個流處理中的概念對應批處理中多個不同批處理作業讀取同一份輸入檔案 (JMS中的主題訂閱與AMQP中的交叉繫結提供了這一功能)。
|
||||
|
||||
![](../img/fig11-1.png)
|
||||
|
||||
@ -542,21 +542,21 @@ CEP的實現包括Esper【69】,IBM InfoSphere Streams【70】,Apama,TIBCO
|
||||
|
||||
當你知道如何確定一個事件的時間戳後,下一步就是如何定義時間段的視窗。然後視窗就可以用於聚合,例如事件計數,或計算視窗內值的平均值。有幾種視窗很常用【79,83】:
|
||||
|
||||
***滾動視窗(Tumbling Window)***
|
||||
* 滾動視窗(Tumbling Window)
|
||||
|
||||
滾動視窗有著固定的長度,每個事件都僅能屬於一個視窗。例如,假設你有一個1分鐘的滾動視窗,則所有時間戳在`10:03:00`和`10:03:59`之間的事件會被分組到一個視窗中,`10:04:00`和`10:04:59`之間的事件被分組到下一個視窗,依此類推。透過將每個事件時間戳四捨五入至最近的分鐘來確定它所屬的視窗,可以實現1分鐘的滾動視窗。
|
||||
滾動視窗有著固定的長度,每個事件都僅能屬於一個視窗。例如,假設你有一個1分鐘的滾動視窗,則所有時間戳在`10:03:00`和`10:03:59`之間的事件會被分組到一個視窗中,`10:04:00`和`10:04:59`之間的事件被分組到下一個視窗,依此類推。透過將每個事件時間戳四捨五入至最近的分鐘來確定它所屬的視窗,可以實現1分鐘的滾動視窗。
|
||||
|
||||
***跳動視窗(Hopping Window)***
|
||||
* 跳動視窗(Hopping Window)
|
||||
|
||||
跳動視窗也有著固定的長度,但允許視窗重疊以提供一些平滑。例如,一個帶有1分鐘跳躍步長的5分鐘視窗將包含`10:03:00`至`10:07:59`之間的事件,而下一個視窗將覆蓋`10:04:00`至`10:08:59`之間的事件,等等。透過首先計算1分鐘的滾動視窗(tunmbling window),然後在幾個相鄰視窗上進行聚合,可以實現這種跳動視窗。
|
||||
跳動視窗也有著固定的長度,但允許視窗重疊以提供一些平滑。例如,一個帶有1分鐘跳躍步長的5分鐘視窗將包含`10:03:00`至`10:07:59`之間的事件,而下一個視窗將覆蓋`10:04:00`至`10:08:59`之間的事件,等等。透過首先計算1分鐘的滾動視窗(tunmbling window),然後在幾個相鄰視窗上進行聚合,可以實現這種跳動視窗。
|
||||
|
||||
***滑動視窗(Sliding Window)***
|
||||
* 滑動視窗(Sliding Window)
|
||||
|
||||
滑動視窗包含了彼此間距在特定時長內的所有事件。例如,一個5分鐘的滑動視窗應當覆蓋`10:03:39`和`10:08:12`的事件,因為它們相距不超過5分鐘(注意滾動視窗與步長5分鐘的跳動視窗可能不會把這兩個事件分組到同一個視窗中,因為它們使用固定的邊界)。透過維護一個按時間排序的事件緩衝區,並不斷從視窗中移除過期的舊事件,可以實現滑動視窗。
|
||||
滑動視窗包含了彼此間距在特定時長內的所有事件。例如,一個5分鐘的滑動視窗應當覆蓋`10:03:39`和`10:08:12`的事件,因為它們相距不超過5分鐘(注意滾動視窗與步長5分鐘的跳動視窗可能不會把這兩個事件分組到同一個視窗中,因為它們使用固定的邊界)。透過維護一個按時間排序的事件緩衝區,並不斷從視窗中移除過期的舊事件,可以實現滑動視窗。
|
||||
|
||||
***會話視窗(Session window)***
|
||||
* 會話視窗(Session window)
|
||||
|
||||
與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果30分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(請參閱“[分組](ch10.md#分組)”)。
|
||||
與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果30分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(請參閱“[分組](ch10.md#分組)”)。
|
||||
|
||||
### 流連線
|
||||
|
||||
@ -684,13 +684,13 @@ Storm的Trident基於類似的想法來處理狀態【78】。依賴冪等性意
|
||||
|
||||
我們花了一些時間比較兩種訊息代理:
|
||||
|
||||
***AMQP/JMS風格的訊息代理***
|
||||
* AMQP/JMS風格的訊息代理
|
||||
|
||||
代理將單條訊息分配給消費者,消費者在成功處理單條訊息後確認訊息。訊息被確認後從代理中刪除。這種方法適合作為一種非同步形式的RPC(另請參閱“[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”),例如在任務佇列中,訊息處理的確切順序並不重要,而且訊息在處理完之後,不需要回頭重新讀取舊訊息。
|
||||
代理將單條訊息分配給消費者,消費者在成功處理單條訊息後確認訊息。訊息被確認後從代理中刪除。這種方法適合作為一種非同步形式的RPC(另請參閱“[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”),例如在任務佇列中,訊息處理的確切順序並不重要,而且訊息在處理完之後,不需要回頭重新讀取舊訊息。
|
||||
|
||||
***基於日誌的訊息代理***
|
||||
* 基於日誌的訊息代理
|
||||
|
||||
代理將一個分割槽中的所有訊息分配給同一個消費者節點,並始終以相同的順序傳遞訊息。並行是透過分割槽實現的,消費者透過存檔最近處理訊息的偏移量來跟蹤工作進度。訊息代理將訊息保留在磁碟上,因此如有必要的話,可以回跳並重新讀取舊訊息。
|
||||
代理將一個分割槽中的所有訊息分配給同一個消費者節點,並始終以相同的順序傳遞訊息。並行是透過分割槽實現的,消費者透過存檔最近處理訊息的偏移量來跟蹤工作進度。訊息代理將訊息保留在磁碟上,因此如有必要的話,可以回跳並重新讀取舊訊息。
|
||||
|
||||
基於日誌的方法與資料庫中的複製日誌(請參閱[第五章](ch5.md))和日誌結構儲存引擎(請參閱[第三章](ch3.md))有相似之處。我們看到,這種方法對於消費輸入流,併產生衍生狀態或衍生輸出資料流的系統而言特別適用。
|
||||
|
||||
@ -704,17 +704,17 @@ Storm的Trident基於類似的想法來處理狀態【78】。依賴冪等性意
|
||||
|
||||
我們區分了流處理中可能出現的三種連線型別:
|
||||
|
||||
***流流連線***
|
||||
* 流流連線
|
||||
|
||||
兩個輸入流都由活動事件組成,而連線運算元在某個時間視窗內搜尋相關的事件。例如,它可能會將同一個使用者30分鐘內進行的兩個活動聯絡在一起。如果你想要找出一個流內的相關事件,連線的兩側輸入可能實際上都是同一個流(**自連線(self-join)**)。
|
||||
兩個輸入流都由活動事件組成,而連線運算元在某個時間視窗內搜尋相關的事件。例如,它可能會將同一個使用者30分鐘內進行的兩個活動聯絡在一起。如果你想要找出一個流內的相關事件,連線的兩側輸入可能實際上都是同一個流(**自連線(self-join)**)。
|
||||
|
||||
***流表連線***
|
||||
* 流表連線
|
||||
|
||||
一個輸入流由活動事件組成,另一個輸入流是資料庫變更日誌。變更日誌保證了資料庫的本地副本是最新的。對於每個活動事件,連線運算元將查詢資料庫,並輸出一個擴充套件的活動事件。
|
||||
一個輸入流由活動事件組成,另一個輸入流是資料庫變更日誌。變更日誌保證了資料庫的本地副本是最新的。對於每個活動事件,連線運算元將查詢資料庫,並輸出一個擴充套件的活動事件。
|
||||
|
||||
***表表連線***
|
||||
* 表表連線
|
||||
|
||||
兩個輸入流都是資料庫變更日誌。在這種情況下,一側的每一個變化都與另一側的最新狀態相連線。結果是兩表連線所得物化檢視的變更流。
|
||||
兩個輸入流都是資料庫變更日誌。在這種情況下,一側的每一個變化都與另一側的最新狀態相連線。結果是兩表連線所得物化檢視的變更流。
|
||||
|
||||
最後,我們討論了在流處理中實現容錯和恰好一次語義的技術。與批處理一樣,我們需要放棄任何失敗任務的部分輸出。然而由於流處理長時間執行並持續產生輸出,所以不能簡單地丟棄所有的輸出。相反,可以使用更細粒度的恢復機制,基於微批次、存檔點、事務或冪等寫入。
|
||||
|
||||
|
@ -575,17 +575,17 @@ COMMIT;
|
||||
|
||||
更一般地來講,我認為術語**一致性(consistency)** 這個術語混淆了兩個值得分別考慮的需求:
|
||||
|
||||
***及時性(Timeliness)***
|
||||
* 及時性(Timeliness)
|
||||
|
||||
及時性意味著確保使用者觀察到系統的最新狀態。我們之前看到,如果使用者從陳舊的資料副本中讀取資料,它們可能會觀察到系統處於不一致的狀態(請參閱“[複製延遲問題](ch5.md#複製延遲問題)”)。但這種不一致是暫時的,而最終會透過等待與重試簡單地得到解決。
|
||||
及時性意味著確保使用者觀察到系統的最新狀態。我們之前看到,如果使用者從陳舊的資料副本中讀取資料,它們可能會觀察到系統處於不一致的狀態(請參閱“[複製延遲問題](ch5.md#複製延遲問題)”)。但這種不一致是暫時的,而最終會透過等待與重試簡單地得到解決。
|
||||
|
||||
CAP定理(請參閱“[線性一致性的代價](ch9.md#線性一致性的代價)”)使用**線性一致性(linearizability)** 意義上的一致性,這是實現及時性的強有力方法。像**寫後讀**這樣及時性更弱的一致性也很有用(請參閱“[讀己之寫](ch5.md#讀己之寫)”)也很有用。
|
||||
CAP定理(請參閱“[線性一致性的代價](ch9.md#線性一致性的代價)”)使用**線性一致性(linearizability)** 意義上的一致性,這是實現及時性的強有力方法。像**寫後讀**這樣及時性更弱的一致性也很有用(請參閱“[讀己之寫](ch5.md#讀己之寫)”)也很有用。
|
||||
|
||||
***完整性(Integrity)***
|
||||
* 完整性(Integrity)
|
||||
|
||||
完整性意味著沒有損壞;即沒有資料丟失,並且沒有矛盾或錯誤的資料。尤其是如果某些衍生資料集是作為底層資料之上的檢視而維護的(請參閱“[從事件日誌中派生出當前狀態](ch11.md#從事件日誌中派生出當前狀態)”),這種衍生必須是正確的。例如,資料庫索引必須正確地反映資料庫的內容 —— 缺失某些記錄的索引並不是很有用。
|
||||
完整性意味著沒有損壞;即沒有資料丟失,並且沒有矛盾或錯誤的資料。尤其是如果某些衍生資料集是作為底層資料之上的檢視而維護的(請參閱“[從事件日誌中派生出當前狀態](ch11.md#從事件日誌中派生出當前狀態)”),這種衍生必須是正確的。例如,資料庫索引必須正確地反映資料庫的內容 —— 缺失某些記錄的索引並不是很有用。
|
||||
|
||||
如果完整性被違背,這種不一致是永久的:在大多數情況下,等待與重試並不能修復資料庫損壞。相反的是,需要顯式地檢查與修復。在ACID事務的上下文中(請參閱“[ACID的含義](ch7.md#ACID的含義)”),一致性通常被理解為某種特定於應用的完整性概念。原子性和永續性是保持完整性的重要工具。
|
||||
如果完整性被違背,這種不一致是永久的:在大多數情況下,等待與重試並不能修復資料庫損壞。相反的是,需要顯式地檢查與修復。在ACID事務的上下文中(請參閱“[ACID的含義](ch7.md#ACID的含義)”),一致性通常被理解為某種特定於應用的完整性概念。原子性和永續性是保持完整性的重要工具。
|
||||
|
||||
|
||||
口號形式:違反及時性,“最終一致性”;違反完整性,“永無一致性”。
|
||||
|
20
zh-tw/ch2.md
20
zh-tw/ch2.md
@ -154,13 +154,13 @@ JSON表示比[圖2-1](../img/fig2-1.png)中的多表模式具有更好的**區
|
||||
|
||||
此外,即便應用程式的最初版本適合無連線的文件模型,隨著功能新增到應用程式中,資料會變得更加互聯。例如,考慮一下對簡歷例子進行的一些修改:
|
||||
|
||||
***組織和學校作為實體***
|
||||
* 組織和學校作為實體
|
||||
|
||||
在前面的描述中,`organization`(使用者工作的公司)和`school_name`(他們學習的地方)只是字串。也許他們應該是對實體的引用呢?然後,每個組織,學校或大學都可以擁有自己的網頁(標識,新聞提要等)。每個簡歷可以連結到它所提到的組織和學校,並且包括他們的圖示和其他資訊(請參閱[圖2-3](../img/fig2-3.png),來自LinkedIn的一個例子)。
|
||||
在前面的描述中,`organization`(使用者工作的公司)和`school_name`(他們學習的地方)只是字串。也許他們應該是對實體的引用呢?然後,每個組織,學校或大學都可以擁有自己的網頁(標識,新聞提要等)。每個簡歷可以連結到它所提到的組織和學校,並且包括他們的圖示和其他資訊(請參閱[圖2-3](../img/fig2-3.png),來自LinkedIn的一個例子)。
|
||||
|
||||
***推薦***
|
||||
* 推薦
|
||||
|
||||
假設你想新增一個新的功能:一個使用者可以為另一個使用者寫一個推薦。在使用者的簡歷上顯示推薦,並附上推薦使用者的姓名和照片。如果推薦人更新他們的照片,那他們寫的任何推薦都需要顯示新的照片。因此,推薦應該擁有作者個人簡介的引用。
|
||||
假設你想新增一個新的功能:一個使用者可以為另一個使用者寫一個推薦。在使用者的簡歷上顯示推薦,並附上推薦使用者的姓名和照片。如果推薦人更新他們的照片,那他們寫的任何推薦都需要顯示新的照片。因此,推薦應該擁有作者個人簡介的引用。
|
||||
|
||||
![](../img/fig2-3.png)
|
||||
|
||||
@ -515,17 +515,17 @@ db.observations.aggregate([
|
||||
|
||||
一個圖由兩種物件組成:**頂點(vertices)**(也稱為**節點(nodes)** 或**實體(entities)**),和**邊(edges)**( 也稱為**關係(relationships)** 或**弧 (arcs)** )。多種資料可以被建模為一個圖形。典型的例子包括:
|
||||
|
||||
***社交圖譜***
|
||||
* 社交圖譜
|
||||
|
||||
頂點是人,邊指示哪些人彼此認識。
|
||||
頂點是人,邊指示哪些人彼此認識。
|
||||
|
||||
***網路圖譜***
|
||||
* 網路圖譜
|
||||
|
||||
頂點是網頁,邊緣表示指向其他頁面的HTML連結。
|
||||
頂點是網頁,邊緣表示指向其他頁面的HTML連結。
|
||||
|
||||
***公路或鐵路網路***
|
||||
* 公路或鐵路網路
|
||||
|
||||
頂點是交叉路口,邊線代表它們之間的道路或鐵路線。
|
||||
頂點是交叉路口,邊線代表它們之間的道路或鐵路線。
|
||||
|
||||
可以將那些眾所周知的演算法運用到這些圖上:例如,汽車導航系統搜尋道路網路中兩點之間的最短路徑,PageRank可以用在網路圖上來確定網頁的流行程度,從而確定該網頁在搜尋結果中的排名。
|
||||
|
||||
|
20
zh-tw/ch3.md
20
zh-tw/ch3.md
@ -108,25 +108,25 @@ $ cat database
|
||||
|
||||
要讓這個簡單的想法在實際中能工作會涉及到大量的細節。簡單來說,下面幾點都是實現過程中需要認真考慮的問題:
|
||||
|
||||
***檔案格式***
|
||||
* 檔案格式
|
||||
|
||||
CSV不是日誌的最佳格式。使用二進位制格式更快,更簡單:首先以位元組為單位對字串的長度進行編碼,然後是原始的字串(不需要轉義)。
|
||||
CSV不是日誌的最佳格式。使用二進位制格式更快,更簡單:首先以位元組為單位對字串的長度進行編碼,然後是原始的字串(不需要轉義)。
|
||||
|
||||
***刪除記錄***
|
||||
* 刪除記錄
|
||||
|
||||
如果要刪除一個鍵及其關聯的值,則必須在資料檔案中追加一個特殊的刪除記錄(有時稱為邏輯刪除(tombstone))。當日志段被合併時,邏輯刪除告訴合併過程丟棄被刪除鍵的任何以前的值。
|
||||
如果要刪除一個鍵及其關聯的值,則必須在資料檔案中追加一個特殊的刪除記錄(有時稱為邏輯刪除(tombstone))。當日志段被合併時,邏輯刪除告訴合併過程丟棄被刪除鍵的任何以前的值。
|
||||
|
||||
***崩潰恢復***
|
||||
* 崩潰恢復
|
||||
|
||||
如果資料庫重新啟動,則記憶體雜湊對映將丟失。原則上,你可以透過從頭到尾讀取整個段檔案並記錄下來每個鍵的最近值來恢復每個段的雜湊對映。但是,如果段檔案很大,可能需要很長時間,這會使服務的重啟比較痛苦。 Bitcask 透過將每個段的雜湊對映的快照儲存在硬碟上來加速恢復,可以使雜湊對映更快地載入到記憶體中。
|
||||
如果資料庫重新啟動,則記憶體雜湊對映將丟失。原則上,你可以透過從頭到尾讀取整個段檔案並記錄下來每個鍵的最近值來恢復每個段的雜湊對映。但是,如果段檔案很大,可能需要很長時間,這會使服務的重啟比較痛苦。 Bitcask 透過將每個段的雜湊對映的快照儲存在硬碟上來加速恢復,可以使雜湊對映更快地載入到記憶體中。
|
||||
|
||||
***部分寫入記錄***
|
||||
* 部分寫入記錄
|
||||
|
||||
資料庫隨時可能崩潰,包括在將記錄追加到日誌的過程中。 Bitcask檔案包含校驗和,允許檢測和忽略日誌中的這些損壞部分。
|
||||
資料庫隨時可能崩潰,包括在將記錄追加到日誌的過程中。 Bitcask檔案包含校驗和,允許檢測和忽略日誌中的這些損壞部分。
|
||||
|
||||
***併發控制***
|
||||
* 併發控制
|
||||
|
||||
由於寫操作是以嚴格的順序追加到日誌中的,所以常見的實現是隻有一個寫入執行緒。也因為資料檔案段是僅追加的或者說是不可變的,所以它們可以被多個執行緒同時讀取。
|
||||
由於寫操作是以嚴格的順序追加到日誌中的,所以常見的實現是隻有一個寫入執行緒。也因為資料檔案段是僅追加的或者說是不可變的,所以它們可以被多個執行緒同時讀取。
|
||||
|
||||
乍一看,僅追加日誌似乎很浪費:為什麼不直接在檔案裡更新,用新值覆蓋舊值?僅追加的設計之所以是個好的設計,有如下幾個原因:
|
||||
|
||||
|
@ -24,13 +24,13 @@
|
||||
|
||||
這意味著,新舊版本的程式碼,以及新舊資料格式可能會在系統中同時共處。系統想要繼續順利執行,就需要保持**雙向相容性**:
|
||||
|
||||
***向後相容 (backward compatibility)***
|
||||
* 向後相容 (backward compatibility)
|
||||
|
||||
新程式碼可以讀舊資料。
|
||||
新程式碼可以讀舊資料。
|
||||
|
||||
***向前相容 (forward compatibility)***
|
||||
* 向前相容 (forward compatibility)
|
||||
|
||||
舊程式碼可以讀新資料。
|
||||
舊程式碼可以讀新資料。
|
||||
|
||||
向後相容性通常並不難實現:新程式碼的作者當然知道由舊程式碼使用的資料格式,因此可以顯示地處理它(最簡單的辦法是,保留舊程式碼即可讀取舊資料)。
|
||||
|
||||
|
70
zh-tw/ch5.md
70
zh-tw/ch5.md
@ -94,7 +94,7 @@
|
||||
|
||||
#### 從庫失效:追趕恢復
|
||||
|
||||
在其本地磁碟上,每個從庫記錄從主庫收到的資料變更。如果從庫崩潰並重新啟動,或者,如果主庫和從庫之間的網路暫時中斷,則比較容易恢復:從庫可以從日誌中知道,在發生故障之前處理的最後一個事務。因此,從庫可以連線到主庫,並請求在從庫斷開連線時發生的所有資料變更。當應用完所有這些變化後,它就趕上了主庫,並可以像以前一樣繼續接收資料變更流。
|
||||
在其本地磁碟上,每個從庫記錄從主庫收到的資料變更。如果從庫崩潰並重新啟動,或者,如果主庫和從庫之間的網路暫時中斷,則比較容易恢復:從庫可以從日誌中知道,在發生故障之前處理的最後一個事務。因此,從庫可以連線到主庫,並請求在從庫斷開期間發生的所有資料變更。當應用完所有這些變化後,它就趕上了主庫,並可以像以前一樣繼續接收資料變更流。
|
||||
|
||||
#### 主庫失效:故障切換
|
||||
|
||||
@ -315,17 +315,17 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
|
||||
|
||||
我們來比較一下在運維多個數據中心時,單主和多主的適應情況。
|
||||
|
||||
***效能***
|
||||
* 效能
|
||||
|
||||
在單主配置中,每個寫入都必須穿過網際網路,進入主庫所在的資料中心。這可能會增加寫入時間,並可能違背了設定多個數據中心的初心。在多主配置中,每個寫操作都可以在本地資料中心進行處理,並與其他資料中心非同步複製。因此,資料中心之間的網路延遲對使用者來說是透明的,這意味著感覺到的效能可能會更好。
|
||||
在單主配置中,每個寫入都必須穿過網際網路,進入主庫所在的資料中心。這可能會增加寫入時間,並可能違背了設定多個數據中心的初心。在多主配置中,每個寫操作都可以在本地資料中心進行處理,並與其他資料中心非同步複製。因此,資料中心之間的網路延遲對使用者來說是透明的,這意味著感覺到的效能可能會更好。
|
||||
|
||||
***容忍資料中心停機***
|
||||
* 容忍資料中心停機
|
||||
|
||||
在單主配置中,如果主庫所在的資料中心發生故障,故障切換必須使另一個數據中心裡的追隨者成為領導者。在多主配置中,每個資料中心可以獨立於其他資料中心繼續執行,並且當發生故障的資料中心歸隊時,複製會自動趕上。
|
||||
在單主配置中,如果主庫所在的資料中心發生故障,故障切換必須使另一個數據中心裡的追隨者成為領導者。在多主配置中,每個資料中心可以獨立於其他資料中心繼續執行,並且當發生故障的資料中心歸隊時,複製會自動趕上。
|
||||
|
||||
***容忍網路問題***
|
||||
* 容忍網路問題
|
||||
|
||||
資料中心之間的通訊通常穿過公共網際網路,這可能不如資料中心內的本地網路可靠。單主配置對這資料中心間的連線問題非常敏感,因為透過這個連線進行的寫操作是同步的。採用非同步複製功能的多主配置通常能更好地承受網路問題:臨時的網路中斷並不會妨礙正在處理的寫入。
|
||||
資料中心之間的通訊通常穿過公共網際網路,這可能不如資料中心內的本地網路可靠。單主配置對這資料中心間的連線問題非常敏感,因為透過這個連線進行的寫操作是同步的。採用非同步複製功能的多主配置通常能更好地承受網路問題:臨時的網路中斷並不會妨礙正在處理的寫入。
|
||||
|
||||
有些資料庫預設情況下支援多主配置,但使用外部工具實現也很常見,例如用於MySQL的Tungsten Replicator 【26】,用於PostgreSQL的BDR【27】以及用於Oracle的GoldenGate 【19】。
|
||||
|
||||
@ -397,13 +397,13 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
|
||||
|
||||
作為解決衝突最合適的方法可能取決於應用程式,大多數多主複製工具允許使用應用程式程式碼編寫衝突解決邏輯。該程式碼可以在寫入或讀取時執行:
|
||||
|
||||
***寫時執行***
|
||||
* 寫時執行
|
||||
|
||||
只要資料庫系統檢測到複製更改日誌中存在衝突,就會呼叫衝突處理程式。例如,Bucardo允許你為此編寫一段Perl程式碼。這個處理程式通常不能提示使用者——它在後臺程序中執行,並且必須快速執行。
|
||||
只要資料庫系統檢測到複製更改日誌中存在衝突,就會呼叫衝突處理程式。例如,Bucardo允許你為此編寫一段Perl程式碼。這個處理程式通常不能提示使用者——它在後臺程序中執行,並且必須快速執行。
|
||||
|
||||
***讀時執行***
|
||||
* 讀時執行
|
||||
|
||||
當檢測到衝突時,所有衝突寫入被儲存。下一次讀取資料時,會將這些多個版本的資料返回給應用程式。應用程式可能會提示使用者或自動解決衝突,並將結果寫回資料庫。例如,CouchDB以這種方式工作。
|
||||
當檢測到衝突時,所有衝突寫入被儲存。下一次讀取資料時,會將這些多個版本的資料返回給應用程式。應用程式可能會提示使用者或自動解決衝突,並將結果寫回資料庫。例如,CouchDB以這種方式工作。
|
||||
|
||||
請注意,衝突解決通常適用於單個行或文件層面,而不是整個事務【36】。因此,如果你有一個事務會原子性地進行幾次不同的寫入(請參閱[第七章](ch7.md),對於衝突解決而言,每個寫入仍需分開單獨考慮。
|
||||
|
||||
@ -492,13 +492,13 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
|
||||
|
||||
在Dynamo風格的資料儲存中經常使用兩種機制:
|
||||
|
||||
***讀修復(Read repair)***
|
||||
* 讀修復(Read repair)
|
||||
|
||||
當客戶端並行讀取多個節點時,它可以檢測到任何陳舊的響應。例如,在[圖5-10](../img/fig5-10.png)中,使用者2345獲得了來自副本3的版本6值和來自副本1和2的版本7值。客戶端發現副本3具有陳舊值,並將新值寫回到該副本。這種方法適用於讀頻繁的值。
|
||||
當客戶端並行讀取多個節點時,它可以檢測到任何陳舊的響應。例如,在[圖5-10](../img/fig5-10.png)中,使用者2345獲得了來自副本3的版本6值和來自副本1和2的版本7值。客戶端發現副本3具有陳舊值,並將新值寫回到該副本。這種方法適用於讀頻繁的值。
|
||||
|
||||
***反熵過程(Anti-entropy process)***
|
||||
* 反熵過程(Anti-entropy process)
|
||||
|
||||
此外,一些資料儲存具有後臺程序,該程序不斷查詢副本之間的資料差異,並將任何缺少的資料從一個副本複製到另一個副本。與基於領導者的複製中的複製日誌不同,此反熵過程不會以任何特定的順序複製寫入,並且在複製資料之前可能會有顯著的延遲。
|
||||
此外,一些資料儲存具有後臺程序,該程序不斷查詢副本之間的資料差異,並將任何缺少的資料從一個副本複製到另一個副本。與基於領導者的複製中的複製日誌不同,此反熵過程不會以任何特定的順序複製寫入,並且在複製資料之前可能會有顯著的延遲。
|
||||
|
||||
並不是所有的系統都實現了這兩個,例如,Voldemort目前沒有反熵過程。請注意,如果沒有反熵過程,某些副本中很少讀取的值可能會丟失,從而降低了永續性,因為只有在應用程式讀取值時才執行讀修復。
|
||||
|
||||
@ -713,38 +713,38 @@ LWW實現了最終收斂的目標,但以**永續性**為代價:如果同一
|
||||
|
||||
在本章中,我們考察了複製的問題。複製可以用於幾個目的:
|
||||
|
||||
***高可用性***
|
||||
* 高可用性
|
||||
|
||||
即使在一臺機器(或多臺機器,或整個資料中心)停機的情況下也能保持系統正常執行
|
||||
即使在一臺機器(或多臺機器,或整個資料中心)停機的情況下也能保持系統正常執行
|
||||
|
||||
***斷開連線的操作***
|
||||
* 斷開連線的操作
|
||||
|
||||
允許應用程式在網路中斷時繼續工作
|
||||
允許應用程式在網路中斷時繼續工作
|
||||
|
||||
***延遲***
|
||||
* 延遲
|
||||
|
||||
將資料放置在距離使用者較近的地方,以便使用者能夠更快地與其互動
|
||||
將資料放置在距離使用者較近的地方,以便使用者能夠更快地與其互動
|
||||
|
||||
***可伸縮性***
|
||||
* 可伸縮性
|
||||
|
||||
透過在副本上讀,能夠處理比單機更大的讀取量
|
||||
透過在副本上讀,能夠處理比單機更大的讀取量
|
||||
|
||||
|
||||
儘管是一個簡單的目標 - 在幾臺機器上保留相同資料的副本,但複製卻是一個非常棘手的問題。它需要仔細考慮併發和所有可能出錯的事情,並處理這些故障的後果。至少,我們需要處理不可用的節點和網路中斷(這還不包括更隱蔽的故障,例如由於軟體錯誤導致的靜默資料損壞)。
|
||||
|
||||
我們討論了複製的三種主要方法:
|
||||
|
||||
***單主複製***
|
||||
* 單主複製
|
||||
|
||||
客戶端將所有寫入操作傳送到單個節點(領導者),該節點將資料更改事件流傳送到其他副本(追隨者)。讀取可以在任何副本上執行,但從追隨者讀取可能是陳舊的。
|
||||
客戶端將所有寫入操作傳送到單個節點(領導者),該節點將資料更改事件流傳送到其他副本(追隨者)。讀取可以在任何副本上執行,但從追隨者讀取可能是陳舊的。
|
||||
|
||||
***多主複製***
|
||||
* 多主複製
|
||||
|
||||
客戶端傳送每個寫入到幾個領導節點之一,其中任何一個都可以接受寫入。領導者將資料更改事件流傳送給彼此以及任何跟隨者節點。
|
||||
客戶端傳送每個寫入到幾個領導節點之一,其中任何一個都可以接受寫入。領導者將資料更改事件流傳送給彼此以及任何跟隨者節點。
|
||||
|
||||
***無主複製***
|
||||
* 無主複製
|
||||
|
||||
客戶端傳送每個寫入到幾個節點,並從多個節點並行讀取,以檢測和糾正具有陳舊資料的節點。
|
||||
客戶端傳送每個寫入到幾個節點,並從多個節點並行讀取,以檢測和糾正具有陳舊資料的節點。
|
||||
|
||||
每種方法都有優點和缺點。單主複製是非常流行的,因為它很容易理解,不需要擔心衝突解決。在出現故障節點,網路中斷和延遲峰值的情況下,多領導者和無領導者複製可以更加穩健,但以更難以推理並僅提供非常弱的一致性保證為代價。
|
||||
|
||||
@ -752,17 +752,17 @@ LWW實現了最終收斂的目標,但以**永續性**為代價:如果同一
|
||||
|
||||
我們研究了一些可能由複製滯後引起的奇怪效應,我們也討論了一些有助於決定應用程式在複製滯後時的行為的一致性模型:
|
||||
|
||||
***寫後讀***
|
||||
* 寫後讀
|
||||
|
||||
使用者應該總是看到自己提交的資料。
|
||||
使用者應該總是看到自己提交的資料。
|
||||
|
||||
***單調讀***
|
||||
* 單調讀
|
||||
|
||||
使用者在看到某一個時間點的資料後,他們不應該再看到某個更早時間點的資料。
|
||||
使用者在看到某一個時間點的資料後,他們不應該再看到某個更早時間點的資料。
|
||||
|
||||
***一致字首讀***
|
||||
* 一致字首讀
|
||||
|
||||
使用者應該看到資料處於一種具有因果意義的狀態:例如,按正確的順序看到一個問題和對應的回答。
|
||||
使用者應該看到資料處於一種具有因果意義的狀態:例如,按正確的順序看到一個問題和對應的回答。
|
||||
|
||||
最後,我們討論了多領導者和無領導者複製方法所固有的併發問題:因為他們允許多個寫入併發發生,這可能會導致衝突。我們研究了一個數據庫可能使用的演算法來確定一個操作是否發生在另一個操作之前,或者它們是否同時發生。我們還談到了透過合併併發更新來解決衝突的方法。
|
||||
|
||||
|
13
zh-tw/ch6.md
13
zh-tw/ch6.md
@ -298,18 +298,17 @@ Couchbase不會自動重新平衡,這簡化了設計。通常情況下,它
|
||||
|
||||
我們討論了兩種主要的分割槽方法:
|
||||
|
||||
***鍵範圍分割槽***
|
||||
* 鍵範圍分割槽
|
||||
|
||||
其中鍵是有序的,並且分割槽擁有從某個最小值到某個最大值的所有鍵。排序的優勢在於可以進行有效的範圍查詢,但是如果應用程式經常訪問相鄰的鍵,則存在熱點的風險。
|
||||
其中鍵是有序的,並且分割槽擁有從某個最小值到某個最大值的所有鍵。排序的優勢在於可以進行有效的範圍查詢,但是如果應用程式經常訪問相鄰的鍵,則存在熱點的風險。
|
||||
|
||||
在這種方法中,當分割槽變得太大時,通常將分割槽分成兩個子分割槽,動態地再平衡分割槽。
|
||||
在這種方法中,當分割槽變得太大時,通常將分割槽分成兩個子分割槽,動態地再平衡分割槽。
|
||||
|
||||
***雜湊分割槽***
|
||||
* 雜湊分割槽
|
||||
|
||||
雜湊函式應用於每個鍵,分割槽擁有一定範圍的雜湊。這種方法破壞了鍵的排序,使得範圍查詢效率低下,但可以更均勻地分配負載。
|
||||
|
||||
透過雜湊進行分割槽時,通常先提前建立固定數量的分割槽,為每個節點分配多個分割槽,並在新增或刪除節點時將整個分割槽從一個節點移動到另一個節點。也可以使用動態分割槽。
|
||||
雜湊函式應用於每個鍵,分割槽擁有一定範圍的雜湊。這種方法破壞了鍵的排序,使得範圍查詢效率低下,但可以更均勻地分配負載。
|
||||
|
||||
透過雜湊進行分割槽時,通常先提前建立固定數量的分割槽,為每個節點分配多個分割槽,並在新增或刪除節點時將整個分割槽從一個節點移動到另一個節點。也可以使用動態分割槽。
|
||||
|
||||
兩種方法搭配使用也是可行的,例如使用複合主鍵:使用鍵的一部分來標識分割槽,而使用另一部分作為排序順序。
|
||||
|
||||
|
92
zh-tw/ch7.md
92
zh-tw/ch7.md
@ -128,13 +128,13 @@ ACID意義上的隔離性意味著,**同時執行的事務是相互隔離的**
|
||||
|
||||
回顧一下,在ACID中,原子性和隔離性描述了客戶端在同一事務中執行多次寫入時,資料庫應該做的事情:
|
||||
|
||||
***原子性***
|
||||
* 原子性
|
||||
|
||||
如果在一系列寫操作的中途發生錯誤,則應中止事務處理,並丟棄當前事務的所有寫入。換句話說,資料庫免去了使用者對部分失敗的擔憂——透過提供“**寧為玉碎,不為瓦全(all-or-nothing)**”的保證。
|
||||
如果在一系列寫操作的中途發生錯誤,則應中止事務處理,並丟棄當前事務的所有寫入。換句話說,資料庫免去了使用者對部分失敗的擔憂——透過提供“**寧為玉碎,不為瓦全(all-or-nothing)**”的保證。
|
||||
|
||||
***隔離性***
|
||||
* 隔離性
|
||||
|
||||
同時執行的事務不應該互相干擾。例如,如果一個事務進行多次寫入,則另一個事務要麼看到全部寫入結果,要麼什麼都看不到,但不應該是一些子集。
|
||||
同時執行的事務不應該互相干擾。例如,如果一個事務進行多次寫入,則另一個事務要麼看到全部寫入結果,要麼什麼都看不到,但不應該是一些子集。
|
||||
|
||||
這些定義假設你想同時修改多個物件(行,文件,記錄)。通常需要**多物件事務(multi-object transaction)** 來保持多塊資料同步。[圖7-2](../img/fig7-2.png)展示了一個來自電郵應用的例子。執行以下查詢來顯示使用者未讀郵件數量:
|
||||
|
||||
@ -301,13 +301,13 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
對於Alice的情況,這不是一個長期持續的問題。因為如果她幾秒鐘後重新整理銀行網站的頁面,她很可能會看到一致的帳戶餘額。但是有些情況下,不能容忍這種暫時的不一致:
|
||||
|
||||
***備份***
|
||||
* 備份
|
||||
|
||||
進行備份需要複製整個資料庫,對大型資料庫而言可能需要花費數小時才能完成。備份程序執行時,資料庫仍然會接受寫入操作。因此備份可能會包含一些舊的部分和一些新的部分。如果從這樣的備份中恢復,那麼不一致(如消失的錢)就會變成永久的。
|
||||
進行備份需要複製整個資料庫,對大型資料庫而言可能需要花費數小時才能完成。備份程序執行時,資料庫仍然會接受寫入操作。因此備份可能會包含一些舊的部分和一些新的部分。如果從這樣的備份中恢復,那麼不一致(如消失的錢)就會變成永久的。
|
||||
|
||||
***分析查詢和完整性檢查***
|
||||
* 分析查詢和完整性檢查
|
||||
|
||||
有時,你可能需要執行一個查詢,掃描大部分的資料庫。這樣的查詢在分析中很常見(請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”),也可能是定期完整性檢查(即監視資料損壞)的一部分。如果這些查詢在不同時間點觀察資料庫的不同部分,則可能會返回毫無意義的結果。
|
||||
有時,你可能需要執行一個查詢,掃描大部分的資料庫。這樣的查詢在分析中很常見(請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”),也可能是定期完整性檢查(即監視資料損壞)的一部分。如果這些查詢在不同時間點觀察資料庫的不同部分,則可能會返回毫無意義的結果。
|
||||
|
||||
**快照隔離(snapshot isolation)**【28】是這個問題最常見的解決方案。想法是,每個事務都從資料庫的**一致快照(consistent snapshot)** 中讀取——也就是說,事務可以看到事務開始時在資料庫中提交的所有資料。即使這些資料隨後被另一個事務更改,每個事務也只能看到該特定時間點的舊資料。
|
||||
|
||||
@ -511,42 +511,42 @@ COMMIT;
|
||||
|
||||
寫偏差乍看像是一個深奧的問題,但一旦意識到這一點,很容易會注意到更多可能的情況。以下是一些例子:
|
||||
|
||||
***會議室預訂系統***
|
||||
* 會議室預訂系統
|
||||
|
||||
比如你想要規定不能在同一時間對同一個會議室進行多次的預訂【43】。當有人想要預訂時,首先檢查是否存在相互衝突的預訂(即預訂時間範圍重疊的同一房間),如果沒有找到,則建立會議(請參閱示例7-2)[^ix]。
|
||||
比如你想要規定不能在同一時間對同一個會議室進行多次的預訂【43】。當有人想要預訂時,首先檢查是否存在相互衝突的預訂(即預訂時間範圍重疊的同一房間),如果沒有找到,則建立會議(請參閱示例7-2)[^ix]。
|
||||
|
||||
[^ix]: 在PostgreSQL中,你可以使用範圍型別優雅地執行此操作,但在其他資料庫中並未得到廣泛支援。
|
||||
[^ix]: 在PostgreSQL中,你可以使用範圍型別優雅地執行此操作,但在其他資料庫中並未得到廣泛支援。
|
||||
|
||||
**例7-2 會議室預訂系統試圖避免重複預訂(在快照隔離下不安全)**
|
||||
**例7-2 會議室預訂系統試圖避免重複預訂(在快照隔離下不安全)**
|
||||
|
||||
```sql
|
||||
BEGIN TRANSACTION;
|
||||
```sql
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- 檢查所有現存的與12:00~13:00重疊的預定
|
||||
SELECT COUNT(*) FROM bookings
|
||||
WHERE room_id = 123 AND
|
||||
-- 檢查所有現存的與12:00~13:00重疊的預定
|
||||
SELECT COUNT(*) FROM bookings
|
||||
WHERE room_id = 123 AND
|
||||
end_time > '2015-01-01 12:00' AND start_time < '2015-01-01 13:00';
|
||||
|
||||
-- 如果之前的查詢返回0
|
||||
INSERT INTO bookings(room_id, start_time, end_time, user_id)
|
||||
-- 如果之前的查詢返回0
|
||||
INSERT INTO bookings(room_id, start_time, end_time, user_id)
|
||||
VALUES (123, '2015-01-01 12:00', '2015-01-01 13:00', 666);
|
||||
|
||||
COMMIT;
|
||||
```
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
不幸的是,快照隔離並不能防止另一個使用者同時插入衝突的會議。為了確保不會遇到排程衝突,你又需要可序列化的隔離級別了。
|
||||
不幸的是,快照隔離並不能防止另一個使用者同時插入衝突的會議。為了確保不會遇到排程衝突,你又需要可序列化的隔離級別了。
|
||||
|
||||
***多人遊戲***
|
||||
* 多人遊戲
|
||||
|
||||
在[例7-1]()中,我們使用一個鎖來防止丟失更新(也就是確保兩個玩家不能同時移動同一個棋子)。但是鎖定並不妨礙玩家將兩個不同的棋子移動到棋盤上的相同位置,或者採取其他違反遊戲規則的行為。按照你正在執行的規則型別,也許可以使用唯一約束(unique constraint),否則你很容易發生寫入偏差。
|
||||
在[例7-1]()中,我們使用一個鎖來防止丟失更新(也就是確保兩個玩家不能同時移動同一個棋子)。但是鎖定並不妨礙玩家將兩個不同的棋子移動到棋盤上的相同位置,或者採取其他違反遊戲規則的行為。按照你正在執行的規則型別,也許可以使用唯一約束(unique constraint),否則你很容易發生寫入偏差。
|
||||
|
||||
***搶注使用者名稱***
|
||||
* 搶注使用者名稱
|
||||
|
||||
在每個使用者擁有唯一使用者名稱的網站上,兩個使用者可能會嘗試同時建立具有相同使用者名稱的帳戶。可以在事務檢查名稱是否被搶佔,如果沒有則使用該名稱建立賬戶。但是像在前面的例子中那樣,在快照隔離下這是不安全的。幸運的是,唯一約束是一個簡單的解決辦法(第二個事務在提交時會因為違反使用者名稱唯一約束而被中止)。
|
||||
在每個使用者擁有唯一使用者名稱的網站上,兩個使用者可能會嘗試同時建立具有相同使用者名稱的帳戶。可以在事務檢查名稱是否被搶佔,如果沒有則使用該名稱建立賬戶。但是像在前面的例子中那樣,在快照隔離下這是不安全的。幸運的是,唯一約束是一個簡單的解決辦法(第二個事務在提交時會因為違反使用者名稱唯一約束而被中止)。
|
||||
|
||||
***防止雙重開支***
|
||||
* 防止雙重開支
|
||||
|
||||
允許使用者花錢或積分的服務,需要檢查使用者的支付數額不超過其餘額。可以透過在使用者的帳戶中插入一個試探性的消費專案來實現這一點,列出帳戶中的所有專案,並檢查總和是否為正值【44】。有了寫入偏差,可能會發生兩個支出專案同時插入,一起導致餘額變為負值,但這兩個事務都不會注意到另一個。
|
||||
允許使用者花錢或積分的服務,需要檢查使用者的支付數額不超過其餘額。可以透過在使用者的帳戶中插入一個試探性的消費專案來實現這一點,列出帳戶中的所有專案,並檢查總和是否為正值【44】。有了寫入偏差,可能會發生兩個支出專案同時插入,一起導致餘額變為負值,但這兩個事務都不會注意到另一個。
|
||||
|
||||
#### 導致寫入偏差的幻讀
|
||||
|
||||
@ -833,43 +833,43 @@ WHERE room_id = 123 AND
|
||||
|
||||
本章深入討論了**併發控制**的話題。我們討論了幾個廣泛使用的隔離級別,特別是**讀已提交**,**快照隔離**(有時稱為可重複讀)和**可序列化**。並透過研究競爭條件的各種例子,來描述這些隔離等級:
|
||||
|
||||
***髒讀***
|
||||
* 髒讀
|
||||
|
||||
一個客戶端讀取到另一個客戶端尚未提交的寫入。**讀已提交**或更強的隔離級別可以防止髒讀。
|
||||
一個客戶端讀取到另一個客戶端尚未提交的寫入。**讀已提交**或更強的隔離級別可以防止髒讀。
|
||||
|
||||
***髒寫***
|
||||
* 髒寫
|
||||
|
||||
一個客戶端覆蓋寫入了另一個客戶端尚未提交的寫入。幾乎所有的事務實現都可以防止髒寫。
|
||||
一個客戶端覆蓋寫入了另一個客戶端尚未提交的寫入。幾乎所有的事務實現都可以防止髒寫。
|
||||
|
||||
***讀取偏差(不可重複讀)***
|
||||
* 讀取偏差(不可重複讀)
|
||||
|
||||
在同一個事務中,客戶端在不同的時間點會看見資料庫的不同狀態。**快照隔離**經常用於解決這個問題,它允許事務從一個特定時間點的一致性快照中讀取資料。快照隔離通常使用**多版本併發控制(MVCC)** 來實現。
|
||||
在同一個事務中,客戶端在不同的時間點會看見資料庫的不同狀態。**快照隔離**經常用於解決這個問題,它允許事務從一個特定時間點的一致性快照中讀取資料。快照隔離通常使用**多版本併發控制(MVCC)** 來實現。
|
||||
|
||||
***更新丟失***
|
||||
* 更新丟失
|
||||
|
||||
兩個客戶端同時執行**讀取-修改-寫入序列**。其中一個寫操作,在沒有合併另一個寫入變更情況下,直接覆蓋了另一個寫操作的結果。所以導致資料丟失。快照隔離的一些實現可以自動防止這種異常,而另一些實現則需要手動鎖定(`SELECT FOR UPDATE`)。
|
||||
兩個客戶端同時執行**讀取-修改-寫入序列**。其中一個寫操作,在沒有合併另一個寫入變更情況下,直接覆蓋了另一個寫操作的結果。所以導致資料丟失。快照隔離的一些實現可以自動防止這種異常,而另一些實現則需要手動鎖定(`SELECT FOR UPDATE`)。
|
||||
|
||||
***寫偏差***
|
||||
* 寫偏差
|
||||
|
||||
一個事務讀取一些東西,根據它所看到的值作出決定,並將該決定寫入資料庫。但是,寫入時,該決定的前提不再是真實的。只有可序列化的隔離才能防止這種異常。
|
||||
一個事務讀取一些東西,根據它所看到的值作出決定,並將該決定寫入資料庫。但是,寫入時,該決定的前提不再是真實的。只有可序列化的隔離才能防止這種異常。
|
||||
|
||||
***幻讀***
|
||||
* 幻讀
|
||||
|
||||
事務讀取符合某些搜尋條件的物件。另一個客戶端進行寫入,影響搜尋結果。快照隔離可以防止直接的幻像讀取,但是寫入偏差上下文中的幻讀需要特殊處理,例如索引範圍鎖定。
|
||||
事務讀取符合某些搜尋條件的物件。另一個客戶端進行寫入,影響搜尋結果。快照隔離可以防止直接的幻像讀取,但是寫入偏差上下文中的幻讀需要特殊處理,例如索引範圍鎖定。
|
||||
|
||||
弱隔離級別可以防止其中一些異常情況,但要求你,也就是應用程式開發人員手動處理剩餘那些(例如,使用顯式鎖定)。只有可序列化的隔離才能防範所有這些問題。我們討論了實現可序列化事務的三種不同方法:
|
||||
|
||||
***字面意義上的序列執行***
|
||||
* 字面意義上的序列執行
|
||||
|
||||
如果每個事務的執行速度非常快,並且事務吞吐量足夠低,足以在單個CPU核上處理,這是一個簡單而有效的選擇。
|
||||
如果每個事務的執行速度非常快,並且事務吞吐量足夠低,足以在單個CPU核上處理,這是一個簡單而有效的選擇。
|
||||
|
||||
***兩階段鎖定***
|
||||
* 兩階段鎖定
|
||||
|
||||
數十年來,兩階段鎖定一直是實現可序列化的標準方式,但是許多應用出於效能問題的考慮避免使用它。
|
||||
數十年來,兩階段鎖定一直是實現可序列化的標準方式,但是許多應用出於效能問題的考慮避免使用它。
|
||||
|
||||
***可序列化快照隔離(SSI)***
|
||||
* 可序列化快照隔離(SSI)
|
||||
|
||||
一個相當新的演算法,避免了先前方法的大部分缺點。它使用樂觀的方法,允許事務執行而無需阻塞。當一個事務想要提交時,它會進行檢查,如果執行不可序列化,事務就會被中止。
|
||||
一個相當新的演算法,避免了先前方法的大部分缺點。它使用樂觀的方法,允許事務執行而無需阻塞。當一個事務想要提交時,它會進行檢查,如果執行不可序列化,事務就會被中止。
|
||||
|
||||
本章中的示例主要是在關係資料模型的上下文中。但是,正如在討論中,無論使用哪種資料模型,如“**[多物件事務的需求](#多物件事務的需求)**”中所討論的,事務都是有價值的資料庫功能。
|
||||
|
||||
|
36
zh-tw/ch8.md
36
zh-tw/ch8.md
@ -549,32 +549,32 @@ Web應用程式確實需要預期受終端使用者控制的客戶端(如Web
|
||||
|
||||
關於時序假設,三種系統模型是常用的:
|
||||
|
||||
***同步模型***
|
||||
* 同步模型
|
||||
|
||||
**同步模型(synchronous model)** 假設網路延遲、程序暫停和和時鐘誤差都是受限的。這並不意味著完全同步的時鐘或零網路延遲;這隻意味著你知道網路延遲、暫停和時鐘漂移將永遠不會超過某個固定的上限【88】。同步模型並不是大多數實際系統的現實模型,因為(如本章所討論的)無限延遲和暫停確實會發生。
|
||||
**同步模型(synchronous model)** 假設網路延遲、程序暫停和和時鐘誤差都是受限的。這並不意味著完全同步的時鐘或零網路延遲;這隻意味著你知道網路延遲、暫停和時鐘漂移將永遠不會超過某個固定的上限【88】。同步模型並不是大多數實際系統的現實模型,因為(如本章所討論的)無限延遲和暫停確實會發生。
|
||||
|
||||
***部分同步模型***
|
||||
* 部分同步模型
|
||||
|
||||
**部分同步(partial synchronous)** 意味著一個系統在大多數情況下像一個同步系統一樣執行,但有時候會超出網路延遲,程序暫停和時鐘漂移的界限【88】。這是很多系統的現實模型:大多數情況下,網路和程序表現良好,否則我們永遠無法完成任何事情,但是我們必須承認,在任何時刻都存在時序假設偶然被破壞的事實。發生這種情況時,網路延遲、暫停和時鐘錯誤可能會變得相當大。
|
||||
**部分同步(partial synchronous)** 意味著一個系統在大多數情況下像一個同步系統一樣執行,但有時候會超出網路延遲,程序暫停和時鐘漂移的界限【88】。這是很多系統的現實模型:大多數情況下,網路和程序表現良好,否則我們永遠無法完成任何事情,但是我們必須承認,在任何時刻都存在時序假設偶然被破壞的事實。發生這種情況時,網路延遲、暫停和時鐘錯誤可能會變得相當大。
|
||||
|
||||
***非同步模型***
|
||||
* 非同步模型
|
||||
|
||||
在這個模型中,一個演算法不允許對時序做任何假設——事實上它甚至沒有時鐘(所以它不能使用超時)。一些演算法被設計為可用於非同步模型,但非常受限。
|
||||
在這個模型中,一個演算法不允許對時序做任何假設——事實上它甚至沒有時鐘(所以它不能使用超時)。一些演算法被設計為可用於非同步模型,但非常受限。
|
||||
|
||||
|
||||
進一步來說,除了時序問題,我們還要考慮**節點失效**。三種最常見的節點系統模型是:
|
||||
|
||||
***崩潰-停止故障***
|
||||
* 崩潰-停止故障
|
||||
|
||||
在**崩潰停止(crash-stop)** 模型中,演算法可能會假設一個節點只能以一種方式失效,即透過崩潰。這意味著節點可能在任意時刻突然停止響應,此後該節點永遠消失——它永遠不會回來。
|
||||
在**崩潰停止(crash-stop)** 模型中,演算法可能會假設一個節點只能以一種方式失效,即透過崩潰。這意味著節點可能在任意時刻突然停止響應,此後該節點永遠消失——它永遠不會回來。
|
||||
|
||||
***崩潰-恢復故障***
|
||||
* 崩潰-恢復故障
|
||||
|
||||
我們假設節點可能會在任何時候崩潰,但也許會在未知的時間之後再次開始響應。在**崩潰-恢復(crash-recovery)** 模型中,假設節點具有穩定的儲存(即,非易失性磁碟儲存)且會在崩潰中保留,而記憶體中的狀態會丟失。
|
||||
我們假設節點可能會在任何時候崩潰,但也許會在未知的時間之後再次開始響應。在**崩潰-恢復(crash-recovery)** 模型中,假設節點具有穩定的儲存(即,非易失性磁碟儲存)且會在崩潰中保留,而記憶體中的狀態會丟失。
|
||||
|
||||
***拜占庭(任意)故障***
|
||||
* 拜占庭(任意)故障
|
||||
|
||||
節點可以做(絕對意義上的)任何事情,包括試圖戲弄和欺騙其他節點,如上一節所述。
|
||||
節點可以做(絕對意義上的)任何事情,包括試圖戲弄和欺騙其他節點,如上一節所述。
|
||||
|
||||
對於真實系統的建模,具有**崩潰-恢復故障(crash-recovery)** 的**部分同步模型(partial synchronous)** 通常是最有用的模型。分散式演算法如何應對這種模型?
|
||||
|
||||
@ -584,17 +584,17 @@ Web應用程式確實需要預期受終端使用者控制的客戶端(如Web
|
||||
|
||||
同樣,我們可以寫下我們想要的分散式演算法的屬性來定義它的正確含義。例如,如果我們正在為一個鎖生成防護令牌(請參閱“[防護令牌](#防護令牌)”),我們可能要求演算法具有以下屬性:
|
||||
|
||||
***唯一性(uniqueness)***
|
||||
* 唯一性(uniqueness)
|
||||
|
||||
沒有兩個防護令牌請求返回相同的值。
|
||||
沒有兩個防護令牌請求返回相同的值。
|
||||
|
||||
***單調序列(monotonic sequence)***
|
||||
* 單調序列(monotonic sequence)
|
||||
|
||||
如果請求 $x$ 返回了令牌 $t_x$,並且請求$y$返回了令牌$t_y$,並且 $x$ 在 $y$ 開始之前已經完成,那麼$t_x <t_y$。
|
||||
如果請求 $x$ 返回了令牌 $t_x$,並且請求$y$返回了令牌$t_y$,並且 $x$ 在 $y$ 開始之前已經完成,那麼$t_x <t_y$。
|
||||
|
||||
***可用性(availability)***
|
||||
* 可用性(availability)
|
||||
|
||||
請求防護令牌並且不會崩潰的節點,最終會收到響應。
|
||||
請求防護令牌並且不會崩潰的節點,最終會收到響應。
|
||||
|
||||
如果一個系統模型中的演算法總是滿足它在所有我們假設可能發生的情況下的性質,那麼這個演算法是正確的。但這如何有意義?如果所有的節點崩潰,或者所有的網路延遲突然變得無限長,那麼沒有任何演算法能夠完成任何事情。
|
||||
|
||||
|
113
zh-tw/ch9.md
113
zh-tw/ch9.md
@ -197,27 +197,27 @@
|
||||
|
||||
使系統容錯最常用的方法是使用複製。我們再來回顧[第五章](ch5.md)中的複製方法,並比較它們是否可以滿足線性一致性:
|
||||
|
||||
***單主複製(可能線性一致)***
|
||||
* 單主複製(可能線性一致)
|
||||
|
||||
在具有單主複製功能的系統中(請參閱“[領導者與追隨者](ch5.md#領導者與追隨者)”),主庫具有用於寫入的資料的主副本,而追隨者在其他節點上保留資料的備份副本。如果從主庫或同步更新的從庫讀取資料,它們**可能(potential)** 是線性一致性的[^iv]。然而,實際上並不是每個單主資料庫都是線性一致性的,無論是因為設計的原因(例如,因為使用了快照隔離)還是因為在併發處理上存在錯誤【10】。
|
||||
在具有單主複製功能的系統中(請參閱“[領導者與追隨者](ch5.md#領導者與追隨者)”),主庫具有用於寫入的資料的主副本,而追隨者在其他節點上保留資料的備份副本。如果從主庫或同步更新的從庫讀取資料,它們**可能(potential)** 是線性一致性的[^iv]。然而,實際上並不是每個單主資料庫都是線性一致性的,無論是因為設計的原因(例如,因為使用了快照隔離)還是因為在併發處理上存在錯誤【10】。
|
||||
|
||||
[^iv]: 對單主資料庫進行分割槽(分片),使得每個分割槽有一個單獨的領導者,不會影響線性一致性,因為線性一致性只是對單一物件的保證。 交叉分割槽事務是一個不同的問題(請參閱“[分散式事務與共識](#分散式事務與共識)”)。
|
||||
[^iv]: 對單主資料庫進行分割槽(分片),使得每個分割槽有一個單獨的領導者,不會影響線性一致性,因為線性一致性只是對單一物件的保證。 交叉分割槽事務是一個不同的問題(請參閱“[分散式事務與共識](#分散式事務與共識)”)。
|
||||
|
||||
從主庫讀取依賴一個假設,你確定地知道領導者是誰。正如在“[真相由多數所定義](ch8.md#真相由多數所定義)”中所討論的那樣,一個節點很可能會認為它是領導者,而事實上並非如此——如果具有錯覺的領導者繼續為請求提供服務,可能違反線性一致性【20】。使用非同步複製,故障切換時甚至可能會丟失已提交的寫入(請參閱“[處理節點宕機](ch5.md#處理節點宕機)”),這同時違反了永續性和線性一致性。
|
||||
從主庫讀取依賴一個假設,你確定地知道領導者是誰。正如在“[真相由多數所定義](ch8.md#真相由多數所定義)”中所討論的那樣,一個節點很可能會認為它是領導者,而事實上並非如此——如果具有錯覺的領導者繼續為請求提供服務,可能違反線性一致性【20】。使用非同步複製,故障切換時甚至可能會丟失已提交的寫入(請參閱“[處理節點宕機](ch5.md#處理節點宕機)”),這同時違反了永續性和線性一致性。
|
||||
|
||||
***共識演算法(線性一致)***
|
||||
* 共識演算法(線性一致)
|
||||
|
||||
一些在本章後面討論的共識演算法,與單領導者複製類似。然而,共識協議包含防止腦裂和陳舊副本的措施。正是由於這些細節,共識演算法可以安全地實現線性一致性儲存。例如,Zookeeper 【21】和etcd 【22】就是這樣工作的。
|
||||
一些在本章後面討論的共識演算法,與單領導者複製類似。然而,共識協議包含防止腦裂和陳舊副本的措施。正是由於這些細節,共識演算法可以安全地實現線性一致性儲存。例如,Zookeeper 【21】和etcd 【22】就是這樣工作的。
|
||||
|
||||
***多主複製(非線性一致)***
|
||||
* 多主複製(非線性一致)
|
||||
|
||||
具有多主程式複製的系統通常不是線性一致的,因為它們同時在多個節點上處理寫入,並將其非同步複製到其他節點。因此,它們可能會產生需要被解決的寫入衝突(請參閱“[處理寫入衝突](ch5.md#處理寫入衝突)”)。這種衝突是因為缺少單一資料副本所導致的。
|
||||
具有多主程式複製的系統通常不是線性一致的,因為它們同時在多個節點上處理寫入,並將其非同步複製到其他節點。因此,它們可能會產生需要被解決的寫入衝突(請參閱“[處理寫入衝突](ch5.md#處理寫入衝突)”)。這種衝突是因為缺少單一資料副本所導致的。
|
||||
|
||||
***無主複製(也許不是線性一致的)***
|
||||
* 無主複製(也許不是線性一致的)
|
||||
|
||||
對於無領導者複製的系統(Dynamo風格;請參閱“[無主複製](ch5.md#無主複製)”),有時候人們會聲稱透過要求法定人數讀寫( $w + r> n$ )可以獲得“強一致性”。這取決於法定人數的具體配置,以及強一致性如何定義(通常不完全正確)。
|
||||
對於無領導者複製的系統(Dynamo風格;請參閱“[無主複製](ch5.md#無主複製)”),有時候人們會聲稱透過要求法定人數讀寫( $w + r> n$ )可以獲得“強一致性”。這取決於法定人數的具體配置,以及強一致性如何定義(通常不完全正確)。
|
||||
|
||||
基於日曆時鐘(例如,在Cassandra中;請參閱“[依賴同步時鐘](ch8.md#依賴同步時鐘)”)的“最後寫入勝利”衝突解決方法幾乎可以確定是非線性一致的,由於時鐘偏差,不能保證時鐘的時間戳與實際事件順序一致。寬鬆的法定人數(請參閱“[寬鬆的法定人數與提示移交](ch5.md#寬鬆的法定人數與提示移交)”)也破壞了線性一致的可能性。即使使用嚴格的法定人數,非線性一致的行為也只是可能的,如下節所示。
|
||||
基於日曆時鐘(例如,在Cassandra中;請參閱“[依賴同步時鐘](ch8.md#依賴同步時鐘)”)的“最後寫入勝利”衝突解決方法幾乎可以確定是非線性一致的,由於時鐘偏差,不能保證時鐘的時間戳與實際事件順序一致。寬鬆的法定人數(請參閱“[寬鬆的法定人數與提示移交](ch5.md#寬鬆的法定人數與提示移交)”)也破壞了線性一致的可能性。即使使用嚴格的法定人數,非線性一致的行為也只是可能的,如下節所示。
|
||||
|
||||
#### 線性一致性和法定人數
|
||||
|
||||
@ -336,13 +336,13 @@ CAP定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一
|
||||
|
||||
全序和偏序之間的差異反映在不同的資料庫一致性模型中:
|
||||
|
||||
***線性一致性***
|
||||
* 線性一致性
|
||||
|
||||
線上性一致的系統中,操作是全序的:如果系統表現的就好像只有一個數據副本,並且所有操作都是原子性的,這意味著對任何兩個操作,我們總是能判定哪個操作先發生。這個全序在[圖9-4](../img/fig9-4.png)中以時間線表示。
|
||||
線上性一致的系統中,操作是全序的:如果系統表現的就好像只有一個數據副本,並且所有操作都是原子性的,這意味著對任何兩個操作,我們總是能判定哪個操作先發生。這個全序在[圖9-4](../img/fig9-4.png)中以時間線表示。
|
||||
|
||||
***因果性***
|
||||
* 因果性
|
||||
|
||||
我們說過,如果兩個操作都沒有在彼此**之前發生**,那麼這兩個操作是併發的(請參閱[“此前發生”的關係和併發](ch5.md#“此前發生”的關係和併發))。換句話說,如果兩個事件是因果相關的(一個發生在另一個事件之前),則它們之間是有序的,但如果它們是併發的,則它們之間的順序是無法比較的。這意味著因果關係定義了一個偏序,而不是一個全序:一些操作相互之間是有順序的,但有些則是無法比較的。
|
||||
我們說過,如果兩個操作都沒有在彼此**之前發生**,那麼這兩個操作是併發的(請參閱[“此前發生”的關係和併發](ch5.md#“此前發生”的關係和併發))。換句話說,如果兩個事件是因果相關的(一個發生在另一個事件之前),則它們之間是有序的,但如果它們是併發的,則它們之間的順序是無法比較的。這意味著因果關係定義了一個偏序,而不是一個全序:一些操作相互之間是有順序的,但有些則是無法比較的。
|
||||
|
||||
因此,根據這個定義,線上性一致的資料儲存中是不存在併發操作的:必須有且僅有一條時間線,所有的操作都在這條時間線上,構成一個全序關係。可能有幾個請求在等待處理,但是資料儲存確保了每個請求都是在唯一時間線上的某個時間點自動處理的,不存在任何併發。
|
||||
|
||||
@ -463,13 +463,13 @@ CAP定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一
|
||||
|
||||
全序廣播通常被描述為在節點間交換訊息的協議。 非正式地講,它要滿足兩個安全屬性:
|
||||
|
||||
***可靠交付(reliable delivery)***
|
||||
* 可靠交付(reliable delivery)
|
||||
|
||||
沒有訊息丟失:如果訊息被傳遞到一個節點,它將被傳遞到所有節點。
|
||||
沒有訊息丟失:如果訊息被傳遞到一個節點,它將被傳遞到所有節點。
|
||||
|
||||
***全序交付(totally ordered delivery)***
|
||||
* 全序交付(totally ordered delivery)
|
||||
|
||||
訊息以相同的順序傳遞給每個節點。
|
||||
訊息以相同的順序傳遞給每個節點。
|
||||
|
||||
正確的全序廣播演算法必須始終保證可靠性和有序性,即使節點或網路出現故障。當然在網路中斷的時候,訊息是傳不出去的,但是演算法可以不斷重試,以便在網路最終修復時,訊息能及時透過並送達(當然它們必須仍然按照正確的順序傳遞)。
|
||||
|
||||
@ -540,16 +540,15 @@ CAP定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一
|
||||
|
||||
節點能達成一致,在很多場景下都非常重要,例如:
|
||||
|
||||
***領導選舉***
|
||||
* 領導選舉
|
||||
|
||||
在單主複製的資料庫中,所有節點需要就哪個節點是領導者達成一致。如果一些節點由於網路故障而無法與其他節點通訊,則可能會對領導權的歸屬引起爭議。在這種情況下,共識對於避免錯誤的故障切換非常重要。錯誤的故障切換會導致兩個節點都認為自己是領導者(**腦裂**,請參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。如果有兩個領導者,它們都會接受寫入,它們的資料會發生分歧,從而導致不一致和資料丟失。
|
||||
在單主複製的資料庫中,所有節點需要就哪個節點是領導者達成一致。如果一些節點由於網路故障而無法與其他節點通訊,則可能會對領導權的歸屬引起爭議。在這種情況下,共識對於避免錯誤的故障切換非常重要。錯誤的故障切換會導致兩個節點都認為自己是領導者(**腦裂**,請參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。如果有兩個領導者,它們都會接受寫入,它們的資料會發生分歧,從而導致不一致和資料丟失。
|
||||
|
||||
***原子提交***
|
||||
* 原子提交
|
||||
|
||||
在支援跨多節點或跨多分割槽事務的資料庫中,一個事務可能在某些節點上失敗,但在其他節點上成功。如果我們想要維護事務的原子性(就ACID而言,請參閱“[原子性](ch7.md#原子性)”),我們必須讓所有節點對事務的結果達成一致:要麼全部中止/回滾(如果出現任何錯誤),要麼它們全部提交(如果沒有出錯)。這個共識的例子被稱為**原子提交(atomic commit)** 問題[^xii]。
|
||||
在支援跨多節點或跨多分割槽事務的資料庫中,一個事務可能在某些節點上失敗,但在其他節點上成功。如果我們想要維護事務的原子性(就ACID而言,請參閱“[原子性](ch7.md#原子性)”),我們必須讓所有節點對事務的結果達成一致:要麼全部中止/回滾(如果出現任何錯誤),要麼它們全部提交(如果沒有出錯)。這個共識的例子被稱為**原子提交(atomic commit)** 問題[^xii]。
|
||||
|
||||
|
||||
[^xii]: 原子提交的形式化與共識稍有不同:原子事務只有在**所有**參與者投票提交的情況下才能提交,如果有任何參與者需要中止,則必須中止。 共識則允許就**任意一個**被參與者提出的候選值達成一致。 然而,原子提交和共識可以相互簡化為對方【70,71】。 **非阻塞**原子提交則要比共識更為困難 —— 請參閱“[三階段提交](#三階段提交)”。
|
||||
[^xii]: 原子提交的形式化與共識稍有不同:原子事務只有在**所有**參與者投票提交的情況下才能提交,如果有任何參與者需要中止,則必須中止。 共識則允許就**任意一個**被參與者提出的候選值達成一致。 然而,原子提交和共識可以相互簡化為對方【70,71】。 **非阻塞**原子提交則要比共識更為困難 —— 請參閱“[三階段提交](#三階段提交)”。
|
||||
|
||||
> ### 共識的不可能性
|
||||
>
|
||||
@ -662,13 +661,13 @@ CAP定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一
|
||||
|
||||
但我們不應該直接忽視分散式事務,而應當更加仔細地審視這些事務,因為從中可以汲取重要的經驗教訓。首先,我們應該精確地說明“**分散式事務**”的含義。兩種截然不同的分散式事務型別經常被混淆:
|
||||
|
||||
***資料庫內部的分散式事務***
|
||||
* 資料庫內部的分散式事務
|
||||
|
||||
一些分散式資料庫(即在其標準配置中使用複製和分割槽的資料庫)支援資料庫節點之間的內部事務。例如,VoltDB和MySQL Cluster的NDB儲存引擎就有這樣的內部事務支援。在這種情況下,所有參與事務的節點都執行相同的資料庫軟體。
|
||||
一些分散式資料庫(即在其標準配置中使用複製和分割槽的資料庫)支援資料庫節點之間的內部事務。例如,VoltDB和MySQL Cluster的NDB儲存引擎就有這樣的內部事務支援。在這種情況下,所有參與事務的節點都執行相同的資料庫軟體。
|
||||
|
||||
***異構分散式事務***
|
||||
* 異構分散式事務
|
||||
|
||||
在**異構(heterogeneous)** 事務中,參與者是由兩種或兩種以上的不同技術組成的:例如來自不同供應商的兩個資料庫,甚至是非資料庫系統(如訊息代理)。跨系統的分散式事務必須確保原子提交,儘管系統可能完全不同。
|
||||
在**異構(heterogeneous)** 事務中,參與者是由兩種或兩種以上的不同技術組成的:例如來自不同供應商的兩個資料庫,甚至是非資料庫系統(如訊息代理)。跨系統的分散式事務必須確保原子提交,儘管系統可能完全不同。
|
||||
|
||||
資料庫內部事務不必與任何其他系統相容,因此它們可以使用任何協議,並能針對特定技術進行特定的最佳化。因此資料庫內部的分散式事務通常工作地很好。另一方面,跨異構技術的事務則更有挑戰性。
|
||||
|
||||
@ -736,21 +735,21 @@ XA事務解決了保持多個參與者(資料系統)相互一致的現實的
|
||||
|
||||
[^xiii]: 這種共識的特殊形式被稱為**統一共識(uniform consensus)**,相當於在具有不可靠故障檢測器的非同步系統中的**常規共識(regular consensus)**【71】。學術文獻通常指的是**程序(process)** 而不是節點,但我們在這裡使用**節點(node)** 來與本書的其餘部分保持一致。
|
||||
|
||||
***一致同意(Uniform agreement)***
|
||||
* 一致同意(Uniform agreement)
|
||||
|
||||
沒有兩個節點的決定不同。
|
||||
沒有兩個節點的決定不同。
|
||||
|
||||
***完整性(Integrity)***
|
||||
* 完整性(Integrity)
|
||||
|
||||
沒有節點決定兩次。
|
||||
沒有節點決定兩次。
|
||||
|
||||
***有效性(Validity)***
|
||||
* 有效性(Validity)
|
||||
|
||||
如果一個節點決定了值 `v` ,則 `v` 由某個節點所提議。
|
||||
如果一個節點決定了值 `v` ,則 `v` 由某個節點所提議。
|
||||
|
||||
***終止(Termination)***
|
||||
* 終止(Termination)
|
||||
|
||||
由所有未崩潰的節點來最終決定值。
|
||||
由所有未崩潰的節點來最終決定值。
|
||||
|
||||
**一致同意**和**完整性**屬性定義了共識的核心思想:所有人都決定了相同的結果,一旦決定了,你就不能改變主意。**有效性**屬性主要是為了排除平凡的解決方案:例如,無論提議了什麼值,你都可以有一個始終決定值為`null`的演算法。;該演算法滿足**一致同意**和**完整性**屬性,但不滿足**有效性**屬性。
|
||||
|
||||
@ -835,21 +834,21 @@ ZooKeeper和etcd被設計為容納少量完全可以放在記憶體中的資料
|
||||
|
||||
ZooKeeper模仿了Google的Chubby鎖服務【14,98】,不僅實現了全序廣播(因此也實現了共識),而且還構建了一組有趣的其他特性,這些特性在構建分散式系統時變得特別有用:
|
||||
|
||||
***線性一致性的原子操作***
|
||||
* 線性一致性的原子操作
|
||||
|
||||
使用原子CAS操作可以實現鎖:如果多個節點同時嘗試執行相同的操作,只有一個節點會成功。共識協議保證了操作的原子性和線性一致性,即使節點發生故障或網路在任意時刻中斷。分散式鎖通常以**租約(lease)** 的形式實現,租約有一個到期時間,以便在客戶端失效的情況下最終能被釋放(請參閱“[程序暫停](ch8.md#程序暫停)”)。
|
||||
使用原子CAS操作可以實現鎖:如果多個節點同時嘗試執行相同的操作,只有一個節點會成功。共識協議保證了操作的原子性和線性一致性,即使節點發生故障或網路在任意時刻中斷。分散式鎖通常以**租約(lease)** 的形式實現,租約有一個到期時間,以便在客戶端失效的情況下最終能被釋放(請參閱“[程序暫停](ch8.md#程序暫停)”)。
|
||||
|
||||
***操作的全序排序***
|
||||
* 操作的全序排序
|
||||
|
||||
如“[領導者和鎖](ch8.md#領導者和鎖)”中所述,當某個資源受到鎖或租約的保護時,你需要一個防護令牌來防止客戶端在程序暫停的情況下彼此衝突。防護令牌是每次鎖被獲取時單調增加的數字。 ZooKeeper透過全序化所有操作來提供這個功能,它為每個操作提供一個單調遞增的事務ID(`zxid`)和版本號(`cversion`)【15】。
|
||||
如“[領導者和鎖](ch8.md#領導者和鎖)”中所述,當某個資源受到鎖或租約的保護時,你需要一個防護令牌來防止客戶端在程序暫停的情況下彼此衝突。防護令牌是每次鎖被獲取時單調增加的數字。 ZooKeeper透過全序化所有操作來提供這個功能,它為每個操作提供一個單調遞增的事務ID(`zxid`)和版本號(`cversion`)【15】。
|
||||
|
||||
***失效檢測***
|
||||
* 失效檢測
|
||||
|
||||
客戶端在ZooKeeper伺服器上維護一個長期會話,客戶端和伺服器週期性地交換心跳包來檢查節點是否還活著。即使連線暫時中斷,或者ZooKeeper節點失效,會話仍保持在活躍狀態。但如果心跳停止的持續時間超出會話超時,ZooKeeper會宣告該會話已死亡。當會話超時時(ZooKeeper稱這些節點為**臨時節點(ephemeral nodes)**),會話持有的任何鎖都可以配置為自動釋放。
|
||||
客戶端在ZooKeeper伺服器上維護一個長期會話,客戶端和伺服器週期性地交換心跳包來檢查節點是否還活著。即使連線暫時中斷,或者ZooKeeper節點失效,會話仍保持在活躍狀態。但如果心跳停止的持續時間超出會話超時,ZooKeeper會宣告該會話已死亡。當會話超時時(ZooKeeper稱這些節點為**臨時節點(ephemeral nodes)**),會話持有的任何鎖都可以配置為自動釋放。
|
||||
|
||||
***變更通知***
|
||||
* 變更通知
|
||||
|
||||
客戶端不僅可以讀取其他客戶端建立的鎖和值,還可以監聽它們的變更。因此,客戶端可以知道另一個客戶端何時加入叢集(基於新客戶端寫入ZooKeeper的值),或發生故障(因其會話超時,而其臨時節點消失)。透過訂閱通知,客戶端不用再透過頻繁輪詢的方式來找出變更。
|
||||
客戶端不僅可以讀取其他客戶端建立的鎖和值,還可以監聽它們的變更。因此,客戶端可以知道另一個客戶端何時加入叢集(基於新客戶端寫入ZooKeeper的值),或發生故障(因其會話超時,而其臨時節點消失)。透過訂閱通知,客戶端不用再透過頻繁輪詢的方式來找出變更。
|
||||
|
||||
在這些功能中,只有線性一致的原子操作才真的需要共識。但正是這些功能的組合,使得像ZooKeeper這樣的系統在分散式協調中非常有用。
|
||||
|
||||
@ -892,29 +891,29 @@ ZooKeeper和它的小夥伴們可以看作是成員資格服務(membership ser
|
||||
|
||||
我們看到,達成共識意味著以這樣一種方式決定某件事:所有節點一致同意所做決定,且這一決定不可撤銷。透過深入挖掘,結果我們發現很廣泛的一系列問題實際上都可以歸結為共識問題,並且彼此等價(從這個意義上來講,如果你有其中之一的解決方案,就可以輕易將它轉換為其他問題的解決方案)。這些等價的問題包括:
|
||||
|
||||
***線性一致性的CAS暫存器***
|
||||
* 線性一致性的CAS暫存器
|
||||
|
||||
暫存器需要基於當前值是否等於操作給出的引數,原子地**決定**是否設定新值。
|
||||
暫存器需要基於當前值是否等於操作給出的引數,原子地**決定**是否設定新值。
|
||||
|
||||
***原子事務提交***
|
||||
* 原子事務提交
|
||||
|
||||
資料庫必須**決定**是否提交或中止分散式事務。
|
||||
資料庫必須**決定**是否提交或中止分散式事務。
|
||||
|
||||
***全序廣播***
|
||||
* 全序廣播
|
||||
|
||||
訊息系統必須**決定**傳遞訊息的順序。
|
||||
訊息系統必須**決定**傳遞訊息的順序。
|
||||
|
||||
***鎖和租約***
|
||||
* 鎖和租約
|
||||
|
||||
當幾個客戶端爭搶鎖或租約時,由鎖來**決定**哪個客戶端成功獲得鎖。
|
||||
當幾個客戶端爭搶鎖或租約時,由鎖來**決定**哪個客戶端成功獲得鎖。
|
||||
|
||||
***成員/協調服務***
|
||||
* 成員/協調服務
|
||||
|
||||
給定某種故障檢測器(例如超時),系統必須**決定**哪些節點活著,哪些節點因為會話超時需要被宣告死亡。
|
||||
給定某種故障檢測器(例如超時),系統必須**決定**哪些節點活著,哪些節點因為會話超時需要被宣告死亡。
|
||||
|
||||
***唯一性約束***
|
||||
* 唯一性約束
|
||||
|
||||
當多個事務同時嘗試使用相同的鍵建立衝突記錄時,約束必須**決定**哪一個被允許,哪些因為違反約束而失敗。
|
||||
當多個事務同時嘗試使用相同的鍵建立衝突記錄時,約束必須**決定**哪一個被允許,哪些因為違反約束而失敗。
|
||||
|
||||
如果你只有一個節點,或者你願意將決策的權能分配給單個節點,所有這些事都很簡單。這就是在單領導者資料庫中發生的事情:所有決策權歸屬於領導者,這就是為什麼這樣的資料庫能夠提供線性一致的操作,唯一性約束,完全有序的複製日誌,以及更多。
|
||||
|
||||
|
@ -11,17 +11,17 @@
|
||||
|
||||
你可能會出於各種各樣的原因,希望將資料庫分佈到多臺機器上:
|
||||
|
||||
***可伸縮性***
|
||||
* 可伸縮性
|
||||
|
||||
如果你的資料量、讀取負載、寫入負載超出單臺機器的處理能力,可以將負載分散到多臺計算機上。
|
||||
如果你的資料量、讀取負載、寫入負載超出單臺機器的處理能力,可以將負載分散到多臺計算機上。
|
||||
|
||||
***容錯/高可用性***
|
||||
* 容錯/高可用性
|
||||
|
||||
如果你的應用需要在單臺機器(或多臺機器,網路或整個資料中心)出現故障的情況下仍然能繼續工作,則可使用多臺機器,以提供冗餘。一臺故障時,另一臺可以接管。
|
||||
如果你的應用需要在單臺機器(或多臺機器,網路或整個資料中心)出現故障的情況下仍然能繼續工作,則可使用多臺機器,以提供冗餘。一臺故障時,另一臺可以接管。
|
||||
|
||||
***延遲***
|
||||
* 延遲
|
||||
|
||||
如果在世界各地都有使用者,你也許會考慮在全球範圍部署多個伺服器,從而每個使用者可以從地理上最近的資料中心獲取服務,避免了等待網路資料包穿越半個世界。
|
||||
如果在世界各地都有使用者,你也許會考慮在全球範圍部署多個伺服器,從而每個使用者可以從地理上最近的資料中心獲取服務,避免了等待網路資料包穿越半個世界。
|
||||
|
||||
## 伸縮至更高的載荷
|
||||
|
||||
@ -51,13 +51,13 @@
|
||||
|
||||
資料分佈在多個節點上有兩種常見的方式:
|
||||
|
||||
***複製(Replication)***
|
||||
* 複製(Replication)
|
||||
|
||||
在幾個不同的節點上儲存資料的相同副本,可能放在不同的位置。 複製提供了冗餘:如果一些節點不可用,剩餘的節點仍然可以提供資料服務。 複製也有助於改善效能。 [第五章](ch5.md)將討論複製。
|
||||
在幾個不同的節點上儲存資料的相同副本,可能放在不同的位置。 複製提供了冗餘:如果一些節點不可用,剩餘的節點仍然可以提供資料服務。 複製也有助於改善效能。 [第五章](ch5.md)將討論複製。
|
||||
|
||||
***分割槽 (Partitioning)***
|
||||
* 分割槽 (Partitioning)
|
||||
|
||||
將一個大型資料庫拆分成較小的子集(稱為**分割槽(partitions)**),從而不同的分割槽可以指派給不同的**節點(node)**(亦稱**分片(shard)**)。 [第六章](ch6.md)將討論分割槽。
|
||||
將一個大型資料庫拆分成較小的子集(稱為**分割槽(partitions)**),從而不同的分割槽可以指派給不同的**節點(node)**(亦稱**分片(shard)**)。 [第六章](ch6.md)將討論分割槽。
|
||||
|
||||
複製和分割槽是不同的機制,但它們經常同時使用。如[圖II-1](../img/figii-1.png)所示。
|
||||
|
||||
|
@ -10,13 +10,13 @@
|
||||
|
||||
從高層次上看,儲存和處理資料的系統可以分為兩大類:
|
||||
|
||||
***記錄系統(System of record)***
|
||||
* 記錄系統(System of record)
|
||||
|
||||
**記錄系統**,也被稱為**真相源(source of truth)**,持有資料的權威版本。當新的資料進入時(例如,使用者輸入)首先會記錄在這裡。每個事實正正好好表示一次(表示通常是**正規化的(normalized)**)。如果其他系統和**記錄系統**之間存在任何差異,那麼記錄系統中的值是正確的(根據定義)。
|
||||
**記錄系統**,也被稱為**真相源(source of truth)**,持有資料的權威版本。當新的資料進入時(例如,使用者輸入)首先會記錄在這裡。每個事實正正好好表示一次(表示通常是**正規化的(normalized)**)。如果其他系統和**記錄系統**之間存在任何差異,那麼記錄系統中的值是正確的(根據定義)。
|
||||
|
||||
***衍生資料系統(Derived data systems)***
|
||||
* 衍生資料系統(Derived data systems)
|
||||
|
||||
**衍生系統**中的資料,通常是另一個系統中的現有資料以某種方式進行轉換或處理的結果。如果丟失衍生資料,可以從原始來源重新建立。典型的例子是**快取(cache)**:如果資料在快取中,就可以由快取提供服務;如果快取不包含所需資料,則降級由底層資料庫提供。非規範化的值,索引和物化檢視亦屬此類。在推薦系統中,預測彙總資料通常衍生自使用者日誌。
|
||||
**衍生系統**中的資料,通常是另一個系統中的現有資料以某種方式進行轉換或處理的結果。如果丟失衍生資料,可以從原始來源重新建立。典型的例子是**快取(cache)**:如果資料在快取中,就可以由快取提供服務;如果快取不包含所需資料,則降級由底層資料庫提供。非規範化的值,索引和物化檢視亦屬此類。在推薦系統中,預測彙總資料通常衍生自使用者日誌。
|
||||
|
||||
從技術上講,衍生資料是**冗餘的(redundant)**,因為它重複了已有的資訊。但是衍生資料對於獲得良好的只讀查詢效能通常是至關重要的。它通常是非規範化的。可以從單個源頭衍生出多個不同的資料集,使你能從不同的“視角”洞察資料。
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user