mirror of
https://github.com/Vonng/ddia.git
synced 2025-03-06 15:40:11 +08:00
remove nested parentheses
This commit is contained in:
parent
07c0c84057
commit
2014894f76
4
ch1.md
4
ch1.md
@ -97,7 +97,7 @@
|
||||
|
||||
直到最近,硬件冗余对于大多数应用来说已经足够了,它使单台机器完全失效变得相当罕见。只要你能快速地把备份恢复到新机器上,故障停机时间对大多数应用而言都算不上灾难性的。只有少量高可用性至关重要的应用才会要求有多套硬件冗余。
|
||||
|
||||
但是随着数据量和应用计算需求的增加,越来越多的应用开始大量使用机器,这会相应地增加硬件故障率。此外在一些云平台(**如亚马逊网络服务(AWS, Amazon Web Services)**)中,虚拟机实例不可用却没有任何警告也是很常见的【7】,因为云平台的设计就是优先考虑**灵活性(flexibility)** 和**弹性(elasticity)**[^i],而不是单机可靠性。
|
||||
但是随着数据量和应用计算需求的增加,越来越多的应用开始大量使用机器,这会相应地增加硬件故障率。此外,在类似亚马逊AWS(Amazon Web Services)的一些云服务平台上,虚拟机实例不可用却没有任何警告也是很常见的【7】,因为云平台的设计就是优先考虑**灵活性(flexibility)** 和**弹性(elasticity)**[^i],而不是单机可靠性。
|
||||
|
||||
如果在硬件冗余的基础上进一步引入软件容错机制,那么系统在容忍整个(单台)机器故障的道路上就更进一步了。这样的系统也有运维上的便利,例如:如果需要重启机器(例如应用操作系统安全补丁),单服务器系统就需要计划停机。而允许机器失效的系统则可以一次修复一个节点,无需整个系统停机。
|
||||
|
||||
@ -226,7 +226,7 @@
|
||||
|
||||
为了弄清异常值有多糟糕,可以看看更高的百分位点,例如第95、99和99.9百分位点(缩写为p95,p99和p999)。它们意味着95%,99%或99.9%的请求响应时间要比该阈值快,例如:如果第95百分位点响应时间是1.5秒,则意味着100个请求中的95个响应时间快于1.5秒,而100个请求中的5个响应时间超过1.5秒。如[图1-4](img/fig1-4.png)所示。
|
||||
|
||||
响应时间的高百分位点(也称为**尾部延迟(tail latencies)**)非常重要,因为它们直接影响用户的服务体验。例如亚马逊在描述内部服务的响应时间要求时以99.9百分位点为准,即使它只影响一千个请求中的一个。这是因为请求响应最慢的客户往往也是数据最多的客户,也可以说是最有价值的客户 —— 因为他们掏钱了【19】。保证网站响应迅速对于保持客户的满意度非常重要,亚马逊观察到:响应时间增加100毫秒,销售量就减少1%【20】;而另一些报告说:慢 1 秒钟会让客户满意度指标减少16%【21,22】。
|
||||
响应时间的高百分位点(也称为**尾部延迟**,即**tail latencies**)非常重要,因为它们直接影响用户的服务体验。例如亚马逊在描述内部服务的响应时间要求时以99.9百分位点为准,即使它只影响一千个请求中的一个。这是因为请求响应最慢的客户往往也是数据最多的客户,也可以说是最有价值的客户 —— 因为他们掏钱了【19】。保证网站响应迅速对于保持客户的满意度非常重要,亚马逊观察到:响应时间增加100毫秒,销售量就减少1%【20】;而另一些报告说:慢 1 秒钟会让客户满意度指标减少16%【21,22】。
|
||||
|
||||
另一方面,优化第99.99百分位点(一万个请求中最慢的一个)被认为太昂贵了,不能为亚马逊的目标带来足够好处。减小高百分位点处的响应时间相当困难,因为它很容易受到随机事件的影响,这超出了控制范围,而且效益也很小。
|
||||
|
||||
|
8
ch10.md
8
ch10.md
@ -250,7 +250,7 @@ MapReduce与Unix命令管道的主要区别在于,MapReduce可以在多台机
|
||||
|
||||
在大多数情况下,应该在Mapper任务中运行的应用代码在将要运行它的机器上还不存在,所以MapReduce框架首先将代码(例如Java程序中的JAR文件)复制到适当的机器。然后启动Map任务并开始读取输入文件,一次将一条记录传入Mapper回调函数。Mapper的输出由键值对组成。
|
||||
|
||||
计算的Reduce端也被分区。虽然Map任务的数量由输入文件块的数量决定,但Reducer的任务的数量是由作业作者配置的(它可以不同于Map任务的数量)。为了确保具有相同键的所有键值对最终落在相同的Reducer处,框架使用键的散列值来确定哪个Reduce任务应该接收到特定的键值对(请参阅“[根据键的散列分区](ch6.md#根据键的散列分区)”))。
|
||||
计算的Reduce端也被分区。虽然Map任务的数量由输入文件块的数量决定,但Reducer的任务的数量是由作业作者配置的(它可以不同于Map任务的数量)。为了确保具有相同键的所有键值对最终落在相同的Reducer处,框架使用键的散列值来确定哪个Reduce任务应该接收到特定的键值对(请参阅“[根据键的散列分区](ch6.md#根据键的散列分区)”)。
|
||||
|
||||
键值对必须进行排序,但数据集可能太大,无法在单台机器上使用常规排序算法进行排序。相反,分类是分阶段进行的。首先每个Map任务都按照Reducer对输出进行分区。每个分区都被写入Mapper程序的本地磁盘,使用的技术与我们在“[SSTables与LSM树](ch3.md#SSTables与LSM树)”中讨论的类似。
|
||||
|
||||
@ -290,7 +290,7 @@ Hadoop的各种高级工具(如Pig 【30】,Hive 【31】,Cascading 【32
|
||||
|
||||
#### 示例:用户活动事件分析
|
||||
|
||||
[图10-2](img/fig10-2.png)给出了一个批处理作业中连接的典型例子。左侧是事件日志,描述登录用户在网站上做的事情(称为**活动事件(activity events)** 或**点击流数据(clickstream data)**),右侧是用户数据库。 你可以将此示例看作是星型模式的一部分(请参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日志是事实表,用户数据库是其中的一个维度。
|
||||
[图10-2](img/fig10-2.png)给出了一个批处理作业中连接的典型例子。左侧是事件日志,描述登录用户在网站上做的事情(称为**活动事件**,即activity events,或**点击流数据**,即clickstream data),右侧是用户数据库。 你可以将此示例看作是星型模式的一部分(请参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日志是事实表,用户数据库是其中的一个维度。
|
||||
|
||||

|
||||
|
||||
@ -344,7 +344,7 @@ Hadoop的各种高级工具(如Pig 【30】,Hive 【31】,Cascading 【32
|
||||
|
||||
如果存在与单个键关联的大量数据,则“将具有相同键的所有记录放到相同的位置”这种模式就被破坏了。例如在社交网络中,大多数用户可能会与几百人有连接,但少数名人可能有数百万的追随者。这种不成比例的活动数据库记录被称为**关键对象(linchpin object)**【38】或**热键(hot key)**。
|
||||
|
||||
在单个Reducer中收集与某个名人相关的所有活动(例如他们发布内容的回复)可能导致严重的**偏斜**(也称为**热点(hot spot)**)—— 也就是说,一个Reducer必须比其他Reducer处理更多的记录(请参阅“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)“)。由于MapReduce作业只有在所有Mapper和Reducer都完成时才完成,所有后续作业必须等待最慢的Reducer才能启动。
|
||||
在单个Reducer中收集与某个名人相关的所有活动(例如他们发布内容的回复)可能导致严重的**偏斜**(也称为**热点**,即hot spot)—— 也就是说,一个Reducer必须比其他Reducer处理更多的记录(请参阅“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)“)。由于MapReduce作业只有在所有Mapper和Reducer都完成时才完成,所有后续作业必须等待最慢的Reducer才能启动。
|
||||
|
||||
如果连接的输入存在热键,可以使用一些算法进行补偿。例如,Pig中的**偏斜连接(skewed join)** 方法首先运行一个抽样作业(Sampling Job)来确定哪些键是热键【39】。连接实际执行时,Mapper会将热键的关联记录**随机**(相对于传统MapReduce基于键散列的确定性方法)发送到几个Reducer之一。对于另外一侧的连接输入,与热键相关的记录需要被复制到**所有**处理该键的Reducer上【40】。
|
||||
|
||||
@ -603,7 +603,7 @@ Spark,Flink和Tez避免将中间状态写入HDFS,因此它们采取了不同
|
||||
|
||||
> 像Spark,Flink和Tez这样的数据流引擎(请参阅“[物化中间状态](#物化中间状态)”)通常将算子作为**有向无环图(DAG)** 的一部分安排在作业中。这与图处理不一样:在数据流引擎中,**从一个算子到另一个算子的数据流**被构造成一个图,而数据本身通常由关系型元组构成。在图处理中,数据本身具有图的形式。又一个不幸的命名混乱!
|
||||
|
||||
许多图算法是通过一次遍历一条边来表示的,将一个顶点与近邻的顶点连接起来,以传播一些信息,并不断重复,直到满足一些条件为止 —— 例如,直到没有更多的边要跟进,或直到一些指标收敛。我们在[图2-6](img/fig2-6.png)中看到一个例子,它通过重复跟进标明地点归属关系的边,生成了数据库中北美包含的所有地点列表(这种算法被称为**传递闭包(transitive closure)**)。
|
||||
许多图算法是通过一次遍历一条边来表示的,将一个顶点与近邻的顶点连接起来,以传播一些信息,并不断重复,直到满足一些条件为止 —— 例如,直到没有更多的边要跟进,或直到一些指标收敛。我们在[图2-6](img/fig2-6.png)中看到一个例子,它通过重复跟进标明地点归属关系的边,生成了数据库中北美包含的所有地点列表(这种算法被称为**传递闭包**,即transitive closure)。
|
||||
|
||||
可以在分布式文件系统中存储图(包含顶点和边的列表的文件),但是这种“重复至完成”的想法不能用普通的MapReduce来表示,因为它只扫过一趟数据。这种算法因此经常以**迭代**的风格实现:
|
||||
|
||||
|
10
ch11.md
10
ch11.md
@ -75,7 +75,7 @@
|
||||
|
||||
#### 消息代理
|
||||
|
||||
一种广泛使用的替代方法是通过**消息代理(message broker)**(也称为**消息队列(message queue)**)发送消息,消息代理实质上是一种针对处理消息流而优化的数据库。它作为服务器运行,生产者和消费者作为客户端连接到服务器。生产者将消息写入代理,消费者通过从代理那里读取来接收消息。
|
||||
一种广泛使用的替代方法是通过**消息代理**(message broker,也称为**消息队列**,即message queue)发送消息,消息代理实质上是一种针对处理消息流而优化的数据库。它作为服务器运行,生产者和消费者作为客户端连接到服务器。生产者将消息写入代理,消费者通过从代理那里读取来接收消息。
|
||||
|
||||
通过将数据集中在代理上,这些系统可以更容易地容忍来来去去的客户端(连接,断开连接和崩溃),而持久性问题则转移到代理的身上。一些消息代理只将消息保存在内存中,而另一些消息代理(取决于配置)将其写入磁盘,以便在代理崩溃的情况下不会丢失。针对缓慢的消费者,它们通常会允许无上限的排队(而不是丢弃消息或背压),尽管这种选择也可能取决于配置。
|
||||
|
||||
@ -98,7 +98,7 @@
|
||||
|
||||
* 负载均衡(load balancing)
|
||||
|
||||
每条消息都被传递给消费者**之一**,所以处理该主题下消息的工作能被多个消费者共享。代理可以为消费者任意分配消息。当处理消息的代价高昂,希望能并行处理消息时,此模式非常有用(在AMQP中,可以通过让多个客户端从同一个队列中消费来实现负载均衡,而在JMS中则称之为**共享订阅(shared subscription)**)。
|
||||
每条消息都被传递给消费者**之一**,所以处理该主题下消息的工作能被多个消费者共享。代理可以为消费者任意分配消息。当处理消息的代价高昂,希望能并行处理消息时,此模式非常有用(在AMQP中,可以通过让多个客户端从同一个队列中消费来实现负载均衡,而在JMS中则称之为**共享订阅**,即shared subscription)。
|
||||
|
||||
* 扇出(fan-out)
|
||||
|
||||
@ -276,7 +276,7 @@ LinkedIn的Databus【25】,Facebook的Wormhole【26】和Yahoo!的Sherpa【27
|
||||
|
||||
我们之前在“[散列索引](ch3.md#散列索引)”中关于日志结构存储引擎的上下文中讨论了日志压缩(请参阅[图3-2](img/fig3-2.png)的示例)。原理很简单:存储引擎定期在日志中查找具有相同键的记录,丢掉所有重复的内容,并只保留每个键的最新更新。这个压缩与合并过程在后台运行。
|
||||
|
||||
在日志结构存储引擎中,具有特殊值NULL(**墓碑(tombstone)**)的更新表示该键被删除,并会在日志压缩过程中被移除。但只要键不被覆盖或删除,它就会永远留在日志中。这种压缩日志所需的磁盘空间仅取决于数据库的当前内容,而不取决于数据库中曾经发生的写入次数。如果相同的键经常被覆盖写入,则先前的值将最终将被垃圾回收,只有最新的值会保留下来。
|
||||
在日志结构存储引擎中,具有特殊值NULL(**墓碑**,即tombstone)的更新表示该键被删除,并会在日志压缩过程中被移除。但只要键不被覆盖或删除,它就会永远留在日志中。这种压缩日志所需的磁盘空间仅取决于数据库的当前内容,而不取决于数据库中曾经发生的写入次数。如果相同的键经常被覆盖写入,则先前的值将最终将被垃圾回收,只有最新的值会保留下来。
|
||||
|
||||
在基于日志的消息代理与变更数据捕获的上下文中也适用相同的想法。如果CDC系统被配置为,每个变更都包含一个主键,且每个键的更新都替换了该键以前的值,那么只需要保留对键的最新写入就足够了。
|
||||
|
||||
@ -491,7 +491,7 @@ CEP的实现包括Esper【69】,IBM InfoSphere Streams【70】,Apama,TIBCO
|
||||
|
||||
批处理可以在几分钟内读取一年的历史事件;在大多数情况下,感兴趣的时间线是历史中的一年,而不是处理中的几分钟。而且使用事件中的时间戳,使得处理是**确定性**的:在相同的输入上再次运行相同的处理过程会得到相同的结果(请参阅“[容错](ch10.md#容错)”)。
|
||||
|
||||
另一方面,许多流处理框架使用处理机器上的本地系统时钟(**处理时间(processing time)**)来确定**窗口(windowing)**【79】。这种方法的优点是简单,如果事件创建与事件处理之间的延迟可以忽略不计,那也是合理的。然而,如果存在任何显著的处理延迟 —— 即,事件处理显著地晚于事件实际发生的时间,这种处理方式就失效了。
|
||||
另一方面,许多流处理框架使用处理机器上的本地系统时钟(**处理时间**,即processing time)来确定**窗口(windowing)**【79】。这种方法的优点是简单,如果事件创建与事件处理之间的延迟可以忽略不计,那也是合理的。然而,如果存在任何显著的处理延迟 —— 即,事件处理显著地晚于事件实际发生的时间,这种处理方式就失效了。
|
||||
|
||||
#### 事件时间与处理时间
|
||||
|
||||
@ -706,7 +706,7 @@ Storm的Trident基于类似的想法来处理状态【78】。依赖幂等性意
|
||||
|
||||
* 流流连接
|
||||
|
||||
两个输入流都由活动事件组成,而连接算子在某个时间窗口内搜索相关的事件。例如,它可能会将同一个用户30分钟内进行的两个活动联系在一起。如果你想要找出一个流内的相关事件,连接的两侧输入可能实际上都是同一个流(**自连接(self-join)**)。
|
||||
两个输入流都由活动事件组成,而连接算子在某个时间窗口内搜索相关的事件。例如,它可能会将同一个用户30分钟内进行的两个活动联系在一起。如果你想要找出一个流内的相关事件,连接的两侧输入可能实际上都是同一个流(**自连接**,即self-join)。
|
||||
|
||||
* 流表连接
|
||||
|
||||
|
6
ch3.md
6
ch3.md
@ -80,7 +80,7 @@ $ cat database
|
||||
|
||||
让我们从**键值数据(key-value Data)** 的索引开始。这不是你可以索引的唯一数据类型,但键值数据是很常见的。对于更复杂的索引来说,这也是一个有用的构建模块。
|
||||
|
||||
键值存储与在大多数编程语言中可以找到的**字典(dictionary)** 类型非常相似,通常字典都是用**散列映射(hash map)**(或**散列表(hash table)**)实现的。散列映射在许多算法教科书中都有描述【1,2】,所以这里我们不会讨论它的工作细节。既然我们已经可以用散列映射来表示**内存中**的数据结构,为什么不使用它来索引**硬盘上**的数据呢?
|
||||
键值存储与在大多数编程语言中可以找到的**字典(dictionary)** 类型非常相似,通常字典都是用**散列映射(hash map)**或**散列表(hash table)**实现的。散列映射在许多算法教科书中都有描述【1,2】,所以这里我们不会讨论它的工作细节。既然我们已经可以用散列映射来表示**内存中**的数据结构,为什么不使用它来索引**硬盘上**的数据呢?
|
||||
|
||||
假设我们的数据存储只是一个追加写入的文件,就像前面的例子一样,那么最简单的索引策略就是:保留一个内存中的散列映射,其中每个键都映射到数据文件中的一个字节偏移量,指明了可以找到对应值的位置,如[图3-1](img/fig3-1.png)所示。当你将新的键值对追加写入文件中时,还要更新散列映射,以反映刚刚写入的数据的偏移量(这同时适用于插入新键与更新现有键)。当你想查找一个值时,使用散列映射来查找数据文件中的偏移量,**寻找(seek)** 该位置并读取该值即可。
|
||||
|
||||
@ -193,7 +193,7 @@ $ cat database
|
||||
|
||||
最初这种索引结构是由Patrick O'Neil等人描述的,且被命名为日志结构合并树(或LSM树)【10】,它是基于更早之前的日志结构文件系统【11】来构建的。基于这种合并和压缩排序文件原理的存储引擎通常被称为LSM存储引擎。
|
||||
|
||||
Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使用类似的方法来存储它的关键词词典【12,13】。全文索引比键值索引复杂得多,但是基于类似的想法:在搜索查询中给出一个单词,找到提及单词的所有文档(网页,产品描述等)。这是通过键值结构实现的,其中键是单词(**关键词(term)**),值是所有包含该单词的文档的ID列表(记录列表)。在Lucene中,从术语到记录列表的这种映射保存在类似于SSTable的有序文件中,并根据需要在后台合并【14】。
|
||||
Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使用类似的方法来存储它的关键词词典【12,13】。全文索引比键值索引复杂得多,但是基于类似的想法:在搜索查询中给出一个单词,找到提及单词的所有文档(网页,产品描述等)。这是通过键值结构实现的,其中键是单词(或**词语**,即term),值是所有包含该单词的文档的ID列表(记录列表)。在Lucene中,从词语到记录列表的这种映射保存在类似于SSTable的有序文件中,并根据需要在后台合并【14】。
|
||||
|
||||
#### 性能优化
|
||||
|
||||
@ -246,7 +246,7 @@ B树的基本底层写操作是用新数据覆写硬盘上的页面,并假定
|
||||
|
||||
而且,一些操作需要覆写几个不同的页面。例如,如果因为插入导致页面过满而拆分页面,则需要写入新拆分的两个页面,并覆写其父页面以更新对两个子页面的引用。这是一个危险的操作,因为如果数据库在仅有部分页面被写入时崩溃,那么最终将导致一个损坏的索引(例如,可能有一个孤儿页面不是任何父项的子项) 。
|
||||
|
||||
为了使数据库能处理异常崩溃的场景,B树实现通常会带有一个额外的硬盘数据结构:**预写式日志(WAL, write-ahead log)**(也称为**重做日志(redo log)**)。这是一个仅追加的文件,每个B树的修改在其能被应用到树本身的页面之前都必须先写入到该文件。当数据库在崩溃后恢复时,这个日志将被用来使B树恢复到一致的状态【5,20】。
|
||||
为了使数据库能处理异常崩溃的场景,B树实现通常会带有一个额外的硬盘数据结构:**预写式日志**(WAL,即write-ahead log,也称为**重做日志**,即redo log)。这是一个仅追加的文件,每个B树的修改在其能被应用到树本身的页面之前都必须先写入到该文件。当数据库在崩溃后恢复时,这个日志将被用来使B树恢复到一致的状态【5,20】。
|
||||
|
||||
另外还有一个更新页面的复杂情况是,如果多个线程要同时访问B树,则需要仔细的并发控制 —— 否则线程可能会看到树处于不一致的状态。这通常是通过使用**锁存器(latches)**(轻量级锁)保护树的数据结构来完成。日志结构化的方法在这方面更简单,因为它们在后台进行所有的合并,而不会干扰新接收到的查询,并且能够时不时地将旧的段原子交换为新的段。
|
||||
|
||||
|
4
ch4.md
4
ch4.md
@ -15,7 +15,7 @@
|
||||
|
||||
在大多数情况下,修改应用程序的功能也意味着需要更改其存储的数据:可能需要使用新的字段或记录类型,或者以新方式展示现有数据。
|
||||
|
||||
我们在[第二章](ch2.md)讨论的数据模型有不同的方法来应对这种变化。关系数据库通常假定数据库中的所有数据都遵循一个模式:尽管可以更改该模式(通过模式迁移,即`ALTER`语句),但是在任何时间点都有且仅有一个正确的模式。相比之下,**读时模式(schema-on-read)**(或 **无模式(schemaless)**)数据库不会强制一个模式,因此数据库可以包含在不同时间写入的新老数据格式的混合(请参阅 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)” )。
|
||||
我们在[第二章](ch2.md)讨论的数据模型有不同的方法来应对这种变化。关系数据库通常假定数据库中的所有数据都遵循一个模式:尽管可以更改该模式(通过模式迁移,即`ALTER`语句),但是在任何时间点都有且仅有一个正确的模式。相比之下,**读时模式(schema-on-read)**(或**无模式**,即schemaless)数据库不会强制一个模式,因此数据库可以包含在不同时间写入的新老数据格式的混合(请参阅 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)” )。
|
||||
|
||||
当数据**格式(format)** 或**模式(schema)** 发生变化时,通常需要对应用程序代码进行相应的更改(例如,为记录添加新字段,然后修改程序开始读写该字段)。但在大型应用程序中,代码变更通常不会立即完成:
|
||||
|
||||
@ -417,7 +417,7 @@ Web服务仅仅是通过网络进行API请求的一系列技术的最新版本
|
||||
|
||||
* 本地函数调用是可预测的,并且成功或失败仅取决于受你控制的参数。网络请求是不可预知的:由于网络问题,请求或响应可能会丢失,或者远程计算机可能很慢或不可用,这些问题完全不在你的控制范围之内。网络问题是常见的,所以你必须预测他们,例如通过重试失败的请求。
|
||||
* 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过。 (我们将在[第八章](ch8.md)更详细地讨论这个问题。)
|
||||
* 如果你重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非你在协议中引入除重( **幂等(idempotence)**)机制。本地函数调用没有这个问题。 (在[第十一章](ch11.md)更详细地讨论幂等性)
|
||||
* 如果你重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非你在协议中引入除重(**幂等**,即idempotence)机制。本地函数调用没有这个问题。 (在[第十一章](ch11.md)更详细地讨论幂等性)
|
||||
* 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的:好的时候它可能会在不到一毫秒的时间内完成,但是当网络拥塞或者远程服务超载时,可能需要几秒钟的时间完成一样的东西。
|
||||
* 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。如果参数是像数字或字符串这样的基本类型倒是没关系,但是对于较大的对象很快就会变成问题。
|
||||
|
||||
|
6
ch5.md
6
ch5.md
@ -128,7 +128,7 @@
|
||||
|
||||
#### 基于语句的复制
|
||||
|
||||
在最简单的情况下,主库记录下它执行的每个写入请求(**语句(statement)**)并将该语句日志发送给其从库。对于关系数据库来说,这意味着每个`INSERT`,`UPDATE`或`DELETE`语句都被转发给每个从库,每个从库解析并执行该SQL语句,就像从客户端收到一样。
|
||||
在最简单的情况下,主库记录下它执行的每个写入请求(**语句**,即statement)并将该语句日志发送给其从库。对于关系数据库来说,这意味着每个`INSERT`,`UPDATE`或`DELETE`语句都被转发给每个从库,每个从库解析并执行该SQL语句,就像从客户端收到一样。
|
||||
|
||||
虽然听上去很合理,但有很多问题会搞砸这种复制方式:
|
||||
|
||||
@ -274,7 +274,7 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志
|
||||
|
||||
防止这种异常,需要另一种类型的保证:**一致前缀读(consistent prefix reads)**【23】。 这个保证说:如果一系列写入按某个顺序发生,那么任何人读取这些写入时,也会看见它们以同样的顺序出现。
|
||||
|
||||
这是**分区(partitioned)**(**分片(sharded)**)数据库中的一个特殊问题,将在[第六章](ch6.md)中讨论。如果数据库总是以相同的顺序应用写入,则读取总是会看到一致的前缀,所以这种异常不会发生。但是在许多分布式数据库中,不同的分区独立运行,因此不存在**全局写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
|
||||
这是**分区(partitioned)** 或 **分片(sharded)** 数据库中的一个特殊问题,将在[第六章](ch6.md)中讨论。如果数据库总是以相同的顺序应用写入,则读取总是会看到一致的前缀,所以这种异常不会发生。但是在许多分布式数据库中,不同的分区独立运行,因此不存在**全局写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些处于较新的状态。
|
||||
|
||||
一种解决方案是,确保任何因果相关的写入都写入相同的分区。对于某些无法高效完成这种操作的应用,还有一些显式跟踪因果依赖关系的算法,本书将在“[“此前发生”的关系和并发](#“此前发生”的关系和并发)”一节中返回这个主题。
|
||||
|
||||
@ -700,7 +700,7 @@ LWW实现了最终收敛的目标,但以**持久性**为代价:如果同一
|
||||
|
||||
所有副本的版本号集合称为**版本向量(version vector)**【56】。这个想法的一些变体正在被使用,但最有趣的可能是在Riak 2.0 【58,59】中使用的**虚线版本向量(dotted version vector)**【57】。我们不会深入细节,但是它的工作方式与我们在购物车示例中看到的非常相似。
|
||||
|
||||
与[图5-13](img/fig5-13.png)中的版本号一样,当读取值时,版本向量会从数据库副本发送到客户端,并且随后写入值时需要将其发送回数据库。(Riak将版本向量编码为一个字符串,它称为**因果上下文(causal context)**)。版本向量允许数据库区分覆盖写入和并发写入。
|
||||
与[图5-13](img/fig5-13.png)中的版本号一样,当读取值时,版本向量会从数据库副本发送到客户端,并且随后写入值时需要将其发送回数据库。(Riak将版本向量编码为一个字符串,并称其为**因果上下文**,即causal context)。版本向量允许数据库区分覆盖写入和并发写入。
|
||||
|
||||
另外,就像在单个副本中的情况一样,应用程序可能需要合并并发值。版本向量结构能够确保从一个副本读取并随后写回到另一个副本是安全的。这样做虽然可能会在其他副本上面创建数据,但只要能正确合并就不会丢失数据。
|
||||
|
||||
|
8
ch6.md
8
ch6.md
@ -85,7 +85,7 @@
|
||||
|
||||
**图6-3 按哈希键分区**
|
||||
|
||||
这种技术擅长在分区之间公平地分配键。分区边界可以是均匀间隔的,也可以是伪随机选择的(在这种情况下,该技术有时也被称为**一致性哈希(consistent hashing)**)。
|
||||
这种技术擅长在分区之间公平地分配键。分区边界可以是均匀间隔的,也可以是伪随机选择的(在这种情况下,该技术有时也被称为**一致性哈希**,即consistent hashing)。
|
||||
|
||||
> #### 一致性哈希
|
||||
>
|
||||
@ -135,7 +135,7 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
|
||||
|
||||
**图6-4 基于文档的次级索引进行分区**
|
||||
|
||||
在这种索引方法中,每个分区是完全独立的:每个分区维护自己的次级索引,仅覆盖该分区中的文档。它不关心存储在其他分区的数据。无论何时你需要写入数据库(添加,删除或更新文档),只需处理包含你正在编写的文档ID的分区即可。出于这个原因,**文档分区索引**也被称为**本地索引(local index)**(而不是将在下一节中描述的**全局索引(global index)**)。
|
||||
在这种索引方法中,每个分区是完全独立的:每个分区维护自己的次级索引,仅覆盖该分区中的文档。它不关心存储在其他分区的数据。无论何时你需要写入数据库(添加,删除或更新文档),只需处理包含你正在编写的文档ID的分区即可。出于这个原因,**文档分区索引**也被称为**本地索引**(而不是将在下一节中描述的**全局索引**)。
|
||||
|
||||
但是,从文档分区索引中读取需要注意:除非你对文档ID做了特别的处理,否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在[图6-4](img/fig6-4.png)中,红色汽车出现在分区0和分区1中。因此,如果要搜索红色汽车,则需要将查询发送到所有分区,并合并所有返回的结果。
|
||||
|
||||
@ -224,7 +224,7 @@ Cassandra采取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
|
||||
|
||||
动态分区的一个优点是分区数量适应总数据量。如果只有少量的数据,少量的分区就足够了,所以开销很小;如果有大量的数据,每个分区的大小被限制在一个可配置的最大值【23】。
|
||||
|
||||
需要注意的是,一个空的数据库从一个分区开始,因为没有关于在哪里绘制分区边界的先验信息。数据集开始时很小,直到达到第一个分区的分割点,所有写入操作都必须由单个节点处理,而其他节点则处于空闲状态。为了解决这个问题,HBase和MongoDB允许在一个空的数据库上配置一组初始分区(这被称为**预分割(pre-splitting)**)。在键范围分区的情况中,预分割需要提前知道键是如何进行分配的【4,26】。
|
||||
需要注意的是,一个空的数据库从一个分区开始,因为没有关于在哪里绘制分区边界的先验信息。数据集开始时很小,直到达到第一个分区的分割点,所有写入操作都必须由单个节点处理,而其他节点则处于空闲状态。为了解决这个问题,HBase和MongoDB允许在一个空的数据库上配置一组初始分区(这被称为**预分割**,即pre-splitting)。在键范围分区的情况中,预分割需要提前知道键是如何进行分配的【4,26】。
|
||||
|
||||
动态分区不仅适用于数据的范围分区,而且也适用于散列分区。从版本2.4开始,MongoDB同时支持范围和散列分区,并且都支持动态分割分区。
|
||||
|
||||
@ -258,7 +258,7 @@ Cassandra和Ketama使用的第三种方法是使分区数与节点数成正比
|
||||
|
||||
概括来说,这个问题有几种不同的方案(如图6-7所示):
|
||||
|
||||
1. 允许客户联系任何节点(例如,通过**循环策略的负载均衡(Round-Robin Load Balancer)**)。如果该节点恰巧拥有请求的分区,则它可以直接处理该请求;否则,它将请求转发到适当的节点,接收回复并传递给客户端。
|
||||
1. 允许客户联系任何节点(例如,通过**循环策略的负载均衡**,即Round-Robin Load Balancer)。如果该节点恰巧拥有请求的分区,则它可以直接处理该请求;否则,它将请求转发到适当的节点,接收回复并传递给客户端。
|
||||
2. 首先将所有来自客户端的请求发送到路由层,它决定了应该处理请求的节点,并相应地转发。此路由层本身不处理任何请求;它仅负责分区的负载均衡。
|
||||
3. 要求客户端知道分区和节点的分配。在这种情况下,客户端可以直接连接到适当的节点,而不需要任何中介。
|
||||
|
||||
|
12
ch7.md
12
ch7.md
@ -21,9 +21,9 @@
|
||||
|
||||
为了实现可靠性,系统必须处理这些故障,确保它们不会导致整个系统的灾难性故障。但是实现容错机制工作量巨大。需要仔细考虑所有可能出错的事情,并进行大量的测试,以确保解决方案真正管用。
|
||||
|
||||
数十年来,**事务(transaction)** 一直是简化这些问题的首选机制。事务是应用程序将多个读写操作组合成一个逻辑单元的一种方式。从概念上讲,事务中的所有读写操作被视作单个操作来执行:整个事务要么成功(**提交(commit)**)要么失败(**中止(abort)**,**回滚(rollback)**)。如果失败,应用程序可以安全地重试。对于事务来说,应用程序的错误处理变得简单多了,因为它不用再担心部分失败的情况了,即某些操作成功,某些失败(无论出于何种原因)。
|
||||
数十年来,**事务(transaction)** 一直是简化这些问题的首选机制。事务是应用程序将多个读写操作组合成一个逻辑单元的一种方式。从概念上讲,事务中的所有读写操作被视作单个操作来执行:整个事务要么成功**提交**(commit),要么失败**中止**(abort)或**回滚**(rollback)。如果失败,应用程序可以安全地重试。对于事务来说,应用程序的错误处理变得简单多了,因为它不用再担心部分失败的情况了,即某些操作成功,某些失败(无论出于何种原因)。
|
||||
|
||||
和事务打交道时间长了,你可能会觉得它显而易见。但我们不应将其视为理所当然。事务不是天然存在的;它们是为了**简化应用编程模型**而创建的。通过使用事务,应用程序可以自由地忽略某些潜在的错误情况和并发问题,因为数据库会替应用处理好这些。(我们称之为**安全保证(safety guarantees)**)。
|
||||
和事务打交道时间长了,你可能会觉得它显而易见。但我们不应将其视为理所当然。事务不是天然存在的;它们是为了**简化应用编程模型**而创建的。通过使用事务,应用程序可以自由地忽略某些潜在的错误情况和并发问题,因为数据库会替应用处理好这些。(我们称之为**安全保证**,即safety guarantees)。
|
||||
|
||||
并不是所有的应用都需要事务,有时候弱化事务保证、或完全放弃事务也是有好处的(例如,为了获得更高性能或更高可用性)。一些安全属性也可以在没有事务的情况下实现。
|
||||
|
||||
@ -87,7 +87,7 @@ ACID一致性的概念是,**对数据的一组特定约束必须始终成立**
|
||||
|
||||
#### 隔离性
|
||||
|
||||
大多数数据库都会同时被多个客户端访问。如果它们各自读写数据库的不同部分,这是没有问题的,但是如果它们访问相同的数据库记录,则可能会遇到**并发**问题(**竞争条件(race conditions)**)。
|
||||
大多数数据库都会同时被多个客户端访问。如果它们各自读写数据库的不同部分,这是没有问题的,但是如果它们访问相同的数据库记录,则可能会遇到**并发**问题(**竞争条件**,即race conditions)。
|
||||
|
||||
[图7-1](img/fig7-1.png)是这类问题的一个简单例子。假设你有两个客户端同时在数据库中增长一个计数器。(假设数据库没有内建的自增操作)每个客户端需要读取计数器的当前值,加 1 ,再回写新值。[图7-1](img/fig7-1.png) 中,因为发生了两次增长,计数器应该从42增至44;但由于竞态条件,实际上只增至 43 。
|
||||
|
||||
@ -226,14 +226,14 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
比起盲目地依赖工具,我们应该对存在的并发问题的种类,以及如何防止这些问题有深入的理解。然后就可以使用我们所掌握的工具来构建可靠和正确的应用程序。
|
||||
|
||||
在本节中,我们将看几个在实践中使用的弱(**非串行的(nonserializable)**)隔离级别,并详细讨论哪种竞争条件可能发生也可能不发生,以便你可以决定什么级别适合你的应用程序。一旦我们完成了这个工作,我们将详细讨论可串行化(请参阅“[可串行化](#可串行化)”)。我们讨论的隔离级别将是非正式的,通过示例来进行。如果你需要严格的定义和分析它们的属性,你可以在学术文献中找到它们[28,29,30]。
|
||||
在本节中,我们将看几个在实践中使用的弱(**非串行的**,即nonserializable)隔离级别,并详细讨论哪种竞争条件可能发生也可能不发生,以便你可以决定什么级别适合你的应用程序。一旦我们完成了这个工作,我们将详细讨论可串行化(请参阅“[可串行化](#可串行化)”)。我们讨论的隔离级别将是非正式的,通过示例来进行。如果你需要严格的定义和分析它们的属性,你可以在学术文献中找到它们[28,29,30]。
|
||||
|
||||
### 读已提交
|
||||
|
||||
最基本的事务隔离级别是**读已提交(Read Committed)**[^v],它提供了两个保证:
|
||||
|
||||
1. 从数据库读时,只能看到已提交的数据(没有**脏读(dirty reads)**)。
|
||||
2. 写入数据库时,只会覆盖已经写入的数据(没有**脏写(dirty writes)**)。
|
||||
1. 从数据库读时,只能看到已提交的数据(没有**脏读**,即dirty reads)。
|
||||
2. 写入数据库时,只会覆盖已经写入的数据(没有**脏写**,即dirty writes)。
|
||||
|
||||
我们来更详细地讨论这两个保证。
|
||||
|
||||
|
14
ch8.md
14
ch8.md
@ -134,7 +134,7 @@
|
||||
|
||||
许多系统需要自动检测故障节点。例如:
|
||||
|
||||
* 负载平衡器需要停止向已死亡的节点转发请求(即从**移出轮询列表(out of rotation)**)。
|
||||
* 负载平衡器需要停止向已死亡的节点转发请求(从轮询列表移出,即out of rotation)。
|
||||
* 在单主复制功能的分布式数据库中,如果主库失效,则需要将从库之一升级为新主库(请参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。
|
||||
|
||||
不幸的是,网络的不确定性使得很难判断一个节点是否工作。在某些特定的情况下,你可能会收到一些反馈信息,明确告诉你某些事情没有成功:
|
||||
@ -169,7 +169,7 @@
|
||||
* 如果多个不同的节点同时尝试将数据包发送到同一目的地,则网络交换机必须将它们排队并将它们逐个送入目标网络链路(如[图8-2](img/fig8-2.png)所示)。在繁忙的网络链路上,数据包可能需要等待一段时间才能获得一个插槽(这称为网络拥塞)。如果传入的数据太多,交换机队列填满,数据包将被丢弃,因此需要重新发送数据包 - 即使网络运行良好。
|
||||
* 当数据包到达目标机器时,如果所有CPU内核当前都处于繁忙状态,则来自网络的传入请求将被操作系统排队,直到应用程序准备好处理它为止。根据机器上的负载,这可能需要一段任意的时间。
|
||||
* 在虚拟化环境中,正在运行的操作系统经常暂停几十毫秒,因为另一个虚拟机正在使用CPU内核。在这段时间内,虚拟机不能从网络中消耗任何数据,所以传入的数据被虚拟机监视器 【26】排队(缓冲),进一步增加了网络延迟的可变性。
|
||||
* TCP执行**流量控制(flow control)**(也称为**拥塞避免(congestion avoidance)** 或**背压(backpressure)**),其中节点会限制自己的发送速率以避免网络链路或接收节点过载【27】。这意味着甚至在数据进入网络之前,在发送者处就需要进行额外的排队。
|
||||
* TCP执行**流量控制**(flow control,也称为**拥塞避免**,即congestion avoidance,或**背压**,即backpressure),其中节点会限制自己的发送速率以避免网络链路或接收节点过载【27】。这意味着甚至在数据进入网络之前,在发送者处就需要进行额外的排队。
|
||||
|
||||

|
||||
|
||||
@ -178,9 +178,9 @@
|
||||
而且,如果TCP在某个超时时间内没有被确认(这是根据观察的往返时间计算的),则认为数据包丢失,丢失的数据包将自动重新发送。尽管应用程序没有看到数据包丢失和重新传输,但它看到了延迟(等待超时到期,然后等待重新传输的数据包得到确认)。
|
||||
|
||||
|
||||
> ### TCP与UDP
|
||||
> #### TCP与UDP
|
||||
>
|
||||
> 一些对延迟敏感的应用程序(如视频会议和IP语音(VoIP))使用UDP而不是TCP。这是在可靠性和和延迟变化之间的折衷:由于UDP不执行流量控制并且不重传丢失的分组,所以避免了网络延迟变化的一些原因(尽管它仍然易受切换队列和调度延迟的影响)。
|
||||
> 一些对延迟敏感的应用程序,比如视频会议和IP语音(VoIP),使用了UDP而不是TCP。这是在可靠性和和延迟变化之间的折衷:由于UDP不执行流量控制并且不重传丢失的分组,所以避免了网络延迟变化的一些原因(尽管它仍然易受切换队列和调度延迟的影响)。
|
||||
>
|
||||
> 在延迟数据毫无价值的情况下,UDP是一个不错的选择。例如,在VoIP电话呼叫中,可能没有足够的时间重新发送丢失的数据包,并在扬声器上播放数据。在这种情况下,重发数据包没有意义——应用程序必须使用静音填充丢失数据包的时隙(导致声音短暂中断),然后在数据流中继续。重试发生在人类层。 (“你能再说一遍吗?声音刚刚断了一会儿。“)
|
||||
|
||||
@ -262,7 +262,7 @@
|
||||
|
||||
#### 日历时钟
|
||||
|
||||
日历时钟是你直观地了解时钟的依据:它根据某个日历(也称为**挂钟时间(wall-clock time)**)返回当前日期和时间。例如,Linux上的`clock_gettime(CLOCK_REALTIME)`[^v]和Java中的`System.currentTimeMillis()`返回自epoch(UTC时间1970年1月1日午夜)以来的秒数(或毫秒),根据公历(Gregorian)日历,不包括闰秒。有些系统使用其他日期作为参考点。
|
||||
日历时钟是你直观地了解时钟的依据:它根据某个日历(也称为**挂钟时间**,即wall-clock time)返回当前日期和时间。例如,Linux上的`clock_gettime(CLOCK_REALTIME)`[^v]和Java中的`System.currentTimeMillis()`返回自epoch(UTC时间1970年1月1日午夜)以来的秒数(或毫秒),根据公历(Gregorian)日历,不包括闰秒。有些系统使用其他日期作为参考点。
|
||||
|
||||
[^v]: 虽然该时钟被称为实时时钟,但它与实时操作系统无关,如“[响应时间保证](#响应时间保证)”中所述。
|
||||
|
||||
@ -291,7 +291,7 @@
|
||||
* 如果某个节点被NTP服务器的防火墙意外阻塞,有可能会持续一段时间都没有人会注意到。有证据表明,这在实践中确实发生过。
|
||||
* NTP同步只能和网络延迟一样好,所以当你在拥有可变数据包延迟的拥塞网络上时,NTP同步的准确性会受到限制。一个实验表明,当通过互联网同步时,35毫秒的最小误差是可以实现的,尽管偶尔的网络延迟峰值会导致大约一秒的误差。根据配置,较大的网络延迟会导致NTP客户端完全放弃。
|
||||
* 一些NTP服务器是错误的或者配置错误的,报告的时间可能相差几个小时【43,44】。还好NTP客户端非常健壮,因为他们会查询多个服务器并忽略异常值。无论如何,依赖于互联网上的陌生人所告诉你的时间来保证你的系统的正确性,这还挺让人担忧的。
|
||||
* 闰秒导致一分钟可能有59秒或61秒,这会打破一些在设计之时未考虑闰秒的系统的时序假设【45】。闰秒已经使许多大型系统崩溃的事实【38,46】说明了,关于时钟的错误假设是多么容易偷偷溜入系统中。处理闰秒的最佳方法可能是让NTP服务器“撒谎”,并在一天中逐渐执行闰秒调整(这被称为**拖尾(smearing)**)【47,48】,虽然实际的NTP服务器表现各异【49】。
|
||||
* 闰秒导致一分钟可能有59秒或61秒,这会打破一些在设计之时未考虑闰秒的系统的时序假设【45】。闰秒已经使许多大型系统崩溃的事实【38,46】说明了,关于时钟的错误假设是多么容易偷偷溜入系统中。处理闰秒的最佳方法可能是让NTP服务器“撒谎”,并在一天中逐渐执行闰秒调整(这被称为**拖尾**,即smearing)【47,48】,虽然实际的NTP服务器表现各异【49】。
|
||||
* 在虚拟机中,硬件时钟被虚拟化,这对于需要精确计时的应用程序提出了额外的挑战【50】。当一个CPU核心在虚拟机之间共享时,每个虚拟机都会暂停几十毫秒,与此同时另一个虚拟机正在运行。从应用程序的角度来看,这种停顿表现为时钟突然向前跳跃【26】。
|
||||
* 如果你在没有完整控制权的设备(例如,移动设备或嵌入式设备)上运行软件,则可能完全不能信任该设备的硬件时钟。一些用户故意将其硬件时钟设置为不正确的日期和时间,例如,为了规避游戏中的时间限制,时钟可能会被设置到很远的过去或将来。
|
||||
|
||||
@ -402,7 +402,7 @@ while (true) {
|
||||
* 在最终用户的设备(如笔记本电脑)上,执行也可能被暂停并随意恢复,例如当用户关闭笔记本电脑的盖子时。
|
||||
* 当操作系统上下文切换到另一个线程时,或者当管理程序切换到另一个虚拟机时(在虚拟机中运行时),当前正在运行的线程可能在代码中的任意点处暂停。在虚拟机的情况下,在其他虚拟机中花费的CPU时间被称为**窃取时间(steal time)**。如果机器处于沉重的负载下(即,如果等待运行的线程队列很长),暂停的线程再次运行可能需要一些时间。
|
||||
* 如果应用程序执行同步磁盘访问,则线程可能暂停,等待缓慢的磁盘I/O操作完成【68】。在许多语言中,即使代码没有包含文件访问,磁盘访问也可能出乎意料地发生——例如,Java类加载器在第一次使用时惰性加载类文件,这可能在程序执行过程中随时发生。 I/O暂停和GC暂停甚至可能合谋组合它们的延迟【69】。如果磁盘实际上是一个网络文件系统或网络块设备(如亚马逊的EBS),I/O延迟进一步受到网络延迟变化的影响【29】。
|
||||
* 如果操作系统配置为允许交换到磁盘(页面交换),则简单的内存访问可能导致**页面错误(page fault)**,要求将磁盘中的页面装入内存。当这个缓慢的I/O操作发生时,线程暂停。如果内存压力很高,则可能需要将另一个页面换出到磁盘。在极端情况下,操作系统可能花费大部分时间将页面交换到内存中,而实际上完成的工作很少(这被称为**抖动(thrashing)**)。为了避免这个问题,通常在服务器机器上禁用页面调度(如果你宁愿干掉一个进程来释放内存,也不愿意冒抖动风险)。
|
||||
* 如果操作系统配置为允许交换到磁盘(页面交换),则简单的内存访问可能导致**页面错误(page fault)**,要求将磁盘中的页面装入内存。当这个缓慢的I/O操作发生时,线程暂停。如果内存压力很高,则可能需要将另一个页面换出到磁盘。在极端情况下,操作系统可能花费大部分时间将页面交换到内存中,而实际上完成的工作很少(这被称为**抖动**,即thrashing)。为了避免这个问题,通常在服务器机器上禁用页面调度(如果你宁愿干掉一个进程来释放内存,也不愿意冒抖动风险)。
|
||||
* 可以通过发送SIGSTOP信号来暂停Unix进程,例如通过在shell中按下Ctrl-Z。 这个信号立即阻止进程继续执行更多的CPU周期,直到SIGCONT恢复为止,此时它将继续运行。 即使你的环境通常不使用SIGSTOP,也可能由运维工程师意外发送。
|
||||
|
||||
所有这些事件都可以随时**抢占(preempt)** 正在运行的线程,并在稍后的时间恢复运行,而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多线程代码线程安全:你不能对时序做任何假设,因为随时可能发生上下文切换,或者出现并行运行。
|
||||
|
10
ch9.md
10
ch9.md
@ -262,7 +262,7 @@
|
||||
|
||||
这个问题不仅仅是单主复制和多主复制的后果:任何线性一致的数据库都有这个问题,不管它是如何实现的。这个问题也不仅仅局限于多数据中心部署,而可能发生在任何不可靠的网络上,即使在同一个数据中心内也是如此。问题面临的权衡如下:[^v]
|
||||
|
||||
* 如果应用需要线性一致性,且某些副本因为网络问题与其他副本断开连接,那么这些副本掉线时不能处理请求。请求必须等到网络问题解决,或直接返回错误。(无论哪种方式,服务都**不可用(unavailable)**)。
|
||||
* 如果应用需要线性一致性,且某些副本因为网络问题与其他副本断开连接,那么这些副本掉线时不能处理请求。请求必须等到网络问题解决,或直接返回错误。(无论哪种方式,服务都**不可用**)。
|
||||
* 如果应用不需要线性一致性,那么某个副本即使与其他副本断开连接,也可以独立处理请求(例如多主复制)。在这种情况下,应用可以在网络问题前保持可用,但其行为不是线性一致的。
|
||||
|
||||
[^v]: 这两种选择有时分别称为CP(在网络分区下一致但不可用)和AP(在网络分区下可用但不一致)。 但是,这种分类方案存在一些缺陷【9】,所以最好不要这样用。
|
||||
@ -271,7 +271,7 @@
|
||||
|
||||
CAP最初是作为一个经验法则提出的,没有准确的定义,目的是开始讨论数据库的权衡。那时候许多分布式数据库侧重于在共享存储的集群上提供线性一致性的语义【18】,CAP定理鼓励数据库工程师向分布式无共享系统的设计领域深入探索,这类架构更适合实现大规模的网络服务【37】。 对于这种文化上的转变,CAP值得赞扬 —— 它见证了自00年代中期以来新数据库的技术爆炸(即NoSQL)。
|
||||
|
||||
> ### CAP定理没有帮助
|
||||
> #### CAP定理没有帮助
|
||||
>
|
||||
> CAP有时以这种面目出现:一致性,可用性和分区容错性:三者只能择其二。不幸的是这种说法很有误导性【32】,因为网络分区是一种故障类型,所以它并不是一个选项:不管你喜不喜欢它都会发生【38】。
|
||||
>
|
||||
@ -603,7 +603,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一
|
||||
>
|
||||
> 两阶段提交(2PC)和两阶段锁定(请参阅“[两阶段锁定](ch7.md#两阶段锁定)”)是两个完全不同的东西。 2PC在分布式数据库中提供原子提交,而2PL提供可串行化的隔离等级。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中不幸的相似性。
|
||||
|
||||
2PC使用一个通常不会出现在单节点事务中的新组件:**协调者(coordinator)**(也称为**事务管理器(transaction manager)**)。协调者通常在请求事务的相同应用进程中以库的形式实现(例如,嵌入在Java EE容器中),但也可以是单独的进程或服务。这种协调者的例子包括Narayana、JOTM、BTM或MSDTC。
|
||||
2PC使用一个通常不会出现在单节点事务中的新组件:**协调者**(coordinator,也称为**事务管理器**,即transaction manager)。协调者通常在请求事务的相同应用进程中以库的形式实现(例如,嵌入在Java EE容器中),但也可以是单独的进程或服务。这种协调者的例子包括Narayana、JOTM、BTM或MSDTC。
|
||||
|
||||
正常情况下,2PC事务以应用在多个数据库节点上读写数据开始。我们称这些数据库节点为**参与者(participants)**。当应用准备提交时,协调者开始阶段 1 :它发送一个**准备(prepare)** 请求到每个节点,询问它们是否能够提交。然后协调者会跟踪参与者的响应:
|
||||
|
||||
@ -796,7 +796,7 @@ XA事务解决了保持多个参与者(数据系统)相互一致的现实的
|
||||
|
||||
#### 纪元编号和法定人数
|
||||
|
||||
迄今为止所讨论的所有共识协议,在内部都以某种形式使用一个领导者,但它们并不能保证领导者是独一无二的。相反,它们可以做出更弱的保证:协议定义了一个**纪元编号(epoch number)**(在Paxos中称为**投票编号(ballot number)**,视图戳复制中的**视图编号(view number)**,以及Raft中的**任期号码(term number)**),并确保在每个时代中,领导者都是唯一的。
|
||||
迄今为止所讨论的所有共识协议,在内部都以某种形式使用一个领导者,但它们并不能保证领导者是独一无二的。相反,它们可以做出更弱的保证:协议定义了一个**纪元编号**(epoch number,在Paxos中被称为**投票编号**,即ballot number,在视图戳复制中被称为**视图编号**,即view number,以及在Raft中被为**任期号码**,即term number),并确保在每个时代中,领导者都是唯一的。
|
||||
|
||||
每次当现任领导被认为挂掉的时候,节点间就会开始一场投票,以选出一个新领导。这次选举被赋予一个递增的纪元编号,因此纪元编号是全序且单调递增的。如果两个不同的时代的领导者之间出现冲突(也许是因为前任领导者实际上并未死亡),那么带有更高纪元编号的领导说了算。
|
||||
|
||||
@ -844,7 +844,7 @@ ZooKeeper模仿了Google的Chubby锁服务【14,98】,不仅实现了全序广
|
||||
|
||||
* 失效检测
|
||||
|
||||
客户端在ZooKeeper服务器上维护一个长期会话,客户端和服务器周期性地交换心跳包来检查节点是否还活着。即使连接暂时中断,或者ZooKeeper节点失效,会话仍保持在活跃状态。但如果心跳停止的持续时间超出会话超时,ZooKeeper会宣告该会话已死亡。当会话超时时(ZooKeeper称这些节点为**临时节点(ephemeral nodes)**),会话持有的任何锁都可以配置为自动释放。
|
||||
客户端在ZooKeeper服务器上维护一个长期会话,客户端和服务器周期性地交换心跳包来检查节点是否还活着。即使连接暂时中断,或者ZooKeeper节点失效,会话仍保持在活跃状态。但如果心跳停止的持续时间超出会话超时,ZooKeeper会宣告该会话已死亡。当会话超时时(ZooKeeper称这些节点为**临时节点**,即ephemeral nodes),会话持有的任何锁都可以配置为自动释放。
|
||||
|
||||
* 变更通知
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
||||
|
||||
## 伸缩至更高的载荷
|
||||
|
||||
如果你需要的只是伸缩至更高的**载荷(load)**,最简单的方法就是购买更强大的机器(有时称为**垂直伸缩(vertical scaling)** 或**向上伸缩(scale up)**)。许多处理器,内存和磁盘可以在同一个操作系统下相互连接,快速的相互连接允许任意处理器访问内存或磁盘的任意部分。在这种 **共享内存架构(shared-memory architecture)** 中,所有的组件都可以看作一台单独的机器[^i]。
|
||||
如果你需要的只是伸缩至更高的**载荷(load)**,最简单的方法就是购买更强大的机器(有时称为**垂直伸缩**,即vertical scaling,或**向上伸缩**,即scale up)。许多处理器,内存和磁盘可以在同一个操作系统下相互连接,快速的相互连接允许任意处理器访问内存或磁盘的任意部分。在这种 **共享内存架构(shared-memory architecture)** 中,所有的组件都可以看作一台单独的机器[^i]。
|
||||
|
||||
[^i]: 在大型机中,尽管任意处理器都可以访问内存的任意部分,但总有一些内存区域与一些处理器更接近(称为**非均匀内存访问(nonuniform memory access, NUMA)**【1】)。 为了有效利用这种架构特性,需要对处理进行细分,以便每个处理器主要访问临近的内存,这意味着即使表面上看起来只有一台机器在运行,**分区(partitioning)** 仍然是必要的。
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
|
||||
### 无共享架构
|
||||
|
||||
相比之下,**无共享架构(shared-nothing architecture)**(有时称为**水平伸缩(horizontal scale)** 或**向外伸缩(scale out)**)已经相当普及。在这种架构中,运行数据库软件的每台机器/虚拟机都称为**节点(node)**。每个节点只使用各自的处理器,内存和磁盘。节点之间的任何协调,都是在软件层面使用传统网络实现的。
|
||||
相比之下,**无共享架构**(shared-nothing architecture,有时被称为**水平伸缩**,即horizontal scaling,或**向外伸缩**,即scaling out)已经相当普及。在这种架构中,运行数据库软件的每台机器/虚拟机都称为**节点(node)**。每个节点只使用各自的处理器,内存和磁盘。节点之间的任何协调,都是在软件层面使用传统网络实现的。
|
||||
|
||||
无共享系统不需要使用特殊的硬件,所以你可以用任意机器——比如性价比最好的机器。你也许可以跨多个地理区域分布数据从而减少用户延迟,或者在损失一整个数据中心的情况下幸免于难。随着云端虚拟机部署的出现,即使是小公司,现在无需Google级别的运维,也可以实现异地分布式架构。
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
|
||||
* 分区 (Partitioning)
|
||||
|
||||
将一个大型数据库拆分成较小的子集(称为**分区(partitions)**),从而不同的分区可以指派给不同的**节点(node)**(亦称**分片(shard)**)。 [第六章](ch6.md)将讨论分区。
|
||||
将一个大型数据库拆分成较小的子集(称为**分区**,即partitions),从而不同的分区可以指派给不同的**节点(node)**(nodes,亦称**分片**,即sharding)。 [第六章](ch6.md)将讨论分区。
|
||||
|
||||
复制和分区是不同的机制,但它们经常同时使用。如[图II-1](img/figii-1.png)所示。
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
* 记录系统(System of record)
|
||||
|
||||
**记录系统**,也被称为**真相源(source of truth)**,持有数据的权威版本。当新的数据进入时(例如,用户输入)首先会记录在这里。每个事实正正好好表示一次(表示通常是**正规化的(normalized)**)。如果其他系统和**记录系统**之间存在任何差异,那么记录系统中的值是正确的(根据定义)。
|
||||
**记录系统**,也被称为**真相源(source of truth)**,持有数据的权威版本。当新的数据进入时(例如,用户输入)首先会记录在这里。每个事实正正好好表示一次(表示通常是**正规化的**,即normalized)。如果其他系统和**记录系统**之间存在任何差异,那么记录系统中的值是正确的(根据定义)。
|
||||
|
||||
* 衍生数据系统(Derived data systems)
|
||||
|
||||
|
@ -97,7 +97,7 @@
|
||||
|
||||
直到最近,硬體冗餘對於大多數應用來說已經足夠了,它使單臺機器完全失效變得相當罕見。只要你能快速地把備份恢復到新機器上,故障停機時間對大多數應用而言都算不上災難性的。只有少量高可用性至關重要的應用才會要求有多套硬體冗餘。
|
||||
|
||||
但是隨著資料量和應用計算需求的增加,越來越多的應用開始大量使用機器,這會相應地增加硬體故障率。此外在一些雲平臺(**如亞馬遜網路服務(AWS, Amazon Web Services)**)中,虛擬機器例項不可用卻沒有任何警告也是很常見的【7】,因為雲平臺的設計就是優先考慮**靈活性(flexibility)** 和**彈性(elasticity)**[^i],而不是單機可靠性。
|
||||
但是隨著資料量和應用計算需求的增加,越來越多的應用開始大量使用機器,這會相應地增加硬體故障率。此外,在類似亞馬遜AWS(Amazon Web Services)的一些雲服務平臺上,虛擬機器例項不可用卻沒有任何警告也是很常見的【7】,因為雲平臺的設計就是優先考慮**靈活性(flexibility)** 和**彈性(elasticity)**[^i],而不是單機可靠性。
|
||||
|
||||
如果在硬體冗餘的基礎上進一步引入軟體容錯機制,那麼系統在容忍整個(單臺)機器故障的道路上就更進一步了。這樣的系統也有運維上的便利,例如:如果需要重啟機器(例如應用作業系統安全補丁),單伺服器系統就需要計劃停機。而允許機器失效的系統則可以一次修復一個節點,無需整個系統停機。
|
||||
|
||||
@ -226,7 +226,7 @@
|
||||
|
||||
為了弄清異常值有多糟糕,可以看看更高的百分位點,例如第95、99和99.9百分位點(縮寫為p95,p99和p999)。它們意味著95%,99%或99.9%的請求響應時間要比該閾值快,例如:如果第95百分位點響應時間是1.5秒,則意味著100個請求中的95個響應時間快於1.5秒,而100個請求中的5個響應時間超過1.5秒。如[圖1-4](../img/fig1-4.png)所示。
|
||||
|
||||
響應時間的高百分位點(也稱為**尾部延遲(tail latencies)**)非常重要,因為它們直接影響使用者的服務體驗。例如亞馬遜在描述內部服務的響應時間要求時以99.9百分位點為準,即使它隻影響一千個請求中的一個。這是因為請求響應最慢的客戶往往也是資料最多的客戶,也可以說是最有價值的客戶 —— 因為他們掏錢了【19】。保證網站響應迅速對於保持客戶的滿意度非常重要,亞馬遜觀察到:響應時間增加100毫秒,銷售量就減少1%【20】;而另一些報告說:慢 1 秒鐘會讓客戶滿意度指標減少16%【21,22】。
|
||||
響應時間的高百分位點(也稱為**尾部延遲**,即**tail latencies**)非常重要,因為它們直接影響使用者的服務體驗。例如亞馬遜在描述內部服務的響應時間要求時以99.9百分位點為準,即使它隻影響一千個請求中的一個。這是因為請求響應最慢的客戶往往也是資料最多的客戶,也可以說是最有價值的客戶 —— 因為他們掏錢了【19】。保證網站響應迅速對於保持客戶的滿意度非常重要,亞馬遜觀察到:響應時間增加100毫秒,銷售量就減少1%【20】;而另一些報告說:慢 1 秒鐘會讓客戶滿意度指標減少16%【21,22】。
|
||||
|
||||
另一方面,最佳化第99.99百分位點(一萬個請求中最慢的一個)被認為太昂貴了,不能為亞馬遜的目標帶來足夠好處。減小高百分位點處的響應時間相當困難,因為它很容易受到隨機事件的影響,這超出了控制範圍,而且效益也很小。
|
||||
|
||||
|
@ -250,7 +250,7 @@ MapReduce與Unix命令管道的主要區別在於,MapReduce可以在多臺機
|
||||
|
||||
在大多數情況下,應該在Mapper任務中執行的應用程式碼在將要執行它的機器上還不存在,所以MapReduce框架首先將程式碼(例如Java程式中的JAR檔案)複製到適當的機器。然後啟動Map任務並開始讀取輸入檔案,一次將一條記錄傳入Mapper回撥函式。Mapper的輸出由鍵值對組成。
|
||||
|
||||
計算的Reduce端也被分割槽。雖然Map任務的數量由輸入檔案塊的數量決定,但Reducer的任務的數量是由作業作者配置的(它可以不同於Map任務的數量)。為了確保具有相同鍵的所有鍵值對最終落在相同的Reducer處,框架使用鍵的雜湊值來確定哪個Reduce任務應該接收到特定的鍵值對(請參閱“[根據鍵的雜湊分割槽](ch6.md#根據鍵的雜湊分割槽)”))。
|
||||
計算的Reduce端也被分割槽。雖然Map任務的數量由輸入檔案塊的數量決定,但Reducer的任務的數量是由作業作者配置的(它可以不同於Map任務的數量)。為了確保具有相同鍵的所有鍵值對最終落在相同的Reducer處,框架使用鍵的雜湊值來確定哪個Reduce任務應該接收到特定的鍵值對(請參閱“[根據鍵的雜湊分割槽](ch6.md#根據鍵的雜湊分割槽)”)。
|
||||
|
||||
鍵值對必須進行排序,但資料集可能太大,無法在單臺機器上使用常規排序演算法進行排序。相反,分類是分階段進行的。首先每個Map任務都按照Reducer對輸出進行分割槽。每個分割槽都被寫入Mapper程式的本地磁碟,使用的技術與我們在“[SSTables與LSM樹](ch3.md#SSTables與LSM樹)”中討論的類似。
|
||||
|
||||
@ -290,7 +290,7 @@ Hadoop的各種高階工具(如Pig 【30】,Hive 【31】,Cascading 【32
|
||||
|
||||
#### 示例:使用者活動事件分析
|
||||
|
||||
[圖10-2](../img/fig10-2.png)給出了一個批處理作業中連線的典型例子。左側是事件日誌,描述登入使用者在網站上做的事情(稱為**活動事件(activity events)** 或**點選流資料(clickstream data)**),右側是使用者資料庫。 你可以將此示例看作是星型模式的一部分(請參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日誌是事實表,使用者資料庫是其中的一個維度。
|
||||
[圖10-2](../img/fig10-2.png)給出了一個批處理作業中連線的典型例子。左側是事件日誌,描述登入使用者在網站上做的事情(稱為**活動事件**,即activity events,或**點選流資料**,即clickstream data),右側是使用者資料庫。 你可以將此示例看作是星型模式的一部分(請參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日誌是事實表,使用者資料庫是其中的一個維度。
|
||||
|
||||

|
||||
|
||||
@ -344,7 +344,7 @@ Hadoop的各種高階工具(如Pig 【30】,Hive 【31】,Cascading 【32
|
||||
|
||||
如果存在與單個鍵關聯的大量資料,則“將具有相同鍵的所有記錄放到相同的位置”這種模式就被破壞了。例如在社交網路中,大多數使用者可能會與幾百人有連線,但少數名人可能有數百萬的追隨者。這種不成比例的活動資料庫記錄被稱為**關鍵物件(linchpin object)**【38】或**熱鍵(hot key)**。
|
||||
|
||||
在單個Reducer中收集與某個名人相關的所有活動(例如他們釋出內容的回覆)可能導致嚴重的**偏斜**(也稱為**熱點(hot spot)**)—— 也就是說,一個Reducer必須比其他Reducer處理更多的記錄(請參閱“[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)“)。由於MapReduce作業只有在所有Mapper和Reducer都完成時才完成,所有後續作業必須等待最慢的Reducer才能啟動。
|
||||
在單個Reducer中收集與某個名人相關的所有活動(例如他們釋出內容的回覆)可能導致嚴重的**偏斜**(也稱為**熱點**,即hot spot)—— 也就是說,一個Reducer必須比其他Reducer處理更多的記錄(請參閱“[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)“)。由於MapReduce作業只有在所有Mapper和Reducer都完成時才完成,所有後續作業必須等待最慢的Reducer才能啟動。
|
||||
|
||||
如果連線的輸入存在熱鍵,可以使用一些演算法進行補償。例如,Pig中的**偏斜連線(skewed join)** 方法首先執行一個抽樣作業(Sampling Job)來確定哪些鍵是熱鍵【39】。連線實際執行時,Mapper會將熱鍵的關聯記錄**隨機**(相對於傳統MapReduce基於鍵雜湊的確定性方法)傳送到幾個Reducer之一。對於另外一側的連線輸入,與熱鍵相關的記錄需要被複制到**所有**處理該鍵的Reducer上【40】。
|
||||
|
||||
@ -603,7 +603,7 @@ Spark,Flink和Tez避免將中間狀態寫入HDFS,因此它們採取了不同
|
||||
|
||||
> 像Spark,Flink和Tez這樣的資料流引擎(請參閱“[物化中間狀態](#物化中間狀態)”)通常將運算元作為**有向無環圖(DAG)** 的一部分安排在作業中。這與圖處理不一樣:在資料流引擎中,**從一個運算元到另一個運算元的資料流**被構造成一個圖,而資料本身通常由關係型元組構成。在圖處理中,資料本身具有圖的形式。又一個不幸的命名混亂!
|
||||
|
||||
許多圖演算法是透過一次遍歷一條邊來表示的,將一個頂點與近鄰的頂點連線起來,以傳播一些資訊,並不斷重複,直到滿足一些條件為止 —— 例如,直到沒有更多的邊要跟進,或直到一些指標收斂。我們在[圖2-6](../img/fig2-6.png)中看到一個例子,它透過重複跟進標明地點歸屬關係的邊,生成了資料庫中北美包含的所有地點列表(這種演算法被稱為**傳遞閉包(transitive closure)**)。
|
||||
許多圖演算法是透過一次遍歷一條邊來表示的,將一個頂點與近鄰的頂點連線起來,以傳播一些資訊,並不斷重複,直到滿足一些條件為止 —— 例如,直到沒有更多的邊要跟進,或直到一些指標收斂。我們在[圖2-6](../img/fig2-6.png)中看到一個例子,它透過重複跟進標明地點歸屬關係的邊,生成了資料庫中北美包含的所有地點列表(這種演算法被稱為**傳遞閉包**,即transitive closure)。
|
||||
|
||||
可以在分散式檔案系統中儲存圖(包含頂點和邊的列表的檔案),但是這種“重複至完成”的想法不能用普通的MapReduce來表示,因為它只掃過一趟資料。這種演算法因此經常以**迭代**的風格實現:
|
||||
|
||||
|
@ -75,7 +75,7 @@
|
||||
|
||||
#### 訊息代理
|
||||
|
||||
一種廣泛使用的替代方法是透過**訊息代理(message broker)**(也稱為**訊息佇列(message queue)**)傳送訊息,訊息代理實質上是一種針對處理訊息流而最佳化的資料庫。它作為伺服器執行,生產者和消費者作為客戶端連線到伺服器。生產者將訊息寫入代理,消費者透過從代理那裡讀取來接收訊息。
|
||||
一種廣泛使用的替代方法是透過**訊息代理**(message broker,也稱為**訊息佇列**,即message queue)傳送訊息,訊息代理實質上是一種針對處理訊息流而最佳化的資料庫。它作為伺服器執行,生產者和消費者作為客戶端連線到伺服器。生產者將訊息寫入代理,消費者透過從代理那裡讀取來接收訊息。
|
||||
|
||||
透過將資料集中在代理上,這些系統可以更容易地容忍來來去去的客戶端(連線,斷開連線和崩潰),而永續性問題則轉移到代理的身上。一些訊息代理只將訊息儲存在記憶體中,而另一些訊息代理(取決於配置)將其寫入磁碟,以便在代理崩潰的情況下不會丟失。針對緩慢的消費者,它們通常會允許無上限的排隊(而不是丟棄訊息或背壓),儘管這種選擇也可能取決於配置。
|
||||
|
||||
@ -98,7 +98,7 @@
|
||||
|
||||
* 負載均衡(load balancing)
|
||||
|
||||
每條訊息都被傳遞給消費者**之一**,所以處理該主題下訊息的工作能被多個消費者共享。代理可以為消費者任意分配訊息。當處理訊息的代價高昂,希望能並行處理訊息時,此模式非常有用(在AMQP中,可以透過讓多個客戶端從同一個佇列中消費來實現負載均衡,而在JMS中則稱之為**共享訂閱(shared subscription)**)。
|
||||
每條訊息都被傳遞給消費者**之一**,所以處理該主題下訊息的工作能被多個消費者共享。代理可以為消費者任意分配訊息。當處理訊息的代價高昂,希望能並行處理訊息時,此模式非常有用(在AMQP中,可以透過讓多個客戶端從同一個佇列中消費來實現負載均衡,而在JMS中則稱之為**共享訂閱**,即shared subscription)。
|
||||
|
||||
* 扇出(fan-out)
|
||||
|
||||
@ -276,7 +276,7 @@ LinkedIn的Databus【25】,Facebook的Wormhole【26】和Yahoo!的Sherpa【27
|
||||
|
||||
我們之前在“[雜湊索引](ch3.md#雜湊索引)”中關於日誌結構儲存引擎的上下文中討論了日誌壓縮(請參閱[圖3-2](../img/fig3-2.png)的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。
|
||||
|
||||
在日誌結構儲存引擎中,具有特殊值NULL(**墓碑(tombstone)**)的更新表示該鍵被刪除,並會在日誌壓縮過程中被移除。但只要鍵不被覆蓋或刪除,它就會永遠留在日誌中。這種壓縮日誌所需的磁碟空間僅取決於資料庫的當前內容,而不取決於資料庫中曾經發生的寫入次數。如果相同的鍵經常被覆蓋寫入,則先前的值將最終將被垃圾回收,只有最新的值會保留下來。
|
||||
在日誌結構儲存引擎中,具有特殊值NULL(**墓碑**,即tombstone)的更新表示該鍵被刪除,並會在日誌壓縮過程中被移除。但只要鍵不被覆蓋或刪除,它就會永遠留在日誌中。這種壓縮日誌所需的磁碟空間僅取決於資料庫的當前內容,而不取決於資料庫中曾經發生的寫入次數。如果相同的鍵經常被覆蓋寫入,則先前的值將最終將被垃圾回收,只有最新的值會保留下來。
|
||||
|
||||
在基於日誌的訊息代理與變更資料捕獲的上下文中也適用相同的想法。如果CDC系統被配置為,每個變更都包含一個主鍵,且每個鍵的更新都替換了該鍵以前的值,那麼只需要保留對鍵的最新寫入就足夠了。
|
||||
|
||||
@ -491,7 +491,7 @@ CEP的實現包括Esper【69】,IBM InfoSphere Streams【70】,Apama,TIBCO
|
||||
|
||||
批處理可以在幾分鐘內讀取一年的歷史事件;在大多數情況下,感興趣的時間線是歷史中的一年,而不是處理中的幾分鐘。而且使用事件中的時間戳,使得處理是**確定性**的:在相同的輸入上再次執行相同的處理過程會得到相同的結果(請參閱“[容錯](ch10.md#容錯)”)。
|
||||
|
||||
另一方面,許多流處理框架使用處理機器上的本地系統時鐘(**處理時間(processing time)**)來確定**視窗(windowing)**【79】。這種方法的優點是簡單,如果事件建立與事件處理之間的延遲可以忽略不計,那也是合理的。然而,如果存在任何顯著的處理延遲 —— 即,事件處理顯著地晚於事件實際發生的時間,這種處理方式就失效了。
|
||||
另一方面,許多流處理框架使用處理機器上的本地系統時鐘(**處理時間**,即processing time)來確定**視窗(windowing)**【79】。這種方法的優點是簡單,如果事件建立與事件處理之間的延遲可以忽略不計,那也是合理的。然而,如果存在任何顯著的處理延遲 —— 即,事件處理顯著地晚於事件實際發生的時間,這種處理方式就失效了。
|
||||
|
||||
#### 事件時間與處理時間
|
||||
|
||||
@ -706,7 +706,7 @@ Storm的Trident基於類似的想法來處理狀態【78】。依賴冪等性意
|
||||
|
||||
* 流流連線
|
||||
|
||||
兩個輸入流都由活動事件組成,而連線運算元在某個時間視窗內搜尋相關的事件。例如,它可能會將同一個使用者30分鐘內進行的兩個活動聯絡在一起。如果你想要找出一個流內的相關事件,連線的兩側輸入可能實際上都是同一個流(**自連線(self-join)**)。
|
||||
兩個輸入流都由活動事件組成,而連線運算元在某個時間視窗內搜尋相關的事件。例如,它可能會將同一個使用者30分鐘內進行的兩個活動聯絡在一起。如果你想要找出一個流內的相關事件,連線的兩側輸入可能實際上都是同一個流(**自連線**,即self-join)。
|
||||
|
||||
* 流表連線
|
||||
|
||||
|
@ -80,7 +80,7 @@ $ cat database
|
||||
|
||||
讓我們從**鍵值資料(key-value Data)** 的索引開始。這不是你可以索引的唯一資料型別,但鍵值資料是很常見的。對於更復雜的索引來說,這也是一個有用的構建模組。
|
||||
|
||||
鍵值儲存與在大多數程式語言中可以找到的**字典(dictionary)** 型別非常相似,通常字典都是用**雜湊對映(hash map)**(或**散列表(hash table)**)實現的。雜湊對映在許多演算法教科書中都有描述【1,2】,所以這裡我們不會討論它的工作細節。既然我們已經可以用雜湊對映來表示**記憶體中**的資料結構,為什麼不使用它來索引**硬碟上**的資料呢?
|
||||
鍵值儲存與在大多數程式語言中可以找到的**字典(dictionary)** 型別非常相似,通常字典都是用**雜湊對映(hash map)**或**散列表(hash table)**實現的。雜湊對映在許多演算法教科書中都有描述【1,2】,所以這裡我們不會討論它的工作細節。既然我們已經可以用雜湊對映來表示**記憶體中**的資料結構,為什麼不使用它來索引**硬碟上**的資料呢?
|
||||
|
||||
假設我們的資料儲存只是一個追加寫入的檔案,就像前面的例子一樣,那麼最簡單的索引策略就是:保留一個記憶體中的雜湊對映,其中每個鍵都對映到資料檔案中的一個位元組偏移量,指明瞭可以找到對應值的位置,如[圖3-1](../img/fig3-1.png)所示。當你將新的鍵值對追加寫入檔案中時,還要更新雜湊對映,以反映剛剛寫入的資料的偏移量(這同時適用於插入新鍵與更新現有鍵)。當你想查詢一個值時,使用雜湊對映來查詢資料檔案中的偏移量,**尋找(seek)** 該位置並讀取該值即可。
|
||||
|
||||
@ -193,7 +193,7 @@ $ cat database
|
||||
|
||||
最初這種索引結構是由Patrick O'Neil等人描述的,且被命名為日誌結構合併樹(或LSM樹)【10】,它是基於更早之前的日誌結構檔案系統【11】來構建的。基於這種合併和壓縮排序檔案原理的儲存引擎通常被稱為LSM儲存引擎。
|
||||
|
||||
Lucene是Elasticsearch和Solr使用的一種全文搜尋的索引引擎,它使用類似的方法來儲存它的關鍵詞詞典【12,13】。全文索引比鍵值索引複雜得多,但是基於類似的想法:在搜尋查詢中給出一個單詞,找到提及單詞的所有文件(網頁,產品描述等)。這是透過鍵值結構實現的,其中鍵是單詞(**關鍵詞(term)**),值是所有包含該單詞的文件的ID列表(記錄列表)。在Lucene中,從術語到記錄列表的這種對映儲存在類似於SSTable的有序檔案中,並根據需要在後臺合併【14】。
|
||||
Lucene是Elasticsearch和Solr使用的一種全文搜尋的索引引擎,它使用類似的方法來儲存它的關鍵詞詞典【12,13】。全文索引比鍵值索引複雜得多,但是基於類似的想法:在搜尋查詢中給出一個單詞,找到提及單詞的所有文件(網頁,產品描述等)。這是透過鍵值結構實現的,其中鍵是單詞(或**詞語**,即term),值是所有包含該單詞的文件的ID列表(記錄列表)。在Lucene中,從詞語到記錄列表的這種對映儲存在類似於SSTable的有序檔案中,並根據需要在後臺合併【14】。
|
||||
|
||||
#### 效能最佳化
|
||||
|
||||
@ -246,7 +246,7 @@ B樹的基本底層寫操作是用新資料覆寫硬碟上的頁面,並假定
|
||||
|
||||
而且,一些操作需要覆寫幾個不同的頁面。例如,如果因為插入導致頁面過滿而拆分頁面,則需要寫入新拆分的兩個頁面,並覆寫其父頁面以更新對兩個子頁面的引用。這是一個危險的操作,因為如果資料庫在僅有部分頁面被寫入時崩潰,那麼最終將導致一個損壞的索引(例如,可能有一個孤兒頁面不是任何父項的子項) 。
|
||||
|
||||
為了使資料庫能處理異常崩潰的場景,B樹實現通常會帶有一個額外的硬碟資料結構:**預寫式日誌(WAL, write-ahead log)**(也稱為**重做日誌(redo log)**)。這是一個僅追加的檔案,每個B樹的修改在其能被應用到樹本身的頁面之前都必須先寫入到該檔案。當資料庫在崩潰後恢復時,這個日誌將被用來使B樹恢復到一致的狀態【5,20】。
|
||||
為了使資料庫能處理異常崩潰的場景,B樹實現通常會帶有一個額外的硬碟資料結構:**預寫式日誌**(WAL,即write-ahead log,也稱為**重做日誌**,即redo log)。這是一個僅追加的檔案,每個B樹的修改在其能被應用到樹本身的頁面之前都必須先寫入到該檔案。當資料庫在崩潰後恢復時,這個日誌將被用來使B樹恢復到一致的狀態【5,20】。
|
||||
|
||||
另外還有一個更新頁面的複雜情況是,如果多個執行緒要同時訪問B樹,則需要仔細的併發控制 —— 否則執行緒可能會看到樹處於不一致的狀態。這通常是透過使用**鎖存器(latches)**(輕量級鎖)保護樹的資料結構來完成。日誌結構化的方法在這方面更簡單,因為它們在後臺進行所有的合併,而不會干擾新接收到的查詢,並且能夠時不時地將舊的段原子交換為新的段。
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
在大多數情況下,修改應用程式的功能也意味著需要更改其儲存的資料:可能需要使用新的欄位或記錄型別,或者以新方式展示現有資料。
|
||||
|
||||
我們在[第二章](ch2.md)討論的資料模型有不同的方法來應對這種變化。關係資料庫通常假定資料庫中的所有資料都遵循一個模式:儘管可以更改該模式(透過模式遷移,即`ALTER`語句),但是在任何時間點都有且僅有一個正確的模式。相比之下,**讀時模式(schema-on-read)**(或 **無模式(schemaless)**)資料庫不會強制一個模式,因此資料庫可以包含在不同時間寫入的新老資料格式的混合(請參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)” )。
|
||||
我們在[第二章](ch2.md)討論的資料模型有不同的方法來應對這種變化。關係資料庫通常假定資料庫中的所有資料都遵循一個模式:儘管可以更改該模式(透過模式遷移,即`ALTER`語句),但是在任何時間點都有且僅有一個正確的模式。相比之下,**讀時模式(schema-on-read)**(或**無模式**,即schemaless)資料庫不會強制一個模式,因此資料庫可以包含在不同時間寫入的新老資料格式的混合(請參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)” )。
|
||||
|
||||
當資料**格式(format)** 或**模式(schema)** 發生變化時,通常需要對應用程式程式碼進行相應的更改(例如,為記錄新增新欄位,然後修改程式開始讀寫該欄位)。但在大型應用程式中,程式碼變更通常不會立即完成:
|
||||
|
||||
@ -417,7 +417,7 @@ Web服務僅僅是透過網路進行API請求的一系列技術的最新版本
|
||||
|
||||
* 本地函式呼叫是可預測的,並且成功或失敗僅取決於受你控制的引數。網路請求是不可預知的:由於網路問題,請求或響應可能會丟失,或者遠端計算機可能很慢或不可用,這些問題完全不在你的控制範圍之內。網路問題是常見的,所以你必須預測他們,例如透過重試失敗的請求。
|
||||
* 本地函式呼叫要麼返回結果,要麼丟擲異常,或者永遠不返回(因為進入無限迴圈或程序崩潰)。網路請求有另一個可能的結果:由於超時,它可能會返回沒有結果。在這種情況下,你根本不知道發生了什麼:如果你沒有得到來自遠端服務的響應,你無法知道請求是否透過。 (我們將在[第八章](ch8.md)更詳細地討論這個問題。)
|
||||
* 如果你重試失敗的網路請求,可能會發生請求實際上正在透過,只有響應丟失。在這種情況下,重試將導致該操作被執行多次,除非你在協議中引入除重( **冪等(idempotence)**)機制。本地函式呼叫沒有這個問題。 (在[第十一章](ch11.md)更詳細地討論冪等性)
|
||||
* 如果你重試失敗的網路請求,可能會發生請求實際上正在透過,只有響應丟失。在這種情況下,重試將導致該操作被執行多次,除非你在協議中引入除重(**冪等**,即idempotence)機制。本地函式呼叫沒有這個問題。 (在[第十一章](ch11.md)更詳細地討論冪等性)
|
||||
* 每次呼叫本地功能時,通常需要大致相同的時間來執行。網路請求比函式呼叫要慢得多,而且其延遲也是非常可變的:好的時候它可能會在不到一毫秒的時間內完成,但是當網路擁塞或者遠端服務超載時,可能需要幾秒鐘的時間完成一樣的東西。
|
||||
* 呼叫本地函式時,可以高效地將引用(指標)傳遞給本地記憶體中的物件。當你發出一個網路請求時,所有這些引數都需要被編碼成可以透過網路傳送的一系列位元組。如果引數是像數字或字串這樣的基本型別倒是沒關係,但是對於較大的物件很快就會變成問題。
|
||||
|
||||
|
@ -128,7 +128,7 @@
|
||||
|
||||
#### 基於語句的複製
|
||||
|
||||
在最簡單的情況下,主庫記錄下它執行的每個寫入請求(**語句(statement)**)並將該語句日誌傳送給其從庫。對於關係資料庫來說,這意味著每個`INSERT`,`UPDATE`或`DELETE`語句都被轉發給每個從庫,每個從庫解析並執行該SQL語句,就像從客戶端收到一樣。
|
||||
在最簡單的情況下,主庫記錄下它執行的每個寫入請求(**語句**,即statement)並將該語句日誌傳送給其從庫。對於關係資料庫來說,這意味著每個`INSERT`,`UPDATE`或`DELETE`語句都被轉發給每個從庫,每個從庫解析並執行該SQL語句,就像從客戶端收到一樣。
|
||||
|
||||
雖然聽上去很合理,但有很多問題會搞砸這種複製方式:
|
||||
|
||||
@ -274,7 +274,7 @@ PostgreSQL和Oracle等使用這種複製方法【16】。主要缺點是日誌
|
||||
|
||||
防止這種異常,需要另一種型別的保證:**一致字首讀(consistent prefix reads)**【23】。 這個保證說:如果一系列寫入按某個順序發生,那麼任何人讀取這些寫入時,也會看見它們以同樣的順序出現。
|
||||
|
||||
這是**分割槽(partitioned)**(**分片(sharded)**)資料庫中的一個特殊問題,將在[第六章](ch6.md)中討論。如果資料庫總是以相同的順序應用寫入,則讀取總是會看到一致的字首,所以這種異常不會發生。但是在許多分散式資料庫中,不同的分割槽獨立執行,因此不存在**全域性寫入順序**:當用戶從資料庫中讀取資料時,可能會看到資料庫的某些部分處於較舊的狀態,而某些處於較新的狀態。
|
||||
這是**分割槽(partitioned)** 或 **分片(sharded)** 資料庫中的一個特殊問題,將在[第六章](ch6.md)中討論。如果資料庫總是以相同的順序應用寫入,則讀取總是會看到一致的字首,所以這種異常不會發生。但是在許多分散式資料庫中,不同的分割槽獨立執行,因此不存在**全域性寫入順序**:當用戶從資料庫中讀取資料時,可能會看到資料庫的某些部分處於較舊的狀態,而某些處於較新的狀態。
|
||||
|
||||
一種解決方案是,確保任何因果相關的寫入都寫入相同的分割槽。對於某些無法高效完成這種操作的應用,還有一些顯式跟蹤因果依賴關係的演算法,本書將在“[“此前發生”的關係和併發](#“此前發生”的關係和併發)”一節中返回這個主題。
|
||||
|
||||
@ -700,7 +700,7 @@ LWW實現了最終收斂的目標,但以**永續性**為代價:如果同一
|
||||
|
||||
所有副本的版本號集合稱為**版本向量(version vector)**【56】。這個想法的一些變體正在被使用,但最有趣的可能是在Riak 2.0 【58,59】中使用的**虛線版本向量(dotted version vector)**【57】。我們不會深入細節,但是它的工作方式與我們在購物車示例中看到的非常相似。
|
||||
|
||||
與[圖5-13](../img/fig5-13.png)中的版本號一樣,當讀取值時,版本向量會從資料庫副本傳送到客戶端,並且隨後寫入值時需要將其傳送回資料庫。(Riak將版本向量編碼為一個字串,它稱為**因果上下文(causal context)**)。版本向量允許資料庫區分覆蓋寫入和併發寫入。
|
||||
與[圖5-13](../img/fig5-13.png)中的版本號一樣,當讀取值時,版本向量會從資料庫副本傳送到客戶端,並且隨後寫入值時需要將其傳送回資料庫。(Riak將版本向量編碼為一個字串,並稱其為**因果上下文**,即causal context)。版本向量允許資料庫區分覆蓋寫入和併發寫入。
|
||||
|
||||
另外,就像在單個副本中的情況一樣,應用程式可能需要合併併發值。版本向量結構能夠確保從一個副本讀取並隨後寫回到另一個副本是安全的。這樣做雖然可能會在其他副本上面建立資料,但只要能正確合併就不會丟失資料。
|
||||
|
||||
|
@ -85,7 +85,7 @@
|
||||
|
||||
**圖6-3 按雜湊鍵分割槽**
|
||||
|
||||
這種技術擅長在分割槽之間公平地分配鍵。分割槽邊界可以是均勻間隔的,也可以是偽隨機選擇的(在這種情況下,該技術有時也被稱為**一致性雜湊(consistent hashing)**)。
|
||||
這種技術擅長在分割槽之間公平地分配鍵。分割槽邊界可以是均勻間隔的,也可以是偽隨機選擇的(在這種情況下,該技術有時也被稱為**一致性雜湊**,即consistent hashing)。
|
||||
|
||||
> #### 一致性雜湊
|
||||
>
|
||||
@ -135,7 +135,7 @@ Cassandra採取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
|
||||
|
||||
**圖6-4 基於文件的次級索引進行分割槽**
|
||||
|
||||
在這種索引方法中,每個分割槽是完全獨立的:每個分割槽維護自己的次級索引,僅覆蓋該分割槽中的文件。它不關心儲存在其他分割槽的資料。無論何時你需要寫入資料庫(新增,刪除或更新文件),只需處理包含你正在編寫的文件ID的分割槽即可。出於這個原因,**文件分割槽索引**也被稱為**本地索引(local index)**(而不是將在下一節中描述的**全域性索引(global index)**)。
|
||||
在這種索引方法中,每個分割槽是完全獨立的:每個分割槽維護自己的次級索引,僅覆蓋該分割槽中的文件。它不關心儲存在其他分割槽的資料。無論何時你需要寫入資料庫(新增,刪除或更新文件),只需處理包含你正在編寫的文件ID的分割槽即可。出於這個原因,**文件分割槽索引**也被稱為**本地索引**(而不是將在下一節中描述的**全域性索引**)。
|
||||
|
||||
但是,從文件分割槽索引中讀取需要注意:除非你對文件ID做了特別的處理,否則沒有理由將所有具有特定顏色或特定品牌的汽車放在同一個分割槽中。在[圖6-4](../img/fig6-4.png)中,紅色汽車出現在分割槽0和分割槽1中。因此,如果要搜尋紅色汽車,則需要將查詢傳送到所有分割槽,併合並所有返回的結果。
|
||||
|
||||
@ -224,7 +224,7 @@ Cassandra採取了折衷的策略【11, 12, 13】。 Cassandra中的表可以使
|
||||
|
||||
動態分割槽的一個優點是分割槽數量適應總資料量。如果只有少量的資料,少量的分割槽就足夠了,所以開銷很小;如果有大量的資料,每個分割槽的大小被限制在一個可配置的最大值【23】。
|
||||
|
||||
需要注意的是,一個空的資料庫從一個分割槽開始,因為沒有關於在哪裡繪製分割槽邊界的先驗資訊。資料集開始時很小,直到達到第一個分割槽的分割點,所有寫入操作都必須由單個節點處理,而其他節點則處於空閒狀態。為了解決這個問題,HBase和MongoDB允許在一個空的資料庫上配置一組初始分割槽(這被稱為**預分割(pre-splitting)**)。在鍵範圍分割槽的情況中,預分割需要提前知道鍵是如何進行分配的【4,26】。
|
||||
需要注意的是,一個空的資料庫從一個分割槽開始,因為沒有關於在哪裡繪製分割槽邊界的先驗資訊。資料集開始時很小,直到達到第一個分割槽的分割點,所有寫入操作都必須由單個節點處理,而其他節點則處於空閒狀態。為了解決這個問題,HBase和MongoDB允許在一個空的資料庫上配置一組初始分割槽(這被稱為**預分割**,即pre-splitting)。在鍵範圍分割槽的情況中,預分割需要提前知道鍵是如何進行分配的【4,26】。
|
||||
|
||||
動態分割槽不僅適用於資料的範圍分割槽,而且也適用於雜湊分割槽。從版本2.4開始,MongoDB同時支援範圍和雜湊分割槽,並且都支援動態分割分割槽。
|
||||
|
||||
@ -258,7 +258,7 @@ Cassandra和Ketama使用的第三種方法是使分割槽數與節點數成正
|
||||
|
||||
概括來說,這個問題有幾種不同的方案(如圖6-7所示):
|
||||
|
||||
1. 允許客戶聯絡任何節點(例如,透過**迴圈策略的負載均衡(Round-Robin Load Balancer)**)。如果該節點恰巧擁有請求的分割槽,則它可以直接處理該請求;否則,它將請求轉發到適當的節點,接收回復並傳遞給客戶端。
|
||||
1. 允許客戶聯絡任何節點(例如,透過**迴圈策略的負載均衡**,即Round-Robin Load Balancer)。如果該節點恰巧擁有請求的分割槽,則它可以直接處理該請求;否則,它將請求轉發到適當的節點,接收回復並傳遞給客戶端。
|
||||
2. 首先將所有來自客戶端的請求傳送到路由層,它決定了應該處理請求的節點,並相應地轉發。此路由層本身不處理任何請求;它僅負責分割槽的負載均衡。
|
||||
3. 要求客戶端知道分割槽和節點的分配。在這種情況下,客戶端可以直接連線到適當的節點,而不需要任何中介。
|
||||
|
||||
|
12
zh-tw/ch7.md
12
zh-tw/ch7.md
@ -21,9 +21,9 @@
|
||||
|
||||
為了實現可靠性,系統必須處理這些故障,確保它們不會導致整個系統的災難性故障。但是實現容錯機制工作量巨大。需要仔細考慮所有可能出錯的事情,並進行大量的測試,以確保解決方案真正管用。
|
||||
|
||||
數十年來,**事務(transaction)** 一直是簡化這些問題的首選機制。事務是應用程式將多個讀寫操作組合成一個邏輯單元的一種方式。從概念上講,事務中的所有讀寫操作被視作單個操作來執行:整個事務要麼成功(**提交(commit)**)要麼失敗(**中止(abort)**,**回滾(rollback)**)。如果失敗,應用程式可以安全地重試。對於事務來說,應用程式的錯誤處理變得簡單多了,因為它不用再擔心部分失敗的情況了,即某些操作成功,某些失敗(無論出於何種原因)。
|
||||
數十年來,**事務(transaction)** 一直是簡化這些問題的首選機制。事務是應用程式將多個讀寫操作組合成一個邏輯單元的一種方式。從概念上講,事務中的所有讀寫操作被視作單個操作來執行:整個事務要麼成功**提交**(commit),要麼失敗**中止**(abort)或**回滾**(rollback)。如果失敗,應用程式可以安全地重試。對於事務來說,應用程式的錯誤處理變得簡單多了,因為它不用再擔心部分失敗的情況了,即某些操作成功,某些失敗(無論出於何種原因)。
|
||||
|
||||
和事務打交道時間長了,你可能會覺得它顯而易見。但我們不應將其視為理所當然。事務不是天然存在的;它們是為了**簡化應用程式設計模型**而建立的。透過使用事務,應用程式可以自由地忽略某些潛在的錯誤情況和併發問題,因為資料庫會替應用處理好這些。(我們稱之為**安全保證(safety guarantees)**)。
|
||||
和事務打交道時間長了,你可能會覺得它顯而易見。但我們不應將其視為理所當然。事務不是天然存在的;它們是為了**簡化應用程式設計模型**而建立的。透過使用事務,應用程式可以自由地忽略某些潛在的錯誤情況和併發問題,因為資料庫會替應用處理好這些。(我們稱之為**安全保證**,即safety guarantees)。
|
||||
|
||||
並不是所有的應用都需要事務,有時候弱化事務保證、或完全放棄事務也是有好處的(例如,為了獲得更高效能或更高可用性)。一些安全屬性也可以在沒有事務的情況下實現。
|
||||
|
||||
@ -87,7 +87,7 @@ ACID一致性的概念是,**對資料的一組特定約束必須始終成立**
|
||||
|
||||
#### 隔離性
|
||||
|
||||
大多數資料庫都會同時被多個客戶端訪問。如果它們各自讀寫資料庫的不同部分,這是沒有問題的,但是如果它們訪問相同的資料庫記錄,則可能會遇到**併發**問題(**競爭條件(race conditions)**)。
|
||||
大多數資料庫都會同時被多個客戶端訪問。如果它們各自讀寫資料庫的不同部分,這是沒有問題的,但是如果它們訪問相同的資料庫記錄,則可能會遇到**併發**問題(**競爭條件**,即race conditions)。
|
||||
|
||||
[圖7-1](../img/fig7-1.png)是這類問題的一個簡單例子。假設你有兩個客戶端同時在資料庫中增長一個計數器。(假設資料庫沒有內建的自增操作)每個客戶端需要讀取計數器的當前值,加 1 ,再回寫新值。[圖7-1](../img/fig7-1.png) 中,因為發生了兩次增長,計數器應該從42增至44;但由於競態條件,實際上只增至 43 。
|
||||
|
||||
@ -226,14 +226,14 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
比起盲目地依賴工具,我們應該對存在的併發問題的種類,以及如何防止這些問題有深入的理解。然後就可以使用我們所掌握的工具來構建可靠和正確的應用程式。
|
||||
|
||||
在本節中,我們將看幾個在實踐中使用的弱(**非序列的(nonserializable)**)隔離級別,並詳細討論哪種競爭條件可能發生也可能不發生,以便你可以決定什麼級別適合你的應用程式。一旦我們完成了這個工作,我們將詳細討論可序列化(請參閱“[可序列化](#可序列化)”)。我們討論的隔離級別將是非正式的,透過示例來進行。如果你需要嚴格的定義和分析它們的屬性,你可以在學術文獻中找到它們[28,29,30]。
|
||||
在本節中,我們將看幾個在實踐中使用的弱(**非序列的**,即nonserializable)隔離級別,並詳細討論哪種競爭條件可能發生也可能不發生,以便你可以決定什麼級別適合你的應用程式。一旦我們完成了這個工作,我們將詳細討論可序列化(請參閱“[可序列化](#可序列化)”)。我們討論的隔離級別將是非正式的,透過示例來進行。如果你需要嚴格的定義和分析它們的屬性,你可以在學術文獻中找到它們[28,29,30]。
|
||||
|
||||
### 讀已提交
|
||||
|
||||
最基本的事務隔離級別是**讀已提交(Read Committed)**[^v],它提供了兩個保證:
|
||||
|
||||
1. 從資料庫讀時,只能看到已提交的資料(沒有**髒讀(dirty reads)**)。
|
||||
2. 寫入資料庫時,只會覆蓋已經寫入的資料(沒有**髒寫(dirty writes)**)。
|
||||
1. 從資料庫讀時,只能看到已提交的資料(沒有**髒讀**,即dirty reads)。
|
||||
2. 寫入資料庫時,只會覆蓋已經寫入的資料(沒有**髒寫**,即dirty writes)。
|
||||
|
||||
我們來更詳細地討論這兩個保證。
|
||||
|
||||
|
14
zh-tw/ch8.md
14
zh-tw/ch8.md
@ -134,7 +134,7 @@
|
||||
|
||||
許多系統需要自動檢測故障節點。例如:
|
||||
|
||||
* 負載平衡器需要停止向已死亡的節點轉發請求(即從**移出輪詢列表(out of rotation)**)。
|
||||
* 負載平衡器需要停止向已死亡的節點轉發請求(從輪詢列表移出,即out of rotation)。
|
||||
* 在單主複製功能的分散式資料庫中,如果主庫失效,則需要將從庫之一升級為新主庫(請參閱“[處理節點宕機](ch5.md#處理節點宕機)”)。
|
||||
|
||||
不幸的是,網路的不確定性使得很難判斷一個節點是否工作。在某些特定的情況下,你可能會收到一些反饋資訊,明確告訴你某些事情沒有成功:
|
||||
@ -169,7 +169,7 @@
|
||||
* 如果多個不同的節點同時嘗試將資料包傳送到同一目的地,則網路交換機必須將它們排隊並將它們逐個送入目標網路鏈路(如[圖8-2](../img/fig8-2.png)所示)。在繁忙的網路鏈路上,資料包可能需要等待一段時間才能獲得一個插槽(這稱為網路擁塞)。如果傳入的資料太多,交換機佇列填滿,資料包將被丟棄,因此需要重新發送資料包 - 即使網路執行良好。
|
||||
* 當資料包到達目標機器時,如果所有CPU核心當前都處於繁忙狀態,則來自網路的傳入請求將被作業系統排隊,直到應用程式準備好處理它為止。根據機器上的負載,這可能需要一段任意的時間。
|
||||
* 在虛擬化環境中,正在執行的作業系統經常暫停幾十毫秒,因為另一個虛擬機器正在使用CPU核心。在這段時間內,虛擬機器不能從網路中消耗任何資料,所以傳入的資料被虛擬機器監視器 【26】排隊(緩衝),進一步增加了網路延遲的可變性。
|
||||
* TCP執行**流量控制(flow control)**(也稱為**擁塞避免(congestion avoidance)** 或**背壓(backpressure)**),其中節點會限制自己的傳送速率以避免網路鏈路或接收節點過載【27】。這意味著甚至在資料進入網路之前,在傳送者處就需要進行額外的排隊。
|
||||
* TCP執行**流量控制**(flow control,也稱為**擁塞避免**,即congestion avoidance,或**背壓**,即backpressure),其中節點會限制自己的傳送速率以避免網路鏈路或接收節點過載【27】。這意味著甚至在資料進入網路之前,在傳送者處就需要進行額外的排隊。
|
||||
|
||||

|
||||
|
||||
@ -178,9 +178,9 @@
|
||||
而且,如果TCP在某個超時時間內沒有被確認(這是根據觀察的往返時間計算的),則認為資料包丟失,丟失的資料包將自動重新發送。儘管應用程式沒有看到資料包丟失和重新傳輸,但它看到了延遲(等待超時到期,然後等待重新傳輸的資料包得到確認)。
|
||||
|
||||
|
||||
> ### TCP與UDP
|
||||
> #### TCP與UDP
|
||||
>
|
||||
> 一些對延遲敏感的應用程式(如影片會議和IP語音(VoIP))使用UDP而不是TCP。這是在可靠性和和延遲變化之間的折衷:由於UDP不執行流量控制並且不重傳丟失的分組,所以避免了網路延遲變化的一些原因(儘管它仍然易受切換佇列和排程延遲的影響)。
|
||||
> 一些對延遲敏感的應用程式,比如影片會議和IP語音(VoIP),使用了UDP而不是TCP。這是在可靠性和和延遲變化之間的折衷:由於UDP不執行流量控制並且不重傳丟失的分組,所以避免了網路延遲變化的一些原因(儘管它仍然易受切換佇列和排程延遲的影響)。
|
||||
>
|
||||
> 在延遲資料毫無價值的情況下,UDP是一個不錯的選擇。例如,在VoIP電話呼叫中,可能沒有足夠的時間重新發送丟失的資料包,並在揚聲器上播放資料。在這種情況下,重發資料包沒有意義——應用程式必須使用靜音填充丟失資料包的時隙(導致聲音短暫中斷),然後在資料流中繼續。重試發生在人類層。 (“你能再說一遍嗎?聲音剛剛斷了一會兒。“)
|
||||
|
||||
@ -262,7 +262,7 @@
|
||||
|
||||
#### 日曆時鐘
|
||||
|
||||
日曆時鐘是你直觀地瞭解時鐘的依據:它根據某個日曆(也稱為**掛鐘時間(wall-clock time)**)返回當前日期和時間。例如,Linux上的`clock_gettime(CLOCK_REALTIME)`[^v]和Java中的`System.currentTimeMillis()`返回自epoch(UTC時間1970年1月1日午夜)以來的秒數(或毫秒),根據公曆(Gregorian)日曆,不包括閏秒。有些系統使用其他日期作為參考點。
|
||||
日曆時鐘是你直觀地瞭解時鐘的依據:它根據某個日曆(也稱為**掛鐘時間**,即wall-clock time)返回當前日期和時間。例如,Linux上的`clock_gettime(CLOCK_REALTIME)`[^v]和Java中的`System.currentTimeMillis()`返回自epoch(UTC時間1970年1月1日午夜)以來的秒數(或毫秒),根據公曆(Gregorian)日曆,不包括閏秒。有些系統使用其他日期作為參考點。
|
||||
|
||||
[^v]: 雖然該時鐘被稱為實時時鐘,但它與實時作業系統無關,如“[響應時間保證](#響應時間保證)”中所述。
|
||||
|
||||
@ -291,7 +291,7 @@
|
||||
* 如果某個節點被NTP伺服器的防火牆意外阻塞,有可能會持續一段時間都沒有人會注意到。有證據表明,這在實踐中確實發生過。
|
||||
* NTP同步只能和網路延遲一樣好,所以當你在擁有可變資料包延遲的擁塞網路上時,NTP同步的準確性會受到限制。一個實驗表明,當透過網際網路同步時,35毫秒的最小誤差是可以實現的,儘管偶爾的網路延遲峰值會導致大約一秒的誤差。根據配置,較大的網路延遲會導致NTP客戶端完全放棄。
|
||||
* 一些NTP伺服器是錯誤的或者配置錯誤的,報告的時間可能相差幾個小時【43,44】。還好NTP客戶端非常健壯,因為他們會查詢多個伺服器並忽略異常值。無論如何,依賴於網際網路上的陌生人所告訴你的時間來保證你的系統的正確性,這還挺讓人擔憂的。
|
||||
* 閏秒導致一分鐘可能有59秒或61秒,這會打破一些在設計之時未考慮閏秒的系統的時序假設【45】。閏秒已經使許多大型系統崩潰的事實【38,46】說明了,關於時鐘的錯誤假設是多麼容易偷偷溜入系統中。處理閏秒的最佳方法可能是讓NTP伺服器“撒謊”,並在一天中逐漸執行閏秒調整(這被稱為**拖尾(smearing)**)【47,48】,雖然實際的NTP伺服器表現各異【49】。
|
||||
* 閏秒導致一分鐘可能有59秒或61秒,這會打破一些在設計之時未考慮閏秒的系統的時序假設【45】。閏秒已經使許多大型系統崩潰的事實【38,46】說明了,關於時鐘的錯誤假設是多麼容易偷偷溜入系統中。處理閏秒的最佳方法可能是讓NTP伺服器“撒謊”,並在一天中逐漸執行閏秒調整(這被稱為**拖尾**,即smearing)【47,48】,雖然實際的NTP伺服器表現各異【49】。
|
||||
* 在虛擬機器中,硬體時鐘被虛擬化,這對於需要精確計時的應用程式提出了額外的挑戰【50】。當一個CPU核心在虛擬機器之間共享時,每個虛擬機器都會暫停幾十毫秒,與此同時另一個虛擬機器正在執行。從應用程式的角度來看,這種停頓表現為時鐘突然向前跳躍【26】。
|
||||
* 如果你在沒有完整控制權的裝置(例如,移動裝置或嵌入式裝置)上執行軟體,則可能完全不能信任該裝置的硬體時鐘。一些使用者故意將其硬體時鐘設定為不正確的日期和時間,例如,為了規避遊戲中的時間限制,時鐘可能會被設定到很遠的過去或將來。
|
||||
|
||||
@ -402,7 +402,7 @@ while (true) {
|
||||
* 在終端使用者的裝置(如膝上型電腦)上,執行也可能被暫停並隨意恢復,例如當用戶關閉膝上型電腦的蓋子時。
|
||||
* 當作業系統上下文切換到另一個執行緒時,或者當管理程式切換到另一個虛擬機器時(在虛擬機器中執行時),當前正在執行的執行緒可能在程式碼中的任意點處暫停。在虛擬機器的情況下,在其他虛擬機器中花費的CPU時間被稱為**竊取時間(steal time)**。如果機器處於沉重的負載下(即,如果等待執行的執行緒佇列很長),暫停的執行緒再次執行可能需要一些時間。
|
||||
* 如果應用程式執行同步磁碟訪問,則執行緒可能暫停,等待緩慢的磁碟I/O操作完成【68】。在許多語言中,即使程式碼沒有包含檔案訪問,磁碟訪問也可能出乎意料地發生——例如,Java類載入器在第一次使用時惰性載入類檔案,這可能在程式執行過程中隨時發生。 I/O暫停和GC暫停甚至可能合謀組合它們的延遲【69】。如果磁碟實際上是一個網路檔案系統或網路塊裝置(如亞馬遜的EBS),I/O延遲進一步受到網路延遲變化的影響【29】。
|
||||
* 如果作業系統配置為允許交換到磁碟(頁面交換),則簡單的記憶體訪問可能導致**頁面錯誤(page fault)**,要求將磁碟中的頁面裝入記憶體。當這個緩慢的I/O操作發生時,執行緒暫停。如果記憶體壓力很高,則可能需要將另一個頁面換出到磁碟。在極端情況下,作業系統可能花費大部分時間將頁面交換到記憶體中,而實際上完成的工作很少(這被稱為**抖動(thrashing)**)。為了避免這個問題,通常在伺服器機器上禁用頁面排程(如果你寧願幹掉一個程序來釋放記憶體,也不願意冒抖動風險)。
|
||||
* 如果作業系統配置為允許交換到磁碟(頁面交換),則簡單的記憶體訪問可能導致**頁面錯誤(page fault)**,要求將磁碟中的頁面裝入記憶體。當這個緩慢的I/O操作發生時,執行緒暫停。如果記憶體壓力很高,則可能需要將另一個頁面換出到磁碟。在極端情況下,作業系統可能花費大部分時間將頁面交換到記憶體中,而實際上完成的工作很少(這被稱為**抖動**,即thrashing)。為了避免這個問題,通常在伺服器機器上禁用頁面排程(如果你寧願幹掉一個程序來釋放記憶體,也不願意冒抖動風險)。
|
||||
* 可以透過傳送SIGSTOP訊號來暫停Unix程序,例如透過在shell中按下Ctrl-Z。 這個訊號立即阻止程序繼續執行更多的CPU週期,直到SIGCONT恢復為止,此時它將繼續執行。 即使你的環境通常不使用SIGSTOP,也可能由運維工程師意外發送。
|
||||
|
||||
所有這些事件都可以隨時**搶佔(preempt)** 正在執行的執行緒,並在稍後的時間恢復執行,而執行緒甚至不會注意到這一點。這個問題類似於在單個機器上使多執行緒程式碼執行緒安全:你不能對時序做任何假設,因為隨時可能發生上下文切換,或者出現並行執行。
|
||||
|
10
zh-tw/ch9.md
10
zh-tw/ch9.md
@ -262,7 +262,7 @@
|
||||
|
||||
這個問題不僅僅是單主複製和多主複製的後果:任何線性一致的資料庫都有這個問題,不管它是如何實現的。這個問題也不僅僅侷限於多資料中心部署,而可能發生在任何不可靠的網路上,即使在同一個資料中心內也是如此。問題面臨的權衡如下:[^v]
|
||||
|
||||
* 如果應用需要線性一致性,且某些副本因為網路問題與其他副本斷開連線,那麼這些副本掉線時不能處理請求。請求必須等到網路問題解決,或直接返回錯誤。(無論哪種方式,服務都**不可用(unavailable)**)。
|
||||
* 如果應用需要線性一致性,且某些副本因為網路問題與其他副本斷開連線,那麼這些副本掉線時不能處理請求。請求必須等到網路問題解決,或直接返回錯誤。(無論哪種方式,服務都**不可用**)。
|
||||
* 如果應用不需要線性一致性,那麼某個副本即使與其他副本斷開連線,也可以獨立處理請求(例如多主複製)。在這種情況下,應用可以在網路問題前保持可用,但其行為不是線性一致的。
|
||||
|
||||
[^v]: 這兩種選擇有時分別稱為CP(在網路分割槽下一致但不可用)和AP(在網路分割槽下可用但不一致)。 但是,這種分類方案存在一些缺陷【9】,所以最好不要這樣用。
|
||||
@ -271,7 +271,7 @@
|
||||
|
||||
CAP最初是作為一個經驗法則提出的,沒有準確的定義,目的是開始討論資料庫的權衡。那時候許多分散式資料庫側重於在共享儲存的叢集上提供線性一致性的語義【18】,CAP定理鼓勵資料庫工程師向分散式無共享系統的設計領域深入探索,這類架構更適合實現大規模的網路服務【37】。 對於這種文化上的轉變,CAP值得讚揚 —— 它見證了自00年代中期以來新資料庫的技術爆炸(即NoSQL)。
|
||||
|
||||
> ### CAP定理沒有幫助
|
||||
> #### CAP定理沒有幫助
|
||||
>
|
||||
> CAP有時以這種面目出現:一致性,可用性和分割槽容錯性:三者只能擇其二。不幸的是這種說法很有誤導性【32】,因為網路分割槽是一種故障型別,所以它並不是一個選項:不管你喜不喜歡它都會發生【38】。
|
||||
>
|
||||
@ -603,7 +603,7 @@ CAP定理的正式定義僅限於很狹隘的範圍【30】,它只考慮了一
|
||||
>
|
||||
> 兩階段提交(2PC)和兩階段鎖定(請參閱“[兩階段鎖定](ch7.md#兩階段鎖定)”)是兩個完全不同的東西。 2PC在分散式資料庫中提供原子提交,而2PL提供可序列化的隔離等級。為了避免混淆,最好把它們看作完全獨立的概念,並忽略名稱中不幸的相似性。
|
||||
|
||||
2PC使用一個通常不會出現在單節點事務中的新元件:**協調者(coordinator)**(也稱為**事務管理器(transaction manager)**)。協調者通常在請求事務的相同應用程序中以庫的形式實現(例如,嵌入在Java EE容器中),但也可以是單獨的程序或服務。這種協調者的例子包括Narayana、JOTM、BTM或MSDTC。
|
||||
2PC使用一個通常不會出現在單節點事務中的新元件:**協調者**(coordinator,也稱為**事務管理器**,即transaction manager)。協調者通常在請求事務的相同應用程序中以庫的形式實現(例如,嵌入在Java EE容器中),但也可以是單獨的程序或服務。這種協調者的例子包括Narayana、JOTM、BTM或MSDTC。
|
||||
|
||||
正常情況下,2PC事務以應用在多個數據庫節點上讀寫資料開始。我們稱這些資料庫節點為**參與者(participants)**。當應用準備提交時,協調者開始階段 1 :它傳送一個**準備(prepare)** 請求到每個節點,詢問它們是否能夠提交。然後協調者會跟蹤參與者的響應:
|
||||
|
||||
@ -796,7 +796,7 @@ XA事務解決了保持多個參與者(資料系統)相互一致的現實的
|
||||
|
||||
#### 紀元編號和法定人數
|
||||
|
||||
迄今為止所討論的所有共識協議,在內部都以某種形式使用一個領導者,但它們並不能保證領導者是獨一無二的。相反,它們可以做出更弱的保證:協議定義了一個**紀元編號(epoch number)**(在Paxos中稱為**投票編號(ballot number)**,檢視戳複製中的**檢視編號(view number)**,以及Raft中的**任期號碼(term number)**),並確保在每個時代中,領導者都是唯一的。
|
||||
迄今為止所討論的所有共識協議,在內部都以某種形式使用一個領導者,但它們並不能保證領導者是獨一無二的。相反,它們可以做出更弱的保證:協議定義了一個**紀元編號**(epoch number,在Paxos中被稱為**投票編號**,即ballot number,在檢視戳複製中被稱為**檢視編號**,即view number,以及在Raft中被為**任期號碼**,即term number),並確保在每個時代中,領導者都是唯一的。
|
||||
|
||||
每次當現任領導被認為掛掉的時候,節點間就會開始一場投票,以選出一個新領導。這次選舉被賦予一個遞增的紀元編號,因此紀元編號是全序且單調遞增的。如果兩個不同的時代的領導者之間出現衝突(也許是因為前任領導者實際上並未死亡),那麼帶有更高紀元編號的領導說了算。
|
||||
|
||||
@ -844,7 +844,7 @@ ZooKeeper模仿了Google的Chubby鎖服務【14,98】,不僅實現了全序廣
|
||||
|
||||
* 失效檢測
|
||||
|
||||
客戶端在ZooKeeper伺服器上維護一個長期會話,客戶端和伺服器週期性地交換心跳包來檢查節點是否還活著。即使連線暫時中斷,或者ZooKeeper節點失效,會話仍保持在活躍狀態。但如果心跳停止的持續時間超出會話超時,ZooKeeper會宣告該會話已死亡。當會話超時時(ZooKeeper稱這些節點為**臨時節點(ephemeral nodes)**),會話持有的任何鎖都可以配置為自動釋放。
|
||||
客戶端在ZooKeeper伺服器上維護一個長期會話,客戶端和伺服器週期性地交換心跳包來檢查節點是否還活著。即使連線暫時中斷,或者ZooKeeper節點失效,會話仍保持在活躍狀態。但如果心跳停止的持續時間超出會話超時,ZooKeeper會宣告該會話已死亡。當會話超時時(ZooKeeper稱這些節點為**臨時節點**,即ephemeral nodes),會話持有的任何鎖都可以配置為自動釋放。
|
||||
|
||||
* 變更通知
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
||||
|
||||
## 伸縮至更高的載荷
|
||||
|
||||
如果你需要的只是伸縮至更高的**載荷(load)**,最簡單的方法就是購買更強大的機器(有時稱為**垂直伸縮(vertical scaling)** 或**向上伸縮(scale up)**)。許多處理器,記憶體和磁碟可以在同一個作業系統下相互連線,快速的相互連線允許任意處理器訪問記憶體或磁碟的任意部分。在這種 **共享記憶體架構(shared-memory architecture)** 中,所有的元件都可以看作一臺單獨的機器[^i]。
|
||||
如果你需要的只是伸縮至更高的**載荷(load)**,最簡單的方法就是購買更強大的機器(有時稱為**垂直伸縮**,即vertical scaling,或**向上伸縮**,即scale up)。許多處理器,記憶體和磁碟可以在同一個作業系統下相互連線,快速的相互連線允許任意處理器訪問記憶體或磁碟的任意部分。在這種 **共享記憶體架構(shared-memory architecture)** 中,所有的元件都可以看作一臺單獨的機器[^i]。
|
||||
|
||||
[^i]: 在大型機中,儘管任意處理器都可以訪問記憶體的任意部分,但總有一些記憶體區域與一些處理器更接近(稱為**非均勻記憶體訪問(nonuniform memory access, NUMA)**【1】)。 為了有效利用這種架構特性,需要對處理進行細分,以便每個處理器主要訪問臨近的記憶體,這意味著即使表面上看起來只有一臺機器在執行,**分割槽(partitioning)** 仍然是必要的。
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
|
||||
### 無共享架構
|
||||
|
||||
相比之下,**無共享架構(shared-nothing architecture)**(有時稱為**水平伸縮(horizontal scale)** 或**向外伸縮(scale out)**)已經相當普及。在這種架構中,執行資料庫軟體的每臺機器/虛擬機器都稱為**節點(node)**。每個節點只使用各自的處理器,記憶體和磁碟。節點之間的任何協調,都是在軟體層面使用傳統網路實現的。
|
||||
相比之下,**無共享架構**(shared-nothing architecture,有時被稱為**水平伸縮**,即horizontal scaling,或**向外伸縮**,即scaling out)已經相當普及。在這種架構中,執行資料庫軟體的每臺機器/虛擬機器都稱為**節點(node)**。每個節點只使用各自的處理器,記憶體和磁碟。節點之間的任何協調,都是在軟體層面使用傳統網路實現的。
|
||||
|
||||
無共享系統不需要使用特殊的硬體,所以你可以用任意機器——比如價效比最好的機器。你也許可以跨多個地理區域分佈資料從而減少使用者延遲,或者在損失一整個資料中心的情況下倖免於難。隨著雲端虛擬機器部署的出現,即使是小公司,現在無需Google級別的運維,也可以實現異地分散式架構。
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
|
||||
* 分割槽 (Partitioning)
|
||||
|
||||
將一個大型資料庫拆分成較小的子集(稱為**分割槽(partitions)**),從而不同的分割槽可以指派給不同的**節點(node)**(亦稱**分片(shard)**)。 [第六章](ch6.md)將討論分割槽。
|
||||
將一個大型資料庫拆分成較小的子集(稱為**分割槽**,即partitions),從而不同的分割槽可以指派給不同的**節點(node)**(nodes,亦稱**分片**,即sharding)。 [第六章](ch6.md)將討論分割槽。
|
||||
|
||||
複製和分割槽是不同的機制,但它們經常同時使用。如[圖II-1](../img/figii-1.png)所示。
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
* 記錄系統(System of record)
|
||||
|
||||
**記錄系統**,也被稱為**真相源(source of truth)**,持有資料的權威版本。當新的資料進入時(例如,使用者輸入)首先會記錄在這裡。每個事實正正好好表示一次(表示通常是**正規化的(normalized)**)。如果其他系統和**記錄系統**之間存在任何差異,那麼記錄系統中的值是正確的(根據定義)。
|
||||
**記錄系統**,也被稱為**真相源(source of truth)**,持有資料的權威版本。當新的資料進入時(例如,使用者輸入)首先會記錄在這裡。每個事實正正好好表示一次(表示通常是**正規化的**,即normalized)。如果其他系統和**記錄系統**之間存在任何差異,那麼記錄系統中的值是正確的(根據定義)。
|
||||
|
||||
* 衍生資料系統(Derived data systems)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user