diff --git a/ch1.md b/ch1.md index 3c8a646..f78214f 100644 --- a/ch1.md +++ b/ch1.md @@ -80,9 +80,9 @@ 如果所有这些在一起意味着“正确工作”,那么可以把可靠性粗略理解为“即使出现问题,也能继续正确工作”。 -​ 造成错误的原因叫做**故障(fault)**,能预料并应对故障的系统特性可称为**容错(fault-tolerant)**或**韧性(resilient)**。“**容错**”一词可能会产生误导,因为它暗示着系统可以容忍所有可能的错误,但在实际中这是不可能的。比方说,如果整个地球(及其上的所有服务器)都被黑洞吞噬了,想要容忍这种错误,需要把网络托管到太空中——这种预算能不能批准就祝你好运了。所以在讨论容错时,只有谈论特定类型的错误才有意义。 +​ 造成错误的原因叫做**故障(fault)**,能预料并应对故障的系统特性可称为**容错(fault-tolerant)** 或**韧性(resilient)**。“**容错**”一词可能会产生误导,因为它暗示着系统可以容忍所有可能的错误,但在实际中这是不可能的。比方说,如果整个地球(及其上的所有服务器)都被黑洞吞噬了,想要容忍这种错误,需要把网络托管到太空中——这种预算能不能批准就祝你好运了。所以在讨论容错时,只有谈论特定类型的错误才有意义。 -​ 注意**故障(fault)**不同于**失效(failure)**【2】。**故障**通常定义为系统的一部分状态偏离其标准,而**失效**则是系统作为一个整体停止向用户提供服务。故障的概率不可能降到零,因此最好设计容错机制以防因**故障**而导致**失效**。本书中我们将介绍几种用不可靠的部件构建可靠系统的技术。 +​ 注意**故障(fault)** 不同于**失效(failure)**【2】。**故障**通常定义为系统的一部分状态偏离其标准,而**失效**则是系统作为一个整体停止向用户提供服务。故障的概率不可能降到零,因此最好设计容错机制以防因**故障**而导致**失效**。本书中我们将介绍几种用不可靠的部件构建可靠系统的技术。 ​ 反直觉的是,在这类容错系统中,通过故意触发来**提高**故障率是有意义的,例如:在没有警告的情况下随机地杀死单个进程。许多高危漏洞实际上是由糟糕的错误处理导致的【3】,因此我们可以通过故意引发故障来确保容错机制不断运行并接受考验,从而提高故障自然发生时系统能正确处理的信心。Netflix公司的*Chaos Monkey*【4】就是这种方法的一个例子。 @@ -90,7 +90,7 @@ ### 硬件故障 -​ 当想到系统失效的原因时,**硬件故障(hardware faults)**总会第一个进入脑海。硬盘崩溃、内存出错、机房断电、有人拔错网线……任何与大型数据中心打过交道的人都会告诉你:一旦你拥有很多机器,这些事情**总**会发生! +​ 当想到系统失效的原因时,**硬件故障(hardware faults)** 总会第一个进入脑海。硬盘崩溃、内存出错、机房断电、有人拔错网线……任何与大型数据中心打过交道的人都会告诉你:一旦你拥有很多机器,这些事情**总**会发生! ​ 据报道称,硬盘的 **平均无故障时间(MTTF mean time to failure)** 约为10到50年【5】【6】。因此从数学期望上讲,在拥有10000个磁盘的存储集群上,平均每天会有1个磁盘出故障。 @@ -98,7 +98,7 @@ ​ 直到最近,硬件冗余对于大多数应用来说已经足够了,它使单台机器完全失效变得相当罕见。只要你能快速地把备份恢复到新机器上,故障停机时间对大多数应用而言都算不上灾难性的。只有少量高可用性至关重要的应用才会要求有多套硬件冗余。 -​ 但是随着数据量和应用计算需求的增加,越来越多的应用开始大量使用机器,这会相应地增加硬件故障率。此外在一些云平台(**如亚马逊网络服务(AWS, Amazon Web Services)**)中,虚拟机实例不可用却没有任何警告也是很常见的【7】,因为云平台的设计就是优先考虑**灵活性(flexibility)**和**弹性(elasticity)**[^i],而不是单机可靠性。 +​ 但是随着数据量和应用计算需求的增加,越来越多的应用开始大量使用机器,这会相应地增加硬件故障率。此外在一些云平台(**如亚马逊网络服务(AWS, Amazon Web Services)**)中,虚拟机实例不可用却没有任何警告也是很常见的【7】,因为云平台的设计就是优先考虑**灵活性(flexibility)** 和**弹性(elasticity)**[^i],而不是单机可靠性。 ​ 如果在硬件冗余的基础上进一步引入软件容错机制,那么系统在容忍整个(单台)机器故障的道路上就更进一步了。这样的系统也有运维上的便利,例如:如果需要重启机器(例如应用操作系统安全补丁),单服务器系统就需要计划停机。而允许机器失效的系统则可以一次修复一个节点,无需整个系统停机。 @@ -234,7 +234,7 @@ ​ 另一方面,优化第99.99百分位点(一万个请求中最慢的一个)被认为太昂贵了,不能为亚马逊的目标带来足够好处。减小高百分位点处的响应时间相当困难,因为它很容易受到随机事件的影响,这超出了控制范围,而且效益也很小。 -​ 百分位点通常用于**服务级别目标(SLO, service level objectives)**和**服务级别协议(SLA, service level agreements)**,即定义服务预期性能和可用性的合同。 SLA可能会声明,如果服务响应时间的中位数小于200毫秒,且99.9百分位点低于1秒,则认为服务工作正常(如果响应时间更长,就认为服务不达标)。这些指标为客户设定了期望值,并允许客户在SLA未达标的情况下要求退款。 +​ 百分位点通常用于**服务级别目标(SLO, service level objectives)** 和**服务级别协议(SLA, service level agreements)**,即定义服务预期性能和可用性的合同。 SLA可能会声明,如果服务响应时间的中位数小于200毫秒,且99.9百分位点低于1秒,则认为服务工作正常(如果响应时间更长,就认为服务不达标)。这些指标为客户设定了期望值,并允许客户在SLA未达标的情况下要求退款。 ​ **排队延迟(queueing delay)** 通常占了高百分位点处响应时间的很大一部分。由于服务器只能并行处理少量的事务(如受其CPU核数的限制),所以只要有少量缓慢的请求就能阻碍后续请求的处理,这种效应有时被称为 **头部阻塞(head-of-line blocking)** 。即使后续请求在服务器上处理的非常迅速,由于需要等待先前请求完成,客户端最终看到的是缓慢的总体响应时间。因为存在这种效应,测量客户端的响应时间非常重要。 @@ -294,7 +294,7 @@ ***可演化性(evolability)*** -​ 使工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配。也称为**可伸缩性(extensibility)**,**可修改性(modifiability)**或**可塑性(plasticity)**。 +​ 使工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配。也称为**可伸缩性(extensibility)**,**可修改性(modifiability)** 或**可塑性(plasticity)**。 ​ 和之前提到的可靠性、可伸缩性一样,实现这些目标也没有简单的解决方案。不过我们会试着想象具有可操作性,简单性和可演化性的系统会是什么样子。 diff --git a/ch10.md b/ch10.md index 9d75e10..f2b0f96 100644 --- a/ch10.md +++ b/ch10.md @@ -165,7 +165,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5 ​ 即使是具有**相同数据模型**的数据库,将数据从一种数据库导出再导入到另一种数据库也并不容易。缺乏整合导致了数据的**巴尔干化**[^译注i]。 -[^译注i]: **巴尔干化(Balkanization)**是一个常带有贬义的地缘政治学术语,其定义为:一个国家或政区分裂成多个互相敌对的国家或政区的过程。 +[^译注i]: **巴尔干化(Balkanization)** 是一个常带有贬义的地缘政治学术语,其定义为:一个国家或政区分裂成多个互相敌对的国家或政区的过程。 @@ -295,7 +295,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5 #### 示例:用户活动事件分析 -​ [图10-2](img/fig10-2.png)给出了一个批处理作业中连接的典型例子。左侧是事件日志,描述登录用户在网站上做的事情(称为**活动事件(activity events)**或**点击流数据(clickstream data)**),右侧是用户数据库。 你可以将此示例看作是星型模式的一部分(请参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日志是事实表,用户数据库是其中的一个维度。 +​ [图10-2](img/fig10-2.png)给出了一个批处理作业中连接的典型例子。左侧是事件日志,描述登录用户在网站上做的事情(称为**活动事件(activity events)** 或**点击流数据(clickstream data)**),右侧是用户数据库。 你可以将此示例看作是星型模式的一部分(请参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日志是事实表,用户数据库是其中的一个维度。 ![](img/fig10-2.png) @@ -351,7 +351,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5 ​ 在单个Reducer中收集与某个名人相关的所有活动(例如他们发布内容的回复)可能导致严重的**偏斜**(也称为**热点(hot spot)**)—— 也就是说,一个Reducer必须比其他Reducer处理更多的记录(请参阅“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)“)。由于MapReduce作业只有在所有Mapper和Reducer都完成时才完成,所有后续作业必须等待最慢的Reducer才能启动。 -​ 如果连接的输入存在热键,可以使用一些算法进行补偿。例如,Pig中的**偏斜连接(skewed join)**方法首先运行一个抽样作业(Sampling Job)来确定哪些键是热键【39】。连接实际执行时,Mapper会将热键的关联记录**随机**(相对于传统MapReduce基于键散列的确定性方法)发送到几个Reducer之一。对于另外一侧的连接输入,与热键相关的记录需要被复制到**所有**处理该键的Reducer上【40】。 +​ 如果连接的输入存在热键,可以使用一些算法进行补偿。例如,Pig中的**偏斜连接(skewed join)** 方法首先运行一个抽样作业(Sampling Job)来确定哪些键是热键【39】。连接实际执行时,Mapper会将热键的关联记录**随机**(相对于传统MapReduce基于键散列的确定性方法)发送到几个Reducer之一。对于另外一侧的连接输入,与热键相关的记录需要被复制到**所有**处理该键的Reducer上【40】。 ​ 这种技术将处理热键的工作分散到多个Reducer上,这样可以使其更好地并行化,代价是需要将连接另一侧的输入记录复制到多个Reducer上。 Crunch中的**分片连接(sharded join)** 方法与之类似,但需要显式指定热键而不是使用抽样作业。这种技术也非常类似于我们在“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”中讨论的技术,使用随机化来缓解分区数据库中的热点。 @@ -623,7 +623,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5 #### Pregel处理模型 -​ 针对图批处理的优化 —— **批量同步并行(BSP,Bulk Synchronous Parallel)**计算模型【70】已经开始流行起来。其中,Apache Giraph 【37】,Spark的GraphX API和Flink的Gelly API 【71】实现了它。它也被称为**Pregel**模型,因为Google的Pregel论文推广了这种处理图的方法【72】。 +​ 针对图批处理的优化 —— **批量同步并行(BSP,Bulk Synchronous Parallel)** 计算模型【70】已经开始流行起来。其中,Apache Giraph 【37】,Spark的GraphX API和Flink的Gelly API 【71】实现了它。它也被称为**Pregel**模型,因为Google的Pregel论文推广了这种处理图的方法【72】。 ​ 回想一下在MapReduce中,Mapper在概念上向Reducer的特定调用“发送消息”,因为框架将所有具有相同键的Mapper输出集中在一起。 Pregel背后有一个类似的想法:一个顶点可以向另一个顶点“发送消息”,通常这些消息是沿着图的边发送的。 diff --git a/ch11.md b/ch11.md index d2bdf87..6957438 100644 --- a/ch11.md +++ b/ch11.md @@ -177,7 +177,7 @@ ​ 如果只追加写入日志,则磁盘空间终究会耗尽。为了回收磁盘空间,日志实际上被分割成段,并不时地将旧段删除或移动到归档存储。 (我们将在后面讨论一种更为复杂的磁盘空间释放方式) -​ 这就意味着如果一个慢消费者跟不上消息产生的速率而落后得太多,它的消费偏移量指向了删除的段,那么它就会错过一些消息。实际上,日志实现了一个有限大小的缓冲区,当缓冲区填满时会丢弃旧消息,它也被称为**循环缓冲区(circular buffer)**或**环形缓冲区(ring buffer)**。不过由于缓冲区在磁盘上,因此缓冲区可能相当的大。 +​ 这就意味着如果一个慢消费者跟不上消息产生的速率而落后得太多,它的消费偏移量指向了删除的段,那么它就会错过一些消息。实际上,日志实现了一个有限大小的缓冲区,当缓冲区填满时会丢弃旧消息,它也被称为**循环缓冲区(circular buffer)** 或**环形缓冲区(ring buffer)**。不过由于缓冲区在磁盘上,因此缓冲区可能相当的大。 ​ 让我们做个简单计算。在撰写本文时,典型的大型硬盘容量为6TB,顺序写入吞吐量为150MB/s。如果以最快的速度写消息,则需要大约11个小时才能填满磁盘。因而磁盘可以缓冲11个小时的消息,之后它将开始覆盖旧的消息。即使使用多个磁盘和机器,这个比率也是一样的。实践中的部署很少能用满磁盘的写入带宽,所以通常可以保存一个几天甚至几周的日志缓冲区。 @@ -325,7 +325,7 @@ #### 命令和事件 -​ 事件溯源的哲学是仔细区分**事件(event)**和**命令(command)**【48】。当来自用户的请求刚到达时,它一开始是一个命令:在这个时间点上它仍然可能可能失败,比如,因为违反了一些完整性条件。应用必须首先验证它是否可以执行该命令。如果验证成功并且命令被接受,则它变为一个持久化且不可变的事件。 +​ 事件溯源的哲学是仔细区分**事件(event)** 和**命令(command)**【48】。当来自用户的请求刚到达时,它一开始是一个命令:在这个时间点上它仍然可能可能失败,比如,因为违反了一些完整性条件。应用必须首先验证它是否可以执行该命令。如果验证成功并且命令被接受,则它变为一个持久化且不可变的事件。 ​ 例如,如果用户试图注册特定用户名,或预定飞机或剧院的座位,则应用需要检查用户名或座位是否已被占用。(先前在“[容错共识](ch8.md#容错共识)”中讨论过这个例子)当检查成功时,应用可以生成一个事件,指示特定的用户名是由特定的用户ID注册的,或者座位已经预留给特定的顾客。 @@ -416,7 +416,7 @@ $$ 2. 你能以某种方式将事件推送给用户,例如发送报警邮件或推送通知,或将事件流式传输到可实时显示的仪表板上。在这种情况下,人是流的最终消费者。 3. 你可以处理一个或多个输入流,并产生一个或多个输出流。流可能会经过由几个这样的处理阶段组成的流水线,最后再输出(选项1或2)。 - 在本章的剩余部分中,我们将讨论选项3:处理流以产生其他衍生流。处理这样的流的代码片段,被称为**算子(operator)**或**作业(job)**。它与我们在[第十章](ch10.md)中讨论过的Unix进程和MapReduce作业密切相关,数据流的模式是相似的:一个流处理器以只读的方式使用输入流,并将其输出以仅追加的方式写入一个不同的位置。 + 在本章的剩余部分中,我们将讨论选项3:处理流以产生其他衍生流。处理这样的流的代码片段,被称为**算子(operator)** 或**作业(job)**。它与我们在[第十章](ch10.md)中讨论过的Unix进程和MapReduce作业密切相关,数据流的模式是相似的:一个流处理器以只读的方式使用输入流,并将其输出以仅追加的方式写入一个不同的位置。 ​ 流处理中的分区和并行化模式也非常类似于[第十章](ch10.md)中介绍的MapReduce和数据流引擎,因此我们不再重复这些主题。基本的Map操作(如转换和过滤记录)也是一样的。 diff --git a/ch12.md b/ch12.md index 222d84d..8367af2 100644 --- a/ch12.md +++ b/ch12.md @@ -119,7 +119,7 @@ > > ​ 大规模的“模式迁移”也发生在非计算机系统中。例如,在19世纪英国铁路建设初期,轨距(两轨之间的距离)就有了各种各样的竞争标准。为一种轨距而建的列车不能在另一种轨距的轨道上运行,这限制了火车网络中可能的相互连接【9】。 > -> ​ 在1846年最终确定了一个标准轨距之后,其他轨距的轨道必须转换 —— 但是如何在不停运火车线路的情况下进行数月甚至数年的迁移?解决的办法是首先通过添加第三条轨道将轨道转换为**双轨距(dual guage)**或**混合轨距**。这种转换可以逐渐完成,当完成时,两种轨距的列车都可以在线路上跑,使用三条轨道中的两条。事实上,一旦所有的列车都转换成标准轨距,那么可以移除提供非标准轨距的轨道。 +> ​ 在1846年最终确定了一个标准轨距之后,其他轨距的轨道必须转换 —— 但是如何在不停运火车线路的情况下进行数月甚至数年的迁移?解决的办法是首先通过添加第三条轨道将轨道转换为**双轨距(dual guage)** 或**混合轨距**。这种转换可以逐渐完成,当完成时,两种轨距的列车都可以在线路上跑,使用三条轨道中的两条。事实上,一旦所有的列车都转换成标准轨距,那么可以移除提供非标准轨距的轨道。 > > ​ 以这种方式“再加工”现有的轨道,让新旧版本并存,可以在几年的时间内逐渐改变轨距。然而,这是一项昂贵的事业,这就是今天非标准轨距仍然存在的原因。例如,旧金山湾区的BART系统使用了与美国大部分地区不同的轨距。 diff --git a/ch2.md b/ch2.md index 6aba98b..f7ed249 100644 --- a/ch2.md +++ b/ch2.md @@ -143,7 +143,7 @@ JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的**局部 使用ID的好处是,ID对人类没有任何意义,因而永远不需要改变:ID可以保持不变,即使它标识的信息发生变化。任何对人类有意义的东西都可能需要在将来某个时候改变——如果这些信息被复制,所有的冗余副本都需要更新。这会导致写入开销,也存在不一致的风险(一些副本被更新了,还有些副本没有被更新)。去除此类重复是数据库 **规范化(normalization)** 的关键思想。[^ii] -[^ii]: 关于关系模型的文献区分了几种不同的规范形式,但这些区别几乎没有实际意义。一个经验法则是,如果重复存储了可以存储在一个地方的值,则模式就不是**规范化(normalized)**的。 +[^ii]: 关于关系模型的文献区分了几种不同的规范形式,但这些区别几乎没有实际意义。一个经验法则是,如果重复存储了可以存储在一个地方的值,则模式就不是**规范化(normalized)** 的。 > 数据库管理员和开发人员喜欢争论规范化和非规范化,让我们暂时保留判断吧。在本书的[第三部分](part-iii.md),我们将回到这个话题,探讨系统的方法用以处理缓存,非规范化和衍生数据。 @@ -515,7 +515,7 @@ db.observations.aggregate([ 但是,要是多对多关系在你的数据中很常见呢?关系模型可以处理多对多关系的简单情况,但是随着数据之间的连接变得更加复杂,将数据建模为图形显得更加自然。 -一个图由两种对象组成:**顶点(vertices)**(也称为**节点(nodes)** 或**实体(entities)**),和**边(edges)**( 也称为**关系(relationships)**或**弧 (arcs)** )。多种数据可以被建模为一个图形。典型的例子包括: +一个图由两种对象组成:**顶点(vertices)**(也称为**节点(nodes)** 或**实体(entities)**),和**边(edges)**( 也称为**关系(relationships)** 或**弧 (arcs)** )。多种数据可以被建模为一个图形。典型的例子包括: ***社交图谱*** diff --git a/ch3.md b/ch3.md index 1960c52..4c387df 100644 --- a/ch3.md +++ b/ch3.md @@ -36,7 +36,7 @@ db_get () { } ``` -这两个函数实现了键值存储的功能。执行 `db_set key value` ,会将 **键(key)**和**值(value)** 存储在数据库中。键和值(几乎)可以是你喜欢的任何东西,例如,值可以是JSON文档。然后调用 `db_get key` ,查找与该键关联的最新值并将其返回。 +这两个函数实现了键值存储的功能。执行 `db_set key value` ,会将 **键(key)** 和**值(value)** 存储在数据库中。键和值(几乎)可以是你喜欢的任何东西,例如,值可以是JSON文档。然后调用 `db_get key` ,查找与该键关联的最新值并将其返回。 麻雀虽小,五脏俱全: @@ -81,7 +81,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)** 该位置并读取该值。 @@ -310,7 +310,7 @@ B树在数据库体系结构中是非常根深蒂固的,为许多工作负载 在某些情况下,从索引到堆文件的额外跳跃对读取来说性能损失太大,因此可能希望将索引行直接存储在索引中。这被称为聚集索引。例如,在MySQL的InnoDB存储引擎中,表的主键总是一个聚集索引,二级索引则引用主键(而不是堆文件中的位置)【31】。在SQL Server中,可以为每个表指定一个聚集索引【32】。 -在 **聚集索引(clustered index)** (在索引中存储所有行数据)和 **非聚集索引(nonclustered index)** (仅在索引中存储对数据的引用)之间的折衷被称为 **包含列的索引(index with included columns)**或**覆盖索引(covering index)**,其存储表的一部分在索引内【33】。这允许通过单独使用索引来回答一些查询(这种情况叫做:索引 **覆盖(cover)** 了查询)【32】。 +在 **聚集索引(clustered index)** (在索引中存储所有行数据)和 **非聚集索引(nonclustered index)** (仅在索引中存储对数据的引用)之间的折衷被称为 **包含列的索引(index with included columns)** 或**覆盖索引(covering index)**,其存储表的一部分在索引内【33】。这允许通过单独使用索引来回答一些查询(这种情况叫做:索引 **覆盖(cover)** 了查询)【32】。 与任何类型的数据重复一样,聚集和覆盖索引可以加快读取速度,但是它们需要额外的存储空间,并且会增加写入开销。数据库还需要额外的努力来执行事务保证,因为应用程序不应看到任何因为重复而导致的不一致。 diff --git a/ch4.md b/ch4.md index d8c7e7f..811f139 100644 --- a/ch4.md +++ b/ch4.md @@ -36,7 +36,7 @@ 向前兼容性可能会更棘手,因为旧版的程序需要忽略新版数据格式中新增的部分。 -本章中将介绍几种编码数据的格式,包括 JSON,XML,Protocol Buffers,Thrift和Avro。尤其将关注这些格式如何应对模式变化,以及它们如何对新旧代码数据需要共存的系统提供支持。然后将讨论如何使用这些格式进行数据存储和通信:在Web服务中,**表述性状态传递(REST)**和**远程过程调用(RPC)**,以及**消息传递系统**(如Actor和消息队列)。 +本章中将介绍几种编码数据的格式,包括 JSON,XML,Protocol Buffers,Thrift和Avro。尤其将关注这些格式如何应对模式变化,以及它们如何对新旧代码数据需要共存的系统提供支持。然后将讨论如何使用这些格式进行数据存储和通信:在Web服务中,**表述性状态传递(REST)** 和**远程过程调用(RPC)**,以及**消息传递系统**(如Actor和消息队列)。 ## 编码数据的格式 @@ -47,7 +47,7 @@ [^i]: 除一些特殊情况外,例如某些内存映射文件或直接在压缩数据上操作(如“[列压缩](ch4.md#列压缩)”中所述)。 -所以,需要在两种表示之间进行某种类型的翻译。 从内存中表示到字节序列的转换称为 **编码(Encoding)** (也称为**序列化(serialization)**或**编组(marshalling)**),反过来称为**解码(Decoding)**[^ii](**解析(Parsing)**,**反序列化(deserialization)**,**反编组( unmarshalling)**)[^译i]。 +所以,需要在两种表示之间进行某种类型的翻译。 从内存中表示到字节序列的转换称为 **编码(Encoding)** (也称为**序列化(serialization)** 或**编组(marshalling)**),反过来称为**解码(Decoding)**[^ii](**解析(Parsing)**,**反序列化(deserialization)**,**反编组( unmarshalling)**)[^译i]。 [^ii]: 请注意,**编码(encode)** 与 **加密(encryption)** 无关。 本书不讨论加密。 [^译i]: Marshal与Serialization的区别:Marshal不仅传输对象的状态,而且会一起传输对象的方法(相关代码)。 diff --git a/ch5.md b/ch5.md index 4ac0f15..8c52c09 100644 --- a/ch5.md +++ b/ch5.md @@ -18,7 +18,7 @@ 本章将假设你的数据集非常小,每台机器都可以保存整个数据集的副本。在[第六章](ch6.md)中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。 -​ 如果复制中的数据不会随时间而改变,那复制就很简单:将数据复制到每个节点一次就万事大吉。复制的困难之处在于处理复制数据的**变更(change)**,这就是本章所要讲的。我们将讨论三种流行的变更复制算法:**单领导者(single leader)**,**多领导者(multi leader)**和**无领导者(leaderless)**。几乎所有分布式数据库都使用这三种方法之一。 +​ 如果复制中的数据不会随时间而改变,那复制就很简单:将数据复制到每个节点一次就万事大吉。复制的困难之处在于处理复制数据的**变更(change)**,这就是本章所要讲的。我们将讨论三种流行的变更复制算法:**单领导者(single leader)**,**多领导者(multi leader)** 和**无领导者(leaderless)**。几乎所有分布式数据库都使用这三种方法之一。 ​ 在复制时需要进行许多权衡:例如,使用同步复制还是异步复制?如何处理失败的副本?这些通常是数据库中的配置选项,细节因数据库而异,但原理在许多不同的实现中都类似。本章会讨论这些决策的后果。 @@ -31,7 +31,7 @@ ​ 每一次向数据库的写入操作都需要传播到所有副本上,否则副本就会包含不一样的数据。最常见的解决方案被称为 **基于领导者的复制(leader-based replication)** (也称 **主动/被动(active/passive)** 或 **主/从(master/slave)** 复制),如[图5-1](#fig5-1.png)所示。它的工作原理如下: 1. 副本之一被指定为 **领导者(leader)**,也称为 **主库(master|primary)** 。当客户端要向数据库写入时,它必须将请求发送给**领导者**,领导者会将新数据写入其本地存储。 -2. 其他副本被称为**追随者(followers)**,亦称为**只读副本(read replicas)**,**从库(slaves)**,**备库( secondaries)**,**热备(hot-standby)**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为**复制日志(replication log)**记录或**变更流(change stream)**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。 +2. 其他副本被称为**追随者(followers)**,亦称为**只读副本(read replicas)**,**从库(slaves)**,**备库( secondaries)**,**热备(hot-standby)**[^i]。每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称之为**复制日志(replication log)** 记录或**变更流(change stream)**。每个跟随者从领导者拉取日志,并相应更新其本地数据库副本,方法是按照领导者处理的相同顺序应用所有写入。 3. 当客户想要从数据库中读取数据时,它可以向领导者或追随者查询。 但只有领导者才能接受写操作(从客户端的角度来看从库都是只读的)。 [^i]: 不同的人对 **热(hot)**,**温(warm)**,**冷(cold)** 备份服务器有不同的定义。 例如在PostgreSQL中,**热备(hot standby)** 指的是能接受客户端读请求的副本。而 **温备(warm standby)** 只是追随领导者,但不处理客户端的任何查询。 就本书而言,这些差异并不重要。 @@ -103,7 +103,7 @@ ​ 故障切换可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的主库)或自动进行。自动故障切换过程通常由以下步骤组成: 1. 确认主库失效。有很多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是简单使用 **超时(Timeout)** :节点频繁地相互来回传递消息,并且如果一个节点在一段时间内(例如30秒)没有响应,就认为它挂了(因为计划内维护而故意关闭主库不算)。 -2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的**控制器节点(controller node)**来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(最小化数据损失)。让所有的节点同意一个新的领导者,是一个**共识**问题,将在[第九章](ch9.md)详细讨论。 +2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的**控制器节点(controller node)** 来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(最小化数据损失)。让所有的节点同意一个新的领导者,是一个**共识**问题,将在[第九章](ch9.md)详细讨论。 3. 重新配置系统以启用新的主库。客户端现在需要将它们的写请求发送给新主库(将在“[请求路由](ch6.md#请求路由)”中讨论这个问题)。如果老领导回来,可能仍然认为自己是主库,没有意识到其他副本已经让它下台了。系统需要确保老领导认可新领导,成为一个从库。 故障切换会出现很多大麻烦: diff --git a/ch6.md b/ch6.md index ab4f47e..6894469 100644 --- a/ch6.md +++ b/ch6.md @@ -121,7 +121,7 @@ ​ 次级索引是关系型数据库的基础,并且在文档数据库中也很普遍。许多键值存储(如HBase和Volde-mort)为了减少实现的复杂度而放弃了次级索引,但是一些(如Riak)已经开始添加它们,因为它们对于数据模型实在是太有用了。并且次级索引也是Solr和Elasticsearch等搜索服务器的基石。 -​ 次级索引的问题是它们不能整齐地映射到分区。有两种用二级索引对数据库进行分区的方法:**基于文档的分区(document-based)**和**基于关键词(term-based)的分区**。 +​ 次级索引的问题是它们不能整齐地映射到分区。有两种用二级索引对数据库进行分区的方法:**基于文档的分区(document-based)** 和**基于关键词(term-based)的分区**。 ### 基于文档的二级索引进行分区 diff --git a/ch7.md b/ch7.md index ec5bbca..0053691 100644 --- a/ch7.md +++ b/ch7.md @@ -47,11 +47,11 @@ ### ACID的含义 -事务所提供的安全保证,通常由众所周知的首字母缩略词ACID来描述,ACID代表**原子性(Atomicity)**,**一致性(Consistency)**,**隔离性(Isolation)**和**持久性(Durability)**。它由Theo Härder和Andreas Reuter于1983年提出,旨在为数据库中的容错机制建立精确的术语。 +事务所提供的安全保证,通常由众所周知的首字母缩略词ACID来描述,ACID代表**原子性(Atomicity)**,**一致性(Consistency)**,**隔离性(Isolation)** 和**持久性(Durability)**。它由Theo Härder和Andreas Reuter于1983年提出,旨在为数据库中的容错机制建立精确的术语。 但实际上,不同数据库的ACID实现并不相同。例如,我们将会看到,关于**隔离性**的含义就有许多含糊不清【8】。高层次上的想法很美好,但魔鬼隐藏在细节里。今天,当一个系统声称自己“符合ACID”时,实际上能期待的是什么保证并不清楚。不幸的是,ACID现在几乎已经变成了一个营销术语。 -(不符合ACID标准的系统有时被称为BASE,它代表**基本可用性(Basically Available)**,**软状态(Soft State)**和**最终一致性(Eventual consistency)**【9】,这比ACID的定义更加模糊,似乎BASE的唯一合理的定义是“不是ACID”,即它几乎可以代表任何你想要的东西。) +(不符合ACID标准的系统有时被称为BASE,它代表**基本可用性(Basically Available)**,**软状态(Soft State)** 和**最终一致性(Eventual consistency)**【9】,这比ACID的定义更加模糊,似乎BASE的唯一合理的定义是“不是ACID”,即它几乎可以代表任何你想要的东西。) 让我们深入了解原子性,一致性,隔离性和持久性的定义,这可以让我们提炼出事务的思想。 @@ -296,7 +296,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true 爱丽丝在银行有1000美元的储蓄,分为两个账户,每个500美元。现在一笔事务从她的一个账户转移了100美元到另一个账户。如果她在事务处理的同时查看其账户余额列表,她可能会碰巧在收款到达前看到收款账户的余额仍然是500美元,而在付款产生后看到付款账户的余额已经是400美元。对爱丽丝来说,现在她的账户似乎总共只有900美元——看起来有100美元已经凭空消失了。 -这种异常被称为**不可重复读(nonrepeatable read)**或**读取偏差(read skew)**:如果Alice在事务结束时再次读取账户1的余额,她将看到与她之前的查询中看到的不同的值(600美元)。在读已提交的隔离条件下,**不可重复读**被认为是可接受的:Alice看到的帐户余额时确实在阅读时已经提交了。 +这种异常被称为**不可重复读(nonrepeatable read)** 或**读取偏差(read skew)**:如果Alice在事务结束时再次读取账户1的余额,她将看到与她之前的查询中看到的不同的值(600美元)。在读已提交的隔离条件下,**不可重复读**被认为是可接受的:Alice看到的帐户余额时确实在阅读时已经提交了。 > 不幸的是,术语**偏差(skew)** 这个词是过载的:以前使用它是因为热点的不平衡工作量(请参阅“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”),而这里偏差意味着异常的时序。 @@ -368,7 +368,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true #### 可重复读与命名混淆 -快照隔离是一个有用的隔离级别,特别对于只读事务而言。但是,许多数据库实现了它,却用不同的名字来称呼。在Oracle中称为**可串行化(Serializable)**的,在PostgreSQL和MySQL中称为**可重复读(repeatable read)**【23】。 +快照隔离是一个有用的隔离级别,特别对于只读事务而言。但是,许多数据库实现了它,却用不同的名字来称呼。在Oracle中称为**可串行化(Serializable)** 的,在PostgreSQL和MySQL中称为**可重复读(repeatable read)**【23】。 这种命名混淆的原因是SQL标准没有**快照隔离**的概念,因为标准是基于System R 1975年定义的隔离级别【2】,那时候**快照隔离**尚未发明。相反,它定义了**可重复读**,表面上看起来与快照隔离很相似。 PostgreSQL和MySQL称其**快照隔离**级别为**可重复读(repeatable read)**,因为这样符合标准要求,所以它们可以声称自己“标准兼容”。 @@ -588,7 +588,7 @@ COMMIT; 这不是一个新问题,从20世纪70年代以来就一直是这样了,当时首先引入了较弱的隔离级别【2】。一直以来,研究人员的答案都很简单:使用**可串行化(serializable)** 的隔离级别! -**可串行化(Serializability)**隔离通常被认为是最强的隔离级别。它保证即使事务可以并行执行,最终的结果也是一样的,就好像它们没有任何并发性,连续挨个执行一样。因此数据库保证,如果事务在单独运行时正常运行,则它们在并发运行时继续保持正确 —— 换句话说,数据库可以防止**所有**可能的竞争条件。 +**可串行化(Serializability)** 隔离通常被认为是最强的隔离级别。它保证即使事务可以并行执行,最终的结果也是一样的,就好像它们没有任何并发性,连续挨个执行一样。因此数据库保证,如果事务在单独运行时正常运行,则它们在并发运行时继续保持正确 —— 换句话说,数据库可以防止**所有**可能的竞争条件。 但如果可串行化隔离级别比弱隔离级别的烂摊子要好得多,那为什么没有人见人爱?为了回答这个问题,我们需要看看实现可串行化的选项,以及它们如何执行。目前大多数提供可串行化的数据库都使用了三种技术之一,本章的剩余部分将会介绍这些技术: @@ -687,7 +687,7 @@ VoltDB还使用存储过程进行复制:但不是将事务的写入结果从 2PL用于MySQL(InnoDB)和SQL Server中的可串行化隔离级别,以及DB2中的可重复读隔离级别【23,36】。 -读与写的阻塞是通过为数据库中每个对象添加锁来实现的。锁可以处于**共享模式(shared mode)**或**独占模式(exclusive mode)**。锁使用如下: +读与写的阻塞是通过为数据库中每个对象添加锁来实现的。锁可以处于**共享模式(shared mode)** 或**独占模式(exclusive mode)**。锁使用如下: - 若事务要读取对象,则须先以共享模式获取锁。允许多个事务同时持有共享锁。但如果另一个事务已经在对象上持有排它锁,则这些事务必须等待。 - 若事务要写入一个对象,它必须首先以独占模式获取该锁。没有其他事务可以同时持有锁(无论是共享模式还是独占模式),所以如果对象上存在任何锁,该事务必须等待。 @@ -710,7 +710,7 @@ VoltDB还使用存储过程进行复制:但不是将事务的写入结果从 #### 谓词锁 -在前面关于锁的描述中,我们掩盖了一个微妙而重要的细节。在“[导致写入偏差的幻读](#导致写入偏差的幻读)”中,我们讨论了**幻读(phantoms)**的问题。即一个事务改变另一个事务的搜索查询的结果。具有可串行化隔离级别的数据库必须防止**幻读**。 +在前面关于锁的描述中,我们掩盖了一个微妙而重要的细节。在“[导致写入偏差的幻读](#导致写入偏差的幻读)”中,我们讨论了**幻读(phantoms)** 的问题。即一个事务改变另一个事务的搜索查询的结果。具有可串行化隔离级别的数据库必须防止**幻读**。 在会议室预订的例子中,这意味着如果一个事务在某个时间窗口内搜索了一个房间的现有预订(见[例7-2]()),则另一个事务不能同时插入或更新同一时间窗口与同一房间的另一个预订 (可以同时插入其他房间的预订,或在不影响另一个预定的条件下预定同一房间的其他时间段)。 diff --git a/ch8.md b/ch8.md index 833f668..f0cdbd2 100644 --- a/ch8.md +++ b/ch8.md @@ -123,7 +123,7 @@ > #### 网络分区 > -> ​ 当网络的一部分由于网络故障而被切断时,有时称为**网络分区(network partition)**或**网络断裂(netsplit)**。在本书中,我们通常会坚持使用更一般的术语**网络故障(network fault)**,以避免与[第六章](ch6.md)讨论的存储系统的分区(分片)相混淆。 +> ​ 当网络的一部分由于网络故障而被切断时,有时称为**网络分区(network partition)** 或**网络断裂(netsplit)**。在本书中,我们通常会坚持使用更一般的术语**网络故障(network fault)**,以避免与[第六章](ch6.md)讨论的存储系统的分区(分片)相混淆。 ​ 即使网络故障在你的环境中非常罕见,故障可能发生的事实,意味着你的软件需要能够处理它们。无论何时通过网络进行通信,都可能会失败,这是无法避免的。 @@ -170,7 +170,7 @@ * 如果多个不同的节点同时尝试将数据包发送到同一目的地,则网络交换机必须将它们排队并将它们逐个送入目标网络链路(如[图8-2](img/fig8-2.png)所示)。在繁忙的网络链路上,数据包可能需要等待一段时间才能获得一个插槽(这称为网络拥塞)。如果传入的数据太多,交换机队列填满,数据包将被丢弃,因此需要重新发送数据包 - 即使网络运行良好。 * 当数据包到达目标机器时,如果所有CPU内核当前都处于繁忙状态,则来自网络的传入请求将被操作系统排队,直到应用程序准备好处理它为止。根据机器上的负载,这可能需要一段任意的时间。 * 在虚拟化环境中,正在运行的操作系统经常暂停几十毫秒,因为另一个虚拟机正在使用CPU内核。在这段时间内,虚拟机不能从网络中消耗任何数据,所以传入的数据被虚拟机监视器 【26】排队(缓冲),进一步增加了网络延迟的可变性。 -* TCP执行**流量控制(flow control)**(也称为**拥塞避免(congestion avoidance)**或**背压(backpressure)**),其中节点会限制自己的发送速率以避免网络链路或接收节点过载【27】。这意味着甚至在数据进入网络之前,在发送者处就需要进行额外的排队。 +* TCP执行**流量控制(flow control)**(也称为**拥塞避免(congestion avoidance)** 或**背压(backpressure)**),其中节点会限制自己的发送速率以避免网络链路或接收节点过载【27】。这意味着甚至在数据进入网络之前,在发送者处就需要进行额外的排队。 ![](img/fig8-2.png) @@ -221,7 +221,7 @@ ​ 但是,目前在多租户数据中心和公共云或通过互联网[^iv]进行通信时,此类服务质量尚未启用。当前部署的技术不允许我们对网络的延迟或可靠性作出任何保证:我们必须假设网络拥塞,排队和无限的延迟总是会发生。因此,超时时间没有“正确”的值——它需要通过实验来确定。 -[^iv]: 互联网服务提供商之间的对等协议和通过**BGP网关协议(BGP)**建立的路由,与IP协议相比,更接近于电路交换。在这个级别上,可以购买专用带宽。但是,互联网路由在网络级别运行,而不是主机之间的单独连接,而且运行时间要长得多。 +[^iv]: 互联网服务提供商之间的对等协议和通过**BGP网关协议(BGP)** 建立的路由,与IP协议相比,更接近于电路交换。在这个级别上,可以购买专用带宽。但是,互联网路由在网络级别运行,而不是主机之间的单独连接,而且运行时间要长得多。 > ### 延迟和资源利用 > @@ -638,7 +638,7 @@ while (true) { * 节点的时钟可能会与其他节点显著不同步(尽管您尽最大努力设置NTP),它可能会突然跳转或跳回,依靠它是很危险的,因为您很可能没有好的方法来测量你的时钟的错误间隔。 * 一个进程可能会在其执行的任何时候暂停一段相当长的时间(可能是因为停止所有处理的垃圾收集器),被其他节点宣告死亡,然后再次复活,却没有意识到它被暂停了。 -这类**部分失效(partial failure)**可能发生的事实是分布式系统的决定性特征。每当软件试图做任何涉及其他节点的事情时,偶尔就有可能会失败,或者随机变慢,或者根本没有响应(最终超时)。在分布式系统中,我们试图在软件中建立**部分失效**的容错机制,这样整个系统在即使某些组成部分被破坏的情况下,也可以继续运行。 +这类**部分失效(partial failure)** 可能发生的事实是分布式系统的决定性特征。每当软件试图做任何涉及其他节点的事情时,偶尔就有可能会失败,或者随机变慢,或者根本没有响应(最终超时)。在分布式系统中,我们试图在软件中建立**部分失效**的容错机制,这样整个系统在即使某些组成部分被破坏的情况下,也可以继续运行。 ​ 为了容忍错误,第一步是**检测**它们,但即使这样也很难。大多数系统没有检测节点是否发生故障的准确机制,所以大多数分布式算法依靠**超时**来确定远程节点是否仍然可用。但是,超时无法区分网络失效和节点失效,并且可变的网络延迟有时会导致节点被错误地怀疑发生故障。此外,有时一个节点可能处于降级状态:例如,由于驱动程序错误,千兆网卡可能突然下降到1 Kb/s的吞吐量【94】。这样一个“跛行”而不是死掉的节点可能比一个干净的失效节点更难处理。 diff --git a/ch9.md b/ch9.md index d7d1672..65d247a 100644 --- a/ch9.md +++ b/ch9.md @@ -55,7 +55,7 @@ ​ 在**最终一致**的数据库,如果你在同一时刻问两个不同副本相同的问题,可能会得到两个不同的答案。这很让人困惑。如果数据库可以提供只有一个副本的假象(即,只有一个数据副本),那么事情就简单太多了。那么每个客户端都会有相同的数据视图,且不必担心复制滞后了。 -​ 这就是**线性一致性(linearizability)**背后的想法【6】(也称为**原子一致性(atomic consistency)**【7】,**强一致性(strong consistency)**,**立即一致性(immediate consistency)**或**外部一致性(external consistency )**【8】)。线性一致性的精确定义相当微妙,我们将在本节的剩余部分探讨它。但是基本的想法是让一个系统看起来好像只有一个数据副本,而且所有的操作都是原子性的。有了这个保证,即使实际中可能有多个副本,应用也不需要担心它们。 +​ 这就是**线性一致性(linearizability)** 背后的想法【6】(也称为**原子一致性(atomic consistency)**【7】,**强一致性(strong consistency)**,**立即一致性(immediate consistency)** 或**外部一致性(external consistency )**【8】)。线性一致性的精确定义相当微妙,我们将在本节的剩余部分探讨它。但是基本的想法是让一个系统看起来好像只有一个数据副本,而且所有的操作都是原子性的。有了这个保证,即使实际中可能有多个副本,应用也不需要担心它们。 ​ 在一个线性一致的系统中,只要一个客户端成功完成写操作,所有客户端从数据库中读取数据必须能够看到刚刚写入的值。要维护数据的单个副本的假象,系统应保障读到的值是最近的、最新的,而不是来自陈旧的缓存或副本。换句话说,线性一致性是一个**新鲜度保证(recency guarantee)**。为了阐明这个想法,我们来看看一个非线性一致系统的例子。 @@ -125,7 +125,7 @@ * 在客户端A从数据库收到响应之前,客户端B的读取返回 `1` ,表示写入值 `1` 已成功。这也是可以的:这并不意味着在写之前读到了值,这只是意味着从数据库到客户端A的正确响应在网络中略有延迟。 -* 此模型不假设有任何事务隔离:另一个客户端可能随时更改值。例如,C首先读取 `1` ,然后读取 `2` ,因为两次读取之间的值由B更改。可以使用原子**比较并设置(cas)**操作来检查该值是否未被另一客户端同时更改:B和C的**cas**请求成功,但是D的**cas**请求失败(在数据库处理它时,`x` 的值不再是 `0` )。 +* 此模型不假设有任何事务隔离:另一个客户端可能随时更改值。例如,C首先读取 `1` ,然后读取 `2` ,因为两次读取之间的值由B更改。可以使用原子**比较并设置(cas)** 操作来检查该值是否未被另一客户端同时更改:B和C的**cas**请求成功,但是D的**cas**请求失败(在数据库处理它时,`x` 的值不再是 `0` )。 * 客户B的最后一次读取(阴影条柱中)不是线性一致性的。 该操作与C的**cas**写操作并发(它将 `x` 从 `2` 更新为 `4` )。在没有其他请求的情况下,B的读取返回 `2` 是可以的。然而,在B的读取开始之前,客户端A已经读取了新的值 `4` ,因此不允许B读取比A更旧的值。再次,与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同。 @@ -139,11 +139,11 @@ > > ***可串行化*** > -> **可串行化(Serializability)**是事务的隔离属性,每个事务可以读写多个对象(行,文档,记录)——请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”。它确保事务的行为,与它们按照**某种**顺序依次执行的结果相同(每个事务在下一个事务开始之前运行完成)。这种执行顺序可以与事务实际执行的顺序不同。【12】。 +> **可串行化(Serializability)** 是事务的隔离属性,每个事务可以读写多个对象(行,文档,记录)——请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”。它确保事务的行为,与它们按照**某种**顺序依次执行的结果相同(每个事务在下一个事务开始之前运行完成)。这种执行顺序可以与事务实际执行的顺序不同。【12】。 > > ***线性一致性*** > -> **线性一致性(Linearizability)**是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写入偏差等问题(请参阅“[写入偏差和幻读](ch7.md#写入偏斜与幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。 +> **线性一致性(Linearizability)** 是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写入偏差等问题(请参阅“[写入偏差和幻读](ch7.md#写入偏斜与幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。 > > 一个数据库可以提供可串行化和线性一致性,这种组合被称为严格的可串行化或**强的单副本可串行化(strong-1SR)**【4,13】。基于两阶段锁定的可串行化实现(请参阅“[两阶段锁定](ch7.md#两阶段锁定)”一节)或**真的串行执行**(请参阅第“[真的串行执行](ch7.md#真的串行执行)”)通常是线性一致性的。 > @@ -291,7 +291,7 @@ #### 线性一致性和网络延迟 -​ 虽然线性一致是一个很有用的保证,但实际上,线性一致的系统惊人的少。例如,现代多核CPU上的内存甚至都不是线性一致的【43】:如果一个CPU核上运行的线程写入某个内存地址,而另一个CPU核上运行的线程不久之后读取相同的地址,并没有保证一定能一定读到第一个线程写入的值(除非使用了**内存屏障(memory barrier)**或**围栏(fence)**【44】)。 +​ 虽然线性一致是一个很有用的保证,但实际上,线性一致的系统惊人的少。例如,现代多核CPU上的内存甚至都不是线性一致的【43】:如果一个CPU核上运行的线程写入某个内存地址,而另一个CPU核上运行的线程不久之后读取相同的地址,并没有保证一定能一定读到第一个线程写入的值(除非使用了**内存屏障(memory barrier)** 或**围栏(fence)**【44】)。 ​ 这种行为的原因是每个CPU核都有自己的内存缓存和存储缓冲区。默认情况下,内存访问首先走缓存,任何变更会异步写入主存。因为缓存访问比主存要快得多【45】,所以这个特性对于现代CPU的良好性能表现至关重要。但是现在就有几个数据副本(一个在主存中,也许还有几个在不同缓存中的其他副本),而且这些副本是异步更新的,所以就失去了线性一致性。 @@ -307,7 +307,7 @@ ​ 之前说过,线性一致寄存器的行为就好像只有单个数据副本一样,且每个操作似乎都是在某个时间点以原子性的方式生效的。这个定义意味着操作是按照某种良好定义的顺序执行的。我们将操作以看上去被执行的顺序连接起来,以此说明了[图9-4](img/fig9-4.png)中的顺序。 -**顺序(ordering)**这一主题在本书中反复出现,这表明它可能是一个重要的基础性概念。让我们简要回顾一下其它曾经出现过**顺序**的上下文: +**顺序(ordering)** 这一主题在本书中反复出现,这表明它可能是一个重要的基础性概念。让我们简要回顾一下其它曾经出现过**顺序**的上下文: * 在[第五章](ch5.md)中我们看到,领导者在单主复制中的主要目的就是,在复制日志中确定**写入顺序(order of write)**——也就是从库应用这些写入的顺序。如果不存在一个领导者,则并发操作可能导致冲突(请参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。 * 在[第七章](ch7.md)中讨论的**可串行化**,是关于事务表现的像按**某种先后顺序(some sequential order)** 执行的保证。它可以字面意义上地以**串行顺序(serial order)** 执行事务来实现,或者允许并行执行,但同时防止序列化冲突来实现(通过锁或中止事务)。 @@ -319,9 +319,9 @@ **顺序**反复出现有几个原因,其中一个原因是,它有助于保持**因果关系(causality)**。在本书中我们已经看到了几个例子,其中因果关系是很重要的: -* 在“[一致前缀读](ch5.md#一致前缀读)”([图5-5](img/fig5-5.png))中,我们看到一个例子:一个对话的观察者首先看到问题的答案,然后才看到被回答的问题。这是令人困惑的,因为它违背了我们对**因(cause)**与**果(effect)**的直觉:如果一个问题被回答,显然问题本身得先在那里,因为给出答案的人必须先看到这个问题(假如他们并没有预见未来的超能力)。我们认为在问题和答案之间存在**因果依赖(causal dependency)**。 +* 在“[一致前缀读](ch5.md#一致前缀读)”([图5-5](img/fig5-5.png))中,我们看到一个例子:一个对话的观察者首先看到问题的答案,然后才看到被回答的问题。这是令人困惑的,因为它违背了我们对**因(cause)** 与**果(effect)** 的直觉:如果一个问题被回答,显然问题本身得先在那里,因为给出答案的人必须先看到这个问题(假如他们并没有预见未来的超能力)。我们认为在问题和答案之间存在**因果依赖(causal dependency)**。 * [图5-9](img/fig5-9.png)中出现了类似的模式,我们看到三位领导者之间的复制,并注意到由于网络延迟,一些写入可能会“压倒”其他写入。从其中一个副本的角度来看,好像有一个对尚不存在的记录的更新操作。这里的因果意味着,一条记录必须先被创建,然后才能被更新。 -* 在“[检测并发写入](ch5.md#检测并发写入)”中我们观察到,如果有两个操作A和B,则存在三种可能性:A发生在B之前,或B发生在A之前,或者A和B**并发**。这种**此前发生(happened before)**关系是因果关系的另一种表述:如果A在B前发生,那么意味着B可能已经知道了A,或者建立在A的基础上,或者依赖于A。如果A和B是**并发**的,那么它们之间并没有因果联系;换句话说,我们确信A和B不知道彼此。 +* 在“[检测并发写入](ch5.md#检测并发写入)”中我们观察到,如果有两个操作A和B,则存在三种可能性:A发生在B之前,或B发生在A之前,或者A和B**并发**。这种**此前发生(happened before)** 关系是因果关系的另一种表述:如果A在B前发生,那么意味着B可能已经知道了A,或者建立在A的基础上,或者依赖于A。如果A和B是**并发**的,那么它们之间并没有因果联系;换句话说,我们确信A和B不知道彼此。 * 在事务快照隔离的上下文中(“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”),我们说事务是从一致性快照中读取的。但此语境中“一致”到底又是什么意思?这意味着**与因果关系保持一致(consistent with causality)**:如果快照包含答案,它也必须包含被回答的问题【48】。在某个时间点观察整个数据库,与因果关系保持一致意味着:因果上在该时间点之前发生的所有操作,其影响都是可见的,但因果上在该时间点之后发生的操作,其影响对观察者不可见。**读偏差(read skew)** 意味着读取的数据处于违反因果关系的状态(不可重复读,如[图7-6](img/fig7-6.png)所示)。 * 事务之间**写偏差(write skew)** 的例子(请参阅“[写入偏斜与幻读](ch7.md#写入偏斜与幻读)”)也说明了因果依赖:在[图7-8](img/fig7-8.png)中,爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。[可串行化快照隔离](ch7.md#可串行化快照隔离)通过跟踪事务之间的因果依赖来检测写偏差。 * 在爱丽丝和鲍勃看球的例子中([图9-1](img/fig9-1.png)),在听到爱丽丝惊呼比赛结果后,鲍勃从服务器得到陈旧结果的事实违背了因果关系:爱丽丝的惊呼因果依赖于得分宣告,所以鲍勃应该也能在听到爱丽斯惊呼后查询到比分。相同的模式在“[跨信道的时序依赖](#跨信道的时序依赖)”一节中,以“图像大小调整服务”的伪装再次出现。 @@ -386,7 +386,7 @@ ​ 虽然因果是一个重要的理论概念,但实际上跟踪所有的因果关系是不切实际的。在许多应用中,客户端在写入内容之前会先读取大量数据,我们无法弄清写入因果依赖于先前全部的读取内容,还是仅包括其中一部分。显式跟踪所有已读数据意味着巨大的额外开销。 -​ 但还有一个更好的方法:我们可以使用**序列号(sequence nunber)**或**时间戳(timestamp)**来排序事件。时间戳不一定来自日历时钟(或物理时钟,它们存在许多问题,如 “[不可靠的时钟](ch8.md#不可靠的时钟)” 中所述)。它可以来自一个**逻辑时钟(logical clock)**,这是一个用来生成标识操作的数字序列的算法,典型实现是使用一个每次操作自增的计数器。 +​ 但还有一个更好的方法:我们可以使用**序列号(sequence nunber)** 或**时间戳(timestamp)** 来排序事件。时间戳不一定来自日历时钟(或物理时钟,它们存在许多问题,如 “[不可靠的时钟](ch8.md#不可靠的时钟)” 中所述)。它可以来自一个**逻辑时钟(logical clock)**,这是一个用来生成标识操作的数字序列的算法,典型实现是使用一个每次操作自增的计数器。 ​ 这样的序列号或时间戳是紧凑的(只有几个字节大小),它提供了一个全序关系:也就是说每个操作都有一个唯一的序列号,而且总是可以比较两个序列号,确定哪一个更大(即哪些操作后发生)。 @@ -461,7 +461,7 @@ ​ 如果你的程序只运行在单个CPU核上,那么定义一个操作全序是很容易的:可以简单地就是CPU执行这些操作的顺序。但是在分布式系统中,让所有节点对同一个全局操作顺序达成一致可能相当棘手。在上一节中,我们讨论了按时间戳或序列号进行排序,但发现它还不如单主复制给力(如果你使用时间戳排序来实现唯一性约束,就不能容忍任何错误,因为你必须要从每个节点都获取到最新的序列号)。 -​ 如前所述,单主复制通过选择一个节点作为主库来确定操作的全序,并在主库的单个CPU核上对所有操作进行排序。接下来的挑战是,如果吞吐量超出单个主库的处理能力,这种情况下如何扩展系统;以及,如果主库失效(“[处理节点宕机](ch5.md#处理节点宕机)”),如何处理故障切换。在分布式系统文献中,这个问题被称为**全序广播(total order broadcast)**或**原子广播(atomic broadcast)**[^ix]【25,57,58】。 +​ 如前所述,单主复制通过选择一个节点作为主库来确定操作的全序,并在主库的单个CPU核上对所有操作进行排序。接下来的挑战是,如果吞吐量超出单个主库的处理能力,这种情况下如何扩展系统;以及,如果主库失效(“[处理节点宕机](ch5.md#处理节点宕机)”),如何处理故障切换。在分布式系统文献中,这个问题被称为**全序广播(total order broadcast)** 或**原子广播(atomic broadcast)**[^ix]【25,57,58】。 [^ix]: “原子广播”是一个传统的术语,非常混乱,而且与“原子”一词的其他用法不一致:它与ACID事务中的原子性没有任何关系,只是与原子操作(在多线程编程的意义上 )或原子寄存器(线性一致存储)有间接的联系。全序组播(total order multicast)是另一个同义词。 @@ -602,7 +602,7 @@ #### 两阶段提交简介 -​ **两阶段提交(two-phase commit)**是一种用于实现跨多个节点的原子事务提交的算法,即确保所有节点提交或所有节点中止。 它是分布式数据库中的经典算法【13,35,75】。 2PC在某些数据库内部使用,也以**XA事务**的形式对应用可用【76,77】(例如Java Transaction API支持)或以SOAP Web服务的`WS-AtomicTransaction` 形式提供给应用【78,79】。 +​ **两阶段提交(two-phase commit)** 是一种用于实现跨多个节点的原子事务提交的算法,即确保所有节点提交或所有节点中止。 它是分布式数据库中的经典算法【13,35,75】。 2PC在某些数据库内部使用,也以**XA事务**的形式对应用可用【76,77】(例如Java Transaction API支持)或以SOAP Web服务的`WS-AtomicTransaction` 形式提供给应用【78,79】。 [图9-9](img/fig9-9.png)说明了2PC的基本流程。2PC中的提交/中止过程分为两个阶段(因此而得名),而不是单节点事务中的单个提交请求。 @@ -697,7 +697,7 @@ ​ *X/Open XA*(**扩展架构(eXtended Architecture)** 的缩写)是跨异构技术实现两阶段提交的标准【76,77】。它于1991年推出并得到了广泛的实现:许多传统关系数据库(包括PostgreSQL,MySQL,DB2,SQL Server和Oracle)和消息代理(包括ActiveMQ,HornetQ,MSMQ和IBM MQ) 都支持XA。 -​ XA不是一个网络协议——它只是一个用来与事务协调者连接的C API。其他语言也有这种API的绑定;例如在Java EE应用的世界中,XA事务是使用**Java事务API(JTA, Java Transaction API)**实现的,而许多使用**Java数据库连接(JDBC, Java Database Connectivity)**的数据库驱动,以及许多使用**Java消息服务(JMS)**API的消息代理都支持**Java事务API(JTA)**。 +​ XA不是一个网络协议——它只是一个用来与事务协调者连接的C API。其他语言也有这种API的绑定;例如在Java EE应用的世界中,XA事务是使用**Java事务API(JTA, Java Transaction API)** 实现的,而许多使用**Java数据库连接(JDBC, Java Database Connectivity)** 的数据库驱动,以及许多使用**Java消息服务(JMS)** API的消息代理都支持**Java事务API(JTA)**。 ​ XA假定你的应用使用网络驱动或客户端库来与**参与者**(数据库或消息服务)进行通信。如果驱动支持XA,则意味着它会调用XA API 以查明操作是否为分布式事务的一部分 —— 如果是,则将必要的信息发往数据库服务器。驱动还会向协调者暴露回调接口,协调者可以通过回调来要求参与者准备、提交或中止。 @@ -742,7 +742,7 @@ ​ 非正式地,共识意味着让几个节点就某事达成一致。例如,如果有几个人**同时(concurrently)** 尝试预订飞机上的最后一个座位,或剧院中的同一个座位,或者尝试使用相同的用户名注册一个帐户。共识算法可以用来确定这些**互不相容(mutually incompatible)** 的操作中,哪一个才是赢家。 -​ 共识问题通常形式化如下:一个或多个节点可以**提议(propose)**某些值,而共识算法**决定(decides)**采用其中的某个值。在座位预订的例子中,当几个顾客同时试图订购最后一个座位时,处理顾客请求的每个节点可以**提议**将要服务的顾客的ID,而**决定**指明了哪个顾客获得了座位。 +​ 共识问题通常形式化如下:一个或多个节点可以**提议(propose)** 某些值,而共识算法**决定(decides)** 采用其中的某个值。在座位预订的例子中,当几个顾客同时试图订购最后一个座位时,处理顾客请求的每个节点可以**提议**将要服务的顾客的ID,而**决定**指明了哪个顾客获得了座位。 在这种形式下,共识算法必须满足以下性质【25】:[^xiii] diff --git a/part-ii.md b/part-ii.md index f303dee..0eaae3f 100644 --- a/part-ii.md +++ b/part-ii.md @@ -25,9 +25,9 @@ ## 伸缩至更高的载荷 -如果你需要的只是伸缩至更高的**载荷(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)**仍然是必要的。 +[^i]: 在大型机中,尽管任意处理器都可以访问内存的任意部分,但总有一些内存区域与一些处理器更接近(称为**非均匀内存访问(nonuniform memory access, NUMA)**【1】)。 为了有效利用这种架构特性,需要对处理进行细分,以便每个处理器主要访问临近的内存,这意味着即使表面上看起来只有一台机器在运行,**分区(partitioning)** 仍然是必要的。 共享内存方法的问题在于,成本增长速度快于线性增长:一台有着双倍处理器数量,双倍内存大小,双倍磁盘容量的机器,通常成本会远远超过原来的两倍。而且可能因为存在瓶颈,并不足以处理双倍的载荷。 diff --git a/preface.md b/preface.md index 0694dc7..c3e24c3 100644 --- a/preface.md +++ b/preface.md @@ -11,7 +11,7 @@ * 即使您在一个小团队中工作,现在也可以构建分布在多台计算机甚至多个地理区域的系统,这要归功于譬如亚马逊网络服务(AWS)等基础设施即服务(IaaS)概念的践行者。 * 许多服务都要求高可用,因停电或维护导致的服务不可用,变得越来越难以接受。 -**数据密集型应用(data-intensive applications)**正在通过使用这些技术进步来推动可能性的边界。一个应用被称为**数据密集型**的,如果**数据是其主要挑战**(数据量,数据复杂度或数据变化速度)—— 与之相对的是**计算密集型**,即处理器速度是其瓶颈。 +**数据密集型应用(data-intensive applications)** 正在通过使用这些技术进步来推动可能性的边界。一个应用被称为**数据密集型**的,如果**数据是其主要挑战**(数据量,数据复杂度或数据变化速度)—— 与之相对的是**计算密集型**,即处理器速度是其瓶颈。 帮助数据密集型应用存储和处理数据的工具与技术,正迅速地适应这些变化。新型数据库系统(“NoSQL”)已经备受关注,而消息队列,缓存,搜索索引,批处理和流处理框架以及相关技术也非常重要。很多应用组合使用这些工具与技术。