From af1b8a24ea76f639750fa5a3abbbd184222b4ef4 Mon Sep 17 00:00:00 2001 From: Vonng Date: Mon, 26 Mar 2018 00:18:02 +0800 Subject: [PATCH 01/17] ch9 50% --- README.md | 3 +- ch5.md | 4 +- ch8.md | 6 +- ch9.md | 417 ++++++++++++++++++++++++++++-------------------------- 4 files changed, 220 insertions(+), 210 deletions(-) diff --git a/README.md b/README.md index a4f4391..d318b82 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,8 @@ All contribution will give proper credit. 贡献者需要同意[法律声明](#法律声明)所叙内容。 1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird) -2. [第一章语法标点修正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree) +2. [第一章语法标点校正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree) +3. [第六章第一部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) by @[MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex) diff --git a/ch5.md b/ch5.md index d81e0b2..f4ee4ca 100644 --- a/ch5.md +++ b/ch5.md @@ -596,9 +596,9 @@ PostgreSQL和Oracle等使用这种复制方法【16】。主要缺点是日志 在所有常见的Dynamo实现中,松散法定人数是可选的。在Riak中,它们默认是启用的,而在Cassandra和Voldemort中它们默认是禁用的【46,49,50】。 -#### 多数据中心操作 +#### 运维多个数据中心 -我们先前讨论了跨数据中心复制作为多主复制的用例(请参阅第162页的“[多重复制]()”)。无主复制还适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。 +我们先前讨论了跨数据中心复制作为多主复制的用例(参阅“[多主复制](#多主复制)”)。无主复制还适用于多数据中心操作,因为它旨在容忍冲突的并发写入,网络中断和延迟尖峰。 Cassandra和Voldemort在正常的无主模型中实现了他们的多数据中心支持:副本的数量n包括所有数据中心的节点,在配置中,您可以指定每个数据中心中您想拥有的副本的数量。无论数据中心如何,每个来自客户端的写入都会发送到所有副本,但客户端通常只等待来自其本地数据中心内的法定节点的确认,从而不会受到跨数据中心链路延迟和中断的影响。对其他数据中心的高延迟写入通常被配置为异步发生,尽管配置有一定的灵活性【50,51】。 diff --git a/ch8.md b/ch8.md index 8d73fe7..2b2a4a8 100644 --- a/ch8.md +++ b/ch8.md @@ -484,15 +484,15 @@ while(true){ 这个问题就是我们先前在“[进程暂停](#进程暂停)”中讨论过的一个例子:如果持有租约的客户端暂停太久,它的租约将到期。另一个客户端可以获得同一文件的租约,并开始写入文件。当暂停的客户端回来时,它认为(不正确)它仍然有一个有效的租约,并继续写入文件。结果,客户的写入冲突和损坏的文件。 -#### 击剑令牌 +#### 防护令牌 -当使用锁或租约来保护对某些资源(如[图8-4](img/fig8-4.png)中的文件存储)的访问时,需要确保一个被误认为自己是“天选者”的节点不能中断系统的其它部分。实现这一目标的一个相当简单的技术就是**屏蔽(fencing)**,如[图8-5]()所示 +当使用锁或租约来保护对某些资源(如[图8-4](img/fig8-4.png)中的文件存储)的访问时,需要确保一个被误认为自己是“天选者”的节点不能中断系统的其它部分。实现这一目标的一个相当简单的技术就是**防护(fencing)**,如[图8-5]()所示 ![](img/fig8-5.png) **图8-5 只允许以增加屏蔽令牌的顺序进行写操作,从而保证存储安全** -我们假设每次锁定服务器授予锁或租约时,它还会返回一个**屏蔽令牌(fencing token)**,这个数字在每次授予锁定时都会增加(例如,由锁定服务增加)。然后,我们可以要求客户端每次向存储服务发送写入请求时,都必须包含当前的屏蔽令牌。 +我们假设每次锁定服务器授予锁或租约时,它还会返回一个**防护令牌(fencing token)**,这个数字在每次授予锁定时都会增加(例如,由锁定服务增加)。然后,我们可以要求客户端每次向存储服务发送写入请求时,都必须包含当前的屏蔽令牌。 在[图8-5](img/fig8-5.png)中,客户端1以33的令牌获得租约,但随后进入一个长时间的停顿并且租约到期。客户端2以34的令牌(该数字总是增加)获取租约,然后将其写入请求发送到存储服务,包括34的令牌。稍后,客户端1恢复生机并将其写入存储服务,包括其令牌值33.但是,存储服务器会记住它已经处理了一个具有更高令牌编号(34)的写入,因此它会拒绝带有令牌33的请求。 diff --git a/ch9.md b/ch9.md index 25b5394..5b86458 100644 --- a/ch9.md +++ b/ch9.md @@ -31,7 +31,7 @@ ## 一致性保证 -在“[复制延迟问题](ch5.md#复制延迟问题)”中,我们看到了数据库复制中发生的一些时序问题。如果你在同一时刻查看两个数据库节点,则可能在两个节点上看到不同的数据,因为写请求在不同的时间到达不同的节点。无论数据库使用何种复制方法(单引导程序,多引导程序或无引导程序复制),都会出现这些不一致情况。 +在“[复制延迟问题](ch5.md#复制延迟问题)”中,我们看到了数据库复制中发生的一些时序问题。如果你在同一时刻查看两个数据库节点,则可能在两个节点上看到不同的数据,因为写请求在不同的时间到达不同的节点。无论数据库使用何种复制方法(单主复制,多主复制或无主复制),都会出现这些不一致情况。 大多数复制的数据库至少提供了**最终一致性**,这意味着如果你停止向数据库写入数据并等待一段不确定的时间,那么最终所有的读取请求都会返回相同的值【1】。换句话说,不一致性是暂时的,最终会自行解决(假设网络中的任何故障最终都会被修复)。最终一致性的一个更好的名字可能是**收敛(convergence)**,因为我们预计所有的复本最终会收敛到相同的值【2】。 @@ -150,9 +150,9 @@ > > **线性一致性(Linearizability)**是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写偏差等问题(请参阅“[写偏差和幻读](ch7.md#写偏差和幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。 > -> 一个数据库可以提供可串行性和线性一致性,这种组合被称为严格的可串行性或强的**单副本强可串行性(strong-1SR)**【4,13】。基于两阶段锁定的可串行化实现(请参见“[两阶段锁定(2PL)](#两阶段锁定(2PL))”一节)或**实际串行执行**(参见第“[实际串行执行](ch7.md#实际串行执行)”)通常是线性一致性的。 +> 一个数据库可以提供可串行性和线性一致性,这种组合被称为严格的可串行性或强的**单副本强可串行性(strong-1SR)**【4,13】。基于两阶段锁定的可串行化实现(参见“[两阶段锁定(2PL)](#两阶段锁定(2PL))”一节)或**实际串行执行**(参见第“[实际串行执行](ch7.md#实际串行执行)”)通常是线性一致性的。 > -> 但是,可序列化的快照隔离(请参见“[可序列化的快照隔离(SSI)](ch7.md#可序列化的快照隔离(SSI))”)不是线性一致性的:按照设计,它可以从一致的快照中进行读取,以避免锁定读者和写者之间的争用。一致性快照的要点就在于**它不会包括比快照更新的写入**,因此从快照读取不是线性一致性的。 +> 但是,可序列化的快照隔离(参见“[可序列化的快照隔离(SSI)](ch7.md#可序列化的快照隔离(SSI))”)不是线性一致性的:按照设计,它可以从一致的快照中进行读取,以避免锁定读者和写者之间的争用。一致性快照的要点就在于**它不会包括比快照更新的写入**,因此从快照读取不是线性一致性的。 @@ -182,7 +182,7 @@ 然而,一个硬性的唯一性约束(关系型数据库中常见的那种)需要线性一致性。其他类型的约束,如外键或属性约束,可以在不需要线性一致性的情况下实现【19】。 -#### 跨信道的时间依赖性 +#### 跨信道的时序依赖 注意[图9-1](img/fig9-1.png)中的一个细节:如果Alice没有惊呼得分,Bob就不会知道他的查询结果是陈旧的。他会在几秒钟之后再次刷新页面,并最终看到最后的分数。由于系统中存在额外的信道(Alice的声音传到了Bob的耳朵中),线性一致性的违背才被注意到。 @@ -208,7 +208,7 @@ ***单主复制(可能线性一致)*** -在具有单主复制功能的系统中(请参见“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能(protential)**是线性一致性的[^iv]。然而,并不是每个单主数据库都是实际线性一致性的,无论是通过设计(例如,因为使用快照隔离)还是并发错误【10】。 +在具有单主复制功能的系统中(参见“[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们**可能(protential)**是线性一致性的[^iv]。然而,并不是每个单主数据库都是实际线性一致性的,无论是通过设计(例如,因为使用快照隔离)还是并发错误【10】。 [^iv]: 对单领域数据库进行分区(分片),以便每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。 交叉分区事务是一个不同的问题(参阅“[分布式事务和共识](#分布式事务和共识)”)。 @@ -250,135 +250,139 @@ ### 线性一致性的代价 -由于一些复制方法可以提供线性一致性,其他复制方法则不能,因此更深入地探讨线性一致性的优缺点是很有趣的。 +一些复制方法可以提供线性一致性,另一些复制方法则不能,因此深入地探讨线性一致性的优缺点是很有趣的。 -我们已经在第五章讨论了不同复制方法的一些用例。例如,我们看到多领导者复制通常是多数据中心复制的理想选择(参阅“[多数据中心操作](ch5.md#多数据中心操作)”)。[图9-7](img/fig9-7.png)说明了这种部署的一个例子。 +我们已经在[第五章中讨论了不同复制方法的一些用例。例如对多数据中心的复制而言,多主复制通常是理想的选择(参阅“[运维多个数据中心](ch5.md#运维多个数据中心)”)。[图9-7](img/fig9-7.png)说明了这种部署的一个例子。 ![](img/fig9-7.png) **图9-7 网络中断迫使在线性一致性和可用性之间做出选择。** -考虑如果两个数据中心之间发生网络中断,会发生什么情况。我们假设每个数据中心内的网络正在工作,客户端可以访问数据中心,但数据中心不能互相连接。 +考虑这样一种情况:如果两个数据中心之间发生网络中断会发生什么?我们假设每个数据中心内的网络正在工作,客户端可以访问数据中心,但数据中心之间彼此无法互相连接。 -使用多领导者数据库,每个数据中心都可以继续正常运行:由于从一个数据中心写入的数据是异步复制到另一个数据中心的,所以在恢复网络连接时,只需排队并交换数据。 +使用多主数据库,每个数据中心都可以继续正常运行:由于在一个数据中心写入的数据是异步复制到另一个数据中心的,所以在恢复网络连接时,写入操作只是简单地排队并交换。 -另一方面,如果使用单主复制,则主库必须位于其中一个数据中心。任何写入和任何线性读取都必须发送给leader,因此,对于连接到跟随者数据中心的任何客户端,这些读取和写入请求必须通过网络同步发送到leader数据中心。 +另一方面,如果使用单主复制,则主库必须位于其中一个数据中心。任何写入和任何线性一致的读取请求都必须发送给该主库,因此对于连接到从库所在数据中心的客户端,这些读取和写入请求必须通过网络同步发送到主库所在的数据中心。 -如果数据中心之间的网络在单引导程序设置中被中断,则连接到跟随者数据中心的客户端不能联系领导者,因此他们不能对数据库进行任何写入,也不能进行任何线性一致性读取。他们仍然可以从追随者读取,但他们可能是陈旧(非线性)。如果应用程序需要线性读写,则网络中断将导致应用程序在不能联系领导者的数据中心中变得不可用。 -如果客户端可以直接连接到领导者数据中心,这不是问题,因为应用程序在那里继续正常工作。但是只能访问下一个数据中心的客户端将会经历停机,直到网络链接被修复。 +在单主配置的条件下,如果数据中心之间的网络被中断,则连接到从库数据中心的客户端无法联系到主库,因此它们无法对数据库执行任何写入,也不能执行任何线性一致的读取。它们仍能从从库读取,但结果可能是陈旧的(非线性一致)。如果应用需要线性一致的读写,却又位于与主库网络中断的数据中心,则网络中断将导致这些应用不可用。 + +如果客户端可以直接连接到主库所在的数据中心,这就不是问题了,哪些应用可以继续正常工作。但直到网络链接修复之前,只能访问从库数据中心的客户端会中断运行。 #### CAP定理 -这个问题不仅仅是单领导者和多领导者复制的结果:任何线性一致性的数据库都有这个问题,不管它是如何实现的。这个问题也不是特定于多数据中心部署,而是可能发生在任何不可靠的网络上,即使在一个数据中心内也是如此。权衡如下:[^v] +这个问题不仅仅是单主复制和多主复制的后果:任何线性一致的数据库都有这个问题,不管它是如何实现的。这个问题也不仅仅局限于多数据中心部署,而可能发生在任何不可靠的网络上,即使在同一个数据中心内也是如此。问题面临的权衡如下:[^v] -* 如果您的应用程序需要线性一致性,并且由于网络问题某些副本与其他副本断开连接,则某些副本在断开连接时无法处理请求:它们必须等待,直到网络问题得到解决,或返回错误(无论哪种方式,他们变得不可用)。 -* 如果您的应用程序不需要线性一致性,那么即使它与其他副本(如多引导程序)断开连接,也可以以每个副本可独立处理请求的方式进行写入。在这种情况下,应用程序可以在网络问题面前保持可用,但其行为不线性一致性。 +* 如果应用需要线性一致性,且某些副本因为网络问题与其他副本断开连接,那么这些副本掉线时不能处理请求。请求必须等到网络问题解决,或直接返回错误。(无论哪种方式,服务都**不可用(unavailable)**)。 +* 如果应用不需要线性一致性,那么某个副本即使与其他副本断开连接,也可以独立处理请求(例如多主复制)。在这种情况下,应用可以在网络问题前保持可用,但其行为不是线性一致的。 -[^v]: 这两种选择有时分别称为CP(在网络分区下一致但不可用)和AP(在网络分区下可用但不一致)。 但是,这种分类方案存在一些缺陷【9】,所以最好避免。 +[^v]: 这两种选择有时分别称为CP(在网络分区下一致但不可用)和AP(在网络分区下可用但不一致)。 但是,这种分类方案存在一些缺陷【9】,所以最好不要这样用。 -因此,不需要线性一致性的应用可以更容忍网络问题。这种见解通常被称为CAP定理【29,30,31,32】,由Eric Brewer于2000年命名,尽管自20世纪70年代以来,分布式数据库的设计者已经知道了这种权衡【33,34,35,36】。 +因此不需要线性一致性的应用对网络问题有更强的容错能力。这种见解通常被称为CAP定理【29,30,31,32】,由Eric Brewer于2000年命名,尽管70年代的分布式数据库设计者早就知道了这种权衡【33,34,35,36】。 -CAP最初是作为一个经验法则提出的,没有准确的定义,目的是开始讨论数据库的权衡。当时,许多分布式数据库侧重于在具有共享存储的机器集群上提供线性一致性的语义【18】,并鼓励数据库工程师探索更广泛的分布式无共享系统的设计空间,这更适合于实施大规模的网络服务【37】。 CAP值得赞扬,因为这种文化转变——见证了自2000年代中期以来新的数据库技术的爆炸式增长(被称为NoSQL)。 +CAP最初是作为一个经验法则提出的,没有准确的定义,目的是开始讨论数据库的权衡。那时候许多分布式数据库侧重于在共享存储的集群上提供线性一致性的语义【18】,CAP定理鼓励数据库工程师向分布式无共享系统的设计领域深入探索,这类架构更适合实现大规模的网络服务【37】。 对于这种文化上的转变,CAP值得赞扬——它见证了自00年代中期以来新数据库的技术爆炸(即NoSQL)。 > ### CAP定理没有帮助 > -> CAP有时以这种面目出现:一致性,可用性和分区容忍:三者只能择其二。 +> CAP有时以这种面目出现:一致性,可用性和分区容忍:三者只能择其二。不幸的是这种说法很有误导性【32】,因为网络分区是一种错误,所以它并不是一个选项:不管你喜不喜欢它都会发生【38】。 > -> 只能选择从3中挑选出2个。不幸的是,这样做是误导的【32】,因为网络分区是一种错误,所以它们不是你所拥有的一个选择:他们会发生,不管你喜欢还是不喜欢【38】。 -> -> 在网络正常工作的时候,系统可以提供一致性(线性一致性)和总体可用性。发生网络故障时,您必须选择线性或总可用性。因此,一个更好的表达CAP的方法可以是一致的,或者在分区时可用【39】。一个更可靠的网络需要减少这个选择,但是在某些时候选择是不可避免的。 +> 在网络正常工作的时候,系统可以提供一致性(线性一致性)和整体可用性。发生网络故障时,你必须在线性一致性和整体可用性之间做出选择。因此,一个更好的表达CAP的方法可以是一致的,或者在分区时可用【39】。一个更可靠的网络需要减少这个选择,但是在某些时候选择是不可避免的。 > > 在CAP的讨论中,术语可用性有几个相互矛盾的定义,形式化作为一个定理【30】并不符合其通常的含义【40】。许多所谓的“高可用”(容错)系统实际上不符合CAP对可用性的特殊定义。总而言之,围绕着CAP有很多误解和困惑,并不能帮助我们更好地理解系统,所以最好避免使用CAP。 -正式定义的CAP定理【30】的范围很窄,它只考虑一个一致性模型(即线性一致性)和一种故障(网络分区[^vi]或活动但彼此断开的节点)。它没有说任何有关网络延迟,死亡节点或其他权衡的事情。 因此,尽管CAP在历史上具有影响力,但对于设计系统来说,它没有实际价值【9,40】。 +CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一个一致性模型(即线性一致性)和一种故障(网络分区[^vi],或活跃但彼此断开的节点)。它没有讨论任何关于网络延迟,死亡节点或其他权衡的事。 因此,尽管CAP在历史上有一些影响力,但对于设计系统而言并没有实际价值【9,40】。 -在分布式系统中有更多有趣的不可能的结果【41】,并且CAP已经被更精确的结果所取代【2,42】,所以它现在基本上是历史感兴趣的。 +在分布式系统中有更多有趣的“不可能”的结果【41】,且CAP定理现在已经被更精确的结果取代【2,42】,所以它现在基本上成了历史古迹了。 -[^vi]: 正如第279页的“实践中的网络故障”中所讨论的,本书使用分区来指将大数据集精细分解成小数据集(分片;参见第6章)。相比之下,网络分区是特定类型的网络故障,我们通常不会将其与其他类型的故障分开考虑。但是,由于是CAP的P,所以在这种情况下我们不能避免混淆。 +[^vi]: 正如“[真实世界的网络故障](ch8.md#真实世界的网络故障)”中所讨论的,本书使用**分区(partition)**指代将大数据集细分为小数据集的操作(分片;参见[第6章](ch6.md))。与之对应的是,**网络分区(network partition)**是一种特定类型的网络故障,我们通常不会将其与其他类型的故障分开考虑。但是,由于它是CAP的P,所以这种情况下不能将其混为一谈。 #### 线性一致性和网络延迟 -虽然线性一致性是一个有用的保证,但实际上很少有系统实际上是线性一致性的。例如,现代多核CPU上的RAM甚至不线性一致性【43】:如果一个CPU内核上运行的线程写入内存地址,而另一个CPU内核上的线程不久后读取相同的地址,保证读取第一个线程写入的值(除非使用了内存屏障或栅栏【44】)。 +虽然线性一致是一个很有用的保证,但实际上,线性一致的系统惊人的少。例如,现代多核CPU上的内存甚至都不是线性一致的【43】:如果一个CPU核上运行的线程写入某个内存地址,而另一个CPU核上运行的线程不久之后读取相同的地址,并没有保证一定能一定读到第一个线程写入的值(除非使用了**内存屏障(memory barrier)**或**围栏(fence)**【44】)。 -这种行为的原因是每个CPU内核都有自己的内存缓存和存储缓冲区。内存访问首先进入缓存默认情况下,任何更改异步写出到主内存。由于在缓存中访问数据比进入主内存要快【45】,所以这个特性对于现代CPU的良好性能是至关重要的。但是,现在有几个数据副本(一个在主内存中,另外几个在不同的高速缓存中),而且这些副本是异步更新的,因此线性一致性会丢失。 +这种行为的原因是每个CPU核都有自己的内存缓存和存储缓冲区。默认情况下,内存访问首先走缓存,任何变更会异步写入主存。因为缓存访问比主存要快得多【45】,所以这个特性对于现代CPU的良好性能表现至关重要。但是现在就有几个数据副本(一个在主存中,也许还有几个在不同缓存中的其他副本),而且这些副本是异步更新的,所以就失去了线性一致性。 -为什么要做这个交换?使用CAP定理来证明多核内存一致性模型是没有意义的:在一台计算机中,我们通常假定可靠的通信,并且我们不希望一个CPU内核能够继续正常的操作与电脑的其他部分断开连接。降低线性一致性的原因是性能,而不是容错。 +为什么要做这个权衡?对多核内存一致性模型而言,CAP定理是没有意义的:在同一台计算机中,我们通常假定通信都是可靠的。并且我们并不指望一个CPU核能在脱离计算机其他部分的条件下继续正常工作。牺牲线性一致性的原因是**性能(performance)**,而不是容错。 -许多分布式数据库也是如此,它们选择不提供线性保证:它们主要是为了提高性能,而不是为了容错【46】。线性一致性速度很慢 - 这一直是事实,不仅在网络故障期间。 +许多分布式数据库也是如此:它们是**为了提高性能**而选择了牺牲线性一致性,而不是为了容错【46】。线性一致的速度很慢——这始终是事实,而不仅仅是网络故障期间。 -我们不能找到一个更有效的线性一致性存储实现吗?看来答案是否定的:Attiya和Welch 【47】证明,如果你想要线性一致性,读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中(请参见“[超时和无限延迟](ch8.md#超时和无限延迟)”(第267页)),线性读写的响应时间不可避免地会很高。线性一致性算法不存在,但是一致性较弱的模型可以更快,所以这种权衡对于延迟敏感的系统是很重要的。在第12章中,我们将讨论一些避免线性一致性而不牺牲正确性的方法。 +能找到一个更高效的线性一致存储实现吗?看起来答案是否定的:Attiya和Welch 【47】证明,如果你想要线性一致性,读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中(参见“[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”),线性读写的响应时间不可避免地会很高。更快地线性一致算法不存在,但更弱的一致性模型可以快得多,所以对延迟敏感的系统而言,这类权衡非常重要。在[第12章](ch12.md)中将讨论一些在不牺牲正确性的前提下,绕开线性一致性的方法。 -## 顺序保证 +## 顺序保证 -我们之前曾经说过,线性一致性寄存器的行为就好像只有一个数据拷贝一样,而且每一个操作在某个时间点似乎都是原子地生效的。这个定义意味着操作按照一定的顺序执行。我们通过按照它们似乎已经执行的顺序加入操作来说明[图9-4]()中的顺序。 +之前说过,线性一致寄存器的行为就好像只有单个数据副本一样,且每个操作在某个时间点似乎都是原子性生效的。这个定义意味着,操作是按照某种良好定义的顺序执行的。我们通过操作(似乎)执行完毕的顺序来连接操作,以此说明[图9-4](img/fig9-4.png)中的顺序。 -订购在本书中一直是反复出现的主题,这表明这可能是一个重要的基本概念。让我们简要回顾一下我们在其中讨论过的其他一些情况: +**顺序(ordering)**这一主题在本书中反复出现,这表明它可能是一个重要的基础性概念。让我们简要回顾一下其他**顺序**曾经出现过的上下文: -* 在[第5章](ch5.md)中,我们看到领导者在单引导者复制中的主要目的是确定复制日志中的写入顺序——也就是追随者应用这些写入的顺序。如果不存在单个领导,则可能由于并发操作而发生冲突(参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。 -* 我们在[第7章](ch7.md)中讨论的可序列化是关于确保事务按照某种顺序执行的行为。它可以通过以该序列字面执行事务来实现,或者通过允许并行执行,同时防止序列化冲突(通过锁定或中止)来实现。 -* 我们在[第8章](ch8.md)讨论过的在分布式系统中使用时间戳和时钟(参阅“[依赖于同步时钟](ch8.md#依赖于同步时钟)”)是另一种将顺序引入无序世界的尝试,例如确定两个写入中的哪一个稍后发生。 +* 在[第5章](ch5.md)中我们看到,领导者在单主复制中的主要目的就是,在复制日志中确定**写入顺序(order of write)**——也就是从库应用这些写入的顺序。如果不存在一个领导者,则并发操作可能导致冲突(参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。 +* 在[第7章](ch7.md)中讨论的**可序列化**,是关于事务表现的像按**某种序列顺序(some sequential order)**执行的保证。它可以通过字面意义上地**序列顺序(serial order)**执行事务来实现,或者通过允许并行执行,同时防止序列化冲突来实现(通过锁或中止事务)。 +* 在[第8章](ch8.md)讨论过的在分布式系统中使用时间戳和时钟(参阅“[依赖于同步时钟](ch8.md#依赖于同步时钟)”)是另一种将顺序引入无序世界的尝试,例如,确定两个写入操作哪一个更晚发生。 -事实证明,排序,线性一致性和共识之间有着深刻的联系。尽管这个概念比本书的其他部分更具理论性和抽象性,但对于澄清我们对什么是系统可以做什么和不可以做什么而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。 +事实证明,顺序,线性一致性和共识之间有着深刻的联系。尽管这个概念比本书其他部分更加理论化和抽象,但对于明确系统的能力范围(可以做什么和不可以做什么)而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。 ### 顺序与因果 -顺序不断涌现有几个原因,其中一个原因是它有助于保持因果关系。在这本书的过程中,我们已经看到了几个例子,其中因果关系是重要的: +**顺序**反复出现有几个原因,其中一个原因是,它有助于保持**因果关系(causality)**。在本书中我们已经看到了几个例子,其中因果关系是很重要的: -* 在“[一致前缀读取]()”([图5-5](img/fig5-5.png))中,我们看到一个例子,一个对话的观察者首先看到问题的答案,然后回答问题。这是令人困惑的,因为它违背了我们对因果的直觉:如果一个问题得到了回答,那么显然这个问题必须首先在那里,因为给出答案的人必须看到这个问题(假设他们不是精神的,未来)。我们说在问题和答案之间存在因果关系。 -* [图5-9](img/fig5-9.png)中出现了类似的模式,我们在这里看到三位领导者之间的复制,并注意到由于网络延迟,一些文字可能会“超过”其他文字。从其中一个副本的角度看,好像有一个不存在的行的更新。这里的因果意味着一行必须先被创建,然后才能被更新。 -* 在“[检测并发写入](ch5.md#检测并发写入)”中,我们观察到如果您有两个操作A和B,则有三种可能性:A发生在B之前,或B发生在A之前,或者A和B并发。这是在关系是因果关系的另一种表达之前发生的:如果A发生在B之前,那么意味着B可能已经知道了A,或者建立在A上,或者依赖于A.如果A和B是并发的,那么他们;换句话说,我们确信没有人知道另一个。 -* 在事务快照隔离的上下文中(“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”),我们说事务从一致的快照中读取。但在这方面“一致”是什么意思?这意味着与因果关系一致:如果快照包含答案,它也必须包含被回答的问题【48】。在一个时间点观察整个数据库使其与因果性保持一致:在那个时间点之前发生的所有操作的效果都是可见的,但是没有发生严重的后续操作。读取偏斜(非重复读取,如[图7-6](img/fig7-6.png)所示)意味着读取的数据处于违反因果关系的状态。 -* 我们在事务之间写偏差的示例(参见“[写偏差和幻象](ch7.md#写偏差和幻象)”)也说明了因果关系:在[图7-8](img/fig7-8.png)中,Alice被允许关闭电话,因为交易认为Bob仍在通话,反之亦然。在这种情况下,去电的动作因果关系取决于观察当前谁在呼叫。[可序列化的快照隔离](ch7.md#可序列化的快照隔离(SSI))通过跟踪事务之间的因果依赖关系来检测写入歪斜。 -* 在爱丽丝和鲍勃看足球的例子中([图9-1](img/fig9-1.png)),在听到爱丽丝惊呼结果之后,鲍勃从服务器得到一个陈旧结果的事实是一个因果关系的违反:爱丽丝的感叹是因果关系依赖于比分,所以鲍勃应该也可以在听完艾丽斯后看到比分。在图像大小调整服务的幌子下,第331页的“[跨渠道时序依赖关系](#跨渠道时序依赖关系)”中再次出现了相同的模式。 +* 在“[一致前缀读](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不知道彼此。 +* 在事务快照隔离的上下文中(“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”),我们说事务是从一致性快照中读取的。但此语境中“一致”到底又是什么意思?这意味着**与因果关系保持一致(consistent with causality)**:如果快照包含答案,它也必须包含被回答的问题【48】。在某个时间点观察整个数据库,与因果关系保持一致意味着:因果上在该时间点之前发生的所有操作,其影响都是可见的,但因果上在该时间点之后发生的操作,其影响对观察者不可见。**读偏差(read skew)**意味着读取的数据处于违反因果关系的状态(不可重复读,如[图7-6](img/fig7-6)所示)。 +* 事务之间**写偏差(write skew)**的例子(参见“[写偏差和幻象](ch7.md#写偏差和幻象)”)也说明了因果依赖:在[图7-8](img/fig7-8.png)中,爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。[可序列化的快照隔离](ch7.md#可序列化的快照隔离(SSI))通过跟踪事务之间的因果依赖来检测写偏差。 +* 在爱丽丝和鲍勃看球的例子中([图9-1](img/fig9-1.png)),在听到爱丽丝惊呼比赛结果后,鲍勃从服务器得到陈旧结果的事实违背了因果关系:爱丽丝的惊呼因果依赖于得分宣告,所以鲍勃应该也能在听到爱丽斯惊呼后查询到比分。相同的模式在“[跨信道的时序依赖](#跨信道的时序依赖)”一节中,以“图像大小调整服务”的伪装再次出现。 -因果关系对事件施加了一种排序:在收到该消息之前发送消息;问题出现在答案之前。而且,就像现实生活中一样,有一件事情会导致另一件事情:一个节点读取一些数据,然后写出一些结果,另一个节点读取写入的内容并依次写入其他内容,等等。这些依赖因果关系的操作链定义了系统中的因果顺序,即在什么之前发生了什么。 +因果关系对事件施加了一种顺序:因在果之前;消息发送在消息收取之前。而且就像现实生活中一样,一件事会导致另一件事:某个节点读取了一些数据然后写入一些结果,另一个节点读取其写入的内容,并依次写入一些其他内容,等等。这些因果依赖的操作链定义了系统中的因果顺序,即,什么在什么之前发生。 -如果一个系统服从因果关系所规定的顺序,我们说它是因果关系一致的。例如,快照隔离提供了因果一致性:当您从数据库中读取数据,并且看到一些数据时,您还必须能够看到任何因果关系的数据(假设在此期间还没有被删除)。 +如果一个系统服从因果关系所规定的顺序,我们说它是**因果一致(causally)**的。例如,快照隔离提供了因果一致性:当你从数据库中读取到一些数据时,你一定还能够看到其因果前驱(假设在此期间这些数据还没有被删除)。 -#### 因果顺序不是全序关系 +#### 因果顺序不是全序的 -**全序关系(total order)**允许任何两个元素进行比较,所以如果你有两个元素,你总是可以说哪个更大,哪个更小。例如,自然数是完全有序的:如果我给你两个数字,比如说5和13,那么你可以告诉我,13大于5。 +**全序(total order)**允许任意两个元素进行比较,所以如果有两个元素,你总是可以说出哪个更大,哪个更小。例如,自然数集是全序的:给定两个自然数,比如说5和13,那么你可以告诉我,13大于5。 -但是,数学集并不完全排序:是`{a, b}`大于`{b, c}`?那么,你不能真正地比较它们,因为它们都不是其中的一个子集。我们说它们是无法比拟的,因此数学集是部分排序的:在某些情况下,一个集大于另一个(如果一个集包含另一个集的所有元素),但在其他情况下它们是无法比拟的。 +然而数学集合并不完全是全序的:`{a, b}` 比 `{b, c}` 更大吗?好吧,你没法真正比较它们,因为二者都不是对方的子集。我们说它们是**无法比较(incomparable)**的,因此数学集合是**偏序(partially order)**的:在某些情况下,可以说一个集合大于另一个(如果一个集合包含另一个集合的所有元素),但在其他情况下它们是无法比较的[^译注i]。 -全局顺序和局部顺序之间的差异反映在不同的数据库一致性模型中: +[^译注i]: 设R为非空集合A上的关系,如果R是自反的、反对称的和可传递的,则称R为A上的偏序关系。简称偏序,通常记作≦。一个集合A与A上的偏序关系R一起叫作偏序集,记作$$或$$。全序、偏序、关系、集合,这些概念的精确定义可以参考任意一本离散数学教材。 + +全序和偏序之间的差异反映在不同的数据库一致性模型中: ***线性一致性*** -在一个线性一致性的系统中,我们有一个总的操作顺序:如果系统的行为就好像只有一个数据副本,并且每个操作都是原子的,这意味着对于任何两个操作,我们总是可以说哪个操作先发生。这个总的排序在[图9-4](img/fig9-4.png)中以时间线表示。 +在线性一致的系统中,操作是全序的:如果系统表现的就好像只有一个数据副本,并且所有操作都是原子性的,这意味着对任何两个操作,我们总是能判定哪个操作先发生。这个全序[图9-4](img/fig9-4.png)中以时间线表示。 -***因果关系*** +***因果性*** -我们说过,如果两个操作都没有发生在另一个之前,那么这两个操作是并发的(请参阅第186页上的“[发生之前的关系和并发]()”)。换句话说,如果两个事件是因果关系的(一个发生在另一个事件之前),则它们被排序,但是如果它们是并发的,则它们是无法比拟的。这意味着因果关系定义了一个部分的秩序,而不是一个整体的秩序:一些行动是相互排序的,但有些是无法比拟的。 +我们说过,如果两个操作都没有在彼此**之前发生**,那么这两个操作是并发的(参阅[“此前发生”的关系和并发](ch5.md#“此前发生”的关系和并发))。换句话说,如果两个事件是因果相关的(一个发生在另一个事件之前),则它们之间是有序的,但如果它们是并发的,则它们之间的顺序是无法比较的。这意味着因果关系定义了一个偏序,而不是一个全序:一些操作相互之间是有顺序的,但有些则是无法比较的。 -因此,根据这个定义,在可数据化数据存储中不存在并发操作:必须有一个时间线,所有的操作都是按顺序排列的。可能有几个请求等待处理,但是数据存储确保了每个请求都是在单个时间点自动处理的,并且在单个时间轴上作用于单个数据副本,而没有任何并发性。 +因此,根据这个定义,在线性一致的数据存储中是不存在并发操作的:必须有且仅有一条时间线,所有的操作都在这条时间线上,构成一个全序关系。可能有几个请求在等待处理,但是数据存储确保了每个请求都是在唯一时间线上的某个时间点自动处理的,不存在任何并发。 -并发性意味着时间线会再次分支和合并 - 在这种情况下,不同分支上的操作是无法比拟的(即并发)。在第五章中我们看到了这种现象:例如,图5-14不是一条直线的总体顺序,而是一堆不同的操作同时进行。图中的箭头表示因果关系 - 操作的部分顺序。 +并发意味着时间线会分岔然后合并——在这种情况下,不同分支上的操作是无法比较的(即并发操作)。在[第五章](ch5.md)中我们看到了这种现象:例如,[图5-14](img/fig5-14.md)并不是一条直线的全序关系,而是一堆不同的操作并发进行。图中的箭头指明了因果依赖——操作的偏序。 -如果您熟悉像Git这样的分布式版本控制系统,那么它们的版本历史非常类似于因果关系图。通常,一个提交会以一条直线进行,但是有时您会得到分支(当多个人同时在一个项目上工作时),并且在这些创建的提交合并时创建合并。 +如果你熟悉像Git这样的分布式版本控制系统,那么其版本历史与因果关系图极其相似。通常,一个**提交(Commit)**发生在另一个提交之后,在一条直线上。但是有时你会遇到分支(当多个人同时在一个项目上工作时),**合并(Merge)**会在这些并发创建的提交相融合时创建。 #### 线性一致性强于因果一致性 -那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性意味着因果关系:任何线性一致性的系统都能正确保存注意力【7】。特别是,如果系统中有多个通信通道(如图9-5中的消息队列和文件存储服务),线性可确保因果关系被自动保留,而系统不必做任何特殊的事情(如通过不同部件之间的时间戳)。 +那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性**隐含着(implies)**因果关系:任何线性一致的系统都能正确保持因果性【7】。特别是,如果系统中有多个通信通道(如[图9-5](img/fig9-5.png)中的消息队列和文件存储服务),线性一致性可以自动保证因果性,系统无需任何特殊操作(如在不同组件间传递时间戳)。 -线性一致性确保因果关系的事实使线性一致性系统变得简单易懂,更具吸引力。然而,正如第335页的“线性可用性的成本”中所讨论的,使系统线性一致性可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果地理位置分散的话)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,这使得它们可以获得更好的性能,但却使它们难以工作。 +线性一致性确保因果性的事实使线性一致系统变得简单易懂,更有吸引力。然而,正如“[线性一致性的代价](#线性一致性的代价)”中所讨论的,使系统线性一致可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果系统在地理上散布)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,从而获得更好的性能,但它们用起来却更困难。 -好消息是中间地带是可能的。线性一致性不是保持因果关系的唯一途径 - 还有其他方法。一个系统可以在原因上是一致的,不会造成使其线性一致性的性能命中(特别是CAP定理不适用)。事实上,因果一致性是最强可能的一致性模型,不会由于网络延迟而减慢,并且在网络故障时仍然可用【2,42】。 +好消息是,折衷是可能的。线性一致性并不是保持因果性的唯一途径——还有其他方法。一个系统可以是因果一致的,而无需承担线性一致带来的性能折损(尤其是,CAP定理不适用的情况)。实际上在所有的不会被网络延迟拖慢的一致性模型中,因果一致性是可行的最强的一致性模型。而且在网络故障时仍能保持可用【2,42】。 -在许多情况下,似乎需要线性一致性的系统实际上只需要确定因果一致性,这可以更有效地实施。基于这种观察,研究人员正在探索新的数据库来保存因果关系,其性能和可用性特征与最终一致的系统类似【49,50,51】。 -由于这项研究是相当新的,其中没有很多已经进入生产系统,仍然有挑战需要克服【52,53】。但是,这是未来系统的一个有利的方向。 +在许多情况下,似乎需要线性一致性的系统实际上只需要因果一致性,因果一致性可以更高效地实现。基于这种观察,研究人员正在探索新型的数据库,既能保证因果一致性,且性能与可用性与最终一致系统类似【49,50,51】。 + +这方面的研究相当新鲜,其中很多尚未应用到生产系统,仍然有不少挑战需要克服【52,53】。但对于未来的系统而言,这是一个有前景的方向。 #### 捕获因果关系 -我们不会详细讨论非线性系统如何在这里维持因果一致性,而只是简要地探讨一些关键的思想。为了保持因果关系,您需要知道哪个操作发生在哪个其他操作之前。这是一个部分命令:并发操作可以按任意顺序进行,但是如果一个操作发生在另一个操作之前,那么它们必须按照每个副本的顺序处理。因此,当一个副本处理一个操作时,它必须确保所有因果先前的操作(之前发生的所有操作)已经被处理;如果前面的某个操作丢失了,后面的操作必须等待,直到前面的操作被处理完毕。 +我们不会在这里讨论非线性系统如何保证因果一致性的细节,而只是简要地探讨一些关键的思想。 + +为了保持因果性,您需要知道哪个操作发生在哪个其他操作之前(**happened before**)。这是一个偏序:并发操作可以以任意顺序进行,但如果一个操作发生在另一个操作之前,那它们必须在所有副本上以那个顺序被处理。因此,当一个副本处理一个操作时,它必须确保所有因果前驱的操作(之前发生的所有操作)已经被处理;如果前面的某个操作丢失了,后面的操作必须等待,直到前面的操作被处理完毕。 为了确定因果依赖关系,我们需要一些方法来描述系统中节点的“知识”。如果节点在发出写入Y时已经看到X值,则X和Y可能是因果关系的。这个分析使用了你在欺诈指控的刑事调查中所期望的那些问题:CEO在做出决定时是否知道X? -在其他操作之前确定哪些操作发生的技术与我们在第181页中的“检测并发写入”中所讨论的内容类似。该节讨论无领导者数据存储区中的因果关系,我们需要检测到同一个关键字为了防止丢失更新。因果关系更进一步:它需要跟踪整个数据库的因果关系,而不仅仅是一个关键。版本向量可以被推广到做这个【54】。 +在其他操作之前确定哪些操作发生的技术与我们在“[检测并发写入](ch5.md#检测并发写入)”中所讨论的内容类似。该节讨论无领导者数据存储区中的因果关系,我们需要检测到同一个关键字为了防止丢失更新。因果关系更进一步:它需要跟踪整个数据库的因果关系,而不仅仅是一个关键。版本向量可以被推广到做这个【54】。 为了确定因果顺序,数据库需要知道应用程序读取哪个版本的数据。这就是为什么在图5-13中,来自先前操作的版本号在写入时被传回到数据库的原因。在SSI的冲突检测中会出现类似的想法,如“[可序列化的快照隔离(SSI)]()”中所述:当事务要提交时,数据库将检查它读取的数据版本是否仍然运行至今。为此,数据库跟踪哪个数据已经被哪个事务读取。 @@ -396,14 +400,14 @@ CAP最初是作为一个经验法则提出的,没有准确的定义,目的 [^vii]: 与因果关系不一致的整个顺序很容易创建,但不是很有用。例如,您可以为每个操作生成随机UUID,并按照字典顺序比较UUID以定义操作的总顺序。这是一个有效的总顺序,但是随机的UUID并不告诉你哪个操作首先实际发生,或者操作是否是并发的。 -在具有单引导程序复制的数据库中(请参见“[领导者与追随者](ch5.md#领导者与追随者)”),复制日志定义了与因果关系一致的写操作总顺序。领导者可以简单地为每个操作增加一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个追随者按照他们在复制日志中出现的顺序来应用写入,那么追随者的状态始终是因果一致的(即使它落后于领导者)。 +在单主复制的数据库中(请参见“[领导者与追随者](ch5.md#领导者与追随者)”),复制日志定义了与因果关系一致的写操作总顺序。领导者可以简单地为每个操作增加一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个追随者按照他们在复制日志中出现的顺序来应用写入,那么追随者的状态始终是因果一致的(即使它落后于领导者)。 #### 非因果序列号发生器 如果没有一个领导者(可能是因为您使用的是多领导者或无领导者的数据库,或者是因为数据库是分区的),那么如何为操作生成序列号还不太清楚。实践中使用了各种方法: * 每个节点都可以生成自己独立的一组序列号。例如,如果有两个节点,一个节点只能生成奇数,而另一个节点只能生成偶数。通常,可以在序列号的二进制表示中保留一些位以包含唯一的节点标识符,这将确保两个不同的节点永远不会生成相同的序列号。 -* 您可以将时间戳从时钟(物理时钟)附加到每个操作【55】。这样的时间戳是不连续的,但是如果它们具有足够高的分辨率,那么它们可能足以完成命令操作。这个事实用于最后的写赢取冲突解决方法(请参阅第291页的“订购事件的时间戳”)。 +* 您可以将时间戳从时钟(物理时钟)附加到每个操作【55】。这样的时间戳是不连续的,但是如果它们具有足够高的分辨率,那么它们可能足以完成命令操作。这个事实用于最后写入为准的冲突解决方法(参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。 * 您可以预先分配序列号的块。例如,节点A可能要求从1到1,000的序列号的块,并且节点B可能要求该区块从1,001到2,000。然后,每个节点可以独立分配其块的序列号,并在序列号的提供开始变低时分配一个新的块。 这三个选项都比单独的领导者增加一个计数器的表现更好,并且更具可扩展性。它们为每个操作生成一个唯一的,大约增加的序列号。然而,他们都有一个问题:他们产生的序列号与因果关系不一致。 @@ -412,41 +416,42 @@ CAP最初是作为一个经验法则提出的,没有准确的定义,目的 * 每个节点可以每秒处理不同数量的操作。因此,如果一个节点产生偶数而另一个产生奇数,则偶数的计数器可能落后于奇数的计数器,反之亦然。如果你有一个奇数的操作和一个偶数的操作,你不能准确地说出哪一个因果关系发生了。 -* 来自物理时钟的时间戳会受到时钟偏移的影响,这可能会使其与因果性不一致。例如,见图8-3,其中显示了一个情况,其中后来发生因果关系的操作实际上被分配了较低的时间戳。[^vii] +* 来自物理时钟的时间戳会受到时钟偏移的影响,这可能会使其与因果性不一致。例如,见[图8-3](img/fig8-3.png),其中显示了一个情况,其中后来发生因果关系的操作实际上被分配了较低的时间戳。[^vii] - [^viii]: 可以使物理时钟时间戳与因果关系一致:在第294页的“用于全局快照的同步时钟”中,我们讨论了Google的Spanner,它可以估计预期的时钟偏差,并在提交写入之前等待不确定性间隔。 这个方法确保了一个事实上的后续交易得到了更大的时间戳。 但是,大多数时钟不能提供所需的不确定性度量。 + [^viii]: 可以使物理时钟时间戳与因果关系一致:在第294页的“用于全局快照的同步时钟”中,我们讨论了Google的Spanner,它可以估计预期的时钟偏差,并在提交写入之前等待不确定性间隔。 这个方法确保了一个事实上的后续事务得到了更大的时间戳。 但是,大多数时钟不能提供所需的不确定性度量。 * 在块分配器的情况下,一个操作可能会被赋予一个范围从1,001到2,000的序列号,而一个因果较晚的操作可能被赋予一个范围从1到1,000的数字。在这里,序列号与因果关系也是不一致的。 -#### Lamport时间戳 +#### 兰伯特时间戳 -尽管刚才描述的三个序列号发生器与因果关系不一致,但实际上有一个简单的方法来产生与因果关系一致的序列号。它被称为Lamport时间戳,莱斯利·兰波特(Leslie Lamport)于1978年提出【56】,现在是分布式系统领域中被引用最多的论文之一。 +尽管刚才描述的三个序列号发生器与因果关系不一致,但实际上有一个简单的方法来产生与因果关系一致的序列号。它被称为兰伯特时间戳,莱斯利·兰伯特(Leslie Lamport)于1978年提出【56】,现在是分布式系统领域中被引用最多的论文之一。 -[图9-8](img/fig9-8.png)说明了Lamport时间戳的使用。每个节点都有一个唯一的标识符,每个节点都保存一个处理操作数量的计数器。 Lamport时间戳然后是一对(计数器,节点ID)。二节点有时可能具有相同的计数器值,但通过在时间戳中包含节点ID,每个时间戳都是唯一的。 +[图9-8](img/fig9-8.png)说明了兰伯特时间戳的使用。每个节点都有一个唯一的标识符,每个节点都保存一个处理操作数量的计数器。 兰伯特时间戳然后是一对(计数器,节点ID)。二节点有时可能具有相同的计数器值,但通过在时间戳中包含节点ID,每个时间戳都是唯一的。 ![](img/fig9-8.png) **图9-8 Lamport时间戳提供了与因果关系一致的总排序。** -Lamport时间戳与物理时间时钟没有任何关系,但是它提供了总计次数:如果您有两个时间戳,则计数器值较大的时间戳是较大的时间戳。如果计数器值相同,则节点ID越大的时间戳越大。 +兰伯特时间戳与物理时间时钟没有任何关系,但是它提供了总计次数:如果您有两个时间戳,则计数器值较大的时间戳是较大的时间戳。如果计数器值相同,则节点ID越大的时间戳越大。 -到目前为止,这个描述与上一节描述的偶数/奇数计数器基本相同。关于Lamport时间戳的关键思想,使它们与因果关系一致,如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大计数器值,并在每个请求中包含最大计数器值。当一个节点接收到一个最大计数器值大于其自身计数器值的请求或响应时,它立即增加自己的计数器到最大值。 +到目前为止,这个描述与上一节描述的偶数/奇数计数器基本相同。关于兰伯特时间戳的关键思想,使它们与因果关系一致,如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大计数器值,并在每个请求中包含最大计数器值。当一个节点接收到一个最大计数器值大于其自身计数器值的请求或响应时,它立即增加自己的计数器到最大值。 这如图9-8所示,其中客户端A从节点2接收计数器值5,然后将最大值5发送到节点1.此时,节点1的计数器仅为1,但是它立即向前移动到5,所以下一个操作的计数器值增加了6。 -只要最大计数器值与每一个操作一起进行,这个方案确保Lamport时间戳的排序与因果性一致,因为每个因果关系导致时间戳增加。 +只要最大计数器值与每一个操作一起进行,这个方案确保兰伯特时间戳的排序与因果性一致,因为每个因果关系导致时间戳增加。 -Lamport时间戳有时会与版本向量混淆,我们在第184页上的“检测并发写入”中看到了这些向量戳。虽然存在一些相似之处,但它们具有不同的目的:版本向量可以区分两个操作是并发还是因果依赖另一个,而Lamport时间戳总是执行一个总的顺序。从Lamport的全部订购时间戳,你不能分辨两个操作是并行还是因果关系。 Lamport时间戳优于版本向量的优点是它们更紧凑。 +兰伯特时间戳有时会与版本向量混淆,我们在第184页上的“检测并发写入”中看到了这些向量戳。虽然存在一些相似之处,但它们具有不同的目的:版本向量可以区分两个操作是并发还是因果依赖另一个,而兰伯特时间戳总是执行一个总的顺序。从兰伯特的全部顺序时间戳,你不能分辨两个操作是并行还是因果关系。 兰伯特时间戳优于版本向量的优点是它们更紧凑。 #### 光有时间戳排序还不够 -虽然Lamport时间戳定义了与因果关系一致的操作总顺序,但它们还不足以解决分布式系统中的许多常见问题。 -例如,考虑一个需要确保用户名唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败。 (我们之前在第301页的“[领导和锁定](#领导和锁定)”中提到过这个问题。) +虽然兰伯特时间戳定义了与因果关系一致的操作总顺序,但它们还不足以解决分布式系统中的许多常见问题。 -乍看之下,似乎总的操作顺序(例如,使用Lamport时间戳)应该足以解决此问题:如果创建了两个具有相同用户名的帐户,请选择时间戳较低的那个作为获胜者(一个谁先抓住用户名),并让更大的时间戳失败。由于时间戳是完全有序的,所以这个比较总是有效的。 +例如,考虑一个需要确保用户名唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败。 (我们之前在“[领导和锁定](#领导和锁定)”中提到过这个问题。) + +乍看之下,似乎总的操作顺序(例如,使用兰伯特时间戳)应该足以解决此问题:如果创建了两个具有相同用户名的帐户,请选择时间戳较低的那个作为获胜者(一个谁先抓住用户名),并让更大的时间戳失败。由于时间戳是完全有序的,所以这个比较总是有效的。 这种方法适用于事后确定胜利者:一旦收集了系统中的所有用户名创建操作,就可以比较他们的时间戳。然而,当一个节点刚刚收到用户的一个请求来创建一个用户名,并且现在需要决定这个请求是成功还是失败,这是不够的。此时,节点不知道另一个节点是否正在同时创建具有相同用户名的帐户,以及其他节点可以分配给该操作的时间戳。 @@ -456,13 +461,13 @@ Lamport时间戳有时会与版本向量混淆,我们在第184页上的“检 总之:为了实现像用户名的唯一性约束这样的事情,仅仅对操作进行全面的排序是不够的,您还需要知道该命令何时完成。如果您有创建用户名的操作,并且您确定没有其他节点可以在您的操作之前为全部顺序插入相同用户名的声明,则可以安全地声明操作成功。 -这个知道什么时候你的总顺序被完成的概念被记录在总顺序广播的话题中。 +这个知道什么时候你的总顺序被完成的概念被记录在全序广播的话题中。 -### 全局序列广播 +### 全序广播 如果你的程序只运行在一个CPU内核上,那么定义一个操作总的顺序是很容易的:它只是CPU执行的顺序。但是,在分布式系统中,让所有节点在相同的操作顺序上达成一致是非常棘手的。在最后一节中,我们讨论了按时间戳或序列号进行排序,但发现它不如单主复制(如果使用时间戳排序来实现唯一性约束,则不能容忍任何错误)。 -如前所述,单引导程序复制通过选择一个节点作为引导程序来确定操作的总顺序,并对引导程序上的单个CPU核心上的所有操作进行排序。接下来的挑战是如果吞吐量大于单个领导者可以处理的情况下如何扩展系统,以及如果领导者失败(参见第156页的“[处理节点宕机](#处理节点宕机)”),如何处理故障转移。在分布式系统文献中,这个问题被称为全序广播或原子广播[^ix]【25,57,58】。 +如前所述,单引导程序复制通过选择一个节点作为引导程序来确定操作的总顺序,并对引导程序上的单个CPU核心上的所有操作进行排序。接下来的挑战是如果吞吐量大于单个领导者可以处理的情况下如何扩展系统,以及如果领导者失败(“[处理节点宕机](#处理节点宕机)”),如何处理故障转移。在分布式系统文献中,这个问题被称为全序广播或原子广播[^ix]【25,57,58】。 [^ix]: “原子广播”这个术语是传统的,但是它是非常混乱的,因为它与原子的其他用法不一致:它与ACID事务中的原子性没有任何关系,只是与原子操作(在多线程编程的意义上 )或原子寄存器(线性一致性存储)。 总的顺序组播是另一个同义词。 @@ -470,7 +475,7 @@ Lamport时间戳有时会与版本向量混淆,我们在第184页上的“检 > > 每个分区有一个单独的引导程序的分区数据库通常只对每个分区进行排序,这意味着它们不能提供跨分区的一致性保证(例如,一致的快照,外键引用)。 所有分区的总排序是可能的,但需要额外的协调【59】。 -总顺序广播通常被描述为在节点之间交换消息的协议。 非正式地,它要求总是满足两个安全属性: +全序广播通常被描述为在节点之间交换消息的协议。 非正式地,它要求总是满足两个安全属性: ***可靠交付(reliable delivery)*** @@ -480,34 +485,34 @@ Lamport时间戳有时会与版本向量混淆,我们在第184页上的“检 消息以相同的顺序传递给每个节点。 -总顺序广播的正确算法必须确保始终满足可靠性和订购属性,即使节点或网络出现故障。当然,在网络中断的时候,消息不会被传送,但是一个算法可以继续重试,以便在网络被最终修复的时候消息能够通过(然后它们仍然必须按照正确的顺序传送)。 +全序广播的正确算法必须确保始终满足可靠性和订购属性,即使节点或网络出现故障。当然,在网络中断的时候,消息不会被传送,但是一个算法可以继续重试,以便在网络被最终修复的时候消息能够通过(然后它们仍然必须按照正确的顺序传送)。 #### 使用全序广播 像ZooKeeper和etcd这样的共识服务实际上是实现全面的顺序播放。这个事实暗示了整个命令广播和共识之间有着密切的联系,我们将在本章后面进行探讨。 -总顺序广播正是您所需的数据库复制:如果每封邮件都表示写入数据库,并且每个副本按相同的顺序处理相同的写入,则副本将保持一致(除了临时复制滞后)。这个原则被称为状态机复制【60】,我们将在第11章中回到它。 -类似地,可以使用总顺序广播来实现可序列化的事务:如第242页上的“实际的串行执行”中所述,如果每个消息表示一个确定性事务作为存储过程来执行,并且每个节点都处理这些消息相同的顺序,那么数据库的分区和副本保持一致【61】。 +全序广播正是您所需的数据库复制:如果每封邮件都表示写入数据库,并且每个副本按相同的顺序处理相同的写入,则副本将保持一致(除了临时复制滞后)。这个原则被称为状态机复制【60】,我们将在[第11章](ch11.md)中回到它。 +类似地,可以使用全序广播来实现可序列化的事务:如“[真的串行执行](ch7.md#真的串行执行)”中所述,如果每个消息表示一个确定性事务作为存储过程来执行,并且每个节点都处理这些消息相同的顺序,那么数据库的分区和副本保持一致【61】。 -总顺序广播的一个重要方面是顺序在交付消息时是固定的:如果后续消息已经交付,节点不允许追溯地将消息插入顺序中的较早位置。这个事实使得全部命令广播比时间戳命令更强。 +全序广播的一个重要方面是顺序在交付消息时是固定的:如果后续消息已经交付,节点不允许追溯地将消息插入顺序中的较早位置。这个事实使得全序广播比时间戳命令更强。 -查看总顺序广播的另一种方式是创建日志(如在复制日志,事务日志或预写日志中):传递消息就像附加到日志。由于所有节点必须以相同的顺序传递相同的消息,因此所有节点都可以读取日志并看到相同的消息序列。 +查看全序广播的另一种方式是创建日志(如在复制日志,事务日志或预写日志中):传递消息就像附加到日志。由于所有节点必须以相同的顺序传递相同的消息,因此所有节点都可以读取日志并看到相同的消息序列。 -全面订购广播对于实施提供防护令牌的锁定服务也很有用(请参见第294页的“防护令牌”)。每个获取锁的请求都作为消息添加到日志中,并且所有消息都按它们在日志中出现的顺序依次编号。序列号可以作为一个击剑标记,因为它是单调递增的。在ZooKeeper中,这个序列号被称为`zxid` 【15】。 +全面订购广播对于实施提供防护令牌的锁定服务也很有用(参见“[防护令牌](ch8.md#防护令牌)”)。每个获取锁的请求都作为消息添加到日志中,并且所有消息都按它们在日志中出现的顺序依次编号。序列号可以作为一个击剑标记,因为它是单调递增的。在ZooKeeper中,这个序列号被称为`zxid` 【15】。 #### 使用全序广播实现线性一致性的存储 -如图9-4所示,在线性一致性的系统中,有一个操作的总顺序。这是否意味着线性一致性与总顺序播放相同?不完全,但两者之间有密切的联系[^x]。 +如[图9-4](img/fig9-4.png)所示,在线性一致性的系统中,有一个操作的全序。这是否意味着线性一致性与全序播放相同?不完全,但两者之间有密切的联系[^x]。 -[^x]: 从形式上讲,线性读写寄存器是一个“更容易”的问题。 总顺序广播等同于共识【67】,在异步崩溃停止模型【68】中没有确定性的解决方案,而线性一致性的读写寄存器可以在同一系统模型中实现【23,24,25】。 然而,支持原子操作,如比较和设置,或者在寄存器中增加和获取,使得它相当于共识【28】。 因此,共识问题和线性一致性的注册问题密切相关。 +[^x]: 从形式上讲,线性读写寄存器是一个“更容易”的问题。 全序广播等同于共识【67】,在异步崩溃停止模型【68】中没有确定性的解决方案,而线性一致性的读写寄存器可以在同一系统模型中实现【23,24,25】。 然而,支持原子操作,如比较和设置,或者在寄存器中增加和获取,使得它相当于共识【28】。 因此,共识问题和线性一致性的注册问题密切相关。 全部顺序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息何时被传送(所以一个接收者可能落后于其他接收者)。相比之下,线性一致性是最近的保证:读取保证看到写入的最新值。 -但是,如果您有全面的顺序广播,则可以在其上构建线性一致性存储。例如,您可以确保用户名唯一标识用户帐户。 +但是,如果您有全序广播,则可以在其上构建线性一致性存储。例如,您可以确保用户名唯一标识用户帐户。 想象一下,对于每一个可能的用户名,你都可以拥有一个带有原子比较和设置操作的线性一致性寄存器。每个寄存器最初的值为空值(表示不使用用户名)。当用户想要创建一个用户名时,对该用户名的注册表执行比较设置操作,在前一个注册值为空的情况下,将其设置为用户账号。如果多个用户试图同时获取相同的用户名,则只有一个比较和设置操作会成功,因为其他用户将看到非空值(由于线性一致性)。 -您可以通过使用全部命令广播作为仅追加日志【62,63】来执行如下线性一致性的比较和设置操作: +您可以通过使用全序广播作为仅追加日志【62,63】来执行如下线性一致性的比较和设置操作: 1. 在日志中添加一条消息,暂时指明您要声明的用户名。 2. 阅读日志,并等待你附加的信息被传回给你。[^xi] @@ -517,27 +522,27 @@ Lamport时间戳有时会与版本向量混淆,我们在第184页上的“检 由于日志条目以相同顺序传递到所有节点,因此如果有多个并发写入,则所有节点将首先同意哪个节点。选择第一个冲突的写入作为胜利者,并中止后面的写入,确保所有节点都同意写入是提交还是中止。一个类似的方法可以用来在一个日志之上实现可序列化的多对象事务【62】。 -虽然此过程确保线性写入,但不能保证线性一致性读取 - 如果您从与日志异步更新的存储中读取数据,则可能是陈旧的。 (具体来说,这里描述的过程提供了顺序一致性【47,64】,有时也称为时间线一致性【65,66】,这比线性一致性要弱一些)。为了使读取线性一致性,有几个选项: +虽然此过程确保线性写入,但不能保证线性一致性读取 - 如果您从与日志异步更新的存储中读取数据,则可能是陈旧的。 (具体来说,这里描述的过程提供了顺序一致性【47,64】,有时也称为时间线一致性【65,66】,这比线性一致性要弱一些)。为了使读取线性一致,有几个选项: * 您可以通过附加消息,读取日志以及在消息被传回给您时执行实际读取来对日志进行排序。消息在日志中的位置因此定义了读取发生的时间点。 (法定读取etcd的工作有点像这样【16】。) * 如果日志允许以线性方式获取最新日志消息的位置,则可以查询该位置,等待直到该位置的所有条目传送给您,然后执行读取。 (这是Zookeeper的`sync()`操作背后的思想【15】)。 * 您可以从写入时同步更新的副本进行读取,因此可以确保最新。 (这种技术用于链式复制【63】;另请参阅第155页上的“复制研究”。) -#### 使用线性一致性存储实现总顺序广播 +#### 使用线性一致性存储实现全序广播 -最后一节介绍了如何从全部命令广播中构建一个线性一致性的比较和设置操作。我们也可以把它转过来,假设我们有线性一致性的存储,并展示如何从它构建全部命令播放。 +最后一节介绍了如何从全序广播中构建一个线性一致性的比较和设置操作。我们也可以把它转过来,假设我们有线性一致性的存储,并展示如何从它构建全部命令播放。 最简单的方法是假设你有一个线性一致性的寄存器来存储一个整数,并且有一个原子增量和获取操作【28】。或者,原子比较和设置操作也可以完成这项工作。 该算法很简单:对于每个要通过全部顺序广播发送的消息,您将递增并获取线性一致性的整数,然后将从寄存器获得的值作为序号附加到消息中。然后,您可以将消息发送到所有节点(重新发送任何丢失的消息),并且收件人将按序号连续发送消息。 -请注意,与Lamport时间戳不同,您通过递增线性一致性寄存器获得的数字形成一个没有间隙的序列。因此,如果一个节点已经发送了消息4并且接收到序列号为6的传入消息,则它知道它在传递消息6之前必须等待消息5.同样的情况并非如此 +请注意,与兰伯特时间戳不同,您通过递增线性一致性寄存器获得的数字形成一个没有间隙的序列。因此,如果一个节点已经发送了消息4并且接收到序列号为6的传入消息,则它知道它在传递消息6之前必须等待消息5.同样的情况并非如此 -与Lamport时间戳 - 事实上,这是总顺序广播和时间戳订购之间的关键区别。 +与兰伯特时间戳——事实上,这是全序广播和时间戳顺序之间的关键区别。 使用原子增量和获取操作来创建线性一致性整数有多困难?像往常一样,如果事情从来没有失败过,那很容易:你可以把它保存在一个节点的变量中。问题在于处理当该节点的网络连接中断时的情况,并在该节点失败时恢复该值【59】。一般来说,如果你对线性一致性序列号的产生者认真思考,你不可避免地会得出一个一致的算法。 -这并非巧合:可以证明,线性一致性的比较和设置(或增量和取得)寄存器和全部命令广播都相当于【28,67】。也就是说,如果你能解决其中的一个问题,你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察力! +这并非巧合:可以证明,线性一致性的比较和设置(或增量和取得)寄存器和全序广播都相当于【28,67】。也就是说,如果你能解决其中的一个问题,你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察力! 现在是时候正面处理共识问题了,我们将在本章的其余部分进行讨论。 @@ -546,64 +551,69 @@ Lamport时间戳有时会与版本向量混淆,我们在第184页上的“检 ## 分布式事务与共识 共识是分布式计算中最重要也是最基本的问题之一。从表面上看,似乎很简单:非正式地说,目标只是让几个节点达成一致。你可能会认为这不应该太难。不幸的是,许多破损的系统已经被误认为这个问题很容易解决。 -虽然共识是非常重要的,但关于它的部分在本书的后半部分已经出现了,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术研究界,对共识的理解也只是在几十年的时间内逐渐显现出来,一路上有许多误解。现在我们已经讨论了复制(第5章),事务(第7章),系统模型(第8章),线性一致性以及总播放(本章),我们终于准备好解决共识问题了。 + +虽然共识是非常重要的,但关于它的部分在本书的后半部分已经出现了,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术研究界,对共识的理解也只是在几十年的时间内逐渐显现出来,一路上有许多误解。现在我们已经讨论了复制([第5章](ch5.md)),事务([第7章](ch7.md)),系统模型([第8章](ch8.md)),线性一致性以及全序([本章](ch9.md)),我们终于准备好解决共识问题了。 在节点达成一致的情况下,有许多情况是很重要的。例如: ***领导选举*** -​ 在具有单引导程序复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会引起争议。在这种情况下,一致性对于避免错误的故障切换非常重要,从而导致两个节点都认为自己是领导者的分裂大脑情况(请参阅第156页的“处理节点中断”)。如果有两个领导者,他们都会接受写入,他们的数据会发生分歧,导致不一致和数据丢失。 +​ 在具有单引导程序复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会引起争议。在这种情况下,一致性对于避免错误的故障切换非常重要,从而导致两个节点都认为自己是领导者的脑裂情况(参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。如果有两个领导者,它们都会接受写入,他们的数据会发生分歧,导致不一致和数据丢失。 ***原子提交*** -在支持跨越多个节点或分区的事务的数据库中,有一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就ACID而言,请参阅第223页的“原子性”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误)或者他们都承诺(如果没有出错)。这个共识的例子被称为原子提交问题[^xii]。 +在支持跨越多个节点或分区的事务的数据库中,有一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就ACID而言,请参“[原子性](ch7.md#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误)或者他们都承诺(如果没有出错)。这个共识的例子被称为原子提交问题[^xii]。 -[^]: 原子提交的形式化与共识稍有不同:原子事务只有在所有参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 允许共识决定其中一位参与者提出的任何价值。 然而,原子的承诺和共识是可以相互压缩的【70,71】。 非阻塞原子提交比共识更难 - 请参阅第359页上的“三阶段提交”。 + +[^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在所有参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 允许共识决定其中一位参与者提出的任何值。 然而,原子的承诺和共识是可以相互压缩的【70,71】。 非阻塞原子提交比共识更难——参阅“[三阶段提交](#三阶段提交)”。 > ### 共识的不可能性 > > 您可能已经听说过作者Fischer,Lynch和Paterson之后的FLP结果【68】,这证明如果存在节点可能崩溃的风险,则不存在总是能够达成一致的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而,在这里,我们正在讨论达成共识的算法。这里发生了什么? > > 答案是FLP结果在异步系统模型中得到了证明(请参阅“系统模型与现实”在本部分),这是一个非常有限的模型,它假定确定性算法不能使用任何时钟或超时。如果算法被允许使用超时或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),那么共识就变得可以解决了【67】。即使只允许算法使用随机数也足以绕过不可能的结果【69】。 +> > 因此,FLP虽然不可能达成共识,但理论上具有重要意义,但实际上分布式系统通常可以达成共识。 -在本节中,我们将首先更详细地检查原子提交问题。具体来说,我们将讨论两阶段提交(2PC)算法,这是解决原子提交最常见的方法,并在各种数据库,消息传递系统和应用服务器中实现。事实证明,2PC是一种一致的算法,但不是一个很好的【70,71】。 +在本节中,我们将首先更详细地检查原子提交问题。具体来说,我们将讨论两阶段提交(2PC)算法,这是解决原子提交最常见的方法,并在各种数据库,消息传递系统和应用服务器中实现。事实证明,2PC是一种一致的算法,但并不是一个很好的算法【70,71】。 -通过从2PC学习,我们将继续努力实现更好的一致性算法,比如ZooKeeper(Zab)和etcd(Raft)中使用的算法。 +通过对2PC学习,我们将继续努力实现更好的一致性算法,比如ZooKeeper(Zab)和etcd(Raft)中使用的算法。 ### 原子提交与二阶段提交(2PC) -在第7章中,我们了解到事务原子性的目的是在出现几次写错的情况下提供简单的语义。事务的结果要么是成功的提交,在这种情况下,所有事务的写入都是持久的,或者中止,在这种情况下,所有事务的写入都被回滚(即撤消或丢弃)。 +在[第7章](ch7.md)中,我们了解到事务原子性的目的是在出现几次写错的情况下提供简单的语义。事务的结果要么是成功的提交,在这种情况下,所有事务的写入都是持久的,或者中止,在这种情况下,所有事务的写入都被回滚(即撤消或丢弃)。 -原子性可以防止失败的事务乱丢数据库,其结果是半成品和半更新状态。这对于多对象事务(请参阅“单对象和多对象操作”一节第228页)和维护二级索引的数据库尤其重要。每个辅助索引都是与主数据分离的数据结构 - 因此,如果您修改了一些数据,则还需要在辅助索引中进行相应的更改。原子性确保二级索引与主数据保持一致(如果索引与主数据不一致,则不会非常有用)。 +原子性可以防止失败的事务搅乱数据库,其结果是半成品和半更新状态。这对于多对象事务(请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护二级索引的数据库尤其重要。每个辅助索引都是与主数据分离的数据结构——因此,如果您修改了一些数据,则还需要在辅助索引中进行相应的更改。原子性确保二级索引与主数据保持一致(如果索引与主数据不一致,则不会很有用)。 #### 从单节点到分布式原子提交 -对于在单个数据库节点执行的事务,原子性通常由存储引擎执行。当客户端请求数据库节点提交事务时,数据库使事务的写入持久化(通常在预先写好的日志中;请参阅第82页的“使B树可靠”),然后将提交记录追加到日志中磁盘。如果数据库在这个过程中间崩溃,当节点重新启动时,事务从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;如果不是,则来自该事务的任何写入都被回滚。 +对于在单个数据库节点执行的事务,原子性通常由存储引擎执行。当客户端请求数据库节点提交事务时,数据库使事务的写入持久化(通常在预写式日志中:参阅“[使B树可靠](ch3.md#使B树可靠)”),然后将提交记录追加到日志中磁盘。如果数据库在这个过程中间崩溃,当节点重新启动时,事务从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;如果不是,则来自该事务的任何写入都被回滚。 -因此,在单个节点上,事务承诺主要取决于数据持久写入磁盘的顺序:首先是数据,然后是提交记录【72】。事务提交或放弃的关键决定时刻是磁盘完成写入提交记录的时刻:在此之前,仍有可能中止(由于崩溃),但在此之后,事务提交 - 特德(即使数据库崩溃)。因此,这是一个单一的设备(一个特定的磁盘驱动器的控制器,连接到一个特定的节点),使提交原子。 +因此,在单个节点上,事务承诺主要取决于数据持久写入磁盘的顺序:首先是数据,然后是提交记录【72】。事务提交或放弃的关键决定时刻是磁盘完成写入提交记录的时刻:在此之前,仍有可能中止(由于崩溃),但在此之后,事务已经提交(即使数据库崩溃)。因此,这是一个单一的设备(一个特定的磁盘驱动器的控制器,连接到一个特定的节点),使得提交具有原子性。 -但是,如果一个事务中涉及多个节点呢?例如,也许在分区数据库中有一个多对象事务,或者是一个由术语分区的二级索引(其中索引条目可能位于与主数据不同的节点上;请参阅“分区和二级索引”第206页)。大多数“NoSQL”分布式数据存储不支持这种分布式事务,而是各种集群关系系统(请参见“实践中的分布式事务”)。 +但是,如果一个事务中涉及多个节点呢?例如,也许在分区数据库中有一个多对象事务,或者是一个由关键词分区的二级索引(其中索引条目可能位于与主数据不同的节点上;参阅“[分区和二级索引](ch6.md#分区和二级索引)”)。大多数“NoSQL”分布式数据存储不支持这种分布式事务,而是各种集群关系系统(请参见“[实践中的分布式事务](#实践中的分布式事务)”)。 -在这些情况下,仅向所有节点发送提交请求并且独立提交每个节点的事务是不够的。这样做很容易发生,提交在一些节点上成功,在其他节点上失败,这违反了原子性保证: +在这些情况下,仅向所有节点发送提交请求并且独立提交每个节点的事务是不够的。这样做很容易发生:提交在某些节点上成功,在其他节点上失败,这违反了原子性保证: * 某些节点可能会检测到约束冲突或冲突,因此需要中止,而其他节点则可以成功进行提交。 * 某些提交请求可能在网络中丢失,最终由于超时而中止,而其他提交请求则通过。 * 在提交记录完全写入之前,某些节点可能会崩溃,并在恢复时回滚,而其他节点则成功提交。 -如果某些节点提交了事务,但其他节点却放弃了这些事务,那么这些节点就会彼此不一致(如图7-3所示)。而且一旦在一个节点上提交了一个事务,如果事后证明它在另一个节点上被中止,它将不能被收回。出于这个原因,一旦确定事务中的所有其他节点也将提交,节点就必须进行提交。 +如果某些节点提交了事务,但其他节点却放弃了这些事务,那么这些节点就会彼此不一致(如[图7-3](img/fig7-3.png)所示)。而且一旦在一个节点上提交了一个事务,如果事后证明它在另一个节点上被中止,它将不能被收回。出于这个原因,一旦确定事务中的所有其他节点也将提交,节点就必须进行提交。 -事务提交必须是不可撤销的 - 您不得改变主意,并在交易提交后追溯中止交易。这个规则的原因是,一旦数据被提交,其他交易就可以看到,因此其他客户可能会开始依赖这些数据。这个原则构成了读取提交隔离的基础,在“读取提交”一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有读取提交数据的事务将基于被追溯声明不存在的数据所以他们也必须恢复。 +事务提交必须是不可撤销的——你不能改变主意,并在事务提交后追溯中止事务。这个规则的原因是,一旦数据被提交,其他事务就可以看到,因此其他客户可能会开始依赖这些数据。这个原则构成了读取提交隔离的基础,在“[读已提交](ch7.md#读已提交)”一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有读取提交数据的事务将基于被追溯声明不存在的数据所以他们也必须恢复。 -(承诺交易的效果有可能在后来被另一个补偿交易取消【73,74】,但从数据库的角度来看,这是一个单独的交易,因此任何交叉交易的正确性要求是应用程序的问题。) +(提交事务的效果有可能在后来被另一个补偿事务取消【73,74】,但从数据库的角度来看,这是一个单独的事务,因此任何交叉事务的正确性要求是应用程序的问题。) #### 介绍两阶段提交 -两阶段提交是一种用于实现跨多个节点的原子事务提交的算法,即确保这一点 +两阶段提交是一种用于实现跨多个节点的原子事务提交的算法,即确保所有节点提交或所有节点中止。 它是分布式数据库中的经典算法【13,35,75】。 2PC在某些数据库内部使用,并且还以XA事务【76,77】(例如Java Transaction API支持)或通过`WS-AtomicTransaction for SOAP Web`服务的形式提供给应用程序【78,79】。 -![](img/fig9-8.png) +[图9-9](img/fig9-9)说明了2PC的基本流程。 与单节点事务一样,2PC中的提交/终止进程分为两个阶段(因此名称),而不是单个提交请求。 + +![](img/fig9-9.png) **图9-9 两阶段提交(2PC)的成功执行** @@ -611,130 +621,126 @@ Lamport时间戳有时会与版本向量混淆,我们在第184页上的“检 > > 两阶段提交(2PC)和两阶段锁定(请参阅第257页上的“两阶段锁定(2PL)”)是两个完全不同的事情。 2PC在分布式数据库中提供原子提交,而2PL提供可序列化的隔离。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中的不幸的相似性。 -2PC使用一个通常不会出现在单节点事务中的新组件:协调器(也称为事务管理器)。协调器通常在请求事务的相同应用程序进程(例如,嵌入在Java EE容器中)中实现为库,但也可以是单独的进程或服务。这种协调员的例子包括Narayana,JOTM,BTM或MSDTC。 +2PC使用一个通常不会出现在单节点事务中的新组件:**协调器(coordinator)**(也称为事务管理器)。协调器通常在请求事务的相同应用程序进程(例如,嵌入在Java EE容器中)中实现为库,但也可以是单独的进程或服务。这种协调者的例子包括Narayana,JOTM,BTM或MSDTC。 -正常情况下,2PC事务从应用程序在多个数据库节点上读写数据开始。我们把这些数据库节点称为交易参与者。当应用程序准备提交时,协调器开始阶段1:它发送一个准备请求到每个节点,询问他们是否能够提交。协调员然后跟踪参与者的回应: +正常情况下,2PC事务从应用程序在多个数据库节点上读写数据开始。我们把这些数据库节点称为事务参与者。当应用程序准备提交时,协调器开始阶段1:它发送一个准备请求到每个节点,询问他们是否能够提交。协调者然后跟踪参与者的回应: -* 如果所有参与者都回答“是”,表示他们已经准备好提交,那么协调员在阶段2发出提交请求,实际发生提交。 -* 如果任何参与者回复“否”,则协调员在阶段2中向所有节点发送放弃请求。 +* 如果所有参与者都回答“是”,表示他们已经准备好提交,那么协调者在阶段2发出**提交(commit)**请求,实际发生提交。 +* 如果任何参与者回复“否”,则协调者在阶段2中向所有节点发送**中止(abort)**请求。 -这个过程有点像西方传统婚姻仪式:部长要求新娘和新郎分别是否要结婚,而且通常都是从两个方面接受“我做”的答案。收到两者都恢复后,因为参与者投票“是”,它不能拒绝提交时恢复。 +这个过程有点像西方传统婚姻仪式:司仪分别询问新娘和新郎是否要结婚,通常是从两方都收到“我愿意”的答复。收到两者的回复后,司仪宣布这对情侣成为夫妻:事务就提交了,这一幸福事实会广播至所有的参与者中。如果新娘与新郎之一没有回复”我愿意“,婚礼就会中止【73】。 -因此,该协议包含两个关键的“不归路”点:当参与者投票“是”时,它承诺它肯定能够稍后提交(尽管协调员可能仍然选择放弃)。一旦协调员决定,这个决定是不可撤销的。这些承诺保证了2PC的原子性。 (单节点原子提交将这两个事件合并为一个:将提交记录写入事务日志。) - -回到婚姻的比喻,在说“我是”之前,你和你的新娘/新郎有“放弃”这个交易的自由,说“不行!”(或者这个)。然而,在说“我这样做”之后,你不能收回那个声明。如果你说“我这样做”后你晕了,而你没有听到部长说“你现在是夫妻”,那不会改变交易的事实。当你稍后恢复意识时,你可以通过查询部长的全球交易ID状态来查明你是否已婚,或者你可以等待部长下一次提交请求的重试(因为重试将一直持续下去你的无意识的时期)。 - -#### 承诺体系 +#### 承诺系统 从这个简短的描述可能不清楚为什么两阶段提交确保了原子性,而跨几个节点的一阶段提交没有。准备和提交请求当然可以在两阶段的情况下轻易地丢失。 2PC有什么不同? 为了理解它的工作原理,我们必须更详细地分解这个过程: -1. 当应用程序想要开始一个分布式事务时,它向协调器请求一个事务ID。此交易ID是全球唯一的。 -2. 应用程序在每个参与者上开始单节点事务,并将全局唯一事务ID附加到单节点事务。所有的读写都是在这些单节点事务之一中完成的。如果在这个阶段出现任何问题(例如,节点崩溃或请求超时),则协调者或任何参与者都可以中止。 -3. 当应用程序准备提交时,协调器向所有参与者发送一个准备请求,标记为全局事务ID。如果这些请求中的任何一个失败或超时,则协调器向所有参与者发送针对该交易ID的放弃请求。 -4. 参与者收到准备请求时,确保在任何情况下都可以明确地进行交易。这包括将所有事务数据写入磁盘(出现故障,电源故障或硬盘空间不足以拒绝稍后提交)以及检查是否存在任何冲突或约束违规。通过向协调者回答“是”,节点承诺在没有错误的情况下提交交易。换句话说,参与者放弃了放弃交易的权利,但没有实际承诺。 -5. 当协调员收到所有准备请求的答复时,就是否提交或中止交易作出明确的决定(只有在所有参与者投赞成票的情况下才提交)。协调员必须把这个决定写到磁盘上的事务日志中,以便它知道它决定的方式,以防随后发生崩溃。这被称为提交点。 -6. 一旦协调员的决定写入磁盘,提交或放弃请求被发送给所有参与者。如果此请求失败或超时,则协调员必须一直重试,直到成功为止。没有更多的事情要做,如果做出决定,那么决定必须执行,不管它需要多少次重试。如果参与者在此期间坠毁,交易将在恢复时进行 - 由于参与者投票“是”,因此恢复时不能拒绝提交。 +1. 当应用想要开启一个分布式事务时,它向协调者请求一个事务ID。此事务ID是全局唯一的。 +2. 应用在每个参与者上开始单节点事务,并将全局唯一事务ID附到单节点事务上。所有的读写都是在这些单节点事务之一中完成的。如果在这个阶段出现任何问题(例如,节点崩溃或请求超时),则协调者或任何参与者都可以中止。 +3. 当应用程序准备提交时,协调器向所有参与者发送一个准备请求,标记为全局事务ID。如果这些请求中的任何一个失败或超时,则协调器向所有参与者发送针对该事务ID的放弃请求。 +4. 参与者收到准备请求时,确保在任何情况下都可以明确地进行事务。这包括将所有事务数据写入磁盘(出现故障,电源故障或硬盘空间不足以拒绝稍后提交)以及检查是否存在任何冲突或约束违规。通过向协调者回答“是”,节点承诺在没有错误的情况下提交事务。换句话说,参与者放弃了中止事务的权利,但没有实际提交。 +5. 当协调者收到所有准备请求的答复时,就是否提交或中止事务作出明确的决定(只有在所有参与者投赞成票的情况下才提交)。协调者必须把这个决定写到磁盘上的事务日志中,以便它知道它决定的方式,以防随后发生崩溃。这被称为**提交点(commit point)**。 +6. 一旦协调者的决定写入磁盘,提交或放弃请求被发送给所有参与者。如果此请求失败或超时,则协调者必须一直重试,直到成功为止。没有更多的事情要做,如果做出决定,那么决定必须执行,不管它需要多少次重试。如果参与者在此期间崩溃,事务将在恢复时进行——由于参与者投票“是”,因此恢复时不能拒绝提交。 -因此,该协议包含两个关键的“不归路”点:当参与者投票“是”时,它承诺它肯定能够稍后提交(尽管协调员可能仍然选择放弃)。一旦协调员决定,这个决定是不可撤销的。这些承诺保证了2PC的原子性。 (单节点原子提交将这两个事件合并为一个:将提交记录写入事务日志。) +因此,该协议包含两个关键的“不归路”点:当参与者投票“是”时,它承诺它肯定能够稍后提交(尽管协调者可能仍然选择放弃)。一旦协调者决定,这个决定是不可撤销的。这些承诺保证了2PC的原子性。 (单节点原子提交将这两个事件合并为一个:将提交记录写入事务日志。) -回到婚姻的比喻,在说“我是”之前,你和你的新娘/新郎有“放弃”这个交易的自由,说“不行!”(或者这个)。然而,在说“我这样做”之后,你不能收回那个声明。如果你说“我这样做”后你晕了,而你没有听到部长说“你现在是夫妻”,那不会改变交易的事实。当你稍后恢复意识时,你可以通过查询部长的全球交易ID状态来查明你是否已婚,或者你可以等待部长下一次提交请求的重试(因为重试将一直持续下去你的无意识的时期)。 +回到婚姻的比喻,在说“我是”之前,你和你的新娘/新郎有“中止”这个事务的自由,通过回复“不行!”(或者差不多效果的话)。然而,在说“我愿意”之后,你就不能收回那个声明了。如果你说“我愿意”后晕倒了,而你没有听到司仪说“你们现在是夫妻了”,那并不会改变事务已经提交的事实。当你稍后恢复意识时,你可以通过查询司仪的全局事务ID状态来查明你是否已婚,或者你可以等待司仪重试下一次提交请求(因为重试将在你无意识期间一直持续)。 #### 协调者失效 -我们已经讨论了在2PC期间如果其中一个参与者或网络发生故障会发生什么情况:如果任何一个准备请求失败或者超时,协调员就中止交易。如果任何提交或中止请求失败,协调器将无条件重试。但是,如果协调员崩溃,会发生什么情况不太清楚。 +我们已经讨论了在2PC期间如果其中一个参与者或网络发生故障会发生什么情况:如果任何一个准备请求失败或者超时,协调者就中止事务。如果任何提交或中止请求失败,协调器将无条件重试。但是,如果协调者崩溃,会发生什么情况并不太清楚。 -如果协调员在发送准备请求之前失败,参与者可以安全地中止交易。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃 - 必须等待协调者回答交易是否已经发生或中止。如果此时协调器崩溃或网络出现故障,参与者只能等待。参与者在这个状态下的交易被怀疑或不确定。 +如果协调者在发送准备请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃——必须等待协调者回答事务是否已经发生或中止。如果此时协调器崩溃或网络出现故障,参与者只能等待。参与者在这个状态下的事务被怀疑或不确定。 情况如[图9-10](img/fig9-10)所示。在这个特定的例子中,协调器实际上决定提交,数据库2收到提交请求。但是,协调器在将提交请求发送到数据库1之前发生崩溃,因此数据库1不知道是否提交或中止。即使超时在这里也没有帮助:如果数据库1在超时后单方面中止,它将最终与提交的数据库2不一致。同样,单方面犯也是不安全的,因为另一个参与者可能已经中止了。 ![](img/fig9-10.png) - **图9-10 参与者投赞成票后,协调员崩溃。数据库1不知道是否提交或中止** + **图9-10 参与者投赞成票后,协调者崩溃。数据库1不知道是否提交或中止** -没有协调员的消息,参与者无法知道是否承诺或放弃。原则上,参与者可以相互沟通,找出每个参与者如何投票并达成一致,但这不是2PC协议的一部分。 +没有协调者的消息,参与者无法知道是否承诺或放弃。原则上,参与者可以相互沟通,找出每个参与者如何投票并达成一致,但这不是2PC协议的一部分。 -2PC可以完成的唯一方法是等待协调员恢复。这就是为什么协调员必须在向参与者发送提交或中止请求之前将其提交或中止决定写入磁盘上的事务日志:协调器恢复后,通过读取其事务日志来确定所有有疑问的事务的状态。任何在协调器日志中没有提交记录的事务都会中止。因此,2PC的提交点归结为协调器上的常规单节点原子提交。 +2PC可以完成的唯一方法是等待协调者恢复。这就是为什么协调者必须在向参与者发送提交或中止请求之前将其提交或中止决定写入磁盘上的事务日志:协调器恢复后,通过读取其事务日志来确定所有有疑问的事务的状态。任何在协调器日志中没有提交记录的事务都会中止。因此,2PC的提交点归结为协调器上的常规单节点原子提交。 #### 三阶段提交 两阶段提交被称为阻塞原子提交协议,因为2PC可能卡住等待协调器恢复。理论上,可以使一个原子提交协议非阻塞,以便在节点失败时不会卡住。但是,在实践中做这个工作并不那么简单。 -作为2PC的替代方案,已经提出了一种称为三阶段提交(3PC)的算法【13,80】。然而,3PC假定一个有界延迟的网络和有限响应时间的节点;在大多数具有无限网络延迟和进程暂停的实际系统中(见第8章),它不能保证原子性。 +作为2PC的替代方案,已经提出了一种称为三阶段提交(3PC)的算法【13,80】。然而,3PC假定一个有界延迟的网络和有限响应时间的节点;在大多数具有无限网络延迟和进程暂停的实际系统中(见[第8章](ch8.md)),它不能保证原子性。 -通常,非阻塞原子提交需要一个完美的故障检测器【67,71】 - 即一个可靠的机制来判断一个节点是否已经崩溃。在无限延迟的网络中,超时不是可靠的故障检测器,因为即使没有节点崩溃,请求也可能由于网络问题而超时。出于这个原因,2PC继续被使用,尽管协调器故障的已知问题。 +通常,非阻塞原子提交需要一个完美的故障检测器【67,71】—— 即一个可靠的机制来判断一个节点是否已经崩溃。在无限延迟的网络中,超时不是可靠的故障检测器,因为即使没有节点崩溃,请求也可能由于网络问题而超时。出于这个原因,2PC仍然被使用,尽管大家都清楚可能有协调器故障的问题。 ### 实践中的分布式事务 -分布式事务,尤其是那些通过两阶段提交实现的事务,声誉混杂。一方面,它们被看作是提供一个难以实现的重要的安全保证;另一方面,他们被批评为造成运营问题,造成业绩下滑,承诺超过他们能够实现的目标【81,82,83,84】。许多云服务由于其产生的操作问题而选择不执行分布式事务【85,86】。 +分布式事务,尤其是那些通过两阶段提交实现的事务,毁誉参半。一方面,它们被看作是提供一个难以实现的重要的安全保证;另一方面,他们被批评为造成运维问题,造成性能下降,承诺超过他们能够实现的目标【81,82,83,84】。许多云服务由于其导致的运维问题而选择不实现分布式事务【85,86】。 -分布式事务的某些实现会带来严重的性能损失 - 例如,MySQL中的分布式事务被报告比单节点事务慢10倍以上【87】,所以当人们建议不要使用这些事务时就不足为奇了。两阶段提交所固有的大部分性能成本是由于崩溃恢复所需的额外磁盘强制(fsync)【88】以及额外的网络往返。 +分布式事务的某些实现会带来严重的性能损失——例如,MySQL中的分布式事务被报告比单节点事务慢10倍以上【87】,所以当人们建议不要使用这些事务时就不足为奇了。两阶段提交所固有的大部分性能成本是由于崩溃恢复所需的额外强制刷盘(`fsync`)【88】以及额外的网络往返。 -但是,我们不应该直接抛弃分布式交易,而应该更加详细地审视这些交易,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明“分布式交易”的含义。两种截然不同的分布式交易类型经常被混淆: +但是,我们不应该直接抛弃分布式事务,而应该更加详细地审视这些事务,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明“**分布式事务**”的含义。两种截然不同的分布式事务类型经常被混淆: ***数据库内部的分布式事务*** -一些分布式数据库(即在其标准配置中使用复制和分区的数据库)支持该数据库节点之间的内部事务。例如,VoltDB和MySQL Cluster的NDB存储引擎就有这样的内部事务支持。在这种情况下,所有参与交易的节点都运行相同的数据库软件。 +一些分布式数据库(即在其标准配置中使用复制和分区的数据库)支持该数据库节点之间的内部事务。例如,VoltDB和MySQL Cluster的NDB存储引擎就有这样的内部事务支持。在这种情况下,所有参与事务的节点都运行相同的数据库软件。 ***异构分布式事务*** -在异构交易中,参与者有两种或两种以上不同的技术:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。 +在异构事务中,参与者有两种或两种以上不同的技术:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。 -数据库内部事务不必与任何其他系统兼容,因此他们可以使用任何协议并应用特定技术的特定优化。因此,数据库内部的分布式事务通常可以很好地工作。另一方面,跨越异构技术的交易则更具挑战性。 +数据库内部事务不必与任何其他系统兼容,因此他们可以使用任何协议并应用特定技术的特定优化。因此,数据库内部的分布式事务通常可以很好地工作。另一方面,跨越异构技术的事务则更具挑战性。 #### 恰好一次的消息处理 异构的分布式事务处理能够以强大的方式集成不同的系统。例如,当且仅当用于处理消息的数据库事务处理时,来自消息队列的消息才能被确认为已处理成功承诺。这是通过自动提交消息确认和数据库写入单个事务来实现的。使用分布式事务支持,即使消息代理和数据库是在不同机器上运行的两个不相关技术,也是可能的。 -如果消息传递或数据库事务失败,两者都会中止,因此消息代理可能会稍后安全地重新传递消息。因此,通过自动提交消息及其处理的副作用,即使在成功之前需要几次重试,也可以确保消息被有效处理一次。中止放弃部分完成的交易的任何副作用。 +如果消息传递或数据库事务失败,两者都会中止,因此消息代理可能会稍后安全地重新传递消息。因此,通过自动提交消息及其处理的副作用,即使在成功之前需要几次重试,也可以确保消息被有效处理一次。中止放弃部分完成的事务的任何副作用。 这样的分布式事务只有在所有受事务影响的系统都能够使用相同的原子提交协议的情况下才是可能的。例如,处理消息的副作用是发送邮件,而邮件服务器不支持两阶段提交:如果邮件处理失败并重试,可能会发送两次或更多次的邮件。但是,如果处理消息的所有副作用在事务中止时回滚,那么可以安全地重新尝试处理步骤,就好像什么都没发生过一样。 -我们将回到第11章中的一次消息处理的主题。让我们首先看看允许这种异构分布式事务的原子提交协议。 +我们将在[第11章](ch11.md)中再次回到”恰好一次“消息处理的主题。让我们首先看看允许这种异构分布式事务的原子提交协议。 #### XA事务 -X/Open XA(**扩展架构(eXtended Architecture)**的缩写)是跨异构技术实现两阶段提交的标准【76,77】。它于1991年推出并得到了广泛的实施:许多传统关系数据库(包括PostgreSQL,MySQL,DB2,SQL Server和Oracle)和消息代理(包括ActiveMQ,HornetQ,MSMQ和IBM MQ) 。 +*X/Open XA*(**扩展架构(eXtended Architecture)**的缩写)是跨异构技术实现两阶段提交的标准【76,77】。它于1991年推出并得到了广泛的实施:许多传统关系数据库(包括PostgreSQL,MySQL,DB2,SQL Server和Oracle)和消息代理(包括ActiveMQ,HornetQ,MSMQ和IBM MQ) 。 -XA不是一个网络协议 - 它只是一个用于与事务协调器连接的C API。此API的绑定以其他语言存在;例如,在Java EE应用程序的世界中,XA事务是使用Java事务API(JTA)实现的,而Java事务API(JTA)则由许多用于使用Java数据库连接(JDBC)的数据库驱动程序以及使用Java消息服务(JMS)API。 +XA不是一个网络协议——它只是一个用于与事务协调器连接的C API。此API的绑定以其他语言存在;例如,在Java EE应用程序的世界中,XA事务是使用Java事务API(JTA)实现的,而Java事务API(JTA)则由许多用于使用Java数据库连接(JDBC)的数据库驱动程序以及使用Java消息服务(JMS)API。 -XA假定您的应用程序使用网络驱动程序或客户端库来与参与者数据库或消息传递服务进行通信。如果驱动程序支持XA,则表示它调用XA API以查明操作是否应该是分布式事务的一部分 - 如果是,则将必要的信息发送到数据库服务器。司机还会提供回调,协调员可以通过回调来要求参与者准备,提交或中止。 +XA假定您的应用程序使用网络驱动程序或客户端库来与参与者数据库或消息传递服务进行通信。如果驱动程序支持XA,则表示它调用XA API以查明操作是否应该是分布式事务的一部分——如果是,则将必要的信息发送到数据库服务器。司机还会提供回调,协调者可以通过回调来要求参与者准备,提交或中止。 -事务协调器实现XA API。标准没有指定应该如何实现,但实际上,协调器通常只是一个库,与发出事务的应用程序(不是单独的服务)一起被加载到相同的进程中。它跟踪交易的参与者,在要求他们准备(通过回调驱动程序)之后收集参与者的回答,并使用本地磁盘上的日志记录每次交易的提交/中止决定。 +事务协调器实现XA API。标准没有指定应该如何实现,但实际上,协调器通常只是一个库,与发出事务的应用程序(不是单独的服务)一起被加载到相同的进程中。它跟踪事务的参与者,在要求他们准备(通过回调驱动程序)之后收集参与者的回答,并使用本地磁盘上的日志记录每次事务的提交/中止决定。 -如果应用程序进程崩溃,或者运行应用程序的机器死亡,协调者就会使用它。然后任何有准备但未提交的交易的参与者都被怀疑。由于协调程序的日志位于应用程序服务器的本地磁盘上,因此必须重新启动该服务器,并且协调程序库必须读取日志以恢复每个事务的提交/中止结果。只有这样,协调员才能使用数据库驱动程序的XA回调来要求参与者提交或中止。数据库服务器不能直接联系协调器,因为所有通信都必须通过其客户端库。 +如果应用程序进程崩溃,或者运行应用程序的机器死亡,协调者就会使用它。然后任何有准备但未提交的事务的参与者都被怀疑。由于协调程序的日志位于应用程序服务器的本地磁盘上,因此必须重新启动该服务器,并且协调程序库必须读取日志以恢复每个事务的提交/中止结果。只有这样,协调者才能使用数据库驱动程序的XA回调来要求参与者提交或中止。数据库服务器不能直接联系协调器,因为所有通信都必须通过其客户端库。 #### 怀疑时持有锁 -为什么我们非常关心交易被怀疑?系统的其他部分不能继续工作,而忽视最终将被清理的有问题的交易吗? +为什么我们非常关心事务被怀疑?系统的其他部分不能继续工作,而忽视最终将被清理的有问题的事务吗? 问题在于锁定。正如在第225页上的“读取已提交”中所讨论的那样,数据库事务通常对其修改的行进行行级别的排他锁定,以防止脏写入。此外,如果要使用可序列化的隔离,则使用两阶段锁定的数据库也必须对事务读取的任何行执行共享锁定(参见“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)。 -在事务提交或中止之前,数据库不能释放这些锁(如[图9-9](img/fig9-9.png)中的阴影区域所示)。因此,在使用两阶段提交时,交易必须在整个时间内保持锁定状态。如果协调员已经坠毁,需要20分钟才能重新启动,这些锁将会保持20分钟。如果协调员的日志由于某种原因完全丢失,这些锁将永久保存,或者至少在管理员手动解决该情况之前。 +在事务提交或中止之前,数据库不能释放这些锁(如[图9-9](img/fig9-9.png)中的阴影区域所示)。因此,在使用两阶段提交时,事务必须在整个时间内保持锁定状态。如果协调者已经坠毁,需要20分钟才能重新启动,这些锁将会保持20分钟。如果协调者的日志由于某种原因完全丢失,这些锁将永久保存,或者至少在管理员手动解决该情况之前。 -当这些锁被保留时,其他事务不能修改这些行。根据数据库的不同,其他事务甚至可能被阻止读取这些行。因此,其他交易不能简单地继续他们的业务 - 如果他们想访问相同的数据,他们将被阻止。这可能会导致大部分应用程序变得不可用,直到有问题的事务得到解决。 +当这些锁被保留时,其他事务不能修改这些行。根据数据库的不同,其他事务甚至可能被阻止读取这些行。因此,其他事务不能简单地继续他们的业务 - 如果他们想访问相同的数据,他们将被阻止。这可能会导致大部分应用程序变得不可用,直到有问题的事务得到解决。 #### 从协调器故障中恢复 -理论上,如果协调器崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何有问题的事务。然而,在实践中,孤立的不确定交易确实发生【89,90】,也就是说,协调者不能以任何理由决定结果的交易(例如,因为交易日志已经由于软件错误)。这些交易不能自动解决,所以他们永远坐在数据库中,持有锁和阻止其他交易。 +理论上,如果协调器崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何有问题的事务。然而,在实践中,孤立的不确定事务确实发生【89,90】,也就是说,协调者不能以任何理由决定结果的事务(例如,因为事务日志已经由于软件错误)。这些事务不能自动解决,所以他们永远坐在数据库中,持有锁和阻止其他事务。 即使重新启动数据库服务器也不能解决这个问题,因为2PC的正确实现必须在重新启动时保留一个有问题的事务的锁(否则就会冒违反原子性保证的风险)。这是一个棘手的情况。 -唯一的出路是让管理员手动决定是提交还是回滚事务。管理员必须检查每个有问题的交易的参与者,确定是否有任何参与者已经提交或中止,然后将相同的结果应用于其他参与者。解决这个问题潜在地需要大量的人工努力,并且在严重的生产中断期间(否则,为什么协调员处于这样一个糟糕的状态),很可能需要在高压力和时间压力下完成。 +唯一的出路是让管理员手动决定是提交还是回滚事务。管理员必须检查每个有问题的事务的参与者,确定是否有任何参与者已经提交或中止,然后将相同的结果应用于其他参与者。解决这个问题潜在地需要大量的人工努力,并且在严重的生产中断期间(否则,为什么协调者处于这样一个糟糕的状态),很可能需要在高压力和时间压力下完成。 -许多XA的实现都有一个叫做启发式决策的紧急逃生舱口:允许参与者单方面决定放弃或进行一个有疑问的交易,而不需要协调员做出明确的决定【76,77,91】。要清楚的是,这里的平庸是可能破坏原子性的委婉说法,因为它违背了两阶段承诺的承诺体系。因此,启发式决策只是为了摆脱灾难性的情况,而不是经常使用。 +许多XA的实现都有一个叫做**启发式决策(heuristic decistions)**的紧急逃生舱口:允许参与者单方面决定放弃或提交一个有疑问的事务,而不需要协调者做出明确的决定【76,77,91】。要清楚的是,这里的启发式是可能破坏原子性的委婉说法,因为它违背了两阶段承诺的承诺体系。因此,启发式决策只是为了逃出灾难性的情况,而不是经常使用。 #### 分布式事务的限制 XA事务解决了保持多个参与者数据系统一致的真实而重要的问题,但正如我们所看到的那样,它们也引入了主要的操作问题。特别是,关键的实现是事务协调器本身就是一种数据库(在其中存储事务结果),因此需要像其他重要数据库一样小心: * 如果协调器没有被复制,而是只在一台机器上运行,那么整个系统是一个失败的单点(因为它的失败导致其他应用程序服务器阻塞在有问题的事务处理的锁上)。令人惊讶的是,许多协调器实现默认情况下不是高度可用,或者只有基本的复制支持。 -* 许多服务器端应用程序都是在无状态模式下开发的(受到HTTP的青睐),所有持久状态都存储在数据库中,具有应用程序服务器可随意添加和删除的优点。但是,当协调器是应用程序服务器的一部分时,它会改变部署的性质。突然间,协调员的日志成为持久系统状态的关键部分 - 与数据库本身一样重要,因为协调员日志是为了在崩溃后恢复疑问交易所必需的。这样的应用程序服务器不再是无状态的。 +* 许多服务器端应用程序都是在无状态模式下开发的(受到HTTP的青睐),所有持久状态都存储在数据库中,具有应用程序服务器可随意添加和删除的优点。但是,当协调器是应用程序服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分——与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复疑问事务所必需的。这样的应用程序服务器不再是无状态的。 * 由于XA需要与各种数据系统兼容,因此这是必须的最低公分母。例如,它不能检测到不同系统间的死锁(因为这将需要一个标准化的协议来让系统交换每个事务正在等待的锁的信息),而且它不适用于[SSI](ch7.md#可串行快照隔离(SSI) ),因为这需要一个协议来识别不同系统之间的冲突。 -* 对于数据库内部的分布式事务(而不是XA),限制不是很大,例如SSI的分布式版本是可能的。然而,仍然存在2PC成功进行交易的问题,所有参与者都必须作出回应。因此,如果系统的任何部分损坏,交易也会失败。因此,分布式事务有扩大故障的趋势,这与我们构建容错系统的目标背道而驰。 +* 对于数据库内部的分布式事务(而不是XA),限制不是很大,例如SSI的分布式版本是可能的。然而,仍然存在2PC成功进行事务的问题,所有参与者都必须作出回应。因此,如果系统的任何部分损坏,事务也会失败。因此,分布式事务有扩大故障的趋势,这与我们构建容错系统的目标背道而驰。 -这些事实是否意味着我们应该放弃保持几个系统一致的所有希望?不完全 - 有其他的方法可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在第十一章和第十二章回到这些章节。但首先,我们应该总结一致的话题。 +这些事实是否意味着我们应该放弃保持几个系统一致的所有希望?不完全是——有其他的方法可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第十一章](ch11.md)和[第十二章](ch12.md)回到这些章节。但首先,我们应该总结共识的话题。 @@ -742,7 +748,9 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 非正式地,共识意味着让几个节点达成一致。例如,如果有几个人同时尝试预订飞机上的最后一个座位或剧院中的同一个座位,或者尝试使用相同的用户名注册一个帐户,则可以使用一个一致的算法来确定哪个其中一个互不相容的行动应该是赢家。 -共识问题通常形式化如下:一个或多个节点可以提出值,并且共识算法决定其中的一个值。在座位预订的例子中,当几个顾客同时试图购买最后一个座位时,处理顾客请求的每个节点可以提出正在服务的顾客的ID,并且决定指示哪个顾客获得座位。在这种形式主义中,共识算法必须满足以下性质【25】:[^xiii] +共识问题通常形式化如下:一个或多个节点可以**提出(propose)**某个值,而共识算法**决定(decides)**采用其中的一个值。在座位预订的例子中,当几个顾客同时试图购买最后一个座位时,处理顾客请求的每个节点可以提出正在服务的顾客的ID,且决定指明了哪个顾客获得座位。 + +在这种形式中,共识算法必须满足以下性质【25】:[^xiii] [^xiii]: 这种共识的特殊形式被称为统一共识,相当于在具有不可靠故障检测器的异步系统中的常规共识【71】。学术文献通常指的是过程而不是节点,但我们在这里使用节点来与本书的其余部分保持一致。 @@ -756,34 +764,34 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 ***有效性*** -​ 如果一个节点决定值`v`,则v由某个节点提出。 +​ 如果一个节点决定值 `v`,则`v`由某个节点提出。 ***终止*** -​ 由每个未崩溃节点来最终决定值。 + 由所有未崩溃的节点来最终决定值。 统一协议和完整性属性定义了共识的核心思想:每个人都决定相同的结果,一旦你决定了,你就不能改变主意。有效性属性主要是为了排除微不足道的解决方案:例如,无论提出什么建议,都可以有一个总是决定为空的算法;该算法将满足协议和完整性属性,但不符合有效性属性。 -如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但是,如果一个节点失败,那么系统就不能再做出任何决定。事实上,这就是我们在两阶段承诺的情况下所看到的:如果协调员失败了,那么不确定的参与者就不能决定是否提交或中止。 +如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但是,如果一个节点失败,那么系统就不能再做出任何决定。事实上,这就是我们在两阶段承诺的情况下所看到的:如果协调者失败了,那么不确定的参与者就不能决定是否提交或中止。 -终止属性正式形成了容错的思想。它基本上说,一个共识算法不能简单地坐下来,永远不要做任何事 - 换句话说,它必须取得进展。即使有些节点出现故障,其他节点也必须做出决定。 (终止是一种活泼的财产,而另外三种是安全属性——参见“[安全性和活性](ch8.md#安全性和活性)”。) +终止属性正式形成了容错的思想。它基本上说,一个共识算法不能简单地坐下来,永远不要做任何事——换句话说,它必须取得进展。即使有些节点出现故障,其他节点也必须做出决定。 (终止是一种**活性属性**,而另外三种是安全属性——参见“[安全性和活性](ch8.md#安全性和活性)”。) -共识的系统模型假设,当一个节点“崩溃”时,它突然消失,永远不会回来。 (而不是软件崩溃,想象一下地震,包含你的节点的数据中心被山体滑坡所摧毁,你必须假设你的节点被埋在30英尺以下的泥土中,并且永远不会回到在线状态。)这个系统模型,任何等待节点恢复的算法都不能满足终止属性。特别是2PC不符合终止的要求。 +共识的系统模型假设,当一个节点“崩溃”时,它突然消失,永远不会回来。(而不是软件崩溃,想象一下地震,包含你的节点的数据中心被山体滑坡所摧毁,你必须假设你的节点被埋在30英尺以下的泥土中,并且永远不会回到在线状态。)这个系统模型,任何等待节点恢复的算法都不能满足终止属性。特别是2PC不符合终止的要求。 -当然,如果所有的节点都崩溃,而且没有一个正在运行,那么任何算法都不可能决定什么。算法可以容忍的失败次数有一个限制:事实上,可以证明任何一致性算法都需要至少大部分节点正确运行,以确保终止【67】。大多数人可以安全地形成法定人数(请参阅第179页上的“读和写的法定人数”)。 +当然,如果所有的节点都崩溃,而且没有一个正在运行,那么任何算法都不可能决定什么。算法可以容忍的失败次数有一个限制:事实上,可以证明任何一致性算法都需要至少大部分节点正确运行,以确保终止【67】。大多数人可以安全地形成法定人数(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。 -因此,终止属性受到不到一半的节点崩溃或不可达的假设。然而,即使大多数节点出现故障或存在严重的网络问题,大多数共识的实施都能确保始终满足安全属性 - 协议,完整性和有效性【92】。因此,大规模的中断可能会阻止系统处理请求,但是它不能通过使系统做出无效的决定来破坏共识系统。 +因此,终止属性受到不到一半的节点崩溃或不可达的假设。然而,即使大多数节点出现故障或存在严重的网络问题,大多数共识的实施都能确保始终满足安全属性——同意,完整性和有效性【92】。因此,大规模的中断可能会阻止系统处理请求,但是它不能通过使系统做出无效的决定来破坏共识系统。 -大多数一致性算法假定没有拜占庭式的错误,正如在“拜占庭式故障”一节中所讨论的那样。也就是说,如果一个节点没有正确地遵循协议(例如,如果它发送矛盾的消息到不同的节点),它可能会破坏协议的安全属性。只要少于三分之一的节点是拜占庭故障【25,93】,就可以对拜占庭故障形成共识,但我们没有空间在本书中详细讨论这些算法。 +大多数一致性算法假定没有拜占庭式的错误,正如在“[拜占庭式错误](#拜占庭式错误)”一节中所讨论的那样。也就是说,如果一个节点没有正确地遵循协议(例如,如果它发送矛盾的消息到不同的节点),它可能会破坏协议的安全属性。只要少于三分之一的节点是拜占庭故障【25,93】,就可以对拜占庭故障形成共识,但我们没有地方在本书中详细讨论这些算法。 -#### 共识算法和总顺序广播 +#### 共识算法和全序广播 -最着名的容错一致性算法是**视图戳复制(viewstamped replication)**(VSR)【94,95】,Paxos 【96,97,98,99】,Raft 【22,100,101】和Zab 【15,21,102】 。这些算法之间有相当多的相似之处,但它们并不相同【103】。在本书中,我们不会详细介绍不同的算法:除非你自己实现一个共识系统(这可能不是一个明智的做法),只要了解一些共同的高级思想就足够了这很难【98,104】)。 +最着名的容错一致性算法是**视图戳复制(viewstamped replication)**(VSR)【94,95】,Paxos 【96,97,98,99】,Raft 【22,100,101】和Zab 【15,21,102】 。这些算法之间有相当多的相似之处,但它们并不相同【103】。在本书中,我们不会详细介绍不同的算法:只要了解一些共同的高级思想就足够了,除非你准备自己实现一个共识系统。(这可能不是一个明智的做法,相当困难【98,104】) -这些算法中的大多数实际上并不直接使用这里描述的形式化模型(建议和决定单个值,同时满足协议,完整性,有效性和终止性质)。相反,他们决定了一系列的值,这使得他们成为了顺序广播算法,正如本章前面所讨论的那样(请参阅第348页上的“全部顺序广播”)。 +这些算法中的大多数实际上并不直接使用这里描述的形式化模型(建议和决定单个值,同时满足协议,完整性,有效性和终止性质)。相反,他们决定了值的顺序,这使得它们成为了全序广播算法,正如本章前面所讨论的那样(参阅“[全序广播](#全序广播)”)。 -请记住,总顺序广播要求将消息按照相同的顺序准确传送到所有节点。如果你仔细想想,这相当于进行了几轮的共识:在每一轮中,节点提出下一个要发送的消息,然后决定下一个要发送的消息总数【67】。 +请记住,全序广播要求将消息按照相同的顺序准确传送到所有节点。如果你仔细想想,这相当于进行了几轮的共识:在每一轮中,节点提出下一个要发送的消息,然后决定下一个要发送的消息总数【67】。 -所以,总的顺序广播相当于重复的一轮共识(每个共同的决定对应于一个消息传递): +所以,全序广播相当于重复的一轮共识(每个共同的决定对应于一个消息传递): * 由于协商一致意见,所有节点决定以相同的顺序传递相同的消息。 @@ -792,28 +800,29 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 * 由于有效性属性,消息不会被破坏,也不是凭空制造的。 * 由于终止属性,消息不会丢失。 -已加密的复制,Raft和Zab直接执行全部命令广播,因为这样做比重复一轮一次一致的共识更有效。在Paxos的情况下,这种优化被称为Multi-Paxos。 +已加密的复制,Raft和Zab直接执行全序广播,因为这样做比重复一轮一次一致的共识更有效。在Paxos的情况下,这种优化被称为Multi-Paxos。 #### 单领导者复制和共识 -在第5章中,我们讨论了单领导者复制(参见第152页的“领导者和追随者”),它将所有的写入操作都交给领导者,并以相同的顺序将他们应用到追随者,从而使复制品保持最新状态。这不是基本上全部命令播放?我们怎么不用担心第五章的共识? +在[第5章](ch5.md)中,我们讨论了单领导者复制(参见“[领导者和追随者](ch5.md#领导者和追随者)”),它将所有的写入操作都交给领导者,并以相同的顺序将他们应用到追随者,从而使副本保持最新状态。这不是基本上全序广播?我们怎么不用担心[第五章](ch5.md)的共识? 答案取决于如何选择领导者。如果领导人是由您的运营团队中的人员手动选择和配置的,那么您基本上拥有独裁种类的“一致性算法”:只允许一个节点接受写入(即,决定写入的顺序复制日志),如果该节点发生故障,则系统将无法写入,直到操作员手动配置其他节点作为主管。这样的制度在实践中可以很好地发挥作用,但是不能达到共识的终止性,因为它需要人为干预才能取得进展。 -一些数据库执行自动领导者选举和故障转移,如果旧领导者失败,则促使追随者成为新的领导者(参见第156页的“处理节点中断”)。这使我们更接近容错的全面命令播出,从而达成共识。 +一些数据库执行自动领导者选举和故障转移,如果旧领导者失败,则促使追随者成为新的领导者(参见第156页的“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们更接近容错的全面命令播出,从而达成共识。 -但是,有一个问题。我们之前曾经讨论过分裂脑的问题,并且说所有的节点都需要同意领导者是谁,否则两个不同的节点都会相信自己是领导者,从而导致数据库进入不一致的状态。因此,我们需要达成共识才能选出一位领导人。但是,如果这里描述的一致性算法实际上是全序广播算法,并且全部命令广播就像单引导复制,单引导复制需要领导,那么... -看来要选一个领导,我们首先需要一个领导。要解决共识,首先要解决共识。我们如何摆脱这个难题? +但是,有一个问题。我们之前曾经讨论过分裂脑的问题,并且说所有的节点都需要同意领导者是谁,否则两个不同的节点都会相信自己是领导者,从而导致数据库进入不一致的状态。因此,我们需要达成共识才能选出一位领导人。但是,如果这里描述的一致性算法实际上是全序广播算法,并且全序广播就像单主复制,单主复制需要领导,那么... + +看来要选出一个领导,我们首先需要一个领导。要解决共识问题,首先要解决共识问题。我们如何摆脱这个难题? #### 时代编号和法定人数 -迄今为止所讨论的所有共识协议在内部都以某种形式使用领导者,但是并不能保证领导者是独一无二的。相反,他们可以做出较弱的保证:协议定义了一个纪元号码(称为Paxos中的选票号码,Viewstamped复制中的视图号码,以及Raft中的术语号码),并确保在每个纪元中领导者是唯一的。 +迄今为止所讨论的所有共识协议在内部都以某种形式使用领导者,但是并不能保证领导者是独一无二的。相反,他们可以做出较弱的保证:协议定义了一个纪元号码(称为Paxos中的选票号码,视图戳复制中的视图号码,以及Raft中的术语号码),并确保在每个纪元中领导者是唯一的。 每当现在的领导被认为是死的时候,就会在节点之间开始投票选出一个新领导。这次选举被赋予了一个递增的时代号码,因此时代号码是完全有序的,单调递增的。如果在两个不同的时代,两个不同的领导者之间有冲突(也许是因为前一个领导者实际上并没有死亡),那么具有更高时代的领导者就占上风了。 -在任何领导人被允许决定任何事情之前,必须首先检查是否没有其他具有较高时代的领导者,这可能会采取相互冲突的决定。领导者如何知道它没有被另一个节点赶下?回想一下第300页的“真理是由多数人定义的”:一个节点不一定相信自己的判断 - 只是因为节点认为它是领导者,并不一定意味着其他节点接受它作为他们的领导者。 +在任何领导人被允许决定任何事情之前,必须首先检查是否没有其他具有较高时代的领导者,这可能会采取相互冲突的决定。领导者如何知道它没有被另一个节点赶下?回想一下第300页的“[真理是由多数所定义](ch8.md#真理是由多数所定义)”:一个节点不一定相信自己的判断——只是因为节点认为它是领导者,并不一定意味着其他节点接受它作为他们的领导者。 -相反,它必须从节点法定人数中收集选票(请参阅第179页上的“读和写的法定人数”)。对于领导者想要做出的每一个决定,都必须将建议值发送给其他节点,并等待法定人数的节点响应提案。法定人数通常但不总是由大部分节点组成【105】。一个节点只有在没有意识到任何具有更高纪元的其他领导者的时候才投票赞成。 +相反,它必须从节点法定人数中收集选票(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对于领导者想要做出的每一个决定,都必须将建议值发送给其他节点,并等待法定人数的节点响应提案。法定人数通常但不总是由大部分节点组成【105】。一个节点只有在没有意识到任何具有更高纪元的其他领导者的时候才投票赞成。 因此,我们有两轮投票:一次是选一位领导人,二是投票领导人的提议。关键的看法是,这两票的法定人数必须重叠:如果一个提案的投票成功,至少有一个投票的节点也必须参加最近的领导人选举【105】。因此,如果一个提案的投票没有显示任何更高的时代,那么现在的领导者就可以得出这样的结论:没有一个更高时代的领袖选举发生了,因此可以确定它仍然是领导。然后它可以安全地决定提出的价值。 @@ -821,13 +830,13 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 #### 共识的局限性 -共识算法对于分布式系统来说是一个巨大的突破:它为具有其他各种不确定性的系统带来了具体的安全属性(一致性,完整性和有效性),而且它们仍然是容错的(只要能够进行处理大多数节点正在工作和可达)。它们提供全部的命令广播,因此它们也可以容错的方式实现线性一致性的原子操作(参见第350页的“使用全部命令广播实现线性一致性存储”)。 +共识算法对于分布式系统来说是一个巨大的突破:它为具有其他各种不确定性的系统带来了具体的安全属性(一致性,完整性和有效性),而且它们仍然是容错的(只要能够进行处理大多数节点正在工作和可达)。它们提供全序广播,因此它们也可以容错的方式实现线性一致性的原子操作(参见“[使用全序广播实现线性一致性存储](#使用全序广播实现线性一致性存储)”)。 尽管如此,它们并没有到处使用,因为它的好处是有代价的。 -节点在决定之前对节点进行投票的过程是一种同步复制。如第153页的“同步与异步复制”中所述,通常将数据库配置为使用异步复制。在这种配置中,一些承诺的数据在故障转移时可能会丢失 - 但是为了获得更好的性能,许多人选择接受这种风险。 +节点在决定之前对节点进行投票的过程是一种同步复制。如“[同步与异步复制](ch5.md#同步与异步复制)”中所述,通常将数据库配置为使用异步复制。在这种配置中,一些承诺的数据在故障转移时可能会丢失——但是为了获得更好的性能,许多人选择接受这种风险。 -共识体系总是需要严格的多数来操作。这意味着您至少需要三个节点才能容忍一个故障(其余三个为大多数),或者至少有五个节点容忍两个故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另请参阅“线性一致性的成本”(第295页))。 +共识体系总是需要严格的多数来操作。这意味着您至少需要三个节点才能容忍一个故障(其余三个为大多数),或者至少有五个节点容忍两个故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另请参阅“[线性一致性的代价](#线性一致性的代价)”(第295页))。 大多数一致性算法假定一组参与投票的节点,这意味着您不能只添加或删除集群中的节点。对共识算法的动态成员扩展允许集群中的节点集随着时间的推移而变化,但是它们比静态成员算法要好得多。 @@ -841,23 +850,23 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 为了理解这一点,简单探讨如何使用像ZooKeeper这样的服务是有帮助的。作为应用程序开发人员,您很少需要直接使用ZooKeeper,因为它实际上不适合作为通用数据库。更有可能的是,通过其他项目间接依赖它,例如HBase,Hadoop YARN,OpenStack Nova和Kafka都依赖ZooKeeper在后台运行。这些项目从中得到什么? -ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据(虽然它们仍然写入磁盘以保持持久性),所以你不希望在这里存储所有的应用程序的数据。使用容错全序广播算法在所有节点上复制少量的数据。正如前面所讨论的那样,全部命令广播就是数据库复制所需要的:如果每条消息代表对数据库的写入,则以相同的顺序应用相同的写入操作可以保持副本之间的一致性。 +ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据(虽然它们仍然写入磁盘以保持持久性),所以你不希望在这里存储所有的应用程序的数据。使用容错全序广播算法在所有节点上复制少量的数据。正如前面所讨论的那样,全序广播就是数据库复制所需要的:如果每条消息代表对数据库的写入,则以相同的顺序应用相同的写入操作可以保持副本之间的一致性。 -ZooKeeper模仿Google的Chubby锁定服务【14,98】,不仅实现了全面的命令广播(因此也实现了共识),而且还构建了一组有趣的其他特性,这些特性在构建分布式系统时变得特别有用: +ZooKeeper模仿Google的Chubby锁定服务【14,98】,不仅实现了全序广播(因此也实现了共识),而且还构建了一组有趣的其他特性,这些特性在构建分布式系统时变得特别有用: ***线性一致性的原子操作*** -使用原子比较和设置操作,可以实现锁定:如果多个节点同时尝试执行相同的操作,则只有其中一个节点会成功。共识协议保证了操作将是原子性和线性一致性的,即使节点发生故障或网络在任何时候都被中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(请参阅第295页上的“进程暂停”)。 +使用原子比较和设置操作,可以实现锁定:如果多个节点同时尝试执行相同的操作,则只有其中一个节点会成功。共识协议保证了操作将是原子性和线性一致性的,即使节点发生故障或网络在任何时候都被中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(请参阅“[进程暂停](ch8.md#进程暂停)”)。 -***操作的总排序*** +***操作的全序*** -如“页首301和锁定”中所述,当某个资源受到锁定或租约的保护时,您需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。击剑标记是每次获得锁定时单调增加的数字。 ZooKeeper通过完全排序所有操作,并为每个操作提供一个单调递增的事务ID(zxid)和版本号(cversion)来提供这个功能【15】。 +如“页首301和锁定”中所述,当某个资源受到锁定或租约的保护时,您需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。击剑标记是每次获得锁定时单调增加的数字。 ZooKeeper通过完全排序所有操作,并为每个操作提供一个单调递增的事务ID(`zxid`)和版本号(`cversion`)来提供这个功能【15】。 -***故障检测*** +***失效检测*** 客户端在ZooKeeper服务器上维护一个长期的会话,客户端和服务器周期性地交换心跳来检查另一个节点是否还活着。即使连接暂时中断,或者ZooKeeper节点失败,会话仍保持活动状态。但是,如果心跳停止持续时间超过会话超时,ZooKeeper会声明该会话已经死亡。当会话超时(ZooKeeper调用这些临时节点)时,会话持有的任何锁都可以配置为自动释放。 -***更改通知*** +***变更通知*** 一个客户端不仅可以读取其他客户端创建的锁和值,还可以监视其中的更改。因此,客户端可以找出另一个客户端何时加入集群(基于它写入ZooKeeper的值),还是另一个客户端发生故障(因为其会话超时并且其临时节点消失)。通过订阅通知,客户避免了不得不经常轮询以找出变化。 @@ -867,15 +876,15 @@ ZooKeeper模仿Google的Chubby锁定服务【14,98】,不仅实现了全面的 #### 将工作分配给节点 -ZooKeeper/Chubby模型运行良好的一个例子是,如果您有几个流程或服务的实例,并且需要选择其中一个实例作为leader或primary。如果领导失败,其他节点之一应该接管。这对于单引导数据库当然是有用的,但对于作业调度程序和类似的有状态系统也是有用的。 +ZooKeeper/Chubby模型运行良好的一个例子是,如果您有几个流程或服务的实例,并且需要选择其中一个实例作为主库或首要。如果领导失败,其他节点之一应该接管。这对于单引导数据库当然是有用的,但对于作业调度程序和类似的有状态系统也是有用的。 -另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式参与者系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入群集时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(请参阅第195页的“重新平衡分区”)。当节点被移除或失败时,其他节点需要接管失败节点的工作。 +另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式参与者系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入群集时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(参阅“[重新平衡分区](ch6.md#重新平衡分区)”)。当节点被移除或失败时,其他节点需要接管失败节点的工作。 这些类型的任务可以通过在ZooKeeper中明智地使用原子操作,各种节点和通知来实现。如果正确完成,这种方法允许应用程序自动从故障中恢复,无需人工干预。尽管Apache Curator 【17】等库已经出现在ZooKeeper客户端API的顶层提供了更高级别的工具,但这样做并不容易,但它仍然比尝试从头开始实现必要的一致性算法要好得多,成绩不佳【107】。 应用程序最初只能在单个节点上运行,但最终可能会增长到数千个节点。试图在如此之多的节点上进行多数选票将是非常低效的。相反,ZooKeeper在固定数量的节点(通常是三到五个)上运行,并在这些节点之间执行其多数票,同时支持潜在的大量客户端。因此,ZooKeeper提供了一种将协调节点(共识,操作排序和故障检测)的一些工作“外包”到外部服务的方式。 -通常,由ZooKeeper管理的数据的类型变化十分缓慢:代表“分区7中的节点运行在10.1.1.23上”的信息可能会在几分钟或几小时的时间内发生变化。它不是用来存储应用程序的运行时状态的,每秒可能会改变数千甚至数百万次。如果应用程序状态需要从一个节点复制到另一个节点,则可以使用其他工具(如Apache BookKeeper 【108】)。 +通常,由ZooKeeper管理的数据的类型变化十分缓慢:代表“分区7中的节点运行在`10.1.1.23`上”的信息可能会在几分钟或几小时的时间内发生变化。它不是用来存储应用程序的运行时状态的,每秒可能会改变数千甚至数百万次。如果应用程序状态需要从一个节点复制到另一个节点,则可以使用其他工具(如Apache BookKeeper 【108】)。 #### 服务发现 @@ -903,11 +912,11 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部 我们还探讨了因果关系,这个因果关系对系统中的事件进行了排序(根据原因和结果发生在什么之前)。与线性一致性不同,线性一致性将所有操作放在单一的完全有序的时间线中,因果性为我们提供了一个较弱的一致性模型:有些东西可以是并发的,所以版本历史就像是一个分支和合并的时间线。因果一致性不具备线性一致性的协调开销,并且对网络问题的敏感性要低得多。 -但是,即使我们捕捉到因果顺序(例如使用Lamport时间戳),我们也看到有些事情不能以这种方式实现:在“时间戳排序不够充分”的第347页中,我们考虑了确保用户名是唯一的,并拒绝同一用户名的并发注册。如果一个节点要接受注册,则需要知道另一个节点不是同时注册相同名称的过程。这个问题导致我们达成共识。 +但是,即使我们捕捉到因果顺序(例如使用兰伯特时间戳),我们也看到有些事情不能以这种方式实现:在“[光有时间戳排序还不够](#光有时间戳排序还不够)”中,我们考虑了确保用户名是唯一的,并拒绝同一用户名的并发注册。如果一个节点要接受注册,则需要知道另一个节点不是同时注册相同名称的过程。这个问题导致我们达成共识。 我们看到,达成共识意味着决定一件事情,使所有节点对所做决定达成一致,从而决定是不可撤销的。通过一些挖掘,事实证明,广泛的问题实际上可以归结为共识,并且彼此是等价的(从这个意义上说,如果你有一个解决方案,你可以很容易地将它转换成解决方案之一其他)。这种等同的问题包括: -***线性一致性的比较和设置寄存器*** +***线性一致性的CAS寄存器*** 寄存器需要基于当前值是否等于操作中给定的参数,自动决定是否设置其值。 @@ -915,7 +924,7 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部 数据库必须决定是否提交或中止分布式事务。 -***全部顺序广播*** +***全序广播*** 消息传递系统必须决定传递消息的顺序。 @@ -923,27 +932,27 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部 当几个客户争抢锁或租约时,锁决定哪个客户成功获得。 -***会员/协调服务*** +***成员/协调服务*** 给定故障检测器(例如,超时),系统必须决定哪些节点处于活动状态,哪些应该被认为是死的,因为它们的会话超时。 ***唯一性约束*** -当多个事务同时尝试使用相同的密钥创建冲突记录时,约束必须决定哪一个允许,哪个会违反约束而失败。 +当多个事务同时尝试使用相同的键创建冲突记录时,约束必须决定哪一个允许,哪个会违反约束而失败。 如果您只有一个节点,或者您愿意将决策功能分配给单个节点,所有这些都很简单。这就是在一个单独的领导者数据库中发生的事情:决策的所有权力归属于领导者,这就是为什么这样的数据库能够提供线性一致性操作,唯一性约束,完全有序的复制日志等等。 但是,如果单个领导失败,或者如果网络中断导致领导不可达,则这样的系统变得无法取得进展。处理这种情况有三种方法: -1. 等待领导者恢复,同时接受系统将被阻止。许多XA/JTA事务协调员选择这个选项。这种方法并不能完全解决共识,因为它不能满足终止财产的要求:如果领导者没有恢复,系统可以被永久封锁。 -2. 通过让人类选择一个新的领导者节点并重新配置系统来使用它来手动故障切换。许多关系数据库都采用这种方法。这是一种“上帝的行为”的共识 - 计算机系统之外的操作人员做出决定。故障转移的速度受到人类行动速度的限制,通常比计算机慢。 +1. 等待领导者恢复,同时接受系统将被阻止。许多XA/JTA事务协调者选择这个选项。这种方法并不能完全解决共识,因为它不能满足终止财产的要求:如果领导者没有恢复,系统可以被永久封锁。 +2. 通过让人类选择一个新的领导者节点并重新配置系统来使用它来手动故障切换。许多关系数据库都采用这种方法。这是一种“上帝的行为”的共识——计算机系统之外的操作人员做出决定。故障转移的速度受到人类行动速度的限制,通常比计算机慢。 3. 使用算法自动选择一个新的领导。这种方法需要一个一致的算法,建议使用经过验证的算法来正确处理不利的网络条件【107】。 尽管一个单独的领导者数据库可以提供线性一致性,而不需要在每个写作上执行一致的算法,但是仍然需要达成共识以保持领导力和领导力的改变。因此,从某种意义上说,有一个领导者只是“把罐子放在路上”:共识还是需要的,只是在一个不同的地方,而不是频繁的。好消息是,容错算法和共识系统存在,我们在本章中简要地讨论它们。 像ZooKeeper这样的工具在提供应用程序可以使用的“外包”协议,故障检测和会员服务方面起着重要的作用。使用起来并不容易,但比开发自己的算法要好得多,可以承受第8章讨论的所有问题。如果你发现自己想要做一个可以归结为一致的东西,而且你想要它要容错,那么建议使用类似ZooKeeper的东西。 -尽管如此,并不是每个系统都需要达成共识:例如,无领导者和多领导者复制系统通常不会使用全球共识。这些系统中出现的冲突(参见第171页的“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间达成共识的结果,但也许没关系:也许我们只需要处理没有线性一致性的东西,学会更好地工作具有分支和合并版本历史记录的数据。 +尽管如此,并不是每个系统都需要达成共识:例如,无领导者和多领导者复制系统通常不会使用全局共识。这些系统中出现的冲突(参见“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间达成共识的结果,但也许没关系:也许我们只需要处理没有线性一致性的东西,学会更好地工作具有分支和合并版本历史记录的数据。 本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于通知这一领域的实际工作是非常有价值的:它们帮助我们推理什么可以做,不可以做什么,帮助我们找到违反直觉的方法其中分布式系统往往是有缺陷的。如果你有时间,这些参考资料是值得探索的。 From 0bcb50c2f8b3e9b15ac6dc320be3d796308bbd13 Mon Sep 17 00:00:00 2001 From: Vonng Date: Mon, 26 Mar 2018 01:06:00 +0800 Subject: [PATCH 02/17] ch9 65% --- README.md | 13 ++-- ch9.md | 223 +++++++++++++++++++++++++++--------------------------- 2 files changed, 119 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index d318b82..c1574b4 100644 --- a/README.md +++ b/README.md @@ -106,18 +106,15 @@ | 第六章:分区 | 初翻 | | | 第七章:事务 | 精翻 60% | Vonng | | 第八章:分布式系统中的问题 | 初翻 | | -| 第九章:一致性与共识 | 初翻 30% | Vonng | +| 第九章:一致性与共识 | 初翻 65% | Vonng | | 第三部分:前言 | 精翻 | | -| 第十章:批处理 | 机翻 | 于鑫 | -| 第十一章:流处理 | 机翻 | 于鑫 | +| 第十章:批处理 | 机翻 | | +| 第十一章:流处理 | 机翻 | | | 第十二章:数据系统的未来 | 机翻 | | | 术语表 | - | | | 后记 | 机翻 | | -计划在3月内完成所有章节的初翻。 - - ## CONTRIBUTION @@ -129,7 +126,9 @@ All contribution will give proper credit. 贡献者需要同意[法律声明](# 1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird) 2. [第一章语法标点校正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree) -3. [第六章第一部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) by @[MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex) +3. [第六章部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) by @[MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex) + + diff --git a/ch9.md b/ch9.md index 5b86458..8eac16c 100644 --- a/ch9.md +++ b/ch9.md @@ -82,7 +82,7 @@ 为了简单起见,[图9-2](img/fig9-2.png)采用了用户请求的视角,而不是数据库内部的视角。每个柱都是由客户端发出的请求,其中柱头是请求发送的时刻,柱尾是客户端收到响应的时刻。因为网络延迟变化无常,客户端不知道数据库处理其请求的精确时间——只知道它发生在发送请求和接收响应的之间的某个时刻。[^i] -[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。即使真实的系统通常没有准确的时钟(请参阅“[不可靠的时钟](ch8.md#不可靠的时钟)”),但这种假设是允许的:为了分析分布式算法,我们可以假设一个精确的全局时钟存在,不过算法无法访问它【47】。算法只能看到由石英振荡器和NTP产生的实时逼近。 +[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。即使真实的系统通常没有准确的时钟(参阅“[不可靠的时钟](ch8.md#不可靠的时钟)”),但这种假设是允许的:为了分析分布式算法,我们可以假设一个精确的全局时钟存在,不过算法无法访问它【47】。算法只能看到由石英振荡器和NTP产生的实时逼近。 在这个例子中,寄存器有两种类型的操作: @@ -101,14 +101,14 @@ [^ii]: 如果读取(与写入同时发生时)可能返回旧值或新值,则称该寄存器为**常规寄存器(regular register)**【7,25】 -为了使系统线性一致,我们需要添加另一个约束,如**图9-3**所示 +为了使系统线性一致,我们需要添加另一个约束,如[图9-3](img/fig9-3.png)所示 ![](img/fig9-3.png) **图9-3 任何一个读取返回新值后,所有后续读取(在相同或其他客户端上)也必须返回新值。** -在一个线性一致的系统中,我们可以想象,在x的值自动翻转从0到1的时候(在写操作的开始和结束之间)必定有一个时间点。因此,如果一个客户端的读取返回新的值1,即使写操作尚未完成,所有后续读取也必须返回新值。 +在一个线性一致的系统中,我们可以想象,在`x`的值自动翻转从`0`到`1`的时候(在写操作的开始和结束之间)必定有一个时间点。因此,如果一个客户端的读取返回新的值`1`,即使写操作尚未完成,所有后续读取也必须返回新值。 -[图9-3](img/fig9-3.png)中的箭头说明了这个时序依赖关系。客户端A是第一个读取新的值1的位置。在A的读取返回之后,B开始新的读取。由于B的读取严格在发生于A的读取之后,因此即使C的写入仍在进行中,也必须返回1。 (与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同:在Alice读取新值之后,Bob也希望读取新的值。) +[图9-3](img/fig9-3.png)中的箭头说明了这个时序依赖关系。客户端A是第一个读取新的值`1`的位置。在A的读取返回之后,B开始新的读取。由于B的读取严格在发生于A的读取之后,因此即使C的写入仍在进行中,也必须返回`1`。 (与[图9-1](img/fig9-1.png)中的Alice和Bob的情况相同:在Alice读取新值之后,Bob也希望读取新的值。) 我们可以进一步细化这个时序图,展示每个操作是如何在特定时刻原子性生效的。[图9-4](img/fig9-4.png)显示了一个更复杂的例子【10】。 @@ -148,7 +148,7 @@ > > ***线性一致性*** > -> **线性一致性(Linearizability)**是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写偏差等问题(请参阅“[写偏差和幻读](ch7.md#写偏差和幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。 +> **线性一致性(Linearizability)**是读取和写入寄存器(单个对象)的**新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写偏差等问题(参阅“[写偏差和幻读](ch7.md#写偏差和幻读)”),除非采取其他措施(例如[物化冲突](ch7.md#物化冲突))。 > > 一个数据库可以提供可串行性和线性一致性,这种组合被称为严格的可串行性或强的**单副本强可串行性(strong-1SR)**【4,13】。基于两阶段锁定的可串行化实现(参见“[两阶段锁定(2PL)](#两阶段锁定(2PL))”一节)或**实际串行执行**(参见第“[实际串行执行](ch7.md#实际串行执行)”)通常是线性一致性的。 > @@ -164,9 +164,9 @@ 一个使用单主复制的系统,需要确保领导真的只有一个,而不是几个(脑裂)。一种选择领导者的方法是使用锁:每个节点在启动时尝试获取锁,成功者成为领导者【14】。不管这个锁是如何实现的,它必须是线性一致的:所有节点必须就哪个节点拥有锁达成一致,否则就没用了。 -诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法,以容错的方式实现线性一致的操作(在本章后面的“[容错共识](#容错共识)”中讨论此类算法)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选择(例如,请参阅第301页上的“[领导者和锁](#领导者和锁)”中的屏蔽问题),而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。 +诸如Apache ZooKeeper 【15】和etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法,以容错的方式实现线性一致的操作(在本章后面的“[容错共识](#容错共识)”中讨论此类算法)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选择(例如,参阅“[领导者和锁](#领导者和锁)”中的屏蔽问题),而像Apache Curator 【17】这样的库则通过在ZooKeeper之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。 -[^iii]: 严格地说,ZooKeeper和etcd提供线性一致性的写操作,但读取可能是陈旧的,因为默认情况下,它们可以由任何一个副本服务。您可以选择请求线性一致性读取:etcd调用这个法定读取【16】,而在ZooKeeper中,您需要在读取【15】之前调用`sync()`。请参阅第350页上的“[使用全局顺序广播实现线性存储](#使用全局顺序广播实现线性存储)”。 +[^iii]: 严格地说,ZooKeeper和etcd提供线性一致性的写操作,但读取可能是陈旧的,因为默认情况下,它们可以由任何一个副本服务。你可以选择请求线性一致性读取:etcd调用这个法定读取【16】,而在ZooKeeper中,你需要在读取【15】之前调用`sync()`。参阅“[使用全局顺序广播实现线性存储](#使用全局顺序广播实现线性存储)”。 分布式锁也在一些分布式数据库(如Oracle Real Application Clusters(RAC)【18】)中以更细的粒度使用。 RAC对每个磁盘页面使用一个锁,多个节点共享对同一个磁盘存储系统的访问权限。由于这些线性一致的锁处于事务执行的关键路径上,RAC部署通常具有用于数据库节点之间通信的专用集群互连网络。 @@ -178,7 +178,7 @@ 如果想要确保银行账户余额永远不会为负数,或者不会出售比仓库里的库存更多的物品,或者两个人不会都预定了航班或剧院里同一时间的同一个位置。这些约束条件都要求所有节点都同意一个最新的值(账户余额,库存水平,座位占用率)。 -在实际应用中,处理这些限制有时是可以接受的(例如,如果航班超额预订,您可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性一致性,我们将在第524页的“[及时性和完整性]()”中讨论这种松散解释的约束。 +在实际应用中,处理这些限制有时是可以接受的(例如,如果航班超额预订,你可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性一致性,我们将在第524页的“[及时性和完整性]()”中讨论这种松散解释的约束。 然而,一个硬性的唯一性约束(关系型数据库中常见的那种)需要线性一致性。其他类型的约束,如外键或属性约束,可以在不需要线性一致性的情况下实现【19】。 @@ -188,7 +188,7 @@ 计算机系统也会出现类似的情况。例如,假设有一个网站,用户可以上传照片,一个后台进程会调整照片大小,降低分辨率以加快下载速度(缩略图)。该系统的架构和数据流如[图9-5](img/fig9-5.png)所示。 -图像缩放器需要明确的指令来执行尺寸缩放作业,指令是Web服务器通过消息队列发送的(请参阅[第11章](ch11.md))。 Web服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将缩放器的指令放入消息队列。 +图像缩放器需要明确的指令来执行尺寸缩放作业,指令是Web服务器通过消息队列发送的(参阅[第11章](ch11.md))。 Web服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将缩放器的指令放入消息队列。 ![](img/fig9-5.png) **图9-5 Web服务器和图像调整器通过文件存储和消息队列进行通信,打开竞争条件的可能性。** @@ -240,7 +240,7 @@ 仲裁条件满足($w + r> n$),但是这个执行是非线性一致的:B的请求在A的请求完成后开始,但是B返回旧值,而A返回新值。 (又一次,如同Alice和Bob的例子 [图9-1]()) -有趣的是,通过牺牲性能,可以使Dynamo风格的法定人数线性化:读取者必须在将结果返回给应用程序之前,同步执行读取修复(请参阅“[读时修复与反熵过程](ch5.md#读时修复与反熵过程)”) ,并且写入者必须在发送写入之前,读取法定数量节点的最新状态【24,25】。然而,由于性能损失,Riak不执行同步读取修复【26】。 Cassandra在进行法定人数读取时,**确实**在等待读取修复完成【27】;但是由于使用了最后写入为准的冲突解决方案,当同一个键有多个并发写入时,将不能保证线性一致性。 +有趣的是,通过牺牲性能,可以使Dynamo风格的法定人数线性化:读取者必须在将结果返回给应用程序之前,同步执行读取修复(参阅“[读时修复与反熵过程](ch5.md#读时修复与反熵过程)”) ,并且写入者必须在发送写入之前,读取法定数量节点的最新状态【24,25】。然而,由于性能损失,Riak不执行同步读取修复【26】。 Cassandra在进行法定人数读取时,**确实**在等待读取修复完成【27】;但是由于使用了最后写入为准的冲突解决方案,当同一个键有多个并发写入时,将不能保证线性一致性。 而且,这种方式只能实现线性一致的读写;不能实现线性一致的比较和设置操作,因为它需要一个共识算法【28】。 @@ -378,13 +378,13 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 我们不会在这里讨论非线性系统如何保证因果一致性的细节,而只是简要地探讨一些关键的思想。 -为了保持因果性,您需要知道哪个操作发生在哪个其他操作之前(**happened before**)。这是一个偏序:并发操作可以以任意顺序进行,但如果一个操作发生在另一个操作之前,那它们必须在所有副本上以那个顺序被处理。因此,当一个副本处理一个操作时,它必须确保所有因果前驱的操作(之前发生的所有操作)已经被处理;如果前面的某个操作丢失了,后面的操作必须等待,直到前面的操作被处理完毕。 +为了保持因果性,你需要知道哪个操作发生在哪个其他操作之前(**happened before**)。这是一个偏序:并发操作可以以任意顺序进行,但如果一个操作发生在另一个操作之前,那它们必须在所有副本上以那个顺序被处理。因此,当一个副本处理一个操作时,它必须确保所有因果前驱的操作(之前发生的所有操作)已经被处理;如果前面的某个操作丢失了,后面的操作必须等待,直到前面的操作被处理完毕。 为了确定因果依赖关系,我们需要一些方法来描述系统中节点的“知识”。如果节点在发出写入Y时已经看到X值,则X和Y可能是因果关系的。这个分析使用了你在欺诈指控的刑事调查中所期望的那些问题:CEO在做出决定时是否知道X? 在其他操作之前确定哪些操作发生的技术与我们在“[检测并发写入](ch5.md#检测并发写入)”中所讨论的内容类似。该节讨论无领导者数据存储区中的因果关系,我们需要检测到同一个关键字为了防止丢失更新。因果关系更进一步:它需要跟踪整个数据库的因果关系,而不仅仅是一个关键。版本向量可以被推广到做这个【54】。 -为了确定因果顺序,数据库需要知道应用程序读取哪个版本的数据。这就是为什么在图5-13中,来自先前操作的版本号在写入时被传回到数据库的原因。在SSI的冲突检测中会出现类似的想法,如“[可序列化的快照隔离(SSI)]()”中所述:当事务要提交时,数据库将检查它读取的数据版本是否仍然运行至今。为此,数据库跟踪哪个数据已经被哪个事务读取。 +为了确定因果顺序,数据库需要知道应用程序读取哪个版本的数据。这就是为什么在[图5-13](img/fig5-13.png)中,来自先前操作的版本号在写入时被传回到数据库的原因。在SSI的冲突检测中会出现类似的想法,如“[可序列化的快照隔离(SSI)]()”中所述:当事务要提交时,数据库将检查它读取的数据版本是否仍然运行至今。为此,数据库跟踪哪个数据已经被哪个事务读取。 @@ -398,17 +398,17 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 特别是,我们可以按照与因果关系一致的顺序创建序列号[^vii]:我们保证,如果操作A因果关系发生在B之前,那么A在总顺序之前发生在B之前(A具有比B更小的序列号)。并行操作可以任意命令。这样一个总的秩序捕获所有的因果信息,但也强加了比由于因果关系所严格要求的更多的秩序。 -[^vii]: 与因果关系不一致的整个顺序很容易创建,但不是很有用。例如,您可以为每个操作生成随机UUID,并按照字典顺序比较UUID以定义操作的总顺序。这是一个有效的总顺序,但是随机的UUID并不告诉你哪个操作首先实际发生,或者操作是否是并发的。 +[^vii]: 与因果关系不一致的整个顺序很容易创建,但不是很有用。例如,你可以为每个操作生成随机UUID,并按照字典顺序比较UUID以定义操作的总顺序。这是一个有效的总顺序,但是随机的UUID并不告诉你哪个操作首先实际发生,或者操作是否是并发的。 在单主复制的数据库中(请参见“[领导者与追随者](ch5.md#领导者与追随者)”),复制日志定义了与因果关系一致的写操作总顺序。领导者可以简单地为每个操作增加一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个追随者按照他们在复制日志中出现的顺序来应用写入,那么追随者的状态始终是因果一致的(即使它落后于领导者)。 #### 非因果序列号发生器 -如果没有一个领导者(可能是因为您使用的是多领导者或无领导者的数据库,或者是因为数据库是分区的),那么如何为操作生成序列号还不太清楚。实践中使用了各种方法: +如果没有一个领导者(可能是因为你使用的是多领导者或无领导者的数据库,或者是因为数据库是分区的),那么如何为操作生成序列号还不太清楚。实践中使用了各种方法: * 每个节点都可以生成自己独立的一组序列号。例如,如果有两个节点,一个节点只能生成奇数,而另一个节点只能生成偶数。通常,可以在序列号的二进制表示中保留一些位以包含唯一的节点标识符,这将确保两个不同的节点永远不会生成相同的序列号。 -* 您可以将时间戳从时钟(物理时钟)附加到每个操作【55】。这样的时间戳是不连续的,但是如果它们具有足够高的分辨率,那么它们可能足以完成命令操作。这个事实用于最后写入为准的冲突解决方法(参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。 -* 您可以预先分配序列号的块。例如,节点A可能要求从1到1,000的序列号的块,并且节点B可能要求该区块从1,001到2,000。然后,每个节点可以独立分配其块的序列号,并在序列号的提供开始变低时分配一个新的块。 +* 你可以将时间戳从时钟(物理时钟)附加到每个操作【55】。这样的时间戳是不连续的,但是如果它们具有足够高的分辨率,那么它们可能足以完成命令操作。这个事实用于最后写入为准的冲突解决方法(参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。 +* 你可以预先分配序列号的块。例如,节点A可能要求从1到1,000的序列号的块,并且节点B可能要求该区块从1,001到2,000。然后,每个节点可以独立分配其块的序列号,并在序列号的提供开始变低时分配一个新的块。 这三个选项都比单独的领导者增加一个计数器的表现更好,并且更具可扩展性。它们为每个操作生成一个唯一的,大约增加的序列号。然而,他们都有一个问题:他们产生的序列号与因果关系不一致。 @@ -435,11 +435,11 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 **图9-8 Lamport时间戳提供了与因果关系一致的总排序。** -兰伯特时间戳与物理时间时钟没有任何关系,但是它提供了总计次数:如果您有两个时间戳,则计数器值较大的时间戳是较大的时间戳。如果计数器值相同,则节点ID越大的时间戳越大。 +兰伯特时间戳与物理时间时钟没有任何关系,但是它提供了总计次数:如果你有两个时间戳,则计数器值较大的时间戳是较大的时间戳。如果计数器值相同,则节点ID越大的时间戳越大。 到目前为止,这个描述与上一节描述的偶数/奇数计数器基本相同。关于兰伯特时间戳的关键思想,使它们与因果关系一致,如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大计数器值,并在每个请求中包含最大计数器值。当一个节点接收到一个最大计数器值大于其自身计数器值的请求或响应时,它立即增加自己的计数器到最大值。 -这如图9-8所示,其中客户端A从节点2接收计数器值5,然后将最大值5发送到节点1.此时,节点1的计数器仅为1,但是它立即向前移动到5,所以下一个操作的计数器值增加了6。 +这如[图9-8](img/fig9-8.png)所示,其中客户端A从节点2接收计数器值5,然后将最大值5发送到节点1.此时,节点1的计数器仅为1,但是它立即向前移动到5,所以下一个操作的计数器值增加了6。 只要最大计数器值与每一个操作一起进行,这个方案确保兰伯特时间戳的排序与因果性一致,因为每个因果关系导致时间戳增加。 @@ -455,11 +455,11 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 这种方法适用于事后确定胜利者:一旦收集了系统中的所有用户名创建操作,就可以比较他们的时间戳。然而,当一个节点刚刚收到用户的一个请求来创建一个用户名,并且现在需要决定这个请求是成功还是失败,这是不够的。此时,节点不知道另一个节点是否正在同时创建具有相同用户名的帐户,以及其他节点可以分配给该操作的时间戳。 -为了确保没有其他节点正在使用相同的用户名和较低的时间戳同时创建一个帐户,您必须检查每个节点,看看它在做什么【56】。如果其中一个节点由于网络问题而出现故障或无法到达,则该系统将停止工作。这不是我们需要的那种容错系统。 +为了确保没有其他节点正在使用相同的用户名和较低的时间戳同时创建一个帐户,你必须检查每个节点,看看它在做什么【56】。如果其中一个节点由于网络问题而出现故障或无法到达,则该系统将停止工作。这不是我们需要的那种容错系统。 这里的问题是,只有在收集了所有的操作之后,操作的总顺序才会出现。如果另一个节点已经产生了一些操作,但是你还不知道它们是什么,那么就不能构造最终的操作顺序:来自另一个节点的未知操作可能需要被插入到总数的不同位置订购。 -总之:为了实现像用户名的唯一性约束这样的事情,仅仅对操作进行全面的排序是不够的,您还需要知道该命令何时完成。如果您有创建用户名的操作,并且您确定没有其他节点可以在您的操作之前为全部顺序插入相同用户名的声明,则可以安全地声明操作成功。 +总之:为了实现像用户名的唯一性约束这样的事情,仅仅对操作进行全面的排序是不够的,你还需要知道该命令何时完成。如果你有创建用户名的操作,并且你确定没有其他节点可以在你的操作之前为全部顺序插入相同用户名的声明,则可以安全地声明操作成功。 这个知道什么时候你的总顺序被完成的概念被记录在全序广播的话题中。 @@ -491,7 +491,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 像ZooKeeper和etcd这样的共识服务实际上是实现全面的顺序播放。这个事实暗示了整个命令广播和共识之间有着密切的联系,我们将在本章后面进行探讨。 -全序广播正是您所需的数据库复制:如果每封邮件都表示写入数据库,并且每个副本按相同的顺序处理相同的写入,则副本将保持一致(除了临时复制滞后)。这个原则被称为状态机复制【60】,我们将在[第11章](ch11.md)中回到它。 +全序广播正是你所需的数据库复制:如果每封邮件都表示写入数据库,并且每个副本按相同的顺序处理相同的写入,则副本将保持一致(除了临时复制滞后)。这个原则被称为状态机复制【60】,我们将在[第11章](ch11.md)中回到它。 类似地,可以使用全序广播来实现可序列化的事务:如“[真的串行执行](ch7.md#真的串行执行)”中所述,如果每个消息表示一个确定性事务作为存储过程来执行,并且每个节点都处理这些消息相同的顺序,那么数据库的分区和副本保持一致【61】。 全序广播的一个重要方面是顺序在交付消息时是固定的:如果后续消息已经交付,节点不允许追溯地将消息插入顺序中的较早位置。这个事实使得全序广播比时间戳命令更强。 @@ -508,25 +508,25 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 全部顺序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息何时被传送(所以一个接收者可能落后于其他接收者)。相比之下,线性一致性是最近的保证:读取保证看到写入的最新值。 -但是,如果您有全序广播,则可以在其上构建线性一致性存储。例如,您可以确保用户名唯一标识用户帐户。 +但是,如果你有全序广播,则可以在其上构建线性一致性存储。例如,你可以确保用户名唯一标识用户帐户。 想象一下,对于每一个可能的用户名,你都可以拥有一个带有原子比较和设置操作的线性一致性寄存器。每个寄存器最初的值为空值(表示不使用用户名)。当用户想要创建一个用户名时,对该用户名的注册表执行比较设置操作,在前一个注册值为空的情况下,将其设置为用户账号。如果多个用户试图同时获取相同的用户名,则只有一个比较和设置操作会成功,因为其他用户将看到非空值(由于线性一致性)。 -您可以通过使用全序广播作为仅追加日志【62,63】来执行如下线性一致性的比较和设置操作: +你可以通过使用全序广播作为仅追加日志【62,63】来执行如下线性一致性的比较和设置操作: -1. 在日志中添加一条消息,暂时指明您要声明的用户名。 +1. 在日志中添加一条消息,暂时指明你要声明的用户名。 2. 阅读日志,并等待你附加的信息被传回给你。[^xi] 3. 检查是否有任何消息声称你想要的用户名。如果所需用户名的第一条消息是你自己的消息,那么你是成功的:你可以提交用户名声明(也许通过附加另一条消息到日志)并确认给客户端。如果所需用户名的第一条消息来自其他用户,则中止操作。 -[^xi]: 如果您不等待,但是在入队之后立即确认写入,则会得到类似于多核x86处理器的内存一致性模型【43】。 该模型既不是线性的也不是连续的。 +[^xi]: 如果你不等待,但是在入队之后立即确认写入,则会得到类似于多核x86处理器的内存一致性模型【43】。 该模型既不是线性的也不是连续的。 由于日志条目以相同顺序传递到所有节点,因此如果有多个并发写入,则所有节点将首先同意哪个节点。选择第一个冲突的写入作为胜利者,并中止后面的写入,确保所有节点都同意写入是提交还是中止。一个类似的方法可以用来在一个日志之上实现可序列化的多对象事务【62】。 -虽然此过程确保线性写入,但不能保证线性一致性读取 - 如果您从与日志异步更新的存储中读取数据,则可能是陈旧的。 (具体来说,这里描述的过程提供了顺序一致性【47,64】,有时也称为时间线一致性【65,66】,这比线性一致性要弱一些)。为了使读取线性一致,有几个选项: +虽然此过程确保线性写入,但不能保证线性一致性读取 - 如果你从与日志异步更新的存储中读取数据,则可能是陈旧的。 (具体来说,这里描述的过程提供了顺序一致性【47,64】,有时也称为时间线一致性【65,66】,这比线性一致性要弱一些)。为了使读取线性一致,有几个选项: -* 您可以通过附加消息,读取日志以及在消息被传回给您时执行实际读取来对日志进行排序。消息在日志中的位置因此定义了读取发生的时间点。 (法定读取etcd的工作有点像这样【16】。) -* 如果日志允许以线性方式获取最新日志消息的位置,则可以查询该位置,等待直到该位置的所有条目传送给您,然后执行读取。 (这是Zookeeper的`sync()`操作背后的思想【15】)。 -* 您可以从写入时同步更新的副本进行读取,因此可以确保最新。 (这种技术用于链式复制【63】;另请参阅第155页上的“复制研究”。) +* 你可以通过附加消息,读取日志以及在消息被传回给你时执行实际读取来对日志进行排序。消息在日志中的位置因此定义了读取发生的时间点。 (法定读取etcd的工作有点像这样【16】。) +* 如果日志允许以线性方式获取最新日志消息的位置,则可以查询该位置,等待直到该位置的所有条目传送给你,然后执行读取。 (这是Zookeeper的`sync()`操作背后的思想【15】)。 +* 你可以从写入时同步更新的副本进行读取,因此可以确保最新。 (这种技术用于链式复制【63】;参阅“[复制研究](ch5.md#设置新从库)”。) #### 使用线性一致性存储实现全序广播 @@ -534,9 +534,9 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 最简单的方法是假设你有一个线性一致性的寄存器来存储一个整数,并且有一个原子增量和获取操作【28】。或者,原子比较和设置操作也可以完成这项工作。 -该算法很简单:对于每个要通过全部顺序广播发送的消息,您将递增并获取线性一致性的整数,然后将从寄存器获得的值作为序号附加到消息中。然后,您可以将消息发送到所有节点(重新发送任何丢失的消息),并且收件人将按序号连续发送消息。 +该算法很简单:对于每个要通过全部顺序广播发送的消息,你将递增并获取线性一致性的整数,然后将从寄存器获得的值作为序号附加到消息中。然后,你可以将消息发送到所有节点(重新发送任何丢失的消息),并且收件人将按序号连续发送消息。 -请注意,与兰伯特时间戳不同,您通过递增线性一致性寄存器获得的数字形成一个没有间隙的序列。因此,如果一个节点已经发送了消息4并且接收到序列号为6的传入消息,则它知道它在传递消息6之前必须等待消息5.同样的情况并非如此 +请注意,与兰伯特时间戳不同,你通过递增线性一致性寄存器获得的数字形成一个没有间隙的序列。因此,如果一个节点已经发送了消息4并且接收到序列号为6的传入消息,则它知道它在传递消息6之前必须等待消息5.同样的情况并非如此 与兰伯特时间戳——事实上,这是全序广播和时间戳顺序之间的关键区别。 @@ -569,13 +569,13 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 > ### 共识的不可能性 > -> 您可能已经听说过作者Fischer,Lynch和Paterson之后的FLP结果【68】,这证明如果存在节点可能崩溃的风险,则不存在总是能够达成一致的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而,在这里,我们正在讨论达成共识的算法。这里发生了什么? +> 你可能已经听说过作者Fischer,Lynch和Paterson之后的FLP结果【68】,这证明如果存在节点可能崩溃的风险,则不存在总是能够达成一致的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而,在这里,我们正在讨论达成共识的算法。这里发生了什么? > -> 答案是FLP结果在异步系统模型中得到了证明(请参阅“系统模型与现实”在本部分),这是一个非常有限的模型,它假定确定性算法不能使用任何时钟或超时。如果算法被允许使用超时或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),那么共识就变得可以解决了【67】。即使只允许算法使用随机数也足以绕过不可能的结果【69】。 +> 答案是FLP结果在异步系统模型中得到了证明(参阅“[系统模型与现实](#系统模型与现实)”),这是一个非常有限的模型,它假定确定性算法不能使用任何时钟或超时。如果算法被允许使用超时或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),那么共识就变得可以解决了【67】。即使只允许算法使用随机数也足以绕过不可能的结果【69】。 > > 因此,FLP虽然不可能达成共识,但理论上具有重要意义,但实际上分布式系统通常可以达成共识。 -在本节中,我们将首先更详细地检查原子提交问题。具体来说,我们将讨论两阶段提交(2PC)算法,这是解决原子提交最常见的方法,并在各种数据库,消息传递系统和应用服务器中实现。事实证明,2PC是一种一致的算法,但并不是一个很好的算法【70,71】。 +在本节中,我们将首先更详细地检查原子提交问题。具体来说,我们将讨论两阶段提交(2PC)算法,这是解决原子提交最常见的方法,并在各种数据库、消息传递系统和应用服务器中实现。事实证明,2PC是一种一致的算法,但并不是一个很好的算法【70,71】。 通过对2PC学习,我们将继续努力实现更好的一致性算法,比如ZooKeeper(Zab)和etcd(Raft)中使用的算法。 @@ -585,11 +585,11 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 在[第7章](ch7.md)中,我们了解到事务原子性的目的是在出现几次写错的情况下提供简单的语义。事务的结果要么是成功的提交,在这种情况下,所有事务的写入都是持久的,或者中止,在这种情况下,所有事务的写入都被回滚(即撤消或丢弃)。 -原子性可以防止失败的事务搅乱数据库,其结果是半成品和半更新状态。这对于多对象事务(请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护二级索引的数据库尤其重要。每个辅助索引都是与主数据分离的数据结构——因此,如果您修改了一些数据,则还需要在辅助索引中进行相应的更改。原子性确保二级索引与主数据保持一致(如果索引与主数据不一致,则不会很有用)。 +原子性可以防止失败的事务搅乱数据库,其结果是半成品和半更新状态。这对于多对象事务(参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护二级索引的数据库尤其重要。每个辅助索引都是与主数据分离的数据结构——因此,如果你修改了一些数据,则还需要在辅助索引中进行相应的更改。原子性确保二级索引与主数据保持一致(如果索引与主数据不一致,则不会很有用)。 #### 从单节点到分布式原子提交 -对于在单个数据库节点执行的事务,原子性通常由存储引擎执行。当客户端请求数据库节点提交事务时,数据库使事务的写入持久化(通常在预写式日志中:参阅“[使B树可靠](ch3.md#使B树可靠)”),然后将提交记录追加到日志中磁盘。如果数据库在这个过程中间崩溃,当节点重新启动时,事务从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;如果不是,则来自该事务的任何写入都被回滚。 +对于在单个数据库节点执行的事务,原子性通常由存储引擎执行。当客户端请求数据库节点提交事务时,数据库使事务的写入持久化(通常在预写式日志中:参阅“[使B树可靠](ch3.md#使B树可靠)”),然后将提交记录追加到日志中磁盘。如果数据库在这个过程中间崩溃,当节点重新启动时,事务从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;否则来自该事务的任何写入都被回滚。 因此,在单个节点上,事务承诺主要取决于数据持久写入磁盘的顺序:首先是数据,然后是提交记录【72】。事务提交或放弃的关键决定时刻是磁盘完成写入提交记录的时刻:在此之前,仍有可能中止(由于崩溃),但在此之后,事务已经提交(即使数据库崩溃)。因此,这是一个单一的设备(一个特定的磁盘驱动器的控制器,连接到一个特定的节点),使得提交具有原子性。 @@ -601,17 +601,17 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 * 某些提交请求可能在网络中丢失,最终由于超时而中止,而其他提交请求则通过。 * 在提交记录完全写入之前,某些节点可能会崩溃,并在恢复时回滚,而其他节点则成功提交。 -如果某些节点提交了事务,但其他节点却放弃了这些事务,那么这些节点就会彼此不一致(如[图7-3](img/fig7-3.png)所示)。而且一旦在一个节点上提交了一个事务,如果事后证明它在另一个节点上被中止,它将不能被收回。出于这个原因,一旦确定事务中的所有其他节点也将提交,节点就必须进行提交。 +如果某些节点提交了事务,但其他节点却放弃了这些事务,那么这些节点就会彼此不一致(如[图7-3](img/fig7-3.png)所示)。而且一旦在一个节点上提交了一个事务,如果事后证明它在另一个节点上被中止,它将不能被撤回。出于这个原因,一旦确定事务中的所有其他节点也将提交,节点就必须进行提交。 -事务提交必须是不可撤销的——你不能改变主意,并在事务提交后追溯中止事务。这个规则的原因是,一旦数据被提交,其他事务就可以看到,因此其他客户可能会开始依赖这些数据。这个原则构成了读取提交隔离的基础,在“[读已提交](ch7.md#读已提交)”一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有读取提交数据的事务将基于被追溯声明不存在的数据所以他们也必须恢复。 +事务提交必须是不可撤销的——你不能改变主意,并在事务提交后追溯中止事务。这个规则的原因是,一旦数据被提交,其他事务就可以看到,因此其他客户可能会开始依赖这些数据。这个原则构成了读已提交隔离等级的基础,在“[读已提交](ch7.md#读已提交)”一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有读取提交数据的事务将基于被追溯声明不存在的数据,所以它们也必须恢复。 -(提交事务的效果有可能在后来被另一个补偿事务取消【73,74】,但从数据库的角度来看,这是一个单独的事务,因此任何交叉事务的正确性要求是应用程序的问题。) +(提交事务的效果有可能后续通过另一个补偿事务取消【73,74】,但从数据库的角度来看,这是一个单独的事务,因此任何关于交叉事务正确性的保证是应用自己的问题。) #### 介绍两阶段提交 两阶段提交是一种用于实现跨多个节点的原子事务提交的算法,即确保所有节点提交或所有节点中止。 它是分布式数据库中的经典算法【13,35,75】。 2PC在某些数据库内部使用,并且还以XA事务【76,77】(例如Java Transaction API支持)或通过`WS-AtomicTransaction for SOAP Web`服务的形式提供给应用程序【78,79】。 -[图9-9](img/fig9-9)说明了2PC的基本流程。 与单节点事务一样,2PC中的提交/终止进程分为两个阶段(因此名称),而不是单个提交请求。 +[图9-9](img/fig9-9)说明了2PC的基本流程。 与单节点事务一样,2PC中的提交/终止进程分为两个阶段(因此而得名),而不是单个提交请求。 ![](img/fig9-9.png) @@ -619,11 +619,11 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 > #### 不要混淆2PC和2PL > -> 两阶段提交(2PC)和两阶段锁定(请参阅第257页上的“两阶段锁定(2PL)”)是两个完全不同的事情。 2PC在分布式数据库中提供原子提交,而2PL提供可序列化的隔离。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中的不幸的相似性。 +> 两阶段提交(2PC)和两阶段锁定(参阅“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)是两个完全不同的事情。 2PC在分布式数据库中提供原子提交,而2PL提供可序列化的隔离。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中的不幸的相似性。 -2PC使用一个通常不会出现在单节点事务中的新组件:**协调器(coordinator)**(也称为事务管理器)。协调器通常在请求事务的相同应用程序进程(例如,嵌入在Java EE容器中)中实现为库,但也可以是单独的进程或服务。这种协调者的例子包括Narayana,JOTM,BTM或MSDTC。 +2PC使用一个通常不会出现在单节点事务中的新组件:**协调者(coordinator)**(也称为事务管理器)。协调者通常在请求事务的相同应用程序进程(例如,嵌入在Java EE容器中)中实现为库,但也可以是单独的进程或服务。这种协调者的例子包括Narayana,JOTM,BTM或MSDTC。 -正常情况下,2PC事务从应用程序在多个数据库节点上读写数据开始。我们把这些数据库节点称为事务参与者。当应用程序准备提交时,协调器开始阶段1:它发送一个准备请求到每个节点,询问他们是否能够提交。协调者然后跟踪参与者的回应: +正常情况下,2PC事务从应用程序在多个数据库节点上读写数据开始。我们把这些数据库节点称为事务参与者。当应用程序准备提交时,协调者开始阶段1:它发送一个准备请求到每个节点,询问他们是否能够提交。协调者然后跟踪参与者的回应: * 如果所有参与者都回答“是”,表示他们已经准备好提交,那么协调者在阶段2发出**提交(commit)**请求,实际发生提交。 * 如果任何参与者回复“否”,则协调者在阶段2中向所有节点发送**中止(abort)**请求。 @@ -638,7 +638,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 1. 当应用想要开启一个分布式事务时,它向协调者请求一个事务ID。此事务ID是全局唯一的。 2. 应用在每个参与者上开始单节点事务,并将全局唯一事务ID附到单节点事务上。所有的读写都是在这些单节点事务之一中完成的。如果在这个阶段出现任何问题(例如,节点崩溃或请求超时),则协调者或任何参与者都可以中止。 -3. 当应用程序准备提交时,协调器向所有参与者发送一个准备请求,标记为全局事务ID。如果这些请求中的任何一个失败或超时,则协调器向所有参与者发送针对该事务ID的放弃请求。 +3. 当应用程序准备提交时,协调者向所有参与者发送一个准备请求,标记为全局事务ID。如果这些请求中的任何一个失败或超时,则协调者向所有参与者发送针对该事务ID的放弃请求。 4. 参与者收到准备请求时,确保在任何情况下都可以明确地进行事务。这包括将所有事务数据写入磁盘(出现故障,电源故障或硬盘空间不足以拒绝稍后提交)以及检查是否存在任何冲突或约束违规。通过向协调者回答“是”,节点承诺在没有错误的情况下提交事务。换句话说,参与者放弃了中止事务的权利,但没有实际提交。 5. 当协调者收到所有准备请求的答复时,就是否提交或中止事务作出明确的决定(只有在所有参与者投赞成票的情况下才提交)。协调者必须把这个决定写到磁盘上的事务日志中,以便它知道它决定的方式,以防随后发生崩溃。这被称为**提交点(commit point)**。 6. 一旦协调者的决定写入磁盘,提交或放弃请求被发送给所有参与者。如果此请求失败或超时,则协调者必须一直重试,直到成功为止。没有更多的事情要做,如果做出决定,那么决定必须执行,不管它需要多少次重试。如果参与者在此期间崩溃,事务将在恢复时进行——由于参与者投票“是”,因此恢复时不能拒绝提交。 @@ -649,26 +649,26 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 #### 协调者失效 -我们已经讨论了在2PC期间如果其中一个参与者或网络发生故障会发生什么情况:如果任何一个准备请求失败或者超时,协调者就中止事务。如果任何提交或中止请求失败,协调器将无条件重试。但是,如果协调者崩溃,会发生什么情况并不太清楚。 +我们已经讨论了在2PC期间如果其中一个参与者或网络发生故障会发生什么情况:如果任何一个准备请求失败或者超时,协调者就中止事务。如果任何提交或中止请求失败,协调者将无条件重试。但是,如果协调者崩溃,会发生什么情况并不太清楚。 -如果协调者在发送准备请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃——必须等待协调者回答事务是否已经发生或中止。如果此时协调器崩溃或网络出现故障,参与者只能等待。参与者在这个状态下的事务被怀疑或不确定。 +如果协调者在发送准备请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃——必须等待协调者回答事务是否已经发生或中止。如果此时协调者崩溃或网络出现故障,参与者只能等待。参与者在这个状态下的事务是存疑的或不确定的。 -情况如[图9-10](img/fig9-10)所示。在这个特定的例子中,协调器实际上决定提交,数据库2收到提交请求。但是,协调器在将提交请求发送到数据库1之前发生崩溃,因此数据库1不知道是否提交或中止。即使超时在这里也没有帮助:如果数据库1在超时后单方面中止,它将最终与提交的数据库2不一致。同样,单方面犯也是不安全的,因为另一个参与者可能已经中止了。 +情况如[图9-10](img/fig9-10)所示。在这个特定的例子中,协调者实际上决定提交,数据库2收到提交请求。但是,协调者在将提交请求发送到数据库1之前发生崩溃,因此数据库1不知道是否提交或中止。即使超时在这里也没有帮助:如果数据库1在超时后单方面中止,它将最终与提交的数据库2不一致。同样,单方面犯也是不安全的,因为另一个参与者可能已经中止了。 ![](img/fig9-10.png)  **图9-10 参与者投赞成票后,协调者崩溃。数据库1不知道是否提交或中止** 没有协调者的消息,参与者无法知道是否承诺或放弃。原则上,参与者可以相互沟通,找出每个参与者如何投票并达成一致,但这不是2PC协议的一部分。 -2PC可以完成的唯一方法是等待协调者恢复。这就是为什么协调者必须在向参与者发送提交或中止请求之前将其提交或中止决定写入磁盘上的事务日志:协调器恢复后,通过读取其事务日志来确定所有有疑问的事务的状态。任何在协调器日志中没有提交记录的事务都会中止。因此,2PC的提交点归结为协调器上的常规单节点原子提交。 +2PC可以完成的唯一方法是等待协调者恢复。这就是为什么协调者必须在向参与者发送提交或中止请求之前将其提交或中止决定写入磁盘上的事务日志:协调者恢复后,通过读取其事务日志来确定所有存疑事务的状态。任何在协调者日志中没有提交记录的事务都会中止。因此,2PC的**提交点**归结为协调者上的常规单节点原子提交。 #### 三阶段提交 -两阶段提交被称为阻塞原子提交协议,因为2PC可能卡住等待协调器恢复。理论上,可以使一个原子提交协议非阻塞,以便在节点失败时不会卡住。但是,在实践中做这个工作并不那么简单。 +两阶段提交被称为阻塞原子提交协议,因为2PC可能卡住,等待协调者恢复。理论上,可以使一个原子提交协议非阻塞,以便在节点失败时不会卡住。但在实践中这个工作并不那么简单。 -作为2PC的替代方案,已经提出了一种称为三阶段提交(3PC)的算法【13,80】。然而,3PC假定一个有界延迟的网络和有限响应时间的节点;在大多数具有无限网络延迟和进程暂停的实际系统中(见[第8章](ch8.md)),它不能保证原子性。 +作为2PC的替代方案,已经提出了一种称为三阶段提交(3PC)的算法【13,80】。然而,3PC假定一个有界延迟的网络和有限响应时间的节点;在大多数具有无限网络延迟和进程暂停的实际系统中(见[第8章](ch8.md)),它并不能保证原子性。 -通常,非阻塞原子提交需要一个完美的故障检测器【67,71】—— 即一个可靠的机制来判断一个节点是否已经崩溃。在无限延迟的网络中,超时不是可靠的故障检测器,因为即使没有节点崩溃,请求也可能由于网络问题而超时。出于这个原因,2PC仍然被使用,尽管大家都清楚可能有协调器故障的问题。 +通常,非阻塞原子提交需要一个完美的故障检测器【67,71】—— 即一个可靠的机制来判断一个节点是否已经崩溃。在无限延迟的网络中,超时不是可靠的故障检测器,因为即使没有节点崩溃,请求也可能由于网络问题而超时。出于这个原因,2PC仍然被使用,尽管大家都清楚可能存在协调者故障的问题。 @@ -678,7 +678,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 分布式事务的某些实现会带来严重的性能损失——例如,MySQL中的分布式事务被报告比单节点事务慢10倍以上【87】,所以当人们建议不要使用这些事务时就不足为奇了。两阶段提交所固有的大部分性能成本是由于崩溃恢复所需的额外强制刷盘(`fsync`)【88】以及额外的网络往返。 -但是,我们不应该直接抛弃分布式事务,而应该更加详细地审视这些事务,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明“**分布式事务**”的含义。两种截然不同的分布式事务类型经常被混淆: +但是,我们不应该直接抛弃分布式事务,而应该更加仔细地审视这些事务,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明“**分布式事务**”的含义。两种截然不同的分布式事务类型经常被混淆: ***数据库内部的分布式事务*** @@ -688,7 +688,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 在异构事务中,参与者有两种或两种以上不同的技术:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。 -数据库内部事务不必与任何其他系统兼容,因此他们可以使用任何协议并应用特定技术的特定优化。因此,数据库内部的分布式事务通常可以很好地工作。另一方面,跨越异构技术的事务则更具挑战性。 +数据库内部事务不必与任何其他系统兼容,因此他们可以使用任何协议并应用特定技术的特定优化。因此,数据库内部的分布式事务通常工作地很好。另一方面,跨越异构技术的事务则更有挑战性。 #### 恰好一次的消息处理 @@ -704,40 +704,41 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 *X/Open XA*(**扩展架构(eXtended Architecture)**的缩写)是跨异构技术实现两阶段提交的标准【76,77】。它于1991年推出并得到了广泛的实施:许多传统关系数据库(包括PostgreSQL,MySQL,DB2,SQL Server和Oracle)和消息代理(包括ActiveMQ,HornetQ,MSMQ和IBM MQ) 。 -XA不是一个网络协议——它只是一个用于与事务协调器连接的C API。此API的绑定以其他语言存在;例如,在Java EE应用程序的世界中,XA事务是使用Java事务API(JTA)实现的,而Java事务API(JTA)则由许多用于使用Java数据库连接(JDBC)的数据库驱动程序以及使用Java消息服务(JMS)API。 +XA不是一个网络协议——它只是一个用于与事务协调者连接的C API。此API的绑定以其他语言存在;例如,在Java EE应用程序的世界中,XA事务是使用Java事务API(JTA)实现的,而Java事务API(JTA)则由许多用于使用Java数据库连接(JDBC)的数据库驱动程序以及使用Java消息服务(JMS)API。 -XA假定您的应用程序使用网络驱动程序或客户端库来与参与者数据库或消息传递服务进行通信。如果驱动程序支持XA,则表示它调用XA API以查明操作是否应该是分布式事务的一部分——如果是,则将必要的信息发送到数据库服务器。司机还会提供回调,协调者可以通过回调来要求参与者准备,提交或中止。 +XA假定你的应用程序使用网络驱动程序或客户端库来与参与者数据库或消息传递服务进行通信。如果驱动程序支持XA,则表示它调用XA API以查明操作是否应该是分布式事务的一部分——如果是,则将必要的信息发送到数据库服务器。驱动还会提供回调,协调者可以通过回调来要求参与者准备,提交或中止。 -事务协调器实现XA API。标准没有指定应该如何实现,但实际上,协调器通常只是一个库,与发出事务的应用程序(不是单独的服务)一起被加载到相同的进程中。它跟踪事务的参与者,在要求他们准备(通过回调驱动程序)之后收集参与者的回答,并使用本地磁盘上的日志记录每次事务的提交/中止决定。 +事务协调者实现XA API。标准没有指定应该如何实现,但实际上,协调者通常只是一个库,与发出事务的应用程序(不是单独的服务)一起被加载到相同的进程中。它跟踪事务的参与者,在要求他们准备(通过回调驱动程序)之后收集参与者的回答,并使用本地磁盘上的日志记录每次事务的提交/中止决定。 -如果应用程序进程崩溃,或者运行应用程序的机器死亡,协调者就会使用它。然后任何有准备但未提交的事务的参与者都被怀疑。由于协调程序的日志位于应用程序服务器的本地磁盘上,因此必须重新启动该服务器,并且协调程序库必须读取日志以恢复每个事务的提交/中止结果。只有这样,协调者才能使用数据库驱动程序的XA回调来要求参与者提交或中止。数据库服务器不能直接联系协调器,因为所有通信都必须通过其客户端库。 +如果应用程序进程崩溃,或者运行应用程序的机器死亡,协调者就会使用它。然后任何有准备但未提交的事务的参与者都被怀疑。由于协调程序的日志位于应用程序服务器的本地磁盘上,因此必须重新启动该服务器,并且协调程序库必须读取日志以恢复每个事务的提交/中止结果。只有这样,协调者才能使用数据库驱动程序的XA回调来要求参与者提交或中止。数据库服务器不能直接联系协调者,因为所有通信都必须通过其客户端库。 #### 怀疑时持有锁 -为什么我们非常关心事务被怀疑?系统的其他部分不能继续工作,而忽视最终将被清理的有问题的事务吗? -问题在于锁定。正如在第225页上的“读取已提交”中所讨论的那样,数据库事务通常对其修改的行进行行级别的排他锁定,以防止脏写入。此外,如果要使用可序列化的隔离,则使用两阶段锁定的数据库也必须对事务读取的任何行执行共享锁定(参见“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)。 +为什么我们非常关心存疑事务?系统的其他部分不能继续工作,而忽视最终将被清理的有问题的事务吗? -在事务提交或中止之前,数据库不能释放这些锁(如[图9-9](img/fig9-9.png)中的阴影区域所示)。因此,在使用两阶段提交时,事务必须在整个时间内保持锁定状态。如果协调者已经坠毁,需要20分钟才能重新启动,这些锁将会保持20分钟。如果协调者的日志由于某种原因完全丢失,这些锁将永久保存,或者至少在管理员手动解决该情况之前。 +问题在于**锁**。正如在“[读已提交](ch7.md#读已提交)”中所讨论的那样,数据库事务通常对其修改的行进行行级别的排他锁定,以防止脏写。此外,如果要使用可序列化的隔离等级,则使用两阶段锁定的数据库也必须对事务读取的任何行执行共享锁定(参见“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)。 + +在事务提交或中止之前,数据库不能释放这些锁(如[图9-9](img/fig9-9.png)中的阴影区域所示)。因此,在使用两阶段提交时,事务必须在整个时间内保持锁定状态。如果协调者已经崩溃,需要20分钟才能重新启动,这些锁将会持有20分钟。如果协调者的日志由于某种原因完全丢失,这些锁将永久保存,或者至少在管理员手动解决该情况之前。 当这些锁被保留时,其他事务不能修改这些行。根据数据库的不同,其他事务甚至可能被阻止读取这些行。因此,其他事务不能简单地继续他们的业务 - 如果他们想访问相同的数据,他们将被阻止。这可能会导致大部分应用程序变得不可用,直到有问题的事务得到解决。 -#### 从协调器故障中恢复 +#### 从协调者故障中恢复 -理论上,如果协调器崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何有问题的事务。然而,在实践中,孤立的不确定事务确实发生【89,90】,也就是说,协调者不能以任何理由决定结果的事务(例如,因为事务日志已经由于软件错误)。这些事务不能自动解决,所以他们永远坐在数据库中,持有锁和阻止其他事务。 +理论上,如果协调者崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何有问题的事务。然而,在实践中,孤立的不确定事务确实发生【89,90】,也就是说,协调者不能以任何理由决定结果的事务(例如,因为事务日志已经由于软件错误)。这些事务不能自动解决,所以他们永远待在数据库中,持有锁并阻止其他事务。 -即使重新启动数据库服务器也不能解决这个问题,因为2PC的正确实现必须在重新启动时保留一个有问题的事务的锁(否则就会冒违反原子性保证的风险)。这是一个棘手的情况。 +即使重新启动数据库服务器也不能解决这个问题,因为2PC的正确实现必须在重新启动时保留一个有问题的事务的锁(否则就会冒有违反原子性保证的风险)。这是一个棘手的情况。 -唯一的出路是让管理员手动决定是提交还是回滚事务。管理员必须检查每个有问题的事务的参与者,确定是否有任何参与者已经提交或中止,然后将相同的结果应用于其他参与者。解决这个问题潜在地需要大量的人工努力,并且在严重的生产中断期间(否则,为什么协调者处于这样一个糟糕的状态),很可能需要在高压力和时间压力下完成。 +唯一的出路是让管理员手动决定是提交还是回滚事务。管理员必须检查每个有问题的事务的参与者,确定是否有任何参与者已经提交或中止,然后将相同的结果应用于其他参与者。解决这个问题潜在地需要大量的人力,并且可能发生在严重的生产中断期间(否则,为什么协调者处于这样一个糟糕的状态),很可能需要在高压力和时间压力下完成。 -许多XA的实现都有一个叫做**启发式决策(heuristic decistions)**的紧急逃生舱口:允许参与者单方面决定放弃或提交一个有疑问的事务,而不需要协调者做出明确的决定【76,77,91】。要清楚的是,这里的启发式是可能破坏原子性的委婉说法,因为它违背了两阶段承诺的承诺体系。因此,启发式决策只是为了逃出灾难性的情况,而不是经常使用。 +许多XA的实现都有一个叫做**启发式决策(heuristic decistions)**的紧急逃生舱口:允许参与者单方面决定放弃或提交一个有疑问的事务,而不需要协调者做出明确的决定【76,77,91】。要清楚的是,这里的启发式是可能破坏原子性的委婉说法,因为它违背了两阶段承诺的承诺体系。因此,启发式决策只是为了逃出灾难性的情况,而不是经常性的使用。 #### 分布式事务的限制 -XA事务解决了保持多个参与者数据系统一致的真实而重要的问题,但正如我们所看到的那样,它们也引入了主要的操作问题。特别是,关键的实现是事务协调器本身就是一种数据库(在其中存储事务结果),因此需要像其他重要数据库一样小心: +XA事务解决了保持多个参与者数据系统一致的真实而重要的问题,但正如我们所看到的那样,它们也引入了主要的运维问题。特别是,关键的实现是事务协调者本身就是一种数据库(在其中存储事务结果),因此需要像其他重要数据库一样小心: -* 如果协调器没有被复制,而是只在一台机器上运行,那么整个系统是一个失败的单点(因为它的失败导致其他应用程序服务器阻塞在有问题的事务处理的锁上)。令人惊讶的是,许多协调器实现默认情况下不是高度可用,或者只有基本的复制支持。 -* 许多服务器端应用程序都是在无状态模式下开发的(受到HTTP的青睐),所有持久状态都存储在数据库中,具有应用程序服务器可随意添加和删除的优点。但是,当协调器是应用程序服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分——与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复疑问事务所必需的。这样的应用程序服务器不再是无状态的。 -* 由于XA需要与各种数据系统兼容,因此这是必须的最低公分母。例如,它不能检测到不同系统间的死锁(因为这将需要一个标准化的协议来让系统交换每个事务正在等待的锁的信息),而且它不适用于[SSI](ch7.md#可串行快照隔离(SSI) ),因为这需要一个协议来识别不同系统之间的冲突。 +* 如果协调者没有被复制,而是只在一台机器上运行,那么整个系统是一个失效的单点(因为它的失效会导致其他应用程序服务器阻塞在有问题的事务处理的锁上)。令人惊讶的是,许多协调者实现默认情况下不是高度可用,或者只有基本的复制支持。 +* 许多服务器端应用程序都是在无状态模式下开发的(受到HTTP的青睐),所有持久状态都存储在数据库中,具有应用程序服务器可随意添加和删除的优点。但是,当协调者是应用程序服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分——与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复疑问事务所必需的。这样的应用程序服务器不再是无状态的。 +* 由于XA需要与各种数据系统兼容,因此它是必须的最小公分母。例如,它不能检测到不同系统间的死锁(因为这将需要一个标准化的协议来让系统交换每个事务正在等待的锁的信息),而且它不适用于[SSI](ch7.md#可串行快照隔离(SSI) ),因为这需要一个协议来识别不同系统之间的冲突。 * 对于数据库内部的分布式事务(而不是XA),限制不是很大,例如SSI的分布式版本是可能的。然而,仍然存在2PC成功进行事务的问题,所有参与者都必须作出回应。因此,如果系统的任何部分损坏,事务也会失败。因此,分布式事务有扩大故障的趋势,这与我们构建容错系统的目标背道而驰。 这些事实是否意味着我们应该放弃保持几个系统一致的所有希望?不完全是——有其他的方法可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第十一章](ch11.md)和[第十二章](ch12.md)回到这些章节。但首先,我们应该总结共识的话题。 @@ -769,27 +770,27 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 ***终止*** 由所有未崩溃的节点来最终决定值。 -统一协议和完整性属性定义了共识的核心思想:每个人都决定相同的结果,一旦你决定了,你就不能改变主意。有效性属性主要是为了排除微不足道的解决方案:例如,无论提出什么建议,都可以有一个总是决定为空的算法;该算法将满足协议和完整性属性,但不符合有效性属性。 +一致同意和完整性属性定义了共识的核心思想:每个人都决定了相同的结果,一旦决定了,你就不能改变主意。有效性属性主要是为了排除平凡的解决方案:例如,无论提出什么建议,都可以有一个总是决定为空的算法;该算法将满足协议和完整性属性,但不符合有效性属性。 -如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但是,如果一个节点失败,那么系统就不能再做出任何决定。事实上,这就是我们在两阶段承诺的情况下所看到的:如果协调者失败了,那么不确定的参与者就不能决定是否提交或中止。 +如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但是,如果该节点失效,那么系统就不能再做出任何决定。事实上,这就是我们在两阶段提交的情况下所看到的:如果协调者失败了,那么不确定的参与者就不能决定是否提交或中止。 -终止属性正式形成了容错的思想。它基本上说,一个共识算法不能简单地坐下来,永远不要做任何事——换句话说,它必须取得进展。即使有些节点出现故障,其他节点也必须做出决定。 (终止是一种**活性属性**,而另外三种是安全属性——参见“[安全性和活性](ch8.md#安全性和活性)”。) +终止属性正式形成了容错的思想。它基本上说的是,一个共识算法不能简单地坐下来,永远不做任何事——换句话说,它必须取得进展。即使有些节点出现故障,其他节点也必须做出决定。 (终止是一种**活性属性**,而另外三种是安全属性——参见“[安全性和活性](ch8.md#安全性和活性)”。) -共识的系统模型假设,当一个节点“崩溃”时,它突然消失,永远不会回来。(而不是软件崩溃,想象一下地震,包含你的节点的数据中心被山体滑坡所摧毁,你必须假设你的节点被埋在30英尺以下的泥土中,并且永远不会回到在线状态。)这个系统模型,任何等待节点恢复的算法都不能满足终止属性。特别是2PC不符合终止的要求。 +共识的系统模型假设,当一个节点“崩溃”时,它突然消失且永远不会回来。(而不是软件崩溃,想象一下地震,包含你的节点的数据中心被山体滑坡所摧毁,你必须假设节点被埋在30英尺以下的泥土中,并且永远不会回到在线状态。)这个系统模型,任何等待节点恢复的算法都不能满足终止属性。特别是2PC不符合终止的要求。 -当然,如果所有的节点都崩溃,而且没有一个正在运行,那么任何算法都不可能决定什么。算法可以容忍的失败次数有一个限制:事实上,可以证明任何一致性算法都需要至少大部分节点正确运行,以确保终止【67】。大多数人可以安全地形成法定人数(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。 +当然,如果所有的节点都崩溃,而且没有一个正在运行,那么任何算法都不可能决定什么。算法可以容忍的失败次数有一个限制:事实上,可以证明任何一致性算法都需要至少大部分节点正确运行,以确保终止属性【67】。大多数人可以安全地形成法定人数(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。 因此,终止属性受到不到一半的节点崩溃或不可达的假设。然而,即使大多数节点出现故障或存在严重的网络问题,大多数共识的实施都能确保始终满足安全属性——同意,完整性和有效性【92】。因此,大规模的中断可能会阻止系统处理请求,但是它不能通过使系统做出无效的决定来破坏共识系统。 -大多数一致性算法假定没有拜占庭式的错误,正如在“[拜占庭式错误](#拜占庭式错误)”一节中所讨论的那样。也就是说,如果一个节点没有正确地遵循协议(例如,如果它发送矛盾的消息到不同的节点),它可能会破坏协议的安全属性。只要少于三分之一的节点是拜占庭故障【25,93】,就可以对拜占庭故障形成共识,但我们没有地方在本书中详细讨论这些算法。 +大多数一致性算法假定没有**拜占庭式错误**,正如在“[拜占庭式错误](#拜占庭式错误)”一节中所讨论的那样。也就是说,如果一个节点没有正确地遵循协议(例如,如果它发送矛盾的消息到不同的节点),它可能会破坏协议的安全属性。只要少于三分之一的节点是拜占庭故障【25,93】,就可以对拜占庭故障形成共识,但我们没有地方在本书中详细讨论这些算法。 #### 共识算法和全序广播 最着名的容错一致性算法是**视图戳复制(viewstamped replication)**(VSR)【94,95】,Paxos 【96,97,98,99】,Raft 【22,100,101】和Zab 【15,21,102】 。这些算法之间有相当多的相似之处,但它们并不相同【103】。在本书中,我们不会详细介绍不同的算法:只要了解一些共同的高级思想就足够了,除非你准备自己实现一个共识系统。(这可能不是一个明智的做法,相当困难【98,104】) -这些算法中的大多数实际上并不直接使用这里描述的形式化模型(建议和决定单个值,同时满足协议,完整性,有效性和终止性质)。相反,他们决定了值的顺序,这使得它们成为了全序广播算法,正如本章前面所讨论的那样(参阅“[全序广播](#全序广播)”)。 +这些算法中的大多数实际上并不直接使用这里描述的形式化模型(建议和决定单个值,一致同意,完整性,有效性和终止性质)。相反,它们决定了值的顺序,这使得它们成为了全序广播算法,正如本章前面所讨论的那样(参阅“[全序广播](#全序广播)”)。 -请记住,全序广播要求将消息按照相同的顺序准确传送到所有节点。如果你仔细想想,这相当于进行了几轮的共识:在每一轮中,节点提出下一个要发送的消息,然后决定下一个要发送的消息总数【67】。 +请记住,全序广播要求将消息按照相同的顺序准确传送到所有节点。如果仔细思考,这相当于进行了几轮的共识:在每一轮中,节点提出下一个要发送的消息,然后决定下一个要发送的消息总数【67】。 所以,全序广播相当于重复的一轮共识(每个共同的决定对应于一个消息传递): @@ -806,13 +807,13 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 在[第5章](ch5.md)中,我们讨论了单领导者复制(参见“[领导者和追随者](ch5.md#领导者和追随者)”),它将所有的写入操作都交给领导者,并以相同的顺序将他们应用到追随者,从而使副本保持最新状态。这不是基本上全序广播?我们怎么不用担心[第五章](ch5.md)的共识? -答案取决于如何选择领导者。如果领导人是由您的运营团队中的人员手动选择和配置的,那么您基本上拥有独裁种类的“一致性算法”:只允许一个节点接受写入(即,决定写入的顺序复制日志),如果该节点发生故障,则系统将无法写入,直到操作员手动配置其他节点作为主管。这样的制度在实践中可以很好地发挥作用,但是不能达到共识的终止性,因为它需要人为干预才能取得进展。 +答案取决于如何选择领导者。如果领导人是由你的运营团队中的人员手动选择和配置的,那么你基本上拥有独裁种类的“一致性算法”:只允许一个节点接受写入(即,决定写入的顺序复制日志),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为主管。这样的制度在实践中可以很好地发挥作用,但是不能达到共识的终止属性,因为它需要人为干预才能取得进展。 -一些数据库执行自动领导者选举和故障转移,如果旧领导者失败,则促使追随者成为新的领导者(参见第156页的“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们更接近容错的全面命令播出,从而达成共识。 +一些数据库执行自动领导者选举和故障转移,如果旧领导者失败,则促使追随者成为新的领导者(参见“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们更接近容错的全面命令播出,从而达成共识。 -但是,有一个问题。我们之前曾经讨论过分裂脑的问题,并且说所有的节点都需要同意领导者是谁,否则两个不同的节点都会相信自己是领导者,从而导致数据库进入不一致的状态。因此,我们需要达成共识才能选出一位领导人。但是,如果这里描述的一致性算法实际上是全序广播算法,并且全序广播就像单主复制,单主复制需要领导,那么... +但是,有一个问题。我们之前曾经讨论过脑裂的问题,并且说过,所有的节点都需要同意领导者是谁,否则两个不同的节点都会认为自己是领导者,从而导致数据库进入不一致的状态。因此,我们需要达成共识才能选出一位领导人。但是,如果这里描述的一致性算法实际上是全序广播算法,并且全序广播就像单主复制,单主复制需要领导,那么... -看来要选出一个领导,我们首先需要一个领导。要解决共识问题,首先要解决共识问题。我们如何摆脱这个难题? +看来要选出一个领导者,我们首先需要一个领导者。要解决共识问题,首先要解决共识问题。我们如何摆脱这个难题? #### 时代编号和法定人数 @@ -820,13 +821,13 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 每当现在的领导被认为是死的时候,就会在节点之间开始投票选出一个新领导。这次选举被赋予了一个递增的时代号码,因此时代号码是完全有序的,单调递增的。如果在两个不同的时代,两个不同的领导者之间有冲突(也许是因为前一个领导者实际上并没有死亡),那么具有更高时代的领导者就占上风了。 -在任何领导人被允许决定任何事情之前,必须首先检查是否没有其他具有较高时代的领导者,这可能会采取相互冲突的决定。领导者如何知道它没有被另一个节点赶下?回想一下第300页的“[真理是由多数所定义](ch8.md#真理是由多数所定义)”:一个节点不一定相信自己的判断——只是因为节点认为它是领导者,并不一定意味着其他节点接受它作为他们的领导者。 +在任何领导人被允许决定任何事情之前,必须首先检查是否没有其他具有较高时代的领导者,这可能会采取相互冲突的决定。领导者如何知道它没有被另一个节点赶下?回想一下在“[真理是由多数所定义](ch8.md#真理是由多数所定义)”中:一个节点不一定能相信自己的判断——因为只有节点自己认为它是领导者,并不一定意味着其他节点接受它作为它们的领导者。 -相反,它必须从节点法定人数中收集选票(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对于领导者想要做出的每一个决定,都必须将建议值发送给其他节点,并等待法定人数的节点响应提案。法定人数通常但不总是由大部分节点组成【105】。一个节点只有在没有意识到任何具有更高纪元的其他领导者的时候才投票赞成。 +相反,它必须从节点法定人数中获取选票(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对于领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应提案。法定人数通常但不总是由大部分节点组成【105】。一个节点只有在没有意识到任何具有更高纪元的其他领导者的时候才投票赞成。 因此,我们有两轮投票:一次是选一位领导人,二是投票领导人的提议。关键的看法是,这两票的法定人数必须重叠:如果一个提案的投票成功,至少有一个投票的节点也必须参加最近的领导人选举【105】。因此,如果一个提案的投票没有显示任何更高的时代,那么现在的领导者就可以得出这样的结论:没有一个更高时代的领袖选举发生了,因此可以确定它仍然是领导。然后它可以安全地决定提出的价值。 -这个投票过程看起来很像两阶段提交。最大的区别是在2PC中协调器不是选出来的,而容错协议算法只需要大部分节点的投票,而2PC则要求每个参与者都做“是”的投票。而且,共识算法定义了一个恢复过程,通过这个过程,节点可以在选举出新的领导者之后进入一个一致的状态,确保总是满足安全属性。这些差异是共识算法的正确性和容错性的关键。 +这个投票过程看起来很像两阶段提交。最大的区别是在2PC中协调者不是选出来的,而容错协议算法只需要大部分节点的投票,而2PC则要求每个参与者都做“是”的投票。而且,共识算法定义了一个恢复过程,通过这个过程,节点可以在选举出新的领导者之后进入一个一致的状态,确保总是满足安全属性。这些差异是共识算法的正确性和容错性的关键。 #### 共识的局限性 @@ -836,9 +837,9 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 节点在决定之前对节点进行投票的过程是一种同步复制。如“[同步与异步复制](ch5.md#同步与异步复制)”中所述,通常将数据库配置为使用异步复制。在这种配置中,一些承诺的数据在故障转移时可能会丢失——但是为了获得更好的性能,许多人选择接受这种风险。 -共识体系总是需要严格的多数来操作。这意味着您至少需要三个节点才能容忍一个故障(其余三个为大多数),或者至少有五个节点容忍两个故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另请参阅“[线性一致性的代价](#线性一致性的代价)”(第295页))。 +共识体系总是需要严格的多数来操作。这意味着你至少需要三个节点才能容忍一个故障(其余三个为大多数),或者至少有五个节点容忍两个故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另参阅“[线性一致性的代价](#线性一致性的代价)”(第295页))。 -大多数一致性算法假定一组参与投票的节点,这意味着您不能只添加或删除集群中的节点。对共识算法的动态成员扩展允许集群中的节点集随着时间的推移而变化,但是它们比静态成员算法要好得多。 +大多数一致性算法假定一组参与投票的节点,这意味着你不能只添加或删除集群中的节点。对共识算法的动态成员扩展允许集群中的节点集随着时间的推移而变化,但是它们比静态成员算法要好得多。 共识系统通常依靠超时来检测失败的节点。在网络延迟高度变化的环境中,特别是在地理上分布的系统中,经常发生一个节点错误地认为由于暂时的网络问题,导致失败的原因。虽然这个错误不会损害安全属性,但频繁的领导者选举会导致糟糕的表现,因为系统最终会花费更多的时间来选择领导者而不是做任何有用的工作。 @@ -848,7 +849,7 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 像ZooKeeper或etcd这样的项目通常被描述为“分布式键值存储”或“协调和配置服务”。这种服务的API看起来非常像数据库:你可以读写给定键的值,并遍历键。所以如果他们基本上是数据库的话,他们为什么要全力实施一个共识算法呢?是什么使他们不同于任何其他类型的数据库? -为了理解这一点,简单探讨如何使用像ZooKeeper这样的服务是有帮助的。作为应用程序开发人员,您很少需要直接使用ZooKeeper,因为它实际上不适合作为通用数据库。更有可能的是,通过其他项目间接依赖它,例如HBase,Hadoop YARN,OpenStack Nova和Kafka都依赖ZooKeeper在后台运行。这些项目从中得到什么? +为了理解这一点,简单探讨如何使用像ZooKeeper这样的服务是有帮助的。作为应用程序开发人员,你很少需要直接使用ZooKeeper,因为它实际上不适合作为通用数据库。更有可能的是,通过其他项目间接依赖它,例如HBase,Hadoop YARN,OpenStack Nova和Kafka都依赖ZooKeeper在后台运行。这些项目从中得到什么? ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据(虽然它们仍然写入磁盘以保持持久性),所以你不希望在这里存储所有的应用程序的数据。使用容错全序广播算法在所有节点上复制少量的数据。正如前面所讨论的那样,全序广播就是数据库复制所需要的:如果每条消息代表对数据库的写入,则以相同的顺序应用相同的写入操作可以保持副本之间的一致性。 @@ -856,11 +857,11 @@ ZooKeeper模仿Google的Chubby锁定服务【14,98】,不仅实现了全序广 ***线性一致性的原子操作*** -使用原子比较和设置操作,可以实现锁定:如果多个节点同时尝试执行相同的操作,则只有其中一个节点会成功。共识协议保证了操作将是原子性和线性一致性的,即使节点发生故障或网络在任何时候都被中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(请参阅“[进程暂停](ch8.md#进程暂停)”)。 +使用原子比较和设置操作,可以实现锁定:如果多个节点同时尝试执行相同的操作,则只有其中一个节点会成功。共识协议保证了操作将是原子性和线性一致性的,即使节点发生故障或网络在任何时候都被中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(参阅“[进程暂停](ch8.md#进程暂停)”)。 ***操作的全序*** -如“页首301和锁定”中所述,当某个资源受到锁定或租约的保护时,您需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。击剑标记是每次获得锁定时单调增加的数字。 ZooKeeper通过完全排序所有操作,并为每个操作提供一个单调递增的事务ID(`zxid`)和版本号(`cversion`)来提供这个功能【15】。 +如“页首301和锁定”中所述,当某个资源受到锁定或租约的保护时,你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。击剑标记是每次获得锁定时单调增加的数字。 ZooKeeper通过完全排序所有操作,并为每个操作提供一个单调递增的事务ID(`zxid`)和版本号(`cversion`)来提供这个功能【15】。 ***失效检测*** @@ -876,29 +877,29 @@ ZooKeeper模仿Google的Chubby锁定服务【14,98】,不仅实现了全序广 #### 将工作分配给节点 -ZooKeeper/Chubby模型运行良好的一个例子是,如果您有几个流程或服务的实例,并且需要选择其中一个实例作为主库或首要。如果领导失败,其他节点之一应该接管。这对于单引导数据库当然是有用的,但对于作业调度程序和类似的有状态系统也是有用的。 +ZooKeeper/Chubby模型运行良好的一个例子是,如果你有几个流程或服务的实例,并且需要选择其中一个实例作为主库或首要。如果领导失败,其他节点之一应该接管。这对于单主数据库当然是有用的,但对于作业调度程序和类似的有状态系统也是有用的。 另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式参与者系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入群集时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(参阅“[重新平衡分区](ch6.md#重新平衡分区)”)。当节点被移除或失败时,其他节点需要接管失败节点的工作。 这些类型的任务可以通过在ZooKeeper中明智地使用原子操作,各种节点和通知来实现。如果正确完成,这种方法允许应用程序自动从故障中恢复,无需人工干预。尽管Apache Curator 【17】等库已经出现在ZooKeeper客户端API的顶层提供了更高级别的工具,但这样做并不容易,但它仍然比尝试从头开始实现必要的一致性算法要好得多,成绩不佳【107】。 -应用程序最初只能在单个节点上运行,但最终可能会增长到数千个节点。试图在如此之多的节点上进行多数选票将是非常低效的。相反,ZooKeeper在固定数量的节点(通常是三到五个)上运行,并在这些节点之间执行其多数票,同时支持潜在的大量客户端。因此,ZooKeeper提供了一种将协调节点(共识,操作排序和故障检测)的一些工作“外包”到外部服务的方式。 +应用最初只能在单个节点上运行,但最终可能会增长到数千个节点。试图在如此之多的节点上进行多数选票将是非常低效的。相反,ZooKeeper在固定数量的节点(通常是三到五个)上运行,并在这些节点之间执行其多数票,同时支持潜在的大量客户端。因此,ZooKeeper提供了一种将协调节点(共识,操作排序和故障检测)的一些工作“外包”到外部服务的方式。 通常,由ZooKeeper管理的数据的类型变化十分缓慢:代表“分区7中的节点运行在`10.1.1.23`上”的信息可能会在几分钟或几小时的时间内发生变化。它不是用来存储应用程序的运行时状态的,每秒可能会改变数千甚至数百万次。如果应用程序状态需要从一个节点复制到另一个节点,则可以使用其他工具(如Apache BookKeeper 【108】)。 #### 服务发现 -ZooKeeper,etcd和Consul也经常用于服务发现——也就是找出你需要连接到哪个IP地址才能到达特定的服务。在云数据中心环境中,虚拟机连续来去常见,您通常不会提前知道您的服务的IP地址。相反,您可以配置您的服务,使其在启动时注册服务注册表中的网络端点,然后可以由其他服务找到它们。 +ZooKeeper,etcd和Consul也经常用于服务发现——也就是找出你需要连接到哪个IP地址才能到达特定的服务。在云数据中心环境中,虚拟机连续来去常见,你通常不会事先知道服务的IP地址。相反,你可以配置你的服务,使其在启动时注册服务注册表中的网络端点,然后可以由其他服务找到它们。 但是,服务发现是否需要达成共识还不太清楚。 DNS是查找服务名称的IP地址的传统方式,它使用多层缓存来实现良好的性能和可用性。从DNS读取是绝对不线性一致性的,如果DNS查询的结果有点陈旧,通常不会有问题【109】。 DNS对网络中断的可靠性和可靠性更为重要。 -尽管服务发现并不需要共识,但领导者选举却是如此。因此,如果您的共识系统已经知道领导是谁,那么也可以使用这些信息来帮助其他服务发现领导是谁。为此,一些共识系统支持只读缓存副本。这些副本异步接收共识算法所有决策的日志,但不主动参与投票。因此,它们能够提供不需要线性一致性的读取请求。 +尽管服务发现并不需要共识,但领导者选举却是如此。因此,如果你的共识系统已经知道领导是谁,那么也可以使用这些信息来帮助其他服务发现领导是谁。为此,一些共识系统支持只读缓存副本。这些副本异步接收共识算法所有决策的日志,但不主动参与投票。因此,它们能够提供不需要线性一致性的读取请求。 #### 成员服务 -ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部分,这个历史可以追溯到20世纪80年代,并且对建立高度可靠的系统(例如空中交通管制)非常重要【110】。 +ZooKeeper和它的小伙伴们可以看作是成员服务研究的悠久历史的一部分,这个历史可以追溯到20世纪80年代,并且对建立高度可靠的系统(例如空中交通管制)非常重要【110】。 -成员资格服务确定哪些节点当前处于活动状态并且是群集的活动成员。正如我们在第8章中看到的那样,由于无限的网络延迟,无法可靠地检测到另一个节点是否发生故障。但是,如果你通过一致的方式进行故障检测,那么节点可以就哪些节点应该被认为是存在或不存在达成一致。 +成员资格服务确定哪些节点当前处于活动状态并且是群集的活动成员。正如我们在[第8章](ch8.md)中看到的那样,由于无限的网络延迟,无法可靠地检测到另一个节点是否发生故障。但是,如果你通过一致的方式进行故障检测,那么节点可以就哪些节点应该被认为是存在或不存在达成一致。 即使它确实存在,仍然可能发生一个节点被错误地宣布死于共识。但是对于一个系统来说,在哪些节点构成当前的成员关系方面是非常有用的。例如,选择领导者可能意味着简单地选择当前成员中编号最小的成员,但如果不同的节点对现有成员的成员有不同意见,则这种方法将不起作用。 @@ -908,13 +909,13 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部 ## 本章小结 -在本章中,我们从几个不同的角度研究了一致性和共识的主题。我们深入研究了线性一致性(一种流行的一致性模型):其目标是使复制的数据看起来好像只有一个副本,并使所有操作都以原子方式运行。虽然线性一致性因为易于理解而变得很吸引人 - 它使数据库在单线程程序中表现得像一个变量一样,但它具有速度慢的缺点,特别是在网络延迟较大的环境中。 +在本章中,我们从几个不同的角度研究了一致性和共识的主题。我们深入研究了线性一致性(一种流行的一致性模型):其目标是使复制的数据看起来好像只有一个副本,并使所有操作都以原子方式运行。虽然线性一致性因为易于理解而变得很吸引人——它使数据库在单线程程序中表现得像一个变量一样,但它具有速度慢的缺点,特别是在网络延迟较大的环境中。 -我们还探讨了因果关系,这个因果关系对系统中的事件进行了排序(根据原因和结果发生在什么之前)。与线性一致性不同,线性一致性将所有操作放在单一的完全有序的时间线中,因果性为我们提供了一个较弱的一致性模型:有些东西可以是并发的,所以版本历史就像是一个分支和合并的时间线。因果一致性不具备线性一致性的协调开销,并且对网络问题的敏感性要低得多。 +我们还探讨了因果性,因果性对系统中的事件进行了排序(根据因和过,以及什么发生在什么之前)。与线性一致性不同,线性一致性将所有操作放在单一的完全有序的时间线中,因果一致性为我们提供了一个较弱的一致性模型:某些东西可以是并发的,所以版本历史就像是一个不断分叉与合并的时间线。因果一致性没有线性一致性的协调开销,并且对网络问题的敏感性要低得多。 -但是,即使我们捕捉到因果顺序(例如使用兰伯特时间戳),我们也看到有些事情不能以这种方式实现:在“[光有时间戳排序还不够](#光有时间戳排序还不够)”中,我们考虑了确保用户名是唯一的,并拒绝同一用户名的并发注册。如果一个节点要接受注册,则需要知道另一个节点不是同时注册相同名称的过程。这个问题导致我们达成共识。 +但是,即使我们捕捉到因果顺序(例如使用兰伯特时间戳),有些事情也不能以这种方式实现:在“[光有时间戳排序还不够](#光有时间戳排序还不够)”中,我们考虑了确保用户名是唯一的,并拒绝同一用户名的并发注册。如果一个节点要接受注册,则需要知道另一个节点不是同时注册相同名称的过程。这个问题导致我们需要**共识**。 -我们看到,达成共识意味着决定一件事情,使所有节点对所做决定达成一致,从而决定是不可撤销的。通过一些挖掘,事实证明,广泛的问题实际上可以归结为共识,并且彼此是等价的(从这个意义上说,如果你有一个解决方案,你可以很容易地将它转换成解决方案之一其他)。这种等同的问题包括: +我们看到,达成共识意味着决定一件事情,使所有节点对所做决定达成一致,从而决定是不可撤销的。通过一些挖掘,事实证明,很广泛的一系列问题实际上都可以归结为共识问题,并且彼此是等价的(从这个意义上来讲,如果你有一个问题的解决方案,你可以容易地将它转换为其他问题的解决方案)。这种等价的问题包括: ***线性一致性的CAS寄存器*** @@ -930,7 +931,7 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部 ***锁和租约*** -当几个客户争抢锁或租约时,锁决定哪个客户成功获得。 +当几个客户争抢锁或租约时,决定哪个客户端成功获得锁。 ***成员/协调服务*** @@ -940,23 +941,25 @@ ZooKeeper和朋友们可以看作是成员服务研究的悠久历史的一部 当多个事务同时尝试使用相同的键创建冲突记录时,约束必须决定哪一个允许,哪个会违反约束而失败。 -如果您只有一个节点,或者您愿意将决策功能分配给单个节点,所有这些都很简单。这就是在一个单独的领导者数据库中发生的事情:决策的所有权力归属于领导者,这就是为什么这样的数据库能够提供线性一致性操作,唯一性约束,完全有序的复制日志等等。 -但是,如果单个领导失败,或者如果网络中断导致领导不可达,则这样的系统变得无法取得进展。处理这种情况有三种方法: -1. 等待领导者恢复,同时接受系统将被阻止。许多XA/JTA事务协调者选择这个选项。这种方法并不能完全解决共识,因为它不能满足终止财产的要求:如果领导者没有恢复,系统可以被永久封锁。 -2. 通过让人类选择一个新的领导者节点并重新配置系统来使用它来手动故障切换。许多关系数据库都采用这种方法。这是一种“上帝的行为”的共识——计算机系统之外的操作人员做出决定。故障转移的速度受到人类行动速度的限制,通常比计算机慢。 -3. 使用算法自动选择一个新的领导。这种方法需要一个一致的算法,建议使用经过验证的算法来正确处理不利的网络条件【107】。 +如果你只有一个节点,或者你愿意将决策功能分配给单个节点,所有这些都很简单。这就是在一个单独的领导者数据库中发生的事情:决策的所有权力归属于领导者,这就是为什么这样的数据库能够提供线性一致性操作,唯一性约束,完全有序的复制日志等等。 -尽管一个单独的领导者数据库可以提供线性一致性,而不需要在每个写作上执行一致的算法,但是仍然需要达成共识以保持领导力和领导力的改变。因此,从某种意义上说,有一个领导者只是“把罐子放在路上”:共识还是需要的,只是在一个不同的地方,而不是频繁的。好消息是,容错算法和共识系统存在,我们在本章中简要地讨论它们。 +但是,如果单个领导失败,或者如果网络中断导致主库不可达,这样的系统变得无法取得进展。处理这种情况有三种方法: -像ZooKeeper这样的工具在提供应用程序可以使用的“外包”协议,故障检测和会员服务方面起着重要的作用。使用起来并不容易,但比开发自己的算法要好得多,可以承受第8章讨论的所有问题。如果你发现自己想要做一个可以归结为一致的东西,而且你想要它要容错,那么建议使用类似ZooKeeper的东西。 +1. 等待领导者恢复,同时系统将阻止接受新请求。许多XA/JTA事务协调者选择这个选项。这种方法并不能完全解决共识,因为它不能满足终止属性的要求:如果领导者没有恢复,系统可能会被永久封锁。 +2. 通过让人类选择一个新的领导者节点,并重新配置系统,执行手动故障切换来使用它。许多关系数据库都采用这种方法。这是一种“上帝的行为”的共识——计算机系统之外的运维人员做出决定。故障转移的速度受到人类行动速度的限制,通常比计算机慢。 +3. 使用算法自动选择一个新的领导。这种方法需要一个共识算法,建议使用经过验证的算法来正确处理不利的网络条件【107】。 -尽管如此,并不是每个系统都需要达成共识:例如,无领导者和多领导者复制系统通常不会使用全局共识。这些系统中出现的冲突(参见“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间达成共识的结果,但也许没关系:也许我们只需要处理没有线性一致性的东西,学会更好地工作具有分支和合并版本历史记录的数据。 +尽管单主数据库可以提供线性一致性,而不需要在每个写操作上执行共识算法,但对于保持领导权和领导权的变更,共识仍然是需要的。因此从某种意义上说,使用单个领导者不过是“缓兵之计”:共识还是需要的,只是在不同的地方,而且没那么频繁。好消息是,容错算法和共识系统是存在的,我们在本章中简要地讨论了它们。 -本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于通知这一领域的实际工作是非常有价值的:它们帮助我们推理什么可以做,不可以做什么,帮助我们找到违反直觉的方法其中分布式系统往往是有缺陷的。如果你有时间,这些参考资料是值得探索的。 +像ZooKeeper这样的工具为应用提供了“外包”的共识、故障检测和成员服务。它们扮演了重要的角色,虽说用起来并不容易,但总比自己去开发一个能经受[第8章](ch8.md)中所有问题考验的算法要好得多。如果你发现自己想要做的是一个可以归结为共识的东西,并且希望它能容错,使用一个类似ZooKeeper的东西是明智之举。 -我们在本书第二部分的末尾介绍了复制([第5章](ch5.md)),分区([第6章](ch6.md)),事务([第7章](ch7.md)),分布式系统故障模型([第8章](ch8.md))以及最后的一致性和共识([第9章](ch9.md))。现在我们已经奠定了坚实的理论基础,在[第三部分](part-iii.md)我们将再次转向更实用的系统,并讨论如何从异构构建块中构建强大的应用程序。 +尽管如此,并不是每个系统都需要共识:例如,无主复制和多主复制系统通常不会使用全局共识。这些系统中出现的冲突(参见“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间没有达成共识的结果,但也许没关系:也许我们只需要简单地在没有线性一致性的环境下应对,并学会更好地与具有分支和合并版本历史的数据打交道。 + +本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于通知这一领域的实际工作是非常有价值的:它们帮助我们推理什么可以做,什么不可以做,帮助我们找到违反直觉的方法,其中分布式系统往往是有缺陷的。如果你有时间,这些参考资料是值得探索的。 + +这里就到了本书[第二部分](part-ii.md)的末尾,第二部介绍了复制([第5章](ch5.md)),分区([第6章](ch6.md)),事务([第7章](ch7.md)),分布式系统故障模型([第8章](ch8.md))以及最后的一致性和共识([第9章](ch9.md))。现在我们已经奠定了坚实的理论基础,在[第三部分](part-iii.md)我们将再次转向更实用的系统,并讨论如何从异构的组件积木块中构建强大的应用。 From b70136534e5fb5b538d00515e66ae180f7a4706d Mon Sep 17 00:00:00 2001 From: Vonng Date: Mon, 26 Mar 2018 03:09:34 +0800 Subject: [PATCH 03/17] ch10 rough trans --- README.md | 2 +- ch10.md | 431 +++++++++++++++++++++++++++--------------------------- ch11.md | 126 ++++++++-------- ch12.md | 210 +++++++++++++------------- ch3.md | 2 +- 5 files changed, 389 insertions(+), 382 deletions(-) diff --git a/README.md b/README.md index c1574b4..47ec489 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ | 第八章:分布式系统中的问题 | 初翻 | | | 第九章:一致性与共识 | 初翻 65% | Vonng | | 第三部分:前言 | 精翻 | | -| 第十章:批处理 | 机翻 | | +| 第十章:批处理 | 草翻 | | | 第十一章:流处理 | 机翻 | | | 第十二章:数据系统的未来 | 机翻 | | | 术语表 | - | | diff --git a/ch10.md b/ch10.md index 21f7d28..acb5ffe 100644 --- a/ch10.md +++ b/ch10.md @@ -2,7 +2,7 @@ ![](img/ch10.png) -> 带有太强个人色彩的系统无法成功。当第一版健壮的设计完成时,不同的人们以自己的方式来测试,真正的考验才开始。 +> 带有太强个人色彩的系统无法成功。当第一版健壮的设计完成时,不同的人们以自己的方式来测试时,真正的考验才开始。 > > ——高德纳 @@ -10,61 +10,58 @@ [TOC] -在本书的前两部分中,我们讨论了很多关于请求和查询以及相应的响应或结果。这种数据处理方式在许多现代数据系统中都是假设的:你要求什么,或者发送指令,一段时间后系统(希望)会给你一个答案。数据库,缓存,搜索索引,Web服务器以及其他许多系统都以这种方式工作。 +在本书的前两部分中,我们讨论了很多关于**请求(requests)**和**查询(queries)**以及相应的**响应(response)**或**结果(results)**。在许多现代数据系统中都假设采用这种数据处理方式:你要求某些东西,或者发送指令,一段时间后(希望)系统会给你一个答案。数据库,缓存,搜索索引,Web服务器以及其他许多系统都以这种方式工作。 -在这样的在线系统中,无论是浏览器请求页面还是调用远程API的服务,我们通常都假设请求是由人类用户触发的,并且用户正在等待响应。他们不必等太久,所以我们非常重视这些系统的响应时间(请参阅第13页的“描述性能”)。 +在这类**在线(online)**系统中,无论是浏览器请求页面还是调用远程API的服务,我们通常都假设请求是由人类用户触发的,且用户正在等待响应。他们不必等太久,所以我们非常重视这些系统的响应时间(参阅“[描述性能](ch1.md)”)。 -Web和越来越多的基于HTTP / REST的API使交互的请求/响应风格变得如此普遍,以至于很容易将其视为理所当然。但我们应该记住,这不是构建系统的唯一方式,其他方法也有其优点。我们来区分三种不同类型的系统: -服务(在线系统) +Web和越来越多的基于HTTP/REST的API使交互的请求/响应风格变得如此普遍,以至于很容易将其视为理所当然。但我们应该记住,这不是构建系统的唯一方式,其他方法也有其优点。我们来区分三种不同类型的系统: + +***服务(在线系统)*** 服务等待客户的请求或指令到达。当收到一个,服务试图尽快处理它,并发回一个响应。响应时间通常是服务性能的主要衡量指标,可用性通常非常重要(如果客户端无法访问服务,用户可能会收到错误消息)。 ***批处理系统(离线系统)*** -一个批处理系统需要大量的输入数据,运行一个工作来处理它,并产生一些输出数据。工作往往需要一段时间(从几分钟到几天),所以通常不会有用户等待工作完成。相反,批量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(通过特定大小的输入数据集所需的时间)。我们讨论本章中的批处理。 +一个批处理系统需要大量的输入数据,运行一个工作来处理它,并产生一些输出数据。工作往往需要一段时间(从几分钟到几天),所以通常不会有用户等待工作完成。相反,批量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(通过特定大小的输入数据集所需的时间)。我们将在本章中讨论批处理。 ***流处理系统(近实时系统)*** -流处理是在线和离线/批处理之间的一个地方(所以有时候被称为近实时或近线处理)。像批处理系统一样,流处理器消耗输入并产生输出(而不是响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则使用固定的一组输入数据进行操作。这种差异使流处理系统比等效的批处理系统具有更低的延迟。由于流处理基于批处理,我们将在第11章讨论它。 +流处理是在线和离线/批处理之间的一个地方(所以有时候被称为近实时或近线处理)。像批处理系统一样,流处理器消耗输入并产生输出(而不是响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则使用固定的一组输入数据进行操作。这种差异使流处理系统比等效的批处理系统具有更低的延迟。由于流处理基于批处理,我们将在[第11章](ch11.md)讨论它。 -正如我们将在本章中看到的那样,批量处理是构建可靠,可扩展和可维护应用程序的重要组成部分。例如,2004年发布的批处理算法Map-Reduce(可能过度热情地)被称为“使得Google具有如此大规模可扩展性的算法”[2]。随后在各种开源数据系统中实施,包括Hadoop,CouchDB和MongoDB。 +正如我们将在本章中看到的那样,批量处理是构建可靠,可扩展和可维护应用程序的重要组成部分。例如,2004年发布的批处理算法Map-Reduce(可能过热地)被称为“使得Google具有如此大规模可扩展性的算法”【2】。随后在各种开源数据系统中实现,包括Hadoop,CouchDB和MongoDB。 -与多年前为数据仓库开发的并行处理系统[3,4]相比,MapReduce是一个相当低级别的编程模型,但它在处理规模方面迈出了重要的一步。在商品硬件上。虽然MapReduce的重要性正在下降[5],但它仍然值得理解,因为它提供了批处理为什么以及如何有用的清晰画面。 - -实际上,批处理是一种非常古老的计算形式。早在可编程数字计算机诞生之前,打孔卡制表机(例如1890年美国人口普查[6]中使用的霍尔里斯机)实现了半机械化的批处理形式,以计算来自大量输入的汇总统计量。 Map-Reduce与1940年代和1950年代广泛用于商业数据处理的机电IBM卡片分类机器有着惊人的相似之处[7]。像往常一样,历史有重演的趋势。 - -在本章中,我们将看看MapReduce和其他一些批处理算法和框架,并探讨它们在现代数据系统中的使用方式。但首先,要开始,我们将看看使用标准Unix工具的数据处理。即使你已经熟悉了它们,Unix的哲学提醒也是值得的,因为从Unix的想法和经验教训转移到大规模,异构的分布式数据系统。 +与多年前为数据仓库开发的并行处理系统【3,4】相比,MapReduce是一个相当低级别的编程模型,但它对于在商用硬件上的处理规模上迈出了重要的一步。。虽然MapReduce的重要性正在下降【5】,但它仍然值得理解,因为它提供了一副批处理为什么,以及如何有用的清晰画面。 +实际上,批处理是一种非常古老的计算形式。早在可编程数字计算机诞生之前,打孔卡制表机(例如1890年美国人口普查【6】中使用的霍尔里斯机)实现了半机械化的批处理形式,以计算来自大量输入的汇总统计量。 Map-Reduce与1940年代和1950年代广泛用于商业数据处理的机电IBM卡片分类机器有着惊人的相似之处【7】。像往常一样,历史总是在不断重复自己。 +在本章中,我们将看看MapReduce和其他一些批处理算法和框架,并探讨它们在现代数据系统中的使用方式。但首先,我们将从如何使用标准Unix工具进行数据处理开始。即使你已经熟悉了它们,也值得重温一下Unix哲学,因为从Unix的想法和经验教训能转移到大规模,异构的分布式数据系统。 ## 使用Unix工具的批处理 -我们从一个简单的例子开始。假设您有一台Web服务器,每次提供请求时都会在日志文件中附加一行。例如,使用nginx默认访问日志格式,日志的一行可能如下所示: +我们从一个简单的例子开始。假设你有一台Web服务器,每次提供请求时都会在日志文件中附加一行。例如,使用nginx默认访问日志格式,日志的一行可能如下所示: -``` +```bash 216.58.210.78 - - [27/Feb/2015:17:55:11 +0000] "GET /css/typography.css HTTP/1.1" 200 3377 "http://martin.kleppmann.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36" ``` -(实际上这只是一行,为了便于阅读,它只是分成多行)。这一行中有很多信息。为了解释它,你需要看看日志格式的定义,如下所示:: +(实际上这只是一行,为了可读性拆分为多行)。这一行中有很多信息。为了解释它,你需要看一看日志格式的定义,如下所示:: -``` +```bash $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" ``` -因此,日志的这一行表明,在2015年2月27日17:55:11 UTC,服务器从客户端IP地址216.58.210.78接收到文件`/css/typography.css`的请求。用户没有被认证,所以`$remote_user`被设置为连字符(`-` )。响应状态是200(即请求成功),响应的大小是3377字节。网页浏览器是Chrome 40,并且它加载了该文件,因为它是在URL `http://martin.kleppmann.com/`的页面中引用的。 - - +因此,日志的这一行表明,在`2015-02-27 17:55:11 UTC`,服务器从客户端IP地址`216.58.210.78`接收到文件`/css/typography.css`的请求。用户没有被认证,所以`$remote_user`被设置为连字符(`-` )。响应状态是200(即请求成功),响应的大小是3377字节。网页浏览器是Chrome 40,并且它加载了该文件,因为它是在URL `http://martin.kleppmann.com/`的页面中引用的。 ### 分析简单日志 -各种工具可以把这些日志文件,并产生漂亮的报告有关您的网站流量,但为了锻炼,让我们建立自己的,使用基本的Unix工具。 例如,假设你想在你的网站上找到五个最受欢迎的网页。 你可以在Unix shell中这样做:[^i] +各种工具可以把这些日志文件,并产生漂亮的报告有关你的网站流量,但出于练习的目的,让我们使用基本的Unix工具自己写一个。 例如,假设你想在你的网站上找到五个最受欢迎的网页。 可以在Unix shell中这样做:[^i] [^i]: 有些人喜欢抬杠,认为`cat`这里并没有必要,因为输入文件可以直接作为awk的参数。 但这种写法让流水线更显眼。 @@ -80,11 +77,11 @@ cat /var/log/nginx/access.log | #1 1. 读取日志文件 2. 将每一行按空格分割成不同的字段,每行只输出第七个这样的字段,恰好是请求的URL。在我们的示例行中,这个请求URL是`/css/typography.css`。 3. 按字母顺序排列请求的URL列表。如果某个URL被请求过n次,那么排序后,该文件将包含连续n次重复的URL。 -4. uniq命令通过检查两条相邻的行是否相同来过滤掉其输入中的重复行。 -c选项告诉它也输出一个计数器:对于每个不同的URL,它会报告输入中出现该URL的次数。 -5. 第二种排序按每行起始处的数字(-n)排序,这是请求URL的次数。然后以反向(-r)顺序返回结果,即首先以最大的数字返回结果。 -6. 最后,头只输出输入的前五行(-n 5),并丢弃其余的。该系列命令的输出如下所示: +4. uniq命令通过检查两条相邻的行是否相同来过滤掉其输入中的重复行。 `-c`选项告诉它也输出一个计数器:对于每个不同的URL,它会报告输入中出现该URL的次数。 +5. 第二种排序按每行起始处的数字(`-n`)排序,这是请求URL的次数。然后以反向(`-r`)顺序返回结果,即首先以最大的数字返回结果。 +6. 最后,头只输出输入的前五行(`-n 5`),并丢弃其余的。该系列命令的输出如下所示: -``` +```bash 4189 /favicon.ico 3631 /2013/05/24/improving-security-of-ssh-private-keys.html 2124 /2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html @@ -92,19 +89,19 @@ cat /var/log/nginx/access.log | #1 915 /css/typography.css ``` -尽管如果你不熟悉Unix工具,上面的命令行可能看起来有点模糊,但是它非常强大。它将在几秒钟内处理千兆字节的日志文件,您可以轻松修改分析以适应您的需求。例如,如果要从报告中省略CSS文件,请将awk参数更改为`'$7 !~ /\.css$/ {print $7}'`等等。 +尽管如果你不熟悉Unix工具,上面的命令行可能看起来有点模糊,但是它非常强大。它将在几秒钟内处理千兆字节的日志文件,你可以轻松修改分析以适应你的需求。例如,如果要从报告中省略CSS文件,请将`awk`参数更改为`'$7 !~ /\.css$/ {print $7}'`等等。 -本书中没有空间来详细探索Unix工具,但是非常值得学习。令人惊讶的是,使用awk,sed,grep,sort,uniq和xargs的组合,可以在几分钟内完成许多数据分析,并且它们的表现令人惊讶地很好[8]。 +本书中没有空间来详细探索Unix工具,但是非常值得学习。令人惊讶的是,使用awk,sed,grep,sort,uniq和xargs的组合,可以在几分钟内完成许多数据分析,并且它们的表现令人惊讶地很好【8】。 #### 命令链与自定义程序 -而不是Unix命令链,你可以写一个简单的程序来做同样的事情。例如,在Ruby中,它可能看起来像这样: +除了Unix命令链,你还可以写一个简单的程序来做同样的事情。例如在Ruby中,它可能看起来像这样: ```ruby counts = Hash.new(0) # 1 File.open('/var/log/nginx/access.log') do |file| file.each do |line| - url = line.split[6] # 2 + url = line.split【6】 # 2 counts[url] += 1 # 3 end end @@ -113,72 +110,76 @@ top5 = counts.map{|url, count| [count, url] }.sort.reverse[0...5] # 4 top5.each{|count, url| puts "#{count} #{url}" } # 5 ``` -1. `counts`是一个哈希表,保持一个计数器的次数,我们已经看到每个网址。计数器默认为零。 -2. 从日志的每一行,我们都把URL作为第七个空格分隔的字段(这里的数组索引是6,因为Ruby的数组是零索引的)。 +1. `counts`是一个哈希表,保存了我们浏览每个URL次数的计数器,计数器默认为零。 +2. 对日志的每一行,从第七个空格分隔的字段提取URL(这里的数组索引是6,因为Ruby的数组索引从零开始)。 3. 增加日志当前行中URL的计数器。 4. 按计数器值(降序)对散列表内容进行排序,并取前五位。 5. 打印出前五个条目。 -这个程序并不像Unix管道那样简洁,但是它的可读性很强,你喜欢的两个中的哪一个是味道的一部分。但是,除了两者之间的表面差异之外,执行流程也有很大差异,如果您在大文件上运行此分析,则会变得明显。 +这个程序并不像Unix管道那样简洁,但是它的可读性很强,你喜欢哪一个属于口味的问题。但两者除了表面上的差异之外,执行流程也有很大差异,如果你在大文件上运行此分析,则会变得明显。 #### 排序与内存中的聚合 Ruby脚本保存一个URL的内存哈希表,其中每个URL映射到它已经被看到的次数。 Unix流水线的例子没有这样一个哈希表,而是依赖于对一个URL列表进行排序,在这个URL列表中,同一个URL的多个发生只是简单的重复。 -哪种方法更好?这取决于你有多少个不同的网址。对于大多数中小型网站,您可能可以适应所有不同的URL,并且可以为每个网址(例如1GB内存)提供一个计数器。在此示例中,作业的工作集(作业需要随机访问的内存量)仅取决于不同URL的数量:如果单个URL有一百万个日志条目,则散列中所需的空间表仍然只有一个URL加上计数器的大小。如果这个工作集足够小,那么内存散列表工作正常,甚至在笔记本电脑上也是如此。 +哪种方法更好?这取决于你有多少个不同的网址。对于大多数中小型网站,你可能可以适应所有不同的URL,并且可以为每个网址(例如1GB内存)提供一个计数器。在此示例中,作业的工作集(作业需要随机访问的内存量)仅取决于不同URL的数量:如果单个URL有一百万个日志条目,则散列中所需的空间表仍然只有一个URL加上计数器的大小。如果这个工作集足够小,那么内存散列表工作正常,甚至在笔记本电脑上也是如此。 -另一方面,如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与我们在第74页的“SSTables和LSM-Trees”中讨论过的原理是一样的:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序的段可以合并为一个更大的排序文件。 Mergesort具有在磁盘上运行良好的顺序访问模式。 (请记住,在顺序I / O中进行优化是第3章中反复出现的主题。这里再次出现相同的模式。) +另一方面,如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与我们在第74页的“[SSTables和LSM树](ch3.md#SSTables和LSM树)”中讨论过的原理是一样的:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序的段可以合并为一个更大的排序文件。 归并排序具有在磁盘上运行良好的顺序访问模式。 (请记住,优化为顺序I/O是[第3章](ch3.md)中反复出现的主题。这里再次出现相同的模式。) -GNU Coreutils(Linux)中的排序实用程序通过溢出到磁盘自动处理大于内存的数据集,并自动并行排序跨多个CPU核心[9]。这意味着我们之前看到的简单的Unix命令链很容易扩展到大数据集,而不会耗尽内存。瓶颈可能是从磁盘读取输入文件的速度。 +GNU Coreutils(Linux)中的排序实用程序通过溢出到磁盘自动处理大于内存的数据集,并自动并行排序跨多个CPU核心【9】。这意味着我们之前看到的简单的Unix命令链很容易扩展到大数据集,而不会耗尽内存。瓶颈可能是从磁盘读取输入文件的速度。 ### Unix哲学 -我们可以非常容易地使用前一个例子中的一系列命令来分析日志文件,这并非巧合:事实上,这实际上是Unix的关键设计思想之一,而且它今天依然令人惊讶。让我们更深入地研究一下,这样我们可以从Unix中借鉴一些想法[10]。 +我们可以非常容易地使用前一个例子中的一系列命令来分析日志文件,这并非巧合:事实上,这实际上是Unix的关键设计思想之一,且它今天仍然令人惊讶地关联。让我们更深入地研究一下,以便从Unix中借鉴一些想法【10】。 -Unix管道的发明者道格·麦克罗伊(Doug McIlroy)在1964年首先描述了这种情况[11]:“当需要以另一种方式处理数据时,我们应该有一些连接程序的方法,比如[a] 。这也是I / O的方式。“管道类比困难了,连接程序和管道的想法成为了现在被称为Unix哲学的一部分 - 一套在开发者中流行的设计原则。 Unix的用户。哲学在1978年描述如下[12,13]: +Unix管道的发明者道格·麦克罗伊(Doug McIlroy)在1964年首先描述了这种情况【11】:“当需要以另一种方式处理数据时,我们应该有一些连接程序的方法,比如[a] 。这也是I/O的方式。“管道类比困难了,连接程序和管道的想法成为了现在被称为**Unix哲学**的一部分 —— 一套在开发者中流行的设计原则。 Unix哲学在1978年描述如下【12,13】: -1. 让每个程序都做好一件事。要做好新的工作,重新建立一个新的“特征”,而不是使旧的计划复杂化。 -2. 期待每个程序的输出成为另一个程序的输入。不要混淆输出与无关的信息。避免使用严格的柱状或二进制输入格式。不要坚持交互式输入。 +1. 让每个程序都做好一件事。要做一件新的工作,写一个新程序,而不是通过添加“功能”让老程序复杂化。 +2. 期待每个程序的输出成为另一个程序的输入。不要将无关信息混入输出。避免使用严格的列数据或二进制输入格式。不要坚持交互式输入。 3. 设计和构建软件,甚至是操作系统,要尽早尝试,最好在几周内完成。不要犹豫,扔掉笨拙的部分,重建它们。 -4. 使用工具优先于不熟练的帮助来减轻编程任务,即使您必须绕道建立工具,并期望在完成使用后将其中一些工具扔掉。 +4. 优先使用工具,而不是不熟练的帮助来减轻编程任务,即使必须曲线救国编写工具,并期望在用完后扔掉大部分。 -这种方法 - 自动化,快速原型设计,迭代式迭代,对实验友好,将大型项目分解成可管理的块 - 听起来非常像今天的敏捷和DevOps运动。奇怪的是,四十年来变化不大。 +这种方法 —— 自动化,快速原型设计,迭代式迭代,实验友好,将大型项目分解成可管理的块 —— 听起来非常像今天的敏捷开发和DevOps运动。奇怪的是,四十年来变化不大。 -排序工具是一个很好的例子。它可以说是一个比大多数编程语言在其标准库(不会溢出到磁盘并且不使用多线程,即使是有益的)中更好的排序实现。然而,这种分类几乎没有用处。它只能与其他Unix工具(如uniq)结合使用。 +排序工具是一个很好的例子。它可以说是一个比大多数编程语言在其标准库(不会溢出到磁盘并且不使用多线程,即使是有益的)中更好的排序实现。然而,单独使用`sort` 几乎没什么用。它只能与其他Unix工具(如`uniq`)结合使用。 像bash这样的Unix shell可以让我们轻松地将这些小程序组合成令人惊讶的强大数据处理作业。尽管这些程序中有很多是由不同人群编写的,但它们可以灵活地结合在一起。 Unix如何实现这种可组合性? #### 统一的接口 -如果您希望一个程序的输出成为另一个程序的输入,那意味着这些程序必须使用相同的数据格式 - 换句话说,一个兼容的接口。如果您希望能够将任何程序的输出连接到任何程序的输入,那意味着所有程序必须使用相同的输入/输出接口。 +如果你希望一个程序的输出成为另一个程序的输入,那意味着这些程序必须使用相同的数据格式 —— 换句话说,一个兼容的接口。如果你希望能够将任何程序的输出连接到任何程序的输入,那意味着所有程序必须使用相同的I/O接口。 -在Unix中,该接口是一个文件(更准确地说,是一个文件描述符)。一个文件只是一个有序的字节序列。因为这是一个非常简单的接口,所以可以使用相同的接口来表示许多不同的东西:文件系统上的实际文件,到另一个进程(Unix套接字,stdin,stdout)的通信通道,设备驱动程序(比如`/dev/audio`或`/dev/lp0`),表示TCP连接的套接字等等。理所当然的事很容易,但实际上这些非常不同的事物可以共享一个统一的界面,所以它们可以很容易地连接在一起[^ii]。 +在Unix中,这种接口是一个**文件(file)**(更准确地说,是一个**文件描述符(file descriptor)**)。一个文件只是一串有序的字节序列。因为这是一个非常简单的接口,所以可以使用相同的接口来表示许多不同的东西:文件系统上的真实文件,到另一个进程(Unix套接字,stdin,stdout)的通信通道,设备驱动程序(比如`/dev/audio`或`/dev/lp0`),表示TCP连接的套接字等等。很容易将这些视为理所当然的事情,但实际上这是非常出色的设计:这些非常不同的事物可以共享一个统一的接口,所以它们可以很容易地连接在一起[^ii]。 [^ii]: 统一接口的另一个例子是URL和HTTP,这是Web的基础。 一个URL标识一个网站上的一个特定的东西(资源),你可以链接到任何其他网站的任何网址。 具有网络浏览器的用户因此可以通过跟随链接在网站之间无缝跳转,即使服务器可能由完全不相关的组织操作。 这个原则今天似乎很明显,但它是使网络取得今天成功的关键。 之前的系统并不是那么统一:例如,在公告板系统(BBS)时代,每个系统都有自己的电话号码和波特率配置。 从一个BBS到另一个BBS的引用必须以电话号码和调制解调器设置的形式; 用户将不得不挂断,拨打其他BBS,然后手动找到他们正在寻找的信息。 这是不可能的直接链接到另一个BBS内的一些内容。 -按照惯例,许多(但不是全部)Unix程序将这个字节序列视为ASCII文本。我们的日志分析示例使用了这个事实:awk,sort,uniq和head都将它们的输入文件视为由`\n`(换行符,ASCII 0x0A)字符分隔的记录列表。 `\n`的选择是任意的 - 可以说,ASCII记录分隔符`0x1E`本来就是一个更好的选择,因为它是为了这个目的而设计的[14],但是无论如何,所有这些程序都使用相同的记录分隔符允许它们互操作。 +按照惯例,许多(但不是全部)Unix程序将这个字节序列视为ASCII文本。我们的日志分析示例使用了这个事实:awk,sort,uniq和head都将它们的输入文件视为由`\n`(换行符,ASCII 0x0A)字符分隔的记录列表。 `\n`的选择是任意的 —— 可以说,ASCII记录分隔符`0x1E`本来就是一个更好的选择,因为它是为了这个目的而设计的【14】,但是无论如何,所有这些程序都使用相同的记录分隔符允许它们互操作。 -每个记录(即一行输入)的解析更加模糊。 Unix工具通常通过空白或制表符将行分割成字段,但也使用CSV(逗号分隔),管道分隔和其他编码。即使像xargs这样一个相当简单的工具也有六个命令行选项,用于指定如何解析输入。 +每个记录(即一行输入)的解析更加模糊。 Unix工具通常通过空白或制表符将行分割成字段,但也使用CSV(逗号分隔),管道分隔和其他编码。即使像`xargs`这样一个相当简单的工具也有六个命令行选项,用于指定如何解析输入。 -ASCII文本的统一接口主要工作,但它不是很漂亮:我们的日志分析示例使用`{print $ 7}`来提取网址,这是不是很可读。在理想的世界中,这可能是`{print $ request_url}`或类似的东西。我们稍后会回到这个想法。 +ASCII文本的统一接口主要工作,但它不是很漂亮:我们的日志分析示例使用`{print $7}`来提取网址,这样可读性不是很好。在理想的世界中,这可能是`{print $request_url}`或类似的东西。我们稍后会回到这个想法。 -尽管几十年后还不够完美,但统一的Unix界面仍然非常显着。与Unix工具一样,软件的交互操作和编写并不是很多,您不能通过自定义分析工具轻松地将电子邮件帐户的内容和在线购物历史记录传送到电子表格中,并将结果发布到社交网络或维基。今天,像Unix工具一样流畅地运行程序是一个例外,而不是规范。 +尽管几十年后还不够完美,但统一的Unix接口仍然是非常出色的设计。与Unix工具一样,软件的交互操作和编写并不是很多,你不能通过自定义分析工具轻松地将电子邮件帐户的内容和在线购物历史记录传送到电子表格中,并将结果发布到社交网络或维基。今天,像Unix工具一样流畅地运行程序是一个例外,而不是规范。 -即使是具有相同数据模型的数据库,也往往不容易将数据从一个数据模型中移出。这种缺乏整合导致数据的巴尔干化。 +即使是具有**相同数据模型(same data model)**的数据库,将数据从一种导到另一种也并不容易。缺乏整合导致了数据的巴尔干化[^译注i]。 -#### 逻辑和布线的分离 +[^译注i]: ****巴尔干化(Balkanization)**是一个常带有贬义的地缘政治学术语,其定义为:一个国家或政区分裂成多个互相敌对的国家或政区的过程。 -Unix工具的另一个特点是使用标准输入(stdin)和标准输出(stdout)。如果你运行一个程序,而不指定任何其他的东西,标准输入来自键盘和标准输出到屏幕上。但是,您也可以从文件输入和/或将输出重定向到文件。管道允许您将一个进程的标准输出附加到另一个进程的标准输入(具有小内存缓冲区,而不需要将整个中间数据流写入磁盘)。 -程序仍然可以直接读取和写入文件,但如果程序不担心特定的文件路径,只使用标准输入和标准输出,则Unix方法效果最好。这允许shell用户以任何他们想要的方式连接输入和输出;该程序不知道或不关心输入来自哪里以及输出到哪里。 (人们可以说这是一种松耦合,后期绑定[15]或控制反转[16])。将输入/输出接线与程序逻辑分开,可以将小工具组合成更大的系统。 -您甚至可以编写自己的程序,并将它们与操作系统提供的工具组合在一起。你的程序只需要从标准输入读取输入,并将输出写入标准输出,并且可以参与数据处理流水线。在日志分析示例中,您可以编写一个工具将用户代理字符串转换为更灵敏的浏览器标识符,或者将IP地址转换为国家代码的工具,并将其插入管道。排序程序并不关心它是否与操作系统的另一部分或者你写的程序通信。 +#### 逻辑和布线相分离 -但是,使用stdin和stdout可以做什么是有限的。需要多个输入或输出的程序是可能的但棘手的。如果程序直接打开文件进行读取和写入,或者将另一个程序作为子进程启动,或者打开网络连接,则无法将程序的输出传输到网络连接中【17,18】[^iii] 。 I / O由程序本身连接。它仍然可以配置(例如通过命令行选项),但是减少了在Shell中连接输入和输出的灵活性。 +Unix工具的另一个特点是使用标准输入(stdin)和标准输出(stdout)。如果你运行一个程序,而不指定任何其他的东西,标准输入来自键盘,标准输出指向屏幕。但是,你也可以从文件输入和/或将输出重定向到文件。管道允许你将一个进程的标准输出附加到另一个进程的标准输入(具有小内存缓冲区,而不需要将整个中间数据流写入磁盘)。 -[^iii]: 除了使用一个单独的工具,如netcat或curl。 Unix开始试图将所有东西都表示为文件,但是BSD套接字API偏离了这个惯例[17]。研究操作系统Plan 9和Inferno在使用文件方面更加一致:它们将TCP连接表示为/ net / tcp中的文件[18]。 +程序仍然可以直接读取和写入文件,但如果程序不担心特定的文件路径,只使用标准输入和标准输出,则Unix方法效果最好。这允许shell用户以任何他们想要的方式连接输入和输出;该程序不知道或不关心输入来自哪里以及输出到哪里。 (人们可以说这是一种**松耦合(loose coupling)**,**晚期绑定(late binding)**【15】或**控制反转(inversion of control)**【16】)。将输入/输出布线与程序逻辑分开,可以将小工具组合成更大的系统。 + +你甚至可以编写自己的程序,并将它们与操作系统提供的工具组合在一起。你的程序只需要从标准输入读取输入,并将输出写入标准输出,并且可以参与数据处理流水线。在日志分析示例中,你可以编写一个工具将用户代理字符串转换为更灵敏的浏览器标识符,或者将IP地址转换为国家代码的工具,并将其插入管道。排序程序并不关心它是否与操作系统的另一部分或者你写的程序通信。 + +但是,使用stdin和stdout可以做什么是有限的。需要多个输入或输出的程序是可能的但棘手的。如果程序直接打开文件进行读取和写入,或者将另一个程序作为子进程启动,或者打开网络连接,则无法将程序的输出传输到网络连接中【17,18】[^iii] 。 I/O由程序本身连接。它仍然可以配置(例如通过命令行选项),但是减少了在Shell中连接输入和输出的灵活性。 + +[^iii]: 除了使用一个单独的工具,如`netcat`或`curl`。 Unix开始试图将所有东西都表示为文件,但是BSD套接字API偏离了这个惯例【17】。研究用操作系统Plan 9和Inferno在使用文件方面更加一致:它们将TCP连接表示为`/net/tcp`中的文件【18】。 @@ -186,14 +187,15 @@ Unix工具的另一个特点是使用标准输入(stdin)和标准输出(st 使Unix工具如此成功的部分原因是它们使得查看正在发生的事情变得非常容易: -Unix命令的输入文件通常被视为不可变的。这意味着您可以随意运行命令,尝试各种命令行选项,而不会损坏输入文件。 +* Unix命令的输入文件通常被视为不可变的。这意味着你可以随意运行命令,尝试各种命令行选项,而不会损坏输入文件。 -* 您可以在任何时候结束管道,将输出管道输送到较少的位置,然后查看它是否具有预期的形式。这种检查能力对调试非常有用。 -* 您可以将一个流水线阶段的输出写入文件,并将该文件用作下一阶段的输入。这使您可以重新启动后面的阶段,而无需重新运行整个管道。 + +* 你可以在任何时候结束管道,将管道输出到`less`,然后查看它是否具有预期的形式。这种检查能力对调试非常有用。 +* 你可以将一个流水线阶段的输出写入文件,并将该文件用作下一阶段的输入。这使你可以重新启动后面的阶段,而无需重新运行整个管道。 因此,与关系数据库的查询优化器相比,即使Unix工具非常简单,工具简单,但仍然非常有用,特别是对于实验而言。 -然而,Unix工具的最大局限在于它们只能在一台机器上运行 - 而Hadoop这样的工具就是在这里工作的。 +然而,Unix工具的最大局限在于它们只能在一台机器上运行 —— 而Hadoop这样的工具即为此而生。 @@ -207,90 +209,91 @@ MapReduce有点像Unix工具,但分布在数千台机器上。像Unix工具一 和大多数Unix工具一样,运行MapReduce作业通常不会修改输入,除了生成输出外没有任何副作用。输出文件以连续的方式写入一次(一旦写入文件,不会修改任何现有的文件部分)。 -虽然Unix工具使用stdin和stdout作为输入和输出,但MapReduce作业在分布式文件系统上读写文件。在Hadoop的Map-Reduce实现中,该文件系统被称为HDFS(Hadoop分布式文件系统),一个开源的重新实现Google文件系统(GFS)[19]。 +虽然Unix工具使用stdin和stdout作为输入和输出,但MapReduce作业在分布式文件系统上读写文件。在Hadoop的Map-Reduce实现中,该文件系统被称为HDFS(Hadoop分布式文件系统),一个开源的重新实现Google文件系统(GFS)【19】。 -除HDFS外,还有各种其他分布式文件系统,如GlusterFS和Quantcast File System(QFS)[20]。诸如Amazon S3,Azure Blob存储和OpenStack Swift [21]等对象存储服务在很多方面都是相似的[^iv]。在本章中,我们将主要使用HDFS作为示例,但是这些原则适用于任何分布式文件系统。 +除HDFS外,还有各种其他分布式文件系统,如GlusterFS和Quantcast File System(QFS)【20】。诸如Amazon S3,Azure Blob存储和OpenStack Swift 【21】等对象存储服务在很多方面都是相似的[^iv]。在本章中,我们将主要使用HDFS作为示例,但是这些原则适用于任何分布式文件系统。 -[^iv]: 一个不同之处在于,对于HDFS,可以将计算任务安排在存储特定文件副本的计算机上运行,而对象存储通常将存储和计算分开。如果网络带宽是一个瓶颈,从本地磁盘读取有性能优势。但是请注意,如果使用删除编码,局部优势将会丢失,因为来自多台机器的数据必须进行合并以重建原始文件[20]。 +[^iv]: 一个不同之处在于,对于HDFS,可以将计算任务安排在存储特定文件副本的计算机上运行,而对象存储通常将存储和计算分开。如果网络带宽是一个瓶颈,从本地磁盘读取有性能优势。但是请注意,如果使用删除编码,局部优势将会丢失,因为来自多台机器的数据必须进行合并以重建原始文件【20】。 -与网络连接存储(NAS)和存储区域网络(SAN)架构的共享磁盘方法相比,HDFS基于无共享原则(参见第二部分的介绍)。共享磁盘存储由集中式存储设备实现,通常使用定制硬件和专用网络基础设施(如光纤通道)。另一方面,无共享方法不需要特殊的硬件,只需要通过传统数据中心网络连接的计算机。 +与网络连接存储(NAS)和存储区域网络(SAN)架构的共享磁盘方法相比,HDFS基于无共享原则(参见[第二部分](part-ii.md)的介绍)。共享磁盘存储由集中式存储设备实现,通常使用定制硬件和专用网络基础设施(如光纤通道)。另一方面,无共享方法不需要特殊的硬件,只需要通过传统数据中心网络连接的计算机。 HDFS包含在每台机器上运行的守护进程,暴露一个允许其他节点访问存储在该机器上的文件的网络服务(假设数据中心中的每台通用计算机都附带有一些磁盘)。名为NameNode的中央服务器会跟踪哪个文件块存储在哪台机器上。因此,HDFS在概念上创建一个可以使用运行守护进程的所有机器的磁盘上的空间的大文件系统。 -为了容忍机器和磁盘故障,文件块被复制到多台机器上。复制可能意味着多个机器上的相同数据的多个副本,如第5章中所述,或者像Reed-Solomon代码这样的擦除编码方案,它允许以比完全复制更低的存储开销恢复丢失的数据[20, 22。这些技术与RAID相似,可以在连接到同一台机器的多个磁盘上提供冗余;区别在于在分布式文件系统中,文件访问和复制是在传统的数据中心网络上完成的,没有特殊的硬件。 +为了容忍机器和磁盘故障,文件块被复制到多台机器上。复制可能意味着多个机器上的相同数据的多个副本,如[第5章](ch5.md)中所述,或者像Reed-Solomon代码这样的擦除编码方案,它允许以比完全复制更低的存储开销恢复丢失的数据【20,22】。这些技术与RAID相似,可以在连接到同一台机器的多个磁盘上提供冗余;区别在于在分布式文件系统中,文件访问和复制是在传统的数据中心网络上完成的,没有特殊的硬件。 -HDFS已经很好地扩展了:在撰写本文时,最大的HDFS部署运行在成千上万台机器上,总存储容量达数百peta-bytes [23]。如此大的规模已经变得可行,因为使用商品硬件和开源软件的HDFS上的数据存储和访问成本远低于专用存储设备上的同等容量[24]。 +HDFS已经很好地扩展了:在撰写本文时,最大的HDFS部署运行在成千上万台机器上,总存储容量达数百PB【23】。如此大的规模已经变得可行,因为使用商品硬件和开源软件的HDFS上的数据存储和访问成本远低于专用存储设备上的同等容量【24】。 ### MapReduce作业执行 -MapReduce是一个编程框架,您可以使用它编写代码来处理HDFS等分布式文件系统中的大型数据集。理解它的最简单方法是参考第391页上的“简单日志分析”中的Web服务器日志分析示例。MapReduce中的数据处理模式与此示例非常相似: +MapReduce是一个编程框架,你可以使用它编写代码来处理HDFS等分布式文件系统中的大型数据集。理解它的最简单方法是参考“[简单日志分析](#简单日志分析)”中的Web服务器日志分析示例。MapReduce中的数据处理模式与此示例非常相似: -1. 读取一组输入文件,并将其分解成记录。在Web服务器日志示例中,每条记录都是日志中的一行(即\ n是记录分隔符)。 -2. 调用Mapper函数从每个输入记录中提取一个键和值。在前面的例子中,mapper函数是`awk'{print $ 7}'`:它提取`URL($7)`作为关键字,并将值保留为空。 +1. 读取一组输入文件,并将其分解成记录。在Web服务器日志示例中,每条记录都是日志中的一行(即`\n`是记录分隔符)。 +2. 调用Mapper函数从每个输入记录中提取一个键和值。在前面的例子中,mapper函数是`awk'{print $7}'`:它提取`URL($7)`作为关键字,并将值保留为空。 3. 按键排序所有的键值对。在日志示例中,这由第一个排序命令完成。 -4. 调用reducer函数遍历排序后的键值对。如果同一个键出现多次,排序使它们在列表中相邻,所以很容易组合这些值而不必在内存中保留很多状态。在前面的例子中,reducer是由uniq -c命令实现的,该命令使用相同的密钥来统计相邻记录的数量。 +4. 调用reducer函数遍历排序后的键值对。如果同一个键出现多次,排序使它们在列表中相邻,所以很容易组合这些值而不必在内存中保留很多状态。在前面的例子中,reducer是由`uniq -c`命令实现的,该命令使用相同的键来统计相邻记录的数量。 -这四个步骤可以由一个MapReduce作业执行。步骤2(地图)和4(减少)是您编写自定义数据处理代码的地方。步骤1(将文件分解成记录)由输入格式解析器处理。步骤3中的排序步骤隐含在MapReduce中 - 您不必编写它,因为映射器的输出始终在给予reducer之前进行排序。 +这四个步骤可以由一个MapReduce作业执行。步骤2(Map)和4(Reduce)是你编写自定义数据处理代码的地方。步骤1(将文件分解成记录)由输入格式解析器处理。步骤3中的排序步骤隐含在MapReduce中 —— 你不必编写它,因为Mapper的输出始终在送往reducer之前进行排序。 -要创建MapReduce作业,您需要实现两个回调函数,mapper和reducer,其行为如下(另请参阅“MapReduce查询”(第46页)): +要创建MapReduce作业,你需要实现两个回调函数,mapper和reducer,其行为如下(参阅“[MapReduce查询](ch2.md#MapReduce查询)”): ***Mapper*** -每个输入记录都会调用一次映射器,其工作是从输入记录中提取键和值。对于每个输入,它可以生成任意数量的键值对(包括无)。它不会保留从一个输入记录到下一个记录的任何状态,因此每个记录都是独立处理的。 +每个输入记录都会调用一次Mapper,其工作是从输入记录中提取键和值。对于每个输入,它可以生成任意数量的键值对(包括None)。它不会保留从一个输入记录到下一个记录的任何状态,因此每个记录都是独立处理的。 ***Reducer*** -MapReduce框架采用由映射器生成的键值对,收集属于同一个键的所有值,并使用迭代器调用reducer以调用该值集合。 Reducer可以产生输出记录(例如相同URL的出现次数)。 +MapReduce框架采用由Mapper生成的键值对,收集属于同一个键的所有值,并使用迭代器调用reducer以调用该值集合。 Reducer可以产生输出记录(例如相同URL的出现次数)。 -在Web服务器日志示例中,我们在第5步中有第二个排序命令,它按请求数对URL进行排序。在MapReduce中,如果您需要第二个排序阶段,则可以通过编写第二个MapReduce作业并将第一个作业的输出用作第二个作业的输入来实现它。这样看来,映射器的作用是将数据放入一个适合排序的表单中,并且还原器的作用是处理已排序的数据。 +在Web服务器日志示例中,我们在第5步中有第二个排序命令,它按请求数对URL进行排序。在MapReduce中,如果你需要第二个排序阶段,则可以通过编写第二个MapReduce作业并将第一个作业的输出用作第二个作业的输入来实现它。这样看来,Mapper的作用是将数据放入一个适合排序的表单中,并且Reducer的作用是处理已排序的数据。 #### 分布式执行MapReduce -Unix命令管道的主要区别在于,MapReduce可以在多台机器上并行执行计算,而无需编写代码来明确处理并行性。映射器和简化器一次只能处理一条记录;他们不需要知道他们的输入来自哪里或者输出什么地方,所以框架可以处理机器之间移动数据的复杂性。 +Unix命令管道的主要区别在于,MapReduce可以在多台机器上并行执行计算,而无需编写代码来明确处理并行性。Mapper和Reducer一次只能处理一条记录;他们不需要知道他们的输入来自哪里或者输出什么地方,所以框架可以处理机器之间移动数据的复杂性。 -在分布式计算中可以使用标准的Unix工具作为映射器和简化器[25],但更常见的是,它们被实现为传统编程语言的函数。在Hadoop MapReduce中,映射器和简化器都是实现特定接口的Java类。在MongoDB和CouchDB中,映射器和简化器都是JavaScript函数(请参阅第46页的“MapReduce查询”)。 +在分布式计算中可以使用标准的Unix工具作为Mapper和Reducer【25】,但更常见的是,它们被实现为传统编程语言的函数。在Hadoop MapReduce中,Mapper和Reducer都是实现特定接口的Java类。在MongoDB和CouchDB中,Mapper和Reducer都是JavaScript函数(参阅“[MapReduce查询](ch2.md#MapReduce查询)”)。 -[图10-1]()显示了Hadoop MapReduce作业中的数据流。其并行化基于分区(参见第6章):作业的输入通常是HDFS中的一个目录,输入目录中的每个文件或文件块都被认为是一个单独的分区,可以单独处理map任务(图10-1中的m 1,m 2和m 3标记)。 +[图10-1]()显示了Hadoop MapReduce作业中的数据流。其并行化基于分区(参见[第6章](ch6.md)):作业的输入通常是HDFS中的一个目录,输入目录中的每个文件或文件块都被认为是一个单独的分区,可以单独处理map任务([图10-1](img/fig10-1.png)中的m1,m2和m3标记)。 -每个输入文件的大小通常是数百兆字节。 MapReduce调度器(图中未显示)试图在其中一台存储输入文件副本的机器上运行每个映射器,只要该机器有足够的备用RAM和CPU资源来运行映射任务[26]。这个原则被称为将数据放在数据附近[27]:它节省了通过网络复制输入文件,减少网络负载和增加局部性。 +每个输入文件的大小通常是数百兆字节。 MapReduce调度器(图中未显示)试图在其中一台存储输入文件副本的机器上运行每个Mapper,只要该机器有足够的备用RAM和CPU资源来运行映射任务【26】。这个原则被称为将数据放在数据附近【27】:它节省了通过网络复制输入文件,减少网络负载和增加局部性。 ![](img/fig10-1.png) **图10-1 具有三个Mapper和三个Reducer的MapReduce任务** -在大多数情况下,应该在映射任务中运行的应用程序代码在分配运行它的任务的计算机上还不存在,所以MapReduce框架首先复制代码(例如Java程序中的JAR文件)到适当的机器。然后启动地图任务并开始读取输入文件,一次将一条记录传递给mapper回调。映射器的输出由键值对组成。 +在大多数情况下,应该在映射任务中运行的应用程序代码在分配运行它的任务的计算机上还不存在,所以MapReduce框架首先复制代码(例如Java程序中的JAR文件)到适当的机器。然后启动Map任务并开始读取输入文件,一次将一条记录传递给Mapper回调。Mapper的输出由键值对组成。 -计算的减少方面也被分割。虽然Map任务的数量由输入文件块的数量决定,但Reducer的任务的数量是由作业作者配置的(它可以不同于地图任务的数量)。为了确保具有相同密钥的所有键值对在相同的缩减器处结束,框架使用密钥的散列值来确定哪个减少的任务应该接收到特定的键值对(参见“通过密钥散列分区”)第203页)。 +计算的减少方面也被分割。虽然Map任务的数量由输入文件块的数量决定,但Reducer的任务的数量是由作业作者配置的(它可以不同于Map任务的数量)。为了确保具有相同键的所有键值对在相同的Reducer处结束,框架使用键的散列值来确定哪个减少的任务应该接收到特定的键值对(参见“[按键散列分区](ch6.md#按键散列分区)”))。 -键值对必须进行排序,但数据集可能太大,无法在单台机器上使用常规排序算法进行排序。相反,分类是分阶段进行的。首先,每个映射任务都基于密钥的散列,通过简化器分割其输出。这些分区中的每一个都被写入映射程序本地磁盘上的已排序文件,使用的技术与我们在第76页的“SSTables and LSM-Trees”中讨论的类似。 +键值对必须进行排序,但数据集可能太大,无法在单台机器上使用常规排序算法进行排序。相反,分类是分阶段进行的。首先,每个映射任务都基于键的散列,通过Reducer分割其输出。这些分区中的每一个都被写入映射程序本地磁盘上的已排序文件,使用的技术与我们在“[SSTables与LSM树](ch3.md#SSTables与LSM树)”中讨论的类似。 -只要映射器完成读取输入文件并写入其排序后的输出文件,MapReduce调度器就会通知减速器他们可以从该映射器开始获取输出文件。减法器连接到每个映射器,并为其分区下载排序后的键值对的文件。通过简化,分类和将数据分区从映射器复制到简化器的过程被称为混洗[26](一个令人困惑的术语 - 不像洗牌一样,在MapReduce中没有随机性)。 +只要Mapper完成读取输入文件并写入其排序后的输出文件,MapReduce调度器就会通知减速器他们可以从该Mapper开始获取输出文件。减法器连接到每个Mapper,并为其分区下载排序后的键值对的文件。通过简化,分类和将数据分区从Mapper复制到Reducer的过程被称为**混洗(shuffle)**【26】(一个令人困惑的术语 —— 不像洗牌一样,在MapReduce中没有随机性)。 -reduce任务从映射器获取文件并将它们合并在一起,并保存排序顺序。因此,如果不同的映射器使用相同的键生成记录,则它们将在合并的缩减器输入中相邻。 +reduce任务从Mapper获取文件并将它们合并在一起,并保存排序顺序。因此,如果不同的Mapper使用相同的键生成记录,则它们将在合并的Reducer输入中相邻。 使用一个键和一个迭代器调用reducer,迭代器使用相同的键(在某些情况下可能不是全部适合内存)逐步扫描所有记录。 Reducer可以使用任意逻辑来处理这些记录,并且可以生成任意数量的输出记录。这些输出记录写入到分布式文件系统上的文件(通常是运行reducer的机器的本地磁盘上的一个副本,其他机器上的副本)。 -MapReduce工作流程 -单个MapReduce作业可以解决的问题范围有限。请参阅日志分析示例,一个MapReduce作业可以确定每个URL的页面浏览次数,但不是最常用的URL,因为这需要第二轮排序。 +#### MapReduce工作流 + +单个MapReduce作业可以解决的问题范围有限。参阅日志分析示例,一个MapReduce作业可以确定每个URL的页面浏览次数,但不是最常用的URL,因为这需要第二轮排序。 因此,将MapReduce作业链接到工作流中是非常常见的,例如,一个作业的输出成为下一个作业的输入。 Hadoop Map-Reduce框架对工作流程没有特别的支持,所以这个链接是通过目录名隐含完成的:第一个作业必须被配置为将其输出写入HDFS中的指定目录,第二个作业必须是配置为读取与其输入相同的目录名称。从MapReduce框架的角度来看,他们是两个独立的工作。 -因此,被链接的MapReduce作业不如Unix命令的流水线(它直接将一个进程的输出作为输入传递给另一个进程,只使用一个小的内存缓冲区),更像是一系列命令,其中每个命令的输出写入临时文件,下一个命令从临时文件中读取。这种设计有利有弊,我们将在第419页“中间状态的物化”中讨论。 +因此,被链接的MapReduce作业不如Unix命令的流水线(它直接将一个进程的输出作为输入传递给另一个进程,只使用一个小的内存缓冲区),更像是一系列命令,其中每个命令的输出写入临时文件,下一个命令从临时文件中读取。这种设计有利有弊,我们将在“[物化中间状态](#物化中间状态)”中讨论。 -当作业成功完成时,批处理作业的输出仅被视为有效(MapReduce丢弃失败作业的部分输出)。因此,工作流程中的一项工作只有在先前的工作 - 即生产其投入方向的工作 - 成功完成时才能开始。处理这些作业之间的依赖关系执行,为Hadoop开发了各种工作流调度器,包括Oozie,Azkaban,Luigi,Airflow和Pinball [28]。 +当作业成功完成时,批处理作业的输出仅被视为有效(MapReduce丢弃失败作业的部分输出)。因此,工作流程中的一项工作只有在先前的工作 —— 即生产其投入方向的工作 —— 成功完成时才能开始。处理这些作业之间的依赖关系执行,为Hadoop开发了各种工作流调度器,包括Oozie,Azkaban,Luigi,Airflow和Pinball 【28】。 -这些调度程序还具有管理功能,在维护大量批处理作业时非常有用。在构建推荐系统[29]时,由50到100个MapReduce作业组成的工作流是常见的,而在大型组织中,许多不同的团队可能运行不同的作业来读取彼此的输出。工具支持对于管理这样复杂的数据流非常重要。 +这些调度程序还具有管理功能,在维护大量批处理作业时非常有用。在构建推荐系统【29】时,由50到100个MapReduce作业组成的工作流是常见的,而在大型组织中,许多不同的团队可能运行不同的作业来读取彼此的输出。工具支持对于管理这样复杂的数据流非常重要。 -Hadoop的各种高级工具(如Pig [30],Hive [31],Cascading [32],Crunch [33]和FlumeJava [34])也设置了多个MapReduce阶段的工作流程 。 +Hadoop的各种高级工具(如Pig 【30】,Hive 【31】,Cascading 【32】,Crunch 【33】和FlumeJava 【34】)也设置了多个MapReduce阶段的工作流程 。 ### Reduce端连接与分组 -我们在第2章中讨论了数据模型和查询语言的联接,但是我们还没有深入探讨联接是如何实现的。现在是我们再次拿起那个线程的时候了。 +我们在[第2章](ch2.md)中讨论了数据模型和查询语言的联接,但是我们还没有深入探讨连接是如何实现的。现在是我们再次拿起那个线程的时候了。 -在许多数据集中,通常一条记录与另一条记录有关联:关系模型中的外键,文档模型中的文档引用或图模型中的边。只要有一些代码需要访问该关联两边的记录(包含引用的记录和被引用的记录),连接就是必需的。正如第2章所讨论的,非规范化可以减少对连接的需求,但通常不会将其完全移除[^v]。 +在许多数据集中,通常一条记录与另一条记录有关联:关系模型中的外键,文档模型中的文档引用或图模型中的边。只要有一些代码需要访问该关联两边的记录(包含引用的记录和被引用的记录),连接就是必需的。正如[第2章](ch2.md)所讨论的,非规范化可以减少对连接的需求,但通常不会将其完全移除[^v]。 -在数据库中,如果执行只涉及少量记录的查询,数据库通常会使用索引来快速定位感兴趣的记录(请参阅第3章)。如果查询涉及连接,则可能需要多个索引查找。然而,MapReduce没有索引的概念 - 至少不是通常意义上的。 +在数据库中,如果执行只涉及少量记录的查询,数据库通常会使用索引来快速定位感兴趣的记录(参阅[第3章](ch3.md))。如果查询涉及连接,则可能需要多个索引查找。然而,MapReduce没有索引的概念 —— 至少不是通常意义上的。 -当MapReduce作业被赋予一组文件作为输入时,它读取所有这些文件的全部内容;一个数据库会调用这个操作一个全表扫描。如果您只想读取少量的记录,则与索引查找相比,全表扫描的成本非常高昂。但是,在分析查询中(请参阅第88页上的“事务处理或分析?”),通常需要计算大量记录的聚合。在这种情况下,扫描整个输入可能是相当合理的事情,特别是如果可以在多台机器上并行处理。 +当MapReduce作业被赋予一组文件作为输入时,它读取所有这些文件的全部内容;一个数据库会调用这个操作一个全表扫描。如果你只想读取少量的记录,则与索引查找相比,全表扫描的成本非常高昂。但是,在分析查询中(参阅“[事务处理或分析?](ch3.md#事务处理还是分析?)”),通常需要计算大量记录的聚合。在这种情况下,扫描整个输入可能是相当合理的事情,特别是如果可以在多台机器上并行处理。 [^v]: 我们在本书中讨论的连接通常是等值连接,即最常见的连接类型,其中记录与其他记录在特定字段(例如ID)中具有相同的值相关联。有些数据库支持更一般的连接类型,例如使用小于运算符而不是等号运算符,但是我们没有空间来覆盖它们。 @@ -298,7 +301,7 @@ Hadoop的各种高级工具(如Pig [30],Hive [31],Cascading [32],Crunch #### 示例:分析用户活动事件 -图10-2给出了一个批处理作业中加入典型的例子。 在左侧是事件日志,描述登录用户在网站上做的事情(称为活动事件或点击流数据),右侧是用户数据库。 您可以将此示例看作是星型模式的一部分(请参阅“星号和雪花:分析的示意图”(第93页)):事件日志是事实表,用户数据库是其中一个尺寸。 +[图10-2](img/fig10-2.png)给出了一个批处理作业中加入典型的例子。 在左侧是事件日志,描述登录用户在网站上做的事情(称为活动事件或点击流数据),右侧是用户数据库。 你可以将此示例看作是星型模式的一部分(参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日志是事实表,用户数据库是其中一个纬度。 ![](img/fig10-2.png) @@ -306,183 +309,184 @@ Hadoop的各种高级工具(如Pig [30],Hive [31],Cascading [32],Crunch 分析任务可能需要将用户活动与用户简档信息相关联:例如,如果简档包含用户的年龄或出生日期,则系统可以确定哪些年龄组最受欢迎。但是,活动事件仅包含用户标识,而不包含完整的用户配置文件信息。在每一个活动事件中嵌入这个简介信息很可能是非常浪费的。因此,活动事件需要加入用户配置文件数据库。 -这个连接的最简单实现将逐个遍历活动事件,并为每个遇到的用户ID查询用户数据库(在远程服务器上)。这是可能的,但是它很可能会遭受非常差的性能:处理吞吐量将受到数据库服务器的往返时间的限制,本地缓存的有效性将很大程度上取决于数据的分布,并行运行大量查询可能会轻易压倒数据库[35]。 +这个连接的最简单实现将逐个遍历活动事件,并为每个遇到的用户ID查询用户数据库(在远程服务器上)。这是可能的,但是它很可能会遭受非常差的性能:处理吞吐量将受到数据库服务器的往返时间的限制,本地缓存的有效性将很大程度上取决于数据的分布,并行运行大量查询可能会轻易压倒数据库【35】。 -为了在批处理过程中实现良好的吞吐量,计算必须(尽可能)在一台机器上进行。通过网络为您要处理的每个记录进行随机访问请求太慢。而且,查询远程数据库意味着批处理作业变得不确定,因为远程数据库中的数据可能会改变。 +为了在批处理过程中实现良好的吞吐量,计算必须(尽可能)在一台机器上进行。通过网络为你要处理的每个记录进行随机访问请求太慢。而且,查询远程数据库意味着批处理作业变得不确定,因为远程数据库中的数据可能会改变。 -因此,更好的方法是获取用户数据库的副本(例如,使用ETL进程从数据库备份中提取数据,请参阅第91页上的“数据仓库”),并将其放入与日志相同的分布式文件系统用户活动事件。然后,您可以将用户数据库存储在HDFS中的一组文件中,并将用户活动记录在另一组文件中,并且可以使用MapReduce将所有相关记录集中到同一地点并高效地处理它们。 +因此,更好的方法是获取用户数据库的副本(例如,使用ETL进程从数据库备份中提取数据,参阅“[数据仓库](ch3.md#数据仓库)”),并将其放入与日志相同的分布式文件系统用户活动事件。然后,你可以将用户数据库存储在HDFS中的一组文件中,并将用户活动记录在另一组文件中,并且可以使用MapReduce将所有相关记录集中到同一地点并高效地处理它们。 #### 排序合并连接 -回想一下,映射器的目的是从每个输入记录中提取一个键和值。在图10-2的情况下,这个键就是用户ID:一组映射器会覆盖活动事件(提取用户ID作为键和活动事件作为值),而另一组映射器将会检查用户数据库(提取用户ID作为键和用户的出生日期作为值)。这个过程如图10-3所示。 +回想一下,Mapper的目的是从每个输入记录中提取一个键和值。在[图10-2](img/fig10-2.png)的情况下,这个键就是用户ID:一组Mapper会覆盖活动事件(提取用户ID作为键和活动事件作为值),而另一组Mapper将会检查用户数据库(提取用户ID作为键和用户的出生日期作为值)。这个过程如[图10-3](img/fig10-3.png)所示。 ![](img/fig10-3.png) **图10-3 Reduce端在user ID上进行归并排序连接,如果输入数据集分片成多个文件,则每个都会被多个Mapper并行处理** 当MapReduce框架通过key对mapper输出进行分区,然后对键值对进行排序时,效果是所有活动事件和用户ID相同的用户记录在reducer输入中彼此相邻。 Map-Reduce作业甚至可以安排记录进行排序,使减速器始终如一 -首先从用户数据库中查看记录,然后按照时间戳顺序查看活动事件 - 这种技术被称为次级排序[26]。 +首先从用户数据库中查看记录,然后按照时间戳顺序查看活动事件 —— 这种技术被称为次级排序【26】。 -然后reducer可以很容易地执行实际的加入逻辑:每个用户ID调用一次reducer函数,并且由于二次排序,第一个值应该是来自用户数据库的出生日期记录。 Reducer将出生日期存储在局部变量中,然后使用相同的用户ID遍历活动事件,输出已观看网址和观看者年龄对。随后的Map- Reduce作业可以计算每个URL的查看者年龄分布,并按年龄组进行聚类。 +然后reducer可以很容易地执行实际的加入逻辑:每个用户ID调用一次reducer函数,并且由于二次排序,第一个值应该是来自用户数据库的出生日期记录。 Reducer将出生日期存储在局部变量中,然后使用相同的用户ID遍历活动事件,输出已观看网址和观看者年龄对。随后的Map-Reduce作业可以计算每个URL的查看者年龄分布,并按年龄组进行聚类。 -由于reducer一次处理一个特定用户ID的所有记录,因此只需要一次将一个用户记录保存在内存中,而不需要通过网络发出任何请求。这个算法被称为排序合并连接,因为映射器输出是按键排序的,然后缩减器将来自连接两边的排序的记录列表合并在一起。 +由于reducer一次处理一个特定用户ID的所有记录,因此只需要一次将一个用户记录保存在内存中,而不需要通过网络发出任何请求。这个算法被称为排序合并连接,因为Mapper输出是按键排序的,然后Reducer将来自连接两边的排序的记录列表合并在一起。 #### 把相关数据放在一起 -在排序合并连接中,映射器和排序过程确保将执行特定用户标识的连接操作的所有必需数据放在一起:一次调用reducer。预先排列了所有需要的数据,reducer可以是一个相当简单,单线程的代码,可以通过高吞吐量和低内存开销通过记录。 +在排序合并连接中,Mapper和排序过程确保将执行特定用户标识的连接操作的所有必需数据放在一起:一次调用reducer。预先排列了所有需要的数据,reducer可以是一个相当简单,单线程的代码,可以通过高吞吐量和低内存开销通过记录。 -查看这种体系结构的一种方法是映射器将“消息”发送给reducer。当一个映射器发出一个键值对时,这个键的作用就像值应该传递到的目标地址。即使密钥只是一个任意的字符串(不是像IP地址和端口号那样的实际的网络地址),它的行为就像一个地址:所有具有相同密钥的密钥对将被传送到相同的目标(a呼叫减速机)。 +查看这种体系结构的一种方法是Mapper将“消息”发送给reducer。当一个Mapper发出一个键值对时,这个键的作用就像值应该传递到的目标地址。即使键只是一个任意的字符串(不是像IP地址和端口号那样的实际的网络地址),它的行为就像一个地址:所有具有相同键的键对将被传送到相同的目标(一次Reduce的调用)。 -使用MapReduce编程模型将计算的物理网络通信方面(从正确的计算机获取数据)从应用程序逻辑中分离出来(处理完数据后)。这种分离与数据库的典型使用形成了鲜明的对比,从数据库中获取数据的请求经常发生在应用程序代码的深处[36]。由于MapReduce能够处理所有的网络通信,因此它也避免了应用程序代码担心部分故障,例如另一个节点的崩溃:MapReduce在不影响应用程序逻辑的情况下,透明地重试失败的任务。 +使用MapReduce编程模型将计算的物理网络通信方面(从正确的计算机获取数据)从应用程序逻辑中分离出来(处理完数据后)。这种分离与数据库的典型使用形成了鲜明的对比,从数据库中获取数据的请求经常发生在应用程序代码的深处【36】。由于MapReduce能够处理所有的网络通信,因此它也避免了应用程序代码担心部分故障,例如另一个节点的崩溃:MapReduce在不影响应用程序逻辑的情况下,透明地重试失败的任务。 ### GROUP BY -除了连接之外,“将相关数据引入同一地点”模式的另一个常见用法是通过某个键(如SQL中的GROUP BY子句)对记录进行分组。所有用相同的密钥记录一个组,并且下一步往往是在每个组内进行某种聚合,例如: +除了连接之外,“将相关数据引入同一地点”模式的另一个常见用法是通过某个键(如SQL中的GROUP BY子句)对记录进行分组。所有用相同的键记录一个组,并且下一步往往是在每个组内进行某种聚合,例如: -* 计算每个组中记录的数量(例如,在统计页面视图的示例中,您将在SQL中表示为COUNT(*)聚合) -* 在SQL中的一个特定字段(SUM(fieldname))中添加值 +* 计算每个组中记录的数量(例如,在统计页面视图的示例中,你将在SQL中表示为COUNT(*)聚合) +* 在SQL中的一个特定字段(`SUM(fieldname)`)中添加值 * 根据某些排名函数选择前k个记录 -使用MapReduce实现这种分组操作的最简单方法是设置映射器,以便它们生成的键值对使用所需的分组键。然后,分区和排序过程将所有记录与同一个缩减器中的相同键集合在一起。因此,在MapReduce上实现时,分组和连接看起来非常相似。 +使用MapReduce实现这种分组操作的最简单方法是设置Mapper,以便它们生成的键值对使用所需的分组键。然后,分区和排序过程将所有记录与同一个Reducer中的相同键集合在一起。因此,在MapReduce上实现时,分组和连接看起来非常相似。 -分组的另一个常见用途是整理特定用户会话的所有活动事件,以便找出用户采取的一系列操作(称为会话化[37])。例如,可以使用这种分析来确定显示网站新版本的用户是否比那些显示旧版本(A / B测试)的用户更有可能进行购买,或计算某个营销活动是值得的。 +分组的另一个常见用途是整理特定用户会话的所有活动事件,以便找出用户采取的一系列操作(称为会话化【37】)。例如,可以使用这种分析来确定显示网站新版本的用户是否比那些显示旧版本(A/B测试)的用户更有可能进行购买,或计算某个营销活动是值得的。 -如果您有多个Web服务器处理用户请求,则特定用户的活动事件很可能分散在各个不同的服务器的日志文件中。您可以通过使用会话cookie,用户ID或类似的标识符作为分组键来实现会话,并将特定用户的所有活动事件放在一起,同时将不同用户的事件分配到不同的分区。 +如果你有多个Web服务器处理用户请求,则特定用户的活动事件很可能分散在各个不同的服务器的日志文件中。你可以通过使用会话cookie,用户ID或类似的标识符作为分组键来实现会话,并将特定用户的所有活动事件放在一起,同时将不同用户的事件分配到不同的分区。 #### 处理倾斜 -如果存在与单个密钥相关的大量数据,则“将具有相同密钥的所有记录带到相同位置”的模式被破坏。例如,在社交网络中,大多数用户可能会连接到几百人,但少数名人可能有数百万的追随者。这种不成比例的活动数据库记录被称为关键对象[38]或热键。 +如果存在与单个键相关的大量数据,则“将具有相同键的所有记录带到相同位置”的模式被破坏。例如,在社交网络中,大多数用户可能会连接到几百人,但少数名人可能有数百万的追随者。这种不成比例的活动数据库记录被称为关键对象【38】或热键。 -在单个缩减器中收集与名人相关的所有活动(例如回复他们发布的内容)可能导致严重的偏差(也称为热点) - 也就是说,一个减速器必须处理比其他更多的记录(参见“歪曲的工作负载和消除热点“)。由于MapReduce作业只有在其所有映射器和缩减器都完成时才完成,所有后续作业必须等待最慢的缩减器才能启动。 +在单个Reducer中收集与名人相关的所有活动(例如回复他们发布的内容)可能导致严重的倾斜(也称为热点)—— 也就是说,一个减速器必须处理比其他更多的记录(参见“[负载倾斜与消除热点](ch6.md#负载倾斜与消除热点)“)。由于MapReduce作业只有在其所有Mapper和Reducer都完成时才完成,所有后续作业必须等待最慢的Reducer才能启动。 -如果加入输入有热键,则可以使用一些算法进行补偿。例如,Pig中的偏斜连接方法首先运行一个抽样作业来确定哪些键是热的[39]。执行实际加入时,映射器发送任何随机选择一个与几个减速器之一相关的热键的记录(与传统的MapReduce相比,它选择一个基于密钥散列确定性的减速器)。对于加入的其他输入,与热键相关的记录需要被复制到所有处理该密钥的缩减器[40]。 +如果加入输入有热点键,则可以使用一些算法进行补偿。例如,Pig中的倾斜连接方法首先运行一个抽样作业来确定哪些键是热的【39】。执行实际加入时,Mapper发送任何随机选择一个与几个减速器之一相关的热键的记录(与传统的MapReduce相比,它选择一个基于键散列确定性的减速器)。对于加入的其他输入,与热键相关的记录需要被复制到所有处理该键的Reducer【40】。 -这种技术将处理热键的工作分散到多个reducer上,这样可以使其更好地并行化,而不必将其他join连接复制到多个reducer。 Crunch中的分片连接方法是相似的,但需要显式指定热键而不是使用采样作业。这种技术也非常类似于我们在第205页的“倾斜的工作负载和减轻热点”中讨论的技术,使用随机化来缓解分区数据库中的热点。 +这种技术将处理热键的工作分散到多个reducer上,这样可以使其更好地并行化,而不必将其他join连接复制到多个reducer。 Crunch中的分片连接方法是相似的,但需要显式指定热键而不是使用采样作业。这种技术也非常类似于我们在“[负载倾斜与消除热点](ch6.md#负载倾斜与消除热点)”中讨论的技术,使用随机化来缓解分区数据库中的热点。 -Hive的偏斜连接优化采取了另一种方法。它需要在表格元数据中明确指定热键,并将与这些键相关的记录与其余文件分开存放。在该表上执行连接时,它将使用地图边连接(请参阅下一节)获取热键。 +Hive的偏斜连接优化采取了另一种方法。它需要在表格元数据中明确指定热键,并将与这些键相关的记录与其余文件分开存放。在该表上执行连接时,它将使用Map边连接(参阅下一节)获取热键。 -使用热键对记录进行分组并汇总记录时,可以分两个阶段进行分组。第一个MapReduce阶段将记录发送到随机缩减器,以便每个缩减器对热键的记录子集执行分组,并为每个键输出更紧凑的聚合值。第二个Map-Reduce作业然后将来自所有第一阶段减速器的值合并为每个键的单个值。 +使用热键对记录进行分组并汇总记录时,可以分两个阶段进行分组。第一个MapReduce阶段将记录发送到随机Reducer,以便每个Reducer对热键的记录子集执行分组,并为每个键输出更紧凑的聚合值。第二个Map-Reduce作业然后将来自所有第一阶段减速器的值合并为每个键的单个值。 ### Map端连接 -上一节描述的连接算法在reducer中执行实际的连接逻辑,因此被称为reduce-side连接。映射器扮演着输入数据的角色:从每个输入记录中提取键和值,将键值对分配给reducer分区,并按键排序。 +上一节描述的连接算法在reducer中执行实际的连接逻辑,因此被称为reduce-side连接。Mapper扮演着输入数据的角色:从每个输入记录中提取键和值,将键值对分配给reducer分区,并按键排序。 -减少方法的优点是不需要对输入数据做任何假设:无论其属性和结构如何,映射器都可以准备数据以准备加入。然而,不利的一面是,所有这些排序,复制到缩减器以及合并减速器输入可能是非常昂贵的。取决于可用的内存缓冲区,当数据通过MapReduce [37]阶段时,数据可能被写入磁盘几次。 +减少方法的优点是不需要对输入数据做任何假设:无论其属性和结构如何,Mapper都可以准备数据以准备加入。然而,不利的一面是,所有这些排序,复制到Reducer以及合并减速器输入可能是非常昂贵的。取决于可用的内存缓冲区,当数据通过MapReduce 【37】阶段时,数据可能被写入磁盘几次。 -另一方面,如果您可以对输入数据进行某些假设,则可以通过使用所谓的map端连接来加快连接速度。这种方法使用了一个缩减的MapReduce作业,其中没有减速器,也没有排序。相反,每个映射器只需从分布式文件系统读取一个输入文件块,然后将一个输出文件写入文件系统即可。 +另一方面,如果你可以对输入数据进行某些假设,则可以通过使用所谓的map端连接来加快连接速度。这种方法使用了一个Reduce的MapReduce作业,其中没有减速器,也没有排序。相反,每个Mapper只需从分布式文件系统读取一个输入文件块,然后将一个输出文件写入文件系统即可。 #### 广播散列连接 -执行地图边连接最简单的方法适用于大数据集与小数据集连接的情况。特别是,小数据集需要足够小,以便可以将其全部加载到每个映射器的内存中。 +执行Map端连接最简单的方法适用于大数据集与小数据集连接的情况。特别是,小数据集需要足够小,以便可以将其全部加载到每个Mapper的内存中。 -例如,假设在图10-2的情况下,用户数据库足够小以适应内存。在这种情况下,当映射器启动时,它可以首先将用户数据库从分布式文件系统读取到内存中的哈希表中。完成此操作后,映射程序可以扫描用户活动事件,并简单地查找散列表中每个事件的用户标识[^vi]。 +例如,假设在[图10-2](img/fig10-2.png)的情况下,用户数据库足够小以适应内存。在这种情况下,当Mapper启动时,它可以首先将用户数据库从分布式文件系统读取到内存中的哈希表中。完成此操作后,Map程序可以扫描用户活动事件,并简单地查找散列表中每个事件的用户标识[^vi]。 [^vi]: 这个例子假定散列表中的每个键只有一个条目,这对用户数据库(用户ID唯一标识一个用户)可能是正确的。通常,哈希表可能需要包含具有相同键的多个条目,并且连接运算符将输出关键字的所有匹配。 -仍然可以有几个映射任务:一个用于连接的大输入的每个文件块(在图10-2的例子中,活动事件是大输入)。这些映射器中的每一个都将小输入全部加载到内存中。 +仍然可以有几个映射任务:一个用于连接的大输入的每个文件块(在[图10-2](img/fig10-2.png)的例子中,活动事件是大输入)。这些Mapper中的每一个都将小输入全部加载到内存中。 -这种简单而有效的算法被称为广播散列连接:广播词反映了这样一个事实,即大输入的分区的每个映射器都读取整个小输入(所以小输入有效地“广播”到大的输入),单词hash反映了它使用一个哈希表。 Pig(名为“replicated join”),Hive(“MapJoin”),Cascading和Crunch支持此连接方法。它也用于数据仓库查询引擎,如Impala [41]。 +这种简单而有效的算法被称为广播散列连接:广播一词反映了这样一个事实,即大输入的分区的每个Mapper都读取整个小输入(所以小输入有效地“广播”到大的输入),单词hash反映了它使用一个哈希表。 Pig(名为“replicated join”),Hive(“MapJoin”),Cascading和Crunch支持此连接方法。它也用于数据仓库查询引擎,如Impala 【41】。 -而不是将小连接输入加载到内存散列表中,另一种方法是将小连接输入存储在本地磁盘上的只读索引中[42]。该索引中经常使用的部分将保留在操作系统的页面缓存中,因此这种方法可以提供与内存中哈希表几乎一样快的随机访问查找,但实际上并不需要数据集适合内存。 +而不是将小连接输入加载到内存散列表中,另一种方法是将小连接输入存储在本地磁盘上的只读索引中【42】。该索引中经常使用的部分将保留在操作系统的页面缓存中,因此这种方法可以提供与内存中哈希表几乎一样快的随机访问查找,但实际上并不需要数据集适合内存。 #### 分区散列连接 -如果以相同方式对映射端连接的输入进行分区,则散列连接方法可以独立应用于每个分区。在图10-2的情况下,您可以根据用户标识的最后一位十进制数字来安排活动事件和用户数据库的每一个(因此每边有10个分区)。例如,映射器3首先将所有具有以3结尾的ID的用户加载到散列表中,然后扫描ID为3的每个用户的所有活动事件。 +如果以相同方式对映射端连接的输入进行分区,则散列连接方法可以独立应用于每个分区。在[图10-2](img/fig10-2.png)的情况下,你可以根据用户标识的最后一位十进制数字来安排活动事件和用户数据库的每一个(因此每边有10个分区)。例如,Mapper3首先将所有具有以3结尾的ID的用户加载到散列表中,然后扫描ID为3的每个用户的所有活动事件。 -如果分区正确完成,您可以确定所有您可能要加入的记录都位于相同编号的分区中,因此每个映射器只能从每个输入数据集中读取一个分区就足够了。这具有的优点是每个映射器都可以将较少量的数据加载到其哈希表中。 +如果分区正确完成,你可以确定所有你可能要加入的记录都位于相同编号的分区中,因此每个Mapper只能从每个输入数据集中读取一个分区就足够了。这具有的优点是每个Mapper都可以将较少量的数据加载到其哈希表中。 -这种方法只适用于两个连接的输入具有相同数量的分区,记录根据相同的密钥和相同的散列函数分配给分区。如果输入是由之前执行过这个分组的MapReduce作业生成的,那么这可能是一个合理的假设。 +这种方法只适用于两个连接的输入具有相同数量的分区,记录根据相同的键和相同的散列函数分配给分区。如果输入是由之前执行过这个分组的MapReduce作业生成的,那么这可能是一个合理的假设。 -分区散列连接在Hive [37]中称为bucketed映射连接。地图边合并连接 +分区散列连接在Hive 【37】中称为bucketed映射连接。Map端合并连接 -如果输入数据集不仅以相同的方式进行分区,而且还基于相同的键进行排序,则应用另一种地图端联接的变体。在这种情况下,输入是否足够小以适应内存并不重要,因为映射器可以执行通常由reducer执行的相同合并操作:按递增键递增读取两个输入文件,以及匹配相同的密钥记录。 +如果输入数据集不仅以相同的方式进行分区,而且还基于相同的键进行排序,则应用另一种Map端联接的变体。在这种情况下,输入是否足够小以适应内存并不重要,因为Mapper可以执行通常由reducer执行的相同合并操作:按递增键递增读取两个输入文件,以及匹配相同的键记录。 -如果地图边合并连接是可能的,则可能意味着先前的MapReduce作业首先将输入数据集引入到这个分区和排序的表单中。原则上,这个加入可以在之前工作的缩减阶段进行。但是,在单独的仅用于地图的作业中执行合并连接仍然是适当的,例如,除了此特定连接之外,还需要分区和排序数据集以用于其他目的。 +如果Map端合并连接是可能的,则可能意味着先前的MapReduce作业首先将输入数据集引入到这个分区和排序的表单中。原则上,这个加入可以在之前工作的Reduce阶段进行。但是,在单独的仅用于Map的作业中执行合并连接仍然是适当的,例如,除了此特定连接之外,还需要分区和排序数据集以用于其他目的。 #### MapReduce与Map端连接的工作流程 当下游作业使用MapReduce连接的输出时,map-side或reduce-side连接的选择会影响输出的结构。 reduce-side连接的输出按连接键进行分区和排序,而map-side连接的输出按照与大输入相同的方式进行分区和排序(因为对每个文件块启动一个map任务无论是使用分区连接还是广播连接,连接的大输入)。 -如前所述,地图边连接也对输入数据集的大小,排序和分区做出了更多的假设。在优化连接策略时,了解分布式文件系统中数据集的物理布局变得非常重要:仅仅知道编码格式和数据存储目录的名称是不够的;您还必须知道数据分区和排序的分区数量和密钥。 +如前所述,Map边连接也对输入数据集的大小,排序和分区做出了更多的假设。在优化连接策略时,了解分布式文件系统中数据集的物理布局变得非常重要:仅仅知道编码格式和数据存储目录的名称是不够的;你还必须知道数据分区和排序的分区数量和键。 -在Hadoop生态系统中,这种关于数据集分区的元数据经常在HCatalog和Hive Metastore中维护[37]。 +在Hadoop生态系统中,这种关于数据集分区的元数据经常在HCatalog和Hive Metastore中维护【37】。 ### 工作流的输出 我们已经谈了很多关于实现MapReduce工作流程的各种算法,但是我们忽略了一个重要的问题:一旦完成,所有处理的结果是什么?我们为什么要把所有这些工作放在首位? -在数据库查询的情况下,我们根据分析目的来区分事务处理(OLTP)目的(请参阅第90页上的“事务处理或分析?”)。我们看到,OLTP查询通常使用索引按键查找少量记录,以便将其呈现给用户(例如,在网页上)。另一方面,分析查询通常会扫描大量记录,执行分组和汇总,输出通常具有报告的形式:显示某个指标随时间变化的图表,或前10个项目根据一些排名,或一些数量分解成子类别。这种报告的消费者通常是需要做出商业决策的分析师或经理。 +在数据库查询的情况下,我们根据分析目的来区分事务处理(OLTP)目的(参阅“[事务处理或分析?](ch3.md#事务处理或分析?)”)。我们看到,OLTP查询通常使用索引按键查找少量记录,以便将其呈现给用户(例如,在网页上)。另一方面,分析查询通常会扫描大量记录,执行分组和汇总,输出通常具有报告的形式:显示某个指标随时间变化的图表,或前10个项目根据一些排名,或一些数量分解成子类别。这种报告的消费者通常是需要做出商业决策的分析师或经理。 -批处理在哪里适合?这不是交易处理,也不是分析。与分析更接近,因为批处理过程通常扫描输入数据集的大部分。但是,MapReduce作业的工作流程与用于分析目的的SQL查询不同(请参阅第418页的“比较Hadoop与分布式数据库”)。批处理过程的输出通常不是报告,而是一些其他类型的结构。 +批处理在哪里适合?这不是交易处理,也不是分析。与分析更接近,因为批处理过程通常扫描输入数据集的大部分。但是,MapReduce作业的工作流程与用于分析目的的SQL查询不同(参阅“[比较Hadoop与分布式数据库](比较Hadoop与分布式数据库)”)。批处理过程的输出通常不是报告,而是一些其他类型的结构。 #### 建立搜索索引 -Google最初使用的MapReduce是为其搜索引擎建立索引,这个索引是作为5到10个MapReduce作业的工作流实现的[1]。虽然Google为了这个目的后来不再使用MapReduce [43],但是如果从建立搜索索引的角度来看,它可以帮助理解MapReduce。 (即使在今天,Hadoop MapReduce仍然是构建Lucene / Solr索引的好方法。) +Google最初使用的MapReduce是为其搜索引擎建立索引,这个索引是作为5到10个MapReduce作业的工作流实现的【1】。虽然Google为了这个目的后来不再使用MapReduce 【43】,但是如果从建立搜索索引的角度来看,它可以帮助理解MapReduce。 (即使在今天,Hadoop MapReduce仍然是构建Lucene / Solr索引的好方法。) -我们在第88页的“全文搜索和模糊索引”中简要地看到了Lucene这样的全文搜索索引是如何工作的:它是一个文件(术语字典),您可以在其中高效地查找特定关键字并找到包含该关键字的所有文档ID列表(发布列表)。这是一个非常简单的搜索索引视图 - 实际上,它需要各种附加数据,以便根据相关性对搜索结果进行排名,纠正拼写错误,解析同义词等等,但这一原则是成立的。 +我们在“[全文搜索和模糊索引](ch3.md#全文搜索和模糊索引)”中简要地看到了Lucene这样的全文搜索索引是如何工作的:它是一个文件(关键词字典),你可以在其中高效地查找特定关键字并找到包含该关键字的所有文档ID列表(发布列表)。这是一个非常简单的搜索索引视图 —— 实际上,它需要各种附加数据,以便根据相关性对搜索结果进行排名,纠正拼写错误,解析同义词等等,但这一原则是成立的。 -如果需要对一组固定文档执行全文搜索,则批处理是构建索引的一种非常有效的方法:映射器根据需要对文档集进行分区,每个reducer构建其分区的索引,并将索引文件写入分布式文件系统。构建这样的文档分区索引(请参阅“分区和二级索引”(第184页))并行处理非常好。 +如果需要对一组固定文档执行全文搜索,则批处理是构建索引的一种非常有效的方法:Mapper根据需要对文档集进行分区,每个reducer构建其分区的索引,并将索引文件写入分布式文件系统。构建这样的文档分区索引(参阅“[分区和二级索引](ch6.md#分区和二级索引)”)并行处理非常好。 由于按关键字查询搜索索引是只读操作,因此这些索引文件一旦创建就是不可变的。 如果索引的文档集合发生更改,则可以选择定期重新运行整个索引工作流程,并在完成后用新的索引文件批量替换以前的索引文件。如果只有少量的文档发生了变化,这种方法可能会带来很高的计算成本,但是它的优点是索引过程很容易推理:文档,索引。 -或者,可以逐渐建立索引。如第3章所述,如果要添加,删除或更新索引中的文档,Lucene会写出新的段文件,并异步合并和压缩背景中的段文件。我们将在第11章中看到更多这样的增量处理。 +或者,可以逐渐建立索引。如第3章所述,如果要添加,删除或更新索引中的文档,Lucene会写出新的段文件,并异步合并和压缩背景中的段文件。我们将在[第11章](ch11.md)中看到更多这样的增量处理。 #### 键值存储作为批处理输出 -搜索索引只是批处理工作流程可能输出的一个示例。批量处理的另一个常见用途是构建机器学习系统,如分类器(例如,垃圾邮件过滤器,异常检测,图像识别)和推荐系统(例如,您可能认识的人,您可能感兴趣的产品或相关搜索) ])。 +搜索索引只是批处理工作流程可能输出的一个示例。批量处理的另一个常见用途是构建机器学习系统,如分类器(例如,垃圾邮件过滤器,异常检测,图像识别)和推荐系统(例如,你可能认识的人,你可能感兴趣的产品或相关搜索)。 -这些批处理作业的输出通常是某种数据库:例如,可以通过用户ID查询以获取该用户的建议朋友的数据库,或者可以通过产品ID查询的数据库以获取相关产品[45]。 +这些批处理作业的输出通常是某种数据库:例如,可以通过用户ID查询以获取该用户的建议朋友的数据库,或者可以通过产品ID查询的数据库以获取相关产品【45】。 这些数据库需要从处理用户请求的Web应用程序中查询,这些请求通常与Hadoop基础架构分离。那么批处理过程的输出如何返回到Web应用程序可以查询的数据库? -最明显的选择可能是直接在映射器或简化器中使用客户端库作为您最喜欢的数据库,并从批处理作业直接写入数据库服务器,一次写入一条记录。这将起作用(假设您的防火墙规则允许从您的Hadoop环境直接访问您的生产数据库),但由于以下几个原因,这是一个坏主意: +最明显的选择可能是直接在Mapper或Reducer中使用客户端库作为你最喜欢的数据库,并从批处理作业直接写入数据库服务器,一次写入一条记录。这将起作用(假设你的防火墙规则允许从你的Hadoop环境直接访问你的生产数据库),但由于以下几个原因,这是一个坏主意: * 正如前面讨论的连接一样,为每个记录提出一个网络请求比批处理任务的正常吞吐量要慢几个数量级。即使客户端库支持批处理,性能也可能很差。 -* MapReduce作业经常并行运行许多任务。如果所有映射器或简化器都同时写入相同的输出数据库,并且批处理过程期望的速率,那么该数据库可能很容易被压倒,并且其查询性能可能受到影响。这可能会导致系统其他部分的操作问题[35]。 -* 通常情况下,MapReduce为作业输出提供了一个干净的“全有或全无”的保证:如果作业成功,则结果就是只执行一次任务的输出,即使某些任务失败并且必须重试。如果整个作业失败,则不会生成输出。然而,从作业内部写入外部系统会产生外部可见的副作用,这种副作用是不能被隐藏的。因此,您不得不担心部分完成的作业对其他系统可见的结果,以及Hadoop任务尝试和推测性执行的复杂性。 +* MapReduce作业经常并行运行许多任务。如果所有Mapper或Reducer都同时写入相同的输出数据库,并且批处理过程期望的速率,那么该数据库可能很容易被压倒,并且其查询性能可能受到影响。这可能会导致系统其他部分的操作问题【35】。 +* 通常情况下,MapReduce为作业输出提供了一个干净的“全有或全无”的保证:如果作业成功,则结果就是只执行一次任务的输出,即使某些任务失败并且必须重试。如果整个作业失败,则不会生成输出。然而,从作业内部写入外部系统会产生外部可见的副作用,这种副作用是不能被隐藏的。因此,你不得不担心部分完成的作业对其他系统可见的结果,以及Hadoop任务尝试和推测性执行的复杂性。 -更好的解决方案是在批处理作业中创建一个全新的数据库,并将其作为文件写入分布式文件系统中作业的输出目录,就像上一节的搜索索引一样。这些数据文件一旦写入就是不可变的,可以批量加载到处理只读查询的服务器中。各种键值存储支持在MapReduce作业中构建数据库文件,包括Voldemort [46],Terrapin [47],ElephantDB [48]和HBase批量加载[49]。 +更好的解决方案是在批处理作业中创建一个全新的数据库,并将其作为文件写入分布式文件系统中作业的输出目录,就像上一节的搜索索引一样。这些数据文件一旦写入就是不可变的,可以批量加载到处理只读查询的服务器中。各种键值存储支持在MapReduce作业中构建数据库文件,包括Voldemort 【46】,Terrapin 【47】,ElephantDB 【48】和HBase批量加载【49】。 -构建这些数据库文件是MapReduce的一个很好的使用方法:使用映射器提取一个键,然后使用该键进行排序已经成为构建索引所需的大量工作。由于大多数这些键值存储是只读的(文件只能由批处理作业一次写入,而且是不可变的),所以数据结构非常简单。例如,它们不需要WAL(请参阅第82页的「使B树可靠」)。 +构建这些数据库文件是MapReduce的一个很好的使用方法:使用Mapper提取一个键,然后使用该键进行排序已经成为构建索引所需的大量工作。由于大多数这些键值存储是只读的(文件只能由批处理作业一次写入,而且是不可变的),所以数据结构非常简单。例如,它们不需要WAL(参阅“[使B树可靠](ch3.md#使B树可靠)”)。 -将数据加载到Voldemort时,服务器将继续向旧数据文件提供请求,同时将新数据文件从分布式文件系统复制到服务器的本地磁盘。一旦复制完成,服务器会自动切换到查询新文件。如果在这个过程中出现任何问题,它可以很容易地再次切换回旧的文件,因为它们仍然存在,并且是不变的[46]。 +将数据加载到Voldemort时,服务器将继续向旧数据文件提供请求,同时将新数据文件从分布式文件系统复制到服务器的本地磁盘。一旦复制完成,服务器会自动切换到查询新文件。如果在这个过程中出现任何问题,它可以很容易地再次切换回旧的文件,因为它们仍然存在,并且是不变的【46】。 #### 批量过程输出的哲学 -本章前面讨论过的Unix哲学(第394页的“Unix哲学”)鼓励通过对数据流的非常明确的实验来进行实验:程序读取输入并写入输出。在这个过程中,输入保持不变,任何以前的输出都被新输出完全替换,并且没有其他副作用。这意味着您可以随心所欲地重新运行一个命令,调整或调试它,而不会扰乱系统的状态。 +本章前面讨论过的Unix哲学(“[Unix哲学](#Unix哲学)”)鼓励通过对数据流的非常明确的实验来进行实验:程序读取输入并写入输出。在这个过程中,输入保持不变,任何以前的输出都被新输出完全替换,并且没有其他副作用。这意味着你可以随心所欲地重新运行一个命令,调整或调试它,而不会扰乱系统的状态。 MapReduce作业的输出处理遵循相同的原理。通过将输入视为不可变且避免副作用(如写入外部数据库),批处理作业不仅实现了良好的性能,而且更容易维护: -* 如果在代码中引入了一个错误,并且输出错误或损坏了,则可以简单地回滚到代码的先前版本,然后重新运行该作业,输出将再次正确。或者,甚至更简单,您可以将旧的输出保存在不同的目录中,然后切换回原来的目录。具有读写事务的数据库没有这个属性:如果你部署了错误的代码,将错误的数据写入数据库,那么回滚代码将无法修复数据库中的数据。 (能够从错误代码中恢复的思想被称为人类容错[50]。) -* 由于易于回滚,功能开发可以比错误意味着不可挽回的损害的环境更快地进行。这种使不可逆性最小化的原则有利于敏捷软件的开发[51]。 +* 如果在代码中引入了一个错误,并且输出错误或损坏了,则可以简单地回滚到代码的先前版本,然后重新运行该作业,输出将再次正确。或者,甚至更简单,你可以将旧的输出保存在不同的目录中,然后切换回原来的目录。具有读写事务的数据库没有这个属性:如果你部署了错误的代码,将错误的数据写入数据库,那么回滚代码将无法修复数据库中的数据。 (能够从错误代码中恢复的思想被称为人类容错【50】。) +* 由于易于回滚,功能开发可以比错误意味着不可挽回的损害的环境更快地进行。这种使不可逆性最小化的原则有利于敏捷软件的开发【51】。 * 如果映射或减少任务失败,MapReduce框架将自动重新调度并在同一个输入上再次运行它。如果失败是由于代码中的一个错误造成的,那么它会一直崩溃,并最终导致作业在几次尝试之后失败。但是如果故障是由于暂时的问题引起的,那么故障是可以容忍的。这种自动重试只是安全的,因为输入是不可变的,而失败任务的输出被MapReduce框架丢弃。 * 同一组文件可用作各种不同作业的输入,其中包括计算度量标准的计算作业,并评估作业的输出是否具有预期的特性(例如,将其与前一次运行的输出进行比较并测量差异) 。 * 与Unix工具类似,MapReduce作业将逻辑与布线(配置输入和输出目录)分开,这就提供了关注点的分离,并且可以重用代码:一个团队可以专注于实现一件好事的工作其他团队可以决定何时何地运行这项工作。 -在这些领域,对Unix运行良好的设计原则似乎也适用于Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因为大多数Unix工具都假定没有类型的文本文件,所以他们必须做大量的输入解析(本章开头的日志分析示例使用{print $ 7}来提取URL)。在Hadoop上,通过使用更多结构化的文件格式,可以消除一些低价值的语法转换:Avro(请参阅第122页上的“Avro”)和Parquet(请参阅第95页上的“面向列的存储”)经常使用,因为它们提供高效的基于模式的编码,并允许随着时间的推移模式的演变(见第4章)。 +在这些领域,对Unix运行良好的设计原则似乎也适用于Hadoop,但Unix和Hadoop在某些方面也有所不同。例如,因为大多数Unix工具都假定没有类型的文本文件,所以他们必须做大量的输入解析(本章开头的日志分析示例使用`{print $7}`来提取URL)。在Hadoop上,通过使用更多结构化的文件格式,可以消除一些低价值的语法转换:Avro(参阅“[Avro](ch4.md#Avro)”)和Parquet(参阅第95页上的“[列存储](ch3.md#列存储)”)经常使用,因为它们提供高效的基于模式的编码,并允许随着时间的推移模式的演变(见第4章)。 ### 比较Hadoop和分布式数据库 正如我们所看到的,Hadoop有点像Unix的分布式版本,其中HDFS是文件系统,而MapReduce是Unix进程的古怪实现(这恰好总是在映射阶段和缩小阶段之间运行排序实用程序)。我们看到了如何在这些基元之上实现各种连接和分组操作。 -当MapReduce论文[1]发表时,它在某种意义上说并不新鲜。我们在前几节中讨论的所有处理和并行连接算法已经在十多年前的所谓的大规模并行处理(MPP)数据库中实现了[3,40]。例如,Gamma数据库机器,Teradata和Tandem NonStop SQL是这方面的先驱[52]。 +当MapReduce论文【1】发表时,它在某种意义上说并不新鲜。我们在前几节中讨论的所有处理和并行连接算法已经在十多年前的所谓的**大规模并行处理(MPP, massively parallel processing)**数据库中实现了【3,40】。例如,Gamma数据库机器,Teradata和Tandem NonStop SQL是这方面的先驱【52】。 -最大的区别是MPP数据库集中于在一组机器上并行执行分析SQL查询,而MapReduce和分布式文件系统[19]的组合则更像是一个可以运行任意程序的通用操作系统。 +最大的区别是MPP数据库集中于在一组机器上并行执行分析SQL查询,而MapReduce和分布式文件系统【19】的组合则更像是一个可以运行任意程序的通用操作系统。 #### 存储的多样性 -数据库要求您根据特定的模型(例如关系或文档)来构造数据,而分布式文件系统中的文件只是字节序列,可以使用任何数据模型和编码来编写。它们可能是数据库记录的集合,但同样可以是文本,图像,视频,传感器读数,稀疏矩阵,特征向量,基因组序列或任何其他类型的数据。 +数据库要求你根据特定的模型(例如关系或文档)来构造数据,而分布式文件系统中的文件只是字节序列,可以使用任何数据模型和编码来编写。它们可能是数据库记录的集合,但同样可以是文本,图像,视频,传感器读数,稀疏矩阵,特征向量,基因组序列或任何其他类型的数据。 -说白了,Hadoop开放了将数据不加区分地转储到HDFS的可能性,之后才想出如何进一步处理它[53]。相比之下,在将数据导入数据库专有存储格式之前,MPP数据库通常需要对数据和查询模式进行仔细的前期建模。 -从纯粹的角度来看,这种仔细的建模和导入似乎是可取的,因为这意味着数据库的用户有更好的质量数据来处理。然而,在实践中,似乎只是简单地使数据可用 - 即使它是一个古怪的,难以使用的原始格式 - 通常比尝试决定理想的数据模型更有价值[54 ]。 +说白了,Hadoop开放了将数据不加区分地转储到HDFS的可能性,之后才想出如何进一步处理它【53】。相比之下,在将数据导入数据库专有存储格式之前,MPP数据库通常需要对数据和查询模式进行仔细的前期建模。 -这个想法与数据仓库类似(请参阅第91页上的“数据仓库”):将大型组织的各个部分的数据集中在一起是很有价值的,因为它可以跨以前不同的数据集进行联接。 MPP数据库所要求的谨慎的模式设计减慢了集中式数据收集速度;以原始形式收集数据,以后担心模式设计,使数据收集速度加快(有时被称为“数据湖”或“企业数据中心”[55])。 +从纯粹的角度来看,这种仔细的建模和导入似乎是可取的,因为这意味着数据库的用户有更好的质量数据来处理。然而,在实践中,似乎只是简单地使数据可用 —— 即使它是一个古怪的,难以使用的原始格式 —— 通常比尝试决定理想的数据模型更有价值[54 ]。 -不加区别的数据倾销改变了解释数据的负担:不是强迫数据集的生产者将其转化为标准化的格式,而是数据的解释成为消费者的问题(模式在读方法[56];请参阅第39页上的“文档模型中的模式灵活性”)。如果生产者和消费者是不同优先级的不同团队,这可能是一个优势。甚至可能不存在一个理想的数据模型,而是对适合不同目的的数据有不同的看法。以原始形式简单地转储数据可以进行多次这样的转换。这种方法被称为寿司原则:“原始数据更好”[57]。 +这个想法与数据仓库类似(参阅“[数据仓库](ch3.md#数据仓库)”):将大型组织的各个部分的数据集中在一起是很有价值的,因为它可以跨以前不同的数据集进行联接。 MPP数据库所要求的谨慎的模式设计减慢了集中式数据收集速度;以原始形式收集数据,以后担心模式设计,使数据收集速度加快(有时被称为“**数据湖(data lake)**”或“**企业数据中心(enterprise data hub)**”【55】)。 -因此,Hadoop经常被用于实现ETL过程(请参阅“数据仓库”第91页):事务处理系统中的数据以某种原始形式转储到分布式文件系统中,然后编写MapReduce作业来清理数据,将其转换为关系表单,并将其导入MPP数据仓库以进行分析。数据建模仍然在发生,但它是在一个单独的步骤中,从数据收集中分离出来的。这种解耦是可能的,因为分布式文件系统支持以任何格式编码的数据。 +不加区别的数据倾销改变了解释数据的负担:不是强迫数据集的生产者将其转化为标准化的格式,而是数据的解释成为消费者的问题(读时模式方法【56】;参阅“[文档模型中的架构灵活性](ch2.md#文档模型中的架构灵活性)”)。如果生产者和消费者是不同优先级的不同团队,这可能是一个优势。甚至可能不存在一个理想的数据模型,而是对适合不同目的的数据有不同的看法。以原始形式简单地转储数据可以进行多次这样的转换。这种方法被称为寿司原则:“原始数据更好”【57】。 + +因此,Hadoop经常被用于实现ETL过程(参阅“[数据仓库](ch3.md#数据仓库)”):事务处理系统中的数据以某种原始形式转储到分布式文件系统中,然后编写MapReduce作业来清理数据,将其转换为关系表单,并将其导入MPP数据仓库以进行分析。数据建模仍然在发生,但它是在一个单独的步骤中,从数据收集中分离出来的。这种解耦是可能的,因为分布式文件系统支持以任何格式编码的数据。 #### 加工模型的多样性 @@ -490,35 +494,35 @@ MPP数据库是单一的,紧密集成的软件,负责磁盘上的存储布 另一方面,并非所有类型的处理都可以合理地表达为SQL查询。例如,如果要构建机器学习和推荐系统,或者使用相关性排名模型的全文搜索索引,或者执行图像分析,则很可能需要更一般的数据处理模型。这些类型的处理通常对特定的应用程序非常具体(例如机器学习的特征工程,机器翻译的自然语言模型,欺诈预测的风险评估函数),因此它们不可避免地需要编写代码,而不仅仅是查询。 -MapReduce使工程师能够轻松地在大型数据集上运行自己的代码。如果你有HDFS和MapReduce,那么你可以在它上面建立一个SQL查询执行引擎,事实上这正是Hive项目所做的[31]。但是,您也可以编写许多其他形式的批处理,这些批处理不适合用SQL查询表示。 +MapReduce使工程师能够轻松地在大型数据集上运行自己的代码。如果你有HDFS和MapReduce,那么你可以在它上面建立一个SQL查询执行引擎,事实上这正是Hive项目所做的【31】。但是,你也可以编写许多其他形式的批处理,这些批处理不适合用SQL查询表示。 -随后,人们发现MapReduce对于某些类型的处理来说太过于限制,执行得太差,因此其他各种处理模型都是在Hadoop之上开发的(我们将在第419页的“Beyond MapReduce”中看到其中的一些)。有两种处理模型,SQL和MapReduce,还不够,需要更多不同的模型!而且由于Hadoop平台的开放性,实施一整套方法是可行的,而这在整体MPP数据库的范围内是不可能的[58]。 +随后,人们发现MapReduce对于某些类型的处理来说太过于限制,执行得太差,因此其他各种处理模型都是在Hadoop之上开发的(我们将在“[后MapReduce时代](#后MapReduce时代)”中看到其中的一些)。有两种处理模型,SQL和MapReduce,还不够,需要更多不同的模型!而且由于Hadoop平台的开放性,实施一整套方法是可行的,而这在整体MPP数据库的范围内是不可能的【58】。 至关重要的是,这些不同的处理模型都可以在一个共享的机器上运行,所有这些机器都可以访问分布式文件系统上的相同文件。在Hadoop方法中,不需要将数据导入到几个不同的专用系统中进行不同类型的处理:系统足够灵活,可以支持同一个群集内不同的工作负载。不需要移动数据使得从数据中获得价值变得容易得多,并且使用新的处理模型更容易进行实验。 -Hadoop生态系统包括随机访问的OLTP数据库,如HBase(请参阅第70页的“SSTables和LSM-Trees”)和MPA样式的分析数据库,如Impala [41]。 HBase和Impala都不使用MapReduce,但都使用HDFS进行存储。它们是访问和处理数据的非常不同的方法,但是它们可以共存并被集成到同一个系统中。 +Hadoop生态系统包括随机访问的OLTP数据库,如HBase(参阅“[SSTables和LSM树](ch3.md#SSTables和LSM树)”)和MPA样式的分析数据库,如Impala 【41】。 HBase和Impala都不使用MapReduce,但都使用HDFS进行存储。它们是访问和处理数据的非常不同的方法,但是它们可以共存并被集成到同一个系统中。 #### 为频繁的故障而设计 在比较MapReduce和MPP数据库时,设计方法的另外两个不同点是:处理故障和使用内存和磁盘。与在线系统相比,批处理对故障不太敏感,因为如果失败,用户不会立即影响用户,并且可以再次运行。 -如果一个节点在执行查询时崩溃,大多数MPP数据库会中止整个查询,并让用户重新提交查询或自动重新运行它[3]。由于查询通常最多运行几秒钟或几分钟,所以这种处理错误的方法是可以接受的,因为重试的代价不是太大。 MPP数据库还倾向于在内存中保留尽可能多的数据(例如,使用散列连接)以避免从磁盘读取的成本。 +如果一个节点在执行查询时崩溃,大多数MPP数据库会中止整个查询,并让用户重新提交查询或自动重新运行它【3】。由于查询通常最多运行几秒钟或几分钟,所以这种处理错误的方法是可以接受的,因为重试的代价不是太大。 MPP数据库还倾向于在内存中保留尽可能多的数据(例如,使用散列连接)以避免从磁盘读取的成本。 另一方面,MapReduce可以容忍映射或减少任务的失败,而不会影响作业的整体,通过以单个任务的粒度重试工作。它也非常渴望将数据写入磁盘,一方面是为了容错,另一方面是假设数据集太大而不能适应内存。 MapReduce方法更适用于较大的作业:处理如此之多的数据并运行很长时间的作业,以至于在此过程中可能至少遇到一个任务故障。在这种情况下,由于单个任务失败而重新运行整个工作将是浪费的。即使以单个任务的粒度进行恢复引入了使得无故障处理更慢的开销,但如果任务失败率足够高,仍然可以进行合理的权衡。 -但是这些假设有多现实呢?在大多数集群中,机器故障确实发生,但是它们不是很频繁 - 可能很少,大多数工作都不会经验,因为机器故障。为了容错,真的值得引起重大的开销吗? +但是这些假设有多现实呢?在大多数集群中,机器故障确实发生,但是它们不是很频繁 —— 可能很少,大多数工作都不会经验,因为机器故障。为了容错,真的值得引起重大的开销吗? -要了解MapReduce节省使用内存和任务级恢复的原因,查看最初设计MapReduce的环境是很有帮助的。 Google拥有混合使用的数据中心,在线生产服务和离线批处理作业在同一台机器上运行。每个任务都有一个使用容器执行的资源分配(CPU核心,RAM,磁盘空间等)。每个任务也具有优先级,如果优先级较高的任务需要更多的资源,则可以终止(抢占)同一台机器上较低优先级的任务以释放资源。优先级还决定了计算资源的定价:团队必须为他们使用的资源付费,而优先级更高的流程花费更多[59]。 +要了解MapReduce节省使用内存和任务级恢复的原因,查看最初设计MapReduce的环境是很有帮助的。 Google拥有混合使用的数据中心,在线生产服务和离线批处理作业在同一台机器上运行。每个任务都有一个使用容器执行的资源分配(CPU核心,RAM,磁盘空间等)。每个任务也具有优先级,如果优先级较高的任务需要更多的资源,则可以终止(抢占)同一台机器上较低优先级的任务以释放资源。优先级还决定了计算资源的定价:团队必须为他们使用的资源付费,而优先级更高的流程花费更多【59】。 这种架构允许非生产(低优先级)计算资源被过度使用,因为系统知道如果必要的话它可以回收资源。与分离生产和非生产任务的系统相比,过度使用资源可以更好地利用机器和提高效率。但是,由于MapReduce作业以低优先级运行,因此它们随时都有被抢占的风险,因为优先级较高的进程需要其资源。批量工作有效地“拿起桌子下面的碎片”,利用高优先级进程已经采取的任何计算资源。 -在谷歌,运行一个小时的MapReduce任务有大约5%被终止的风险,为更高优先级的进程腾出空间。由于硬件问题,机器重新启动或其他原因,这个速率比故障率高出一个数量级[59]。按照这种抢先率,如果一个作业有100个任务,每个任务运行10分钟,那么至少有一个任务在完成之前将被终止的风险大于50%。 +在谷歌,运行一个小时的MapReduce任务有大约5%被终止的风险,为更高优先级的进程腾出空间。由于硬件问题,机器重新启动或其他原因,这个速率比故障率高出一个数量级【59】。按照这种抢先率,如果一个作业有100个任务,每个任务运行10分钟,那么至少有一个任务在完成之前将被终止的风险大于50%。 这就是为什么MapReduce能够容忍频繁意外的任务终止的原因:这不是因为硬件特别不可靠,这是因为任意终止进程的自由可以在计算集群中更好地利用资源。 -在开源的集群调度器中,抢占的使用较少。 YARN的CapacityScheduler支持抢占以平衡不同队列的资源分配[58],但在编写本文时,YARN,Mesos或Kubernetes不支持通用优先级抢占[60]。在任务不经常被终止的环境中,MapReduce的设计决策没有多少意义。在下一节中,我们将看看MapReduce的一些替代方案,这些替代方案做出了不同的设计决定。 +在开源的集群调度器中,抢占的使用较少。 YARN的CapacityScheduler支持抢占以平衡不同队列的资源分配【58】,但在编写本文时,YARN,Mesos或Kubernetes不支持通用优先级抢占【60】。在任务不经常被终止的环境中,MapReduce的设计决策没有多少意义。在下一节中,我们将看看MapReduce的一些替代方案,这些替代方案做出了不同的设计决定。 @@ -526,11 +530,11 @@ MapReduce方法更适用于较大的作业:处理如此之多的数据并运 虽然MapReduce在二十世纪二十年代后期变得非常流行并受到大量的炒作,但它只是分布式系统的许多可能的编程模型之一。根据数据量,数据结构和处理类型,其他工具可能更适合表达计算。 -尽管如此,我们在讨论MapReduce的这一章花了很多时间,因为它是一个有用的学习工具,因为它是分布式文件系统的一个相当清晰和简单的抽象。也就是说,能够理解它在做什么,而不是在易于使用的意义上是简单的。恰恰相反:使用原始的MapReduce API来实现复杂的处理工作实际上是非常困难和费力的 - 例如,您需要从头开始实现任何连接算法[37]。 +尽管如此,我们在讨论MapReduce的这一章花了很多时间,因为它是一个有用的学习工具,因为它是分布式文件系统的一个相当清晰和简单的抽象。也就是说,能够理解它在做什么,而不是在易于使用的意义上是简单的。恰恰相反:使用原始的MapReduce API来实现复杂的处理工作实际上是非常困难和费力的 —— 例如,你需要从头开始实现任何连接算法【37】。 -针对直接使用MapReduce的困难,在MapReduce上创建了各种更高级的编程模型(Pig,Hive,Cascading,Crunch)作为抽象。如果您了解MapReduce的工作原理,那么它们相当容易学习,而且它们的高级构造使许多常见的批处理任务更容易实现。 +针对直接使用MapReduce的困难,在MapReduce上创建了各种更高级的编程模型(Pig,Hive,Cascading,Crunch)作为抽象。如果你了解MapReduce的工作原理,那么它们相当容易学习,而且它们的高级构造使许多常见的批处理任务更容易实现。 -但是,MapReduce执行模型本身也存在一些问题,这些问题并没有通过增加另一个抽象层次来解决,而且在某些类型的处理中表现得很差。一方面,MapReduce非常强大:您可以使用它来处理频繁任务终止的不可靠多租户系统上几乎任意大量的数据,并且仍然可以完成工作(虽然速度很慢)。另一方面,对于某些类型的处理来说,其他工具有时也会更快。 +但是,MapReduce执行模型本身也存在一些问题,这些问题并没有通过增加另一个抽象层次来解决,而且在某些类型的处理中表现得很差。一方面,MapReduce非常强大:你可以使用它来处理频繁任务终止的不可靠多租户系统上几乎任意大量的数据,并且仍然可以完成工作(虽然速度很慢)。另一方面,对于某些类型的处理来说,其他工具有时也会更快。 在本章的其余部分中,我们将介绍一些批处理方法。在第十一章我们将转向流处理,这可以看作是加速批处理的另一种方法。 @@ -538,44 +542,44 @@ MapReduce方法更适用于较大的作业:处理如此之多的数据并运 如前所述,每个MapReduce作业都独立于其他任何作业。作业与世界其他地方的主要联系点是分布式文件系统上的输入和输出目录。如果希望一个作业的输出成为第二个作业的输入,则需要将第二个作业的输入目录配置为与第一个作业的输出目录相同,并且外部工作流调度程序必须仅在第一份工作已经完成。 -如果第一个作业的输出是要在组织内广泛发布的数据集,则此设置是合理的。在这种情况下,您需要能够通过名称来引用它,并将其用作多个不同作业(包括由其他团队开发的作业)的输入。将数据发布到分布式文件系统中的众所周知的位置允许松耦合,这样作业就不需要知道是谁在输入输出或消耗其输出(请参阅“分离逻辑和布线”在本页395)。 +如果第一个作业的输出是要在组织内广泛发布的数据集,则此设置是合理的。在这种情况下,你需要能够通过名称来引用它,并将其用作多个不同作业(包括由其他团队开发的作业)的输入。将数据发布到分布式文件系统中的众所周知的位置允许松耦合,这样作业就不需要知道是谁在输入输出或消耗其输出(参阅“[分离逻辑和布线](#分离逻辑和布线)”在本页395)。 -但是,在很多情况下,您知道一个工作的输出只能用作另一个工作的输入,这个工作由同一个团队维护。在这种情况下,分布式文件系统上的文件只是简单的中间状态:一种将数据从一个作业传递到下一个作业的方式。在用于构建由50或100个MapReduce作业[29]组成的推荐系统的复杂工作流程中,存在很多这样的中间状态。 +但是,在很多情况下,你知道一个工作的输出只能用作另一个工作的输入,这个工作由同一个团队维护。在这种情况下,分布式文件系统上的文件只是简单的中间状态:一种将数据从一个作业传递到下一个作业的方式。在用于构建由50或100个MapReduce作业【29】组成的推荐系统的复杂工作流程中,存在很多这样的中间状态。 -将这个中间状态写入文件的过程称为物化。 (我们在第101页的“聚合:数据立方体和物化视图”中已经在物化视图的背景下遇到了这个术语。它意味着要急于计算某个操作的结果并写出来,而不是计算需要时按要求。) +将这个中间状态写入文件的过程称为物化。 (我们在“[聚合:数据立方体和物化视图](ch2.md#聚合:数据立方体和物化视图)”中已经在物化视图的背景下遇到了这个术语。它意味着要急于计算某个操作的结果并写出来,而不是计算需要时按要求。) 相反,本章开头的日志分析示例使用Unix管道将一个命令的输出与另一个命令的输出连接起来。管道并没有完全实现中间状态,而是只使用一个小的内存缓冲区,将输出逐渐流向输入。 MapReduce的完全实现中间状态的方法与Unix管道相比存在不足: * MapReduce作业只有在前面的作业(生成其输入)中的所有任务都完成时才能启动,而由Unix管道连接的进程同时启动,输出一旦生成就会被使用。不同机器上的偏差或不同的负荷意味着一份工作往往会有一些比其他人更快完成的离散任务。必须等到所有前面的工作完成才能减慢整个工作流程的执行。 -* 映射器通常是多余的:它们只读取刚刚由reducer写入的相同文件,并为下一个分区和排序阶段做好准备。在许多情况下,映射器代码可能是以前的reducer的一部分:如果reducer输出被分区和排序的方式与mapper输出相同,那么reducers可以直接链接在一起,而不与mapper阶段交错。 +* Mapper通常是多余的:它们只读取刚刚由reducer写入的相同文件,并为下一个分区和排序阶段做好准备。在许多情况下,Mapper代码可能是以前的reducer的一部分:如果reducer输出被分区和排序的方式与mapper输出相同,那么reducers可以直接链接在一起,而不与mapper阶段交错。 * 将中间状态存储在分布式文件系统中意味着这些文件被复制到多个节点,这对于这样的临时数据通常是过度的。 #### 数据流引擎 -了解决MapReduce的这些问题,开发了几种用于分布式批量计算的新的执行引擎,其中最着名的是Spark [61,62],Tez [63,64]和Flink [65,66]。他们设计的方式有很多不同之处,但他们有一个共同点:他们把整个工作流作为一项工作来处理,而不是把它分解成独立的子作业。 +了解决MapReduce的这些问题,开发了几种用于分布式批量计算的新的执行引擎,其中最着名的是Spark 【61,62】,Tez 【63,64】和Flink 【65,66】。他们设计的方式有很多不同之处,但他们有一个共同点:他们把整个工作流作为一项工作来处理,而不是把它分解成独立的子作业。 由于它们通过几个处理阶段明确地建模数据流,所以这些系统被称为数据流引擎。像MapReduce一样,它们通过反复调用用户定义的函数来在单个线程上一次处理一条记录。他们通过对输入进行分区来并行工作,并将一个功能的输出复制到网络上,成为另一个功能的输入。 -与MapReduce不同,这些功能不需要交替映射和缩减的严格角色,而是可以以更灵活的方式进行组合。我们称之为这些函数操作符,数据流引擎提供了几个不同的选项来连接一个操作符的输出到另一个的输入: +与MapReduce不同,这些功能不需要交替映射和Reduce的严格角色,而是可以以更灵活的方式进行组合。我们称之为这些函数操作符,数据流引擎提供了几个不同的选项来连接一个操作符的输出到另一个的输入: -* 一个选项是通过键对记录进行重新分区和排序,就像在MapReduce的混洗阶段一样(请参阅“分布式执行MapReduce”)。此功能可以像在MapReduce中一样启用排序合并连接和分组。 +* 一个选项是通过键对记录进行重新分区和排序,就像在MapReduce的混洗阶段一样(参阅“[分布式执行MapReduce](#分布式执行MapReduce)”)。此功能可以像在MapReduce中一样启用排序合并连接和分组。 * 另一种可能是采取几个输入,并以相同的方式进行分区,但跳过排序。这节省了分区散列连接的工作,其中记录的分区是重要的,但顺序是不相关的,因为构建散列表随机化了顺序。 * 对于广播散列连接,可以将一个运算符的相同输出发送到连接运算符的所有分区。 -这种处理引擎的风格基于像Dryad [67]和Nephele [68]这样的研究系统,与MapReduce模型相比,它提供了几个优点: +这种处理引擎的风格基于像Dryad 【67】和Nephele 【68】这样的研究系统,与MapReduce模型相比,它提供了几个优点: * 排序等昂贵的工作只需要在实际需要的地方执行,而不是在每个Map和Reduce阶段之间默认发生。 -* 没有不必要的地图任务,因为映射器所做的工作通常可以合并到前面的reduce操作器中(因为映射器不会更改数据集的分区)。 +* 没有不必要的Map任务,因为Mapper所做的工作通常可以合并到前面的reduce操作器中(因为Mapper不会更改数据集的分区)。 * 由于工作流程中的所有连接和数据依赖性都是明确声明的,因此调度程序会概述哪些数据是必需的,因此可以进行本地优化。例如,它可以尝试将占用某些数据的任务放在与生成它的任务相同的机器上,以便可以通过共享内存缓冲区交换数据,而不必通过网络复制数据。 -* 通常将操作员之间的中间状态保存在内存中或写入本地磁盘就足够了,这比写入HDFS需要更少的I / O(必须将其复制到多个计算机并写入到每个代理的磁盘上)。 MapReduce已经将这种优化用于映射器的输出,但是数据流引擎将该思想推广到了所有的中间状态。 +* 通常将操作员之间的中间状态保存在内存中或写入本地磁盘就足够了,这比写入HDFS需要更少的I/O(必须将其复制到多个计算机并写入到每个代理的磁盘上)。 MapReduce已经将这种优化用于Mapper的输出,但是数据流引擎将该思想推广到了所有的中间状态。 * 操作员可以在输入准备就绪后立即开始执行;在下一个开始之前不需要等待整个前一阶段的完成。 * 与MapReduce(为每个任务启动一个新的JVM)相比,现有的Java虚拟机(JVM)进程可以重用来运行新操作,从而减少启动开销。 -您可以使用数据流引擎来执行与MapReduce工作流相同的计算,并且由于此处所述的优化,通常执行速度会明显更快。既然操作符是map和reduce的泛化,相同的处理代码可以在任一执行引擎上运行:Pig,Hive或Cascading中实现的工作流可以通过简单的配置更改从MapReduce切换到Tez或Spark,而无需修改代码[64]。 +你可以使用数据流引擎来执行与MapReduce工作流相同的计算,并且由于此处所述的优化,通常执行速度会明显更快。既然操作符是map和reduce的泛化,相同的处理代码可以在任一执行引擎上运行:Pig,Hive或Cascading中实现的工作流可以通过简单的配置更改从MapReduce切换到Tez或Spark,而无需修改代码【64】。 -Tez是一个相当薄的库,它依赖于YARN shuffle服务来实现节点间数据的实际复制[58],而Spark和Flink则是包含自己的网络通信层,调度器和面向用户的API的大型框架。我们将在短期内讨论这些高级API。 +Tez是一个相当薄的库,它依赖于YARN shuffle服务来实现节点间数据的实际复制【58】,而Spark和Flink则是包含自己的网络通信层,调度器和面向用户的API的大型框架。我们将在短期内讨论这些高级API。 #### 容错 @@ -583,7 +587,7 @@ Tez是一个相当薄的库,它依赖于YARN shuffle服务来实现节点间 Spark,Flink和Tez避免将中间状态写入HDFS,因此他们采取了不同的方法来容忍错误:如果一台机器发生故障,并且该机器上的中间状态丢失,则会从其他仍然可用的数据重新计算在可能的情况下是在先的中间阶段,或者是通常在HDFS上的原始输入数据)。 -为了实现这个重新计算,框架必须跟踪一个给定的数据是如何计算的 - 使用哪个输入分区,以及哪个操作符被应用到它。 Spark使用弹性分布式数据集(RDD)抽象来追踪数据的祖先[61],而Flink检查点操作符状态,允许其恢复运行在执行过程中遇到错误的操作符[66]。 +为了实现这个重新计算,框架必须跟踪一个给定的数据是如何计算的 —— 使用哪个输入分区,以及哪个操作符被应用到它。 Spark使用弹性分布式数据集(RDD)抽象来追踪数据的祖先【61】,而Flink检查点操作符状态,允许其恢复运行在执行过程中遇到错误的操作符【66】。 在重新计算数据时,重要的是要知道计算是否是确定性的:也就是说,给定相同的输入数据,操作员是否始终生成相同的输出?如果一些丢失的数据已经发送给下游运营商,这个问题就很重要。如果运营商重新启动,重新计算的数据与原有的丢失数据不一致,下游运营商很难解决新旧数据之间的矛盾。对于不确定性运营商来说,解决方案通常是杀死下游运营商,然后再运行新数据。 @@ -595,19 +599,19 @@ Spark,Flink和Tez避免将中间状态写入HDFS,因此他们采取了不同 回到Unix的类比,我们看到MapReduce就像是将每个命令的输出写入临时文件,而数据流引擎看起来更像是Unix管道。尤其是Flink是围绕流水线执行的思想而建立的:也就是说,将运算符的输出递增地传递给其他操作符,并且在开始处理之前不等待输入完成。 -排序操作不可避免地需要消耗其整个输入,然后才能生成任何输出,因为最后一个输入记录可能是具有最低密钥的输入记录,因此需要作为第一个输出记录。任何需要分类的操作员都需要至少暂时地累积状态。但是工作流程的许多其他部分可以以流水线方式执行。 +排序操作不可避免地需要消耗其整个输入,然后才能生成任何输出,因为最后一个输入记录可能是具有最低键的输入记录,因此需要作为第一个输出记录。任何需要分类的操作员都需要至少暂时地累积状态。但是工作流程的许多其他部分可以以流水线方式执行。 -当作业完成时,它的输出需要持续到某个地方,以便用户可以找到并使用它 - 很可能它会再次写入分布式文件系统。因此,在使用数据流引擎时,HDFS上的物化数据集通常仍是作业的输入和最终输出。和MapReduce一样,输入是不可变的,输出被完全替换。对MapReduce的改进是,您可以节省自己将所有中间状态写入文件系统。 +当作业完成时,它的输出需要持续到某个地方,以便用户可以找到并使用它—— 很可能它会再次写入分布式文件系统。因此,在使用数据流引擎时,HDFS上的物化数据集通常仍是作业的输入和最终输出。和MapReduce一样,输入是不可变的,输出被完全替换。对MapReduce的改进是,你可以节省自己将所有中间状态写入文件系统。 ### 图与迭代处理 -在第49页上的“类似图形的数据模型”中,我们讨论了使用图形来建模数据,并使用图形查询语言来遍历图形中的边和顶点。第2章的讨论集中在OLTP风格的使用上:快速执行查询来查找少量符合特定条件的顶点。 +在“[图数据模型](ch2.md#图数据模型)”中,我们讨论了使用图形来建模数据,并使用图形查询语言来遍历图形中的边和顶点。[第2章](ch2.md)的讨论集中在OLTP风格的使用上:快速执行查询来查找少量符合特定条件的顶点。 -在批处理环境中查看图表也很有趣,其目标是在整个图表上执行某种离线处理或分析。这种需求经常出现在机器学习应用程序(如推荐引擎)或排序系统中。例如,最着名的图形分析算法之一是PageRank [69],它试图根据其他网页链接的网页来估计网页的流行度。它被用作确定网络搜索引擎呈现结果的顺序的公式的一部分。 +在批处理环境中查看图表也很有趣,其目标是在整个图表上执行某种离线处理或分析。这种需求经常出现在机器学习应用程序(如推荐引擎)或排序系统中。例如,最着名的图形分析算法之一是PageRank 【69】,它试图根据其他网页链接的网页来估计网页的流行度。它被用作确定网络搜索引擎呈现结果的顺序的公式的一部分。 -> 像Spark,Flink和Tez这样的数据流引擎(参见第419页“中间状态的实现化”)通常将操作符作为有向无环图(DAG)排列在作业中。这与图形处理不一样:在数据流引擎中,从一个操作符到另一个操作符的数据流被构造成一个图,而数据本身通常由关系式元组构成。在图形处理中,数据本身具有图形的形式。另一个不幸的命名混乱! +> 像Spark,Flink和Tez这样的数据流引擎(参见“[中间状态的物化](#中间状态的物化)”)通常将操作符作为**有向无环图(DAG)**排列在作业中。这与图形处理不一样:在数据流引擎中,从一个操作符到另一个操作符的数据流被构造成一个图,而数据本身通常由关系式元组构成。在图形处理中,数据本身具有图形的形式。另一个不幸的命名混乱! -许多图算法是通过一次遍历一个边来表示的,将一个顶点与相邻的顶点连接起来以便传播一些信息,并且重复直到满足一些条件为止 - 例如,直到没有更多的边要跟随,或者直到一些度量收敛。我们在图2-6中看到一个例子,它通过重复地跟踪指示哪个位置在哪个其他位置(这种算法被称为传递闭包)的边缘,列出了包含在数据库中的北美所有位置。 +许多图算法是通过一次遍历一个边来表示的,将一个顶点与相邻的顶点连接起来以便传播一些信息,并且重复直到满足一些条件为止——例如,直到没有更多的边要跟随,或者直到一些度量收敛。我们在[图2-6](img/fig2-6.png)中看到一个例子,它通过重复地跟踪指示哪个位置在哪个其他位置(这种算法被称为传递闭包)的边缘,列出了包含在数据库中的北美所有位置。 可以在分布式文件系统(包含顶点和边的列表的文件)中存储图形,但是这种“重复直到完成”的想法不能用普通的MapReduce来表示,因为它只执行一次数据传递。这种算法因此经常以迭代方式实现: @@ -618,20 +622,20 @@ Spark,Flink和Tez避免将中间状态写入HDFS,因此他们采取了不同 这种方法是有效的,但是用MapReduce实现它往往是非常低效的,因为MapReduce没有考虑算法的迭代性质:它总是读取整个输入数据集并产生一个全新的输出数据集,即使只有一小部分该图与上次迭代相比已经改变。 Pregel处理模型 -作为批处理图形的优化,计算的批量同步并行(BSP)模型[70]已经流行起来。其中,它由Apache Giraph [37],Spark的GraphX API和Flink的Gelly API [71]实现。它也被称为Pregel模型,正如Google的Pregel论文推广这种处理图的方法[72]。 +作为批处理图形的优化,计算的批量同步并行(BSP)模型【70】已经流行起来。其中,它由Apache Giraph 【37】,Spark的GraphX API和Flink的Gelly API 【71】实现。它也被称为Pregel模型,正如Google的Pregel论文推广这种处理图的方法【72】。 -回想一下在MapReduce中,映射器在概念上“发送消息”给reducer的特定调用,因为框架将所有的mapper输出集中在一起。 Pregel背后有一个类似的想法:一个顶点可以“发送消息”到另一个顶点,通常这些消息沿着图的边被发送。 +回想一下在MapReduce中,Mapper在概念上“发送消息”给reducer的特定调用,因为框架将所有的mapper输出集中在一起。 Pregel背后有一个类似的想法:一个顶点可以“发送消息”到另一个顶点,通常这些消息沿着图的边被发送。 -在每次迭代中,为每个顶点调用一个函数,将所有发送给它的消息传递给它 - 就像调用reducer一样。与MapReduce的不同之处在于,在Pregel模型中,顶点从一次迭代到下一次迭代记忆它的状态,所以这个函数只需要处理新的传入消息。如果在图的某个部分没有发送消息,则不需要做任何工作。 +在每次迭代中,为每个顶点调用一个函数,将所有发送给它的消息传递给它 —— 就像调用reducer一样。与MapReduce的不同之处在于,在Pregel模型中,顶点从一次迭代到下一次迭代记忆它的状态,所以这个函数只需要处理新的传入消息。如果在图的某个部分没有发送消息,则不需要做任何工作。 -这与演员模型有些相似(请参阅第130页上的“分布式演员框架”),除非顶点状态和顶点之间的消息具有容错性和耐久性,并且通信以固定的方式进行,否则将每个顶点视为主角轮次:在每一次迭代中,框架传递在前一次迭代中发送的所有消息。演员通常没有这样的时间保证。 +这与演员模型有些相似(参阅“[分布式的Actor框架](ch4.md#分布式的Actor框架)”),除非顶点状态和顶点之间的消息具有容错性和耐久性,并且通信以固定的方式进行,否则将每个顶点视为主角轮次:在每一次迭代中,框架传递在前一次迭代中发送的所有消息。演员通常没有这样的时间保证。 #### 容错 顶点只能通过消息传递进行通信(而不是直接相互查询)的事实有助于提高Pregel作业的性能,因为消息可以成批处理,而且等待通信的次数也减少了。唯一的等待是在迭代之间:由于Pregel模型保证所有在一次迭代中发送的消息都在下一次迭代中传递,所以先前的迭代必须完全完成,并且所有的消息必须在网络上复制,然后下一个开始。 -即使底层网络可能丢失,重复或任意延迟消息(请参阅第267页上的“不可靠网络”),Pregel实施可保证在接下来的迭代中消息在其目标顶点处理一次。像MapReduce一样,该框架透明地从故障中恢复,以简化Pregel顶层算法的编程模型。 +即使底层网络可能丢失,重复或任意延迟消息(参阅“[不可靠的网络](ch8.md#不可靠的网络)”),Pregel实施可保证在接下来的迭代中消息在其目标顶点处理一次。像MapReduce一样,该框架透明地从故障中恢复,以简化Pregel顶层算法的编程模型。 -这种容错是通过在迭代结束时定期检查所有顶点的状态来实现的,即将其全部状态写入持久存储。如果某个节点发生故障并且其内存中状态丢失,则最简单的解决方法是将整个图计算回滚到上一个检查点,然后重新启动计算。如果算法是确定性的并且记录了消息,那么也可以选择性地只恢复丢失的分区(就像我们之前讨论过的数据流引擎)[72]。 +这种容错是通过在迭代结束时定期检查所有顶点的状态来实现的,即将其全部状态写入持久存储。如果某个节点发生故障并且其内存中状态丢失,则最简单的解决方法是将整个图计算回滚到上一个检查点,然后重新启动计算。如果算法是确定性的并且记录了消息,那么也可以选择性地只恢复丢失的分区(就像我们之前讨论过的数据流引擎)【72】。 #### 并行执行 @@ -641,7 +645,7 @@ Pregel处理模型 因此,图算法通常会有很多跨机器通信,而中间状态(节点之间发送的消息)往往比原始图大。通过网络发送消息的开销会显着减慢分布式图算法的速度。 -出于这个原因,如果你的图可以放在一台计算机的内存中,那么单机(甚至可能是单线程)算法很可能会超越分布式批处理[73,74]。即使图形大于内存,也可以放在单个计算机的磁盘上,使用GraphChi等框架进行单机处理是一个可行的选择[75]。如果图形太大而不适合单个机器,像Pregel这样的分布式方法是不可避免的。有效的并行化图算法是一个正在进行的领域。 +出于这个原因,如果你的图可以放在一台计算机的内存中,那么单机(甚至可能是单线程)算法很可能会超越分布式批处理【73,74】。即使图形大于内存,也可以放在单个计算机的磁盘上,使用GraphChi等框架进行单机处理是一个可行的选择【75】。如果图形太大而不适合单个机器,像Pregel这样的分布式方法是不可避免的。有效的并行化图算法是一个正在进行的领域。 @@ -649,27 +653,27 @@ Pregel处理模型 自MapReduce第一次流行以来,分布式批处理的执行引擎已经成熟。到目前为止,基础设施已经足够强大,能够存储和处理超过10,000台机器群集上的数PB的数据。由于在这种规模下物理操作批处理过程的问题已经或多或少得到了解决,所以已经转向其他领域:改进编程模型,提高处理效率,扩大这些技术可以解决的问题集。 -如前所述,Hive,Pig,Cascading和Crunch等高级语言和API由于手工编写MapReduce作业而变得非常流行。随着Tez的出现,这些高级语言还有额外的好处,可以移动到新的数据流执行引擎,而无需重写作业代码。 Spark和Flink也包括他们自己的高级数据流API,经常从FlumeJava中获得灵感[34]。 +如前所述,Hive,Pig,Cascading和Crunch等高级语言和API由于手工编写MapReduce作业而变得非常流行。随着Tez的出现,这些高级语言还有额外的好处,可以移动到新的数据流执行引擎,而无需重写作业代码。 Spark和Flink也包括他们自己的高级数据流API,经常从FlumeJava中获得灵感【34】。 这些数据流API通常使用关系式构建块来表达一个计算:连接数据集以获取某个字段的值;按键分组元组;过滤一些条件;并通过计数,求和或其他函数来聚合元组。在内部,这些操作是使用本章前面讨论过的各种连接和分组算法来实现的。 -除了需要较少代码的明显优势之外,这些高级接口还允许交互式使用,在这种交互式使用中,您可以将分析代码逐步编写到shell中并经常运行,以观察它正在做什么。这种发展风格在探索数据集和试验处理方法时非常有用。这也让人联想到Unix哲学,我们在第394页的“Unix哲学”中讨论过这个问题。 +除了需要较少代码的明显优势之外,这些高级接口还允许交互式使用,在这种交互式使用中,你可以将分析代码逐步编写到shell中并经常运行,以观察它正在做什么。这种发展风格在探索数据集和试验处理方法时非常有用。这也让人联想到Unix哲学,我们在第394页的“Unix哲学”中讨论过这个问题。 而且,这些高级接口不仅使人类使用系统的效率更高,而且提高了机器级别的工作执行效率。 #### 向声明式查询语言的转变 -与拼写执行连接的代码相比,指定连接为关系运算符的优点是,框架可以分析连接输入的属性,并自动决定哪个上述连接算法最适合手头的任务。 Hive,Spark和Flink都有基于代价的查询优化器,可以做到这一点,甚至可以改变连接顺序,使中间状态的数量最小化[66,77,78,79]。 +与拼写执行连接的代码相比,指定连接为关系运算符的优点是,框架可以分析连接输入的属性,并自动决定哪个上述连接算法最适合手头的任务。 Hive,Spark和Flink都有基于代价的查询优化器,可以做到这一点,甚至可以改变连接顺序,使中间状态的数量最小化【66,77,78,79】。 连接算法的选择可以对批处理作业的性能产生很大的影响,不必理解和记住本章中讨论的各种连接算法。如果以声明的方式指定连接,则这是可能的:应用程序简单地说明哪些连接是必需的,查询优化器决定如何最好地执行连接。我们以前在第42页的“数据的查询语言”中遇到了这个想法。 但是,在其他方面,MapReduce及其数据流后继与SQL的完全声明性查询模型有很大不同。 MapReduce是围绕函数回调的思想构建的:对于每个记录或者一组记录,调用一个用户定义的函数(mapper或reducer),并且该函数可以自由地调用任意代码来决定输出什么。这种方法的优点是可以绘制在现有库的大型生态系统上进行分析,自然语言分析,图像分析以及运行数字或统计算法等。 -轻松运行任意代码的自由是从MPP数据库(参见“比较Hadoop到分布式数据库”一节,第414页)中分离出来的MapReduce传统批处理系统。虽然数据库具有编写用户定义函数的功能,但是它们通常使用起来很麻烦,而且与大多数编程语言中广泛使用的程序包管理器和依赖管理系统(例如Maven for Java,npm for Java-Script,和Ruby的Ruby的Ruby)。 +轻松运行任意代码的自由是从MPP数据库(参见“[比较Hadoop和分布式数据库](#比较Hadoop和分布式数据库)”一节)中分离出来的MapReduce传统批处理系统。虽然数据库具有编写用户定义函数的功能,但是它们通常使用起来很麻烦,而且与大多数编程语言中广泛使用的程序包管理器和依赖管理系统(例如Maven for Java,npm for Java-Script,和Ruby的Ruby的Ruby)。 -但是,数据流引擎已经发现,除了连接之外,在合并更多的声明性特征方面也是有优势的。例如,如果一个回调函数只包含一个简单的过滤条件,或者只是从一条记录中选择了一些字段,那么在调用每条记录的函数时会有相当大的CPU开销。如果以声明方式表示这样简单的过滤和映射操作,那么查询优化器可以利用面向列的存储布局(请参阅第95页的“面向列的存储”),并从磁盘只读取所需的列。 Hive,Spark DataFrames和Impala也使用向量化执行(请参阅第99页的“内存带宽和向量化处理”):在对CPU缓存很友好的内部循环中迭代数据,并避免函数调用。 +但是,数据流引擎已经发现,除了连接之外,在合并更多的声明性特征方面也是有优势的。例如,如果一个回调函数只包含一个简单的过滤条件,或者只是从一条记录中选择了一些字段,那么在调用每条记录的函数时会有相当大的CPU开销。如果以声明方式表示这样简单的过滤和映射操作,那么查询优化器可以利用面向列的存储布局(参阅“[列存储](ch3.md#列存储)”),并从磁盘只读取所需的列。 Hive,Spark DataFrames和Impala也使用向量化执行(参阅“[内存带宽和向量处理](ch3.md#内存带宽和向量处理)”):在对CPU缓存很友好的内部循环中迭代数据,并避免函数调用。 -Spark生成JVM字节码[79],Impala使用LLVM为这些内部循环生成本机代码[41]。 +Spark生成JVM字节码【79】,Impala使用LLVM为这些内部循环生成本机代码【41】。 通过将声明性方面与高级API结合起来,并使查询优化器可以在执行期间利用这些优化方法,批处理框架看起来更像MPP数据库(并且可以实现可比较的性能)。同时,通过具有运行任意代码和以任意格式读取数据的可扩展性,它们保持了灵活性的优势。 @@ -677,12 +681,14 @@ Spark生成JVM字节码[79],Impala使用LLVM为这些内部循环生成本机 尽管能够运行任意代码的可扩展性是有用的,但是在标准处理模式不断重复发生的情况下也有许多常见的情况,所以值得重用通用构建块的实现。传统上,MPP数据库满足了商业智能分析师和业务报告的需求,但这只是许多使用批处理的领域之一。 -另一个越来越重要的领域是统计和数值算法,它们是机器学习应用(如分类和推荐系统)所需要的。可重复使用的实现正在出现:例如,Mahout在MapReduce,Spark和Flink之上实现了用于机器学习的各种算法,而MADlib在关系型MPP数据库(Apache HAWQ)中实现了类似的功能[54]。 +另一个越来越重要的领域是统计和数值算法,它们是机器学习应用(如分类和推荐系统)所需要的。可重复使用的实现正在出现:例如,Mahout在MapReduce,Spark和Flink之上实现了用于机器学习的各种算法,而MADlib在关系型MPP数据库(Apache HAWQ)中实现了类似的功能【54】。 -空间算法也是有用的,例如k-最近邻居[80],它在一些多维空间中搜索与给定物品接近的物品 - 这是一种类似的搜索。近似搜索对于基因组分析算法也很重要,它们需要找到相似但不相同的字符串[81]。 +空间算法也是有用的,例如最近邻搜索(kNN)【80】,它在一些多维空间中搜索与给定物品接近的物品 - 这是一种类似的搜索。近似搜索对于基因组分析算法也很重要,它们需要找到相似但不相同的字符串【81】。 批处理引擎正被用于分布式执行来自日益广泛的领域的算法。随着批处理系统获得内置功能和高级声明性操作符,并且随着MPP数据库变得更加可编程和灵活,两者开始看起来更相似:最终,它们都只是存储和处理数据的系统。 + + ## 本章小结 @@ -694,8 +700,9 @@ Spark生成JVM字节码[79],Impala使用LLVM为这些内部循环生成本机 ***分区*** -在MapReduce中,映射器根据输入文件块进行分区。映射器的输出被重新分区,分类并合并到可配置数量的reducer分区中。这个过程的目的是把所有的相关数据 - 例如,所有的记录都放在同一个地方。 -Post-MapReduce数据流引擎尽量避免排序,除非它是必需的,但它们采取了大致类似的分区方法。 +在MapReduce中,Mapper根据输入文件块进行分区。Mapper的输出被重新分区,分类并合并到可配置数量的reducer分区中。这个过程的目的是把所有的相关数据 —— 例如,所有的记录都放在同一个地方。 + +后MapReduce数据流引擎尽量避免排序,除非它是必需的,但它们采取了大致类似的分区方法。 ***容错*** @@ -705,23 +712,23 @@ MapReduce经常写入磁盘,这使得从单个失败的任务中轻松地恢 ***排序合并连接*** -每个正在连接的输入都通过一个提取连接键的映射器。通过分区,排序和合并,具有相同密钥的所有记录最终都会进入reducer的相同调用。这个函数可以输出连接的记录。 +每个正在连接的输入都通过一个提取连接键的Mapper。通过分区,排序和合并,具有相同键的所有记录最终都会进入reducer的相同调用。这个函数可以输出连接的记录。 ***广播散列连接*** -两个连接输入之一是小的,所以它没有分区,它可以被完全加载到一个哈希表。因此,您可以为大连接输入的每个分区启动一个映射器,将小输入的散列表加载到每个映射器中,然后一次扫描大输入一条记录,查询每条记录的散列表。 +两个连接输入之一是小的,所以它没有分区,它可以被完全加载到一个哈希表。因此,你可以为大连接输入的每个分区启动一个Mapper,将小输入的散列表加载到每个Mapper中,然后一次扫描大输入一条记录,查询每条记录的散列表。 ***分区散列连接*** -如果两个连接输入以相同的方式分区(使用相同的密钥,相同的散列函数和相同数量的分区),则可以独立地为每个分区使用散列表方法。 +如果两个连接输入以相同的方式分区(使用相同的键,相同的散列函数和相同数量的分区),则可以独立地为每个分区使用散列表方法。 -分布式批处理引擎有一个有意限制的编程模型:回调函数(比如映射器和简化器)被认为是无状态的,除了指定的输出外,没有外部可见的副作用。这个限制允许框架隐藏抽象背后的一些硬分布式系统问题:面对崩溃和网络问题,任务可以安全地重试,任何失败任务的输出都被丢弃。如果某个分区的多个任务成功,则只有其中一个实际上使其输出可见。 +分布式批处理引擎有一个有意限制的编程模型:回调函数(比如Mapper和Reducer)被认为是无状态的,除了指定的输出外,没有外部可见的副作用。这个限制允许框架隐藏抽象背后的一些硬分布式系统问题:面对崩溃和网络问题,任务可以安全地重试,任何失败任务的输出都被丢弃。如果某个分区的多个任务成功,则只有其中一个实际上使其输出可见。 -得益于这个框架,您在批处理作业中的代码无需担心实现容错机制:框架可以保证作业的最终输出与没有发生错误的情况相同,也许不得不重新尝试各种任务。这些可靠的语义要比在线服务处理用户请求时经常使用的要多得多,而且在处理请求的副作用时写入数据库。 +得益于这个框架,你在批处理作业中的代码无需担心实现容错机制:框架可以保证作业的最终输出与没有发生错误的情况相同,也许不得不重新尝试各种任务。这些可靠的语义要比在线服务处理用户请求时经常使用的要多得多,而且在处理请求的副作用时写入数据库。 -批量处理工作的显着特点是它读取一些输入数据并产生一些输出数据,而不修改输入 - 换句话说,输出是从输入导出的。重要的是,输入数据是有界的:它有一个已知的,固定的大小(例如,它包含一些时间点的日志文件或数据库内容的快照)。因为它是有界的,一个工作知道什么时候它完成了整个输入的读取,所以一个工作最终完成。 +批量处理工作的显着特点是它读取一些输入数据并产生一些输出数据,而不修改输入—— 换句话说,输出是从输入衍生出的。重要的是,输入数据是有界的:它有一个已知的,固定的大小(例如,它包含一些时间点的日志文件或数据库内容的快照)。因为它是有界的,一个工作知道什么时候它完成了整个输入的读取,所以一个工作最终完成。 -在下一章中,我们将转向流处理,其中的输入是未知的 - 也就是说,你还有一份工作,但是它的输入是永无止境的数据流。在这种情况下,工作永远不会完成,因为在任何时候都可能有更多的工作进来。我们将看到流和批处理在某些方面是相 +在下一章中,我们将转向流处理,其中的输入是未知的 —— 也就是说,你还有一份工作,但是它的输入是永无止境的数据流。在这种情况下,工作永远不会完成,因为在任何时候都可能有更多的工作进来。我们将看到流和批处理在某些方面是相似的。但是关于无尽数据流的假设,也对我们构建系统的方式产生了很大的改变。 diff --git a/ch11.md b/ch11.md index cb38386..87f4be0 100644 --- a/ch11.md +++ b/ch11.md @@ -14,11 +14,11 @@ 然而,在第10章中仍然有一个大的假设:即输入是有界的,即已知和有限的大小,所以批处理知道它何时完成了它的输入。例如,MapReduce中心的排序操作必须读取其全部输入,然后才能开始生成输出:可能发生最后一个输入记录是具有最低键的输入记录,因此必须是第一个输出记录,所以提前开始输出不是一种选择。 -实际上,很多数据是无限的,因为它随着时间的推移逐渐到达:你的用户昨天和今天产生了数据,明天他们将继续产生更多的数据。除非您停业,否则这个过程永远都不会结束,所以数据集从来就不会以任何有意义的方式“完成”[1]。因此,批处理程序必须将数据人为地分成固定时间段的数据块,例如,在每天结束时处理一天的数据,或者在每小时结束时处理一小时的数据。 +实际上,很多数据是无限的,因为它随着时间的推移逐渐到达:你的用户昨天和今天产生了数据,明天他们将继续产生更多的数据。除非您停业,否则这个过程永远都不会结束,所以数据集从来就不会以任何有意义的方式“完成”【1】。因此,批处理程序必须将数据人为地分成固定时间段的数据块,例如,在每天结束时处理一天的数据,或者在每小时结束时处理一小时的数据。 日常批处理过程中的问题是,输入的更改只会在一天之后的输出中反映出来,这对于许多急躁的用户来说太慢了。为了减少延迟,我们可以更频繁地运行处理 - 比如说,在每秒钟的末尾,甚至连续地处理秒数的数据,完全放弃固定的时间片,并简单地处理每一个事件。这是流处理背后的想法。 -一般来说,“流”是指随着时间的推移逐渐可用的数据。这个概念出现在很多地方:Unix的stdin和stdout,编程语言(lazy lists)[2],文件系统API(如Java的FileInputStream),TCP连接,通过互联网传送音频和视频等等。 +一般来说,“流”是指随着时间的推移逐渐可用的数据。这个概念出现在很多地方:Unix的stdin和stdout,编程语言(lazy lists)【2】,文件系统API(如Java的FileInputStream),TCP连接,通过互联网传送音频和视频等等。 在本章中,我们将把事件流视为一个数据管理机制:我们在上一章中看到的批量数据的无界的,递增处理的对应物。我们将首先讨论流如何被表示,存储和通过网络传输。在第451页的“数据库和流”中,我们将调查流和数据库之间的关系。最后,在第464页的“Processing Streams”中,我们将探索连续处理这些流的方法和工具,以及它们可以用来构建应用程序的方法。 @@ -41,13 +41,13 @@ 事件可能被编码为文本字符串或JSON,或者以某种二进制形式编码,如第4章所述。这种编码允许您存储一个事件,例如将其附加到一个文件,将其插入关系表,或将其写入文档数据库。它还允许您通过网络将事件发送到另一个节点以进行处理。 -在批处理中,文件被写入一次,然后可能被多个作业读取。类似地,在流媒体术语中,一个事件由生产者(也称为发布者或发送者)生成一次,然后由多个消费者(订阅者或接收者)进行处理[3]。在文件系统中,文件名标识一组相关记录;在流媒体系统中,相关的事件通常被组合成一个主题或流。 +在批处理中,文件被写入一次,然后可能被多个作业读取。类似地,在流媒体术语中,一个事件由生产者(也称为发布者或发送者)生成一次,然后由多个消费者(订阅者或接收者)进行处理【3】。在文件系统中,文件名标识一组相关记录;在流媒体系统中,相关的事件通常被组合成一个主题或流。 原则上,文件或数据库足以连接生产者和消费者:生产者将其生成的每个事件写入数据存储,并且每个使用者定期轮询数据存储以检查自上次运行以来出现的事件。这实际上是批处理在每天结束时处理一天的数据的过程。 但是,如果数据存储不是为这种用途而设计的,那么在延迟较小的情况下继续进行处理时,轮询将变得非常昂贵。您调查的次数越多,返回新事件的请求百分比越低,因此开销越高。相反,当新事件出现时,最好通知消费者。 -数据库传统上不太支持这种通知机制:关系数据库通常具有触发器,它们可以对变化作出反应(例如,将一行插入到表中),但是它们的功能非常有限,有点在数据库设计中是事后的[4,5]。相反,已经开发了专门的工具来提供事件通知。 +数据库传统上不太支持这种通知机制:关系数据库通常具有触发器,它们可以对变化作出反应(例如,将一行插入到表中),但是它们的功能非常有限,有点在数据库设计中是事后的【4,5】。相反,已经开发了专门的工具来提供事件通知。 @@ -61,11 +61,11 @@ 1. 如果生产者发送消息的速度比消费者能够处理的速度快,会发生什么?一般来说,有三种选择:系统可以放置消息,缓冲队列中的消息或应用背压(也称为流量控制;即阻止生产者发送更多的消息)。例如,Unix管道和TCP使用背压:他们有一个小的固定大小的缓冲区,如果填满,发件人被阻塞,直到收件人从缓冲区中取出数据(参见“网络拥塞和排队”(第282页))。 - 如果消息被缓存在队列中,那么了解该队列增长会发生什么很重要。如果队列不再适合内存,或者将消息写入磁盘,系统是否会崩溃?如果是这样,磁盘访问如何影响邮件系统的性能[6]? + 如果消息被缓存在队列中,那么了解该队列增长会发生什么很重要。如果队列不再适合内存,或者将消息写入磁盘,系统是否会崩溃?如果是这样,磁盘访问如何影响邮件系统的性能【6】? 2. 如果节点崩溃或暂时脱机,会发生什么情况 - 是否有消息丢失?与数据库一样,持久性可能需要写入磁盘和/或复制的某种组合(请参阅第227页的侧栏“复制和耐久性”),这有成本。如果您有时可能会丢失消息,则可能在同一硬件上获得更高的吞吐量和更低的延迟。 -消息丢失是否可以接受取决于应用程序。例如,对于定期传输的传感器读数和指标,偶尔丢失的数据点可能并不重要,因为更新后的值将在短时间后发送。但是,要注意,如果大量的消息被丢弃,那么衡量标准是不正确的[7]。如果您正在计数事件,那么更重要的是它们可靠地传送,因为每个丢失的消息都意味着不正确的计数器。 +消息丢失是否可以接受取决于应用程序。例如,对于定期传输的传感器读数和指标,偶尔丢失的数据点可能并不重要,因为更新后的值将在短时间后发送。但是,要注意,如果大量的消息被丢弃,那么衡量标准是不正确的【7】。如果您正在计数事件,那么更重要的是它们可靠地传送,因为每个丢失的消息都意味着不正确的计数器。 我们在第10章中探讨的批处理系统的一个很好的特性是它们提供了强大的可靠性保证:失败的任务会自动重试,失败任务的部分输出会自动丢弃。这意味着输出与没有发生故障一样,这有助于简化编程模型。在本章的后面,我们将研究如何在流式上下文中提供类似的保证。 @@ -73,13 +73,13 @@ 许多消息传递系统使用生产者和消费者之间的直接网络通信,而不通过中间节点: -* UDP组播广泛应用于金融行业,例如股票市场,其中低时延非常重要[8]。虽然UDP本身是不可靠的,但应用程序级别的协议可以恢复丢失的数据包(生产者必须记住它发送的数据包,以便它可以按需重新发送数据包)。 +* UDP组播广泛应用于金融行业,例如股票市场,其中低时延非常重要【8】。虽然UDP本身是不可靠的,但应用程序级别的协议可以恢复丢失的数据包(生产者必须记住它发送的数据包,以便它可以按需重新发送数据包)。 -* 无代理的消息库,如ZeroMQ [9]和nanomsg采取类似的方法,通过TCP或IP多播实现发布/订阅消息传递。 +* 无代理的消息库,如ZeroMQ 【9】和nanomsg采取类似的方法,通过TCP或IP多播实现发布/订阅消息传递。 - StatsD [10]和Brubeck [7]使用不可靠的UDP消息传递来收集网络中所有机器的指标并对其进行监控。 (在StatsD协议中,如果接收到所有消息,则计数器度量标准是正确的;使用UDP将使得度量标准最好近似为[11]。另请参阅“TCP与UDP” + StatsD 【10】和Brubeck 【7】使用不可靠的UDP消息传递来收集网络中所有机器的指标并对其进行监控。 (在StatsD协议中,如果接收到所有消息,则计数器度量标准是正确的;使用UDP将使得度量标准最好近似为【11】。另请参阅“TCP与UDP” -* 如果消费者在网络上公开服务,生产者可以直接发送HTTP或RPC请求(请参阅第131页的“通过服务进行数据流:REST和RPC”)以将消息推送给使用者。这就是webhooks背后的想法[12],一种服务的回调URL被注册到另一个服务中,并且每当事件发生时都会向该URL发出请求。 +* 如果消费者在网络上公开服务,生产者可以直接发送HTTP或RPC请求(请参阅第131页的“通过服务进行数据流:REST和RPC”)以将消息推送给使用者。这就是webhooks背后的想法【12】,一种服务的回调URL被注册到另一个服务中,并且每当事件发生时都会向该URL发出请求。 尽管这些直接消息传递系统在设计它们的情况下运行良好,但是它们通常要求应用程序代码知道消息丢失的可能性。他们可以容忍的错误是相当有限的:即使协议检测并重新传输在网络中丢失的数据包,他们通常假设生产者和消费者不断在线。 @@ -98,11 +98,11 @@ 有些消息代理甚至可以使用XA或JTA参与两阶段提交协议(请参阅第367页的“实践中的分布式事务”)。这个功能与数据库在本质上非常相似,尽管消息代理和数据库之间仍存在重要的实际差异: * 数据库通常保留数据,直到被明确删除,而大多数消息代理在消息成功传递给消费者时自动删除消息。这样的消息代理不适合长期的数据存储。 -* 由于他们很快删除了邮件,大多数邮件经纪人都认为他们的工作集合相当小,即队列很短。如果代理需要缓冲很多消息,因为消费者速度较慢(如果消息不再适合内存,则可能会将消息泄漏到磁盘),每个消息需要更长的时间处理,整体吞吐量可能会降低[6]。 +* 由于他们很快删除了邮件,大多数邮件经纪人都认为他们的工作集合相当小,即队列很短。如果代理需要缓冲很多消息,因为消费者速度较慢(如果消息不再适合内存,则可能会将消息泄漏到磁盘),每个消息需要更长的时间处理,整体吞吐量可能会降低【6】。 * 数据库通常支持二级索引和各种搜索数据的方式,而消息代理通常支持某种方式订阅匹配某种模式的主题子集。机制是不同的,但是这两种方式都是客户选择想要了解的数据部分的根本途径。 * 查询数据库时,结果通常基于数据的时间点快照;如果另一个客户端随后向数据库写入更改查询结果的内容,则第一个客户端不会发现其先前的结果现在已过期(除非它重复查询或轮询更改)。相比之下,消息代理不支持任意查询,但是当数据发生变化时(即新消息可用时),它们会通知客户端。 -这是消息代理的传统观点,它被封装在JMS [14]和AMQP [15]等标准中,并以RabbitMQ,ActiveMQ,HornetQ,Qpid,TIBCO企业消息服务,IBM MQ,Azure服务总线和Google Cloud Pub/Sub [16]。 +这是消息代理的传统观点,它被封装在JMS 【14】和AMQP 【15】等标准中,并以RabbitMQ,ActiveMQ,HornetQ,Qpid,TIBCO企业消息服务,IBM MQ,Azure服务总线和Google Cloud Pub/Sub 【16】。 #### 多个消费者 @@ -161,7 +161,7 @@ **图11-3 生产者通过将消息附加到主题分区文件来发送消息,消费者依次读取这些文件** -Apache Kafka [17,18],Amazon Kinesis Streams [19]和Twitter的DistributedLog [20,21]都是基于日志的消息代理。 Google Cloud Pub / Sub在体系结构上相似,但是暴露了JMS风格的API而不是日志抽象[16]。尽管这些消息代理将所有消息写入磁盘,但通过在多台机器上进行分区,每秒能够实现数百万条消息的吞吐量,并通过复制消息来实现容错性[22,23]。 +Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的DistributedLog 【20,21】都是基于日志的消息代理。 Google Cloud Pub / Sub在体系结构上相似,但是暴露了JMS风格的API而不是日志抽象【16】。尽管这些消息代理将所有消息写入磁盘,但通过在多台机器上进行分区,每秒能够实现数百万条消息的吞吐量,并通过复制消息来实现容错性【22,23】。 #### 日志与传统消息相比 @@ -192,7 +192,7 @@ Apache Kafka [17,18],Amazon Kinesis Streams [19]和Twitter的DistributedLog [2 让我们来做一个后台计算。在撰写本文时,典型的大型硬盘驱动器容量为6TB,顺序写入吞吐量为150MB / s。如果您以最快的速度写邮件,则需要大约11个小时才能填满驱动器。因此,磁盘可以缓存11个小时的消息,之后它将开始覆盖旧的消息。即使您使用多个硬盘驱动器和机器,这个比率也是一样的。在实践中,部署很少使用磁盘的完整写入带宽,所以日志通常可以保存几天甚至几周的缓冲区。 -不管你保留多长时间的消息,一个日志的吞吐量或多或少保持不变,因为无论如何每个消息都被写入磁盘[18]。这种行为与将邮件默认保存在内存中的消息传递系统形成鲜明对比,如果队列变得太大,只将其写入磁盘:当这些系统开始写入磁盘时,这些系统速度很快,并且变慢得多,所以吞吐量取决于保留的历史数量。 +不管你保留多长时间的消息,一个日志的吞吐量或多或少保持不变,因为无论如何每个消息都被写入磁盘【18】。这种行为与将邮件默认保存在内存中的消息传递系统形成鲜明对比,如果队列变得太大,只将其写入磁盘:当这些系统开始写入磁盘时,这些系统速度很快,并且变慢得多,所以吞吐量取决于保留的历史数量。 #### 当消费者跟不上生产者时 @@ -210,7 +210,7 @@ Apache Kafka [17,18],Amazon Kinesis Streams [19]和Twitter的DistributedLog [2 处理的唯一副作用,除了消费者的任何产出之外,消费者补偿正在向前发展。但是偏移量是在消费者的控制之下的,所以如果需要的话可以很容易地被操纵:例如,你可以用昨天的偏移量开始一个消费者的副本,并将输出写到不同的位置,以便重新处理最后一天值得的消息。您可以重复这个任意次数,改变处理代码。 -这方面使得基于日志的消息传递更像上一章的批处理过程,其中派生数据通过可重复的转换过程与输入数据明确分离。它允许更多的实验,更容易从错误和错误中恢复,使其成为在组织内集成数据流的好工具[24]。 +这方面使得基于日志的消息传递更像上一章的批处理过程,其中派生数据通过可重复的转换过程与输入数据明确分离。它允许更多的实验,更容易从错误和错误中恢复,使其成为在组织内集成数据流的好工具【24】。 @@ -269,7 +269,7 @@ Apache Kafka [17,18],Amazon Kinesis Streams [19]和Twitter的DistributedLog [2 数据库触发器可用于通过注册触发器来实现更改数据捕获(请参阅第152页的“基于触发器的复制”),这些触发器可观察数据表的所有更改,并将相应的条目添加到更改日志表中。但是,他们往往是脆弱的,并有显着的性能开销。解析复制日志可以是一个更强大的方法,但它也带来了挑战,例如处理模式更改。 -LinkedIn的Databus [25],Facebook的Wormhole [26]和Yahoo!的Sherpa [27]大规模地使用这个想法。 Bottled Water使用解码预写日志的API来实现PostgreSQL的CDC [28],Maxwell和Debezium通过解析binlog为MySQL做类似的事情[29,30,31],Mongoriver读取MongoDB oplog [32,33] ,而GoldenGate为Oracle提供类似的功能[34,35]。 +LinkedIn的Databus 【25】,Facebook的Wormhole 【26】和Yahoo!的Sherpa 【27】大规模地使用这个想法。 Bottled Water使用解码预写日志的API来实现PostgreSQL的CDC 【28】,Maxwell和Debezium通过解析binlog为MySQL做类似的事情【29,30,31】,Mongoriver读取MongoDB oplog 【32,33】 ,而GoldenGate为Oracle提供类似的功能【34,35】。 像消息代理一样,更改数据捕获通常是异步的:记录数据库系统不会等待更改应用到消费者,然后再进行更改。这种设计具有的操作优势是添加缓慢的使用者不会影响记录系统太多,但是它具有所有复制滞后问题的缺点(请参见第161页中的“复制滞后问题”)。 @@ -297,11 +297,11 @@ Apache Kafka支持此日志压缩功能。正如我们将在本章后面看到 #### API支持更改流 -越来越多的数据库开始支持变更流作为一流的接口,而不是典型的改造和逆向工程CDC的努力。例如,RethinkDB允许查询在查询更改结果[36],Firebase [37]和CouchDB [38]基于同样可用于应用程序的更改提要进行数据同步时订阅通知,而Meteor使用MongoDB oplog订阅数据更改并更新用户界面[39]。 +越来越多的数据库开始支持变更流作为一流的接口,而不是典型的改造和逆向工程CDC的努力。例如,RethinkDB允许查询在查询更改结果【36】,Firebase 【37】和CouchDB 【38】基于同样可用于应用程序的更改提要进行数据同步时订阅通知,而Meteor使用MongoDB oplog订阅数据更改并更新用户界面【39】。 -VoltDB允许事务以流的形式连续地从数据库中导出数据[40]。数据库将关系数据模型中的输出流表示为一个表,事务可以在其中插入元组,但不能被查询。然后这个流由提交事务已经写入这个特殊表的元组日志组成,它们按照提交的顺序。外部使用者可以异步使用此日志并使用它来更新派生的数据系统。 +VoltDB允许事务以流的形式连续地从数据库中导出数据【40】。数据库将关系数据模型中的输出流表示为一个表,事务可以在其中插入元组,但不能被查询。然后这个流由提交事务已经写入这个特殊表的元组日志组成,它们按照提交的顺序。外部使用者可以异步使用此日志并使用它来更新派生的数据系统。 -Kafka Connect [41]致力于将广泛的数据库系统的变更数据捕获工具与Kafka集成。一旦更改事件发生在Kafka中,它就可以用来更新派生的数据系统,比如搜索索引,也可以用于本章稍后讨论的流处理系统。 +Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获工具与Kafka集成。一旦更改事件发生在Kafka中,它就可以用来更新派生的数据系统,比如搜索索引,也可以用于本章稍后讨论的流处理系统。 ### 事件源 @@ -316,9 +316,9 @@ Kafka Connect [41]致力于将广泛的数据库系统的变更数据捕获工 例如,存储“学生取消课程注册”事件清楚地表达了单一行为的中性意图,而副作用“从注册表中删除了一个条目,并且一个取消原因被添加到学生反馈表“嵌入了很多有关方式的假设数据稍后将被使用。如果引入新的应用程序功能,例如“将地点提供给等待列表中的下一个人” - 事件采购方法允许将新的副作用轻松地链接到现有事件上。 -事件采购类似于编年史数据模型[45],事件日志和事实表之间也有相似之处,您可以在星型模式中找到它(请参阅第93页上的“星星和雪花:分析模式”) 。 +事件采购类似于编年史数据模型【45】,事件日志和事实表之间也有相似之处,您可以在星型模式中找到它(请参阅第93页上的“星星和雪花:分析模式”) 。 -专门的数据库如Event Store [46]已经被开发来支持使用事件采购的应用程序,但总的来说,这个方法是独立于任何特定的工具的。传统的数据库或基于日志的消息代理也可以用来构建这种风格的应用程序。 +专门的数据库如Event Store 【46】已经被开发来支持使用事件采购的应用程序,但总的来说,这个方法是独立于任何特定的工具的。传统的数据库或基于日志的消息代理也可以用来构建这种风格的应用程序。 #### 从事件日志中导出当前状态 @@ -335,7 +335,7 @@ Kafka Connect [41]致力于将广泛的数据库系统的变更数据捕获工 #### 命令和事件 -事件采购哲学是仔细区分事件和命令[48]。当来自用户的请求首先到达时,它最初是一个命令:在这一点上它可能仍然失败,例如因为违反了一些完整性条件。应用程序必须首先验证它是否可以执行该命令。如果验证成功并且命令被接受,则它变成一个持久且不可变的事件。 +事件采购哲学是仔细区分事件和命令【48】。当来自用户的请求首先到达时,它最初是一个命令:在这一点上它可能仍然失败,例如因为违反了一些完整性条件。应用程序必须首先验证它是否可以执行该命令。如果验证成功并且命令被接受,则它变成一个持久且不可变的事件。 例如,如果用户试图注册特定用户名,或在飞机上或剧院中预定座位,则应用程序需要检查用户名或座位是否已被占用。 (我们先前在第364页的“容错概念”中讨论过这个例子。)当检查成功时,应用程序可以生成一个事件来指示特定的用户名是由特定的用户ID注册的,座位已经预留给特定的顾客。 @@ -361,7 +361,7 @@ Kafka Connect [41]致力于将广泛的数据库系统的变更数据捕获工 **图11-6 当前应用程序状态和事件流之间的关系** -如果你持久地存储更新日志,那么这只是使状态重现的效果。如果你认为事件的日志是你的记录系统,并且从它派生出任何可变状态,那么就更容易推断通过系统的数据流。正如帕特·赫兰(Pat Helland)所说的[52]: +如果你持久地存储更新日志,那么这只是使状态重现的效果。如果你认为事件的日志是你的记录系统,并且从它派生出任何可变状态,那么就更容易推断通过系统的数据流。正如帕特·赫兰(Pat Helland)所说的【52】: > 事务日志记录对数据库所做的所有更改。高速追加是更改日志的唯一方法。从这个角度来看,数据库的内容会保存日志中最新记录值的缓存。事实是日志。数据库是日志子集的缓存。该缓存子集恰好是来自日志的每个记录和索引值的最新值。 @@ -369,23 +369,23 @@ Kafka Connect [41]致力于将广泛的数据库系统的变更数据捕获工 #### 不可变事件的优点 -数据库中的不变性是一个古老的想法。例如,会计师在数个世纪以来一直使用不变性财务簿记。当一笔交易发生时,它被记录在一个仅追加分类帐中,这本质上是描述货币,商品或服务已经转手的事件日志。账目,如损益或资产负债表,是从分类账中的交易中加起来得来的[53]。 +数据库中的不变性是一个古老的想法。例如,会计师在数个世纪以来一直使用不变性财务簿记。当一笔交易发生时,它被记录在一个仅追加分类帐中,这本质上是描述货币,商品或服务已经转手的事件日志。账目,如损益或资产负债表,是从分类账中的交易中加起来得来的【53】。 -如果发生错误,会计师不会删除或更改分类帐中的错误交易 - 而是增加另一笔交易,以补偿错误,例如退还不正确的费用。不正确的交易将永远保留在分类帐中,因为审计原因可能很重要。如果从不正确的分类账导出的错误数字已经公布,那么下一个会计期间的数字就包括一个更正。这个过程在会计中是完全正常的[54]。 +如果发生错误,会计师不会删除或更改分类帐中的错误交易 - 而是增加另一笔交易,以补偿错误,例如退还不正确的费用。不正确的交易将永远保留在分类帐中,因为审计原因可能很重要。如果从不正确的分类账导出的错误数字已经公布,那么下一个会计期间的数字就包括一个更正。这个过程在会计中是完全正常的【54】。 尽管这种可审计性在金融系统中尤其重要,但对于不受这种严格管制的许多其他系统也是有益的。如“批处理输出的哲学”(第439页)中所述,如果您意外地部署了将错误数据写入数据库的错误代码,那么如果代码能够破坏性地覆盖数据,恢复将更加困难。通过不可变事件的追加日志,诊断发生的事情和从问题中恢复起来要容易得多。 -不可变的事件也捕获比当前状态更多的信息。例如,在购物网站上,顾客可以将物品添加到他们的购物车,然后再将其移除。虽然第二个事件从订单履行角度取消了第一个事件,但为了分析目的,客户正在考虑某个特定项目,但是之后决定采取反对措施。也许他们会选择在未来购买,或者他们找到替代品。这个信息被记录在一个事件日志中,但是当它们从购物车中被删除时,这个信息会丢失在删除项目的数据库中[42]。 +不可变的事件也捕获比当前状态更多的信息。例如,在购物网站上,顾客可以将物品添加到他们的购物车,然后再将其移除。虽然第二个事件从订单履行角度取消了第一个事件,但为了分析目的,客户正在考虑某个特定项目,但是之后决定采取反对措施。也许他们会选择在未来购买,或者他们找到替代品。这个信息被记录在一个事件日志中,但是当它们从购物车中被删除时,这个信息会丢失在删除项目的数据库中【42】。 #### 从同一事件日志中获取多个视图 -而且,通过从不变事件日志中分离可变状态,可以从事件的相同日志中派生出几个不同的面向读取的表示。这就像一个流的多个消费者一样工作(图11-5):例如,分析数据库Druid使用这种方法从Kafka直接获取[55],Pista chio是一个分布式的键值存储,使用Kafka作为提交日志[56],Kafka Connect接收器可以将来自Kafka的数据导出到各种不同的数据库和索引[41]。对于许多其他存储和索引系统(如搜索服务器)来说,类似地从分布式日志中获取输入也是有意义的(请参阅第455页上的“保持系统同步”)。 +而且,通过从不变事件日志中分离可变状态,可以从事件的相同日志中派生出几个不同的面向读取的表示。这就像一个流的多个消费者一样工作(图11-5):例如,分析数据库Druid使用这种方法从Kafka直接获取【55】,Pista chio是一个分布式的键值存储,使用Kafka作为提交日志【56】,Kafka Connect接收器可以将来自Kafka的数据导出到各种不同的数据库和索引【41】。对于许多其他存储和索引系统(如搜索服务器)来说,类似地从分布式日志中获取输入也是有意义的(请参阅第455页上的“保持系统同步”)。 从事件日志到数据库有一个明确的转换步骤,可以更容易地随时间推移您的应用程序:如果您想要引入一个以新的方式呈现现有数据的新功能,您可以使用事件日志来构建一个单独的新功能的读取优化视图,并与现有的一起运行 -系统而不必修改它们。并行运行旧系统和新系统通常比在现有系统中执行复杂的模式迁移更容易。一旦旧的系统不再需要,你可以简单地关闭它并回收它的资源[47,57]。 +系统而不必修改它们。并行运行旧系统和新系统通常比在现有系统中执行复杂的模式迁移更容易。一旦旧的系统不再需要,你可以简单地关闭它并回收它的资源【47,57】。 -如果您不必担心如何查询和访问数据,那么存储数据通常是非常简单的。模式设计,索引和存储引擎的许多复杂性都是希望支持某些查询和访问模式的结果(参见第3章)。出于这个原因,通过将数据写入的形式与读取形式分开,并允许几个不同的读取视图,可以获得很大的灵活性。这个想法有时被称为命令查询责任分离(CQRS)[42,58,59]。 +如果您不必担心如何查询和访问数据,那么存储数据通常是非常简单的。模式设计,索引和存储引擎的许多复杂性都是希望支持某些查询和访问模式的结果(参见第3章)。出于这个原因,通过将数据写入的形式与读取形式分开,并允许几个不同的读取视图,可以获得很大的灵活性。这个想法有时被称为命令查询责任分离(CQRS)【42,58,59】。 数据库和模式设计的传统方法是基于数据必须以与查询相同的形式写入的谬误。有关正常化和非规范化的争论(请参阅第31页上的“多对一和多对多关系”),如果可以将数据从写入优化的事件日志转换为读取优化的应用程序状态,则变得基本无关紧要:在读取优化的视图中对数据进行非规范化是完全合理的,因为翻译过程为您提供了一种机制,使其与事件日志保持一致。 @@ -399,19 +399,19 @@ Kafka Connect [41]致力于将广泛的数据库系统的变更数据捕获工 另一方面,从事件日志导出当前状态也简化了并发控制的某些方面。对多个对象事务的需求(请参阅第228页上的“单对象和多对象操作”)源于单个用户操作,需要在多个不同的位置更改数据。通过事件采购,您可以设计一个事件,以便对用户操作进行独立的描述。用户操作只需要在一个地方进行一次写操作,即将事件附加到日志中,这很容易使原子化。 -如果事件日志和应用程序状态以相同的方式分区(例如,为分区3中的客户处理事件只需要更新应用程序状态的分区3),则直接的单线程日志消费者不需要并发控制(write-by)构造,它一次只处理一个事件(另请参阅第252页的“实际的串行执行”)。该日志通过在分区中定义事件的串行顺序来消除并发性的不确定性[24]。如果一个事件触及多个状态分区,那么需要做更多的工作,我们将在第12章讨论。 +如果事件日志和应用程序状态以相同的方式分区(例如,为分区3中的客户处理事件只需要更新应用程序状态的分区3),则直接的单线程日志消费者不需要并发控制(write-by)构造,它一次只处理一个事件(另请参阅第252页的“实际的串行执行”)。该日志通过在分区中定义事件的串行顺序来消除并发性的不确定性【24】。如果一个事件触及多个状态分区,那么需要做更多的工作,我们将在第12章讨论。 #### 不变性的限制 许多不使用事件源模型的系统依赖于不可变性:各种数据库在内部使用不可变的数据结构或多版本数据来支持时间点快照(请参见“索引和快照隔离” )。 Git,Mercurial和Fossil等版本控制系统也依靠不可变的数据来保存文件的版本历史记录。 -永远保持所有变化的不变的历史在多大程度上是可行的?答案取决于数据集中的流失量。一些工作负载主要是添加数据,很少更新或删除;他们很容易使不变。其他工作负载在较小的数据集上有较高的更新和删除率;在这些情况下,不可改变的历史可能变得过于庞大,碎片化可能成为一个问题,压缩和垃圾收集的表现对于操作的鲁棒性变得至关重要[60,61]。 +永远保持所有变化的不变的历史在多大程度上是可行的?答案取决于数据集中的流失量。一些工作负载主要是添加数据,很少更新或删除;他们很容易使不变。其他工作负载在较小的数据集上有较高的更新和删除率;在这些情况下,不可改变的历史可能变得过于庞大,碎片化可能成为一个问题,压缩和垃圾收集的表现对于操作的鲁棒性变得至关重要【60,61】。 除了性能方面的原因外,也可能出于管理方面的原因需要删除数据的情况,尽管这些数据都是不可变的。例如,隐私条例可能要求在关闭帐户后删除用户的个人信息,数据保护立法可能要求删除错误的信息,或者可能需要包含敏感信息的意外泄露。 -在这种情况下,仅仅在日志中添加另一个事件来指示先前的数据应该被视为删除是不够的 - 您实际上是想重写历史并假装数据从未写在第一位。例如,Datomic调用这个特性excision [62],而Fossil版本控制系统有一个类似的概念叫做shunning [63]。 +在这种情况下,仅仅在日志中添加另一个事件来指示先前的数据应该被视为删除是不够的 - 您实际上是想重写历史并假装数据从未写在第一位。例如,Datomic调用这个特性excision 【62】,而Fossil版本控制系统有一个类似的概念叫做shunning 【63】。 -真正的删除数据是非常困难的[64],因为拷贝可以存在于很多地方:例如,存储引擎,文件系统和SSD通常写入一个新的位置,而不是覆盖到位[52],而备份通常是故意不可改变的防止意外删除或腐败。删除更多的是“使检索数据更难”,而不是“使检索数据不可能”。无论如何,有时您必须尝试,正如我们在“立法和自律”中所看到的第542页。 +真正的删除数据是非常困难的【64】,因为拷贝可以存在于很多地方:例如,存储引擎,文件系统和SSD通常写入一个新的位置,而不是覆盖到位【52】,而备份通常是故意不可改变的防止意外删除或腐败。删除更多的是“使检索数据更难”,而不是“使检索数据不可能”。无论如何,有时您必须尝试,正如我们在“立法和自律”中所看到的第542页。 @@ -444,13 +444,13 @@ Kafka Connect [41]致力于将广泛的数据库系统的变更数据捕获工 #### 复杂的事件处理 -复杂事件处理(CEP)是20世纪90年代为分析事件流而开发的一种方法,尤其适用于需要搜索某些事件模式的应用程序[65,66]。与正则表达式允许您在字符串中搜索特定字符模式的方式类似,CEP允许您指定规则以在流中搜索某些事件模式。 +复杂事件处理(CEP)是20世纪90年代为分析事件流而开发的一种方法,尤其适用于需要搜索某些事件模式的应用程序【65,66】。与正则表达式允许您在字符串中搜索特定字符模式的方式类似,CEP允许您指定规则以在流中搜索某些事件模式。 -CEP系统通常使用高级声明式查询语言(如SQL或图形用户界面)来描述应该检测到的事件模式。这些查询被提交给一个处理引擎,该引擎使用输入流并在内部维护一个执行所需匹配的状态机。当发现匹配时,引擎发出一个复杂的事件(因此名字)与事件模式的细节[67]。 +CEP系统通常使用高级声明式查询语言(如SQL或图形用户界面)来描述应该检测到的事件模式。这些查询被提交给一个处理引擎,该引擎使用输入流并在内部维护一个执行所需匹配的状态机。当发现匹配时,引擎发出一个复杂的事件(因此名字)与事件模式的细节【67】。 -在这些系统中,查询和数据之间的关系与普通数据库相比是颠倒的。通常情况下,数据库会持久存储数据,并将查询视为暂时的:当查询进入时,数据库搜索与查询匹配的数据,然后在查询完成时忘记查询。 CEP引擎反转了这些角色:查询是长期存储的,来自输入流的事件不断流过它们,以搜索匹配事件模式的查询[68]。 +在这些系统中,查询和数据之间的关系与普通数据库相比是颠倒的。通常情况下,数据库会持久存储数据,并将查询视为暂时的:当查询进入时,数据库搜索与查询匹配的数据,然后在查询完成时忘记查询。 CEP引擎反转了这些角色:查询是长期存储的,来自输入流的事件不断流过它们,以搜索匹配事件模式的查询【68】。 -CEP的实现包括Esper [69],IBM InfoSphere Streams [70],Apama,TIBCO StreamBase和SQLstream。像Samza这样的分布式流处理器也获得了对流声明式查询的SQL支持[71]。 +CEP的实现包括Esper 【69】,IBM InfoSphere Streams 【70】,Apama,TIBCO StreamBase和SQLstream。像Samza这样的分布式流处理器也获得了对流声明式查询的SQL支持【71】。 #### 流分析 @@ -464,23 +464,23 @@ CEP的实现包括Esper [69],IBM InfoSphere Streams [70],Apama,TIBCO Strea -流分析系统有时使用概率算法,例如Bloom filter(我们在第79页的“性能优化”中遇到过),设置成员资格,HyperLogLog [72]基数估计以及各种百分比估计算法(请参阅“Percentiles in Practice “第16页)。概率算法产生近似的结果,但是具有在流处理器中比精确算法需要少得多的存储器的优点。近似算法的使用有时会使人们相信流处理系统总是有损和不精确的,但这是错误的:流处理没有任何内在的近似,而概率算法只是一个优化[73]。 -许多开源分布式流处理框架的设计都是以分析为基础的:例如Apache Storm,Spark Streaming,Flink,Concord,Samza和Kafka Streams [74]。托管服务包括Google Cloud Dataflow和Azure Stream Analytics。 +流分析系统有时使用概率算法,例如Bloom filter(我们在第79页的“性能优化”中遇到过),设置成员资格,HyperLogLog 【72】基数估计以及各种百分比估计算法(请参阅“Percentiles in Practice “第16页)。概率算法产生近似的结果,但是具有在流处理器中比精确算法需要少得多的存储器的优点。近似算法的使用有时会使人们相信流处理系统总是有损和不精确的,但这是错误的:流处理没有任何内在的近似,而概率算法只是一个优化【73】。 +许多开源分布式流处理框架的设计都是以分析为基础的:例如Apache Storm,Spark Streaming,Flink,Concord,Samza和Kafka Streams 【74】。托管服务包括Google Cloud Dataflow和Azure Stream Analytics。 #### 保持物化视图 -我们在第451页的“数据库和数据流”中看到,可以使用数据库更改流来保持派生数据系统(如缓存,搜索索引和数据仓库)与源数据库保持最新。我们可以将这些示例视为维护实体化视图的具体情况(请参阅“聚合:数据多维数据集和实例化视图”(第101页)):导出某个数据集的替代视图,以便可以高效地查询它,并在底层数据更改[50]。 +我们在第451页的“数据库和数据流”中看到,可以使用数据库更改流来保持派生数据系统(如缓存,搜索索引和数据仓库)与源数据库保持最新。我们可以将这些示例视为维护实体化视图的具体情况(请参阅“聚合:数据多维数据集和实例化视图”(第101页)):导出某个数据集的替代视图,以便可以高效地查询它,并在底层数据更改【50】。 同样,在事件采购中,应用程序状态通过应用事件日志来维护;这里的应用状态也是一种物化视图。与流分析场景不同,在某个时间窗口内仅考虑事件通常是不够的:构建物化视图可能需要在任意时间段内的所有事件,除了可能由日志压缩丢弃的任何过时事件(请参阅“日志压缩“)。实际上,您需要一个可以一直延伸到一开始的窗口。 -原则上,任何流处理器都可以用于物化视图维护,尽管永久维护事件的需要与一些主要在有限持续时间的窗口上运行的面向分析的框架的假设背道而驰。 Samza和Kafka Streams支持这种用法,建立在Kafka对夯实的支持上[75]。 +原则上,任何流处理器都可以用于物化视图维护,尽管永久维护事件的需要与一些主要在有限持续时间的窗口上运行的面向分析的框架的假设背道而驰。 Samza和Kafka Streams支持这种用法,建立在Kafka对夯实的支持上【75】。 #### 在流上搜索 除了允许搜索由多个事件组成的模式的CEP外,还有时需要基于复杂的标准(例如全文搜索查询)来搜索单个事件。 -例如,媒体监测服务可以订阅新闻文章和媒体广播,并搜索任何关于公司,产品或感兴趣的话题的新闻。这是通过预先制定一个搜索查询来完成的,然后不断地将新闻项目流与这个查询进行匹配。在一些网站上也有类似的功能:例如,房地产网站的用户在市场上出现符合其搜索条件的新房产时,可以要求通知。 Elasticsearch [76]的渗滤器功能是实现这种流式搜索的一种选择。 +例如,媒体监测服务可以订阅新闻文章和媒体广播,并搜索任何关于公司,产品或感兴趣的话题的新闻。这是通过预先制定一个搜索查询来完成的,然后不断地将新闻项目流与这个查询进行匹配。在一些网站上也有类似的功能:例如,房地产网站的用户在市场上出现符合其搜索条件的新房产时,可以要求通知。 Elasticsearch 【76】的渗滤器功能是实现这种流式搜索的一种选择。 -传统的搜索引擎首先索引文件,然后在索引上运行查询。相比之下,搜索一个数据流将会处理它的头部:查询被存储,文档通过查询运行,就像CEP一样。在最简单的情况下,您可以针对每个查询来测试每个文档,但是如果您有大量查询,这可能会变慢。为了优化过程,可以对查询和文档进行索引,从而缩小可能匹配的查询集合[77]。 +传统的搜索引擎首先索引文件,然后在索引上运行查询。相比之下,搜索一个数据流将会处理它的头部:查询被存储,文档通过查询运行,就像CEP一样。在最简单的情况下,您可以针对每个查询来测试每个文档,但是如果您有大量查询,这可能会变慢。为了优化过程,可以对查询和文档进行索引,从而缩小可能匹配的查询集合【77】。 #### 消息传递和RPC @@ -491,7 +491,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 * 参与者之间的交流往往是短暂的,而且是一对一的,而事件日志则是持久的,多用户的。 * 参与者可以以任意方式进行通信(包括循环请求/响应模式),但流处理器通常设置在非循环流水线中,其中每个流是一个特定作业的输出,并且从一组明确定义的输入流派生。 -也就是说,RPC类系统和流处理之间有一些交叉区域。例如,Apache Storm有一个称为分布式RPC的功能,它允许将用户查询分散到一系列也处理事件流的节点上;这些查询然后与来自输入流的事件交织,结果可以被汇总并发回给用户[78]。 (另请参阅“多分区数据处理”(第514页)。) +也就是说,RPC类系统和流处理之间有一些交叉区域。例如,Apache Storm有一个称为分布式RPC的功能,它允许将用户查询分散到一系列也处理事件流的节点上;这些查询然后与来自输入流的事件交织,结果可以被汇总并发回给用户【78】。 (另请参阅“多分区数据处理”(第514页)。) 也可以使用actor框架来处理流。但是,很多这样的框架在崩溃的情况下不能保证消息的传递,所以这个过程不是容错的,除非你实现了额外的重试逻辑。 @@ -503,7 +503,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 批处理可以在几分钟内读取一年的历史事件;在大多数情况下,感兴趣的时间表是历史的一年,而不是几分钟的处理。而且,在事件中使用时间戳允许处理确定性的:在相同的输入上再次运行相同的处理过程会得到相同的结果(请参阅“故障容错”在页面429)。 -另一方面,许多流处理框架使用处理机器上的本地系统时钟(处理时间)来确定窗口[79]。这种方法具有简单的优点,事件创建和事件处理之间的延迟可以忽略不计。然而,如果存在任何显着的处理滞后,即处理可能比事件实际发生的时间显着晚,则会中断处理。 +另一方面,许多流处理框架使用处理机器上的本地系统时钟(处理时间)来确定窗口【79】。这种方法具有简单的优点,事件创建和事件处理之间的延迟可以忽略不计。然而,如果存在任何显着的处理滞后,即处理可能比事件实际发生的时间显着晚,则会中断处理。 #### 事件时间与处理时间 @@ -527,12 +527,12 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 例如,假设您将事件分组为一分钟的窗口,以便您可以统计每分钟的请求数。你已经计算了一些事件,这些事件的时间戳是在第37分钟的时间落下的,时间已经推移了。现在大部分的事件都在一小时的第38和第39分钟之内。你什么时候宣布你已经完成了第37分钟的窗口,并输出其计数器值? -在一段时间没有看到任何新的事件之后,您可以超时并宣布一个窗口,但仍然可能发生某些事件被缓存在另一台计算机上,由于网络中断而延迟。您需要能够处理窗口已经声明完成后到达的这样的滞留事件。大体上,你有两个选择[1]: +在一段时间没有看到任何新的事件之后,您可以超时并宣布一个窗口,但仍然可能发生某些事件被缓存在另一台计算机上,由于网络中断而延迟。您需要能够处理窗口已经声明完成后到达的这样的滞留事件。大体上,你有两个选择【1】: 1. 忽略这些零散的事件,因为它们在正常情况下可能只是一小部分事件。您可以将丢弃事件的数量作为度量标准进行跟踪,并在您开始丢弃大量数据时发出警报。 2. 发布一个更正,更新的窗口与包含散兵队员的价值。您可能还需要收回以前的输出。 -在某些情况下,可以使用特殊的消息来指示“从现在开始,不会有比t更早的时间戳的消息”,消费者可以使用它来触发窗口[81]。但是,如果不同机器上的多个生产者正在生成事件,每个事件都有自己的最小时间戳阈值,则消费者需要分别跟踪每个生产者。在这种情况下添加和删除生产者是比较棘手的。 +在某些情况下,可以使用特殊的消息来指示“从现在开始,不会有比t更早的时间戳的消息”,消费者可以使用它来触发窗口【81】。但是,如果不同机器上的多个生产者正在生成事件,每个事件都有自己的最小时间戳阈值,则消费者需要分别跟踪每个生产者。在这种情况下添加和删除生产者是比较棘手的。 #### 你用的是什么时间? @@ -540,7 +540,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 在这种情况下,根据移动设备的本地时钟,事件的时间戳实际上应该是发生用户交互的时间。但是,用户控制的设备上的时钟通常是不可信的,因为它可能会被意外或故意设置为错误的时间(请参见“时钟同步和精度”(第269页))。服务器收到事件的时间(根据服务器的时钟)更可能是准确的,因为服务器在您的控制之下,但在描述用户交互方面意义不大。 -要调整不正确的设备时钟,一种方法是记录三个时间戳[82]: +要调整不正确的设备时钟,一种方法是记录三个时间戳【82】: * 事件发生的时间,根据设备时钟 * 根据设备时钟将事件发送到服务器的时间 @@ -552,7 +552,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 #### 窗口的类型 -一旦你知道如何确定一个事件的时间戳,下一步就是决定如何定义一段时间的窗口。窗口然后可以用于聚合,例如计数事件,或计算窗口内的值的平均值。有几种窗口是常用的[79,83]: +一旦你知道如何确定一个事件的时间戳,下一步就是决定如何定义一段时间的窗口。窗口然后可以用于聚合,例如计数事件,或计算窗口内的值的平均值。有几种窗口是常用的【79,83】: ***Tumbling窗口*** @@ -574,10 +574,10 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 在第10章中,我们讨论了批处理作业如何通过关键连接数据集,以及这种连接如何构成数据管道的重要组成部分。由于流处理将数据管道概括为对无界数据集进行增量处理,因此对流进行连接的需求也完全相同。 -然而,新事件随时可能出现在一个流中,这使得加入流比批处理作业更具挑战性。为了更好地理解情况,我们来区分三种不同类型的连接:流 - 流连接,流表连接和表连接[84]。在下面的章节中,我们将通过例子来说明。 +然而,新事件随时可能出现在一个流中,这使得加入流比批处理作业更具挑战性。为了更好地理解情况,我们来区分三种不同类型的连接:流 - 流连接,流表连接和表连接【84】。在下面的章节中,我们将通过例子来说明。 流 - 流连接(窗口连接) -假设您的网站上有搜索功能,并且想要检测搜索到的网址的近期趋势。每次有人输入搜索查询时,都会记录包含查询和返回结果的事件。每当有人点击其中一个搜索结果时,就会记录另一个记录点击的事件。为了计算搜索结果中每个网址的点击率,您需要将搜索操作和点击操作的事件组合在一起,这些事件通过具有相同的会话ID进行连接。广告系统需要类似的分析[85]。 +假设您的网站上有搜索功能,并且想要检测搜索到的网址的近期趋势。每次有人输入搜索查询时,都会记录包含查询和返回结果的事件。每当有人点击其中一个搜索结果时,就会记录另一个记录点击的事件。为了计算搜索结果中每个网址的点击率,您需要将搜索操作和点击操作的事件组合在一起,这些事件通过具有相同的会话ID进行连接。广告系统需要类似的分析【85】。 如果用户放弃他们的搜索,点击可能永远不会到来,即使它到了,搜索和点击之间的时间可能是高度可变的:在很多情况下,它可能是几秒钟,但可能长达几天或几周(如果用户运行搜索,忘记关于该浏览器选项卡,然后返回到选项卡,稍后再单击一个结果)。由于可变的网络延迟,点击事件甚至可能在搜索事件之前到达。您可以选择合适的加入窗口,例如,如果间隔至多一小时发生一次搜索,您可以选择加入搜索。 @@ -591,7 +591,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 包含用户ID的活动事件流,并且输出是活动事件流,其中用户ID已经用关于用户的简档信息来扩充。这个过程有时被称为使用来自数据库的信息来丰富活动事件。 -要执行此联接,流程过程需要一次查看一个活动事件,在数据库中查找事件的用户标识,并将该概要信息添加到活动事件中。数据库查询可以通过查询远程数据库来实现;但是,正如在“示例:分析用户活动事件”一节中讨论的,此类远程查询可能会很慢并且有可能导致数据库过载[75]。 +要执行此联接,流程过程需要一次查看一个活动事件,在数据库中查找事件的用户标识,并将该概要信息添加到活动事件中。数据库查询可以通过查询远程数据库来实现;但是,正如在“示例:分析用户活动事件”一节中讨论的,此类远程查询可能会很慢并且有可能导致数据库过载【75】。 另一种方法是将数据库副本加载到流处理器中,以便在本地进行查询而无需网络往返。这种技术与我们在408页的“Map-Side连接”中讨论的散列连接非常相似:如果数据库的本地副本足够小,则本地副本可能是内存中的散列表,或者是本地磁盘上的索引。 与批处理作业的区别在于批处理作业使用数据库的时间点快照作为输入,而流处理器是长时间运行的,并且数据库的内容可能随时间而改变,所以流处理器数据库的本地副本需要保持最新。这个问题可以通过更改数据捕获来解决:流处理器可以订阅用户配置文件数据库的更新日志以及活动事件流。在创建或修改配置文件时,流处理器会更新其本地副本。因此,我们获得两个流之间的连接:活动事件和配置文件更新。 流表连接实际上非常类似于流 - 流连接;最大的区别在于对于表changelog流,连接使用一个可以回溯到“开始时间”(概念上是无限的窗口)的窗口,新版本的记录会覆盖较早的版本。对于流输入,连接可能根本没有维护窗口。 @@ -606,7 +606,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 * 当用户u1开始跟随用户u2时,u2最近的tweets被添加到u1的时间线上。 * 当用户u1取消关注用户u2时,u1的推文将从u1的时间线中移除。 -要在流处理器中实现这种缓存维护,需要用于推文(发送和删除)和跟随关系(跟随和取消跟随)的事件流。流过程需要维护一个包含每个用户关注者集合的数据库,以便知道当一个新的tweet到达时需要更新哪些时间轴[86]。 +要在流处理器中实现这种缓存维护,需要用于推文(发送和删除)和跟随关系(跟随和取消跟随)的事件流。流过程需要维护一个包含每个用户关注者集合的数据库,以便知道当一个新的tweet到达时需要更新哪些时间轴【86】。 查看这个流过程的另一种方式是维护一个连接两个表(tweet和follow)的查询的物化视图,如下所示: ```sql @@ -623,29 +623,29 @@ GROUP BY follows.follower_id #### 连接的时间依赖性 -这就产生了一个问题:如果不同的事件发生在相似的时间周围,他们按照何种顺序进行处理?在流表连接示例中,如果用户更新其配置文件,哪些活动事件与旧配置文件(在配置文件更新之前处理)结合,哪些与新配置文件结合(在配置文件更新之后处理)?换句话说:如果状态随着时间的推移而改变,并且你加入了某个状态,那么你使用什么时间点来加入[45]? +这就产生了一个问题:如果不同的事件发生在相似的时间周围,他们按照何种顺序进行处理?在流表连接示例中,如果用户更新其配置文件,哪些活动事件与旧配置文件(在配置文件更新之前处理)结合,哪些与新配置文件结合(在配置文件更新之后处理)?换句话说:如果状态随着时间的推移而改变,并且你加入了某个状态,那么你使用什么时间点来加入【45】? 这种时间依赖性可能发生在许多地方。例如,如果您销售东西,则需要对发票进行适当的税率,这取决于国家或州,产品类型和销售日期(因为税率会随时变化)。将销售额加入税率表时,如果您正在重新处理历史数据,您可能希望加入销售时的税率,这可能与当前的税率不同。 -如果跨流的事件排序是未确定的,那么这个连接变得不确定[87],这意味着你不能在相同的输入上重新运行相同的工作,并且必然会得到相同的结果:输入流上的事件可能交织在当你再次运行这个工作时,采用不同的方式 +如果跨流的事件排序是未确定的,那么这个连接变得不确定【87】,这意味着你不能在相同的输入上重新运行相同的工作,并且必然会得到相同的结果:输入流上的事件可能交织在当你再次运行这个工作时,采用不同的方式 -在数据仓库中,这个问题被称为缓慢变化的维度(SCD),通常通过对特定版本的联合记录使用唯一的标识符来解决:例如,每当税率改变时,新的标识符,并且发票包括销售时的税率标识符[88,89]。这种变化使连接成为确定性的,但是由于表中所有记录的版本都需要保留,导致日志压缩是不可能的。 +在数据仓库中,这个问题被称为缓慢变化的维度(SCD),通常通过对特定版本的联合记录使用唯一的标识符来解决:例如,每当税率改变时,新的标识符,并且发票包括销售时的税率标识符【88,89】。这种变化使连接成为确定性的,但是由于表中所有记录的版本都需要保留,导致日志压缩是不可能的。 ### 容错 在本章的最后一节中,让我们考虑流处理器如何容忍错误。我们在第10章中看到,批处理框架可以很容易地容忍错误:如果MapReduce作业中的任务失败,可以简单地在另一台机器上重新启动,并且丢弃失败任务的输出。这种透明的重试是可能的,因为输入文件是不可变的,每个任务都将其输出写入到HDFS上的单独文件,并且输出仅在任务成功完成时可见。 -特别是,批处理容错方法可确保批处理作业的输出与没有出错的情况相同,即使事实上某些任务失败了。看起来好像每个输入记录都被处理了一次 - 没有记录被跳过,而且没有处理两次。尽管重新启动任务意味着实际上可能会多次处理记录,但输出中的可见效果好像只处理过一次。这个原则被称为一次语义学,虽然有效 - 一次将是一个更具描述性的术语[90]。 +特别是,批处理容错方法可确保批处理作业的输出与没有出错的情况相同,即使事实上某些任务失败了。看起来好像每个输入记录都被处理了一次 - 没有记录被跳过,而且没有处理两次。尽管重新启动任务意味着实际上可能会多次处理记录,但输出中的可见效果好像只处理过一次。这个原则被称为一次语义学,虽然有效 - 一次将是一个更具描述性的术语【90】。 在流处理过程中也出现了同样的容错问题,但是处理起来不那么直观:等到某个任务完成之后才使其输出可见,因为流是无限的,因此您永远无法完成处理。 #### 小批量和检查点 -一个解决方案是将流分解成小块,并像小型批处理一样处理每个块。这种方法被称为microbatching,它被用于Spark Streaming [91]。批处理大小通常约为1秒,这是性能折中的结果:较小的批次会导致更大的调度和协调开销,而较大的批次意味着流处理器的结果变得可见之前的较长延迟。 +一个解决方案是将流分解成小块,并像小型批处理一样处理每个块。这种方法被称为microbatching,它被用于Spark Streaming 【91】。批处理大小通常约为1秒,这是性能折中的结果:较小的批次会导致更大的调度和协调开销,而较大的批次意味着流处理器的结果变得可见之前的较长延迟。 微缩也隐含地提供了与批量大小相等的翻滚窗口(通过处理时间而不是事件时间戳)。任何需要更大窗口的作业都需要明确地将状态从一个微阵列转移到下一个微阵列。 -Apache Flink中使用的一种变体方法是定期生成状态滚动检查点并将其写入持久存储器[92,93]。如果流操作符崩溃,它可以从最近的检查点重新启动,并放弃在最后一个检查点和崩溃之间生成的任何输出。检查点由消息流中的条形码触发,类似于微型图形之间的边界,但不强制特定的窗口大小。 +Apache Flink中使用的一种变体方法是定期生成状态滚动检查点并将其写入持久存储器【92,93】。如果流操作符崩溃,它可以从最近的检查点重新启动,并放弃在最后一个检查点和崩溃之间生成的任何输出。检查点由消息流中的条形码触发,类似于微型图形之间的边界,但不强制特定的窗口大小。 在流处理框架的范围内,微观网格化和检查点方法提供了与批处理一样的一次语义。但是,只要输出离开流处理器(例如,通过写入数据库,向外部消息代理发送消息或发送电子邮件),框架将不再能够放弃失败批处理的输出。在这种情况下,重新启动失败的任务会导致外部副作用发生两次,单独使用微配量或检查点不足以防止此问题。 @@ -655,17 +655,17 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 这些事情要么都是原子地发生,要么都不发生,但是不应该彼此不同步。如果这种方法听起来很熟悉,那是因为我们在分布式事务和两阶段提交的情况下,在第360页的“准确一次的消息处理”中讨论了它。 -在第9章中,我们讨论了分布式交易(如XA)的传统实现中的问题。然而,在更受限制的环境中,可以有效地实现这样的原子提交设施。 Google云数据流[81,92]和VoltDB [94]中使用了这种方法,并计划在Apache Kafka [95,96]中添加类似的功能。与XA不同,这些实现不会尝试跨异构技术提供事务,而是通过在流处理框架中管理状态更改和消息传递来保持内部事务。事务协议的开销可以通过在单个事务中处理几个输入消息来分摊。 +在第9章中,我们讨论了分布式交易(如XA)的传统实现中的问题。然而,在更受限制的环境中,可以有效地实现这样的原子提交设施。 Google云数据流【81,92】和VoltDB 【94】中使用了这种方法,并计划在Apache Kafka 【95,96】中添加类似的功能。与XA不同,这些实现不会尝试跨异构技术提供事务,而是通过在流处理框架中管理状态更改和消息传递来保持内部事务。事务协议的开销可以通过在单个事务中处理几个输入消息来分摊。 #### 幂等 -我们的目标是放弃任何失败的任务的部分输出,以便他们可以安全地重试,而不会两次生效。分布式事务是实现这一目标的一种方式,但另一种方式是依赖幂等性[97]。 +我们的目标是放弃任何失败的任务的部分输出,以便他们可以安全地重试,而不会两次生效。分布式事务是实现这一目标的一种方式,但另一种方式是依赖幂等性【97】。 幂等操作是可以多次执行的操作,并且与只执行一次操作具有相同的效果。例如,将键值存储中的某个键设置为某个固定值是幂等的(再次写入该值会覆盖具有相同值的值),而递增计数器不是幂等的(再次执行递增意味着该值递增两次)。 即使一个操作不是天生的幂等,它往往可以与一些额外的元数据幂等。例如,在使用来自卡夫卡的消息时,每条消息都有一个持续的,单调递增的偏移量。将值写入外部数据库时,可以将触发上次写入的消息的偏移量与值包含在一起。因此,您可以判断是否已应用更新,并避免再次执行相同的更新。 -风暴三叉戟的状态处理基于类似的想法[78]。依赖幂等性意味着一些假设:重启一个失败的任务必须以相同的顺序重播相同的消息(一个基于日志的消息代理这样做),处理必须是确定性的,其他节点不能同时更新相同的值[ 98,99]。 +风暴三叉戟的状态处理基于类似的想法【78】。依赖幂等性意味着一些假设:重启一个失败的任务必须以相同的顺序重播相同的消息(一个基于日志的消息代理这样做),处理必须是确定性的,其他节点不能同时更新相同的值[ 98,99]。 当从一个处理节点故障转移到另一个处理节点时,可能需要进行防护(请参阅第291页上的“领导和锁定”),以防止被认为是死的节点的干扰 @@ -675,7 +675,7 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 一种选择是将状态保持在远程数据存储中并复制它,尽管如每个单独消息的远程数据库查询速度可能会很慢,正如在“流表加入(第473页)”中所述。另一种方法是保持流处理器的本地状态,并定期复制。然后,当流处理器从故障中恢复时,新任务可以读取复制状态并恢复处理而不丢失数据。 -例如,Flink定期捕获操作员状态的快照,并将它们写入HDFS等持久存储器中[92,93]。 Samza和Kafka Streams通过将状态更改发送到具有日志压缩功能的专用Kafka主题来复制状态更改,这类似于更改数据捕获[84,100]。 VoltDB通过冗余处理多个节点上的每个输入消息来复制状态(请参阅第252页的“实际的串行执行”)。 +例如,Flink定期捕获操作员状态的快照,并将它们写入HDFS等持久存储器中【92,93】。 Samza和Kafka Streams通过将状态更改发送到具有日志压缩功能的专用Kafka主题来复制状态更改,这类似于更改数据捕获[84,100]。 VoltDB通过冗余处理多个节点上的每个输入消息来复制状态(请参阅第252页的“实际的串行执行”)。 在某些情况下,甚至可能不需要复制状态,因为它可以从输入流重建。例如,如果状态由一个相当短的窗口中的聚合组成,则它可能足够快,以便重放与该窗口相对应的输入事件。如果状态是通过更改数据捕获维护的数据库的本地副本,那么也可以从日志压缩的更改流重建数据库(请参阅“日志压缩”一节第456页)。 diff --git a/ch12.md b/ch12.md index de0a052..2692b77 100644 --- a/ch12.md +++ b/ch12.md @@ -30,7 +30,7 @@ ### 组合使用衍生数据的工具 -例如,为了处理任意关键字的查询,需要将OLTP数据库与全文搜索索引集成在一起是很常见的。尽管一些数据库(如PostgreSQL)包含了全文索引功能,可以满足简单的应用需求[1],但更复杂的搜索工具需要专业的信息检索工具。相反,搜索索引通常不适合作为一个持久的记录系统,因此许多应用程序需要结合两种不同的工具来满足所有要求。 +例如,为了处理任意关键字的查询,需要将OLTP数据库与全文搜索索引集成在一起是很常见的。尽管一些数据库(如PostgreSQL)包含了全文索引功能,可以满足简单的应用需求【1】,但更复杂的搜索工具需要专业的信息检索工具。相反,搜索索引通常不适合作为一个持久的记录系统,因此许多应用程序需要结合两种不同的工具来满足所有要求。 我们谈到了将数据系统集成到“使系统保持同步”(第452页)的问题。随着数据的不同表示数量的增加,集成问题变得更加困难。除了数据库和搜索索引之外,也许您需要保留分析系统(数据仓库或批处理和流处理系统)中的数据副本。维护从原始数据衍生的对象的高速缓存或非规范化版本;通过机器学习,分类,排名或推荐系统传递数据;或根据对数据的更改发送通知。 @@ -85,10 +85,10 @@ 但是,在一个地方存储友谊状态并在另一个地方存储消息的系统中,不友好事件和消息发送事件之间的顺序依赖关系可能会丢失。如果未捕获到因果依赖关系,则发送有关新消息的通知的服务可能会在不友好事件之前处理消息发送事件,从而错误地向前伙伴发送通知。 -在本例中,通知实际上是消息和好友列表之间的连接,使得它与我们先前讨论的连接的时间问题有关(请参阅第475页的“连接的时间依赖性”)。不幸的是,这个问题似乎并没有一个简单的答案[2,3]。起点包括: +在本例中,通知实际上是消息和好友列表之间的连接,使得它与我们先前讨论的连接的时间问题有关(请参阅第475页的“连接的时间依赖性”)。不幸的是,这个问题似乎并没有一个简单的答案【2,3】。起点包括: * 逻辑时间戳可以提供没有协调的全部订购(请参见“序列号排序”第页343),因此它们可能有助于总订单广播不可行的情况。但是,他们仍然要求收件人处理不按顺序发送的事件,并且需要传递其他元数据。 -* 如果您可以记录一个事件来记录用户在做出决定之前所看到的系统状态,并给该事件一个唯一的标识符,那么后面的任何事件都可以引用该事件标识符来记录因果关系[4] 。我们将在第513页的“Reads are events too”中回到这个想法。 +* 如果您可以记录一个事件来记录用户在做出决定之前所看到的系统状态,并给该事件一个唯一的标识符,那么后面的任何事件都可以引用该事件标识符来记录因果关系【4】 。我们将在第513页的“Reads are events too”中回到这个想法。 * 冲突解决算法(请参阅“自动冲突解决”(第165页))有助于处理以意外顺序传递的事件。它们对于维护状态很有用,但如果行为有外部副作用(例如,也许,随着时间的推移,应用程序开发模式将出现,使得能够有效地捕获因果依赖关系,并且保持正确的衍生状态,而不会迫使所有事件经历全部命令广播的瓶颈。 ### 批量处理与流处理 @@ -99,17 +99,17 @@ 正如我们在第10章和第11章中看到的,批处理和流处理有许多共同的原则,主要的根本区别在于流处理器在无界数据集上运行,而批处理输入是已知的有限大小。处理引擎的实现方式也有很多细节上的差异,但是这些区别开始模糊。 -Spark在批处理引擎上执行流处理,将流分解为微格式,而Apache Flink则在流处理引擎上执行批处理[5]。原则上,一种类型的处理可以在另一种类型上仿真,但是性能特征会有所不同:例如,在跳跃或滑动窗口时,微博可能表现不佳[6]。 +Spark在批处理引擎上执行流处理,将流分解为微格式,而Apache Flink则在流处理引擎上执行批处理【5】。原则上,一种类型的处理可以在另一种类型上仿真,但是性能特征会有所不同:例如,在跳跃或滑动窗口时,微博可能表现不佳【6】。 #### 保持衍生状态 批处理具有非常强大的功能特性(即使代码不是用函数式编程语言编写的):它鼓励确定性的纯函数,其输出仅依赖于输入,除了显式输出外没有副作用,处理输入作为不可变的,并作为附加的输出。流处理类似,但它扩展了运算符以允许受管理的容错状态(请参阅第478页的“重建失败后的状态”)。 -具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(请参见第478页的“幂等性”),但也简化了有关组织中数据流的推理[7]。无论衍生数据是搜索索引,统计模型还是缓存,从数据管道角度来看,从另一个衍生出一件事情,通过功能应用程序代码推送一个系统中的状态更改和应用对衍生系统的影响。 +具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(请参见第478页的“幂等性”),但也简化了有关组织中数据流的推理【7】。无论衍生数据是搜索索引,统计模型还是缓存,从数据管道角度来看,从另一个衍生出一件事情,通过功能应用程序代码推送一个系统中的状态更改和应用对衍生系统的影响。 原则上,衍生数据系统可以同步维护,就像关系数据库在与被索引表写入操作相同的事务中同步更新辅助索引一样。然而,异步是基于事件日志的系统稳健的原因:它允许系统的一部分故障被本地包含,而如果任何一个参与者失败,分布式事务将中止,因此他们倾向于通过将故障扩展到系统的其余部分(请参阅第363页的“分布式事务的限制”)。 -我们在第206页的“分区和二级索引”中看到,二级索引经常跨越分区边界。具有二级索引的分区系统需要将写入发送到多个分区(如果索引是分词)或将读取发送到所有分区(如果索引是文档分区的话)。如果索引是异步维护的,这种交叉分区通信也是最可靠和可扩展的[8](另请参阅“多分区数据处理”第479页)。 +我们在第206页的“分区和二级索引”中看到,二级索引经常跨越分区边界。具有二级索引的分区系统需要将写入发送到多个分区(如果索引是分词)或将读取发送到所有分区(如果索引是文档分区的话)。如果索引是异步维护的,这种交叉分区通信也是最可靠和可扩展的【8】(另请参阅“多分区数据处理”第479页)。 #### 为应用程序演变重新处理数据 @@ -119,19 +119,19 @@ Spark在批处理引擎上执行流处理,将流分解为微格式,而Apache > ### 在铁路上的模式迁移 > -> 大规模的“模式迁移”也发生在非计算机系统中。例如,在19世纪英国铁路建设初期,轨距(两轨之间的距离)就有了各种各样的交易标准。为一个测量仪而建的列车不能在另一个测量仪的轨道上运行,这限制了火车网络中可能的相互连接[9]。 +> 大规模的“模式迁移”也发生在非计算机系统中。例如,在19世纪英国铁路建设初期,轨距(两轨之间的距离)就有了各种各样的交易标准。为一个测量仪而建的列车不能在另一个测量仪的轨道上运行,这限制了火车网络中可能的相互连接【9】。 > > 在1846年最终确定了一个标准仪表之后,其他仪表的轨道必须转换 - 但是如何在不关闭火车线路的情况下进行数月甚至数年?解决的办法是首先将轨道转换为双轨或混合轨距,方法是增加第三轨。这种转换可以逐渐完成,当完成时,两个仪表的列车可以在三条轨道中的两条轨道上运行。事实上,一旦所有的列车都转换成标准轨距,那么可以移除提供非标准轨距的轨道。 > > 以这种方式“再加工”现有的轨道,让新旧版本并存,可以在几年的时间内逐渐改变轨距。然而,这是一项昂贵的事业,这就是今天非标准仪表仍然存在的原因。例如,旧金山湾区的BART系统使用与美国大部分地区不同的仪表。 -衍生视图允许逐步演变。如果您想重新构建数据集,则不需要执行迁移作为突然切换。相反,您可以将旧架构和新架构并排维护为相同基础数据上的两个独立衍生视图。然后,您可以开始将少量用户转移到新视图,以测试其性能并发现任何错误,而大多数用户仍然会被路由到旧视图。逐渐地,您可以增加访问新视图的用户比例,最终您可以删除旧视图[10]。 +衍生视图允许逐步演变。如果您想重新构建数据集,则不需要执行迁移作为突然切换。相反,您可以将旧架构和新架构并排维护为相同基础数据上的两个独立衍生视图。然后,您可以开始将少量用户转移到新视图,以测试其性能并发现任何错误,而大多数用户仍然会被路由到旧视图。逐渐地,您可以增加访问新视图的用户比例,最终您可以删除旧视图【10】。 -这种逐渐迁移的美妙之处在于,如果出现问题,每个阶段的过程都很容易逆转:您始终有一个可以回到的工作系统。通过降低不可逆损害的风险,您可以更有信心继续前进,从而更快地改善您的系统[11]。 +这种逐渐迁移的美妙之处在于,如果出现问题,每个阶段的过程都很容易逆转:您始终有一个可以回到的工作系统。通过降低不可逆损害的风险,您可以更有信心继续前进,从而更快地改善您的系统【11】。 #### Lambda架构 -如果批处理用于重新处理历史数据,并且流处理用于处理最近的更新,那么您如何将这两者结合起来?拉姆达体系结构[12]是这方面的一个建议,引起了很多关注。 +如果批处理用于重新处理历史数据,并且流处理用于处理最近的更新,那么您如何将这两者结合起来?拉姆达体系结构【12】是这方面的一个建议,引起了很多关注。 lambda体系结构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件源(请参阅第457页上的“事件源”)。从这些事件中,推导出读取优化的视图。 lambda体系结构建议并行运行两个不同的系统:批处理系统(如Hadoop MapReduce)和独立的流处理系统(如Storm)。 @@ -139,13 +139,13 @@ lambda体系结构的核心思想是通过将不可变事件附加到不断增 拉姆达体系结构是一个有影响力的想法,它将数据系统的设计变得更好,尤其是通过推广将视图衍生到不可变事件流和在需要时重新处理事件的原则。但是,我也认为它有一些实际问题: -* 必须保持相同的逻辑才能在批处理和流处理框架中运行,这是额外的工作。虽然像Summingbird [13]这样的库提供了一个抽象的计算,可以在一个批处理或流的上下文中运行,调试,调整和维护两个不同系统的操作复杂性仍然[14]。 +* 必须保持相同的逻辑才能在批处理和流处理框架中运行,这是额外的工作。虽然像Summingbird 【13】这样的库提供了一个抽象的计算,可以在一个批处理或流的上下文中运行,调试,调整和维护两个不同系统的操作复杂性仍然【14】。 * 由于流管道和批处理管道产生单独的输出,因此需要合并它们以响应用户请求。如果计算是通过滚动窗口的简单聚合,则合并相当容易,但如果使用更复杂的操作(例如连接和会话化)导出视图,或者输出不是时间序列,则显得非常困难。 * 尽管有能力重新处理整个历史数据集是很好的,但在大型数据集上这样做经常会很昂贵。因此,批处理流水线通常需要设置为处理增量批处理(例如,在每小时结束时处理一小时的数据),而不是重新处理所有内容。这引发了第468页的“关于时间的推理”中讨论的问题,例如处理分段器和处理跨批次边界的窗口。增加批量计算会增加复杂性,使其更类似于流式传输层,这与保持批处理层尽可能简单的目标背道而驰。 #### 统一批处理和流处理 -最近的工作使得lambda体系结构的优点在没有其缺点的情况下得以实现,允许批处理计算(重新处理历史数据)和流计算(处理事件到达时)在同一个系统中实现[15]。 +最近的工作使得lambda体系结构的优点在没有其缺点的情况下得以实现,允许批处理计算(重新处理历史数据)和流计算(处理事件到达时)在同一个系统中实现【15】。 在一个系统中统一批处理和流处理需要以下功能,这些功能越来越广泛: @@ -161,7 +161,7 @@ lambda体系结构的核心思想是通过将不可变事件附加到不断增 ## 拆分数据库 -在最抽象的层面上,数据库,Hadoop和操作系统都执行相同的功能:它们存储一些数据,并允许您处理和查询数据[16]。数据库将数据存储在某些数据模型(表中的文档,文档中的顶点,图形中的顶点等)的记录中,而操作系统的文件系统则将数据存储在文件中 - 但在其核心上,都是“信息管理”系统[ 17。正如我们在第10章中看到的,Hadoop生态系统有点像Unix的分布式版本。 +在最抽象的层面上,数据库,Hadoop和操作系统都执行相同的功能:它们存储一些数据,并允许您处理和查询数据【16】。数据库将数据存储在某些数据模型(表中的文档,文档中的顶点,图形中的顶点等)的记录中,而操作系统的文件系统则将数据存储在文件中 - 但在其核心上,都是“信息管理”系统[ 17。正如我们在第10章中看到的,Hadoop生态系统有点像Unix的分布式版本。 当然,有很多实际的差异。例如,许多文件系统不能很好地处理包含1000万个小文件的目录,而包含1000万个小记录的数据库是完全正常且不起眼的。无论如何,操作系统和数据库之间的相似之处和差异值得探讨。 @@ -180,7 +180,7 @@ Unix和关系数据库以非常不同的哲学来处理信息管理问题。 Uni * 二级索引,使您可以根据字段的值有效地搜索记录(请参阅第79页上的“其他索引结构” * 物化视图,这是一种预先计算的查询结果缓存(请参阅“聚合:数据立方体和物化视图”(第101页) * 复制日志,保持其他节点上数据的副本最新(请参阅第158页中的“复制日志的实现”) -* 全文搜索索引,允许在文本中进行关键字搜索(请参见第88页上的“全文搜索和模糊索引”)以及内置于某些关系数据库[1] +* 全文搜索索引,允许在文本中进行关键字搜索(请参见第88页上的“全文搜索和模糊索引”)以及内置于某些关系数据库【1】 在第十章和第十一章中,出现了类似的主题。我们讨论了如何构建全文搜索索引(请参阅第357页上的“批处理工作流的输出”),了解有关实例化视图维护(请参阅“维护实例化视图”一节第437页)以及有关将更改从数据库复制到衍生数据系统(请参阅第454页的“更改数据捕获”)。 @@ -196,7 +196,7 @@ Unix和关系数据库以非常不同的哲学来处理信息管理问题。 Uni #### 一切的元数据库 -有鉴于此,我认为整个组织的数据流开始像一个巨大的数据库[7]。每当批处理,流或ETL过程将数据从一个地方传输到另一个地方并形成表单时,就像数据库子系统一样,使索引或物化视图保持最新。 +有鉴于此,我认为整个组织的数据流开始像一个巨大的数据库【7】。每当批处理,流或ETL过程将数据从一个地方传输到另一个地方并形成表单时,就像数据库子系统一样,使索引或物化视图保持最新。 像这样看,批处理和流处理器就像触发器,存储过程和物化视图维护例程的精细实现。他们维护的衍生数据系统就像不同的索引类型。例如,关系数据库可能支持B树索引,散列索引,空间索引(请参阅第79页的“多列索引”)以及其他类型的索引。在新兴的衍生数据系统体系结构中,不是将这些设施作为单个集成数据库产品的功能实现,而是由各种不同的软件提供,运行在不同的机器上,由不同的团队管理。 @@ -204,23 +204,23 @@ Unix和关系数据库以非常不同的哲学来处理信息管理问题。 Uni **联合数据库:统一读取** -可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 - 一种称为联邦数据库或多存储的方法[18,19]。例如,PostgreSQL的外部数据包装功能符合这种模式[20]。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。 +可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 - 一种称为联邦数据库或多存储的方法【18,19】。例如,PostgreSQL的外部数据包装功能符合这种模式【20】。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。 联合查询接口遵循单一集成系统的关系传统,具有高级查询语言和优雅的语义,但却是一个复杂的实现。 **非捆绑数据库:统一写入** -虽然联合会解决了跨多个不同系统的只读查询问题,但它并没有很好的解决跨系统同步写入的问题。我们说过,在单个数据库中,创建一致的索引是一项内置功能。当我们构建多个存储系统时,我们同样需要确保所有数据更改都会在所有正确的位置结束,即使在出现故障时也是如此。将存储系统可靠地插接在一起(例如,通过更改数据捕获和事件日志)更容易,就像将数据库的索引维护功能以可以跨不同技术同步写入的方式分开[7,21]。 +虽然联合会解决了跨多个不同系统的只读查询问题,但它并没有很好的解决跨系统同步写入的问题。我们说过,在单个数据库中,创建一致的索引是一项内置功能。当我们构建多个存储系统时,我们同样需要确保所有数据更改都会在所有正确的位置结束,即使在出现故障时也是如此。将存储系统可靠地插接在一起(例如,通过更改数据捕获和事件日志)更容易,就像将数据库的索引维护功能以可以跨不同技术同步写入的方式分开【7,21】。 -unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件事[22],通过统一的低级API(管道)进行通信,并且可以使用更高级别的语言(shell)[16] 。 +unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件事【22】,通过统一的低级API(管道)进行通信,并且可以使用更高级别的语言(shell)【16】 。 #### 开展分拆工作 联邦和非捆绑是同一个硬币的两个方面:用不同的组件构成可靠,可扩展和可维护的系统。联合只读联邦和非捆绑是同一个硬币的两个方面:用不同的组件构成可靠,可扩展和可维护的系统。联合只读查询需要将一个数据模型映射到另一个数据模型,这需要一些思考,但最终还是一个可管理的问题。我认为保持写入到几个存储系统是同步的更困难的工程问题,所以我将重点关注它。 -传统的同步写入方法需要跨异构存储系统的分布式事务[18],我认为这是错误的解决方案(请参阅“导出的数据与分布式事务”第495页)。单个存储或流处理系统内的事务是可行的,但是当数据跨越不同技术之间的边界时,我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法。 +传统的同步写入方法需要跨异构存储系统的分布式事务【18】,我认为这是错误的解决方案(请参阅“导出的数据与分布式事务”第495页)。单个存储或流处理系统内的事务是可行的,但是当数据跨越不同技术之间的边界时,我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法。 -例如,分布式事务在某些流处理器中使用,以精确匹配一次语义(请参阅第477页的“重新访问原子提交”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理器写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的事件的有序日志(参见第478页的“幂等性”)是一种更简单的抽象,因此在异构系统中实现更加可行[7]。 +例如,分布式事务在某些流处理器中使用,以精确匹配一次语义(请参阅第477页的“重新访问原子提交”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理器写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的事件的有序日志(参见第478页的“幂等性”)是一种更简单的抽象,因此在异构系统中实现更加可行【7】。 基于日志的集成的一大优势是各个组件之间的松散耦合,这体现在两个方面: @@ -231,7 +231,7 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 如果分拆确实成为未来的方式,它将不会取代目前形式的数据库 - 它们仍然会像以往一样需要。数据库仍然需要维护流处理器中的状态,并且为批处理和流处理器的输出提供查询服务(请参阅第419页上的“批处理工作流的输出”和第464页上的“处理流”)。专门的查询引擎将继续对特定的工作负载非常重要:例如,MPP数据仓库中的查询引擎针对探索性分析查询进行了优化,并且能够很好地处理这种类型的工作负载(请参阅第417页的“将Hadoop与分布式数据库进行比较” 。 -运行几个不同基础架构的复杂性可能是一个问题:每一个软件都有一个学习曲线,配置问题和操作怪癖,因此值得部署尽可能少的移动部件。与由应用程序代码[23]组成的多个工具组成的系统相比,单一集成软件产品也可以在其设计的工作负载类型上实现更好,更可预测的性能。正如我在前言中所说的那样,为了扩大规模而建设你不需要的是浪费精力,并且可能会将你锁定在一个不灵活的设计中。实际上,这是一种过早优化的形式。 +运行几个不同基础架构的复杂性可能是一个问题:每一个软件都有一个学习曲线,配置问题和操作怪癖,因此值得部署尽可能少的移动部件。与由应用程序代码【23】组成的多个工具组成的系统相比,单一集成软件产品也可以在其设计的工作负载类型上实现更好,更可预测的性能。正如我在前言中所说的那样,为了扩大规模而建设你不需要的是浪费精力,并且可能会将你锁定在一个不灵活的设计中。实际上,这是一种过早优化的形式。 分拆的目标不是要针对个别数据库与特定工作负载的性能进行竞争;我们的目标是允许您结合多个不同的数据库,以便在比单个软件可能实现的更广泛的工作负载范围内实现更好的性能。这是关于广度,而不是深度 - 与我们在第414页上的“比较Hadoop与分布式数据库”中讨论的存储和处理模型的多样性一样。 @@ -241,19 +241,19 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 用于组成数据系统的工具正在变得越来越好,但我认为缺少一个主要部分:我们还没有Unix shell的非捆绑式数据库(即,用于组成存储和处理系统的高级语言简单和陈述的方式)。 -例如,如果我们可以简单地声明mysql |,我就会喜欢它elasticsearch,类似于Unix管道[22],这将成为CREATE INDEX的非捆绑等价物:它将MySQL数据库中的所有文档并将其索引到Elasticsearch集群中。然后它会不断捕获对数据库所做的所有更改,并自动将它们应用于搜索索引,而无需编写自定义应用程序代码。几乎任何类型的存储或索引系统都可以实现这种集成。 +例如,如果我们可以简单地声明mysql |,我就会喜欢它elasticsearch,类似于Unix管道【22】,这将成为CREATE INDEX的非捆绑等价物:它将MySQL数据库中的所有文档并将其索引到Elasticsearch集群中。然后它会不断捕获对数据库所做的所有更改,并自动将它们应用于搜索索引,而无需编写自定义应用程序代码。几乎任何类型的存储或索引系统都可以实现这种集成。 -同样,能够更容易地预先计算和更新缓存将是一件好事。回想一下,物化视图本质上是一个预先计算的缓存,所以您可以通过为复杂查询声明指定物化视图来创建缓存,包括图上的递归查询(请参阅第49页上的“类图数据模型”)和应用逻辑。在这方面有一些有趣的早期研究,如差异数据流[24,25],我希望这些想法能够在生产系统中找到自己的方法。 +同样,能够更容易地预先计算和更新缓存将是一件好事。回想一下,物化视图本质上是一个预先计算的缓存,所以您可以通过为复杂查询声明指定物化视图来创建缓存,包括图上的递归查询(请参阅第49页上的“类图数据模型”)和应用逻辑。在这方面有一些有趣的早期研究,如差异数据流【24,25】,我希望这些想法能够在生产系统中找到自己的方法。 ### 围绕数据流设计应用 -通过使用应用程序代码组成专门的存储和处理系统来分离数据库的方法也被称为“数据库内外”方法[26],在2014年的一次会议演讲标题之后[27]。然而,称它为“新建筑”太宏大。我把它看作是一个设计模式,一个讨论的起点,我们只是简单地给它起一个名字,以便我们可以更好地谈论它。 +通过使用应用程序代码组成专门的存储和处理系统来分离数据库的方法也被称为“数据库内外”方法【26】,在2014年的一次会议演讲标题之后【27】。然而,称它为“新建筑”太宏大。我把它看作是一个设计模式,一个讨论的起点,我们只是简单地给它起一个名字,以便我们可以更好地谈论它。 -这些想法不是我的;他们只是我认为我们应该学习的其他人思想的融合。尤其是,数据流语言(如Oz [28]和Juttle [29]),功能反应式编程(FRP)语言(如Elm [30,31])和逻辑编程语言(如Bloom [ 32。 Jay Kreps [7]提出了在这种背景下解绑的术语。 +这些想法不是我的;他们只是我认为我们应该学习的其他人思想的融合。尤其是,数据流语言(如Oz 【28】和Juttle 【29】),功能反应式编程(FRP)语言(如Elm 【30,31】)和逻辑编程语言(如Bloom [ 32。 Jay Kreps 【7】提出了在这种背景下解绑的术语。 -即使电子表格也具有比大多数主流编程语言遥远的数据流编程功能[33]。在电子表格中,可以将公式放入一个单元格中(例如,另一列中的单元格总和),并且只要公式的任何输入发生更改,公式的结果都会自动重新计算。这正是我们在数据系统级所需要的:当数据库中的记录发生更改时,我们希望自动更新该记录的任何索引,并且自动刷新依赖于记录的任何缓存视图或聚合。您不必担心这种刷新如何发生的技术细节,但能够简单地相信它可以正常工作。 +即使电子表格也具有比大多数主流编程语言遥远的数据流编程功能【33】。在电子表格中,可以将公式放入一个单元格中(例如,另一列中的单元格总和),并且只要公式的任何输入发生更改,公式的结果都会自动重新计算。这正是我们在数据系统级所需要的:当数据库中的记录发生更改时,我们希望自动更新该记录的任何索引,并且自动刷新依赖于记录的任何缓存视图或聚合。您不必担心这种刷新如何发生的技术细节,但能够简单地相信它可以正常工作。 -因此,我认为绝大多数数据系统仍然可以从VisiCalc在1979年已经具备的功能中学习[34]。与电子表格的不同之处在于,今天的数据系统需要具有容错性,可扩展性以及持久存储数据。他们还需要能够整合不同人群编写的不同技术,并重用现有的库和服务:期望使用某种特定语言,框架或工具开发所有软件是不切实际的。 +因此,我认为绝大多数数据系统仍然可以从VisiCalc在1979年已经具备的功能中学习【34】。与电子表格的不同之处在于,今天的数据系统需要具有容错性,可扩展性以及持久存储数据。他们还需要能够整合不同人群编写的不同技术,并重用现有的库和服务:期望使用某种特定语言,框架或工具开发所有软件是不切实际的。 在本节中,我将详细介绍这些想法,并探讨一些围绕非捆绑数据库和数据流的想法构建应用程序的方法。 @@ -266,7 +266,7 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 * 在机器学习系统中,我们可以将模型视为通过应用各种特征提取和统计分析功能从训练数据中导出。当模型应用于新的输入数据时,模型的输出是从输入和模型(因此间接地从训练数据)中导出的。 * 缓存通常包含将以用户界面(UI)显示的形式的数据聚合。因此填充缓存需要知道UI中引用的字段; UI中的更改可能需要更新缓存填充方式的定义以及重建缓存。 -辅助索引的衍生函数通常是必需的,因此它作为核心特性被构建到许多数据库中,您可以仅通过说CREATE INDEX来调用它。对于全文索引,常见语言的基本语言特征可能内置到数据库中,但更复杂的特征通常需要特定于域的调整。在机器学习中,特征工程是众所周知的特定于应用程序的特征,并且通常必须包含关于应用程序的用户交互和部署的详细知识[35]。 +辅助索引的衍生函数通常是必需的,因此它作为核心特性被构建到许多数据库中,您可以仅通过说CREATE INDEX来调用它。对于全文索引,常见语言的基本语言特征可能内置到数据库中,但更复杂的特征通常需要特定于域的调整。在机器学习中,特征工程是众所周知的特定于应用程序的特征,并且通常必须包含关于应用程序的用户交互和部署的详细知识【35】。 当创建衍生数据集的函数不是像创建二级索引那样的标准Cookie切割函数时,需要自定义代码来处理特定于应用程序的方面。而这个自定义代码是许多数据库难以抗争虽然关系数据库通常支持触发器,存储过程和用户定义的函数,它们可以用来在数据库中执行应用程序代码,但它们在数据库设计中已经有所反应了(请参阅“传输事件流”(第447页))。 @@ -277,7 +277,7 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 另一方面,Mesos,YARN,Docker,Kubernetes等部署和集群管理工具专为运行应用程序代码而设计。通过专注于做好一件事情,他们能够做得比将数据库作为其众多功能之一执行用户定义的功能要好得多。 我认为让系统的某些部分专门用于持久数据存储以及专门运行应用程序代码的其他部分是有意义的。这两者可以在保持独立的同时互动。 -现在大多数Web应用程序都是作为无状态服务部署的,其中任何用户请求都可以路由到任何应用程序服务器,并且服务器在发送响应后会忘记所有请求。这种部署方式很方便,因为可以随意添加或删除服务器,但状态必须到某个地方:通常是数据库。趋势是将无状态应用程序逻辑与状态管理(数据库)分开:不将应用程序逻辑放入数据库中,也不将持久状态置于应用程序中[36]。正如职能规划界人士喜欢开玩笑说的那样,“我们相信教会与国家的分离”【37】 [^i] +现在大多数Web应用程序都是作为无状态服务部署的,其中任何用户请求都可以路由到任何应用程序服务器,并且服务器在发送响应后会忘记所有请求。这种部署方式很方便,因为可以随意添加或删除服务器,但状态必须到某个地方:通常是数据库。趋势是将无状态应用程序逻辑与状态管理(数据库)分开:不将应用程序逻辑放入数据库中,也不将持久状态置于应用程序中【36】。正如职能规划界人士喜欢开玩笑说的那样,“我们相信教会与国家的分离”【37】 [^i] [^i]: 解释一个笑话很少改进它,但我不想让任何人感到被遗漏。 在这里,Church是数学家Alonzo Church的参考资料,他创建了lambda演算,这是早期的计算形式,是大多数函数式编程语言的基础。 lambda演算不具有可变状态(即没有变量可以被覆盖),所以可以说可变状态与Church的工作是分开的。 @@ -291,7 +291,7 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 从数据流的角度思考应用意味着重新谈判应用代码和状态管理之间的关系。我们不是将数据库视为被应用程序操纵的被动变量,而是更多地考虑状态,状态更改和处理它们的代码之间的相互作用和协作。应用程序代码通过在另一个地方触发状态更改来响应状态更改。 -我们在第451页的“数据库和数据流”中看到了这一思路,我们讨论将数据库更改的日志作为我们可以指定的事件流处理。消息传递系统(如角色)(请参阅第136页的“消息传递数据流”)也具有响应事件的概念。早在20世纪80年代,元组空间模型探索表示分布式计算的过程,观察状态变化并对它们做出反应[38,39]。 +我们在第451页的“数据库和数据流”中看到了这一思路,我们讨论将数据库更改的日志作为我们可以指定的事件流处理。消息传递系统(如角色)(请参阅第136页的“消息传递数据流”)也具有响应事件的概念。早在20世纪80年代,元组空间模型探索表示分布式计算的过程,观察状态变化并对它们做出反应【38,39】。 如前所述,当触发器由于数据更改而触发时,或者次级索引更新以反映索引表中的更改时,数据库内部会发生类似的情况。分解数据库意味着将此想法应用于在主数据库之外创建衍生数据集:缓存,全文搜索索引,机器学习或分析系统。我们可以为此使用流处理和消息传递系统。 @@ -307,9 +307,9 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 当前流行的应用程序开发风格涉及将功能分解为一组通过同步网络请求(如REST API)进行通信的服务(请参阅第121页的“通过服务实现数据流:REST和RPC”)。这种面向服务的体系结构优于单个单一应用程序的优势主要在于通过松散耦合的组织可伸缩性:不同的团队可以在不同的服务上工作,从而减少团队之间的协调工作(只要服务可以独立部署和更新)。 -将流操作符合并到数据流系统中与微服务方法有很多相似的特征[40]。但是,底层的通信机制是非常不同的:单向异步消息流而不是同步请求/响应交互。 +将流操作符合并到数据流系统中与微服务方法有很多相似的特征【40】。但是,底层的通信机制是非常不同的:单向异步消息流而不是同步请求/响应交互。 -除了第136页上的“消息传递数据流”中列出的优点(如更好的容错性),数据流系统还可以获得更好的性能。例如,假设客户正在购买以一种货币定价但以另一种货币支付的商品。为了执行货币转换,您需要知道当前的汇率。这个操作可以通过两种方式实现[40,41]: +除了第136页上的“消息传递数据流”中列出的优点(如更好的容错性),数据流系统还可以获得更好的性能。例如,假设客户正在购买以一种货币定价但以另一种货币支付的商品。为了执行货币转换,您需要知道当前的汇率。这个操作可以通过两种方式实现【40,41】: 1. 在微服务方法中,处理购买的代码可能会查询汇率服务或数据库以获取特定货币的当前汇率。 2. 在数据流方法中,处理采购的代码将提前订阅汇率更新流,并在当地数据库发生更改时将当前汇率记录下来。处理采购时,只需查询本地数据库即可。 @@ -364,9 +364,9 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 传统上,网络浏览器是无状态的客户端,只有在您连接到互联网时才能做有用的事情(只有您可以离线执行的唯一的事情是在您之前在线加载的页面上下滚动)。然而,最近的“单页面”JavaScript Web应用程序已经获得了很多有状态的功能,包括客户端用户界面交互和Web浏览器中的持久本地存储。移动应用程序可以类似地在设备上存储大量状态,并且不需要往返于大多数用户交互的服务器。 -这些不断变化的功能引发了对离线优先应用程序的重新兴趣,这些应用程序尽可能地在同一设备上使用本地数据库,无需连接互联网,并且在网络连接时与后台远程服务器同步可用[42]。由于移动设备通常具有缓慢且不可靠的蜂窝互联网连接,因此,如果用户的用户界面不必等待同步网络请求,并且应用程序大多离线工作,则对用户来说是一大优势(请参阅“具有离线操作的客户端”第170页)。 +这些不断变化的功能引发了对离线优先应用程序的重新兴趣,这些应用程序尽可能地在同一设备上使用本地数据库,无需连接互联网,并且在网络连接时与后台远程服务器同步可用【42】。由于移动设备通常具有缓慢且不可靠的蜂窝互联网连接,因此,如果用户的用户界面不必等待同步网络请求,并且应用程序大多离线工作,则对用户来说是一大优势(请参阅“具有离线操作的客户端”第170页)。 -当我们摆脱无国籍客户与中央数据库交谈的假设,并转向终端用户设备上维护的状态时,开启了一个全新的机会。特别是,我们可以将设备上的状态视为服务器上的状态缓存。屏幕上的像素是客户端应用程序中模型对象的物化视图;模型对象是远程数据中心的本地状态副本[27]。 +当我们摆脱无国籍客户与中央数据库交谈的假设,并转向终端用户设备上维护的状态时,开启了一个全新的机会。特别是,我们可以将设备上的状态视为服务器上的状态缓存。屏幕上的像素是客户端应用程序中模型对象的物化视图;模型对象是远程数据中心的本地状态副本【27】。 #### 将状态更改推送给客户端 @@ -374,13 +374,13 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 更新的协议已经超越了HTTP的基本请求/响应模式:服务器发送的事件(EventSource API)和WebSockets提供了通信渠道,通过这些通信渠道,Web浏览器可以与服务器保持开放的TCP连接,服务器可以只要保持连接状态,就会主动将消息推送到浏览器。这为服务器提供了一个机会,主动通知最终用户客户端本地存储状态的任何变化,从而减少客户端状态的陈旧程度。 -就我们的写入路径和读取路径模型而言,主动将状态改变到客户端设备意味着将写入路径一直延伸到最终用户。当客户端首次初始化时,它仍然需要使用读取路径来获取其初始状态,但此后可能依赖于服务器发送的状态更改流。我们在流处理和消息传递方面讨论的想法并不局限于仅在数据中心运行:我们可以进一步采用这些想法,并将它们一直延伸到终端用户设备[43]。 +就我们的写入路径和读取路径模型而言,主动将状态改变到客户端设备意味着将写入路径一直延伸到最终用户。当客户端首次初始化时,它仍然需要使用读取路径来获取其初始状态,但此后可能依赖于服务器发送的状态更改流。我们在流处理和消息传递方面讨论的想法并不局限于仅在数据中心运行:我们可以进一步采用这些想法,并将它们一直延伸到终端用户设备【43】。 这些设备有时会脱机,并且在此期间无法收到服务器状态更改的任何通知。但是我们已经解决了这个问题:在第449页的“消费者偏移量”中,我们讨论了基于日志的消息中介的使用者在失败或断开连接后可以重新连接,并确保它不会错过任何到达的消息它被断开。同样的技术适用于单个用户,每个设备都是小事件的小用户。 #### 端到端的事件流 -最近用于开发有状态客户端和用户界面的工具(如Elm语言[30]和Facebook的React,Flux和Redux工具链)已经通过订阅表示用户的事件流来管理内部客户端状态输入或来自服务器的响应,其结构与事件源相似(请参阅第457页的“事件源”)。 +最近用于开发有状态客户端和用户界面的工具(如Elm语言【30】和Facebook的React,Flux和Redux工具链)已经通过订阅表示用户的事件流来管理内部客户端状态输入或来自服务器的响应,其结构与事件源相似(请参阅第457页的“事件源”)。 将这种编程模型扩展为允许服务器将状态改变事件推送到客户端事件管道中是非常自然的。因此,状态变化可以通过端到端的写入路径流动:从触发状态改变的一个设备上的交互,通过事件日志以及通过多个衍生的数据系统和流处理器,一直到用户界面在另一台设备上观察状态的人。这些状态变化可以以相当低的延迟传播 - 比如说,在一秒内结束。 @@ -388,46 +388,46 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 挑战在于无状态客户端和请求/响应交互的假设在我们的数据库,库,框架和协议中非常深入。许多数据存储支持读取和写入操作,请求返回一个响应,但是少得多提供订阅更改的能力 - 即随着时间的推移返回响应流的请求(请参阅“更改流的API支持” 。 -为了将写入路径扩展到最终用户,我们需要从根本上重新思考我们构建这些系统的方式:从请求/响应交互转向发布/订阅数据流[27]。我认为更具响应性的用户界面和更好的离线支持的优势将使其值得付出努力。如果您正在设计数据系统,我希望您会记住订阅更改的选项,而不只是查询当前状态。 +为了将写入路径扩展到最终用户,我们需要从根本上重新思考我们构建这些系统的方式:从请求/响应交互转向发布/订阅数据流【27】。我认为更具响应性的用户界面和更好的离线支持的优势将使其值得付出努力。如果您正在设计数据系统,我希望您会记住订阅更改的选项,而不只是查询当前状态。 #### 读也是事件 我们讨论过,当流处理器将衍生数据写入存储(数据库,缓存或索引)时,以及当用户请求查询该存储时,存储将充当写入路径和读取路径之间的边界。该商店允许对数据进行随机访问读取查询,否则这些查询将需要扫描整个事件日志。 -在很多情况下,数据存储与流式传输系统是分开的。但请记住,流处理器还需要维护状态以执行聚合和连接(请参阅第472页的“流连接”)。这种状态通常隐藏在流处理器内部,但是一些框架允许它也被外部客户端查询[45],将流处理器本身变成一种简单的数据库。 +在很多情况下,数据存储与流式传输系统是分开的。但请记住,流处理器还需要维护状态以执行聚合和连接(请参阅第472页的“流连接”)。这种状态通常隐藏在流处理器内部,但是一些框架允许它也被外部客户端查询【45】,将流处理器本身变成一种简单的数据库。 -我想进一步考虑这个想法。正如到目前为止所讨论的那样,对商店的写入是通过事件日志进行的,而读取是瞬时网络请求,直接进入存储被查询数据的节点。这是一个合理的设计,但不是唯一可能的设计。也可以将读取请求表示为事件流,并通过流处理器发送读取事件和写入事件;处理器通过将读取结果发送到输出流来响应读取事件[46]。 +我想进一步考虑这个想法。正如到目前为止所讨论的那样,对商店的写入是通过事件日志进行的,而读取是瞬时网络请求,直接进入存储被查询数据的节点。这是一个合理的设计,但不是唯一可能的设计。也可以将读取请求表示为事件流,并通过流处理器发送读取事件和写入事件;处理器通过将读取结果发送到输出流来响应读取事件【46】。 当写入和读取都被表示为事件,并且被路由到同一个流操作符以便处理时,我们实际上是在读查询流和数据库之间执行流表连接。读取事件需要发送到保存数据的数据库分区(请参阅第214页的“请求路由”),就像批处理和流处理器在连接时需要在同一个键上共同输入一样(请参阅“Reduce-Side连接和分组“)。 -服务请求和正在执行的连接之间的这种对应关系是非常重要的[47]。一次性读取请求只是通过连接运算符传递请求,然后立即忘记它;订阅请求是与连接另一端的过去和未来事件的持续连接。 +服务请求和正在执行的连接之间的这种对应关系是非常重要的【47】。一次性读取请求只是通过连接运算符传递请求,然后立即忘记它;订阅请求是与连接另一端的过去和未来事件的持续连接。 -记录读取事件的日志可能对于追踪整个系统中的因果关系和数据来源也有好处:它可以让您在做出特定决策之前重建用户看到的内容。例如,在网上商店,向客户显示的预测出货日期和库存状态可能影响他们是否选择购买物品[4]。要分析此连接,您需要记录用户查询运输和库存状态的结果。 +记录读取事件的日志可能对于追踪整个系统中的因果关系和数据来源也有好处:它可以让您在做出特定决策之前重建用户看到的内容。例如,在网上商店,向客户显示的预测出货日期和库存状态可能影响他们是否选择购买物品【4】。要分析此连接,您需要记录用户查询运输和库存状态的结果。 -将读取事件写入持久存储器可以更好地跟踪因果关系(请参阅第493页的“订购事件以捕获因果关系”),但会产生额外的存储和I / O成本。优化这些系统以减少开销仍然是一个开放的研究问题[2]。但是,如果您已经为了操作目的而记录了读取请求,作为请求处理的副作用,将日志作为请求的来源并不是什么大的改变。 +将读取事件写入持久存储器可以更好地跟踪因果关系(请参阅第493页的“订购事件以捕获因果关系”),但会产生额外的存储和I / O成本。优化这些系统以减少开销仍然是一个开放的研究问题【2】。但是,如果您已经为了操作目的而记录了读取请求,作为请求处理的副作用,将日志作为请求的来源并不是什么大的改变。 #### 多分区数据处理 对于只涉及单个分区的查询,通过流发送查询和收集响应流的努力可能是过度的。然而,这个想法打开了分布式执行复杂查询的可能性,这需要合并来自多个分区的数据,利用流处理器已经提供的消息路由,分区和加入的基础设施。 -Storm的分布式RPC功能支持这种使用模式(请参阅第468页的“消息传递和RPC”)。例如,它已被用于计算在Twitter上看到过网址的人数 - 即,每个人都推送了该网址的跟随者集合[48]。由于Twitter用户组是分区的,因此这种计算需要合并来自多个分区的结果 +Storm的分布式RPC功能支持这种使用模式(请参阅第468页的“消息传递和RPC”)。例如,它已被用于计算在Twitter上看到过网址的人数 - 即,每个人都推送了该网址的跟随者集合【48】。由于Twitter用户组是分区的,因此这种计算需要合并来自多个分区的结果 -这种模式的另一个例子是欺诈预防:为了评估特定购买事件是否具有欺诈风险,您可以检查用户的IP地址,电子邮件地址,帐单地址,送货地址等的信誉分数。这些信誉数据库中的每一个都是自身分区的,因此为特定购买事件收集分数需要一系列具有不同分区数据集的联合[49]。 +这种模式的另一个例子是欺诈预防:为了评估特定购买事件是否具有欺诈风险,您可以检查用户的IP地址,电子邮件地址,帐单地址,送货地址等的信誉分数。这些信誉数据库中的每一个都是自身分区的,因此为特定购买事件收集分数需要一系列具有不同分区数据集的联合【49】。 MPP数据库的内部查询执行图具有相似的特征(请参阅第417页的“比较Hadoop与分布式数据库”)。如果您需要执行这种多分区连接,使用提供此功能的数据库可能比使用流处理器实现它更简单。但是,将查询视为流提供了一个选项,可以实现大规模应用程序,这些应用程序可以在传统的现成解决方案的限制下运行。 ## 目标是正确性 -对于只读取数据的无状态服务,出现问题时不会造成什么大问题:您可以修复该错误并重新启动服务,并且一切都恢复正常。像数据库这样的有状态的系统并不是那么简单:它们被设计成永远记住事物(或多或少),所以如果出现问题,效果也可能永远持续下去,这意味着它们需要更仔细的思考[50]。 +对于只读取数据的无状态服务,出现问题时不会造成什么大问题:您可以修复该错误并重新启动服务,并且一切都恢复正常。像数据库这样的有状态的系统并不是那么简单:它们被设计成永远记住事物(或多或少),所以如果出现问题,效果也可能永远持续下去,这意味着它们需要更仔细的思考【50】。 我们希望构建可靠和正确的应用程序(即,即使面对各种故障,其语义也能很好地定义和理解的程序)。大约四十年来,原子性,隔离性和耐久性(第7章)的交易特性一直是构建正确应用的首选工具。但是,这些基础比看起来更弱:例如见证弱隔离级别的混合(请参见“弱隔离级别”(第233页))。 在某些领域,事务被完全抛弃,并被提供更好性能和可伸缩性的模型取代,但是更复杂的语义(例如,请参阅第167页上的“无Leaderless复制”)。一致性经常被讨论,但定义不明确(参见第224页的“一致性”和第9章)。有些人主张我们应该“为了更好的可用性而拥抱弱一致性”,而对实际上的实际意义缺乏清晰的认识。 -对于如此重要的话题,我们的理解和我们的工程方法是惊人的片状。例如,确定在特定事务隔离级别或复制配置下运行特定应用程序是否安全是非常困难的[51,52]。通常简单的解决方案似乎在并发性低的情况下正常工作,并且没有错误,但是在要求更高的情况下会出现许多细微的错误。 +对于如此重要的话题,我们的理解和我们的工程方法是惊人的片状。例如,确定在特定事务隔离级别或复制配置下运行特定应用程序是否安全是非常困难的【51,52】。通常简单的解决方案似乎在并发性低的情况下正常工作,并且没有错误,但是在要求更高的情况下会出现许多细微的错误。 -例如,凯尔金斯伯里(Kyle Kingsbury)的杰普森(Jepsen)实验[53]强调了一些产品声称的安全保证与存在网络问题和崩溃时的实际行为之间的明显差异。即使像数据库这样的基础设施产品没有问题,应用程序代码仍然需要正确使用它们提供的功能,如果配置很难理解,这是很容易出错的(这是弱隔离级别,法定配置, 等等)。 +例如,凯尔金斯伯里(Kyle Kingsbury)的杰普森(Jepsen)实验【53】强调了一些产品声称的安全保证与存在网络问题和崩溃时的实际行为之间的明显差异。即使像数据库这样的基础设施产品没有问题,应用程序代码仍然需要正确使用它们提供的功能,如果配置很难理解,这是很容易出错的(这是弱隔离级别,法定配置, 等等)。 如果您的应用程序可以容忍偶尔以不可预测的方式破坏或丢失数据,那么生活就会简单得多,您可能只需横过手指就能逃脱,希望获得最好的效果。另一方面,如果您需要更强的正确性保证,那么可序列化和原子提交就是建立的方法,但是它们是有代价的:它们通常只在单个数据中心(排除地理分布式体系结构)中工作,您可以实现的规模和容错性能。 @@ -464,16 +464,16 @@ BEGIN TRANSACTION; COMMIT; ``` -客户端可以重新连接到数据库并重试事务,但现在它在TCP重复抑制的范围之外。由于例12-1中的交易不是幂等的,可能会发生\$22而不是所需的\$11。因此,尽管例12-1是一个交易原子性的标准例子,但它实际上并不正确,而真正的银行并不像这样工作[3]。 +客户端可以重新连接到数据库并重试事务,但现在它在TCP重复抑制的范围之外。由于例12-1中的交易不是幂等的,可能会发生\$22而不是所需的\$11。因此,尽管例12-1是一个交易原子性的标准例子,但它实际上并不正确,而真正的银行并不像这样工作【3】。 两阶段提交(请参阅第354页上的“原子提交和两阶段提交(2PC)”)协议会破坏TCP连接和事务之间的1:1映射,因为它们必须允许事务协调器在数据库之后重新连接到数据库一个网络故障,并告诉它是否提交或中止有疑问的交易。这足以确保交易只能执行一次吗?不幸的是,即使我们可以抑制数据库客户端和服务器之间的重复事务,我们仍然需要担心最终用户设备和应用程序服务器之间的网络。例如,如果最终用户客户端是Web浏览器,则可能使用HTTP POST请求向服务器提交指令。也许用户处于一个微弱的蜂窝数据连接,他们成功地发送POST,但是信号在他们能够从服务器接收响应之前变得太弱。 -在这种情况下,用户可能会显示错误消息,并且可能会手动重试。 Web浏览器警告说,“你确定要再次提交这个表单吗?” - 用户说是,因为他们希望操作发生。 (Post / Redirect / Get模式[54]可以避免在正常操作中出现此警告消息,但如果POST请求超时,它将无济于事。)从Web服务器的角度来看,重试是一个单独的请求,并且从数据库的角度来看,这是一个单独的事务。通常的重复数据删除机制无济于事。 +在这种情况下,用户可能会显示错误消息,并且可能会手动重试。 Web浏览器警告说,“你确定要再次提交这个表单吗?” - 用户说是,因为他们希望操作发生。 (Post / Redirect / Get模式【54】可以避免在正常操作中出现此警告消息,但如果POST请求超时,它将无济于事。)从Web服务器的角度来看,重试是一个单独的请求,并且从数据库的角度来看,这是一个单独的事务。通常的重复数据删除机制无济于事。 #### 操作标识符 要通过几次网络通信使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的 - 您需要考虑请求的端到端流。 -例如,您可以为操作(例如UUID)生成唯一的标识符,并将其作为隐藏的表单字段包含在客户机应用程序中,或计算所有相关表单字段的散列以衍生操作ID [3]。如果Web浏览器提交两次POST请求,这两个请求将具有相同的操作ID。然后,您可以将该操作ID传递到数据库,并检查您是否只使用给定的ID执行一个操作,如示例12-2中所示。 +例如,您可以为操作(例如UUID)生成唯一的标识符,并将其作为隐藏的表单字段包含在客户机应用程序中,或计算所有相关表单字段的散列以衍生操作ID 【3】。如果Web浏览器提交两次POST请求,这两个请求将具有相同的操作ID。然后,您可以将该操作ID传递到数据库,并检查您是否只使用给定的ID执行一个操作,如示例12-2中所示。 **例12-2 使用唯一的ID来抑制重复的请求** ```sql @@ -493,14 +493,14 @@ COMMIT; **端到端的论点** -抑制重复交易的这种情况只是一个更普遍的原则的一个例子,这个原则被称为端对端的论点,它在1984年由Saltzer,Reed和Clark阐述[55]: +抑制重复交易的这种情况只是一个更普遍的原则的一个例子,这个原则被称为端对端的论点,它在1984年由Saltzer,Reed和Clark阐述【55】: 只有在通信系统端点的应用程序的知识和帮助下,所讨论的功能才能够完全正确地实现。因此,将这种被质疑的功能作为通信系统本身的功能是不可能的。 (有时,通信系统提供的功能的不完整版本可能有助于提高性能。) 在我们的例子中,有问题的函数是重复抑制。我们看到TCP在TCP连接级别抑制重复的数据包,一些流处理器在消息处理级别提供了所谓的唯一的语义,但是这不足以防止用户提交重复请求一次。 TCP,数据库事务和流处理器本身并不能完全排除这些重复。解决这个问题需要一个端到端的解决方案:从最终用户客户端一直传递到数据库的事务标识符。 端到端参数也适用于检查数据的完整性:以太网,TCP和TLS中内置的校验和可以检测网络中数据包的损坏情况,但是它们无法检测到发送和接收软件中的错误网络连接的末端,或数据存储在磁盘上的损坏。如果您想要捕获所有可能的数据损坏源,则还需要端到端的校验和。 -类似的说法适用于加密[55]:家庭WiFi网络上的密码可以防止人们窃听您的WiFi流量,但不会对互联网上其他地方的攻击者进行窥探;您的客户端和服务器之间的TLS / SSL可以得到保护对网络攻击者,而不是对服务器的妥协。只有端到端的加密和认证可以防止所有这些事情。 +类似的说法适用于加密【55】:家庭WiFi网络上的密码可以防止人们窃听您的WiFi流量,但不会对互联网上其他地方的攻击者进行窥探;您的客户端和服务器之间的TLS / SSL可以得到保护对网络攻击者,而不是对服务器的妥协。只有端到端的加密和认证可以防止所有这些事情。 尽管低级功能(TCP复制抑制,以太网校验和,WiFi加密)无法单独提供所需的端到端功能,但它们仍然很有用,因为它们可以降低较高级别出现问题的可能性。例如,如果我们没有TCP将数据包放回正确的顺序,那么HTTP请求通常会被破坏。我们只需要记住,低级别的可靠性功能本身并不足以确保端到端的正确性。 @@ -534,13 +534,13 @@ COMMIT; 唯一性检查可以根据需要唯一的值进行划分。例如,如果需要通过请求标识确保唯一性(如例12-2所示),则可以确保具有相同请求标识的所有请求都路由到同一分区(请参阅第6章)。如果您需要用户名是唯一的,您可以通过用户名的哈希分区。 -但是,排除了异步多主复制,因为它可能会导致不同的主设备同时接受冲突的写操作,因此这些值不再是唯一的(请参阅第295页的“实现可线性化系统”)。如果你想立即拒绝任何违反约束的写入,同步协调是不可避免的[56]。 +但是,排除了异步多主复制,因为它可能会导致不同的主设备同时接受冲突的写操作,因此这些值不再是唯一的(请参阅第295页的“实现可线性化系统”)。如果你想立即拒绝任何违反约束的写入,同步协调是不可避免的【56】。 #### 基于日志的消息传递的唯一性 该日志确保所有消费者以相同的顺序查看消息 - 这种保证在形式上被称为全部命令广播并且等同于共识(参见第346页上的“全部命令广播”)。在使用基于日志的消息传递的非捆绑数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。 -流处理器在单个线程上依次占用日志分区中的所有消息(请参见第448页的“与传统消息传递相比的日志”)。因此,如果日志根据需要唯一的值进行分区,则流处理器可以明确并确定性地确定几个冲突操作中的哪一个先到达。例如,在多个用户尝试声明相同用户名的情况下[57]: +流处理器在单个线程上依次占用日志分区中的所有消息(请参见第448页的“与传统消息传递相比的日志”)。因此,如果日志根据需要唯一的值进行分区,则流处理器可以明确并确定性地确定几个冲突操作中的哪一个先到达。例如,在多个用户尝试声明相同用户名的情况下【57】: 1. 对用户名的每个请求都被编码为一条消息,并附加到由用户名散列确定的分区。 2. 流处理器使用本地数据库连续读取日志中的请求,以跟踪使用哪些用户名。对于每个可用的用户名请求,它都会记录该名称并将成功消息发送到输出流。对于每个已经被使用的用户名请求,它都会向输出流发送拒绝消息。 @@ -548,7 +548,7 @@ COMMIT; 该算法基本上与第363页上的“使用全阶广播实现线性化存储”中的算法相同。它可以通过增加分区数容易地扩展为较大的请求吞吐量,因为可以独立处理每个分区。 -该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如第174页上的“什么是冲突?”和第246页上的“写入偏移和幻影”中所述,冲突的定义可能取决于应用程序,但流处理器可以使用任意逻辑来验证请求。这个想法与Bayou在20世纪90年代开创的方法类似[58]。 +该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如第174页上的“什么是冲突?”和第246页上的“写入偏移和幻影”中所述,冲突的定义可能取决于应用程序,但流处理器可以使用任意逻辑来验证请求。这个想法与Bayou在20世纪90年代开创的方法类似【58】。 #### 多分区请求处理 @@ -590,7 +590,7 @@ CAP定理(参见第359页的“线性化成本”)使用线性化的意义 我要断言,在大多数应用中,完整性比时间要重要得多。违反时效可能令人讨厌和混淆,但是对正直的侵犯可能是灾难性的。 -例如,在您的信用卡对账单上,如果您在过去24小时内完成的交易尚未出现,这并不奇怪 - 这些系统有一定的滞后是正常的。我们知道银行协调和异步结算交易,并且这里的及时性并不重要[3]。但是,如果报表余额不等于交易总和加上先前的报表余额(数额错误),或者交易是向您收取但未支付给商家的话,那将是非常糟糕的(消失的金钱)。这样的问题会违反系统的完整性。 +例如,在您的信用卡对账单上,如果您在过去24小时内完成的交易尚未出现,这并不奇怪 - 这些系统有一定的滞后是正常的。我们知道银行协调和异步结算交易,并且这里的及时性并不重要【3】。但是,如果报表余额不等于交易总和加上先前的报表余额(数额错误),或者交易是向您收取但未支付给商家的话,那将是非常糟糕的(消失的金钱)。这样的问题会违反系统的完整性。 #### 数据流系统的正确性 @@ -617,8 +617,8 @@ ACID事务通常既提供时间性(例如线性化)又提供完整性(例 然而,另一个要认识到的是,许多真正的应用程序实际上可以摆脱唯一性较弱的概念: -* 如果两个人同时注册相同的用户名或预订相同的座位,则可以发送其中一个消息来道歉,并要求他们选择不同的用户名。这种纠正错误的变化被称为补偿性交易[59,60]。 -* 如果客户订购的物品多于仓库中的物品,则可以订购更多库存,为延误向客户道歉,并向他们提供折扣。实际上,如果叉车在仓库中的某些物品上方跑过来,而库存的物品数量比您想象的要少,那么您就必须这样做[61]。因此,无论如何,道歉工作流程已经需要成为业务流程的一部分,因此可能不需要对库存中的项目数量进行线性化约束。 +* 如果两个人同时注册相同的用户名或预订相同的座位,则可以发送其中一个消息来道歉,并要求他们选择不同的用户名。这种纠正错误的变化被称为补偿性交易【59,60】。 +* 如果客户订购的物品多于仓库中的物品,则可以订购更多库存,为延误向客户道歉,并向他们提供折扣。实际上,如果叉车在仓库中的某些物品上方跑过来,而库存的物品数量比您想象的要少,那么您就必须这样做【61】。因此,无论如何,道歉工作流程已经需要成为业务流程的一部分,因此可能不需要对库存中的项目数量进行线性化约束。 * 同样地,许多航空公司预计飞行员会错过飞机,许多旅馆超额预订客房,预计部分客人将取消预订。在这些情况下,出于商业原因故意违反了“每个座位一人”的约束,并且处理补偿过程(退款,升级,在邻近酒店提供免费房间)以处理需求超过供应的情况。即使没有超额预订,为了应对由于恶劣天气而被取消的航班或者罢工的员工,这些问题的恢复仅仅是商业活动的正常组成部分,就需要道歉和赔偿流程。 * 如果有人收取比他们账户中更多的钱,银行可以向他们收取透支费用,并要求他们偿还欠款。通过限制每天的提款总额,银行的风险是有限的。 @@ -635,11 +635,11 @@ ACID事务通常既提供时间性(例如线性化)又提供完整性(例 1. 数据流系统可以保持对衍生数据的完整性保证,而无需原子提交,线性化或同步跨分区协调。 2. 虽然严格的唯一性约束要求及时性和协调性,但许多应用程序实际上可以很好地处理宽松的约束,只要整个过程保持完整性,它们可能会被暂时违反并予以修复。 -总之,这些观察意味着数据流系统可以为许多应用程序提供数据管理服务,而不需要协调,同时仍然提供强大的完整性保证。这种避免协调的数据系统具有很大的吸引力:它们可以比需要执行同步协调的系统获得更好的性能和容错能力[56]。 +总之,这些观察意味着数据流系统可以为许多应用程序提供数据管理服务,而不需要协调,同时仍然提供强大的完整性保证。这种避免协调的数据系统具有很大的吸引力:它们可以比需要执行同步协调的系统获得更好的性能和容错能力【56】。 例如,这种系统可以在多引导器配置中跨多个数据中心进行分布式操作,在区域之间异步复制。任何一个数据中心都可以继续独立运行,因为不需要同步跨区域协调。这样一个系统的时效性保证会很弱 - 如果不引入协调,它就不可能线性化,但它仍然可以提供强有力的完整性保证。 -在这种情况下,序列化事务作为维护衍生状态的一部分仍然是有用的,但它们可以在小范围内运行,在那里它们工作得很好[8]。异构分布式事务(如XA事务)(请参阅“实践中的分布式事务”(第367页))不是必需的。同步协调仍然可以在需要的地方引入(例如,在无法恢复的操作之前执行严格的限制),但是如果只有一个小的协议,则不需要任何东西来支付协调费用应用程序的一部分需要它[43]。 +在这种情况下,序列化事务作为维护衍生状态的一部分仍然是有用的,但它们可以在小范围内运行,在那里它们工作得很好【8】。异构分布式事务(如XA事务)(请参阅“实践中的分布式事务”(第367页))不是必需的。同步协调仍然可以在需要的地方引入(例如,在无法恢复的操作之前执行严格的限制),但是如果只有一个小的协议,则不需要任何东西来支付协调费用应用程序的一部分需要它【43】。 查看协调和约束的另一种方法是:它们减少了由于不一致而必须做出的道歉数量,但也可能会降低系统的性能和可用性,从而可能会增加必须制定的道歉数量中断。您不能将道歉数量减少到零,但您可以根据自己的需求寻找最佳平衡点 - 这是既不存在太多不一致性又不存在太多可用性问题的最佳选择。 @@ -651,17 +651,17 @@ ACID事务通常既提供时间性(例如线性化)又提供完整性(例 我们已经看到,数据可能会在磁盘未触及时破损(请参阅第227页的“复制和耐久性”),并且网络上的数据损坏有时可能会妨碍TCP校验和(请参阅第293页的“虚弱形式的说谎” )。也许这是我们应该更加关注的事情? -我过去曾经使用过的一个应用程序收集了来自客户端的崩溃报告,我们收到的一些报告只能通过在这些设备的内存中随机位翻转来解释。这看起来不太可能,但是如果你有足够的设备来运行你的软件,那么即使发生的事情也不会发生。除了由于硬件故障或辐射导致的随机存储器损坏之外,某些病态存储器访问模式甚至可以在没有故障的存储器中翻转位[62] - 可用于破坏操作系统中安全机制的效应[63]技术被称为rowhammer)。一旦你仔细观察,硬件并不是完美的抽象。 +我过去曾经使用过的一个应用程序收集了来自客户端的崩溃报告,我们收到的一些报告只能通过在这些设备的内存中随机位翻转来解释。这看起来不太可能,但是如果你有足够的设备来运行你的软件,那么即使发生的事情也不会发生。除了由于硬件故障或辐射导致的随机存储器损坏之外,某些病态存储器访问模式甚至可以在没有故障的存储器中翻转位【62】 - 可用于破坏操作系统中安全机制的效应【63】技术被称为rowhammer)。一旦你仔细观察,硬件并不是完美的抽象。 -要清楚的是,随机位翻转在现代硬件上仍然非常罕见[64]。我只想指出,他们并没有超越可能性领域,所以他们值得关注。 +要清楚的是,随机位翻转在现代硬件上仍然非常罕见【64】。我只想指出,他们并没有超越可能性领域,所以他们值得关注。 #### 在软件缺陷面前保持完整性 -除了这些硬件问题之外,总是存在软件错误的风险,这些错误不会被较低级别的网络,内存或文件系统校验和所捕获。即使广泛使用的数据库软件也存在一些问题:即使MySQL和PostgreSQL是健壮且充分的,但我个人发现MySQL的例子未能正确维护唯一性约束[65]和PostgreSQL的序列化隔离级别,表现出写入偏斜异常[66]他们认为这些数据库已经被许多人进行了多年的战斗测试。在不太成熟的软件中,情况可能会更糟糕。 +除了这些硬件问题之外,总是存在软件错误的风险,这些错误不会被较低级别的网络,内存或文件系统校验和所捕获。即使广泛使用的数据库软件也存在一些问题:即使MySQL和PostgreSQL是健壮且充分的,但我个人发现MySQL的例子未能正确维护唯一性约束【65】和PostgreSQL的序列化隔离级别,表现出写入偏斜异常【66】他们认为这些数据库已经被许多人进行了多年的战斗测试。在不太成熟的软件中,情况可能会更糟糕。 尽管在仔细设计,测试和审查方面做出了相当大的努力,但错误仍在蔓延。虽然它们很少,并且最终被发现并修复,但仍然存在一段时期,这些错误可能会破坏数据。 -当涉及到应用程序代码时,我们不得不承担更多的错误,因为大多数应用程序在数据库代码所做的评审和测试的数量上没有接近的地方。许多应用程序甚至没有正确使用数据库提供的用于保存完整性的功能,例如外键或唯一性约束[36]。 +当涉及到应用程序代码时,我们不得不承担更多的错误,因为大多数应用程序在数据库代码所做的评审和测试的数量上没有接近的地方。许多应用程序甚至没有正确使用数据库提供的用于保存完整性的功能,例如外键或唯一性约束【36】。 ACID意义上的一致性(请参阅第224页的“一致性”)基于数据库以一致状态启动并且事务将其从一个一致状态转换为另一个一致状态的想法。因此,我们期望数据库始终处于一致状态。但是,如果您认为该交易没有错误,则这个概念才有意义。如果应用程序错误地使用数据库以某种方式,例如,不安全地使用弱隔离级别,数据库的完整性无法得到保证。 @@ -671,13 +671,13 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 如“不可变事件的优点”一节中所述,审计不仅适用于财务应用程序。然而,可审计性在财务中非常重要,因为每个人都知道错误发生了,而且我们都认识到需要能够检测和解决问题。 -成熟的系统同样倾向于考虑不太可能的事情出错的可能性,并管理这种风险。例如,HDFS和Amazon S3等大规模存储系统不完全信任磁盘:它们运行后台进程,这些后台进程持续读回文件,将其与其他副本进行比较,并将文件从一个磁盘移动到另一个磁盘,以便减轻沉默腐败的风险[67]。 +成熟的系统同样倾向于考虑不太可能的事情出错的可能性,并管理这种风险。例如,HDFS和Amazon S3等大规模存储系统不完全信任磁盘:它们运行后台进程,这些后台进程持续读回文件,将其与其他副本进行比较,并将文件从一个磁盘移动到另一个磁盘,以便减轻沉默腐败的风险【67】。 如果你想确保你的数据仍然存在,你必须真正阅读并检查。大多数时候它仍然会在那里,但如果不是这样,你真的很想早日找到答案。通过同样的说法,尝试从备份中不时恢复是非常重要的,否则只有当它太迟而您已经丢失数据时才会发现备份被破坏。不要盲目地相信它是全部工作。 #### 验证的文化 -像HDFS和S3这样的系统仍然必须假定磁盘大部分时间都能正常工作 - 这是一个合理的假设,但假设它们始终正常工作并不相同。然而,目前还没有多少系统有这种“信任但是验证”的方式来持续审计自己。许多人认为正确性保证是绝对的,并且没有为罕见数据损坏的可能性做出规定。我希望将来我们会看到更多的自我验证或自我审计系统,不断检查自己的诚信,而不是依赖盲目的信任[68]。 +像HDFS和S3这样的系统仍然必须假定磁盘大部分时间都能正常工作 - 这是一个合理的假设,但假设它们始终正常工作并不相同。然而,目前还没有多少系统有这种“信任但是验证”的方式来持续审计自己。许多人认为正确性保证是绝对的,并且没有为罕见数据损坏的可能性做出规定。我希望将来我们会看到更多的自我验证或自我审计系统,不断检查自己的诚信,而不是依赖盲目的信任【68】。 我担心ACID数据库的文化导致我们在盲目信任技术(如交易机制)的基础上开发应用程序,而忽视了这种过程中的任何可审计性。由于我们所信任的技术在大多数情况下工作得很好,审计机制并不值得投资。 @@ -691,7 +691,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 清楚地看到数据流(请参阅第419页上的“批处理输出的原理”)可以使数据的来源更加清晰,从而使完整性检查更加可行。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理和流处理器,以检查是否获得相同的结果,或者甚至并行运行冗余衍生。 -确定性和定义良好的数据流也使调试和跟踪系统的执行变得容易,以确定它为什么做了某些事情[4,69]。如果出现意想不到的事情,那么具有诊断能力来重现导致意外事件的确切环境 - 一种时间行程调试功能是非常有价值的。 +确定性和定义良好的数据流也使调试和跟踪系统的执行变得容易,以确定它为什么做了某些事情【4,69】。如果出现意想不到的事情,那么具有诊断能力来重现导致意外事件的确切环境 - 一种时间行程调试功能是非常有价值的。 #### 再次是端到端的论点 @@ -699,13 +699,13 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 检查数据系统的完整性最好是以端到端的方式完成(请参阅“数据库的端到端争论”页码:51):我们可以在完整性检查中包含的系统越多,那里的机会就越少在这个过程的某个阶段,腐败是被忽视的。如果我们可以检查整个衍生数据管道是端到端正确的,那么沿着路径的任何磁盘,网络,服务和算法都隐含在检查中。 -持续的端到端完整性检查可以提高您对系统正确性的信心,从而使您的移动速度更快[70]。与自动化测试一样,审计增加了发现错误的可能性,从而降低了系统更改或新存储技术造成损害的风险。如果您不害怕进行更改,您可以更好地开发应用程序以满足不断变化的需求。 +持续的端到端完整性检查可以提高您对系统正确性的信心,从而使您的移动速度更快【70】。与自动化测试一样,审计增加了发现错误的可能性,从而降低了系统更改或新存储技术造成损害的风险。如果您不害怕进行更改,您可以更好地开发应用程序以满足不断变化的需求。 #### 审计数据系统的工具 目前,数据系统并不多,这使可审计性成为高层关注的重点。一些应用程序实现自己的审计机制,例如将所有更改记录到单独的审计表中,但是保证审计日志的完整性,并且数据库状态仍然很困难。事务日志可以通过定期使用硬件安全模块对事务日志进行签名来进行防篡改,但这并不能保证正确的事务首先进入日志。 -使用加密工具来证明系统的完整性是十分有趣的,这种方式对于广泛的硬件和软件问题以及甚至是潜在的恶意行为都很有效。加密货币,区块链和分布式账本技术,如比特币,以太坊,波纹,恒星等等[71,72,73]已经出现在这个领域。 +使用加密工具来证明系统的完整性是十分有趣的,这种方式对于广泛的硬件和软件问题以及甚至是潜在的恶意行为都很有效。加密货币,区块链和分布式账本技术,如比特币,以太坊,波纹,恒星等等【71,72,73】已经出现在这个领域。 我没有资格评论这些技术作为商定合同的货币或机制的优点。但是,从数据系统的角度来看,它们包含了一些有趣的想法。实质上,它们是分布式数据库,具有数据模型和事务机制,不同副本可以由互不信任的组织托管。复制品不断检查彼此的完整性,并使用共识协议来约定应执行的交易。 @@ -725,47 +725,47 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 我们将数据作为一个抽象的东西来讨论,但请记住,许多数据集都是关于人的:他们的行为,他们的兴趣,他们的身份。我们必须以人性和尊重来对待这些数据。用户也是人类,人的尊严是至高无上的。 -软件开发日益涉及重要的道德选择。有一些指导原则可以帮助软件工程师解决这些问题,例如ACM的软件工程道德规范和专业实践[77],但很少在实践中讨论,应用和实施。因此,工程师和产品经理有时会对隐私和产品潜在的负面后果采取非常傲慢的态度[78,79,80]。 +软件开发日益涉及重要的道德选择。有一些指导原则可以帮助软件工程师解决这些问题,例如ACM的软件工程道德规范和专业实践【77】,但很少在实践中讨论,应用和实施。因此,工程师和产品经理有时会对隐私和产品潜在的负面后果采取非常傲慢的态度【78,79,80】。 技术本身并不好或不好,重要的是如何使用它以及它如何影响人。对于像搜索引擎这样的软件系统来说,就像使用像武器这样的武器非常相似。我认为,软件工程师仅仅专注于技术并忽视其后果是不够的:伦理责任也是我们的责任。有关道德的推理是困难的,但忽视这一点太重要了。 ### 预测分析 -例如,预测分析是“大数据”炒作的主要部分。使用数据分析预测天气或疾病传播是一回事[81];另一个问题是要预测一个罪犯是否可能再犯罪,一个贷款申请人是否有可能违约,或者一个保险客户是否有可能做出昂贵的索赔。后者直接影响到个人的生活。当然,支付网络希望防止欺诈交易,银行希望避免不良贷款,航空公司希望避免劫持,公司希望避免激怒无效或不可靠的人。从他们的角度来看,错过商业机会的成本很低,但不良贷款或有问题的员工的成本要高得多,因此组织希望保持谨慎是很自然的。如果有疑问,他们最好不要说。 +例如,预测分析是“大数据”炒作的主要部分。使用数据分析预测天气或疾病传播是一回事【81】;另一个问题是要预测一个罪犯是否可能再犯罪,一个贷款申请人是否有可能违约,或者一个保险客户是否有可能做出昂贵的索赔。后者直接影响到个人的生活。当然,支付网络希望防止欺诈交易,银行希望避免不良贷款,航空公司希望避免劫持,公司希望避免激怒无效或不可靠的人。从他们的角度来看,错过商业机会的成本很低,但不良贷款或有问题的员工的成本要高得多,因此组织希望保持谨慎是很自然的。如果有疑问,他们最好不要说。 -然而,随着算法决策变得越来越普遍,某人(准确地或错误地)被某种算法标记为有风险,可能会遭受大量“不”决定。系统地排除工作,航空旅行,保险,物业租赁,金融服务和社会其他关键方面是个人自由的巨大制约因素,因此被称为“算法监狱”[82]。在尊重人权的国家,刑事司法系统在被证明有罪之前就认定无罪。另一方面,自动化系统可以有系统地,任意地排除一个人参与社会活动,而没有任何内疚的证据,而且几乎没有上诉的机会。 +然而,随着算法决策变得越来越普遍,某人(准确地或错误地)被某种算法标记为有风险,可能会遭受大量“不”决定。系统地排除工作,航空旅行,保险,物业租赁,金融服务和社会其他关键方面是个人自由的巨大制约因素,因此被称为“算法监狱”【82】。在尊重人权的国家,刑事司法系统在被证明有罪之前就认定无罪。另一方面,自动化系统可以有系统地,任意地排除一个人参与社会活动,而没有任何内疚的证据,而且几乎没有上诉的机会。 #### 偏见和歧视 -算法做出的决定不一定比人类做出的更好或更差。每个人都可能有偏见,即使他们积极地试图抵消它们,歧视性做法也可能在文化上被制度化。人们希望根据数据做出决定,而不是通过人们的主观和现场评估来更加公平,给那些在传统系统中经常被忽视的人更好的机会[83]。 +算法做出的决定不一定比人类做出的更好或更差。每个人都可能有偏见,即使他们积极地试图抵消它们,歧视性做法也可能在文化上被制度化。人们希望根据数据做出决定,而不是通过人们的主观和现场评估来更加公平,给那些在传统系统中经常被忽视的人更好的机会【83】。 -当我们开发预测分析系统时,我们不仅仅是通过使用软件来指定什么时候说是或否的规则来自动化人的决定;我们甚至将规则本身从数据中推断出来。但是,这些系统学到的模式是不透明的:即使数据中存在一些相关性,我们也可能不知道为什么。如果在算法输入中存在系统偏差,系统很可能会在输出中学习和放大偏差[84]。 +当我们开发预测分析系统时,我们不仅仅是通过使用软件来指定什么时候说是或否的规则来自动化人的决定;我们甚至将规则本身从数据中推断出来。但是,这些系统学到的模式是不透明的:即使数据中存在一些相关性,我们也可能不知道为什么。如果在算法输入中存在系统偏差,系统很可能会在输出中学习和放大偏差【84】。 -在许多国家,反歧视法律禁止根据种族,年龄,性别,性别,残疾或信仰等受保护特征对待不同的人。一个人的数据的其他特征可能会被分析,但是如果他们与受保护的特征相关会发生什么?例如,在种族隔离的邻居中,一个人的邮政编码,甚至是他们的IP地址,都是一个强烈的种族预测。这样说,相信一种算法可以以某种方式将偏倚的数据作为输入并产生公平和公正的输出[85],这似乎是荒谬的。然而,这种观点似乎常常被数据驱动型决策制定的支持者所暗示,这种态度被讽刺为“机器学习就像洗钱对于偏见”[86]。 +在许多国家,反歧视法律禁止根据种族,年龄,性别,性别,残疾或信仰等受保护特征对待不同的人。一个人的数据的其他特征可能会被分析,但是如果他们与受保护的特征相关会发生什么?例如,在种族隔离的邻居中,一个人的邮政编码,甚至是他们的IP地址,都是一个强烈的种族预测。这样说,相信一种算法可以以某种方式将偏倚的数据作为输入并产生公平和公正的输出【85】,这似乎是荒谬的。然而,这种观点似乎常常被数据驱动型决策制定的支持者所暗示,这种态度被讽刺为“机器学习就像洗钱对于偏见”【86】。 -预测分析系统只是从过去推断出来的;如果过去是歧视性的,他们就会把这种歧视归为法律。如果我们希望未来比过去更好,那么就需要道德想象力,而这只有人类才能提供[87]。数据和模型应该是我们的工具,而不是我们的主人。 +预测分析系统只是从过去推断出来的;如果过去是歧视性的,他们就会把这种歧视归为法律。如果我们希望未来比过去更好,那么就需要道德想象力,而这只有人类才能提供【87】。数据和模型应该是我们的工具,而不是我们的主人。 #### 责任和问责 -自动决策开启了责任和问责的问题[87]。如果一个人犯了错误,他们可以被追究责任,受决定影响的人可以上诉。算法也会犯错误,但是如果他们出错,谁会犯错误[88]?当一辆自驾车引发事故时,谁负责?如果自动信用评分算法系统地区分特定种族或宗教的人,是否有任何追索权?如果机器学习系统的决定受到司法审查,您能向法官解释算法是如何做出决定的吗? +自动决策开启了责任和问责的问题【87】。如果一个人犯了错误,他们可以被追究责任,受决定影响的人可以上诉。算法也会犯错误,但是如果他们出错,谁会犯错误【88】?当一辆自驾车引发事故时,谁负责?如果自动信用评分算法系统地区分特定种族或宗教的人,是否有任何追索权?如果机器学习系统的决定受到司法审查,您能向法官解释算法是如何做出决定的吗? -信用评级机构是收集数据来做出人们决策的一个老例子。不良信用评分会使生活变得困难,但至少一个信用评分通常是基于一个人的实际借款历史的相关事实,并且记录中的任何错误都可以得到纠正(尽管机构通常不会这么容易)。然而,基于机器学习的评分算法通常使用更广泛的输入范围,并且更加不透明,使得难以理解特定决策是如何发生的以及是否有人正在以不公平或歧视的方式被对待[89]。 +信用评级机构是收集数据来做出人们决策的一个老例子。不良信用评分会使生活变得困难,但至少一个信用评分通常是基于一个人的实际借款历史的相关事实,并且记录中的任何错误都可以得到纠正(尽管机构通常不会这么容易)。然而,基于机器学习的评分算法通常使用更广泛的输入范围,并且更加不透明,使得难以理解特定决策是如何发生的以及是否有人正在以不公平或歧视的方式被对待【89】。 -信用评分总结了“你过去的表现如何?”,而预测分析通常是基于“谁与你类似,以及过去人们喜欢你的行为如何?”。与他人行为相似的绘图意味着刻板印象人们,例如根据他们居住的地方(一个关于种族和社会经济阶层的密切代理人)。那些放错人的人呢?而且,如果由于错误的数据而做出的决定是不正确的,则追索权几乎是不可能的[87]。 +信用评分总结了“你过去的表现如何?”,而预测分析通常是基于“谁与你类似,以及过去人们喜欢你的行为如何?”。与他人行为相似的绘图意味着刻板印象人们,例如根据他们居住的地方(一个关于种族和社会经济阶层的密切代理人)。那些放错人的人呢?而且,如果由于错误的数据而做出的决定是不正确的,则追索权几乎是不可能的【87】。 很多数据本质上是统计的,这意味着即使概率分布总体上是正确的,个别情况也可能是错的。例如,如果贵国的平均寿命是80岁,那么这并不意味着在80岁生日时就会死亡。从平均分布和概率分布来看,你不能说很多关于某个特定人的生活年龄。同样,预测系统的输出是概率性的,在个别情况下可能是错误的。 盲目相信数据至高无上的决策不仅是妄想,它是非常危险的。随着数据驱动的决策变得越来越普遍,我们需要弄清楚如何使算法负责任和透明,如何避免加强现有的偏见,以及如何在不可避免的错误时加以修复。 -我们还需要弄清楚如何防止数据被用来伤害人,并且实现其正向潜力。例如,分析可以揭示人们生活的财务和社会特征。一方面,这种权力可以用来把援助和支持集中在帮助那些最需要援助的人身上。另一方面,它有时被掠夺性企业用于识别弱势群体并向高风险产品销售,如高成本贷款和无价值的大学学位[87,90]。 +我们还需要弄清楚如何防止数据被用来伤害人,并且实现其正向潜力。例如,分析可以揭示人们生活的财务和社会特征。一方面,这种权力可以用来把援助和支持集中在帮助那些最需要援助的人身上。另一方面,它有时被掠夺性企业用于识别弱势群体并向高风险产品销售,如高成本贷款和无价值的大学学位【87,90】。 #### 反馈回路 -即使预测性应用程序对人们的影响较小,比如推荐系统,也存在难以解决的问题。当服务善于预测用户想要看到什么内容时,他们最终可能只会向人们展示他们已经同意的观点,导致产生陈腔滥调,错误信息和极化可能滋生的回声室。我们已经看到社交媒体呼应室对竞选活动的影响[91]。 +即使预测性应用程序对人们的影响较小,比如推荐系统,也存在难以解决的问题。当服务善于预测用户想要看到什么内容时,他们最终可能只会向人们展示他们已经同意的观点,导致产生陈腔滥调,错误信息和极化可能滋生的回声室。我们已经看到社交媒体呼应室对竞选活动的影响【91】。 -当预测分析影响人们的生活时,特别是由于自我强化反馈循环而出现有害问题。例如,考虑雇主使用信用评分来评估潜在员工的情况。你可能是一个信誉好的好员工,但是由于你不能控制的不幸,你会突然发现自己陷入财务困境。由于您错过了账单付款,您的信用评分会受到影响,您将不太可能找到工作。失业使你陷入贫困,这进一步恶化了你的分数,使其更难找到工作[87]。由于有毒的假设,这是一个下降的螺旋,隐藏在数学严谨和数据伪装的背后。 +当预测分析影响人们的生活时,特别是由于自我强化反馈循环而出现有害问题。例如,考虑雇主使用信用评分来评估潜在员工的情况。你可能是一个信誉好的好员工,但是由于你不能控制的不幸,你会突然发现自己陷入财务困境。由于您错过了账单付款,您的信用评分会受到影响,您将不太可能找到工作。失业使你陷入贫困,这进一步恶化了你的分数,使其更难找到工作【87】。由于有毒的假设,这是一个下降的螺旋,隐藏在数学严谨和数据伪装的背后。 -我们不能总是预测这种反馈循环何时发生。然而,通过考虑整个系统(不仅仅是计算机化的部分,而且还有与之互动的人),可以预测许多后果 - 一种称为系统思维的方法[92]。我们可以尝试理解数据分析系统如何响应不同的行为,结构或特性。该系统是否加强和扩大了人们之间现有的差异(例如,使富人更穷或更穷),还是试图打击不公正?即使有最好的意图,我们也必须小心意想不到的后果。 +我们不能总是预测这种反馈循环何时发生。然而,通过考虑整个系统(不仅仅是计算机化的部分,而且还有与之互动的人),可以预测许多后果 - 一种称为系统思维的方法【92】。我们可以尝试理解数据分析系统如何响应不同的行为,结构或特性。该系统是否加强和扩大了人们之间现有的差异(例如,使富人更穷或更穷),还是试图打击不公正?即使有最好的意图,我们也必须小心意想不到的后果。 #### 隐私和跟踪 @@ -781,24 +781,24 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 #### 监控 -作为一个思想实验,尝试用监视来替换单词数据,并观察常用短语是否听起来如此好[93]。这样如何:“在我们的监控驱动的组织中,我们收集实时监控流并将它们存储在我们的监控仓库中。我们的监控科学家使用高级分析和监测处理来获得新的见解。“ +作为一个思想实验,尝试用监视来替换单词数据,并观察常用短语是否听起来如此好【93】。这样如何:“在我们的监控驱动的组织中,我们收集实时监控流并将它们存储在我们的监控仓库中。我们的监控科学家使用高级分析和监测处理来获得新的见解。“ -这个思想实验对于本书“设计监控 - 密集型应用程序”来说是异乎寻常的争论,但我认为需要强调的话来强调这一点。在我们制作软件“吃世界”的尝试中[94],我们已经建立了世界上迄今为止所见过的最伟大的大众监视基础设施。我们正朝着物联网迈进,我们正在迅速接近这样一个世界:每个有人居住的空间至少包含一个互联网连接的麦克风,以智能手机,智能电视,语音控制助理设备,婴儿监视器甚至儿童玩具使用基于云的语音识别。这些设备中的很多都有可怕的安全记录[95]。 +这个思想实验对于本书“设计监控 - 密集型应用程序”来说是异乎寻常的争论,但我认为需要强调的话来强调这一点。在我们制作软件“吃世界”的尝试中【94】,我们已经建立了世界上迄今为止所见过的最伟大的大众监视基础设施。我们正朝着物联网迈进,我们正在迅速接近这样一个世界:每个有人居住的空间至少包含一个互联网连接的麦克风,以智能手机,智能电视,语音控制助理设备,婴儿监视器甚至儿童玩具使用基于云的语音识别。这些设备中的很多都有可怕的安全记录【95】。 -即使是极权主义和专制政权也只能梦想在每个房间放置一个麦克风,并强迫每个人不断地携带能够追踪其位置和动作的设备。然而,我们显然自愿地,甚至热心地投身于这个全面监视的世界。不同之处在于数据是由公司而不是由政府机构收集的[96]。 +即使是极权主义和专制政权也只能梦想在每个房间放置一个麦克风,并强迫每个人不断地携带能够追踪其位置和动作的设备。然而,我们显然自愿地,甚至热心地投身于这个全面监视的世界。不同之处在于数据是由公司而不是由政府机构收集的【96】。 -并不是所有的数据收集都必须符合监督的要求,但检查它们可以帮助我们理解我们与数据收集者的关系。为什么我们似乎很乐意接受企业的监督?也许你觉得你没有隐瞒 - 换句话说,你完全符合现有的权力结构,你不是被边缘化的少数派,你不必害怕迫害[97]。不是每个人都如此幸运。也许这是因为目的似乎是良性的 - 这不是强制性的,也不是强制性的,而只是更好的建议和更个性化的营销。但是,结合上一节中对预测分析的讨论,这种区分似乎不太清晰。 +并不是所有的数据收集都必须符合监督的要求,但检查它们可以帮助我们理解我们与数据收集者的关系。为什么我们似乎很乐意接受企业的监督?也许你觉得你没有隐瞒 - 换句话说,你完全符合现有的权力结构,你不是被边缘化的少数派,你不必害怕迫害【97】。不是每个人都如此幸运。也许这是因为目的似乎是良性的 - 这不是强制性的,也不是强制性的,而只是更好的建议和更个性化的营销。但是,结合上一节中对预测分析的讨论,这种区分似乎不太清晰。 -我们已经看到与汽车追踪设备挂钩的汽车保险费以及取决于佩戴健身追踪设备的人的健康保险范围。当监视被用来确定在生活的重要方面如保险或就业等方面的东西时,它开始变得不那么温和。此外,数据分析可以揭示出令人惊讶的侵入性事物:例如,智能手表或健身追踪器中的运动传感器可用于以相当好的准确度计算出您正在输入的内容(例如密码)[98]。而分析算法只会变得更好。 +我们已经看到与汽车追踪设备挂钩的汽车保险费以及取决于佩戴健身追踪设备的人的健康保险范围。当监视被用来确定在生活的重要方面如保险或就业等方面的东西时,它开始变得不那么温和。此外,数据分析可以揭示出令人惊讶的侵入性事物:例如,智能手表或健身追踪器中的运动传感器可用于以相当好的准确度计算出您正在输入的内容(例如密码)【98】。而分析算法只会变得更好。 #### 同意和选择的自由 我们可能会断言用户自愿选择使用跟踪其活动的服务,并且他们已同意服务条款和隐私政策,因此他们同意收集数据。我们甚至可以声称,用户正在接受有价值的服务,以换取所提供的数据,并且为了提供服务,跟踪是必要的。毫无疑问,社交网络,搜索引擎以及其他各种免费的在线服务对于用户来说都是有价值的,但是这个说法存在问题。 用户几乎不知道他们提供给我们的数据库的数据,或者它们如何保留和处理 - 而大多数隐私政策的作用更多的是模糊而不是照亮。如果不了解他们的数据会发生什么,用户无法给出任何有意义的同意。通常,来自一个用户的数据还说明关于不是该服务的用户并且没有同意任何条款的其他人的事情。我们在本书的这一部分讨论的衍生数据集(来自整个用户群的数据可能与行为跟踪和外部数据源相结合)恰恰是用户无法获得任何有意义理解的数据种类。 -而且,数据是通过单向过程从用户中提取出来的,而不是真正的互惠关系,而不是公平的价值交换。没有对话,用户无法选择他们提供的数据量以及他们收到的服务回报:服务和用户之间的关系是非常不对称和片面的。这些条款是由服务设置,而不是由用户[99]。 +而且,数据是通过单向过程从用户中提取出来的,而不是真正的互惠关系,而不是公平的价值交换。没有对话,用户无法选择他们提供的数据量以及他们收到的服务回报:服务和用户之间的关系是非常不对称和片面的。这些条款是由服务设置,而不是由用户【99】。 -对于不同意监视的用户,唯一真正的选择就是不使用服务。但是这个选择也不是免费的:如果一项服务如此受欢迎以至于“被大多数人认为是基本社会参与的必要条件”[99],那么指望人们选择退出这项服务是不合理的 - 使用它事实上是强制性的。例如,在大多数西方社会群体中,携带智能手机,使用Facebook进行社交以及使用Google查找信息已成为常态。特别是当一项服务具有网络效应时,人们选择不使用它会产生社会成本。 +对于不同意监视的用户,唯一真正的选择就是不使用服务。但是这个选择也不是免费的:如果一项服务如此受欢迎以至于“被大多数人认为是基本社会参与的必要条件”【99】,那么指望人们选择退出这项服务是不合理的 - 使用它事实上是强制性的。例如,在大多数西方社会群体中,携带智能手机,使用Facebook进行社交以及使用Google查找信息已成为常态。特别是当一项服务具有网络效应时,人们选择不使用它会产生社会成本。 由于跟踪用户而拒绝使用服务,这只是少数拥有足够的时间和知识来了解其隐私政策的人员的一种选择,并且有可能错过社会参与或专业人士如果他们参与了服务,可能会出现机会。对于处境不太好的人来说,没有任何意义上的自由选择:监督变得不可避免。 @@ -806,7 +806,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 有时候,人们声称“隐私已经死了”,理由是有些用户愿意把各种有关他们生活的事情发布到社交媒体上,有时是通常的,有时是个人的。但是,这种说法是错误的,并且依赖于对隐私一词的错误理解。 -拥有隐私并不意味着保密一切;它意味着要有自由选择要向谁揭示哪些事情,要公开什么,以及要保密什么。隐私权是一项决定权:它可以让每个人决定他们希望在每个场合保密和透明度之间的区域[99]。这是一个人的自由和自主的重要方面。 +拥有隐私并不意味着保密一切;它意味着要有自由选择要向谁揭示哪些事情,要公开什么,以及要保密什么。隐私权是一项决定权:它可以让每个人决定他们希望在每个场合保密和透明度之间的区域【99】。这是一个人的自由和自主的重要方面。 当通过监控基础设施从人身上提取数据时,隐私权不一定被侵蚀,而是转移到数据收集器。获取数据的公司本质上是说“相信我们用你的数据做正确的事情”,这意味着决定要透露什么和保密的权利是从个人转移到公司的。 @@ -814,35 +814,35 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 即使特定用户无法从特定广告定位的人群中个人身份识别,他们已经失去了一些关于披露一些隐私信息的机构,例如他们是否患有某种疾病。决定根据个人喜好向谁透露什么的不是用户,而是公司以最大化利润为目标行使隐私权。 -许多公司都有一个不被视为令人毛骨悚然的目标 - 避免了他们的数据收集的实际侵入性问题,而是专注于管理用户感知。即使这些看法经常被糟糕的管理:例如,事实可能是事实上的正确,但如果它触发痛苦的回忆,用户可能不希望被提醒它[100]。对于任何类型的数据,我们都应该期望在某种程度上出现错误,不可取或不适当的可能性,我们需要建立处理这些失败的机制。无论是“不可取的”还是“不适当的”,当然都是由人的判断决定的;除非我们明确地规划它们尊重人类的需求,否则算法会忽略这些概念。作为这些系统的工程师,我们必须谦虚,接受和规划这些失败。 +许多公司都有一个不被视为令人毛骨悚然的目标 - 避免了他们的数据收集的实际侵入性问题,而是专注于管理用户感知。即使这些看法经常被糟糕的管理:例如,事实可能是事实上的正确,但如果它触发痛苦的回忆,用户可能不希望被提醒它【100】。对于任何类型的数据,我们都应该期望在某种程度上出现错误,不可取或不适当的可能性,我们需要建立处理这些失败的机制。无论是“不可取的”还是“不适当的”,当然都是由人的判断决定的;除非我们明确地规划它们尊重人类的需求,否则算法会忽略这些概念。作为这些系统的工程师,我们必须谦虚,接受和规划这些失败。 允许在线服务的用户控制其他用户可以看到的数据的哪些方面的隐私设置是将一些控制交还给用户的起点。但是,无论设置如何,服务本身仍然可以不受限制地访问数据,并且可以以隐私策略允许的任何方式自由使用它。即使服务承诺不会将数据出售给第三方,它通常会授予自己不受限制的权利,以在内部处理和分析数据,而且往往比用户明显看到的要多得多。 -这种从个人到公司的大规模隐私权转移历史上是史无前例的[99]。监控一直存在,但它过去是昂贵和手动的,不可扩展和自动化。信托关系一直存在,例如患者与其医生之间,或被告与其律师之间 - 但在这些情况下,数据的使用严格受到道德,法律和监管限制的约束。互联网服务使得在没有有意义的同意的情况下收集大量敏感信息变得容易得多,并且在没有用户理解他们的私人数据正在发生的情况下大规模使用它。 +这种从个人到公司的大规模隐私权转移历史上是史无前例的【99】。监控一直存在,但它过去是昂贵和手动的,不可扩展和自动化。信托关系一直存在,例如患者与其医生之间,或被告与其律师之间 - 但在这些情况下,数据的使用严格受到道德,法律和监管限制的约束。互联网服务使得在没有有意义的同意的情况下收集大量敏感信息变得容易得多,并且在没有用户理解他们的私人数据正在发生的情况下大规模使用它。 #### 数据作为资产和权力 由于行为数据是用户与服务交互的副产品,因此有时称为“数据耗尽” - 表明数据是毫无价值的浪费材料。从这个角度来看,行为和预测分析可以被看作是一种从数据中提取价值的回收形式,否则这些数据会被抛弃。 -更准确的说是反过来看:从经济的角度来看,如果有针对性的广告是为服务付费的话,那么关于人的行为数据就是服务的核心资产。在这种情况下,用户与之交互的应用程序仅仅是一种诱骗用户将更多的个人信息提供给监控基础设施的手段[99]。数据提取机器人讥讽地发现,在线服务中常常表现出令人愉快的人类创造力和社交关系。 +更准确的说是反过来看:从经济的角度来看,如果有针对性的广告是为服务付费的话,那么关于人的行为数据就是服务的核心资产。在这种情况下,用户与之交互的应用程序仅仅是一种诱骗用户将更多的个人信息提供给监控基础设施的手段【99】。数据提取机器人讥讽地发现,在线服务中常常表现出令人愉快的人类创造力和社交关系。 个人数据是宝贵资产的说法得到了数据中介的支持,这个数据中介是一个隐蔽的行业,它主要是为了市场目的而采购,收集,分析,推断和转售侵入性个人数据[ 90。初创公司通过他们的用户数量,通过“眼球”,即通过他们的监视能力来估价。 -因为数据很有价值,所以很多人都想要它。当然,公司需要它 - 这就是为什么他们收集它的原因。但政府也想获得它:通过秘密交易,胁迫,法律强制或者只是偷窃[101]。当公司破产时,收集到的个人资料就是被出售的资产之一。而且,数据难以确保,因此违规事件经常令人不安地发生[102]。 +因为数据很有价值,所以很多人都想要它。当然,公司需要它 - 这就是为什么他们收集它的原因。但政府也想获得它:通过秘密交易,胁迫,法律强制或者只是偷窃【101】。当公司破产时,收集到的个人资料就是被出售的资产之一。而且,数据难以确保,因此违规事件经常令人不安地发生【102】。 -这些观察已经导致批评者说数据不仅仅是一种资产,而是一种“有毒资产”[101],或者至少是“有害物质”[103]。即使我们认为我们有能力防止滥用数据,但是每当我们收集数据时,我们都需要平衡这些好处和落入他们手中的风险:计算机系统可能会被犯罪分子或敌对的外国情报服务,数据可能会被内部人士泄露,公司可能会落入不合法的管理层之中,而这些管理层不会分享我们的价值观,或者这个国家可能会被毫无疑问迫使我们交出数据的政权所接管。 +这些观察已经导致批评者说数据不仅仅是一种资产,而是一种“有毒资产”【101】,或者至少是“有害物质”【103】。即使我们认为我们有能力防止滥用数据,但是每当我们收集数据时,我们都需要平衡这些好处和落入他们手中的风险:计算机系统可能会被犯罪分子或敌对的外国情报服务,数据可能会被内部人士泄露,公司可能会落入不合法的管理层之中,而这些管理层不会分享我们的价值观,或者这个国家可能会被毫无疑问迫使我们交出数据的政权所接管。 -正如古老的格言所言,“知识就是力量”。此外,“在避免审查自己的同时审视他人是最重要的权力形式之一”[105]。这就是为什么极权政府希望监督:这让他们有能力控制人口。尽管今天的科技公司并没有公开地寻求政治权力,但是他们积累的数据和知识却给他们带来了很多权力,其中大部分是在公共监督之外偷偷摸摸的[106]。 +正如古老的格言所言,“知识就是力量”。此外,“在避免审查自己的同时审视他人是最重要的权力形式之一”【105】。这就是为什么极权政府希望监督:这让他们有能力控制人口。尽管今天的科技公司并没有公开地寻求政治权力,但是他们积累的数据和知识却给他们带来了很多权力,其中大部分是在公共监督之外偷偷摸摸的【106】。 #### 记住工业革命 -数据是信息时代的决定性特征。互联网,数据存储,处理和软件驱动的自动化正在对全球经济和人类社会产生重大影响。随着我们的日常生活和社会组织在过去的十年中发生了变化,并且在未来的十年中可能会继续发生根本性的变化,所以我们就会想到与工业革命的比较[87,96]。 +数据是信息时代的决定性特征。互联网,数据存储,处理和软件驱动的自动化正在对全球经济和人类社会产生重大影响。随着我们的日常生活和社会组织在过去的十年中发生了变化,并且在未来的十年中可能会继续发生根本性的变化,所以我们就会想到与工业革命的比较【87,96】。 工业革命是通过重大的技术和农业进步来实现的,它带来了持续的经济增长,长期的生活水平显着提高。然而,它也带来了一些重大问题:空气污染(由于烟雾和化学过程)以及水(工业和人类的废物)造成的污染是可怕的。工厂老板生活在辉煌中,而城市工人经常住在非常贫困的住房里,并且在恶劣的条件下长时间工作。童工是常见的,包括在矿场工作的危险和低薪。 -花了很长时间才制定了保护措施,例如环境保护条例,工作场所安全协议,禁止使用童工和食品卫生检查。毫无疑问,生产成本增加了,因为事实再也不能把废物倒入河流,销售污染的食物或者剥削工人。但是,整个社会都从中受益匪浅,我们中很少人会想要回到这些规定之前的时间[87]。 +花了很长时间才制定了保护措施,例如环境保护条例,工作场所安全协议,禁止使用童工和食品卫生检查。毫无疑问,生产成本增加了,因为事实再也不能把废物倒入河流,销售污染的食物或者剥削工人。但是,整个社会都从中受益匪浅,我们中很少人会想要回到这些规定之前的时间【87】。 -就像工业革命有一个黑暗的一面需要管理一样,我们对信息时代的转变也有我们需要面对和解决的重大问题。我相信收集和使用数据是其中的一个问题。用布鲁斯·施奈尔(Bruce Schneier)[96]的话来说: +就像工业革命有一个黑暗的一面需要管理一样,我们对信息时代的转变也有我们需要面对和解决的重大问题。我相信收集和使用数据是其中的一个问题。用布鲁斯·施奈尔(Bruce Schneier)【96】的话来说: > 数据是信息时代的污染问题,保护隐私是环境挑战。几乎所有的电脑都能生成信息。它停留在周围,溃烂。我们如何处理它 - 我们如何控制它以及如何处理它 - 对我们信息经济的健康至关重要。正如我们今天回顾工业时代的早期十年,并想知道我们的祖先在匆忙建立一个工业世界的过程中如何忽略污染,我们的孙辈在信息时代的前几十年将回顾我们,就我们如何应对数据收集和滥用的挑战来判断我们。 > @@ -850,17 +850,17 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 #### 立法和自律 -数据保护法可能有助于维护个人的权利。例如,1995年的“欧洲数据保护指令”规定,个人数据必须“为特定的,明确的和合法的目的收集,而不是以与这些目的不相符的方式进一步处理”,并且数据必须“充分,与收集目的相关并不过分“[107]。 +数据保护法可能有助于维护个人的权利。例如,1995年的“欧洲数据保护指令”规定,个人数据必须“为特定的,明确的和合法的目的收集,而不是以与这些目的不相符的方式进一步处理”,并且数据必须“充分,与收集目的相关并不过分“【107】。 -但是,这个立法在今天的互联网环境下是否有效还是有疑问的[108]。这些规则直接违背了大数据的理念,即最大限度地收集数据,将其与其他数据集结合起来进行试验和探索,以便产生新的见解。探索意味着将数据用于未预期的目的,这与用户同意的“特定和明确”目的相反(如果我们可以有意义地谈论同意的话)[109]。目前正在制定更新的规定[89]。 +但是,这个立法在今天的互联网环境下是否有效还是有疑问的【108】。这些规则直接违背了大数据的理念,即最大限度地收集数据,将其与其他数据集结合起来进行试验和探索,以便产生新的见解。探索意味着将数据用于未预期的目的,这与用户同意的“特定和明确”目的相反(如果我们可以有意义地谈论同意的话)【109】。目前正在制定更新的规定【89】。 -收集大量有关人员数据的公司反对监管,认为这是创新的负担和阻碍。在某种程度上,这种反对是有道理的。例如,分享医疗数据时,隐私存在明显风险,但也有潜在机会:如果数据分析能够帮助我们实现更好的诊断或找到更好的治疗方法,可以预防多少人死亡[110]?过度监管可能会阻止这种突破。这种潜在机会与风险之间难以平衡[105]。 +收集大量有关人员数据的公司反对监管,认为这是创新的负担和阻碍。在某种程度上,这种反对是有道理的。例如,分享医疗数据时,隐私存在明显风险,但也有潜在机会:如果数据分析能够帮助我们实现更好的诊断或找到更好的治疗方法,可以预防多少人死亡【110】?过度监管可能会阻止这种突破。这种潜在机会与风险之间难以平衡【105】。 -从根本上说,我认为我们需要在个人数据方面进行科技行业的文化转变。我们应该停止将用户作为度量标准进行优化,并记住他们是值得尊重,尊严和代理的人。我们应该自我调节我们的数据收集和处理实践,以建立和维持依赖我们软件的人们的信任[111]。我们应该自己去教育最终用户如何使用他们的数据,而不是让他们处于黑暗中。 +从根本上说,我认为我们需要在个人数据方面进行科技行业的文化转变。我们应该停止将用户作为度量标准进行优化,并记住他们是值得尊重,尊严和代理的人。我们应该自我调节我们的数据收集和处理实践,以建立和维持依赖我们软件的人们的信任【111】。我们应该自己去教育最终用户如何使用他们的数据,而不是让他们处于黑暗中。 我们应该允许每个人保持他们的隐私 - 即他们控制自己的数据 - 而不是通过监视来窃取他们的控制权。我们控制数据的个人权利就像是一个国家公园的自然环境:如果我们没有明确的保护和关心,它将被破坏。这将是公地的悲剧,我们都会因此而变得更糟。无所不在的监视不是不可避免的,我们仍然能够阻止它。 -我们究竟能做到这一点是一个悬而未决的问题。首先,我们不应该永久保留数据,但一旦不再需要就立即清除数据[111,112]。清除数据与不变性的想法背道而驰(请参阅第463页的“不变性的限制”),但可以解决该问题。我所看到的一种很有前途的方法是通过加密协议来实施访问控制,而不仅仅是通过策略[113,114]。总的来说,文化和态度的变化是必要的。 +我们究竟能做到这一点是一个悬而未决的问题。首先,我们不应该永久保留数据,但一旦不再需要就立即清除数据【111,112】。清除数据与不变性的想法背道而驰(请参阅第463页的“不变性的限制”),但可以解决该问题。我所看到的一种很有前途的方法是通过加密协议来实施访问控制,而不仅仅是通过策略【113,114】。总的来说,文化和态度的变化是必要的。 ## 本章小结 diff --git a/ch3.md b/ch3.md index 651f869..575e50d 100644 --- a/ch3.md +++ b/ch3.md @@ -142,7 +142,7 @@ $ cat database -### SSTables和LSM-Trees +### SSTables和LSM树 在[图3-3](img/fig3-3.png)中,每个日志结构存储段都是一系列键值对。这些对按照它们写入的顺序出现,日志中稍后的值优先于日志中较早的相同键的值。除此之外,文件中键值对的顺序并不重要。 From 9549a154713620d29a7d8203ed3b91cff941e972 Mon Sep 17 00:00:00 2001 From: Vonng Date: Mon, 26 Mar 2018 03:41:45 +0800 Subject: [PATCH 04/17] ch11 rough trans --- ch11.md | 192 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 102 insertions(+), 90 deletions(-) diff --git a/ch11.md b/ch11.md index 87f4be0..e00cee8 100644 --- a/ch11.md +++ b/ch11.md @@ -10,16 +10,16 @@ [TOC] -在第10章中,我们讨论了批处理技术,它将一组文件读取为输入并生成一组新的输出文件。输出是派生数据的一种形式;也就是说,如果需要,可以通过再次运行批处理过程来重新创建数据集。我们看到了如何使用这个简单而强大的想法来创建搜索索引,推荐系统,分析等等。 +在[第10章](ch10.md)中,我们讨论了批处理技术,它将一组文件读取为输入并生成一组新的输出文件。输出是派生数据的一种形式;也就是说,如果需要,可以通过再次运行批处理过程来重新创建数据集。我们看到了如何使用这个简单而强大的想法来创建搜索索引,推荐系统,分析等等。 -然而,在第10章中仍然有一个大的假设:即输入是有界的,即已知和有限的大小,所以批处理知道它何时完成了它的输入。例如,MapReduce中心的排序操作必须读取其全部输入,然后才能开始生成输出:可能发生最后一个输入记录是具有最低键的输入记录,因此必须是第一个输出记录,所以提前开始输出不是一种选择。 +然而,在[第10章](ch10.md)中仍然有一个大的假设:即输入是有界的,即已知和有限的大小,所以批处理知道它何时完成了它的输入。例如,MapReduce中心的排序操作必须读取其全部输入,然后才能开始生成输出:可能发生最后一个输入记录是具有最低键的输入记录,因此必须是第一个输出记录,所以提前开始输出不是一种选择。 实际上,很多数据是无限的,因为它随着时间的推移逐渐到达:你的用户昨天和今天产生了数据,明天他们将继续产生更多的数据。除非您停业,否则这个过程永远都不会结束,所以数据集从来就不会以任何有意义的方式“完成”【1】。因此,批处理程序必须将数据人为地分成固定时间段的数据块,例如,在每天结束时处理一天的数据,或者在每小时结束时处理一小时的数据。 日常批处理过程中的问题是,输入的更改只会在一天之后的输出中反映出来,这对于许多急躁的用户来说太慢了。为了减少延迟,我们可以更频繁地运行处理 - 比如说,在每秒钟的末尾,甚至连续地处理秒数的数据,完全放弃固定的时间片,并简单地处理每一个事件。这是流处理背后的想法。 一般来说,“流”是指随着时间的推移逐渐可用的数据。这个概念出现在很多地方:Unix的stdin和stdout,编程语言(lazy lists)【2】,文件系统API(如Java的FileInputStream),TCP连接,通过互联网传送音频和视频等等。 -在本章中,我们将把事件流视为一个数据管理机制:我们在上一章中看到的批量数据的无界的,递增处理的对应物。我们将首先讨论流如何被表示,存储和通过网络传输。在第451页的“数据库和流”中,我们将调查流和数据库之间的关系。最后,在第464页的“Processing Streams”中,我们将探索连续处理这些流的方法和工具,以及它们可以用来构建应用程序的方法。 +在本章中,我们将把事件流视为一个数据管理机制:我们在上一章中看到的批量数据的无界的,递增处理的对应物。我们将首先讨论流如何被表示,存储和通过网络传输。在“[数据库和流](#数据库和流)”中,我们将研究流和数据库之间的关系。最后在“[流处理](#流处理)”中,我们将探索连续处理这些流的方法和工具,以及它们可以用来构建应用程序的方法。 @@ -27,19 +27,19 @@ 在批处理领域,作业的输入和输出是文件(也许在分布式文件系统上)。什么是类似的流媒体? -当输入是一个文件(一个字节序列)时,第一个处理步骤通常是将其解析为一系列记录。在流处理的上下文中,记录通常被称为事件,但它本质上是一样的:一个小的,自包含的,不可变的对象,包含某个时间点发生的事情的细节。一个事件通常包含一个时间戳,指示何时根据时钟来发生(参见第288页上的“单调对时钟”)。 +当输入是一个文件(一个字节序列)时,第一个处理步骤通常是将其解析为一系列记录。在流处理的上下文中,记录通常被称为事件,但它本质上是一样的:一个小的,自包含的,不可变的对象,包含某个时间点发生的事情的细节。一个事件通常包含一个时间戳,指示何时根据时钟来发生(参见“[单调钟与时钟](ch8.md#单调钟与时钟)”)。 -例如,发生的事情可能是用户采取的行动,例如查看页面或进行购买。它也可能来源于机器,例如来自温度传感器的周期性测量或者CPU利用率度量。在第391页上的“使用Unix工具进行批处理”的示例中,Web服务器日志的每一行都是一个事件。 +例如,发生的事情可能是用户采取的行动,例如查看页面或进行购买。它也可能来源于机器,例如来自温度传感器的周期性测量或者CPU利用率度量。在“[使用Unix工具进行批处理](ch10.md#使用Unix工具进行批处理)”的示例中,Web服务器日志的每一行都是一个事件。 事件可能被编码为文本字符串或JSON,或者以某种二进制形式编码,如第4章所述。这种编码允许您存储一个事件,例如将其附加到一个文件,将其插入关系表,或将其写入文档数据库。它还允许您通过网络将事件发送到另一个节点以进行处理。 在批处理领域,作业的输入和输出是文件(也许在分布式文件系统上)。什么是类似的流媒体? -当输入是一个文件(一个字节序列)时,第一个处理步骤通常是将其解析为一系列记录。在流处理的上下文中,记录通常被称为事件,但它本质上是一样的:一个小的,自包含的,不可变的对象,包含某个时间点发生的事情的细节。一个事件通常包含一个时间戳,指示何时根据时钟来发生(参见第288页上的“单调对时钟”)。 +当输入是一个文件(一个字节序列)时,第一个处理步骤通常是将其解析为一系列记录。在流处理的上下文中,记录通常被称为事件,但它本质上是一样的:一个小的,自包含的,不可变的对象,包含某个时间点发生的事情的细节。一个事件通常包含一个时间戳,指示何时根据时钟来发生(参见“[单调钟与时钟](ch8.md#单调钟与时钟)”)。 -例如,发生的事情可能是用户采取的行动,例如查看页面或进行购买。它也可能来源于机器,例如来自温度传感器的周期性测量或者CPU利用率度量。在第391页上的“使用Unix工具进行批处理”的示例中,Web服务器日志的每一行都是一个事件。 +例如,发生的事情可能是用户采取的行动,例如查看页面或进行购买。它也可能来源于机器,例如来自温度传感器的周期性测量或者CPU利用率度量。在“[使用Unix工具进行批处理](ch10.md#使用Unix工具进行批处理)”的示例中,Web服务器日志的每一行都是一个事件。 -事件可能被编码为文本字符串或JSON,或者以某种二进制形式编码,如第4章所述。这种编码允许您存储一个事件,例如将其附加到一个文件,将其插入关系表,或将其写入文档数据库。它还允许您通过网络将事件发送到另一个节点以进行处理。 +事件可能被编码为文本字符串或JSON,或者以某种二进制形式编码,如[第4章](ch4.md)所述。这种编码允许您存储一个事件,例如将其附加到一个文件,将其插入关系表,或将其写入文档数据库。它还允许您通过网络将事件发送到另一个节点以进行处理。 在批处理中,文件被写入一次,然后可能被多个作业读取。类似地,在流媒体术语中,一个事件由生产者(也称为发布者或发送者)生成一次,然后由多个消费者(订阅者或接收者)进行处理【3】。在文件系统中,文件名标识一组相关记录;在流媒体系统中,相关的事件通常被组合成一个主题或流。 @@ -53,17 +53,17 @@ ### 消息系统 -通知消费者有关新事件的常用方法是使用消息传递系统:生产者发送包含事件的消息,然后将消息推送给消费者。我们之前在第136页的“消息传递数据流”中介绍了这些系统,但现在我们将详细介绍这些系统。 +通知消费者有关新事件的常用方法是使用消息传递系统:生产者发送包含事件的消息,然后将消息推送给消费者。我们之前在“[消息传递中的数据流](ch4.md#消息传递中的数据流)”中介绍了这些系统,但现在我们将详细介绍这些系统。 像生产者和消费者之间的Unix管道或TCP连接这样的直接通信渠道将是实现消息传递系统的简单方法。但是,大多数消息传递系统都在这个基本模型上扩展特别是,Unix管道和TCP将一个发送者与一个接收者完全连接,而一个消息传递系统允许多个生产者节点将消息发送到相同的主题,并允许多个消费者节点接收主题中的消息。 在这个发布/订阅模式中,不同的系统采取多种方法,对于所有目的都没有一个正确的答案。为了区分这些系统,询问以下两个问题特别有帮助: -1. 如果生产者发送消息的速度比消费者能够处理的速度快,会发生什么?一般来说,有三种选择:系统可以放置消息,缓冲队列中的消息或应用背压(也称为流量控制;即阻止生产者发送更多的消息)。例如,Unix管道和TCP使用背压:他们有一个小的固定大小的缓冲区,如果填满,发件人被阻塞,直到收件人从缓冲区中取出数据(参见“网络拥塞和排队”(第282页))。 +1. 如果生产者发送消息的速度比消费者能够处理的速度快,会发生什么?一般来说,有三种选择:系统可以放置消息,缓冲队列中的消息或应用背压(也称为流量控制;即阻止生产者发送更多的消息)。例如,Unix管道和TCP使用背压:他们有一个小的固定大小的缓冲区,如果填满,发件人被阻塞,直到收件人从缓冲区中取出数据(参见“[网络拥塞和排队](ch8.md#网络拥塞和排队)”)。 如果消息被缓存在队列中,那么了解该队列增长会发生什么很重要。如果队列不再适合内存,或者将消息写入磁盘,系统是否会崩溃?如果是这样,磁盘访问如何影响邮件系统的性能【6】? -2. 如果节点崩溃或暂时脱机,会发生什么情况 - 是否有消息丢失?与数据库一样,持久性可能需要写入磁盘和/或复制的某种组合(请参阅第227页的侧栏“复制和耐久性”),这有成本。如果您有时可能会丢失消息,则可能在同一硬件上获得更高的吞吐量和更低的延迟。 +2. 如果节点崩溃或暂时脱机,会发生什么情况 - 是否有消息丢失?与数据库一样,持久性可能需要写入磁盘和/或复制的某种组合(参阅“[复制和持久性](ch7.md#复制和持久性)”),这有成本。如果您有时可能会丢失消息,则可能在同一硬件上获得更高的吞吐量和更低的延迟。 消息丢失是否可以接受取决于应用程序。例如,对于定期传输的传感器读数和指标,偶尔丢失的数据点可能并不重要,因为更新后的值将在短时间后发送。但是,要注意,如果大量的消息被丢弃,那么衡量标准是不正确的【7】。如果您正在计数事件,那么更重要的是它们可靠地传送,因为每个丢失的消息都意味着不正确的计数器。 @@ -77,9 +77,9 @@ * 无代理的消息库,如ZeroMQ 【9】和nanomsg采取类似的方法,通过TCP或IP多播实现发布/订阅消息传递。 - StatsD 【10】和Brubeck 【7】使用不可靠的UDP消息传递来收集网络中所有机器的指标并对其进行监控。 (在StatsD协议中,如果接收到所有消息,则计数器度量标准是正确的;使用UDP将使得度量标准最好近似为【11】。另请参阅“TCP与UDP” + StatsD 【10】和Brubeck 【7】使用不可靠的UDP消息传递来收集网络中所有机器的指标并对其进行监控。 (在StatsD协议中,如果接收到所有消息,则计数器度量标准是正确的;使用UDP将使得度量标准最好近似为【11】。另请参阅“[TCP与UDP](ch8.md#TCP与UDP)” -* 如果消费者在网络上公开服务,生产者可以直接发送HTTP或RPC请求(请参阅第131页的“通过服务进行数据流:REST和RPC”)以将消息推送给使用者。这就是webhooks背后的想法【12】,一种服务的回调URL被注册到另一个服务中,并且每当事件发生时都会向该URL发出请求。 +* 如果消费者在网络上公开服务,生产者可以直接发送HTTP或RPC请求(参阅“[通过服务进行数据流:REST和RPC](ch4.md#通过服务进行数据流:REST和RPC)”)以将消息推送给使用者。这就是webhooks背后的想法【12】,一种服务的回调URL被注册到另一个服务中,并且每当事件发生时都会向该URL发出请求。 尽管这些直接消息传递系统在设计它们的情况下运行良好,但是它们通常要求应用程序代码知道消息丢失的可能性。他们可以容忍的错误是相当有限的:即使协议检测并重新传输在网络中丢失的数据包,他们通常假设生产者和消费者不断在线。 @@ -95,7 +95,7 @@ #### 消息代理与数据库进行比较 -有些消息代理甚至可以使用XA或JTA参与两阶段提交协议(请参阅第367页的“实践中的分布式事务”)。这个功能与数据库在本质上非常相似,尽管消息代理和数据库之间仍存在重要的实际差异: +有些消息代理甚至可以使用XA或JTA参与两阶段提交协议(参阅“[实践中的分布式事务](ch9.md#实践中的分布式事务)”)。这个功能与数据库在本质上非常相似,尽管消息代理和数据库之间仍存在重要的实际差异: * 数据库通常保留数据,直到被明确删除,而大多数消息代理在消息成功传递给消费者时自动删除消息。这样的消息代理不适合长期的数据存储。 * 由于他们很快删除了邮件,大多数邮件经纪人都认为他们的工作集合相当小,即队列很短。如果代理需要缓冲很多消息,因为消费者速度较慢(如果消息不再适合内存,则可能会将消息泄漏到磁盘),每个消息需要更长的时间处理,整体吞吐量可能会降低【6】。 @@ -119,21 +119,22 @@ ![](img/fig11-1.png) **图11-1。 (a)负载平衡:分担消费者之间消费话题的工作; (b)扇出:将消息传递给多个消费者。** + 两种模式可以组合:例如,两个独立的消费者组可以每个订阅一个话题,使得每个组共同收到所有消息,但是在每个组内,只有一个节点接收每个消息。 #### 确认和重新交付 消费者可能会随时崩溃,所以可能发生的情况是经纪人向消费者提供消息,但消费者从不处理消费者,或者在消费者崩溃之前只处理消费者。为了确保消息不会丢失,消息代理使用确认:客户端必须明确告诉代理处理消息的时间,以便代理可以将其从队列中移除。 -如果与客户端的连接关闭或超时而没有代理收到确认,则认为消息未处理,因此它将消息再次传递给另一个消费者。 (请注意,可能发生这样的消息实际上是完全处理的,但网络中的确认丢失了,处理这种情况需要一个原子提交协议,正如在第360页的“实践中的分布式事务”中所讨论的那样) +如果与客户端的连接关闭或超时而没有代理收到确认,则认为消息未处理,因此它将消息再次传递给另一个消费者。 (请注意,可能发生这样的消息实际上是完全处理的,但网络中的确认丢失了,处理这种情况需要一个原子提交协议,正如在“[实践中的分布式事务](ch9.md#实践中的分布式事务)”中所讨论的那样) -当与负载平衡相结合时,这种重新传递行为对消息的排序有一个有趣的影响。在图11-2中,消费者通常按照生产者发送的顺序处理消息。然而,消费者2在处理消息m3时崩溃,与此同时消费者1正在处理消息m4。未确认的消息m3随后被重新发送给消费者1,结果消费者1按照m4,m3,m5的顺序处理消息。因此,m3和m4不是以与生产者1发送的顺序相同的顺序交付的。 +当与负载平衡相结合时,这种重新传递行为对消息的排序有一个有趣的影响。在[图11-2](img/fig11-2.png)中,消费者通常按照生产者发送的顺序处理消息。然而,消费者2在处理消息m3时崩溃,与此同时消费者1正在处理消息m4。未确认的消息m3随后被重新发送给消费者1,结果消费者1按照m4,m3,m5的顺序处理消息。因此,m3和m4不是以与生产者1发送的顺序相同的顺序交付的。 ![](img/fig11-2.png) **图11-2 消费者2在处理m3时崩溃,因此稍后再次向消费者1递送** -即使消息代理试图保留消息的顺序(如JMS和AMQP标准所要求的),负载平衡与重新传递的组合也不可避免地导致消息被重新排序。为避免此问题,您可以为每个使用者使用单独的队列(即不使用负载平衡功能)。如果消息是完全独立的,消息重新排序并不是一个问题,但是如果消息之间存在因果依赖关系,这一点很重要,我们将在后面的章节中看到。 +即使消息代理试图保留消息的顺序(如JMS和AMQP标准所要求的),负载平衡与重新传递的组合也不可避免地导致消息被重新排序。为避免此问题,您可以为每个使用者使用单独的队列(即不使用负载均衡功能)。如果消息是完全独立的,消息重新排序并不是一个问题,但是如果消息之间存在因果依赖关系,这一点很重要,我们将在后面的章节中看到。 ### 分区日志 @@ -141,7 +142,7 @@ 数据库和文件系统采用相反的方法:写入数据库或文件的所有内容通常都要被永久记录下来,至少在某些人明确选择将其删除之前。 -思维方式上的这种差异对如何创建派生数据有很大的影响。批处理过程的一个关键特征是,你可以反复运行它们,试验处理步骤,没有损坏输入的风险(因为输入是只读的)。 AMQP / JMS风格的消息并非如此:如果确认导致从代理中删除消息,则接收消息具有破坏性,因此您不能再次运行同一消费者,并期望得到相同的结果。 +思维方式上的这种差异对如何创建派生数据有很大的影响。批处理过程的一个关键特征是,你可以反复运行它们,试验处理步骤,没有损坏输入的风险(因为输入是只读的)。 AMQP/JMS风格的消息并非如此:如果确认导致从代理中删除消息,则接收消息具有破坏性,因此您不能再次运行同一消费者,并期望得到相同的结果。 如果您将新消费者添加到消息系统,则通常只会开始接收在注册后发送的消息;任何先前的消息已经消失,无法恢复。将它与文件和数据库进行对比,您可以随时添加新客户端,并且可以读取过去任意写入的数据(只要应用程序没有明确覆盖或删除数据)。 @@ -149,13 +150,13 @@ #### 使用日志进行消息存储 -日志只是磁盘上只能追加记录的序列。我们先前在第3章中的日志结构存储引擎和预写日志的上下文中讨论了日志,在第5章中也讨论了复制的上下文中。 +日志只是磁盘上只能追加记录的序列。我们先前在第3章中的日志结构存储引擎和预写日志的上下文中讨论了日志,在[第5章](ch5.md)中也讨论了复制的上下文中。 -可以使用相同的结构来实现消息代理:生产者通过将消息附加到日志的末尾来发送消息,并且消费者通过依次读取日志来接收消息。如果消费者到达日志的末尾,则等待通知新消息已被追加。 Unix工具tail -f监视数据被附加的文件,基本上是这样工作的。 +可以使用相同的结构来实现消息代理:生产者通过将消息附加到日志的末尾来发送消息,并且消费者通过依次读取日志来接收消息。如果消费者到达日志的末尾,则等待通知新消息已被追加。 Unix工具`tail -f`监视数据被附加的文件,基本上是这样工作的。 -为了扩展到比单个磁盘提供更高的吞吐量,可以对日志进行分区(在第6章的意义上)。然后可以在不同的机器上托管不同的分区,使每个分区成为一个单独的日志,可以独立于其他分区读取和写入。然后可以将一个话题定义为一组分段,它们都携带相同类型的消息。这种方法如图11-3所示。 +为了扩展到比单个磁盘提供更高的吞吐量,可以对日志进行分区(在[第6章](ch6.md)的意义上)。然后可以在不同的机器上托管不同的分区,使每个分区成为一个单独的日志,可以独立于其他分区读取和写入。然后可以将一个话题定义为一组分段,它们都携带相同类型的消息。这种方法如[图11-3](img/fig11-3.png)所示。 -在每个分区中,代理为每个消息分配一个单调递增的序列号或偏移量(在图11-3中,框中的数字是消息偏移量)。这样的序列号是有意义的,因为分区是仅附加的,所以分区内的消息是完全有序的。没有跨不同分区的订购保证。 +在每个分区中,代理为每个消息分配一个单调递增的序列号或偏移量(在[图11-3](img/fig11-3.png)中,框中的数字是消息偏移量)。这样的序列号是有意义的,因为分区是仅附加的,所以分区内的消息是完全有序的。没有跨不同分区的订购保证。 ![](img/fig11-3.png) @@ -170,9 +171,9 @@ Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的Distribut 每个客户端然后使用分配的分区中的所有消息。通常情况下,当一个用户被分配了一个日志分区时,它会以直接的单线程的方式顺序地读取分区中的消息。这种粗粒度的负载均衡方法有一些缺点: * 共享消费主题工作的节点数最多可以是该主题中的日志分区数,因为同一个分区内的消息被传递到同一个节点[^i]。 -* 如果单个消息处理缓慢,则会阻止处理该分区中的后续消息(一种行头阻塞形式;请参阅第13页上的“描述性能”)。 +* 如果单个消息处理缓慢,则会阻止处理该分区中的后续消息(一种行头阻塞形式;请参阅“[描述性能](ch1.md#描述性能)”)。 -因此,在处理消息可能代价高昂,并且希望逐个消息地平行处理以及消息排序并不那么重要的情况下,消息代理的JMS / AMQP风格是可取的。另一方面,在消息吞吐量高的情况下,每条消息的处理速度都很快,消息的排序也是重要的,基于日志的方法运行得很好。 +因此,在处理消息可能代价高昂,并且希望逐个消息地平行处理以及消息排序并不那么重要的情况下,消息代理的JMS/AMQP风格是可取的。另一方面,在消息吞吐量高的情况下,每条消息的处理速度都很快,消息的排序也是重要的,基于日志的方法运行得很好。 [^i]: 有可能创建一个负载均衡方案,在这个方案中,两个消费者通过读取全部消息来共享处理分区的工作,但是其中一个只考虑具有偶数偏移量的消息,而另一个消费者处理奇数编号的偏移量。或者,您可以将消息处理扩展到线程池,但这种方法会使消费者偏移管理变得复杂。一般来说,分区的单线程处理是可取的,并行分区可以通过使用更多的分区来增加。 @@ -180,7 +181,7 @@ Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的Distribut 消耗一个分区依次可以很容易地判断哪些消息已经被处理:所有消息的偏移量小于消费者的当前偏移量已经被处理,并且具有更大偏移量的所有消息还没有被看到。因此,经纪人不需要跟踪每条消息的确认,只需要定期记录消费者的偏移。在这种方法中减少的簿记开销以及批处理和流水线的机会有助于提高基于日志的系统的吞吐量。 -实际上,这种偏移量与单领先数据库复制中常见的日志序列号非常相似,我们在第151页的“设置新的跟踪者”中讨论了这种情况。在数据库复制中,日志序列号允许跟随者断开连接后,重新连接到领导,并在不跳过任何写入的情况下恢复复制。这里使用的原则完全相同:信息经纪人的行为像一个领导者数据库,而消费者就像一个追随者。 +实际上,这种偏移量与单领先数据库复制中常见的日志序列号非常相似,我们在“[设置新从库](ch5.md#设置新从库)”中讨论了这种情况。在数据库复制中,日志序列号允许跟随者断开连接后,重新连接到领导,并在不跳过任何写入的情况下恢复复制。这里使用的原则完全相同:消息代理的行为像一个领导者数据库,而消费者就像一个追随者。 如果消费者节点失败,则使用者组中的另一个节点将被分配失败的使用者分区,并开始以最后记录的偏移量使用消息。如果消费者已经处理了后续的消息,但还没有记录他们的偏移量,那么在重新启动后这些消息将被第二次处理。本章后面我们将讨论处理这个问题的方法。 @@ -190,13 +191,13 @@ Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的Distribut 这就意味着,如果一个消费者的速度慢,消费者的消费速度落后于消费者的偏移量,那么消费者的偏移量就会指向一个已经被删除的消费者。实际上,日志实现了一个有限大小的缓冲区,当旧的消息满时,它也被称为循环缓冲区或环形缓冲区。但是,由于该缓冲区在磁盘上,因此可能相当大。 -让我们来做一个后台计算。在撰写本文时,典型的大型硬盘驱动器容量为6TB,顺序写入吞吐量为150MB / s。如果您以最快的速度写邮件,则需要大约11个小时才能填满驱动器。因此,磁盘可以缓存11个小时的消息,之后它将开始覆盖旧的消息。即使您使用多个硬盘驱动器和机器,这个比率也是一样的。在实践中,部署很少使用磁盘的完整写入带宽,所以日志通常可以保存几天甚至几周的缓冲区。 +让我们来做一个后台计算。在撰写本文时,典型的大型硬盘驱动器容量为6TB,顺序写入吞吐量为150MB/s。如果您以最快的速度写邮件,则需要大约11个小时才能填满驱动器。因此,磁盘可以缓存11个小时的消息,之后它将开始覆盖旧的消息。即使您使用多个硬盘驱动器和机器,这个比率也是一样的。在实践中,部署很少使用磁盘的完整写入带宽,所以日志通常可以保存几天甚至几周的缓冲区。 不管你保留多长时间的消息,一个日志的吞吐量或多或少保持不变,因为无论如何每个消息都被写入磁盘【18】。这种行为与将邮件默认保存在内存中的消息传递系统形成鲜明对比,如果队列变得太大,只将其写入磁盘:当这些系统开始写入磁盘时,这些系统速度很快,并且变慢得多,所以吞吐量取决于保留的历史数量。 #### 当消费者跟不上生产者时 -在“信息系统”第441页的开头,我们讨论了如果消费者无法跟上生产者发送信息的速度的三种选择:丢弃信息,缓冲或施加背压。在这个分类法中,基于日志的方法是一种缓冲形式,具有较大但固定大小的缓冲区(受可用磁盘空间的限制)。 +在“[信息系统]()”第441页的开头,我们讨论了如果消费者无法跟上生产者发送信息的速度的三种选择:丢弃信息,缓冲或施加背压。在这个分类法中,基于日志的方法是一种缓冲形式,具有较大但固定大小的缓冲区(受可用磁盘空间的限制)。 如果消费者远远落后于它所要求的信息比保留在磁盘上的信息要旧,那么它将不能读取这些信息,所以代理人有效地丢弃了比缓冲区容量更大的旧信息。您可以监控消费者在日志头后面的距离,如果显着落后,则会发出警报。由于缓冲区很大,因此有足够的时间让人类操作员修复缓慢的消费者,并在消息开始丢失之前让其赶上。 @@ -220,30 +221,31 @@ Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的Distribut 我们之前曾经说过,事件是某个时刻发生的事情的记录。发生的事情可能是用户操作(例如键入搜索查询)或传感器读取,但也可能是写入数据库。事情被写入数据库的事实是可以被捕获,存储和处理的事件。这一观察结果表明,数据库和数据流之间的连接不仅仅是磁盘上日志的物理存储 - 这是非常重要的。 -事实上,复制日志(请参阅第158页上的“复制日志的实现”)是数据库写入事件的流,由领导者在处理事务时生成。追随者将写入流应用到他们自己的数据库副本,从而最终得到相同数据的精确副本。复制日志中的事件描述发生的数据更改。 +事实上,复制日志(参阅“[复制日志的实现](ch5.md#复制日志的实现)”)是数据库写入事件的流,由领导者在处理事务时生成。追随者将写入流应用到他们自己的数据库副本,从而最终得到相同数据的精确副本。复制日志中的事件描述发生的数据更改。 + +我们还在“[全序广播](ch9.md#全序广播)”中遇到了状态机复制原理,其中指出:如果每个事件代表对数据库的写入,并且每个副本按相同的顺序处理相同的事件,则副本将所有这些都以相同的最终状态结束。 (处理一个事件被认为是一个确定性的操作。)这只是事件流的另一种情况! -我们还在第348页的“全序广播”中遇到了状态机复制原理,其中指出:如果每个事件代表对数据库的写入,并且每个副本按相同的顺序处理相同的事件,则副本将所有这些都以相同的最终状态结束。 (处理一个事件被认为是一个确定性的操作。)这只是事件流的另一种情况! 在本节中,我们将首先看看异构数据系统中出现的一个问题,然后探讨如何通过将事件流的想法带入数据库来解决这个问题。 ### 保持系统同步 正如我们在本书中所看到的,没有一个系统能够满足所有的数据存储,查询和处理需求。在实践中,大多数不重要的应用程序需要结合多种不同的技术来满足他们的需求:例如,使用OLTP数据库来为用户请求提供服务,缓存来加速常见请求,处理全文索引搜索查询和用于分析的数据仓库。每个数据都有其自己的数据副本,存储在自己的表示中,并根据自己的目的进行了优化。 -由于相同或相关的数据出现在不同的地方,因此需要保持相互同步:如果某个项目在数据库中进行了更新,则还需要在缓存,搜索索引和数据仓库中进行更新。对于数据仓库,这种同步通常由ETL进程执行(参见第91页的“数据仓库”),通常通过获取数据库的完整副本,转换数据库并将其批量加载到数据仓库中 - 换句话说,一个批处理。同样,我们在第419页的“批量工作流的输出”中看到了如何使用批处理过程创建搜索索引,建议系统和其他派生数据系统。 +由于相同或相关的数据出现在不同的地方,因此需要保持相互同步:如果某个项目在数据库中进行了更新,则还需要在缓存,搜索索引和数据仓库中进行更新。对于数据仓库,这种同步通常由ETL进程执行(参见“[数据仓库](ch3.md#数据仓库)”),通常通过获取数据库的完整副本,转换数据库并将其批量加载到数据仓库中 —— 换句话说,一个批处理。同样,我们在“[批量工作流的输出](ch10.md#批量工作流的输出)”中看到了如何使用批处理过程创建搜索索引,建议系统和其他派生数据系统。 如果周期性的完整数据库转储过于缓慢,有时使用的替代方法是双重写入,其中应用程序代码在数据更改时明确写入每个系统:例如,首先写入数据库,然后更新搜索索引,然后使缓存条目无效(或者甚至同时执行这些写入)。 -但是,双重写入有一些严重的问题,其中一个是图11-4所示的竞争条件。在这个例子中,两个客户端同时想要更新一个项目X:客户端1想要将值设置为A,客户端2想要将其设置为B.两个客户端首先将新值写入数据库,然后将其写入到搜索索引。由于运行时间不正确,这些请求是交错的:数据库首先看到从客户端1的写入将值设置为A,然后从客户端2写入将值设置为B,因此数据库中的最终值为B.搜索索引首先看到来自客户端2,然后是客户端1的写入,所以搜索索引中的最终值是A.这两个系统现在永久地不一致,即使没有发生错误。 +但是,双重写入有一些严重的问题,其中一个是[图11-4](img/fig11-4.png)所示的竞争条件。在这个例子中,两个客户端同时想要更新一个项目X:客户端1想要将值设置为A,客户端2想要将其设置为B.两个客户端首先将新值写入数据库,然后将其写入到搜索索引。由于运行时间不正确,这些请求是交错的:数据库首先看到从客户端1的写入将值设置为A,然后从客户端2写入将值设置为B,因此数据库中的最终值为B.搜索索引首先看到来自客户端2,然后是客户端1的写入,所以搜索索引中的最终值是A.这两个系统现在永久地不一致,即使没有发生错误。 ![](img/fig11-4.png) **图11-4 在数据库中,X首先被设置为A,然后被设置为B,而在搜索索引处,写入以相反的顺序到达** -除非有一些额外的并发检测机制,例如我们在第184页上的“检测并发写入”中讨论的版本向量,否则您甚至不会注意到发生了并发写入 - 一个值将简单地以无提示方式覆盖另一个值。 +除非有一些额外的并发检测机制,例如我们在“[检测并发写入](ch5.md#检测并发写入)”中讨论的版本向量,否则您甚至不会注意到发生了并发写入 —— 一个值将简单地以无提示方式覆盖另一个值。 -双重写入的另一个问题是其中一个写入可能会失败,而另一个成功。这是一个容错问题,而不是一个并发问题,但也会造成两个系统互相矛盾的结果。确保它们都成功或者两者都失败是原子提交问题的一个例子,这个问题的解决是昂贵的(请参阅第354页上的“原子提交和两阶段提交(2PC)”)。 +双重写入的另一个问题是其中一个写入可能会失败,而另一个成功。这是一个容错问题,而不是一个并发问题,但也会造成两个系统互相矛盾的结果。确保它们都成功或者两者都失败是原子提交问题的一个例子,这个问题的解决是昂贵的(参阅“[原子提交和两阶段提交(2PC)](ch7.md#原子提交和两阶段提交(2PC))”)。 -如果只有一个复制的数据库和一个领导者,那么这个领导者决定了写入顺序,所以状态机复制方法可以在数据库的副本中工作。然而,在图11-4中,没有一个领导者:数据库可能有一个领导者,搜索索引可能有一个领导者,但是既不在另一个领导者之后,也可能发生冲突(参见“多领导者复制“)。 +如果只有一个复制的数据库和一个领导者,那么这个领导者决定了写入顺序,所以状态机复制方法可以在数据库的副本中工作。然而,在[图11-4](img/fig11-4.png)中,没有一个领导者:数据库可能有一个领导者,搜索索引可能有一个领导者,但是既不在另一个领导者之后,也可能发生冲突(参见“[多领导者复制](ch5.md#多领导者复制)“)。 如果实际上只有一个领导者(例如数据库),并且如果我们可以使搜索索引成为数据库的追随者,情况会更好。但这在实践中可能吗? @@ -257,21 +259,23 @@ Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的Distribut 最近,人们对变更数据捕获(CDC)越来越感兴趣,它是观察写入数据库的所有数据变化并将其提取出来,并将其复制到其他系统中的过程。 CDC特别感兴趣的是,如果改变可以立即用于流,可以立即写入。 -例如,您可以捕获数据库中的更改并不断将相同的更改应用于搜索索引。如果更改的日志以相同的顺序应用,则可以预期搜索索引中的数据与数据库中的数据匹配。搜索索引和任何其他派生的数据系统只是变化流的消费者,如图11-5所示。 +例如,您可以捕获数据库中的更改并不断将相同的更改应用于搜索索引。如果更改的日志以相同的顺序应用,则可以预期搜索索引中的数据与数据库中的数据匹配。搜索索引和任何其他派生的数据系统只是变化流的消费者,如[图11-5](img/fig11-5.png)所示。 ![](img/fig11-5.png) **图11-5 将数据按顺序写入一个数据库,然后按照相同的顺序将这些更改应用到其他系统** -实施更改数据捕获 + +#### 实现变更数据捕获 + 我们可以调用日志消费者导出的数据系统,正如在第三部分的介绍中所讨论的:存储在搜索索引和数据仓库中的数据只是记录系统中数据的另一个视图。更改数据捕获是一种机制,可确保对记录系统所做的所有更改都反映在派生数据系统中,以便派生系统具有数据的准确副本。 从本质上说,改变数据捕获使得一个数据库成为领导者(从中捕获变化的数据库),并将其他人变成追随者。基于日志的消息代理非常适合从源数据库传输更改事件,因为它保留了消息的排序(避免了图11-2的重新排序问题)。 -数据库触发器可用于通过注册触发器来实现更改数据捕获(请参阅第152页的“基于触发器的复制”),这些触发器可观察数据表的所有更改,并将相应的条目添加到更改日志表中。但是,他们往往是脆弱的,并有显着的性能开销。解析复制日志可以是一个更强大的方法,但它也带来了挑战,例如处理模式更改。 +数据库触发器可用于通过注册触发器来实现更改数据捕获(参阅“[基于触发器的复制](ch5.md#基于触发器的复制)”),这些触发器可观察数据表的所有更改,并将相应的条目添加到更改日志表中。但是,他们往往是脆弱的,并有显着的性能开销。解析复制日志可以是一个更强大的方法,但它也带来了挑战,例如处理模式更改。 LinkedIn的Databus 【25】,Facebook的Wormhole 【26】和Yahoo!的Sherpa 【27】大规模地使用这个想法。 Bottled Water使用解码预写日志的API来实现PostgreSQL的CDC 【28】,Maxwell和Debezium通过解析binlog为MySQL做类似的事情【29,30,31】,Mongoriver读取MongoDB oplog 【32,33】 ,而GoldenGate为Oracle提供类似的功能【34,35】。 -像消息代理一样,更改数据捕获通常是异步的:记录数据库系统不会等待更改应用到消费者,然后再进行更改。这种设计具有的操作优势是添加缓慢的使用者不会影响记录系统太多,但是它具有所有复制滞后问题的缺点(请参见第161页中的“复制滞后问题”)。 +像消息代理一样,更改数据捕获通常是异步的:记录数据库系统不会等待更改应用到消费者,然后再进行更改。这种设计具有的操作优势是添加缓慢的使用者不会影响记录系统太多,但是它具有所有复制滞后问题的缺点(参见“[复制延迟问题](ch5.md#复制延迟问题)”)。 #### 初始快照 @@ -285,13 +289,13 @@ LinkedIn的Databus 【25】,Facebook的Wormhole 【26】和Yahoo!的Sherpa 如果只能保留有限的日志历史记录,则每次需要添加新的派生数据系统时都需要执行快照过程。但是,日志压缩提供了一个很好的选择。 -在日志结构化的存储引擎的情况下,我们先讨论了第72页的“Hash索引”中的日志压缩(参见图3-2的示例)。原理很简单:存储引擎使用相同的密钥定期查找日志记录,丢弃任何重复内容,并且只保留每个密钥的最新更新。这个压缩和合并过程在后台运行。 +在日志结构化的存储引擎的情况下,我们先讨论了“[Hash索引](ch3.md#Hash索引)”中的日志压缩(参见[图3-2](img/fig3-2.png)的示例)。原理很简单:存储引擎使用相同的密钥定期查找日志记录,丢弃任何重复内容,并且只保留每个密钥的最新更新。这个压缩和合并过程在后台运行。 在日志结构存储引擎中,具有特殊空值(逻辑删除)的更新指示删除了一个密钥,并在日志压缩过程中将其删除。但只要密钥不被覆盖或删除,它就永远留在日志中。这种压缩日志所需的磁盘空间仅取决于数据库的当前内容,而不取决于数据库中发生的写入次数。如果相同的密钥经常被覆盖,则以前的值将最终被垃圾收集,并且只保留最新的值。 在基于日志的消息代理和更改数据捕获方面,相同的想法也适用。如果CDC系统设置为每个更改都有一个主键,并且每个键的更新都替换了该键的以前的值,那么仅保留最近写入的特定键就足够了。 -现在,无论何时要重建派生数据系统(如搜索索引),都可以从日志压缩主题的偏移量0开始新的使用者,然后依次扫描日志中的所有消息。日志保证包含数据库中每个键的最新值(也可能是一些较旧的值) - 换句话说,您可以使用它来获取数据库内容的完整副本,而无需获取CDC的另一个快照源数据库。 +现在,无论何时要重建派生数据系统(如搜索索引),都可以从日志压缩主题的偏移量0开始新的使用者,然后依次扫描日志中的所有消息。日志保证包含数据库中每个键的最新值(也可能是一些较旧的值)—— 换句话说,您可以使用它来获取数据库内容的完整副本,而无需获取CDC的另一个快照源数据库。 Apache Kafka支持此日志压缩功能。正如我们将在本章后面看到的,它允许消息代理被用于持久存储,而不仅仅是用于临时消息。 @@ -309,14 +313,14 @@ Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获 与更改数据捕获类似,事件采购涉及将所有对应用程序状态的更改存储为更改事件的日志。最大的区别是事件源代码在不同的抽象层次上应用了这个想法: -* 在更改数据捕获中,应用程序以可变方式使用数据库,随意更新和删除记录。从数据库中提取较低级别的更改日志(例如,通过解析复制日志),从而确保从数据库中提取的写入顺序与实际写入的顺序相匹配,从而避免图中的竞争条件11-4。写入数据库的应用程序不需要知道CDC正在发生。 +* 在更改数据捕获中,应用程序以可变方式使用数据库,随意更新和删除记录。从数据库中提取较低级别的更改日志(例如,通过解析复制日志),从而确保从数据库中提取的写入顺序与实际写入的顺序相匹配,从而避免[图11-4](img/fig11-4.png)中的竞争条件。写入数据库的应用程序不需要知道CDC正在发生。 * 在事件源中,应用程序逻辑是基于写入事件日志的不可变事件而显式构建的。在这种情况下,事件存储是附加的,更新或删除是不鼓励或禁止的。事件旨在反映应用程序级别发生的事情,而不是低级状态更改。 事件源是一种强大的数据建模技术:从应用程序的角度来看,将用户的行为记录为不可变的事件更有意义,而不是记录这些行为对可变数据库的影响。事件采购使得随着时间的推移而逐渐发展应用程序变得更加容易,通过更容易理解事情发生的原因以及防范应用程序错误(请参阅“不可变事件的优点”),帮助进行调试。 -例如,存储“学生取消课程注册”事件清楚地表达了单一行为的中性意图,而副作用“从注册表中删除了一个条目,并且一个取消原因被添加到学生反馈表“嵌入了很多有关方式的假设数据稍后将被使用。如果引入新的应用程序功能,例如“将地点提供给等待列表中的下一个人” - 事件采购方法允许将新的副作用轻松地链接到现有事件上。 +例如,存储“学生取消课程注册”事件清楚地表达了单一行为的中性意图,而副作用“从注册表中删除了一个条目,并且一个取消原因被添加到学生反馈表“嵌入了很多有关方式的假设数据稍后将被使用。如果引入新的应用程序功能,例如“将地点提供给等待列表中的下一个人” —— 事件顺序方法允许将新的副作用轻松地链接到现有事件上。 -事件采购类似于编年史数据模型【45】,事件日志和事实表之间也有相似之处,您可以在星型模式中找到它(请参阅第93页上的“星星和雪花:分析模式”) 。 +事件顺序类似于编年史数据模型【45】,事件日志和事实表之间也有相似之处,您可以在星型模式中找到它(参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。 专门的数据库如Event Store 【46】已经被开发来支持使用事件采购的应用程序,但总的来说,这个方法是独立于任何特定的工具的。传统的数据库或基于日志的消息代理也可以用来构建这种风格的应用程序。 @@ -324,7 +328,7 @@ Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获 事件日志本身并不是很有用,因为用户通常期望看到系统的当前状态,而不是修改的历史。例如,在购物网站上,用户期望能够看到他们购物车的当前内容,而不是他们对购物车所做的所有改变的附加列表。 -因此,使用事件源的应用程序需要记录事件的日志(表示写入系统的数据),并将其转换为适合向用户显示的应用程序状态(从系统读取数据的方式[47 ])。这种转换可以使用任意的逻辑,但它应该是确定性的,以便您可以再次运行它并从事件日志中派生相同的应用程序状态。 +因此,使用事件源的应用程序需要记录事件的日志(表示写入系统的数据),并将其转换为适合向用户显示的应用程序状态(从系统读取数据的方式【47】)。这种转换可以使用任意的逻辑,但它应该是确定性的,以便您可以再次运行它并从事件日志中派生相同的应用程序状态。 与更改数据捕获一样,重放事件日志可以让您重新构建系统的当前状态。但是,日志压缩需要以不同的方式处理: @@ -337,7 +341,7 @@ Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获 事件采购哲学是仔细区分事件和命令【48】。当来自用户的请求首先到达时,它最初是一个命令:在这一点上它可能仍然失败,例如因为违反了一些完整性条件。应用程序必须首先验证它是否可以执行该命令。如果验证成功并且命令被接受,则它变成一个持久且不可变的事件。 -例如,如果用户试图注册特定用户名,或在飞机上或剧院中预定座位,则应用程序需要检查用户名或座位是否已被占用。 (我们先前在第364页的“容错概念”中讨论过这个例子。)当检查成功时,应用程序可以生成一个事件来指示特定的用户名是由特定的用户ID注册的,座位已经预留给特定的顾客。 +例如,如果用户试图注册特定用户名,或在飞机上或剧院中预定座位,则应用程序需要检查用户名或座位是否已被占用。 (我们先前在第364页的“[容错概念](ch8.md#容错概念)”中讨论过这个例子。)当检查成功时,应用程序可以生成一个事件来指示特定的用户名是由特定的用户ID注册的,座位已经预留给特定的顾客。 在事件发生的时候,这成为事实。即使客户稍后决定更改或取消预订,事实仍然是事实,他们以前曾为某个特定的座位进行预订,而更改或取消是稍后添加的单独事件。 @@ -365,7 +369,7 @@ Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获 > 事务日志记录对数据库所做的所有更改。高速追加是更改日志的唯一方法。从这个角度来看,数据库的内容会保存日志中最新记录值的缓存。事实是日志。数据库是日志子集的缓存。该缓存子集恰好是来自日志的每个记录和索引值的最新值。 -日志压缩(如第456页的“日志压缩”中所述)是一种桥接日志和数据库状态之间区别的方法:它只保留每条记录的最新版本,并丢弃被覆盖的版本。 +日志压缩(如“[日志压缩](#日志压缩)”中所述)是一种桥接日志和数据库状态之间区别的方法:它只保留每条记录的最新版本,并丢弃被覆盖的版本。 #### 不可变事件的优点 @@ -379,17 +383,17 @@ Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获 #### 从同一事件日志中获取多个视图 -而且,通过从不变事件日志中分离可变状态,可以从事件的相同日志中派生出几个不同的面向读取的表示。这就像一个流的多个消费者一样工作(图11-5):例如,分析数据库Druid使用这种方法从Kafka直接获取【55】,Pista chio是一个分布式的键值存储,使用Kafka作为提交日志【56】,Kafka Connect接收器可以将来自Kafka的数据导出到各种不同的数据库和索引【41】。对于许多其他存储和索引系统(如搜索服务器)来说,类似地从分布式日志中获取输入也是有意义的(请参阅第455页上的“保持系统同步”)。 +而且,通过从不变事件日志中分离可变状态,可以从事件的相同日志中派生出几个不同的面向读取的表示。这就像一个流的多个消费者一样工作([图11-5](img/fig11-5.png)):例如,分析数据库Druid使用这种方法从Kafka直接获取【55】,Pista chio是一个分布式的键值存储,使用Kafka作为提交日志【56】,Kafka Connect接收器可以将来自Kafka的数据导出到各种不同的数据库和索引【41】。对于许多其他存储和索引系统(如搜索服务器)来说,类似地从分布式日志中获取输入也是有意义的(请参阅第455页上的“保持系统同步”)。 从事件日志到数据库有一个明确的转换步骤,可以更容易地随时间推移您的应用程序:如果您想要引入一个以新的方式呈现现有数据的新功能,您可以使用事件日志来构建一个单独的新功能的读取优化视图,并与现有的一起运行 系统而不必修改它们。并行运行旧系统和新系统通常比在现有系统中执行复杂的模式迁移更容易。一旦旧的系统不再需要,你可以简单地关闭它并回收它的资源【47,57】。 -如果您不必担心如何查询和访问数据,那么存储数据通常是非常简单的。模式设计,索引和存储引擎的许多复杂性都是希望支持某些查询和访问模式的结果(参见第3章)。出于这个原因,通过将数据写入的形式与读取形式分开,并允许几个不同的读取视图,可以获得很大的灵活性。这个想法有时被称为命令查询责任分离(CQRS)【42,58,59】。 +如果您不必担心如何查询和访问数据,那么存储数据通常是非常简单的。模式设计,索引和存储引擎的许多复杂性都是希望支持某些查询和访问模式的结果(参见[第3章](ch3.md))。出于这个原因,通过将数据写入的形式与读取形式分开,并允许几个不同的读取视图,可以获得很大的灵活性。这个想法有时被称为命令查询责任分离(CQRS)【42,58,59】。 数据库和模式设计的传统方法是基于数据必须以与查询相同的形式写入的谬误。有关正常化和非规范化的争论(请参阅第31页上的“多对一和多对多关系”),如果可以将数据从写入优化的事件日志转换为读取优化的应用程序状态,则变得基本无关紧要:在读取优化的视图中对数据进行非规范化是完全合理的,因为翻译过程为您提供了一种机制,使其与事件日志保持一致。 -在第11页的“描述负载”中,我们讨论了Twitter的家庭时间表,最近一个特定用户正在关注的人(如邮箱)写的最近发布的推文缓存。这是阅读优化状态的另一个例子:家庭时间表高度变形,因为你的推文在所有跟随你的人的时间线上都是重复的。然而,扇出服务保持这种复制状态与新的推文和新的以下关系保持同步,这保持了复制的可管理性。 +在“[描述负载](ch1.md#描述负载)”中,我们讨论了推特主页时间表,最近一个特定用户正在关注的人(如邮箱)写的最近发布的推文缓存。这是阅读优化状态的另一个例子:家庭时间表高度变形,因为你的推文在所有跟随你的人的时间线上都是重复的。然而,扇出服务保持这种复制状态与新的推文和新的以下关系保持同步,这保持了复制的可管理性。 #### 并发控制 @@ -403,15 +407,15 @@ Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获 #### 不变性的限制 -许多不使用事件源模型的系统依赖于不可变性:各种数据库在内部使用不可变的数据结构或多版本数据来支持时间点快照(请参见“索引和快照隔离” )。 Git,Mercurial和Fossil等版本控制系统也依靠不可变的数据来保存文件的版本历史记录。 +许多不使用事件源模型的系统依赖于不可变性:各种数据库在内部使用不可变的数据结构或多版本数据来支持时间点快照(参见“[索引和快照隔离](ch7.md#索引和快照隔离)” )。 Git,Mercurial和Fossil等版本控制系统也依靠不可变的数据来保存文件的版本历史记录。 永远保持所有变化的不变的历史在多大程度上是可行的?答案取决于数据集中的流失量。一些工作负载主要是添加数据,很少更新或删除;他们很容易使不变。其他工作负载在较小的数据集上有较高的更新和删除率;在这些情况下,不可改变的历史可能变得过于庞大,碎片化可能成为一个问题,压缩和垃圾收集的表现对于操作的鲁棒性变得至关重要【60,61】。 除了性能方面的原因外,也可能出于管理方面的原因需要删除数据的情况,尽管这些数据都是不可变的。例如,隐私条例可能要求在关闭帐户后删除用户的个人信息,数据保护立法可能要求删除错误的信息,或者可能需要包含敏感信息的意外泄露。 -在这种情况下,仅仅在日志中添加另一个事件来指示先前的数据应该被视为删除是不够的 - 您实际上是想重写历史并假装数据从未写在第一位。例如,Datomic调用这个特性excision 【62】,而Fossil版本控制系统有一个类似的概念叫做shunning 【63】。 +在这种情况下,仅仅在日志中添加另一个事件来指示先前的数据应该被视为删除是不够的 —— 您实际上是想重写历史并假装数据从未写在第一位。例如,Datomic调用这个特性excision 【62】,而Fossil版本控制系统有一个类似的概念叫做shunning 【63】。 -真正的删除数据是非常困难的【64】,因为拷贝可以存在于很多地方:例如,存储引擎,文件系统和SSD通常写入一个新的位置,而不是覆盖到位【52】,而备份通常是故意不可改变的防止意外删除或腐败。删除更多的是“使检索数据更难”,而不是“使检索数据不可能”。无论如何,有时您必须尝试,正如我们在“立法和自律”中所看到的第542页。 +真正的删除数据是非常困难的【64】,因为拷贝可以存在于很多地方:例如,存储引擎,文件系统和SSD通常写入一个新的位置,而不是覆盖到位【52】,而备份通常是故意不可改变的防止意外删除或腐败。删除更多的是“使检索数据更难”,而不是“使检索数据不可能”。无论如何,有时您必须尝试,正如我们在“[立法和自律](ch12.md#立法和自律)”中所看到的。 @@ -419,15 +423,15 @@ Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获 到目前为止,本章中我们已经讨论了流的来源(用户活动事件,传感器和写入数据库),我们讨论了流如何传输(通过直接消息传送,通过消息代理和事件日志)。 -剩下的就是讨论一下你可以用流做什么 - 也就是说,你可以处理它。一般来说,有三种选择: +剩下的就是讨论一下你可以用流做什么 —— 也就是说,你可以处理它。一般来说,有三种选择: 1. 您可以将事件中的数据写入数据库,缓存,搜索索引或类似的存储系统,然后由其他客户端查询。如图11-5所示,这是保持数据库与系统其他部分发生更改同步的好方法 - 特别是当流消费者是写入数据库的唯一客户端时。写入存储系统的流程相当于我们在“批处理工作流程的输出”页面上讨论的内容。 2. 您可以以某种方式将事件推送给用户,例如通过发送电子邮件警报或推送通知,或通过将事件流式传输到可实时显示的实时仪表板。在这种情况下,人是流的最终消费者。 3. 您可以处理一个或多个输入流以产生一个或多个输出流。数据流可能会经过由几个这样的处理阶段组成的流水线,然后才会输出(选项1或2)。 -在本章的其余部分中,我们将讨论选项3:处理流以产生其他派生流。处理这样的流的代码片段被称为操作员或作业。它与我们在第10章中讨论过的Unix进程和MapReduce作业密切相关,数据流的模式是相似的:一个流处理器以只读的方式使用输入流,并将其输出写入一个不同的位置时尚。 +在本章的其余部分中,我们将讨论选项3:处理流以产生其他派生流。处理这样的流的代码片段被称为操作员或作业。它与我们在[第10章](ch10.md)中讨论过的Unix进程和MapReduce作业密切相关,数据流的模式是相似的:一个流处理器以只读的方式使用输入流,并将其输出写入一个不同的位置时尚。 -流处理器中的分区和并行化模式也非常类似于第10章中介绍的MapReduce和数据流引擎,因此我们不在这里重复这些主题。基本的映射操作(如转换和过滤记录)也是一样的。 +流处理器中的分区和并行化模式也非常类似于[第10章](ch10.md)中介绍的MapReduce和数据流引擎,因此我们不在这里重复这些主题。基本的映射操作(如转换和过滤记录)也是一样的。 批量作业的一个关键区别是流不会结束。这种差别有很多含义:正如本章开始部分所讨论的,排序对无界数据集没有意义,因此不能使用排序合并联接(请参阅“减少联接和分组”)。容错机制也必须改变:对于已经运行了几分钟的批处理作业,可以简单地从头开始重新启动失败的任务,但是对于已经运行数年的流作业,在开始后重新开始崩溃可能不是一个可行的选择。 @@ -460,18 +464,20 @@ CEP的实现包括Esper 【69】,IBM InfoSphere Streams 【70】,Apama,TIB * 计算一段时间内某个值的滚动平均值 * 将当前的统计数据与以前的时间间隔进行比较(例如,检测趋势或提醒与上周同期相比过高或过低的指标) -这些统计信息通常是在固定的时间间隔内进行计算的,例如,您可能想知道在过去5分钟内每秒对服务的平均查询次数,以及在此期间的第99百分位响应时间。在几分钟内平均,从一秒钟到下一秒钟平滑无关的波动,同时还能及时了解交通模式的任何变化。您汇总的时间间隔称为窗口,我们将在第468页的“关于时间的推理”中更详细地讨论窗口。 +这些统计信息通常是在固定的时间间隔内进行计算的,例如,您可能想知道在过去5分钟内每秒对服务的平均查询次数,以及在此期间的第99百分位响应时间。在几分钟内平均,从一秒钟到下一秒钟平滑无关的波动,同时还能及时了解交通模式的任何变化。您汇总的时间间隔称为窗口,我们将在“[关于时间的推理](#关于时间的推理)”中更详细地讨论窗口。 -流分析系统有时使用概率算法,例如Bloom filter(我们在第79页的“性能优化”中遇到过),设置成员资格,HyperLogLog 【72】基数估计以及各种百分比估计算法(请参阅“Percentiles in Practice “第16页)。概率算法产生近似的结果,但是具有在流处理器中比精确算法需要少得多的存储器的优点。近似算法的使用有时会使人们相信流处理系统总是有损和不精确的,但这是错误的:流处理没有任何内在的近似,而概率算法只是一个优化【73】。 +流分析系统有时使用概率算法,例如Bloom filter(我们在“[性能优化](ch3.md#性能优化)”中遇到过),设置成员资格,HyperLogLog 【72】基数估计以及各种百分比估计算法(请参阅“[实践中的百分位点](ch1.md#实践中的百分位点)“第16页)。概率算法产生近似的结果,但是具有在流处理器中比精确算法需要少得多的存储器的优点。近似算法的使用有时会使人们相信流处理系统总是有损和不精确的,但这是错误的:流处理没有任何内在的近似,而概率算法只是一个优化【73】。 + 许多开源分布式流处理框架的设计都是以分析为基础的:例如Apache Storm,Spark Streaming,Flink,Concord,Samza和Kafka Streams 【74】。托管服务包括Google Cloud Dataflow和Azure Stream Analytics。 #### 保持物化视图 -我们在第451页的“数据库和数据流”中看到,可以使用数据库更改流来保持派生数据系统(如缓存,搜索索引和数据仓库)与源数据库保持最新。我们可以将这些示例视为维护实体化视图的具体情况(请参阅“聚合:数据多维数据集和实例化视图”(第101页)):导出某个数据集的替代视图,以便可以高效地查询它,并在底层数据更改【50】。 +我们在“[数据库和数据流](#数据库和数据流)”中看到,可以使用数据库更改流来保持派生数据系统(如缓存,搜索索引和数据仓库)与源数据库保持最新。我们可以将这些示例视为维护实体化视图的具体情况(请参阅“[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”):导出某个数据集的替代视图,以便可以高效地查询它,并在底层数据更改【50】。 + +同样,在事件采购中,应用程序状态通过应用事件日志来维护;这里的应用状态也是一种物化视图。与流分析场景不同,在某个时间窗口内仅考虑事件通常是不够的:构建物化视图可能需要在任意时间段内的所有事件,除了可能由日志压缩丢弃的任何过时事件(请参阅“[日志压缩](#日志压缩)“)。实际上,您需要一个可以一直延伸到一开始的窗口。 -同样,在事件采购中,应用程序状态通过应用事件日志来维护;这里的应用状态也是一种物化视图。与流分析场景不同,在某个时间窗口内仅考虑事件通常是不够的:构建物化视图可能需要在任意时间段内的所有事件,除了可能由日志压缩丢弃的任何过时事件(请参阅“日志压缩“)。实际上,您需要一个可以一直延伸到一开始的窗口。 原则上,任何流处理器都可以用于物化视图维护,尽管永久维护事件的需要与一些主要在有限持续时间的窗口上运行的面向分析的框架的假设背道而驰。 Samza和Kafka Streams支持这种用法,建立在Kafka对夯实的支持上【75】。 #### 在流上搜索 @@ -484,14 +490,14 @@ CEP的实现包括Esper 【69】,IBM InfoSphere Streams 【70】,Apama,TIB #### 消息传递和RPC -在第136页的“消息传递数据流”中,我们讨论了消息传递系统作为RPC的替代方案,即作为通信服务的机制,例如在参与者模型中所使用的。虽然这些系统也是基于消息和事件,但我们通常不会将它们视为流处理器: +在第136页的“[消息传递数据流](ch4.md#消息传递数据流)”中,我们讨论了消息传递系统作为RPC的替代方案,即作为通信服务的机制,例如在参与者模型中所使用的。虽然这些系统也是基于消息和事件,但我们通常不会将它们视为流处理器: Actor框架主要是管理通信模块的并发和分布式执行的机制,而流处理主要是数据管理技术。 * 参与者之间的交流往往是短暂的,而且是一对一的,而事件日志则是持久的,多用户的。 * 参与者可以以任意方式进行通信(包括循环请求/响应模式),但流处理器通常设置在非循环流水线中,其中每个流是一个特定作业的输出,并且从一组明确定义的输入流派生。 -也就是说,RPC类系统和流处理之间有一些交叉区域。例如,Apache Storm有一个称为分布式RPC的功能,它允许将用户查询分散到一系列也处理事件流的节点上;这些查询然后与来自输入流的事件交织,结果可以被汇总并发回给用户【78】。 (另请参阅“多分区数据处理”(第514页)。) +也就是说,RPC类系统和流处理之间有一些交叉区域。例如,Apache Storm有一个称为分布式RPC的功能,它允许将用户查询分散到一系列也处理事件流的节点上;这些查询然后与来自输入流的事件交织,结果可以被汇总并发回给用户【78】。 (另参阅“[多分区数据处理](ch12.md#多分区数据处理)”) 也可以使用actor框架来处理流。但是,很多这样的框架在崩溃的情况下不能保证消息的传递,所以这个过程不是容错的,除非你实现了额外的重试逻辑。 @@ -501,13 +507,13 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 在批处理过程中,处理任务通过大量的历史事件迅速收缩。如果需要按时间分类,批处理需要查看每个事件中嵌入的时间戳。查看运行批处理的机器的系统时钟没有意义,因为处理运行的时间与事件实际发生的时间无关。 -批处理可以在几分钟内读取一年的历史事件;在大多数情况下,感兴趣的时间表是历史的一年,而不是几分钟的处理。而且,在事件中使用时间戳允许处理确定性的:在相同的输入上再次运行相同的处理过程会得到相同的结果(请参阅“故障容错”在页面429)。 +批处理可以在几分钟内读取一年的历史事件;在大多数情况下,感兴趣的时间表是历史的一年,而不是几分钟的处理。而且,在事件中使用时间戳允许处理确定性的:在相同的输入上再次运行相同的处理过程会得到相同的结果(参阅“[故障容错](ch10.md#故障容错)”)。 另一方面,许多流处理框架使用处理机器上的本地系统时钟(处理时间)来确定窗口【79】。这种方法具有简单的优点,事件创建和事件处理之间的延迟可以忽略不计。然而,如果存在任何显着的处理滞后,即处理可能比事件实际发生的时间显着晚,则会中断处理。 #### 事件时间与处理时间 -有许多原因可能会延迟处理:排队,网络故障(请参阅第267页的“不可靠的网络”),导致消息代理或处理器中出现争用的性能问题,重新启动流消费者或重新处理过去的事件(请参阅第451页的“重播旧消息”),或者在修复代码中的错误之后进行恢复。 +有许多原因可能会延迟处理:排队,网络故障(请参阅第267页的“[不可靠的网络](ch8.md#不可靠的网络)”),导致消息代理或处理器中出现争用的性能问题,重新启动流消费者或重新处理过去的事件(参阅“[重播旧消息](#重播旧消息)”),或者在修复代码中的错误之后进行恢复。 而且,消息延迟还可能导致消息的不可预知的排序。例如,假设用户首先发出一个Web请求(由Web服务器A处理),然后发出第二个请求(由服务器B处理)。 A和B发出描述他们处理的请求的事件,但是B的事件在A的事件发生之前到达消息代理。现在,流处理器将首先看到B事件,然后看到A事件,即使它们实际上是以相反的顺序发生的。 @@ -515,7 +521,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 [^ii]: 感谢Flink社区的Kostas Kloudas提出这个比喻。 -令人困惑的事件时间和处理时间导致错误的数据。例如,假设您有一个流处理器来测量请求率(计算每秒请求数)。如果您重新部署流处理器,则可能会关闭一分钟,并在事件恢复时处理积压的事件。如果您根据处理时间来衡量速率,那么看起来好像在处理积压时突然出现异常的请求高峰,而事实上请求的实际速率是稳定的(图11-7)。 +令人困惑的事件时间和处理时间导致错误的数据。例如,假设您有一个流处理器来测量请求率(计算每秒请求数)。如果您重新部署流处理器,则可能会关闭一分钟,并在事件恢复时处理积压的事件。如果您根据处理时间来衡量速率,那么看起来好像在处理积压时突然出现异常的请求高峰,而事实上请求的实际速率是稳定的([图11-7](img/fig11-7.png))。 ![](img/fig11-7.png) @@ -538,7 +544,7 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 当事件可以在系统中的多个点缓冲时,为事件分配时间戳更加困难。例如,考虑将使用率度量的事件报告给服务器的移动应用程序。该应用程序可能会在设备处于脱机状态时使用,在这种情况下,它将在设备上本地缓冲事件,并在下一次可用的互联网连接(可能是几小时甚至几天)时将它们发送到服务器。对于这个流的任何消费者来说,这些事件将显示为极其滞后的落后者。 -在这种情况下,根据移动设备的本地时钟,事件的时间戳实际上应该是发生用户交互的时间。但是,用户控制的设备上的时钟通常是不可信的,因为它可能会被意外或故意设置为错误的时间(请参见“时钟同步和精度”(第269页))。服务器收到事件的时间(根据服务器的时钟)更可能是准确的,因为服务器在您的控制之下,但在描述用户交互方面意义不大。 +在这种情况下,根据移动设备的本地时钟,事件的时间戳实际上应该是发生用户交互的时间。但是,用户控制的设备上的时钟通常是不可信的,因为它可能会被意外或故意设置为错误的时间(请参见“[时钟同步与准确性](ch8.md#时钟同步与准确性)”)。服务器收到事件的时间(根据服务器的时钟)更可能是准确的,因为服务器在您的控制之下,但在描述用户交互方面意义不大。 要调整不正确的设备时钟,一种方法是记录三个时间戳【82】: @@ -556,11 +562,11 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 ***Tumbling窗口*** -一个翻滚的窗口有一个固定的长度,每个事件都属于一个窗口。例如,如果您有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窗*** -跳频窗口也具有固定的长度,但允许窗口重叠以提供一些平滑。例如,1分钟跳跃大小的5分钟窗口将包含10:03:00至10:07:59之间的事件,则下一个窗口将覆盖10:04:00至10:08之间的事件: 59,等等。您可以通过首先计算1分钟滚动窗口,然后聚合在几个相邻的窗口上来实现此跳频窗口。 +跳频窗口也具有固定的长度,但允许窗口重叠以提供一些平滑。例如,1分钟跳跃大小的5分钟窗口将包含`10:03:00`至`10:07:59`之间的事件,则下一个窗口将覆盖`10:04:00`至`10:08`之间的事件: 59,等等。您可以通过首先计算1分钟滚动窗口,然后聚合在几个相邻的窗口上来实现此跳频窗口。 ***滑动窗口*** @@ -568,13 +574,13 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 ***会话窗口*** -与其他窗口类型不同,会话窗口没有固定的持续时间。相反,它是通过将同一用户的所有事件分组在一起,并在时间上紧密地组合在一起来定义的,并且当用户在一段时间内不活动时(例如,如果30分钟内没有事件),窗口结束。会话化是网站分析的常见要求(请参阅第406页的“GROUP BY”)。 +与其他窗口类型不同,会话窗口没有固定的持续时间。相反,它是通过将同一用户的所有事件分组在一起,并在时间上紧密地组合在一起来定义的,并且当用户在一段时间内不活动时(例如,如果30分钟内没有事件),窗口结束。会话化是网站分析的常见要求(参阅“[GROUP BY](ch10.md#GROUP BY)”)。 ### 流式连接 -在第10章中,我们讨论了批处理作业如何通过关键连接数据集,以及这种连接如何构成数据管道的重要组成部分。由于流处理将数据管道概括为对无界数据集进行增量处理,因此对流进行连接的需求也完全相同。 +在[第10章](ch10.md)中,我们讨论了批处理作业如何通过关键连接数据集,以及这种连接如何构成数据管道的重要组成部分。由于流处理将数据管道概括为对无界数据集进行增量处理,因此对流进行连接的需求也完全相同。 -然而,新事件随时可能出现在一个流中,这使得加入流比批处理作业更具挑战性。为了更好地理解情况,我们来区分三种不同类型的连接:流 - 流连接,流表连接和表连接【84】。在下面的章节中,我们将通过例子来说明。 +然而,新事件随时可能出现在一个流中,这使得加入流比批处理作业更具挑战性。为了更好地理解情况,我们来区分三种不同类型的连接:流-流连接,流表连接和表连接【84】。在下面的章节中,我们将通过例子来说明。 流 - 流连接(窗口连接) 假设您的网站上有搜索功能,并且想要检测搜索到的网址的近期趋势。每次有人输入搜索查询时,都会记录包含查询和返回结果的事件。每当有人点击其中一个搜索结果时,就会记录另一个记录点击的事件。为了计算搜索结果中每个网址的点击率,您需要将搜索操作和点击操作的事件组合在一起,这些事件通过具有相同的会话ID进行连接。广告系统需要类似的分析【85】。 @@ -587,19 +593,21 @@ Actor框架主要是管理通信模块的并发和分布式执行的机制,而 #### 流表连接(stream enrichment) -在第404页的“示例:用户活动事件分析”(图10-2)中,我们看到了加入两个数据集的批量作业示例:一组用户活动事件和一个用户配置文件数据库。将用户活动事件视为流,并在流处理器中连续执行相同的连接是很自然的:输入是 +在“[示例:用户活动事件分析](ch10.md#示例:用户活动事件分析)”([图10-2](img/fig10-2.png))中,我们看到了加入两个数据集的批量作业示例:一组用户活动事件和一个用户配置文件数据库。将用户活动事件视为流,并在流处理器中连续执行相同的连接是很自然的:输入是包含用户ID的活动事件流,并且输出是活动事件流,其中用户ID已经用关于用户的简档信息来扩充。这个过程有时被称为使用来自数据库的信息来丰富活动事件。 -包含用户ID的活动事件流,并且输出是活动事件流,其中用户ID已经用关于用户的简档信息来扩充。这个过程有时被称为使用来自数据库的信息来丰富活动事件。 +要执行此联接,流程过程需要一次查看一个活动事件,在数据库中查找事件的用户标识,并将该概要信息添加到活动事件中。数据库查询可以通过查询远程数据库来实现;但是,正如在“[示例:分析用户活动事件](ch10.md#示例:分析用户活动事件)”一节中讨论的,此类远程查询可能会很慢并且有可能导致数据库过载【75】。 + +另一种方法是将数据库副本加载到流处理器中,以便在本地进行查询而无需网络往返。这种技术与我们在“[Map端连接](ch10.md#Map端连接)”中讨论的散列连接非常相似:如果数据库的本地副本足够小,则本地副本可能是内存中的散列表,或者是本地磁盘上的索引。 -要执行此联接,流程过程需要一次查看一个活动事件,在数据库中查找事件的用户标识,并将该概要信息添加到活动事件中。数据库查询可以通过查询远程数据库来实现;但是,正如在“示例:分析用户活动事件”一节中讨论的,此类远程查询可能会很慢并且有可能导致数据库过载【75】。 -另一种方法是将数据库副本加载到流处理器中,以便在本地进行查询而无需网络往返。这种技术与我们在408页的“Map-Side连接”中讨论的散列连接非常相似:如果数据库的本地副本足够小,则本地副本可能是内存中的散列表,或者是本地磁盘上的索引。 与批处理作业的区别在于批处理作业使用数据库的时间点快照作为输入,而流处理器是长时间运行的,并且数据库的内容可能随时间而改变,所以流处理器数据库的本地副本需要保持最新。这个问题可以通过更改数据捕获来解决:流处理器可以订阅用户配置文件数据库的更新日志以及活动事件流。在创建或修改配置文件时,流处理器会更新其本地副本。因此,我们获得两个流之间的连接:活动事件和配置文件更新。 + 流表连接实际上非常类似于流 - 流连接;最大的区别在于对于表changelog流,连接使用一个可以回溯到“开始时间”(概念上是无限的窗口)的窗口,新版本的记录会覆盖较早的版本。对于流输入,连接可能根本没有维护窗口。 #### 表格表连接(物化视图维护) -考虑我们在第11页的“描述负载”中讨论的Twitter时间线示例。我们说过,当用户想要查看他们的家庭时间线时,对用户所关注的所有人进行迭代是非常昂贵的,推文,并合并它们。 -相反,我们需要一个时间线缓存:一种每个用户的“收件箱”,在发送tweets的时候写入这些信息,以便读取时间线是一次查询。实现和维护此缓存需要以下事件处理: +考虑我们在“[描述负载](ch1.md#描述负载)”中讨论的推特时间线例子。我们说过,当用户想要查看他们的主页时间线时,对用户所关注的所有人进行迭代是非常昂贵的,推文,并合并它们。 + +相反,我们需要一个时间线缓存:一种每个用户的“收件箱”,在发送推文的时候写入这些信息,以便读取时间线是一次查询。实现和维护此缓存需要以下事件处理: * 当用户发送新的推文时,它将被添加到每个跟随你的用户的时间线上。 * 用户删除推文时,将从所有用户的时间表中删除。 @@ -619,7 +627,7 @@ GROUP BY follows.follower_id 流的连接直接对应于该查询中的表的连接。时间轴实际上是这个查询结果的缓存,每当基础表发生变化时都会更新[^iii]。 -[^iii]: 如果你把一个流视为一个表的衍生物,如图11-6所示,并且把一个连接看作是两个表u·v的乘积,那么会发生一些有趣的事情:物化连接的变化流遵循产品规则 u·v)'= u'v + uv'。 换句话说,任何tweets的变化都与当前的追随者联系在一起,任何追随者的变化都与当前的tweets [49,50]相结合。 +[^iii]: 如果你把一个流视为一个表的衍生物,如[图11-6](img/fig11-6.png)所示,并且把一个连接看作是两个表u·v的乘积,那么会发生一些有趣的事情:物化连接的变化流遵循产品规则(u·v)'= u'v + uv'(u·v)'= u'v + uv'。 换句话说,任何tweets的变化都与当前的追随者联系在一起,任何追随者的变化都与当前的tweets 【49,50】相结合。 #### 连接的时间依赖性 @@ -633,15 +641,15 @@ GROUP BY follows.follower_id ### 容错 -在本章的最后一节中,让我们考虑流处理器如何容忍错误。我们在第10章中看到,批处理框架可以很容易地容忍错误:如果MapReduce作业中的任务失败,可以简单地在另一台机器上重新启动,并且丢弃失败任务的输出。这种透明的重试是可能的,因为输入文件是不可变的,每个任务都将其输出写入到HDFS上的单独文件,并且输出仅在任务成功完成时可见。 +在本章的最后一节中,让我们考虑流处理器如何容忍错误。我们在[第10章](ch10.md)中看到,批处理框架可以很容易地容忍错误:如果MapReduce作业中的任务失败,可以简单地在另一台机器上重新启动,并且丢弃失败任务的输出。这种透明的重试是可能的,因为输入文件是不可变的,每个任务都将其输出写入到HDFS上的单独文件,并且输出仅在任务成功完成时可见。 -特别是,批处理容错方法可确保批处理作业的输出与没有出错的情况相同,即使事实上某些任务失败了。看起来好像每个输入记录都被处理了一次 - 没有记录被跳过,而且没有处理两次。尽管重新启动任务意味着实际上可能会多次处理记录,但输出中的可见效果好像只处理过一次。这个原则被称为一次语义学,虽然有效 - 一次将是一个更具描述性的术语【90】。 +特别是,批处理容错方法可确保批处理作业的输出与没有出错的情况相同,即使事实上某些任务失败了。看起来好像每个输入记录都被处理了一次 —— 没有记录被跳过,而且没有处理两次。尽管重新启动任务意味着实际上可能会多次处理记录,但输出中的可见效果好像只处理过一次。这个原则被称为一次语义学,虽然有效 —— 一次将是一个更具描述性的术语【90】。 在流处理过程中也出现了同样的容错问题,但是处理起来不那么直观:等到某个任务完成之后才使其输出可见,因为流是无限的,因此您永远无法完成处理。 #### 小批量和检查点 -一个解决方案是将流分解成小块,并像小型批处理一样处理每个块。这种方法被称为microbatching,它被用于Spark Streaming 【91】。批处理大小通常约为1秒,这是性能折中的结果:较小的批次会导致更大的调度和协调开销,而较大的批次意味着流处理器的结果变得可见之前的较长延迟。 +一个解决方案是将流分解成小块,并像小型批处理一样处理每个块。这种方法被称为**小批量(microbatching)**,它被用于Spark Streaming 【91】。批处理大小通常约为1秒,这是性能折中的结果:较小的批次会导致更大的调度和协调开销,而较大的批次意味着流处理器的结果变得可见之前的较长延迟。 微缩也隐含地提供了与批量大小相等的翻滚窗口(通过处理时间而不是事件时间戳)。任何需要更大窗口的作业都需要明确地将状态从一个微阵列转移到下一个微阵列。 @@ -655,7 +663,7 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 这些事情要么都是原子地发生,要么都不发生,但是不应该彼此不同步。如果这种方法听起来很熟悉,那是因为我们在分布式事务和两阶段提交的情况下,在第360页的“准确一次的消息处理”中讨论了它。 -在第9章中,我们讨论了分布式交易(如XA)的传统实现中的问题。然而,在更受限制的环境中,可以有效地实现这样的原子提交设施。 Google云数据流【81,92】和VoltDB 【94】中使用了这种方法,并计划在Apache Kafka 【95,96】中添加类似的功能。与XA不同,这些实现不会尝试跨异构技术提供事务,而是通过在流处理框架中管理状态更改和消息传递来保持内部事务。事务协议的开销可以通过在单个事务中处理几个输入消息来分摊。 +在[第9章](ch9.md)中,我们讨论了分布式交易(如XA)的传统实现中的问题。然而,在更受限制的环境中,可以有效地实现这样的原子提交设施。 Google云数据流【81,92】和VoltDB 【94】中使用了这种方法,并计划在Apache Kafka 【95,96】中添加类似的功能。与XA不同,这些实现不会尝试跨异构技术提供事务,而是通过在流处理框架中管理状态更改和消息传递来保持内部事务。事务协议的开销可以通过在单个事务中处理几个输入消息来分摊。 #### 幂等 @@ -665,19 +673,19 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 即使一个操作不是天生的幂等,它往往可以与一些额外的元数据幂等。例如,在使用来自卡夫卡的消息时,每条消息都有一个持续的,单调递增的偏移量。将值写入外部数据库时,可以将触发上次写入的消息的偏移量与值包含在一起。因此,您可以判断是否已应用更新,并避免再次执行相同的更新。 -风暴三叉戟的状态处理基于类似的想法【78】。依赖幂等性意味着一些假设:重启一个失败的任务必须以相同的顺序重播相同的消息(一个基于日志的消息代理这样做),处理必须是确定性的,其他节点不能同时更新相同的值[ 98,99]。 +风暴三叉戟的状态处理基于类似的想法【78】。依赖幂等性意味着一些假设:重启一个失败的任务必须以相同的顺序重播相同的消息(一个基于日志的消息代理这样做),处理必须是确定性的,其他节点不能同时更新相同的值【98,99】。 -当从一个处理节点故障转移到另一个处理节点时,可能需要进行防护(请参阅第291页上的“领导和锁定”),以防止被认为是死的节点的干扰 +当从一个处理节点故障转移到另一个处理节点时,可能需要进行防护(参阅“[领导和锁](ch8.md#领导和锁)”),以防止被认为是死的节点的干扰 #### 失败后重建状态 任何需要状态的流进程(例如,任何窗口聚合(例如计数器,平均值和直方图)以及用于连接的任何表和索引)都必须确保在失败之后可以恢复此状态。 -一种选择是将状态保持在远程数据存储中并复制它,尽管如每个单独消息的远程数据库查询速度可能会很慢,正如在“流表加入(第473页)”中所述。另一种方法是保持流处理器的本地状态,并定期复制。然后,当流处理器从故障中恢复时,新任务可以读取复制状态并恢复处理而不丢失数据。 +一种选择是将状态保持在远程数据存储中并复制它,尽管如每个单独消息的远程数据库查询速度可能会很慢,正如在“[流表连接](#流表连接)”中所述。另一种方法是保持流处理器的本地状态,并定期复制。然后,当流处理器从故障中恢复时,新任务可以读取复制状态并恢复处理而不丢失数据。 -例如,Flink定期捕获操作员状态的快照,并将它们写入HDFS等持久存储器中【92,93】。 Samza和Kafka Streams通过将状态更改发送到具有日志压缩功能的专用Kafka主题来复制状态更改,这类似于更改数据捕获[84,100]。 VoltDB通过冗余处理多个节点上的每个输入消息来复制状态(请参阅第252页的“实际的串行执行”)。 +例如,Flink定期捕获操作员状态的快照,并将它们写入HDFS等持久存储器中【92,93】。 Samza和Kafka Streams通过将状态更改发送到具有日志压缩功能的专用Kafka主题来复制状态更改,这类似于更改数据捕获【84,100】。 VoltDB通过冗余处理多个节点上的每个输入消息来复制状态(请参阅第252页的“实际的串行执行”)。 -在某些情况下,甚至可能不需要复制状态,因为它可以从输入流重建。例如,如果状态由一个相当短的窗口中的聚合组成,则它可能足够快,以便重放与该窗口相对应的输入事件。如果状态是通过更改数据捕获维护的数据库的本地副本,那么也可以从日志压缩的更改流重建数据库(请参阅“日志压缩”一节第456页)。 +在某些情况下,甚至可能不需要复制状态,因为它可以从输入流重建。例如,如果状态由一个相当短的窗口中的聚合组成,则它可能足够快,以便重放与该窗口相对应的输入事件。如果状态是通过更改数据捕获维护的数据库的本地副本,那么也可以从日志压缩的更改流重建数据库(请参阅“[日志压缩](#日志压缩)”一节)。 但是,所有这些权衡取决于底层基础架构的性能特征:在某些系统中,网络延迟可能低于磁盘访问延迟,网络带宽可能与磁盘带宽相当。在所有情况下都没有普遍理想的权衡,随着存储和网络技术的发展,本地和远程状态的优点也可能会发生变化。 @@ -685,24 +693,28 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 ## 本章小结 -在本章中,我们讨论了事件流,他们所服务的目的以及如何处理它们。在某些方面,流处理非常类似于我们在第10章讨论的批处理,而是在无限的(永无止境的)流而不是固定大小的输入上持续进行。从这个角度来看,消息代理和事件日志可以作为文件系统的流媒体。 +在本章中,我们讨论了事件流,他们所服务的目的以及如何处理它们。在某些方面,流处理非常类似于我们在[第10章](ch10.md)讨论的批处理,而是在无限的(永无止境的)流而不是固定大小的输入上持续进行。从这个角度来看,消息代理和事件日志可以作为文件系统的流媒体。 我们花了一些时间比较两种消息代理: ***AMQP/JMS风格的消息代理*** + + ​ 经纪人将个人消息分配给消费者,消费者在成功处理个人消息时确认消息。消息被确认后从代理中删除。这种方法适合作为RPC的异步形式(另请参阅第136页的“消息传递数据流”),例如在任务队列中,消息处理的确切顺序并不重要,没有在处理之后,需要重新读取旧消息。 ***基于日志的消息代理*** ​ 代理将分区中的所有消息分配给相同的使用者节点,并始终以相同的顺序传递消息。并行性是通过划分来实现的,消费者通过检查他们所处理的最后一个消息的偏移来跟踪他们的进度。代理将消息保留在磁盘上,因此如有必要,可以跳回并重新读取旧消息。 -基于日志的方法与数据库中的复制日志(参见第5章)和日志结构存储引擎(请参阅第3章)具有相似之处。我们看到,这种方法特别适用于消耗输入流并生成派生状态或派生输出流的流处理系统。 -就流的来源而言,我们讨论了几种可能性:用户活动事件,提供定期读数的传感器和数据馈送(例如金融市场数据)自然地表示为流。我们看到,将数据写入数据流也是有用的:我们可以捕获更改日志 - 即对数据库所做的所有更改的历史记录 - 隐式地通过更改数据捕获或通过事件明确地捕获采购。日志压缩允许流保留数据库内容的完整副本。 + +基于日志的方法与数据库中的复制日志(参见[第5章](ch5.md))和日志结构存储引擎(请参阅[第3章](ch3.md))具有相似之处。我们看到,这种方法特别适用于消耗输入流并生成派生状态或派生输出流的流处理系统。 + +就流的来源而言,我们讨论了几种可能性:用户活动事件,提供定期读数的传感器和数据馈送(例如金融市场数据)自然地表示为流。我们看到,将数据写入数据流也是有用的:我们可以捕获更改日志 —— 即对数据库所做的所有更改的历史记录 —— 隐式地通过更改数据捕获或通过事件明确地捕获采购。日志压缩允许流保留数据库内容的完整副本。 将数据库表示为流为系统集成提供了强大的机会。您可以通过使用更改日志并将其应用于派生系统,使派生的数据系统(如搜索索引,缓存和分析系统)保持最新。您甚至可以从头开始,从开始一直到现在消耗更改的日志,从而为现有数据构建新的视图。 -将状态保持为流并重放消息的设施也是在各种流处理框架中实现流连接和容错的技术的基础。我们讨论了流处理的几个目的,包括搜索事件模式(复杂事件处理),计算加窗聚合(流分析)以及保持派生数据系统处于最新状态(材料化视图)。 +将状态保持为流并重放消息的设施也是在各种流处理框架中实现流连接和容错的技术的基础。我们讨论了流处理的几个目的,包括搜索事件模式(复杂事件处理),计算加窗聚合(流分析)以及保持派生数据系统处于最新状态(物化视图)。 然后我们讨论了在流处理器中推理时间的困难,包括处理时间和事件时间戳之间的区别,以及在你认为窗口完成之后处理到达的离散事件的问题。 From e6df7385f65892b6d23267cf6e8de92d16e80c6f Mon Sep 17 00:00:00 2001 From: Vonng Date: Mon, 26 Mar 2018 03:49:15 +0800 Subject: [PATCH 05/17] ch11 link fix --- README.md | 2 +- ch11.md | 50 +++++++++++++++++++++----------------------------- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 47ec489..e1a1be0 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ | 第九章:一致性与共识 | 初翻 65% | Vonng | | 第三部分:前言 | 精翻 | | | 第十章:批处理 | 草翻 | | -| 第十一章:流处理 | 机翻 | | +| 第十一章:流处理 | 草翻 | | | 第十二章:数据系统的未来 | 机翻 | | | 术语表 | - | | | 后记 | 机翻 | | diff --git a/ch11.md b/ch11.md index e00cee8..ffdffff 100644 --- a/ch11.md +++ b/ch11.md @@ -269,7 +269,7 @@ Apache Kafka 【17,18】,Amazon Kinesis Streams 【19】和Twitter的Distribut 我们可以调用日志消费者导出的数据系统,正如在第三部分的介绍中所讨论的:存储在搜索索引和数据仓库中的数据只是记录系统中数据的另一个视图。更改数据捕获是一种机制,可确保对记录系统所做的所有更改都反映在派生数据系统中,以便派生系统具有数据的准确副本。 -从本质上说,改变数据捕获使得一个数据库成为领导者(从中捕获变化的数据库),并将其他人变成追随者。基于日志的消息代理非常适合从源数据库传输更改事件,因为它保留了消息的排序(避免了图11-2的重新排序问题)。 +从本质上说,改变数据捕获使得一个数据库成为领导者(从中捕获变化的数据库),并将其他人变成追随者。基于日志的消息代理非常适合从源数据库传输更改事件,因为它保留了消息的排序(避免了[图11-2](img/fig11-2.png)的重新排序问题)。 数据库触发器可用于通过注册触发器来实现更改数据捕获(参阅“[基于触发器的复制](ch5.md#基于触发器的复制)”),这些触发器可观察数据表的所有更改,并将相应的条目添加到更改日志表中。但是,他们往往是脆弱的,并有显着的性能开销。解析复制日志可以是一个更强大的方法,但它也带来了挑战,例如处理模式更改。 @@ -316,7 +316,7 @@ Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获 * 在更改数据捕获中,应用程序以可变方式使用数据库,随意更新和删除记录。从数据库中提取较低级别的更改日志(例如,通过解析复制日志),从而确保从数据库中提取的写入顺序与实际写入的顺序相匹配,从而避免[图11-4](img/fig11-4.png)中的竞争条件。写入数据库的应用程序不需要知道CDC正在发生。 * 在事件源中,应用程序逻辑是基于写入事件日志的不可变事件而显式构建的。在这种情况下,事件存储是附加的,更新或删除是不鼓励或禁止的。事件旨在反映应用程序级别发生的事情,而不是低级状态更改。 -事件源是一种强大的数据建模技术:从应用程序的角度来看,将用户的行为记录为不可变的事件更有意义,而不是记录这些行为对可变数据库的影响。事件采购使得随着时间的推移而逐渐发展应用程序变得更加容易,通过更容易理解事情发生的原因以及防范应用程序错误(请参阅“不可变事件的优点”),帮助进行调试。 +事件源是一种强大的数据建模技术:从应用程序的角度来看,将用户的行为记录为不可变的事件更有意义,而不是记录这些行为对可变数据库的影响。事件采购使得随着时间的推移而逐渐发展应用程序变得更加容易,通过更容易理解事情发生的原因以及防范应用程序错误(请参阅“[不可变事件的优点](#不可变事件的优点)”),帮助进行调试。 例如,存储“学生取消课程注册”事件清楚地表达了单一行为的中性意图,而副作用“从注册表中删除了一个条目,并且一个取消原因被添加到学生反馈表“嵌入了很多有关方式的假设数据稍后将被使用。如果引入新的应用程序功能,例如“将地点提供给等待列表中的下一个人” —— 事件顺序方法允许将新的副作用轻松地链接到现有事件上。 @@ -359,7 +359,7 @@ Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获 无论国家如何变化,总会有一系列事件导致这些变化。即使事情已经解决,事实仍然是事实发生的事实。关键的想法是可变状态和不可变事件的附加日志不相互矛盾:它们是同一枚硬币的两面。所有变化的日志,变化日志,代表了随着时间的推移状态的演变。 -如果您有数学上的倾向,那么您可能会说应用程序状态是随着时间的推移整合了一个事件流而得到的,而且当您按照时间区分状态时会得到一个更改流,如图11-6所示[ 49,50,51]。这个比喻有一定的局限性(例如,国家的二阶导数似乎没有意义),但这是考虑数据的一个有用的起点。 +如果您有数学上的倾向,那么您可能会说应用程序状态是随着时间的推移整合了一个事件流而得到的,而且当您按照时间区分状态时会得到一个更改流,如[图11-6](img/fig11-6.png)所示【49,50,51】。这个比喻有一定的局限性(例如,国家的二阶导数似乎没有意义),但这是考虑数据的一个有用的起点。 ![](img/fig11-6.png) @@ -383,7 +383,7 @@ Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获 #### 从同一事件日志中获取多个视图 -而且,通过从不变事件日志中分离可变状态,可以从事件的相同日志中派生出几个不同的面向读取的表示。这就像一个流的多个消费者一样工作([图11-5](img/fig11-5.png)):例如,分析数据库Druid使用这种方法从Kafka直接获取【55】,Pista chio是一个分布式的键值存储,使用Kafka作为提交日志【56】,Kafka Connect接收器可以将来自Kafka的数据导出到各种不同的数据库和索引【41】。对于许多其他存储和索引系统(如搜索服务器)来说,类似地从分布式日志中获取输入也是有意义的(请参阅第455页上的“保持系统同步”)。 +而且,通过从不变事件日志中分离可变状态,可以从事件的相同日志中派生出几个不同的面向读取的表示。这就像一个流的多个消费者一样工作([图11-5](img/fig11-5.png)):例如,分析数据库Druid使用这种方法从Kafka直接获取【55】,Pista chio是一个分布式的键值存储,使用Kafka作为提交日志【56】,Kafka Connect接收器可以将来自Kafka的数据导出到各种不同的数据库和索引【41】。对于许多其他存储和索引系统(如搜索服务器)来说,类似地从分布式日志中获取输入也是有意义的(请参阅“[保持系统同步](#保持系统同步)”)。 从事件日志到数据库有一个明确的转换步骤,可以更容易地随时间推移您的应用程序:如果您想要引入一个以新的方式呈现现有数据的新功能,您可以使用事件日志来构建一个单独的新功能的读取优化视图,并与现有的一起运行 @@ -391,19 +391,19 @@ Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获 如果您不必担心如何查询和访问数据,那么存储数据通常是非常简单的。模式设计,索引和存储引擎的许多复杂性都是希望支持某些查询和访问模式的结果(参见[第3章](ch3.md))。出于这个原因,通过将数据写入的形式与读取形式分开,并允许几个不同的读取视图,可以获得很大的灵活性。这个想法有时被称为命令查询责任分离(CQRS)【42,58,59】。 -数据库和模式设计的传统方法是基于数据必须以与查询相同的形式写入的谬误。有关正常化和非规范化的争论(请参阅第31页上的“多对一和多对多关系”),如果可以将数据从写入优化的事件日志转换为读取优化的应用程序状态,则变得基本无关紧要:在读取优化的视图中对数据进行非规范化是完全合理的,因为翻译过程为您提供了一种机制,使其与事件日志保持一致。 +数据库和模式设计的传统方法是基于数据必须以与查询相同的形式写入的谬误。有关正常化和非规范化的争论(参阅“[多对一和多对多的关系](ch2.md#多对一和多对多的关系)”),如果可以将数据从写入优化的事件日志转换为读取优化的应用程序状态,则变得基本无关紧要:在读取优化的视图中对数据进行非规范化是完全合理的,因为翻译过程为您提供了一种机制,使其与事件日志保持一致。 在“[描述负载](ch1.md#描述负载)”中,我们讨论了推特主页时间表,最近一个特定用户正在关注的人(如邮箱)写的最近发布的推文缓存。这是阅读优化状态的另一个例子:家庭时间表高度变形,因为你的推文在所有跟随你的人的时间线上都是重复的。然而,扇出服务保持这种复制状态与新的推文和新的以下关系保持同步,这保持了复制的可管理性。 #### 并发控制 -事件采集和更改数据捕获的最大缺点是事件日志的消费者通常是异步的,所以用户可能会写入日志,然后从日志派生的视图中读取并查找他们的写作还没有反映在读取视图。我们在第162页的“阅读您自己的作品”中讨论了这个问题和潜在的解决方案。 +事件采集和更改数据捕获的最大缺点是事件日志的消费者通常是异步的,所以用户可能会写入日志,然后从日志派生的视图中读取并查找他们的写作还没有反映在读取视图。我们在“[读己之写](ch5.md#读己之写)”中讨论了这个问题和潜在的解决方案。 -一种解决方案是同步执行读取视图的更新,并将事件附加到日志中。这需要一个事务来将写入操作合并到一个原子单元中,所以要么需要将事件日志和读取视图保存在同一个存储系统中,要么需要跨不同系统的分布式事务。或者,您可以使用第350页上的“使用总订单广播实现线性化存储”中讨论的方法。 +一种解决方案是同步执行读取视图的更新,并将事件附加到日志中。这需要一个事务来将写入操作合并到一个原子单元中,所以要么需要将事件日志和读取视图保存在同一个存储系统中,要么需要跨不同系统的分布式事务。或者,您可以使用在“[使用全序广播实现线性化存储](ch9.md#使用全序广播实现线性化存储)”中讨论的方法。 -另一方面,从事件日志导出当前状态也简化了并发控制的某些方面。对多个对象事务的需求(请参阅第228页上的“单对象和多对象操作”)源于单个用户操作,需要在多个不同的位置更改数据。通过事件采购,您可以设计一个事件,以便对用户操作进行独立的描述。用户操作只需要在一个地方进行一次写操作,即将事件附加到日志中,这很容易使原子化。 +另一方面,从事件日志导出当前状态也简化了并发控制的某些方面。对多个对象事务的需求(参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)源于单个用户操作,需要在多个不同的位置更改数据。通过事件采购,您可以设计一个事件,以便对用户操作进行独立的描述。用户操作只需要在一个地方进行一次写操作,即将事件附加到日志中,这很容易使原子化。 -如果事件日志和应用程序状态以相同的方式分区(例如,为分区3中的客户处理事件只需要更新应用程序状态的分区3),则直接的单线程日志消费者不需要并发控制(write-by)构造,它一次只处理一个事件(另请参阅第252页的“实际的串行执行”)。该日志通过在分区中定义事件的串行顺序来消除并发性的不确定性【24】。如果一个事件触及多个状态分区,那么需要做更多的工作,我们将在第12章讨论。 +如果事件日志和应用程序状态以相同的方式分区(例如,为分区3中的客户处理事件只需要更新应用程序状态的分区3),则直接的单线程日志消费者不需要并发控制(write-by)构造,它一次只处理一个事件(参阅“[真的的串行执行](ch7.md#真的的串行执行)”)。该日志通过在分区中定义事件的串行顺序来消除并发性的不确定性【24】。如果一个事件触及多个状态分区,那么需要做更多的工作,我们将在[第12章](ch12.md)讨论。 #### 不变性的限制 @@ -425,7 +425,7 @@ Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获 剩下的就是讨论一下你可以用流做什么 —— 也就是说,你可以处理它。一般来说,有三种选择: -1. 您可以将事件中的数据写入数据库,缓存,搜索索引或类似的存储系统,然后由其他客户端查询。如图11-5所示,这是保持数据库与系统其他部分发生更改同步的好方法 - 特别是当流消费者是写入数据库的唯一客户端时。写入存储系统的流程相当于我们在“批处理工作流程的输出”页面上讨论的内容。 +1. 您可以将事件中的数据写入数据库,缓存,搜索索引或类似的存储系统,然后由其他客户端查询。如[图11-5](img/fig11-5.png)所示,这是保持数据库与系统其他部分发生更改同步的好方法 —— 特别是当流消费者是写入数据库的唯一客户端时。写入存储系统的流程相当于我们在“批处理工作流程的输出”页面上讨论的内容。 2. 您可以以某种方式将事件推送给用户,例如通过发送电子邮件警报或推送通知,或通过将事件流式传输到可实时显示的实时仪表板。在这种情况下,人是流的最终消费者。 3. 您可以处理一个或多个输入流以产生一个或多个输出流。数据流可能会经过由几个这样的处理阶段组成的流水线,然后才会输出(选项1或2)。 @@ -433,7 +433,7 @@ Kafka Connect 【41】致力于将广泛的数据库系统的变更数据捕获 流处理器中的分区和并行化模式也非常类似于[第10章](ch10.md)中介绍的MapReduce和数据流引擎,因此我们不在这里重复这些主题。基本的映射操作(如转换和过滤记录)也是一样的。 -批量作业的一个关键区别是流不会结束。这种差别有很多含义:正如本章开始部分所讨论的,排序对无界数据集没有意义,因此不能使用排序合并联接(请参阅“减少联接和分组”)。容错机制也必须改变:对于已经运行了几分钟的批处理作业,可以简单地从头开始重新启动失败的任务,但是对于已经运行数年的流作业,在开始后重新开始崩溃可能不是一个可行的选择。 +批量作业的一个关键区别是流不会结束。这种差别有很多含义:正如本章开始部分所讨论的,排序对无界数据集没有意义,因此不能使用排序合并联接(请参阅“[减少连接和分组](ch10.md#减少连接和分组)”)。容错机制也必须改变:对于已经运行了几分钟的批处理作业,可以简单地从头开始重新启动失败的任务,但是对于已经运行数年的流作业,在开始后重新开始崩溃可能不是一个可行的选择。 ### 流处理的应用 @@ -683,7 +683,7 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 一种选择是将状态保持在远程数据存储中并复制它,尽管如每个单独消息的远程数据库查询速度可能会很慢,正如在“[流表连接](#流表连接)”中所述。另一种方法是保持流处理器的本地状态,并定期复制。然后,当流处理器从故障中恢复时,新任务可以读取复制状态并恢复处理而不丢失数据。 -例如,Flink定期捕获操作员状态的快照,并将它们写入HDFS等持久存储器中【92,93】。 Samza和Kafka Streams通过将状态更改发送到具有日志压缩功能的专用Kafka主题来复制状态更改,这类似于更改数据捕获【84,100】。 VoltDB通过冗余处理多个节点上的每个输入消息来复制状态(请参阅第252页的“实际的串行执行”)。 +例如,Flink定期捕获操作员状态的快照,并将它们写入HDFS等持久存储器中【92,93】。 Samza和Kafka Streams通过将状态更改发送到具有日志压缩功能的专用Kafka主题来复制状态更改,这类似于更改数据捕获【84,100】。 VoltDB通过冗余处理多个节点上的每个输入消息来复制状态(参阅“[真的串行执行](ch7.md#真的串行执行)”)。 在某些情况下,甚至可能不需要复制状态,因为它可以从输入流重建。例如,如果状态由一个相当短的窗口中的聚合组成,则它可能足够快,以便重放与该窗口相对应的输入事件。如果状态是通过更改数据捕获维护的数据库的本地副本,那么也可以从日志压缩的更改流重建数据库(请参阅“[日志压缩](#日志压缩)”一节)。 @@ -700,7 +700,7 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 ***AMQP/JMS风格的消息代理*** -​ 经纪人将个人消息分配给消费者,消费者在成功处理个人消息时确认消息。消息被确认后从代理中删除。这种方法适合作为RPC的异步形式(另请参阅第136页的“消息传递数据流”),例如在任务队列中,消息处理的确切顺序并不重要,没有在处理之后,需要重新读取旧消息。 +​ 经纪人将个人消息分配给消费者,消费者在成功处理个人消息时确认消息。消息被确认后从代理中删除。这种方法适合作为RPC的异步形式(另请参阅“[消息传递数据流](ch4.md#消息传递数据流)”),例如在任务队列中,消息处理的确切顺序并不重要,没有在处理之后,需要重新读取旧消息。 ***基于日志的消息代理*** @@ -820,8 +820,7 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 *engineering.linkedin.com*, December 16, 2013. 1. Shirshanka Das, Chavdar Botev, Kapil Surlaker, - et al.: “[All Aboard the Databus!](http://www.socc2012.org/s18-das.pdf),” at *3rd ACM - Symposium on Cloud Computing* (SoCC), October 2012. + et al.: “[All Aboard the Databus!](http://www.socc2012.org/s18-das.pdf),” at *3rd ACM Symposium on Cloud Computing* (SoCC), October 2012. 1. Yogeshwer Sharma, Philippe Ajoux, Petchean Ang, et al.: “[Wormhole: Reliable Pub-Sub to Support Geo-Replicated Internet Services](https://www.usenix.org/system/files/conference/nsdi15/nsdi15-paper-sharma.pdf),” at *12th USENIX Symposium on @@ -873,8 +872,7 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 February 18, 2016. 1. Greg Young: - “[CQRS and Event Sourcing](https://www.youtube.com/watch?v=JHGkaShoyNs),” at *Code on - the Beach*, August 2014. + “[CQRS and Event Sourcing](https://www.youtube.com/watch?v=JHGkaShoyNs),” at *Code on the Beach*, August 2014. 1. Martin Fowler: “[Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html),” *martinfowler.com*, @@ -885,15 +883,13 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 Addison-Wesley Professional, 2013. ISBN: 978-0-321-83457-7 1. H. V. Jagadish, Inderpal Singh Mumick, and Abraham Silberschatz: - “[View Maintenance Issues for the Chronicle Data Model](http://www.mathcs.emory.edu/~cheung/papers/StreamDB/Histogram/1995-Jagadish-Histo.pdf),” at *14th ACM SIGACT-SIGMOD-SIGART Symposium - on Principles of Database Systems* (PODS), May 1995. + “[View Maintenance Issues for the Chronicle Data Model](http://www.mathcs.emory.edu/~cheung/papers/StreamDB/Histogram/1995-Jagadish-Histo.pdf),” at *14th ACM SIGACT-SIGMOD-SIGART Symposium on Principles of Database Systems* (PODS), May 1995. [doi:10.1145/212433.220201](http://dx.doi.org/10.1145/212433.220201) 1. “[Event Store 3.5.0 Documentation](http://docs.geteventstore.com/),” Event Store LLP, *docs.geteventstore.com*, February 2016. 1. Martin Kleppmann: - *Making Sense of Stream - Processing*. Report, O'Reilly Media, May 2016. + *Making Sense of Stream Processing*. Report, O'Reilly Media, May 2016. 1. Sander Mak: “[Event-Sourced Architectures with Akka](http://www.slideshare.net/SanderMak/eventsourced-architectures-with-akka),” at *JavaOne*, September 2014. @@ -907,8 +903,7 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 ISBN: 978-0-262-57122-7 1. Timothy Griffin and Leonid Libkin: - “[Incremental Maintenance of Views with Duplicates](http://homepages.inf.ed.ac.uk/libkin/papers/sigmod95.pdf),” at *ACM International Conference on Management of - Data* (SIGMOD), May 1995. + “[Incremental Maintenance of Views with Duplicates](http://homepages.inf.ed.ac.uk/libkin/papers/sigmod95.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), May 1995. [doi:10.1145/223784.223849](http://dx.doi.org/10.1145/223784.223849) 1. Pat Helland: @@ -978,8 +973,7 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 1. Philippe Flajolet, Éric Fusy, Olivier Gandouet, and Frédéric Meunier: - “[HyperLo⁠g​Log: The Analysis of a Near-Optimal Cardinality Estimation Algorithm](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf),” at *Conference on Analysis of - Algorithms* (AofA), June 2007. + “[HyperLo⁠g​Log: The Analysis of a Near-Optimal Cardinality Estimation Algorithm](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf),” at *Conference on Analysis of Algorithms* (AofA), June 2007. 1. Jay Kreps: “[Questioning the Lambda Architecture](https://www.oreilly.com/ideas/questioning-the-lambda-architecture),” *oreilly.com*, July 2, 2014. @@ -1019,8 +1013,7 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 1. Rajagopal Ananthanarayanan, Venkatesh Basker, Sumit Das, et al.: - “[Photon: Fault-Tolerant and Scalable Joining of Continuous Data Streams](http://research.google.com/pubs/pub41318.html),” at *ACM International Conference on Management of - Data* (SIGMOD), June 2013. + “[Photon: Fault-Tolerant and Scalable Joining of Continuous Data Streams](http://research.google.com/pubs/pub41318.html),” at *ACM International Conference on Management of Data* (SIGMOD), June 2013. [doi:10.1145/2463676.2465272](http://dx.doi.org/10.1145/2463676.2465272) 1. Martin Kleppmann: @@ -1053,8 +1046,7 @@ Apache Flink中使用的一种变体方法是定期生成状态滚动检查点 “[Lightweight Asynchronous Snapshots for Distributed Dataflows](http://arxiv.org/abs/1506.08603),” arXiv:1506.08603 [cs.DC], June 29, 2015. 1. Ryan Betts and John Hugg: - *Fast Data: Smart and - at Scale*. Report, O'Reilly Media, October 2015. + *Fast Data: Smart and at Scale*. Report, O'Reilly Media, October 2015. 1. Flavio Junqueira: “[Making Sense of Exactly-Once Semantics](http://conferences.oreilly.com/strata/hadoop-big-data-eu/public/schedule/detail/49690),” at *Strata+Hadoop World London*, June 2016. From fd3cbf57011a5ec97f99173ad4ef72487683f516 Mon Sep 17 00:00:00 2001 From: Vonng Date: Mon, 26 Mar 2018 04:18:17 +0800 Subject: [PATCH 06/17] part-iii rough trans --- README.md | 2 +- ch12.md | 348 ++++++++++++++++++++++++++---------------------------- 2 files changed, 167 insertions(+), 183 deletions(-) diff --git a/README.md b/README.md index e1a1be0..0067825 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ | 第三部分:前言 | 精翻 | | | 第十章:批处理 | 草翻 | | | 第十一章:流处理 | 草翻 | | -| 第十二章:数据系统的未来 | 机翻 | | +| 第十二章:数据系统的未来 | 草翻 | | | 术语表 | - | | | 后记 | 机翻 | | diff --git a/ch12.md b/ch12.md index 2692b77..9a981b0 100644 --- a/ch12.md +++ b/ch12.md @@ -14,11 +14,11 @@ 对未来的看法和猜测当然是主观的,所以我在写这篇个人意见的时候会用到第一人称。我们欢迎您不同意并形成自己的观点,但我希望本章的观点至少可以成为一个富有成效的讨论的出发点,并为经常混淆的概念提供一些清晰。 -第1章概述了本书的目标:探索如何创建可靠,可伸缩和可维护的应用程序和系统。这些主题贯穿了所有的章节:例如,我们讨论了许多有助于提高可靠性的容错算法,提高可扩展性的分区,以及提高可维护性的进化和抽象机制。在本章中,我们将把所有这些想法结合在一起,并以这些想法为基础来设想未来。我们的目标是发现如何设计比今天更好的应用程序 - 强大,正确,可演化,并最终对人类有益。 +第1章概述了本书的目标:探索如何创建可靠,可伸缩和可维护的应用程序和系统。这些主题贯穿了所有的章节:例如,我们讨论了许多有助于提高可靠性的容错算法,提高可扩展性的分区,以及提高可维护性的进化和抽象机制。在本章中,我们将把所有这些想法结合在一起,并以这些想法为基础来设想未来。我们的目标是发现如何设计比今天更好的应用程序——强大,正确,可演化,并最终对人类有益。 ## 数据集成 -本书中反复出现的主题是,对于任何给定的问题,都有几种解决方案,所有这些解决方案都有不同的优点,缺点和折衷。例如,在第3章讨论存储引擎时,我们看到了日志结构存储,B树和列式存储。在第5章讨论复制时,我们看到了单领导,多领导和无领导的方法。 +本书中反复出现的主题是,对于任何给定的问题,都有几种解决方案,所有这些解决方案都有不同的优点,缺点和折衷。例如,在[第3章](ch3.md)讨论存储引擎时,我们看到了日志结构存储,B树和列式存储。在[第5章](ch5.md)讨论复制时,我们看到了单领导,多领导和无领导的方法。 如果你有一个问题,例如“我想存储一些数据并稍后再查询”,那么没有一个正确的解决方案,但是在不同的情况下,每种方法都是适当的。软件实现通常必须选择一种特定的方法。要使一个代码路径健壮并且很好地尝试在一个软件中执行所有操作几乎可以保证实现效果很差,这很难。 @@ -32,7 +32,7 @@ 例如,为了处理任意关键字的查询,需要将OLTP数据库与全文搜索索引集成在一起是很常见的。尽管一些数据库(如PostgreSQL)包含了全文索引功能,可以满足简单的应用需求【1】,但更复杂的搜索工具需要专业的信息检索工具。相反,搜索索引通常不适合作为一个持久的记录系统,因此许多应用程序需要结合两种不同的工具来满足所有要求。 -我们谈到了将数据系统集成到“使系统保持同步”(第452页)的问题。随着数据的不同表示数量的增加,集成问题变得更加困难。除了数据库和搜索索引之外,也许您需要保留分析系统(数据仓库或批处理和流处理系统)中的数据副本。维护从原始数据衍生的对象的高速缓存或非规范化版本;通过机器学习,分类,排名或推荐系统传递数据;或根据对数据的更改发送通知。 +我们谈到了将数据系统集成到“[使系统保持同步]()”(第452页)的问题。随着数据的不同表示数量的增加,集成问题变得更加困难。除了数据库和搜索索引之外,也许您需要保留分析系统(数据仓库或批处理和流处理系统)中的数据副本。维护从原始数据衍生的对象的高速缓存或非规范化版本;通过机器学习,分类,排名或推荐系统传递数据;或根据对数据的更改发送通知。 令人惊讶的是,我经常看到软件工程师做出如下陈述:“根据我的经验,99%的人只需要X”或“......不需要X”(对于X的各种值)。我认为这样的陈述更多地讲述了讲话者的经验,而不是技术的实际有用性。您可能想要对数据执行的各种操作范围非常广泛。一个人认为是一个模糊和毫无意义的功能,可能是别人的核心要求。如果缩小数据流并考虑整个组织的数据流,那么对数据集成的需求往往就会变得明显。 @@ -40,88 +40,88 @@ 当需要在多个存储系统中维护相同数据的副本以满足不同的访问模式时,您需要非常清楚输入和输出:哪些数据先写入,哪些表示来自哪些源?如何以正确的格式将数据导入所有正确的地方? -例如,您可能会首先将数据写入记录数据库系统,捕获对该数据库所做的更改(请参阅第454页上的“更改数据捕获”),然后将更改应用于数据库中的搜索索引相同的顺序。如果更改数据捕获(CDC)是更新索引的唯一方式,则可以确定该索引完全来自记录系统,因此与其保持一致(禁止软件中的错误)。写入数据库是向该系统提供新输入的唯一方式。 +例如,您可能会首先将数据写入记录数据库系统,捕获对该数据库所做的更改(请参阅第454页上的“[捕获变更数据]()”),然后将更改应用于数据库中的搜索索引相同的顺序。如果更改数据捕获(CDC)是更新索引的唯一方式,则可以确定该索引完全来自记录系统,因此与其保持一致(禁止软件中的错误)。写入数据库是向该系统提供新输入的唯一方式。 -允许应用程序直接写入搜索索引和数据库引入了图11-4所示的问题,其中两个客户端同时发送冲突写入,并且两个存储系统按不同顺序处理它们。在这种情况下,既不是数据库也不是 +允许应用程序直接写入搜索索引和数据库引入了[图11-4]()所示的问题,其中两个客户端同时发送冲突写入,并且两个存储系统按不同顺序处理它们。在这种情况下,既不是数据库也不是 -基于事件日志更新衍生数据系统通常可以做出决定性的和幂等的(参见第478页的“幂等性”),使得从故障中恢复相当容易。 +基于事件日志更新衍生数据系统通常可以做出决定性的和幂等的(参见第478页的“[幂等性]()”),使得从故障中恢复相当容易。 #### 衍生数据与分布式事务 -保持不同数据系统彼此一致的经典方法涉及分布式事务,如第354页的“原子提交和两阶段提交(2PC)”中所述。与分布式事务相比,使用衍生数据系统的方法如何? +保持不同数据系统彼此一致的经典方法涉及分布式事务,如第354页的“[原子提交和两阶段提交(2PC)]()”中所述。与分布式事务相比,使用衍生数据系统的方法如何? -在抽象层面,他们通过不同的方式达到类似的目标。分布式事务通过使用锁定进行互斥来决定写入的顺序(请参阅第257页上的“两阶段锁定(2PL)”),而CDC和事件源使用日志进行排序。分布式事务使用原子提交来确保更改只生效一次,而基于日志的系统通常基于确定性重试和幂等性。 +在抽象层面,他们通过不同的方式达到类似的目标。分布式事务通过使用锁定进行互斥来决定写入的顺序(请参阅第257页上的“[两阶段锁定(2PL)]()”),而CDC和事件源使用日志进行排序。分布式事务使用原子提交来确保更改只生效一次,而基于日志的系统通常基于确定性重试和幂等性。 -最大的不同之处在于事务系统通常提供线性(请参阅第324页的“线性化”),这意味着有用的保证,例如读取自己的写入(请参阅第162页的“读取自己的写入”)。另一方面,衍生数据系统通常是异步更新的,因此它们不会默认提供相同的时间保证。 +最大的不同之处在于事务系统通常提供线性(请参阅第324页的“[线性一致性]()”),这意味着有用的保证,例如读取自己的写入(请参阅第162页的“[读己之写]()”)。另一方面,衍生数据系统通常是异步更新的,因此它们不会默认提供相同的时间保证。 -在愿意支付分布式交易成本的有限环境中,它们已被成功使用。但是,我认为XA的容错能力和性能特征较差(请参阅第364页的“实践中的分布式事务”),这严重限制了它的实用性。我相信有可能为分布式事务创建一个更好的协议,但是将这样一个协议广泛采用并与现有工具集成将是具有挑战性的,并且不太可能很快发生。 +在愿意支付分布式交易成本的有限环境中,它们已被成功使用。但是,我认为XA的容错能力和性能特征较差(请参阅第364页的“[实践中的分布式事务]()”),这严重限制了它的实用性。我相信有可能为分布式事务创建一个更好的协议,但是将这样一个协议广泛采用并与现有工具集成将是具有挑战性的,并且不太可能很快发生。 -在没有广泛支持良好分布式事务协议的情况下,我认为基于日志的衍生数据是集成不同数据系统的最有前途的方法。然而,诸如阅读自己写作的保证是有用的,我认为告诉每个人“最终的一致性是不可避免的 - 吸收它并学会处理它”是没有成果的(至少不是没有良好的指导如何处理它)。 +在没有广泛支持良好分布式事务协议的情况下,我认为基于日志的衍生数据是集成不同数据系统的最有前途的方法。然而,诸如阅读自己写作的保证是有用的,我认为告诉每个人“最终的一致性是不可避免的 —— 吸收它并学会处理它”是没有成果的(至少不是没有良好的指导如何处理它)。 在第51页的“瞄准正确性”中,我们将讨论一些在异步衍生系统之上实现更强保障的方法,并在分布式事务和异步基于对数系统之间建立中间立场。 #### 全局有序的限制 -对于足够小的系统,构建一个完全有序的事件日志是完全可行的(正如单引导程序复制数据库的流行所证明的那样,这正好构建了这样一个日志)。但是,随着系统向更大更复杂的工作负载扩展,限制开始出现: +对于足够小的系统,构建一个完全有序的事件日志是完全可行的(正如单主复制数据库的流行所证明的那样,这正好构建了这样一个日志)。但是,随着系统向更大更复杂的工作负载扩展,限制开始出现: -* 在大多数情况下,构建完全有序的日志需要所有事件通过决定订购的单个领导节点。如果事件吞吐量大于单台计算机可处理的事件,则需要将其分割到多台计算机上(请参见第446页的“分区日志”)。然后,两个不同分区中的事件顺序不明确。 +* 在大多数情况下,构建完全有序的日志需要所有事件通过决定订购的单个领导节点。如果事件吞吐量大于单台计算机可处理的事件,则需要将其分割到多台计算机上(请参见第446页的“[分区日志]()”)。然后,两个不同分区中的事件顺序不明确。 -* 如果服务器分布在多个地理位置分散的数据中心上,例如为了容忍整个数据中心脱机,您通常在每个数据中心都有单独的领导者,因为网络延迟会导致同步的跨数据中心协调效率低下(请参阅“Multi -Leader复制“)。这意味着源自两个不同数据中心的事件的未定义排序。 +* 如果服务器分布在多个地理位置分散的数据中心上,例如为了容忍整个数据中心脱机,您通常在每个数据中心都有单独的领导者,因为网络延迟会导致同步的跨数据中心协调效率低下(请参阅“[多主复制]()“)。这意味着源自两个不同数据中心的事件的未定义排序。 -* 将应用程序部署为微服务时(请参阅第125页上的“通过服务进行数据流:REST和RPC”),常见的设计选择是将每个服务及其持久状态作为独立单元进行部署,服务之间不共享持久状态。当两个事件来自不同的服务时,这些事件没有定义的顺序。 +* 将应用程序部署为微服务时(请参阅第125页上的“[通过服务进行数据流:REST和RPC]()”),常见的设计选择是将每个服务及其持久状态作为独立单元进行部署,服务之间不共享持久状态。当两个事件来自不同的服务时,这些事件没有定义的顺序。 -* 某些应用程序保持客户端状态,该状态在用户输入时立即更新(无需等待服务器确认),甚至可以继续脱机工作(请参阅第170页的“脱机操作的客户端”)。有了这样的应用程序,客户端和服务器很可能以不同的顺序看到事件。 +* 某些应用程序保持客户端状态,该状态在用户输入时立即更新(无需等待服务器确认),甚至可以继续脱机工作(请参阅第170页的“[脱机操作的客户端]()”)。有了这样的应用程序,客户端和服务器很可能以不同的顺序看到事件。 -在形式上,决定事件的总次序称为总次序广播,相当于共识(请参阅第366页上的“共识算法和总次序广播”)。大多数共识算法都是针对单个节点的吞吐量足以处理整个事件流的情况而设计的,并且这些算法不提供多个节点共享事件排序工作的机制。设计共识算法仍然是一个开放的研究问题,它可以扩展到单个节点的吞吐量之外,并且在地理上分散的环境中工作良好。 +在形式上,决定事件的总次序称为总次序广播,相当于共识(请参阅第366页上的“[共识算法和全序广播]()”)。大多数共识算法都是针对单个节点的吞吐量足以处理整个事件流的情况而设计的,并且这些算法不提供多个节点共享事件排序工作的机制。设计共识算法仍然是一个开放的研究问题,它可以扩展到单个节点的吞吐量之外,并且在地理上分散的环境中工作良好。 #### 订购事件以捕捉因果关系 -在事件之间不存在因果关系的情况下,缺乏全部命令并不是一个大问题,因为并发事件可以任意排序。其他一些情况很容易处理:例如,当同一对象有多个更新时,它们可以通过将特定对象ID的所有更新路由到相同的日志分区来完全排序。然而,因果关系有时会以更微妙的方式出现(另请参阅“订购和因果关系”第319页)。 +在事件之间不存在因果关系的情况下,缺乏全部命令并不是一个大问题,因为并发事件可以任意排序。其他一些情况很容易处理:例如,当同一对象有多个更新时,它们可以通过将特定对象ID的所有更新路由到相同的日志分区来完全排序。然而,因果关系有时会以更微妙的方式出现(另请参阅“[顺序和因果关系]()”第319页)。 例如,考虑一个社交网络服务,以及两个相互关系但刚分手的用户。其中一个用户将另一个作为朋友移除,然后向其余的朋友发送消息,抱怨他们的前伴侣。用户的意图是他们的前配偶不应该看到粗鲁的信息,因为信息是在朋友状态被撤销后发送的。 但是,在一个地方存储友谊状态并在另一个地方存储消息的系统中,不友好事件和消息发送事件之间的顺序依赖关系可能会丢失。如果未捕获到因果依赖关系,则发送有关新消息的通知的服务可能会在不友好事件之前处理消息发送事件,从而错误地向前伙伴发送通知。 -在本例中,通知实际上是消息和好友列表之间的连接,使得它与我们先前讨论的连接的时间问题有关(请参阅第475页的“连接的时间依赖性”)。不幸的是,这个问题似乎并没有一个简单的答案【2,3】。起点包括: +在本例中,通知实际上是消息和好友列表之间的连接,使得它与我们先前讨论的连接的时间问题有关(请参阅第475页的“[连接的时间依赖性]()”)。不幸的是,这个问题似乎并没有一个简单的答案【2,3】。起点包括: -* 逻辑时间戳可以提供没有协调的全部订购(请参见“序列号排序”第页343),因此它们可能有助于总订单广播不可行的情况。但是,他们仍然要求收件人处理不按顺序发送的事件,并且需要传递其他元数据。 -* 如果您可以记录一个事件来记录用户在做出决定之前所看到的系统状态,并给该事件一个唯一的标识符,那么后面的任何事件都可以引用该事件标识符来记录因果关系【4】 。我们将在第513页的“Reads are events too”中回到这个想法。 -* 冲突解决算法(请参阅“自动冲突解决”(第165页))有助于处理以意外顺序传递的事件。它们对于维护状态很有用,但如果行为有外部副作用(例如,也许,随着时间的推移,应用程序开发模式将出现,使得能够有效地捕获因果依赖关系,并且保持正确的衍生状态,而不会迫使所有事件经历全部命令广播的瓶颈。 +* 逻辑时间戳可以提供没有协调的全部订购(请参见“[序列号排序]()”第页343),因此它们可能有助于全序广播不可行的情况。但是,他们仍然要求收件人处理不按顺序发送的事件,并且需要传递其他元数据。 +* 如果您可以记录一个事件来记录用户在做出决定之前所看到的系统状态,并给该事件一个唯一的标识符,那么后面的任何事件都可以引用该事件标识符来记录因果关系【4】 。我们将在第513页的“[读也是事件](#读也是事件)”中回到这个想法。 +* 冲突解决算法(请参阅“[自动冲突解决]()”(第165页))有助于处理以意外顺序传递的事件。它们对于维护状态很有用,但如果行为有外部副作用(例如,也许,随着时间的推移,应用程序开发模式将出现,使得能够有效地捕获因果依赖关系,并且保持正确的衍生状态,而不会迫使所有事件经历全部命令广播的瓶颈。 ### 批量处理与流处理 我会说数据集成的目标是确保数据在所有正确的地方以正确的形式结束。这样做需要消耗投入,转化,加入,过滤,汇总,培训模型,评估并最终写入适当的输出。批处理和流处理器是实现这一目标的工具。 -批处理和流处理的输出是衍生的数据集,例如搜索索引,实例化视图,向用户显示的建议,聚合度量等(请参阅“批处理工作流的输出”(第417页)和“流处理的用法”第465页)。 +批处理和流处理的输出是衍生的数据集,例如搜索索引,实例化视图,向用户显示的建议,聚合度量等(请参阅“[批处理工作流的输出]()”(第417页)和“[流处理的用法]()”第465页)。 -正如我们在第10章和第11章中看到的,批处理和流处理有许多共同的原则,主要的根本区别在于流处理器在无界数据集上运行,而批处理输入是已知的有限大小。处理引擎的实现方式也有很多细节上的差异,但是这些区别开始模糊。 +正如我们在[第10章](ch10.md)和[第11章](ch11.md)中看到的,批处理和流处理有许多共同的原则,主要的根本区别在于流处理器在无界数据集上运行,而批处理输入是已知的有限大小。处理引擎的实现方式也有很多细节上的差异,但是这些区别开始模糊。 Spark在批处理引擎上执行流处理,将流分解为微格式,而Apache Flink则在流处理引擎上执行批处理【5】。原则上,一种类型的处理可以在另一种类型上仿真,但是性能特征会有所不同:例如,在跳跃或滑动窗口时,微博可能表现不佳【6】。 #### 保持衍生状态 -批处理具有非常强大的功能特性(即使代码不是用函数式编程语言编写的):它鼓励确定性的纯函数,其输出仅依赖于输入,除了显式输出外没有副作用,处理输入作为不可变的,并作为附加的输出。流处理类似,但它扩展了运算符以允许受管理的容错状态(请参阅第478页的“重建失败后的状态”)。 +批处理具有非常强大的功能特性(即使代码不是用函数式编程语言编写的):它鼓励确定性的纯函数,其输出仅依赖于输入,除了显式输出外没有副作用,处理输入作为不可变的,并作为附加的输出。流处理类似,但它扩展了运算符以允许受管理的容错状态(请参阅第478页的“[重建失败后的状态”]())。 -具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(请参见第478页的“幂等性”),但也简化了有关组织中数据流的推理【7】。无论衍生数据是搜索索引,统计模型还是缓存,从数据管道角度来看,从另一个衍生出一件事情,通过功能应用程序代码推送一个系统中的状态更改和应用对衍生系统的影响。 +具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(请参见第478页的“[幂等性]()”),但也简化了有关组织中数据流的推理【7】。无论衍生数据是搜索索引,统计模型还是缓存,从数据管道角度来看,从另一个衍生出一件事情,通过功能应用程序代码推送一个系统中的状态更改和应用对衍生系统的影响。 -原则上,衍生数据系统可以同步维护,就像关系数据库在与被索引表写入操作相同的事务中同步更新辅助索引一样。然而,异步是基于事件日志的系统稳健的原因:它允许系统的一部分故障被本地包含,而如果任何一个参与者失败,分布式事务将中止,因此他们倾向于通过将故障扩展到系统的其余部分(请参阅第363页的“分布式事务的限制”)。 +原则上,衍生数据系统可以同步维护,就像关系数据库在与被索引表写入操作相同的事务中同步更新辅助索引一样。然而,异步是基于事件日志的系统稳健的原因:它允许系统的一部分故障被本地包含,而如果任何一个参与者失败,分布式事务将中止,因此他们倾向于通过将故障扩展到系统的其余部分(请参阅第363页的“[分布式事务的限制]()”)。 -我们在第206页的“分区和二级索引”中看到,二级索引经常跨越分区边界。具有二级索引的分区系统需要将写入发送到多个分区(如果索引是分词)或将读取发送到所有分区(如果索引是文档分区的话)。如果索引是异步维护的,这种交叉分区通信也是最可靠和可扩展的【8】(另请参阅“多分区数据处理”第479页)。 +我们在第206页的“分区和二级索引”中看到,二级索引经常跨越分区边界。具有二级索引的分区系统需要将写入发送到多个分区(如果索引是分词)或将读取发送到所有分区(如果索引是文档分区的话)。如果索引是异步维护的,这种交叉分区通信也是最可靠和可扩展的【8】(另请参阅“[多分区数据处理]()”第479页)。 #### 为应用程序演变重新处理数据 在维护衍生数据时,批处理和流处理都是有用的。流处理允许将输入中的变化以低延迟反映在衍生视图中,而批处理允许重新处理大量累积的历史数据以便将新视图导出到现有数据集上。 -特别是,重新处理现有数据为维护系统提供了一个良好的机制,并将其发展为支持新功能和变更需求(参见第4章)。如果不进行重新处理,模式演化就会局限于简单的变化,例如向记录中添加新的可选字段或添加新类型的记录。无论是在写模式还是在读模式中都是如此(请参阅第39页的“文档模型中的模式灵活性”)。另一方面,通过重新处理,可以将数据集重组为一个完全不同的模型,以便更好地满足新的要求。 +特别是,重新处理现有数据为维护系统提供了一个良好的机制,并将其发展为支持新功能和变更需求(参见[第4章](ch4.md))。如果不进行重新处理,模式演化就会局限于简单的变化,例如向记录中添加新的可选字段或添加新类型的记录。无论是在写模式还是在读模式中都是如此(请参阅第39页的“[文档模型中的模式灵活性]()”)。另一方面,通过重新处理,可以将数据集重组为一个完全不同的模型,以便更好地满足新的要求。 > ### 在铁路上的模式迁移 > > 大规模的“模式迁移”也发生在非计算机系统中。例如,在19世纪英国铁路建设初期,轨距(两轨之间的距离)就有了各种各样的交易标准。为一个测量仪而建的列车不能在另一个测量仪的轨道上运行,这限制了火车网络中可能的相互连接【9】。 > -> 在1846年最终确定了一个标准仪表之后,其他仪表的轨道必须转换 - 但是如何在不关闭火车线路的情况下进行数月甚至数年?解决的办法是首先将轨道转换为双轨或混合轨距,方法是增加第三轨。这种转换可以逐渐完成,当完成时,两个仪表的列车可以在三条轨道中的两条轨道上运行。事实上,一旦所有的列车都转换成标准轨距,那么可以移除提供非标准轨距的轨道。 +> 在1846年最终确定了一个标准仪表之后,其他仪表的轨道必须转换 —— 但是如何在不关闭火车线路的情况下进行数月甚至数年?解决的办法是首先将轨道转换为双轨或混合轨距,方法是增加第三轨。这种转换可以逐渐完成,当完成时,两个仪表的列车可以在三条轨道中的两条轨道上运行。事实上,一旦所有的列车都转换成标准轨距,那么可以移除提供非标准轨距的轨道。 > > 以这种方式“再加工”现有的轨道,让新旧版本并存,可以在几年的时间内逐渐改变轨距。然而,这是一项昂贵的事业,这就是今天非标准仪表仍然存在的原因。例如,旧金山湾区的BART系统使用与美国大部分地区不同的仪表。 @@ -133,15 +133,15 @@ Spark在批处理引擎上执行流处理,将流分解为微格式,而Apache 如果批处理用于重新处理历史数据,并且流处理用于处理最近的更新,那么您如何将这两者结合起来?拉姆达体系结构【12】是这方面的一个建议,引起了很多关注。 -lambda体系结构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件源(请参阅第457页上的“事件源”)。从这些事件中,推导出读取优化的视图。 lambda体系结构建议并行运行两个不同的系统:批处理系统(如Hadoop MapReduce)和独立的流处理系统(如Storm)。 +lambda体系结构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件源(请参阅第457页上的“[事件源]()”)。从这些事件中,推导出读取优化的视图。 Lambda体系结构建议并行运行两个不同的系统:批处理系统(如Hadoop MapReduce)和独立的流处理系统(如Storm)。 -在lambda方法中,流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成衍生视图的更正版本。这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的(请参阅“故障容错”)。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。 +在lambda方法中,流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成衍生视图的更正版本。这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的(请参阅“[故障容错]()”)。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。 拉姆达体系结构是一个有影响力的想法,它将数据系统的设计变得更好,尤其是通过推广将视图衍生到不可变事件流和在需要时重新处理事件的原则。但是,我也认为它有一些实际问题: * 必须保持相同的逻辑才能在批处理和流处理框架中运行,这是额外的工作。虽然像Summingbird 【13】这样的库提供了一个抽象的计算,可以在一个批处理或流的上下文中运行,调试,调整和维护两个不同系统的操作复杂性仍然【14】。 * 由于流管道和批处理管道产生单独的输出,因此需要合并它们以响应用户请求。如果计算是通过滚动窗口的简单聚合,则合并相当容易,但如果使用更复杂的操作(例如连接和会话化)导出视图,或者输出不是时间序列,则显得非常困难。 -* 尽管有能力重新处理整个历史数据集是很好的,但在大型数据集上这样做经常会很昂贵。因此,批处理流水线通常需要设置为处理增量批处理(例如,在每小时结束时处理一小时的数据),而不是重新处理所有内容。这引发了第468页的“关于时间的推理”中讨论的问题,例如处理分段器和处理跨批次边界的窗口。增加批量计算会增加复杂性,使其更类似于流式传输层,这与保持批处理层尽可能简单的目标背道而驰。 +* 尽管有能力重新处理整个历史数据集是很好的,但在大型数据集上这样做经常会很昂贵。因此,批处理流水线通常需要设置为处理增量批处理(例如,在每小时结束时处理一小时的数据),而不是重新处理所有内容。这引发了第468页的“[关于时间的推理]()”中讨论的问题,例如处理分段器和处理跨批次边界的窗口。增加批量计算会增加复杂性,使其更类似于流式传输层,这与保持批处理层尽可能简单的目标背道而驰。 #### 统一批处理和流处理 @@ -149,9 +149,9 @@ lambda体系结构的核心思想是通过将不可变事件附加到不断增 在一个系统中统一批处理和流处理需要以下功能,这些功能越来越广泛: -* 通过处理最近事件流的相同处理引擎来重放历史事件的能力。例如,基于日志的消息代理可以重放消息(请参阅第451页的“重放旧消息”),某些流处理器可以从HDFS等分布式文件系统读取输入。 -* 对于流处理器来说,只有一次语义 - 即确保输出与未发生故障的输出相同,即使事实上发生故障(请参阅“故障容错”(第476页))。与批处理一样,这需要丢弃任何失败任务的部分输出。 -* 按事件时间进行窗口化的工具,而不是按处理时间进行窗口化,因为处理历史事件时的处理时间毫无意义(请参阅第468页的“关于时间的推理”)。例如,Apache Beam提供了用于表达这种计算的API,然后可以使用Apache Flink或Google Cloud Dataflow运行。 +* 通过处理最近事件流的相同处理引擎来重放历史事件的能力。例如,基于日志的消息代理可以重放消息(请参阅第451页的“[重放旧消息]()”),某些流处理器可以从HDFS等分布式文件系统读取输入。 +* 对于流处理器来说,只有一次语义 —— 即确保输出与未发生故障的输出相同,即使事实上发生故障(请参阅“[故障容错]()”(第476页))。与批处理一样,这需要丢弃任何失败任务的部分输出。 +* 按事件时间进行窗口化的工具,而不是按处理时间进行窗口化,因为处理历史事件时的处理时间毫无意义(请参阅第468页的“[关于时间的推理]()”)。例如,Apache Beam提供了用于表达这种计算的API,然后可以使用Apache Flink或Google Cloud Dataflow运行。 @@ -161,7 +161,7 @@ lambda体系结构的核心思想是通过将不可变事件附加到不断增 ## 拆分数据库 -在最抽象的层面上,数据库,Hadoop和操作系统都执行相同的功能:它们存储一些数据,并允许您处理和查询数据【16】。数据库将数据存储在某些数据模型(表中的文档,文档中的顶点,图形中的顶点等)的记录中,而操作系统的文件系统则将数据存储在文件中 - 但在其核心上,都是“信息管理”系统[ 17。正如我们在第10章中看到的,Hadoop生态系统有点像Unix的分布式版本。 +在最抽象的层面上,数据库,Hadoop和操作系统都执行相同的功能:它们存储一些数据,并允许您处理和查询数据【16】。数据库将数据存储在某些数据模型(表中的文档,文档中的顶点,图形中的顶点等)的记录中,而操作系统的文件系统则将数据存储在文件中——但在其核心上,都是“信息管理”系统【17】。正如我们在[第10章](ch10.md)中看到的,Hadoop生态系统有点像Unix的分布式版本。 当然,有很多实际的差异。例如,许多文件系统不能很好地处理包含1000万个小文件的目录,而包含1000万个小记录的数据库是完全正常且不起眼的。无论如何,操作系统和数据库之间的相似之处和差异值得探讨。 @@ -177,34 +177,34 @@ Unix和关系数据库以非常不同的哲学来处理信息管理问题。 Uni 在本书的过程中,我们讨论了数据库提供的各种功能及其工作原理,其中包括: -* 二级索引,使您可以根据字段的值有效地搜索记录(请参阅第79页上的“其他索引结构” -* 物化视图,这是一种预先计算的查询结果缓存(请参阅“聚合:数据立方体和物化视图”(第101页) -* 复制日志,保持其他节点上数据的副本最新(请参阅第158页中的“复制日志的实现”) -* 全文搜索索引,允许在文本中进行关键字搜索(请参见第88页上的“全文搜索和模糊索引”)以及内置于某些关系数据库【1】 +* 二级索引,使您可以根据字段的值有效地搜索记录(请参阅第79页上的“[其他索引结构]()”) +* 物化视图,这是一种预先计算的查询结果缓存(请参阅“[聚合:数据立方体和物化视图]()”,第101页) +* 复制日志,保持其他节点上数据的副本最新(请参阅第158页中的“[复制日志的实现]()”) +* 全文搜索索引,允许在文本中进行关键字搜索(请参见第88页上的“[全文搜索和模糊索引]()”)以及内置于某些关系数据库【1】 -在第十章和第十一章中,出现了类似的主题。我们讨论了如何构建全文搜索索引(请参阅第357页上的“批处理工作流的输出”),了解有关实例化视图维护(请参阅“维护实例化视图”一节第437页)以及有关将更改从数据库复制到衍生数据系统(请参阅第454页的“更改数据捕获”)。 +在第十章和第十一章中,出现了类似的主题。我们讨论了如何构建全文搜索索引(请参阅第357页上的“[批处理工作流的输出]()”),了解有关实例化视图维护(请参阅“[维护实例化视图]()”一节第437页)以及有关将更改从数据库复制到衍生数据系统(请参阅第454页的“[更改数据捕获]()”)。 数据库中内置的功能与人们用批处理和流处理器构建的衍生数据系统似乎有相似之处。 #### 创建一个索引 -想想当你运行CREATE INDEX在关系数据库中创建一个新的索引时会发生什么。数据库必须扫描表的一致性快照,挑选出所有被索引的字段值,对它们进行排序,然后写出索引。然后它必须处理自一致快照以来所做的写入操作(假设表在创建索引时未被锁定,所以写操作可能会继续)。一旦完成,只要事务写入表中,数据库就必须继续保持索引最新。 +想想当你运行`CREATE INDEX`在关系数据库中创建一个新的索引时会发生什么。数据库必须扫描表的一致性快照,挑选出所有被索引的字段值,对它们进行排序,然后写出索引。然后它必须处理自一致快照以来所做的写入操作(假设表在创建索引时未被锁定,所以写操作可能会继续)。一旦完成,只要事务写入表中,数据库就必须继续保持索引最新。 -此过程非常类似于设置新的追随者副本(请参阅第155页的“设置新的追随者”),也非常类似于流系统中的引导更改数据捕获(请参阅第455页的“初始快照”)。 +此过程非常类似于设置新的追随者副本(请参阅第155页的“[设置新的追随者]()”),也非常类似于流系统中的引导更改数据捕获(请参阅第455页的“[初始快照]()”)。 -无论何时运行CREATE INDEX,数据库都会重新处理现有数据集(如第494页的“重新处理应用程序数据的演变数据”中所述),并将该索引作为新视图导出到现有数据上。现有数据可能是状态的快照,而不是所有发生变化的日志,但两者密切相关(请参阅“状态,数据流和不变性”第459页)。 +无论何时运行CREATE INDEX,数据库都会重新处理现有数据集(如第494页的“[重新处理应用程序数据的演变数据]()”中所述),并将该索引作为新视图导出到现有数据上。现有数据可能是状态的快照,而不是所有发生变化的日志,但两者密切相关(请参阅“[状态,数据流和不变性]()”第459页)。 #### 一切的元数据库 有鉴于此,我认为整个组织的数据流开始像一个巨大的数据库【7】。每当批处理,流或ETL过程将数据从一个地方传输到另一个地方并形成表单时,就像数据库子系统一样,使索引或物化视图保持最新。 -像这样看,批处理和流处理器就像触发器,存储过程和物化视图维护例程的精细实现。他们维护的衍生数据系统就像不同的索引类型。例如,关系数据库可能支持B树索引,散列索引,空间索引(请参阅第79页的“多列索引”)以及其他类型的索引。在新兴的衍生数据系统体系结构中,不是将这些设施作为单个集成数据库产品的功能实现,而是由各种不同的软件提供,运行在不同的机器上,由不同的团队管理。 +像这样看,批处理和流处理器就像触发器,存储过程和物化视图维护例程的精细实现。他们维护的衍生数据系统就像不同的索引类型。例如,关系数据库可能支持B树索引,散列索引,空间索引(请参阅第79页的“[多列索引]()”)以及其他类型的索引。在新兴的衍生数据系统体系结构中,不是将这些设施作为单个集成数据库产品的功能实现,而是由各种不同的软件提供,运行在不同的机器上,由不同的团队管理。 这些发展在未来将会把我们带到哪里?如果我们从没有适合所有访问模式的单一数据模型或存储格式的前提出发,我推测有两种途径可以将不同的存储和处理工具组合成一个有凝聚力的系统: **联合数据库:统一读取** -可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 - 一种称为联邦数据库或多存储的方法【18,19】。例如,PostgreSQL的外部数据包装功能符合这种模式【20】。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。 +可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 —— 一种称为联邦数据库或多存储的方法【18,19】。例如,PostgreSQL的外部数据包装功能符合这种模式【20】。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。 联合查询接口遵循单一集成系统的关系传统,具有高级查询语言和优雅的语义,但却是一个复杂的实现。 @@ -212,28 +212,28 @@ Unix和关系数据库以非常不同的哲学来处理信息管理问题。 Uni 虽然联合会解决了跨多个不同系统的只读查询问题,但它并没有很好的解决跨系统同步写入的问题。我们说过,在单个数据库中,创建一致的索引是一项内置功能。当我们构建多个存储系统时,我们同样需要确保所有数据更改都会在所有正确的位置结束,即使在出现故障时也是如此。将存储系统可靠地插接在一起(例如,通过更改数据捕获和事件日志)更容易,就像将数据库的索引维护功能以可以跨不同技术同步写入的方式分开【7,21】。 -unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件事【22】,通过统一的低级API(管道)进行通信,并且可以使用更高级别的语言(shell)【16】 。 +非捆绑方法遵循Unix传统的小型工具,它可以很好地完成一件事【22】,通过统一的低级API(管道)进行通信,并且可以使用更高级别的语言(shell)【16】 。 #### 开展分拆工作 联邦和非捆绑是同一个硬币的两个方面:用不同的组件构成可靠,可扩展和可维护的系统。联合只读联邦和非捆绑是同一个硬币的两个方面:用不同的组件构成可靠,可扩展和可维护的系统。联合只读查询需要将一个数据模型映射到另一个数据模型,这需要一些思考,但最终还是一个可管理的问题。我认为保持写入到几个存储系统是同步的更困难的工程问题,所以我将重点关注它。 -传统的同步写入方法需要跨异构存储系统的分布式事务【18】,我认为这是错误的解决方案(请参阅“导出的数据与分布式事务”第495页)。单个存储或流处理系统内的事务是可行的,但是当数据跨越不同技术之间的边界时,我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法。 +传统的同步写入方法需要跨异构存储系统的分布式事务【18】,我认为这是错误的解决方案(请参阅“[导出的数据与分布式事务]()”第495页)。单个存储或流处理系统内的事务是可行的,但是当数据跨越不同技术之间的边界时,我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法。 -例如,分布式事务在某些流处理器中使用,以精确匹配一次语义(请参阅第477页的“重新访问原子提交”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理器写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的事件的有序日志(参见第478页的“幂等性”)是一种更简单的抽象,因此在异构系统中实现更加可行【7】。 +例如,分布式事务在某些流处理器中使用,以精确匹配一次语义(请参阅第477页的“[重新访问原子提交]()”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理器写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的事件的有序日志(参见第478页的“[幂等性]()”)是一种更简单的抽象,因此在异构系统中实现更加可行【7】。 基于日志的集成的一大优势是各个组件之间的松散耦合,这体现在两个方面: -1. 在系统级别,异步事件流使整个系统对各个组件的中断或性能下降更加稳健。如果使用者运行缓慢或失败,那么事件日志可以缓冲消息(请参阅“磁盘空间使用情况”第369页),以便生产者和任何其他使用者可以继续不受影响地运行。有问题的消费者可以在固定时赶上,因此不会错过任何数据,并且包含故障。相比之下,分布式事务的同步交互往往会将本地故障升级为大规模故障(请参见第363页的“分布式事务的限制”)。 +1. 在系统级别,异步事件流使整个系统对各个组件的中断或性能下降更加稳健。如果使用者运行缓慢或失败,那么事件日志可以缓冲消息(请参阅“[磁盘空间使用情况]()”第369页),以便生产者和任何其他使用者可以继续不受影响地运行。有问题的消费者可以在固定时赶上,因此不会错过任何数据,并且包含故障。相比之下,分布式事务的同步交互往往会将本地故障升级为大规模故障(请参见第363页的“[分布式事务的限制]()”)。 2. 在人力方面,分拆数据系统允许不同的团队独立开发,改进和维护不同的软件组件和服务。专业化使得每个团队都可以专注于做好一件事,并与其他团队的系统进行明确的界面。事件日志提供了一个足够强大的接口,以捕获相当强的一致性属性(由于持久性和事件的顺序),但也足够普遍适用于几乎任何类型的数据。 #### 非捆绑与集成系统 -如果分拆确实成为未来的方式,它将不会取代目前形式的数据库 - 它们仍然会像以往一样需要。数据库仍然需要维护流处理器中的状态,并且为批处理和流处理器的输出提供查询服务(请参阅第419页上的“批处理工作流的输出”和第464页上的“处理流”)。专门的查询引擎将继续对特定的工作负载非常重要:例如,MPP数据仓库中的查询引擎针对探索性分析查询进行了优化,并且能够很好地处理这种类型的工作负载(请参阅第417页的“将Hadoop与分布式数据库进行比较” 。 +如果分拆确实成为未来的方式,它将不会取代目前形式的数据库 —— 它们仍然会像以往一样需要。数据库仍然需要维护流处理器中的状态,并且为批处理和流处理器的输出提供查询服务(请参阅第419页上的“[批处理工作流的输出]()”和第464页上的“[处理流]()”)。专门的查询引擎将继续对特定的工作负载非常重要:例如,MPP数据仓库中的查询引擎针对探索性分析查询进行了优化,并且能够很好地处理这种类型的工作负载(请参阅第417页的“[将Hadoop与分布式数据库进行比较]()” 。 运行几个不同基础架构的复杂性可能是一个问题:每一个软件都有一个学习曲线,配置问题和操作怪癖,因此值得部署尽可能少的移动部件。与由应用程序代码【23】组成的多个工具组成的系统相比,单一集成软件产品也可以在其设计的工作负载类型上实现更好,更可预测的性能。正如我在前言中所说的那样,为了扩大规模而建设你不需要的是浪费精力,并且可能会将你锁定在一个不灵活的设计中。实际上,这是一种过早优化的形式。 -分拆的目标不是要针对个别数据库与特定工作负载的性能进行竞争;我们的目标是允许您结合多个不同的数据库,以便在比单个软件可能实现的更广泛的工作负载范围内实现更好的性能。这是关于广度,而不是深度 - 与我们在第414页上的“比较Hadoop与分布式数据库”中讨论的存储和处理模型的多样性一样。 +分拆的目标不是要针对个别数据库与特定工作负载的性能进行竞争;我们的目标是允许您结合多个不同的数据库,以便在比单个软件可能实现的更广泛的工作负载范围内实现更好的性能。这是关于广度,而不是深度 —— 与我们在第414页上的“[比较Hadoop与分布式数据库]()”中讨论的存储和处理模型的多样性一样。 因此,如果有一项技术可以满足您的所有需求,那么您最好使用该产品,而不是试图用低级组件重新实现它。只有当没有单一软件满足您的所有需求时,才会出现拆分和合成的优势。 @@ -241,13 +241,13 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 用于组成数据系统的工具正在变得越来越好,但我认为缺少一个主要部分:我们还没有Unix shell的非捆绑式数据库(即,用于组成存储和处理系统的高级语言简单和陈述的方式)。 -例如,如果我们可以简单地声明mysql |,我就会喜欢它elasticsearch,类似于Unix管道【22】,这将成为CREATE INDEX的非捆绑等价物:它将MySQL数据库中的所有文档并将其索引到Elasticsearch集群中。然后它会不断捕获对数据库所做的所有更改,并自动将它们应用于搜索索引,而无需编写自定义应用程序代码。几乎任何类型的存储或索引系统都可以实现这种集成。 +例如,如果我们可以简单地声明`mysql |elasticsearch`,类似于Unix管道【22】,这将成为`CREATE INDEX`的非捆绑等价物:它将MySQL数据库中的所有文档并将其索引到Elasticsearch集群中。然后它会不断捕获对数据库所做的所有更改,并自动将它们应用于搜索索引,而无需编写自定义应用程序代码。几乎任何类型的存储或索引系统都可以实现这种集成。 -同样,能够更容易地预先计算和更新缓存将是一件好事。回想一下,物化视图本质上是一个预先计算的缓存,所以您可以通过为复杂查询声明指定物化视图来创建缓存,包括图上的递归查询(请参阅第49页上的“类图数据模型”)和应用逻辑。在这方面有一些有趣的早期研究,如差异数据流【24,25】,我希望这些想法能够在生产系统中找到自己的方法。 +同样,能够更容易地预先计算和更新缓存将是一件好事。回想一下,物化视图本质上是一个预先计算的缓存,所以您可以通过为复杂查询声明指定物化视图来创建缓存,包括图上的递归查询(请参阅第49页上的“[图数据模型]()”)和应用逻辑。在这方面有一些有趣的早期研究,如差异数据流【24,25】,我希望这些想法能够在生产系统中找到自己的方法。 ### 围绕数据流设计应用 -通过使用应用程序代码组成专门的存储和处理系统来分离数据库的方法也被称为“数据库内外”方法【26】,在2014年的一次会议演讲标题之后【27】。然而,称它为“新建筑”太宏大。我把它看作是一个设计模式,一个讨论的起点,我们只是简单地给它起一个名字,以便我们可以更好地谈论它。 +通过使用应用程序代码组成专门的存储和处理系统来分离数据库的方法也被称为“数据库内外”方法【26】,在2014年的一次会议演讲标题之后【27】。然而,称它为“新架构”太宏大。我把它看作是一个设计模式,一个讨论的起点,我们只是简单地给它起一个名字,以便我们可以更好地谈论它。 这些想法不是我的;他们只是我认为我们应该学习的其他人思想的融合。尤其是,数据流语言(如Oz 【28】和Juttle 【29】),功能反应式编程(FRP)语言(如Elm 【30,31】)和逻辑编程语言(如Bloom [ 32。 Jay Kreps 【7】提出了在这种背景下解绑的术语。 @@ -261,14 +261,14 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 当一个数据集来自另一个数据集时,它会经历某种转换函数。例如: -* 辅助索引是一种具有直接转换函数的衍生数据集:对于基表中的每一行或文档,它挑选被索引的列或字段中的值,并按这些值排序(假设B - 树或SSTable索引,按键排序,如第3章所述)。 +* 辅助索引是一种具有直接转换函数的衍生数据集:对于基表中的每一行或文档,它挑选被索引的列或字段中的值,并按这些值排序(假设B树或SSTable索引,按键排序,如[第3章](ch3.md)所述)。 * 通过应用各种自然语言处理功能(如语言检测,分词,词干或词汇化,拼写纠正和同义词识别)创建全文搜索索引,然后构建用于高效查找的数据结构(例如作为倒排索引)。 * 在机器学习系统中,我们可以将模型视为通过应用各种特征提取和统计分析功能从训练数据中导出。当模型应用于新的输入数据时,模型的输出是从输入和模型(因此间接地从训练数据)中导出的。 -* 缓存通常包含将以用户界面(UI)显示的形式的数据聚合。因此填充缓存需要知道UI中引用的字段; UI中的更改可能需要更新缓存填充方式的定义以及重建缓存。 +* 缓存通常包含将以用户界面(UI)显示的形式的数据聚合。因此填充缓存需要知道UI中引用的字段;UI中的更改可能需要更新缓存填充方式的定义以及重建缓存。 -辅助索引的衍生函数通常是必需的,因此它作为核心特性被构建到许多数据库中,您可以仅通过说CREATE INDEX来调用它。对于全文索引,常见语言的基本语言特征可能内置到数据库中,但更复杂的特征通常需要特定于域的调整。在机器学习中,特征工程是众所周知的特定于应用程序的特征,并且通常必须包含关于应用程序的用户交互和部署的详细知识【35】。 +辅助索引的衍生函数通常是必需的,因此它作为核心特性被构建到许多数据库中,您可以仅通过说`CREATE INDEX`来调用它。对于全文索引,常见语言的基本语言特征可能内置到数据库中,但更复杂的特征通常需要特定于域的调整。在机器学习中,特征工程是众所周知的特定于应用程序的特征,并且通常必须包含关于应用程序的用户交互和部署的详细知识【35】。 -当创建衍生数据集的函数不是像创建二级索引那样的标准Cookie切割函数时,需要自定义代码来处理特定于应用程序的方面。而这个自定义代码是许多数据库难以抗争虽然关系数据库通常支持触发器,存储过程和用户定义的函数,它们可以用来在数据库中执行应用程序代码,但它们在数据库设计中已经有所反应了(请参阅“传输事件流”(第447页))。 +当创建衍生数据集的函数不是像创建二级索引那样的标准Cookie切割函数时,需要自定义代码来处理特定于应用程序的方面。而这个自定义代码是许多数据库难以抗争虽然关系数据库通常支持触发器,存储过程和用户定义的函数,它们可以用来在数据库中执行应用程序代码,但它们在数据库设计中已经有所反应了(请参阅“[传输事件流]()”(第447页))。 #### 应用程序代码和状态的分离 @@ -283,21 +283,21 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 在这个典型的Web应用程序模型中,数据库充当一种可以通过网络同步访问的可变共享变量。应用程序可以读取和更新变量,并且数据库负责保持持久性,提供一些并发控制和容错。 -但是,在大多数编程语言中,您无法订阅可变变量中的更改 - 您只能定期读取它。与电子表格不同,如果变量的值发生变化,变量的读者不会收到通知。 (您可以在自己的代码中实现这样的通知 - 这被称为观察者模式 - 但大多数语言没有将此模式作为内置功能。) +但是,在大多数编程语言中,您无法订阅可变变量中的更改 —— 您只能定期读取它。与电子表格不同,如果变量的值发生变化,变量的读者不会收到通知。 (您可以在自己的代码中实现这样的通知 —— 这被称为观察者模式 —— 但大多数语言没有将此模式作为内置功能。) -数据库继承了这种可变数据的被动方法:如果你想知道数据库的内容是否发生了变化,通常你唯一的选择就是轮询(即定期重复你的查询)。 订阅更改只是刚刚开始出现的功能(请参阅第455页的“更改流的API支持”)。 +数据库继承了这种可变数据的被动方法:如果你想知道数据库的内容是否发生了变化,通常你唯一的选择就是轮询(即定期重复你的查询)。 订阅更改只是刚刚开始出现的功能(请参阅第455页的“[更改流的API支持]()”)。 #### 数据流:状态变化和应用程序代码之间的相互影响 从数据流的角度思考应用意味着重新谈判应用代码和状态管理之间的关系。我们不是将数据库视为被应用程序操纵的被动变量,而是更多地考虑状态,状态更改和处理它们的代码之间的相互作用和协作。应用程序代码通过在另一个地方触发状态更改来响应状态更改。 -我们在第451页的“数据库和数据流”中看到了这一思路,我们讨论将数据库更改的日志作为我们可以指定的事件流处理。消息传递系统(如角色)(请参阅第136页的“消息传递数据流”)也具有响应事件的概念。早在20世纪80年代,元组空间模型探索表示分布式计算的过程,观察状态变化并对它们做出反应【38,39】。 +我们在第451页的“[数据库和数据流]()”中看到了这一思路,我们讨论将数据库更改的日志作为我们可以指定的事件流处理。消息传递系统(如角色)(请参阅第136页的“[消息传递数据流]()”)也具有响应事件的概念。早在20世纪80年代,元组空间模型探索表示分布式计算的过程,观察状态变化并对它们做出反应【38,39】。 如前所述,当触发器由于数据更改而触发时,或者次级索引更新以反映索引表中的更改时,数据库内部会发生类似的情况。分解数据库意味着将此想法应用于在主数据库之外创建衍生数据集:缓存,全文搜索索引,机器学习或分析系统。我们可以为此使用流处理和消息传递系统。 -需要记住的重要一点是,维护衍生数据与传统设计消息传递系统的异步作业执行不同(请参阅第448页上的“与传统消息传递相比的日志”): +需要记住的重要一点是,维护衍生数据与传统设计消息传递系统的异步作业执行不同(请参阅第448页上的“[与传统消息传递相比的日志]()”): -•在维护衍生数据时,状态更改的顺序通常很重要(如果多个视图是从事件日志衍生的,则需要按照相同的顺序处理事件,以便它们保持一致)。如第445页上的“确认和重新传递”中所述,许多消息代理在重新传送未确认消息时没有此属性。双重写入也被排除(请参阅第454页上的“保持系统同步”)。 +•在维护衍生数据时,状态更改的顺序通常很重要(如果多个视图是从事件日志衍生的,则需要按照相同的顺序处理事件,以便它们保持一致)。如第445页上的“[确认和重新传递]()”中所述,许多消息代理在重新传送未确认消息时没有此属性。双重写入也被排除(请参阅第454页上的“[保持系统同步]()”)。 •容错是导出数据的关键:仅丢失单个消息会导致衍生数据集永远与其数据源不同步。消息传递和衍生状态更新都必须可靠。例如,许多角色系统默认在内存中维护角色状态和消息,所以如果运行角色的机器崩溃,他们就会丢失。 @@ -309,16 +309,16 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 将流操作符合并到数据流系统中与微服务方法有很多相似的特征【40】。但是,底层的通信机制是非常不同的:单向异步消息流而不是同步请求/响应交互。 -除了第136页上的“消息传递数据流”中列出的优点(如更好的容错性),数据流系统还可以获得更好的性能。例如,假设客户正在购买以一种货币定价但以另一种货币支付的商品。为了执行货币转换,您需要知道当前的汇率。这个操作可以通过两种方式实现【40,41】: +除了第136页上的“[消息传递数据流]()”中列出的优点(如更好的容错性),数据流系统还可以获得更好的性能。例如,假设客户正在购买以一种货币定价但以另一种货币支付的商品。为了执行货币转换,您需要知道当前的汇率。这个操作可以通过两种方式实现【40,41】: 1. 在微服务方法中,处理购买的代码可能会查询汇率服务或数据库以获取特定货币的当前汇率。 2. 在数据流方法中,处理采购的代码将提前订阅汇率更新流,并在当地数据库发生更改时将当前汇率记录下来。处理采购时,只需查询本地数据库即可。 -第二种方法已经将同步网络请求替换为对本地数据库进行查询的另一服务(即使在同一个进程中,该请求也可能在同一台机器上)[^ii]。数据流不仅方法更快,而且更稳健到另一项服务的失败。最快和最可靠的网络请求根本就没有网络请求!我们现在不使用RPC,而是在购买事件和汇率更新事件之间建立流联接(请参阅第473页的“流表联接(流增强)”)。 +第二种方法已经将同步网络请求替换为对本地数据库进行查询的另一服务(即使在同一个进程中,该请求也可能在同一台机器上)[^ii]。数据流不仅方法更快,而且更稳健到另一项服务的失败。最快和最可靠的网络请求根本就没有网络请求!我们现在不使用RPC,而是在购买事件和汇率更新事件之间建立流联接(请参阅第473页的“[流表联接(流增强)]()”)。 -[^ii]: 在微服务方法中,您可以通过在处理购买的服务中本地缓存汇率来避免同步网络请求。 但是,为了使缓存保持新鲜,您需要定期轮询更新的汇率,或订阅更改流 - 这正是数据流方法中发生的情况。 +[^ii]: 在微服务方法中,您可以通过在处理购买的服务中本地缓存汇率来避免同步网络请求。 但是,为了使缓存保持新鲜,您需要定期轮询更新的汇率,或订阅更改流——这正是数据流方法中发生的情况。 -加入是时间相关的:如果购买事件在稍后的时间点被重新处理,汇率将会改变。如果要重建原始输出,则需要获取原始购买时的历史汇率。无论您是查询服务还是订阅汇率更新流,您都需要处理这种时间依赖性(请参阅第475页的“连接的时间依赖性”)。 +加入是时间相关的:如果购买事件在稍后的时间点被重新处理,汇率将会改变。如果要重建原始输出,则需要获取原始购买时的历史汇率。无论您是查询服务还是订阅汇率更新流,您都需要处理这种时间依赖性(请参阅第475页的“[连接的时间依赖性]()”)。 订阅一系列更改,而不是在需要时查询当前状态,使我们更接近类似电子表格的计算模型:当某些数据发生更改时,依赖于此的所有衍生数据都可以快速更新。还有很多未解决的问题,例如围绕时间依赖连接等问题,但我认为围绕数据流想法构建应用程序是一个非常有希望的方向。 @@ -326,7 +326,7 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 ### 观察衍生数据状态 -在抽象层面,上一节讨论的数据流系统为您提供了创建衍生数据集(例如搜索索引,物化视图和预测模型)并使其保持最新的过程。让我们称这个过程为写入路径:只要某些信息被写入系统,它可能会经历批处理和流处理的多个阶段,并且最终每个衍生数据集都会更新以合并写入的数据。图12-1显示了更新搜索索引的示例。 +在抽象层面,上一节讨论的数据流系统为您提供了创建衍生数据集(例如搜索索引,物化视图和预测模型)并使其保持最新的过程。让我们称这个过程为写入路径:只要某些信息被写入系统,它可能会经历批处理和流处理的多个阶段,并且最终每个衍生数据集都会更新以合并写入的数据。[图12-1](img/fig12-1.png)显示了更新搜索索引的示例。 ![](img/fig12-1.png) @@ -334,9 +334,9 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 但为什么你首先创建衍生数据集?很可能是因为你想在以后再次查询它。这是读取路径:在提供从衍生数据集中读取的用户请求时,可能会对结果执行一些更多处理,然后构建对用户的响应。 -总而言之,写入路径和读取路径涵盖了数据的整个旅程,从收集数据的地步到使用数据(可能是由另一个人)。写入路径是预先计算的行程的一部分 - 即,一旦数据进入,即刻完成,无论是否有人要求查看它。阅读路径是旅程中只有当有人要求时才会发生的部分。如果您熟悉函数式编程语言,则可能会注意到写入路径类似于急切的评估,读取路径类似于懒惰评估。 +总而言之,写入路径和读取路径涵盖了数据的整个旅程,从收集数据的地步到使用数据(可能是由另一个人)。写入路径是预先计算的行程的一部分 —— 即,一旦数据进入,即刻完成,无论是否有人要求查看它。阅读路径是旅程中只有当有人要求时才会发生的部分。如果您熟悉函数式编程语言,则可能会注意到写入路径类似于急切的评估,读取路径类似于懒惰评估。 -如图12-1所示,衍生数据集是写入路径和读取路径相遇的地方。它代表了在写入时需要完成的工作量与在读取时需要完成的工作量之间的权衡。 +如[图12-1](img/fig12-1.png)所示,衍生数据集是写入路径和读取路径相遇的地方。它代表了在写入时需要完成的工作量与在读取时需要完成的工作量之间的权衡。 #### 物化视图和缓存 @@ -370,7 +370,7 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 #### 将状态更改推送给客户端 -在典型的网页中,如果您在Web浏览器中加载页面,并且随后服务器上的数据发生更改,则浏览器在重新加载页面之前不会查找有关更改。浏览器只能在一个时间点读取数据,假设它是静态的 - 它不会订阅来自服务器的更新。因此,设备上的状态是一个陈旧的缓存,除非您明确轮询更改,否则不会更新。 (像RSS这样的基于HTTP的订阅源订阅协议实际上只是一种基本的调查形式。) +在典型的网页中,如果您在Web浏览器中加载页面,并且随后服务器上的数据发生更改,则浏览器在重新加载页面之前不会查找有关更改。浏览器只能在一个时间点读取数据,假设它是静态的 —— 它不会订阅来自服务器的更新。因此,设备上的状态是一个陈旧的缓存,除非您明确轮询更改,否则不会更新。 (像RSS这样的基于HTTP的订阅源订阅协议实际上只是一种基本的调查形式。) 更新的协议已经超越了HTTP的基本请求/响应模式:服务器发送的事件(EventSource API)和WebSockets提供了通信渠道,通过这些通信渠道,Web浏览器可以与服务器保持开放的TCP连接,服务器可以只要保持连接状态,就会主动将消息推送到浏览器。这为服务器提供了一个机会,主动通知最终用户客户端本地存储状态的任何变化,从而减少客户端状态的陈旧程度。 @@ -380,13 +380,13 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 #### 端到端的事件流 -最近用于开发有状态客户端和用户界面的工具(如Elm语言【30】和Facebook的React,Flux和Redux工具链)已经通过订阅表示用户的事件流来管理内部客户端状态输入或来自服务器的响应,其结构与事件源相似(请参阅第457页的“事件源”)。 +最近用于开发有状态客户端和用户界面的工具(如Elm语言【30】和Facebook的React,Flux和Redux工具链)已经通过订阅表示用户的事件流来管理内部客户端状态输入或来自服务器的响应,其结构与事件源相似(请参阅第457页的“[事件源]()”)。 -将这种编程模型扩展为允许服务器将状态改变事件推送到客户端事件管道中是非常自然的。因此,状态变化可以通过端到端的写入路径流动:从触发状态改变的一个设备上的交互,通过事件日志以及通过多个衍生的数据系统和流处理器,一直到用户界面在另一台设备上观察状态的人。这些状态变化可以以相当低的延迟传播 - 比如说,在一秒内结束。 +将这种编程模型扩展为允许服务器将状态改变事件推送到客户端事件管道中是非常自然的。因此,状态变化可以通过端到端的写入路径流动:从触发状态改变的一个设备上的交互,通过事件日志以及通过多个衍生的数据系统和流处理器,一直到用户界面在另一台设备上观察状态的人。这些状态变化可以以相当低的延迟传播——比如说,在一秒内结束。 一些应用程序(如即时消息传递和在线游戏)已经具有这种“实时”体系结构(从低延迟的交互意义上说,不是“响应时间保证”在本页中的含义)。但为什么我们不用这种方式构建所有的应用程序? -挑战在于无状态客户端和请求/响应交互的假设在我们的数据库,库,框架和协议中非常深入。许多数据存储支持读取和写入操作,请求返回一个响应,但是少得多提供订阅更改的能力 - 即随着时间的推移返回响应流的请求(请参阅“更改流的API支持” 。 +挑战在于无状态客户端和请求/响应交互的假设在我们的数据库,库,框架和协议中非常深入。许多数据存储支持读取和写入操作,请求返回一个响应,但是少得多提供订阅更改的能力 —— 即随着时间的推移返回响应流的请求(请参阅“更改流的API支持” 。 为了将写入路径扩展到最终用户,我们需要从根本上重新思考我们构建这些系统的方式:从请求/响应交互转向发布/订阅数据流【27】。我认为更具响应性的用户界面和更好的离线支持的优势将使其值得付出努力。如果您正在设计数据系统,我希望您会记住订阅更改的选项,而不只是查询当前状态。 @@ -394,36 +394,37 @@ unbundled方法遵循Unix传统的小型工具,它可以很好地完成一件 我们讨论过,当流处理器将衍生数据写入存储(数据库,缓存或索引)时,以及当用户请求查询该存储时,存储将充当写入路径和读取路径之间的边界。该商店允许对数据进行随机访问读取查询,否则这些查询将需要扫描整个事件日志。 -在很多情况下,数据存储与流式传输系统是分开的。但请记住,流处理器还需要维护状态以执行聚合和连接(请参阅第472页的“流连接”)。这种状态通常隐藏在流处理器内部,但是一些框架允许它也被外部客户端查询【45】,将流处理器本身变成一种简单的数据库。 +在很多情况下,数据存储与流式传输系统是分开的。但请记住,流处理器还需要维护状态以执行聚合和连接(请参阅第472页的“[流连接]()”)。这种状态通常隐藏在流处理器内部,但是一些框架允许它也被外部客户端查询【45】,将流处理器本身变成一种简单的数据库。 我想进一步考虑这个想法。正如到目前为止所讨论的那样,对商店的写入是通过事件日志进行的,而读取是瞬时网络请求,直接进入存储被查询数据的节点。这是一个合理的设计,但不是唯一可能的设计。也可以将读取请求表示为事件流,并通过流处理器发送读取事件和写入事件;处理器通过将读取结果发送到输出流来响应读取事件【46】。 -当写入和读取都被表示为事件,并且被路由到同一个流操作符以便处理时,我们实际上是在读查询流和数据库之间执行流表连接。读取事件需要发送到保存数据的数据库分区(请参阅第214页的“请求路由”),就像批处理和流处理器在连接时需要在同一个键上共同输入一样(请参阅“Reduce-Side连接和分组“)。 +当写入和读取都被表示为事件,并且被路由到同一个流操作符以便处理时,我们实际上是在读查询流和数据库之间执行流表连接。读取事件需要发送到保存数据的数据库分区(请参阅第214页的“[请求路由]()”),就像批处理和流处理器在连接时需要在同一个键上共同输入一样(请参阅“[Reduce端连接和分组]()“)。 服务请求和正在执行的连接之间的这种对应关系是非常重要的【47】。一次性读取请求只是通过连接运算符传递请求,然后立即忘记它;订阅请求是与连接另一端的过去和未来事件的持续连接。 记录读取事件的日志可能对于追踪整个系统中的因果关系和数据来源也有好处:它可以让您在做出特定决策之前重建用户看到的内容。例如,在网上商店,向客户显示的预测出货日期和库存状态可能影响他们是否选择购买物品【4】。要分析此连接,您需要记录用户查询运输和库存状态的结果。 -将读取事件写入持久存储器可以更好地跟踪因果关系(请参阅第493页的“订购事件以捕获因果关系”),但会产生额外的存储和I / O成本。优化这些系统以减少开销仍然是一个开放的研究问题【2】。但是,如果您已经为了操作目的而记录了读取请求,作为请求处理的副作用,将日志作为请求的来源并不是什么大的改变。 +将读取事件写入持久存储器可以更好地跟踪因果关系(请参阅第493页的“[顺序事件以捕获因果关系]()”),但会产生额外的存储和I/O成本。优化这些系统以减少开销仍然是一个开放的研究问题【2】。但是,如果您已经为了操作目的而记录了读取请求,作为请求处理的副作用,将日志作为请求的来源并不是什么大的改变。 #### 多分区数据处理 对于只涉及单个分区的查询,通过流发送查询和收集响应流的努力可能是过度的。然而,这个想法打开了分布式执行复杂查询的可能性,这需要合并来自多个分区的数据,利用流处理器已经提供的消息路由,分区和加入的基础设施。 -Storm的分布式RPC功能支持这种使用模式(请参阅第468页的“消息传递和RPC”)。例如,它已被用于计算在Twitter上看到过网址的人数 - 即,每个人都推送了该网址的跟随者集合【48】。由于Twitter用户组是分区的,因此这种计算需要合并来自多个分区的结果 +Storm的分布式RPC功能支持这种使用模式(请参阅第468页的“[消息传递和RPC]()”)。例如,它已被用于计算在Twitter上看到过网址的人数 —— 即,每个人都推送了该网址的跟随者集合【48】。由于Twitter用户组是分区的,因此这种计算需要合并来自多个分区的结果 这种模式的另一个例子是欺诈预防:为了评估特定购买事件是否具有欺诈风险,您可以检查用户的IP地址,电子邮件地址,帐单地址,送货地址等的信誉分数。这些信誉数据库中的每一个都是自身分区的,因此为特定购买事件收集分数需要一系列具有不同分区数据集的联合【49】。 -MPP数据库的内部查询执行图具有相似的特征(请参阅第417页的“比较Hadoop与分布式数据库”)。如果您需要执行这种多分区连接,使用提供此功能的数据库可能比使用流处理器实现它更简单。但是,将查询视为流提供了一个选项,可以实现大规模应用程序,这些应用程序可以在传统的现成解决方案的限制下运行。 +MPP数据库的内部查询执行图具有相似的特征(请参阅第417页的“[比较Hadoop与分布式数据库]()”)。如果您需要执行这种多分区连接,使用提供此功能的数据库可能比使用流处理器实现它更简单。但是,将查询视为流提供了一个选项,可以实现大规模应用程序,这些应用程序可以在传统的现成解决方案的限制下运行。 + ## 目标是正确性 对于只读取数据的无状态服务,出现问题时不会造成什么大问题:您可以修复该错误并重新启动服务,并且一切都恢复正常。像数据库这样的有状态的系统并不是那么简单:它们被设计成永远记住事物(或多或少),所以如果出现问题,效果也可能永远持续下去,这意味着它们需要更仔细的思考【50】。 -我们希望构建可靠和正确的应用程序(即,即使面对各种故障,其语义也能很好地定义和理解的程序)。大约四十年来,原子性,隔离性和耐久性(第7章)的交易特性一直是构建正确应用的首选工具。但是,这些基础比看起来更弱:例如见证弱隔离级别的混合(请参见“弱隔离级别”(第233页))。 +我们希望构建可靠和正确的应用程序(即,即使面对各种故障,其语义也能很好地定义和理解的程序)。大约四十年来,原子性,隔离性和耐久性(第7章)的交易特性一直是构建正确应用的首选工具。但是,这些基础比看起来更弱:例如见证弱隔离级别的混合(请参见“[弱隔离级别]()”(第233页))。 -在某些领域,事务被完全抛弃,并被提供更好性能和可伸缩性的模型取代,但是更复杂的语义(例如,请参阅第167页上的“无Leaderless复制”)。一致性经常被讨论,但定义不明确(参见第224页的“一致性”和第9章)。有些人主张我们应该“为了更好的可用性而拥抱弱一致性”,而对实际上的实际意义缺乏清晰的认识。 +在某些领域,事务被完全抛弃,并被提供更好性能和可伸缩性的模型取代,但是更复杂的语义(例如,请参阅第167页上的“[无主复制]()”)。一致性经常被讨论,但定义不明确(参见第224页的“[一致性]()”和[第9章](ch9.md))。有些人主张我们应该“为了更好的可用性而拥抱弱一致性”,而对实际上的实际意义缺乏清晰的认识。 对于如此重要的话题,我们的理解和我们的工程方法是惊人的片状。例如,确定在特定事务隔离级别或复制配置下运行特定应用程序是否安全是非常困难的【51,52】。通常简单的解决方案似乎在并发性低的情况下正常工作,并且没有错误,但是在要求更高的情况下会出现许多细微的错误。 @@ -437,23 +438,23 @@ MPP数据库的内部查询执行图具有相似的特征(请参阅第417页 仅仅因为应用程序使用提供比较强的安全属性的数据系统(例如可序列化的事务),并不意味着应用程序可以保证没有数据丢失或损坏。例如,如果一个应用程序有一个错误导致它写入不正确的数据,或者从数据库中删除数据,那么可序列化的事务不会为你节省。 -这个例子可能看起来很无聊,但值得认真对待:应用程序错误发生,人们犯错误。我在第459页的“状态,流和不可变性”中使用了这个例子来支持不可变和只能追加的数据,因为如果删除错误代码的能力来破坏好的数据,更容易从这些错误中恢复数据。 +这个例子可能看起来很无聊,但值得认真对待:应用程序错误发生,人们犯错误。我在第459页的“[状态,流和不可变性]()”中使用了这个例子来支持不可变和只能追加的数据,因为如果删除错误代码的能力来破坏好的数据,更容易从这些错误中恢复数据。 虽然不变性是有用的,但它本身并非万能的。让我们看看可能发生的数据损坏的一个更为简单的例子。 #### 正好执行一次操作 -在第476页的“容错”中,我们遇到了一种精确调用一次(或有效一次)语义的想法。如果在处理消息时出现问题,您可以放弃(丢弃消息 - 即导致数据丢失)或再次尝试。如果再试一次,第一次就有成功的风险,但是你没有发现成功,所以这个消息最终被处理了两次。 +在第476页的“[容错]()”中,我们遇到了一种精确调用一次(或有效一次)语义的想法。如果在处理消息时出现问题,您可以放弃(丢弃消息 —— 即导致数据丢失)或再次尝试。如果再试一次,第一次就有成功的风险,但是你没有发现成功,所以这个消息最终被处理了两次。 处理两次是数据损坏的一种形式:对于相同的服务向客户收费两次(计费太多)或增加计数器两次(夸大一些度量)是不可取的。在这种情况下,正好一次就意味着安排计算,使得最终效果与没有发生错误的情况相同,即使操作实际上由于某种错误而被重试。我们以前讨论过实现这一目标的几种方法。 -最有效的方法之一是使幂等操作(参见第478页的“幂等性”);即确保它具有相同的效果,无论是执行一次还是多次。但是,采取一种不自然是幂等的操作并使其具有幂等性需要付出一定的努力和关注:您可能需要维护一些额外的元数据(例如更新了值的操作ID集合),并在从一个节点到另一个节点(请参阅第295页上的“领导和锁定”)。 +最有效的方法之一是使幂等操作(参见第478页的“[幂等性]()”);即确保它具有相同的效果,无论是执行一次还是多次。但是,采取一种不自然是幂等的操作并使其具有幂等性需要付出一定的努力和关注:您可能需要维护一些额外的元数据(例如更新了值的操作ID集合),并在从一个节点到另一个节点(请参阅第295页上的“[领导和锁定]()”)。 #### 重复抑制 除了流处理之外,还需要抑制重复的相同模式出现在许多其他位置。例如,TCP使用数据包上的序列号将它们按正确的顺序排列在收件人处,并确定网络上是否有数据包丢失或重复。任何丢失的数据包都会被重新传输,并且在将数据交给应用程序之前,TCP堆栈会删除任何重复数据包。 -但是,此重复抑制仅适用于单个TCP连接的上下文中。假设TCP连接是一个客户端与数据库的连接,并且它正在执行示例12-1中的事务。在许多数据库中,事务与客户端连接有关(如果客户端发送了多个查询,数据库就知道它们属于同一个事务,因为它们是在同一个TCP连接上发送的)。如果客户端在发送COMMIT之后但在从数据库服务器回听之前遇到网络中断和连接超时,则不知道事务是否已被提交或中止(图8-1)。 +但是,此重复抑制仅适用于单个TCP连接的上下文中。假设TCP连接是一个客户端与数据库的连接,并且它正在执行[例12-1]()中的事务。在许多数据库中,事务与客户端连接有关(如果客户端发送了多个查询,数据库就知道它们属于同一个事务,因为它们是在同一个TCP连接上发送的)。如果客户端在发送COMMIT之后但在从数据库服务器回听之前遇到网络中断和连接超时,则不知道事务是否已被提交或中止([图8-1](img/fig8-1.png))。 **例12-1 将资金从一个账户转移到另一个账户的非赦免** @@ -464,16 +465,16 @@ BEGIN TRANSACTION; COMMIT; ``` -客户端可以重新连接到数据库并重试事务,但现在它在TCP重复抑制的范围之外。由于例12-1中的交易不是幂等的,可能会发生\$22而不是所需的\$11。因此,尽管例12-1是一个交易原子性的标准例子,但它实际上并不正确,而真正的银行并不像这样工作【3】。 +客户端可以重新连接到数据库并重试事务,但现在它在TCP重复抑制的范围之外。由于[例12-1]()中的交易不是幂等的,可能会发生\$22而不是所需的\$11。因此,尽管[例12-1]()是一个交易原子性的标准例子,但它实际上并不正确,而真正的银行并不像这样工作【3】。 -两阶段提交(请参阅第354页上的“原子提交和两阶段提交(2PC)”)协议会破坏TCP连接和事务之间的1:1映射,因为它们必须允许事务协调器在数据库之后重新连接到数据库一个网络故障,并告诉它是否提交或中止有疑问的交易。这足以确保交易只能执行一次吗?不幸的是,即使我们可以抑制数据库客户端和服务器之间的重复事务,我们仍然需要担心最终用户设备和应用程序服务器之间的网络。例如,如果最终用户客户端是Web浏览器,则可能使用HTTP POST请求向服务器提交指令。也许用户处于一个微弱的蜂窝数据连接,他们成功地发送POST,但是信号在他们能够从服务器接收响应之前变得太弱。 +两阶段提交(请参阅第354页上的“[原子提交和两阶段提交(2PC)]()”)协议会破坏TCP连接和事务之间的1:1映射,因为它们必须允许事务协调器在数据库之后重新连接到数据库一个网络故障,并告诉它是否提交或中止有疑问的交易。这足以确保交易只能执行一次吗?不幸的是,即使我们可以抑制数据库客户端和服务器之间的重复事务,我们仍然需要担心最终用户设备和应用程序服务器之间的网络。例如,如果最终用户客户端是Web浏览器,则可能使用HTTP POST请求向服务器提交指令。也许用户处于一个微弱的蜂窝数据连接,他们成功地发送POST,但是信号在他们能够从服务器接收响应之前变得太弱。 -在这种情况下,用户可能会显示错误消息,并且可能会手动重试。 Web浏览器警告说,“你确定要再次提交这个表单吗?” - 用户说是,因为他们希望操作发生。 (Post / Redirect / Get模式【54】可以避免在正常操作中出现此警告消息,但如果POST请求超时,它将无济于事。)从Web服务器的角度来看,重试是一个单独的请求,并且从数据库的角度来看,这是一个单独的事务。通常的重复数据删除机制无济于事。 +在这种情况下,用户可能会显示错误消息,并且可能会手动重试。 Web浏览器警告说,“你确定要再次提交这个表单吗?” —— 用户说是,因为他们希望操作发生。 (Post / Redirect / Get模式【54】可以避免在正常操作中出现此警告消息,但如果POST请求超时,它将无济于事。)从Web服务器的角度来看,重试是一个单独的请求,并且从数据库的角度来看,这是一个单独的事务。通常的重复数据删除机制无济于事。 #### 操作标识符 -要通过几次网络通信使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的 - 您需要考虑请求的端到端流。 -例如,您可以为操作(例如UUID)生成唯一的标识符,并将其作为隐藏的表单字段包含在客户机应用程序中,或计算所有相关表单字段的散列以衍生操作ID 【3】。如果Web浏览器提交两次POST请求,这两个请求将具有相同的操作ID。然后,您可以将该操作ID传递到数据库,并检查您是否只使用给定的ID执行一个操作,如示例12-2中所示。 +要通过几次网络通信使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的—— 您需要考虑请求的端到端流。 +例如,您可以为操作(例如UUID)生成唯一的标识符,并将其作为隐藏的表单字段包含在客户机应用程序中,或计算所有相关表单字段的散列以衍生操作ID 【3】。如果Web浏览器提交两次POST请求,这两个请求将具有相同的操作ID。然后,您可以将该操作ID传递到数据库,并检查您是否只使用给定的ID执行一个操作,如[例12-2]()中所示。 **例12-2 使用唯一的ID来抑制重复的请求** ```sql @@ -487,15 +488,16 @@ BEGIN TRANSACTION; COMMIT; ``` -例12-2依赖于request_id列上的唯一性约束。如果一个事务尝试插入一个已经存在的ID,那么INSERT失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束(而在第248页上的“编写偏斜和幻影”中讨论过,应用程序级别的check-then-insert可能会在不可序列化的隔离下失败)。 +[例12-2]()依赖于`request_id`列上的唯一性约束。如果一个事务尝试插入一个已经存在的ID,那么`INSERT`失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束(而在第248页上的“[写入偏差和幻读]()”中讨论过,应用程序级别的check-then-insert可能会在不可序列化的隔离下失败)。 -除了抑制重复的请求之外,示例12-2中的请求表充当事件日志的一种,暗示着事件源的方向(请参阅第457页的“事件源”)。账户余额的更新事实上不必与插入事件相同的事务发生,因为它们是多余的,并且可以从下游消费者中的请求事件衍生出来 - 只要该事件只处理一次,这可以再次使用请求ID来执行。 +除了抑制重复的请求之外,示[例12-2]()中的请求表充当事件日志的一种,暗示着事件源的方向(请参阅第457页的“[事件源]()”)。账户余额的更新事实上不必与插入事件相同的事务发生,因为它们是多余的,并且可以从下游消费者中的请求事件衍生出来 —— 只要该事件只处理一次,这可以再次使用请求ID来执行。 **端到端的论点** 抑制重复交易的这种情况只是一个更普遍的原则的一个例子,这个原则被称为端对端的论点,它在1984年由Saltzer,Reed和Clark阐述【55】: 只有在通信系统端点的应用程序的知识和帮助下,所讨论的功能才能够完全正确地实现。因此,将这种被质疑的功能作为通信系统本身的功能是不可能的。 (有时,通信系统提供的功能的不完整版本可能有助于提高性能。) + 在我们的例子中,有问题的函数是重复抑制。我们看到TCP在TCP连接级别抑制重复的数据包,一些流处理器在消息处理级别提供了所谓的唯一的语义,但是这不足以防止用户提交重复请求一次。 TCP,数据库事务和流处理器本身并不能完全排除这些重复。解决这个问题需要一个端到端的解决方案:从最终用户客户端一直传递到数据库的事务标识符。 端到端参数也适用于检查数据的完整性:以太网,TCP和TLS中内置的校验和可以检测网络中数据包的损坏情况,但是它们无法检测到发送和接收软件中的错误网络连接的末端,或数据存储在磁盘上的损坏。如果您想要捕获所有可能的数据损坏源,则还需要端到端的校验和。 @@ -508,11 +510,11 @@ COMMIT; 这使我回到我的原始论文:仅仅因为应用程序使用提供比较强的安全属性的数据系统(如可序列化事务),并不意味着应用程序保证不会丢失数据或损坏。应用程序本身也需要采取端到端的措施,例如重复压制。 -这是一个耻辱,因为容错机制很难得到正确的。低级可靠性机制(如TCP中的可靠性机制)运行良好,因此其余较高级别的故障发生得相当少。将抽象中的高级容错机制封装起来非常好,以便应用程序代码不必担心它 - 但是我担心我们还没有找到合适的抽象。 +这是一个耻辱,因为容错机制很难得到正确的。低级可靠性机制(如TCP中的可靠性机制)运行良好,因此其余较高级别的故障发生得相当少。将抽象中的高级容错机制封装起来非常好,以便应用程序代码不必担心它 —— 但是我担心我们还没有找到合适的抽象。 长期以来,交易被认为是一个很好的抽象,我相信它们是有用的。正如第7章介绍中所讨论的那样,它们会涉及各种可能的问题(并发写入,约束违规,崩溃,网络中断,磁盘故障),并将其折叠为两种可能的结果:提交或中止。这是编程模型的一个巨大的简化,但我担心这是不够的。 -事务处理非常昂贵,尤其是涉及异构存储技术时(请参阅第364页的“实践中的分布式事务”)。当我们拒绝使用分布式事务是因为它们太昂贵时,我们最终不得不在应用程序代码中重新实现容错机制。正如本书中大量的例子所显示的那样,关于并发性和部分失败的推理是困难且违反直觉的,所以我怀疑大多数应用程序级别的机制不能正常工作。结果是丢失或损坏的数据。 +事务处理非常昂贵,尤其是涉及异构存储技术时(请参阅第364页的“[实践中的分布式事务]()”)。当我们拒绝使用分布式事务是因为它们太昂贵时,我们最终不得不在应用程序代码中重新实现容错机制。正如本书中大量的例子所显示的那样,关于并发性和部分失败的推理是困难且违反直觉的,所以我怀疑大多数应用程序级别的机制不能正常工作。结果是丢失或损坏的数据。 出于这些原因,我认为值得探索的容错抽象方法能够容易地提供特定于应用程序的端到端正确性属性,而且还可以在大型分布式环境中保持良好的性能和良好的操作特性。 @@ -520,39 +522,39 @@ COMMIT; ### 强制实施约束 -让我们考虑非捆绑数据库的想法背景下的正确性(“剥离数据库”,第499页)。我们看到,使用从客户端一直传递到记录写入的数据库的请求标识可以实现端到端的重复压缩。其他类型的限制呢? +让我们考虑非捆绑数据库的想法背景下的正确性(“[剥离数据库]()”,第499页)。我们看到,使用从客户端一直传递到记录写入的数据库的请求标识可以实现端到端的重复压缩。其他类型的限制呢? -特别是让我们关注唯一性约束 - 例如我们在例12-2中所依赖的约束。在第330页的“约束和唯一性保证”中,我们看到了几个其他需要强制实施唯一性的应用程序功能示例:用户名或电子邮件地址必须唯一标识用户,文件存储服务不能包含多个文件同名,两个人不能在航班或剧院预订同一个座位。 +特别是让我们关注唯一性约束 —— 例如我们在[例12-2]()中所依赖的约束。在第330页的“[约束和唯一性保证]()”中,我们看到了几个其他需要强制实施唯一性的应用程序功能示例:用户名或电子邮件地址必须唯一标识用户,文件存储服务不能包含多个文件同名,两个人不能在航班或剧院预订同一个座位。 其他类型的约束非常相似:例如,确保帐户余额永远不会变为负数,您不会出售比仓库中的库存更多的物料,或者会议室没有重复的预订。执行唯一性的技术通常也可以用于这些约束。 #### 唯一性约束需要达成共识 -在第9章中我们看到,在分布式环境中,强制执行唯一性约束需要达成共识:如果存在多个具有相同值的并发请求,则系统需要决定哪个冲突操作被接受,并拒绝其他违规操作的约束。 +在[第9章](ch9.md)中我们看到,在分布式环境中,强制执行唯一性约束需要达成共识:如果存在多个具有相同值的并发请求,则系统需要决定哪个冲突操作被接受,并拒绝其他违规操作的约束。 -达成这一共识的最常见方式是将单个节点作为领导者,并将其负责制定所有决策。只要您不介意通过单个节点发送所有请求(即使客户端位于世界的另一端),并且只要该节点没有失败,就可以正常工作。如果您需要容忍领导者失败,那么您又回到了共识问题(请参阅第367页上的“单领导表示和共识”)。 +达成这一共识的最常见方式是将单个节点作为领导者,并将其负责制定所有决策。只要您不介意通过单个节点发送所有请求(即使客户端位于世界的另一端),并且只要该节点没有失败,就可以正常工作。如果您需要容忍领导者失败,那么您又回到了共识问题(请参阅第367页上的“[单领导表示和共识]()”)。 -唯一性检查可以根据需要唯一的值进行划分。例如,如果需要通过请求标识确保唯一性(如例12-2所示),则可以确保具有相同请求标识的所有请求都路由到同一分区(请参阅第6章)。如果您需要用户名是唯一的,您可以通过用户名的哈希分区。 +唯一性检查可以根据需要唯一的值进行划分。例如,如果需要通过请求标识确保唯一性(如[例12-2]()所示),则可以确保具有相同请求标识的所有请求都路由到同一分区(请参阅[第6章](ch6.md))。如果您需要用户名是唯一的,您可以通过用户名的哈希分区。 -但是,排除了异步多主复制,因为它可能会导致不同的主设备同时接受冲突的写操作,因此这些值不再是唯一的(请参阅第295页的“实现可线性化系统”)。如果你想立即拒绝任何违反约束的写入,同步协调是不可避免的【56】。 +但是,排除了异步多主复制,因为它可能会导致不同的主设备同时接受冲突的写操作,因此这些值不再是唯一的(请参阅第295页的“[实现可线性化系统]()”)。如果你想立即拒绝任何违反约束的写入,同步协调是不可避免的【56】。 #### 基于日志的消息传递的唯一性 -该日志确保所有消费者以相同的顺序查看消息 - 这种保证在形式上被称为全部命令广播并且等同于共识(参见第346页上的“全部命令广播”)。在使用基于日志的消息传递的非捆绑数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。 +该日志确保所有消费者以相同的顺序查看消息 —— 这种保证在形式上被称为全部命令广播并且等同于共识(参见第346页上的“[全序广播]()”)。在使用基于日志的消息传递的非捆绑数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。 -流处理器在单个线程上依次占用日志分区中的所有消息(请参见第448页的“与传统消息传递相比的日志”)。因此,如果日志根据需要唯一的值进行分区,则流处理器可以明确并确定性地确定几个冲突操作中的哪一个先到达。例如,在多个用户尝试声明相同用户名的情况下【57】: +流处理器在单个线程上依次占用日志分区中的所有消息(请参见第448页的“[与传统消息传递相比的日志]()”)。因此,如果日志根据需要唯一的值进行分区,则流处理器可以明确并确定性地确定几个冲突操作中的哪一个先到达。例如,在多个用户尝试声明相同用户名的情况下【57】: 1. 对用户名的每个请求都被编码为一条消息,并附加到由用户名散列确定的分区。 2. 流处理器使用本地数据库连续读取日志中的请求,以跟踪使用哪些用户名。对于每个可用的用户名请求,它都会记录该名称并将成功消息发送到输出流。对于每个已经被使用的用户名请求,它都会向输出流发送拒绝消息。 3. 请求用户名的客户端观察输出流并等待与其请求相对应的成功或拒绝消息。 -该算法基本上与第363页上的“使用全阶广播实现线性化存储”中的算法相同。它可以通过增加分区数容易地扩展为较大的请求吞吐量,因为可以独立处理每个分区。 +该算法基本上与第363页上的“[使用全序广播实现线性化存储]()”中的算法相同。它可以通过增加分区数容易地扩展为较大的请求吞吐量,因为可以独立处理每个分区。 -该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如第174页上的“什么是冲突?”和第246页上的“写入偏移和幻影”中所述,冲突的定义可能取决于应用程序,但流处理器可以使用任意逻辑来验证请求。这个想法与Bayou在20世纪90年代开创的方法类似【58】。 +该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如第174页上的“什么是冲突?”和第246页上的“[写入偏差和幻读]()”中所述,冲突的定义可能取决于应用程序,但流处理器可以使用任意逻辑来验证请求。这个想法与Bayou在20世纪90年代开创的方法类似【58】。 #### 多分区请求处理 -当涉及多个分区时,确保操作以原子方式执行,同时满足约束条件变得更有趣。在示例12-2中,可能有三个分区:一个包含请求ID,一个包含收款人账户,另一个包含付款人账户。没有理由把这三件事放在同一个分区,因为它们都是相互独立的。 +当涉及多个分区时,确保操作以原子方式执行,同时满足约束条件变得更有趣。在示[例12-2]()中,可能有三个分区:一个包含请求ID,一个包含收款人账户,另一个包含付款人账户。没有理由把这三件事放在同一个分区,因为它们都是相互独立的。 在数据库的传统方法中,执行此事务需要跨所有三个分区进行原子提交,这实质上会将它强制为与任何这些分区上的所有其他事务的总顺序。由于现在存在跨分区协调,不能再独立处理不同的分区,因此吞吐量可能会受到影响。 但是,事实证明,使用分区日志可以实现同等的正确性,并且不需要原子提交: @@ -561,15 +563,15 @@ COMMIT; 2. 流处理器读取请求的日志。对于每个请求消息,它发出两条消息以输出流:付款人账户A(由A分配)的借方指令和收款人账户B(由B分区)的信贷指令。原始的请求ID包含在那些发出的消息中。 3. 其他处理器使用信用卡和借记指令流,通过请求ID进行扣除,并将更改应用于账户余额。 -步骤1和步骤2是必要的,因为如果客户直接发送信用和借记指令,则需要在这两个分区之间进行原子提交以确保两者都不发生。为了避免分布式事务的需要,我们首先将请求永久记录为单条消息,然后从第一条消息中获取信用和借记指令。单对象写入在几乎所有数据系统中都是原子性的(请参阅“单对象写入”第213页),因此请求既可以出现在日志中,也可以不出现,而不需要多分区原子com-麻省理工学院。 +步骤1和步骤2是必要的,因为如果客户直接发送信用和借记指令,则需要在这两个分区之间进行原子提交以确保两者都不发生。为了避免分布式事务的需要,我们首先将请求永久记录为单条消息,然后从第一条消息中获取信用和借记指令。单对象写入在几乎所有数据系统中都是原子性的(请参阅“[单对象写入]()”第213页),因此请求既可以出现在日志中,也可以不出现,而不需要多分区原子com-麻省理工学院。 如果步骤2中的流处理器崩溃,则从上一个检查点恢复处理。这样做时,它不会跳过任何请求消息,但可能会多次处理请求并产生重复的信用和借记指令。但是,由于它是确定性的,因此它只会再次生成相同的指令,并且步骤3中的处理器可以使用端到端请求ID轻松地对它们进行重复数据删除。 -如果您想确保付款人帐户不会因此次转账而透支,您可以额外使用流处理器(分区)使用多个不同分区的阶段的想法与我们所讨论的类似“多分区数据处理”一节第514页(另请参阅“并发控制”一节第462页)。 +如果您想确保付款人帐户不会因此次转账而透支,您可以额外使用流处理器(分区)使用多个不同分区的阶段的想法与我们所讨论的类似“多分区数据处理”一节第514页(另请参阅“[并发控制]()”一节第462页)。 ### 及时性与完整性 -事务的一个方便属性是它们通常是可线性化的(请参阅“可用性”),也就是说,一个作者等待事务提交,之后其所有读者立即可以看到它的写入。 +事务的一个方便属性是它们通常是可线性化的(请参阅“[可用性]()”),也就是说,一个作者等待事务提交,之后其所有读者立即可以看到它的写入。 在跨流处理器的多个阶段拆分操作时情况并非如此:日志的使用者在设计上是异步的,因此发送者不会等到其消息已经被消费者处理。但是,客户端可能会等待消息出现在输出流上。这是我们在检查是否满足唯一性约束时在“基于日志的消息传递中的唯一性”一节中所做的操作。 @@ -579,18 +581,18 @@ COMMIT; ***及时性*** -及时性意味着确保用户观察系统处于最新状态。我们之前看到,如果用户从数据的陈旧副本中读取数据,他们可能会以不一致的状态观察数据(请参阅第161页上的“复制滞后的问题”)。但是,这种不一致是暂时的,最终只能通过等待和再次尝试来解决。 +及时性意味着确保用户观察系统处于最新状态。我们之前看到,如果用户从数据的陈旧副本中读取数据,他们可能会以不一致的状态观察数据(请参阅第161页上的“[复制延迟问题]()”)。但是,这种不一致是暂时的,最终只能通过等待和再次尝试来解决。 -CAP定理(参见第359页的“线性化成本”)使用线性化的意义上的一致性,这是实现及时性的强有力的方法。像写后读一致性这样的时效性较弱的属性(请参阅第162页的“读取自己写的内容”)也很有用。 +CAP定理(参见第359页的“[线性一致性的代价]()”)使用线性一致的意义上的一致性,这是实现及时性的强有力的方法。像写后读一致性这样的时效性较弱的属性(请参阅第162页的“[读己之写]()”)也很有用。 ***完整性*** -诚信意味着没有腐败;即没有数据丢失,并且没有矛盾或错误的数据。尤其是,如果将某些衍生数据集作为某些基础数据的视图进行维护(请参阅“从事件日志导出当前状态”(第458页)),衍生必须正确。例如,数据库索引必须正确地反映数据库的内容 - 缺少某些记录的索引不是很有用。如果完整性受到侵犯,这种不一致是永久性的:在大多数情况下,等待并再次尝试不会修复数据库损坏。相反,需要明确的检查和修理。在ACID事务的上下文中(参见第223页上的“ACID的含义”),一致性通常被理解为某种特定于应用程序的完整性概念。原子性和耐久性是保持完整性的重要工具。 +诚信意味着没有腐败;即没有数据丢失,并且没有矛盾或错误的数据。尤其是,如果将某些衍生数据集作为某些基础数据的视图进行维护(请参阅“[从事件日志导出当前状态]()”(第458页)),衍生必须正确。例如,数据库索引必须正确地反映数据库的内容 —— 缺少某些记录的索引不是很有用。如果完整性受到侵犯,这种不一致是永久性的:在大多数情况下,等待并再次尝试不会修复数据库损坏。相反,需要明确的检查和修理。在ACID事务的上下文中(参见第223页上的“[ACID的含义]()”),一致性通常被理解为某种特定于应用程序的完整性概念。原子性和耐久性是保持完整性的重要工具。 口号形式:违反及时性是“最终一致性”,而违反诚信则是“永久不一致”。 我要断言,在大多数应用中,完整性比时间要重要得多。违反时效可能令人讨厌和混淆,但是对正直的侵犯可能是灾难性的。 -例如,在您的信用卡对账单上,如果您在过去24小时内完成的交易尚未出现,这并不奇怪 - 这些系统有一定的滞后是正常的。我们知道银行协调和异步结算交易,并且这里的及时性并不重要【3】。但是,如果报表余额不等于交易总和加上先前的报表余额(数额错误),或者交易是向您收取但未支付给商家的话,那将是非常糟糕的(消失的金钱)。这样的问题会违反系统的完整性。 +例如,在您的信用卡对账单上,如果您在过去24小时内完成的交易尚未出现,这并不奇怪 —— 这些系统有一定的滞后是正常的。我们知道银行协调和异步结算交易,并且这里的及时性并不重要【3】。但是,如果报表余额不等于交易总和加上先前的报表余额(数额错误),或者交易是向您收取但未支付给商家的话,那将是非常糟糕的(消失的金钱)。这样的问题会违反系统的完整性。 #### 数据流系统的正确性 @@ -598,16 +600,16 @@ ACID事务通常既提供时间性(例如线性化)又提供完整性(例 另一方面,我们在本章中讨论的基于事件的数据流系统的一个有趣特性是它们将时间性和完整性分开。在异步处理事件流时,不能保证及时性,除非在返回之前明确地构建等待消息到达的消费者。但是,完整性实际上是流式传输系统的核心。 -一次或一次有效的语义(请参阅“故障容错”一节第437页)是一种保持完整性的机制。如果事件丢失,或者事件发生两次,数据系统的完整性可能被侵犯。因此,容错消息传递和重复抑制(例如,幂等操作)对于在面对故障时保持数据系统的完整性是重要的。 +一次或一次有效的语义(请参阅“[故障容错]()”一节第437页)是一种保持完整性的机制。如果事件丢失,或者事件发生两次,数据系统的完整性可能被侵犯。因此,容错消息传递和重复抑制(例如,幂等操作)对于在面对故障时保持数据系统的完整性是重要的。 正如我们在上一节看到的那样,可靠的流处理系统可以在不需要分布式事务和原子提交协议的情况下保持整体性,这意味着它们可以实现更好的可比较的正确性 性能和运行稳健性。我们通过以下机制的结合实现了这一完整性: -* 将写入操作的内容表示为单个消息,可以轻松地以原子方式编写 - 这种方法非常适合事件采购(请参阅第457页的“事件采购”)。 -* 使用确定性描述函数从该单个消息中获取所有其他状态更新,这与存储过程类似(请参见第252页的“实际的串行执行”和第479页的“作为衍生函数的应用程序代码” +* 将写入操作的内容表示为单个消息,可以轻松地以原子方式编写 —— 这种方法非常适合事件采购(请参阅第457页的“事件采购”)。 +* 使用确定性描述函数从该单个消息中获取所有其他状态更新,这与存储过程类似(请参见第252页的“[真的串行执行]()”和第479页的“[作为衍生函数的应用程序代码]()”) * 通过所有这些级别的处理传递客户端生成的请求ID,启用端到端重复抑制和幂等性 -* 使消息不可变并允许衍生数据不时重新处理,这使得从错误中恢复变得更加容易(请参阅“不可变事件的优点”第367页) +* 使消息不可变并允许衍生数据不时重新处理,这使得从错误中恢复变得更加容易(请参阅“[不可变事件的优点]()”第367页) 这种机制的组合在我看来是未来构建容错应用程序的一个非常有前途的方向。 @@ -617,7 +619,7 @@ ACID事务通常既提供时间性(例如线性化)又提供完整性(例 然而,另一个要认识到的是,许多真正的应用程序实际上可以摆脱唯一性较弱的概念: -* 如果两个人同时注册相同的用户名或预订相同的座位,则可以发送其中一个消息来道歉,并要求他们选择不同的用户名。这种纠正错误的变化被称为补偿性交易【59,60】。 +* 如果两个人同时注册相同的用户名或预订相同的座位,则可以发送其中一个消息来道歉,并要求他们选择不同的用户名。这种纠正错误的变化被称为补偿性事务【59,60】。 * 如果客户订购的物品多于仓库中的物品,则可以订购更多库存,为延误向客户道歉,并向他们提供折扣。实际上,如果叉车在仓库中的某些物品上方跑过来,而库存的物品数量比您想象的要少,那么您就必须这样做【61】。因此,无论如何,道歉工作流程已经需要成为业务流程的一部分,因此可能不需要对库存中的项目数量进行线性化约束。 * 同样地,许多航空公司预计飞行员会错过飞机,许多旅馆超额预订客房,预计部分客人将取消预订。在这些情况下,出于商业原因故意违反了“每个座位一人”的约束,并且处理补偿过程(退款,升级,在邻近酒店提供免费房间)以处理需求超过供应的情况。即使没有超额预订,为了应对由于恶劣天气而被取消的航班或者罢工的员工,这些问题的恢复仅仅是商业活动的正常组成部分,就需要道歉和赔偿流程。 * 如果有人收取比他们账户中更多的钱,银行可以向他们收取透支费用,并要求他们偿还欠款。通过限制每天的提款总额,银行的风险是有限的。 @@ -637,21 +639,21 @@ ACID事务通常既提供时间性(例如线性化)又提供完整性(例 总之,这些观察意味着数据流系统可以为许多应用程序提供数据管理服务,而不需要协调,同时仍然提供强大的完整性保证。这种避免协调的数据系统具有很大的吸引力:它们可以比需要执行同步协调的系统获得更好的性能和容错能力【56】。 -例如,这种系统可以在多引导器配置中跨多个数据中心进行分布式操作,在区域之间异步复制。任何一个数据中心都可以继续独立运行,因为不需要同步跨区域协调。这样一个系统的时效性保证会很弱 - 如果不引入协调,它就不可能线性化,但它仍然可以提供强有力的完整性保证。 +例如,这种系统可以在多引导器配置中跨多个数据中心进行分布式操作,在区域之间异步复制。任何一个数据中心都可以继续独立运行,因为不需要同步跨区域协调。这样一个系统的时效性保证会很弱 —— 如果不引入协调,它就不可能线性化,但它仍然可以提供强有力的完整性保证。 -在这种情况下,序列化事务作为维护衍生状态的一部分仍然是有用的,但它们可以在小范围内运行,在那里它们工作得很好【8】。异构分布式事务(如XA事务)(请参阅“实践中的分布式事务”(第367页))不是必需的。同步协调仍然可以在需要的地方引入(例如,在无法恢复的操作之前执行严格的限制),但是如果只有一个小的协议,则不需要任何东西来支付协调费用应用程序的一部分需要它【43】。 +在这种情况下,序列化事务作为维护衍生状态的一部分仍然是有用的,但它们可以在小范围内运行,在那里它们工作得很好【8】。异构分布式事务(如XA事务)(请参阅“[实践中的分布式事务]()”(第367页))不是必需的。同步协调仍然可以在需要的地方引入(例如,在无法恢复的操作之前执行严格的限制),但是如果只有一个小的协议,则不需要任何东西来支付协调费用应用程序的一部分需要它【43】。 -查看协调和约束的另一种方法是:它们减少了由于不一致而必须做出的道歉数量,但也可能会降低系统的性能和可用性,从而可能会增加必须制定的道歉数量中断。您不能将道歉数量减少到零,但您可以根据自己的需求寻找最佳平衡点 - 这是既不存在太多不一致性又不存在太多可用性问题的最佳选择。 +查看协调和约束的另一种方法是:它们减少了由于不一致而必须做出的道歉数量,但也可能会降低系统的性能和可用性,从而可能会增加必须制定的道歉数量中断。您不能将道歉数量减少到零,但您可以根据自己的需求寻找最佳平衡点 —— 这是既不存在太多不一致性又不存在太多可用性问题的最佳选择。 ### 信任,但需要验证 -我们所有关于正确性,完整性和容错性的讨论都假设某些事情可能会出错,但其他事情则不会。我们将这些假设称为我们的系统模型(请参阅“将系统模型映射到现实世界”一节第309页):例如,我们应该假设进程可能会崩溃,机器可能突然断电,网络可能会任意延迟或丢弃消息。但是我们也可以假设写入磁盘的数据在fsync之后不会丢失,内存中的数据没有损坏,并且CPU的乘法指令总是返回正确的结果。 +我们所有关于正确性,完整性和容错性的讨论都假设某些事情可能会出错,但其他事情则不会。我们将这些假设称为我们的系统模型(请参阅“[将系统模型映射到现实世界]()”一节第309页):例如,我们应该假设进程可能会崩溃,机器可能突然断电,网络可能会任意延迟或丢弃消息。但是我们也可以假设写入磁盘的数据在`fsync`之后不会丢失,内存中的数据没有损坏,并且CPU的乘法指令总是返回正确的结果。 这些假设是非常合理的,因为大多数时候它们都是真实的,如果我们不得不经常担心计算机出错,那么很难完成任何事情。传统上,系统模型采用二元方法处理故障:我们假设有些事情会发生,而其他事情永远不会发生。实际上,这更像是一个概率问题:有些事情更可能,其他事情不太可能。问题是违反我们的假设是否经常发生,以至于我们可能在实践中遇到它们。 -我们已经看到,数据可能会在磁盘未触及时破损(请参阅第227页的“复制和耐久性”),并且网络上的数据损坏有时可能会妨碍TCP校验和(请参阅第293页的“虚弱形式的说谎” )。也许这是我们应该更加关注的事情? +我们已经看到,数据可能会在磁盘未触及时破损(请参阅第227页的“[复制和耐久性]()”),并且网络上的数据损坏有时可能会妨碍TCP校验和(请参阅第293页的“[虚弱形式的说谎]()” )。也许这是我们应该更加关注的事情? -我过去曾经使用过的一个应用程序收集了来自客户端的崩溃报告,我们收到的一些报告只能通过在这些设备的内存中随机位翻转来解释。这看起来不太可能,但是如果你有足够的设备来运行你的软件,那么即使发生的事情也不会发生。除了由于硬件故障或辐射导致的随机存储器损坏之外,某些病态存储器访问模式甚至可以在没有故障的存储器中翻转位【62】 - 可用于破坏操作系统中安全机制的效应【63】技术被称为rowhammer)。一旦你仔细观察,硬件并不是完美的抽象。 +我过去曾经使用过的一个应用程序收集了来自客户端的崩溃报告,我们收到的一些报告只能通过在这些设备的内存中随机位翻转来解释。这看起来不太可能,但是如果你有足够的设备来运行你的软件,那么即使发生的事情也不会发生。除了由于硬件故障或辐射导致的随机存储器损坏之外,某些病态存储器访问模式甚至可以在没有故障的存储器中翻转位【62】 —— 可用于破坏操作系统中安全机制的效应【63】技术被称为rowhammer)。一旦你仔细观察,硬件并不是完美的抽象。 要清楚的是,随机位翻转在现代硬件上仍然非常罕见【64】。我只想指出,他们并没有超越可能性领域,所以他们值得关注。 @@ -663,7 +665,7 @@ ACID事务通常既提供时间性(例如线性化)又提供完整性(例 当涉及到应用程序代码时,我们不得不承担更多的错误,因为大多数应用程序在数据库代码所做的评审和测试的数量上没有接近的地方。许多应用程序甚至没有正确使用数据库提供的用于保存完整性的功能,例如外键或唯一性约束【36】。 -ACID意义上的一致性(请参阅第224页的“一致性”)基于数据库以一致状态启动并且事务将其从一个一致状态转换为另一个一致状态的想法。因此,我们期望数据库始终处于一致状态。但是,如果您认为该交易没有错误,则这个概念才有意义。如果应用程序错误地使用数据库以某种方式,例如,不安全地使用弱隔离级别,数据库的完整性无法得到保证。 +ACID意义上的一致性(请参阅第224页的“[一致性]()”)基于数据库以一致状态启动并且事务将其从一个一致状态转换为另一个一致状态的想法。因此,我们期望数据库始终处于一致状态。但是,如果您认为该交易没有错误,则这个概念才有意义。如果应用程序错误地使用数据库以某种方式,例如,不安全地使用弱隔离级别,数据库的完整性无法得到保证。 #### 不要盲目信任他们的承诺 @@ -677,7 +679,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 #### 验证的文化 -像HDFS和S3这样的系统仍然必须假定磁盘大部分时间都能正常工作 - 这是一个合理的假设,但假设它们始终正常工作并不相同。然而,目前还没有多少系统有这种“信任但是验证”的方式来持续审计自己。许多人认为正确性保证是绝对的,并且没有为罕见数据损坏的可能性做出规定。我希望将来我们会看到更多的自我验证或自我审计系统,不断检查自己的诚信,而不是依赖盲目的信任【68】。 +像HDFS和S3这样的系统仍然必须假定磁盘大部分时间都能正常工作 —— 这是一个合理的假设,但假设它们始终正常工作并不相同。然而,目前还没有多少系统有这种“信任但是验证”的方式来持续审计自己。许多人认为正确性保证是绝对的,并且没有为罕见数据损坏的可能性做出规定。我希望将来我们会看到更多的自我验证或自我审计系统,不断检查自己的诚信,而不是依赖盲目的信任【68】。 我担心ACID数据库的文化导致我们在盲目信任技术(如交易机制)的基础上开发应用程序,而忽视了这种过程中的任何可审计性。由于我们所信任的技术在大多数情况下工作得很好,审计机制并不值得投资。 @@ -685,19 +687,19 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 #### 设计可审计性 -如果一个事务在一个数据库中改变了多个对象,事实上很难说事实是什么意思。即使您捕获事务日志(请参阅第454页上的“更改数据捕获”),各种表中的插入,更新和删除操作并不一定清楚表明为何执行这些突变。决定这些突变的应用逻辑的调用是暂时的,不能被复制。 +如果一个事务在一个数据库中改变了多个对象,事实上很难说事实是什么意思。即使您捕获事务日志(请参阅第454页上的“[更改数据捕获]()”),各种表中的插入,更新和删除操作并不一定清楚表明为何执行这些突变。决定这些突变的应用逻辑的调用是暂时的,不能被复制。 相比之下,基于事件的系统可以提供更好的可审计性。在事件源方法中,系统的用户输入被表示为一个单一的不可变事件,并且任何结果状态更新都来自该事件。衍生可以做出确定性和可重复性,以便通过相同版本的衍生代码运行相同的事件日志将导致相同的状态更新。 -清楚地看到数据流(请参阅第419页上的“批处理输出的原理”)可以使数据的来源更加清晰,从而使完整性检查更加可行。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理和流处理器,以检查是否获得相同的结果,或者甚至并行运行冗余衍生。 +清楚地看到数据流(请参阅第419页上的“[批处理输出的原理]()”)可以使数据的来源更加清晰,从而使完整性检查更加可行。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理和流处理器,以检查是否获得相同的结果,或者甚至并行运行冗余衍生。 -确定性和定义良好的数据流也使调试和跟踪系统的执行变得容易,以确定它为什么做了某些事情【4,69】。如果出现意想不到的事情,那么具有诊断能力来重现导致意外事件的确切环境 - 一种时间行程调试功能是非常有价值的。 +确定性和定义良好的数据流也使调试和跟踪系统的执行变得容易,以确定它为什么做了某些事情【4,69】。如果出现意想不到的事情,那么具有诊断能力来重现导致意外事件的确切环境 —— 一种时间行程调试功能是非常有价值的。 #### 再次是端到端的论点 -如果我们不能完全相信系统的每个组件都不会发生腐败 - 每一个硬件都是无错的,并且每一个软件都没有缺陷 - 那么我们至少必须定期检查数据的完整性。如果我们不检查,我们就不会发现腐败,直到它太晚了,并且已经造成了一些下游损害,在这一点上追踪这个问题将变得更加困难和昂贵。 +如果我们不能完全相信系统的每个组件都不会发生腐败 —— 每一个硬件都是无错的,并且每一个软件都没有缺陷 —— 那么我们至少必须定期检查数据的完整性。如果我们不检查,我们就不会发现腐败,直到它太晚了,并且已经造成了一些下游损害,在这一点上追踪这个问题将变得更加困难和昂贵。 -检查数据系统的完整性最好是以端到端的方式完成(请参阅“数据库的端到端争论”页码:51):我们可以在完整性检查中包含的系统越多,那里的机会就越少在这个过程的某个阶段,腐败是被忽视的。如果我们可以检查整个衍生数据管道是端到端正确的,那么沿着路径的任何磁盘,网络,服务和算法都隐含在检查中。 +检查数据系统的完整性最好是以端到端的方式完成(请参阅“[数据库的端到端争论]()”,51页):我们可以在完整性检查中包含的系统越多,那里的机会就越少在这个过程的某个阶段,腐败是被忽视的。如果我们可以检查整个衍生数据管道是端到端正确的,那么沿着路径的任何磁盘,网络,服务和算法都隐含在检查中。 持续的端到端完整性检查可以提高您对系统正确性的信心,从而使您的移动速度更快【70】。与自动化测试一样,审计增加了发现错误的可能性,从而降低了系统更改或新存储技术造成损害的风险。如果您不害怕进行更改,您可以更好地开发应用程序以满足不断变化的需求。 @@ -709,7 +711,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 我没有资格评论这些技术作为商定合同的货币或机制的优点。但是,从数据系统的角度来看,它们包含了一些有趣的想法。实质上,它们是分布式数据库,具有数据模型和事务机制,不同副本可以由互不信任的组织托管。复制品不断检查彼此的完整性,并使用共识协议来约定应执行的交易。 -我对这些技术的拜占庭容错方面有些怀疑(参见第304页的“拜占庭故障”),而且我发现工作证明(比如比特币挖掘)技术非常浪费。比特币的交易吞吐量相当低,尽管出于政治和经济原因而不是技术交易。但是,完整性检查方面很有趣。 +我对这些技术的拜占庭容错方面有些怀疑(参见第304页的“[拜占庭故障]()”),而且我发现工作证明(比如比特币挖掘)技术非常浪费。比特币的交易吞吐量相当低,尽管出于政治和经济原因而不是技术交易。但是,完整性检查方面很有趣。 我可以想象完整性检查和审计算法(如证书透明度和分布式分类账算法)在一般数据系统中得到越来越广泛的应用。 一些工作将需要使它们具有同样的可扩展性,因为没有加密审计的系统,并且尽可能降低性能损失。 但我认为这是一个值得关注的领域。 @@ -765,11 +767,11 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 当预测分析影响人们的生活时,特别是由于自我强化反馈循环而出现有害问题。例如,考虑雇主使用信用评分来评估潜在员工的情况。你可能是一个信誉好的好员工,但是由于你不能控制的不幸,你会突然发现自己陷入财务困境。由于您错过了账单付款,您的信用评分会受到影响,您将不太可能找到工作。失业使你陷入贫困,这进一步恶化了你的分数,使其更难找到工作【87】。由于有毒的假设,这是一个下降的螺旋,隐藏在数学严谨和数据伪装的背后。 -我们不能总是预测这种反馈循环何时发生。然而,通过考虑整个系统(不仅仅是计算机化的部分,而且还有与之互动的人),可以预测许多后果 - 一种称为系统思维的方法【92】。我们可以尝试理解数据分析系统如何响应不同的行为,结构或特性。该系统是否加强和扩大了人们之间现有的差异(例如,使富人更穷或更穷),还是试图打击不公正?即使有最好的意图,我们也必须小心意想不到的后果。 +我们不能总是预测这种反馈循环何时发生。然而,通过考虑整个系统(不仅仅是计算机化的部分,而且还有与之互动的人),可以预测许多后果—— 一种称为系统思维的方法【92】。我们可以尝试理解数据分析系统如何响应不同的行为,结构或特性。该系统是否加强和扩大了人们之间现有的差异(例如,使富人更穷或更穷),还是试图打击不公正?即使有最好的意图,我们也必须小心意想不到的后果。 #### 隐私和跟踪 -除了预测分析的问题 - 即使用数据来做出关于人的自动决策 - 数据收集本身也存在道德问题。收集数据的组织与正在收集数据的人之间有什么关系? +除了预测分析的问题 —— 即使用数据来做出关于人的自动决策 —— 数据收集本身也存在道德问题。收集数据的组织与正在收集数据的人之间有什么关系? 当系统仅存储用户明确输入的数据时,系统会以特定方式存储和处理数据,系统正在为用户执行服务:用户就是客户。但是,当用户的活动被跟踪并记录为他们正在做的其他事情的副作用时,这种关系就不那么清晰了。该服务不再仅仅是用户告诉它做的事情,而是服务于它自己的利益,这可能与用户的兴趣相冲突。 @@ -783,11 +785,11 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 作为一个思想实验,尝试用监视来替换单词数据,并观察常用短语是否听起来如此好【93】。这样如何:“在我们的监控驱动的组织中,我们收集实时监控流并将它们存储在我们的监控仓库中。我们的监控科学家使用高级分析和监测处理来获得新的见解。“ -这个思想实验对于本书“设计监控 - 密集型应用程序”来说是异乎寻常的争论,但我认为需要强调的话来强调这一点。在我们制作软件“吃世界”的尝试中【94】,我们已经建立了世界上迄今为止所见过的最伟大的大众监视基础设施。我们正朝着物联网迈进,我们正在迅速接近这样一个世界:每个有人居住的空间至少包含一个互联网连接的麦克风,以智能手机,智能电视,语音控制助理设备,婴儿监视器甚至儿童玩具使用基于云的语音识别。这些设备中的很多都有可怕的安全记录【95】。 +这个思想实验对于本书“设计监控 —— 密集型应用程序”来说是异乎寻常的争论,但我认为需要强调的话来强调这一点。在我们制作软件“吃世界”的尝试中【94】,我们已经建立了世界上迄今为止所见过的最伟大的大众监视基础设施。我们正朝着物联网迈进,我们正在迅速接近这样一个世界:每个有人居住的空间至少包含一个互联网连接的麦克风,以智能手机,智能电视,语音控制助理设备,婴儿监视器甚至儿童玩具使用基于云的语音识别。这些设备中的很多都有可怕的安全记录【95】。 即使是极权主义和专制政权也只能梦想在每个房间放置一个麦克风,并强迫每个人不断地携带能够追踪其位置和动作的设备。然而,我们显然自愿地,甚至热心地投身于这个全面监视的世界。不同之处在于数据是由公司而不是由政府机构收集的【96】。 -并不是所有的数据收集都必须符合监督的要求,但检查它们可以帮助我们理解我们与数据收集者的关系。为什么我们似乎很乐意接受企业的监督?也许你觉得你没有隐瞒 - 换句话说,你完全符合现有的权力结构,你不是被边缘化的少数派,你不必害怕迫害【97】。不是每个人都如此幸运。也许这是因为目的似乎是良性的 - 这不是强制性的,也不是强制性的,而只是更好的建议和更个性化的营销。但是,结合上一节中对预测分析的讨论,这种区分似乎不太清晰。 +并不是所有的数据收集都必须符合监督的要求,但检查它们可以帮助我们理解我们与数据收集者的关系。为什么我们似乎很乐意接受企业的监督?也许你觉得你没有隐瞒 —— 换句话说,你完全符合现有的权力结构,你不是被边缘化的少数派,你不必害怕迫害【97】。不是每个人都如此幸运。也许这是因为目的似乎是良性的 —— 这不是强制性的,也不是强制性的,而只是更好的建议和更个性化的营销。但是,结合上一节中对预测分析的讨论,这种区分似乎不太清晰。 我们已经看到与汽车追踪设备挂钩的汽车保险费以及取决于佩戴健身追踪设备的人的健康保险范围。当监视被用来确定在生活的重要方面如保险或就业等方面的东西时,它开始变得不那么温和。此外,数据分析可以揭示出令人惊讶的侵入性事物:例如,智能手表或健身追踪器中的运动传感器可用于以相当好的准确度计算出您正在输入的内容(例如密码)【98】。而分析算法只会变得更好。 @@ -795,10 +797,10 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 我们可能会断言用户自愿选择使用跟踪其活动的服务,并且他们已同意服务条款和隐私政策,因此他们同意收集数据。我们甚至可以声称,用户正在接受有价值的服务,以换取所提供的数据,并且为了提供服务,跟踪是必要的。毫无疑问,社交网络,搜索引擎以及其他各种免费的在线服务对于用户来说都是有价值的,但是这个说法存在问题。 -用户几乎不知道他们提供给我们的数据库的数据,或者它们如何保留和处理 - 而大多数隐私政策的作用更多的是模糊而不是照亮。如果不了解他们的数据会发生什么,用户无法给出任何有意义的同意。通常,来自一个用户的数据还说明关于不是该服务的用户并且没有同意任何条款的其他人的事情。我们在本书的这一部分讨论的衍生数据集(来自整个用户群的数据可能与行为跟踪和外部数据源相结合)恰恰是用户无法获得任何有意义理解的数据种类。 +用户几乎不知道他们提供给我们的数据库的数据,或者它们如何保留和处理 —— 而大多数隐私政策的作用更多的是模糊而不是照亮。如果不了解他们的数据会发生什么,用户无法给出任何有意义的同意。通常,来自一个用户的数据还说明关于不是该服务的用户并且没有同意任何条款的其他人的事情。我们在本书的这一部分讨论的衍生数据集(来自整个用户群的数据可能与行为跟踪和外部数据源相结合)恰恰是用户无法获得任何有意义理解的数据种类。 而且,数据是通过单向过程从用户中提取出来的,而不是真正的互惠关系,而不是公平的价值交换。没有对话,用户无法选择他们提供的数据量以及他们收到的服务回报:服务和用户之间的关系是非常不对称和片面的。这些条款是由服务设置,而不是由用户【99】。 -对于不同意监视的用户,唯一真正的选择就是不使用服务。但是这个选择也不是免费的:如果一项服务如此受欢迎以至于“被大多数人认为是基本社会参与的必要条件”【99】,那么指望人们选择退出这项服务是不合理的 - 使用它事实上是强制性的。例如,在大多数西方社会群体中,携带智能手机,使用Facebook进行社交以及使用Google查找信息已成为常态。特别是当一项服务具有网络效应时,人们选择不使用它会产生社会成本。 +对于不同意监视的用户,唯一真正的选择就是不使用服务。但是这个选择也不是免费的:如果一项服务如此受欢迎以至于“被大多数人认为是基本社会参与的必要条件”【99】,那么指望人们选择退出这项服务是不合理的 —— 使用它事实上是强制性的。例如,在大多数西方社会群体中,携带智能手机,使用Facebook进行社交以及使用Google查找信息已成为常态。特别是当一项服务具有网络效应时,人们选择不使用它会产生社会成本。 由于跟踪用户而拒绝使用服务,这只是少数拥有足够的时间和知识来了解其隐私政策的人员的一种选择,并且有可能错过社会参与或专业人士如果他们参与了服务,可能会出现机会。对于处境不太好的人来说,没有任何意义上的自由选择:监督变得不可避免。 @@ -814,21 +816,21 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 即使特定用户无法从特定广告定位的人群中个人身份识别,他们已经失去了一些关于披露一些隐私信息的机构,例如他们是否患有某种疾病。决定根据个人喜好向谁透露什么的不是用户,而是公司以最大化利润为目标行使隐私权。 -许多公司都有一个不被视为令人毛骨悚然的目标 - 避免了他们的数据收集的实际侵入性问题,而是专注于管理用户感知。即使这些看法经常被糟糕的管理:例如,事实可能是事实上的正确,但如果它触发痛苦的回忆,用户可能不希望被提醒它【100】。对于任何类型的数据,我们都应该期望在某种程度上出现错误,不可取或不适当的可能性,我们需要建立处理这些失败的机制。无论是“不可取的”还是“不适当的”,当然都是由人的判断决定的;除非我们明确地规划它们尊重人类的需求,否则算法会忽略这些概念。作为这些系统的工程师,我们必须谦虚,接受和规划这些失败。 +许多公司都有一个不被视为令人毛骨悚然的目标 —— 避免了他们的数据收集的实际侵入性问题,而是专注于管理用户感知。即使这些看法经常被糟糕的管理:例如,事实可能是事实上的正确,但如果它触发痛苦的回忆,用户可能不希望被提醒它【100】。对于任何类型的数据,我们都应该期望在某种程度上出现错误,不可取或不适当的可能性,我们需要建立处理这些失败的机制。无论是“不可取的”还是“不适当的”,当然都是由人的判断决定的;除非我们明确地规划它们尊重人类的需求,否则算法会忽略这些概念。作为这些系统的工程师,我们必须谦虚,接受和规划这些失败。 允许在线服务的用户控制其他用户可以看到的数据的哪些方面的隐私设置是将一些控制交还给用户的起点。但是,无论设置如何,服务本身仍然可以不受限制地访问数据,并且可以以隐私策略允许的任何方式自由使用它。即使服务承诺不会将数据出售给第三方,它通常会授予自己不受限制的权利,以在内部处理和分析数据,而且往往比用户明显看到的要多得多。 -这种从个人到公司的大规模隐私权转移历史上是史无前例的【99】。监控一直存在,但它过去是昂贵和手动的,不可扩展和自动化。信托关系一直存在,例如患者与其医生之间,或被告与其律师之间 - 但在这些情况下,数据的使用严格受到道德,法律和监管限制的约束。互联网服务使得在没有有意义的同意的情况下收集大量敏感信息变得容易得多,并且在没有用户理解他们的私人数据正在发生的情况下大规模使用它。 +这种从个人到公司的大规模隐私权转移历史上是史无前例的【99】。监控一直存在,但它过去是昂贵和手动的,不可扩展和自动化。信托关系一直存在,例如患者与其医生之间,或被告与其律师之间 —— 但在这些情况下,数据的使用严格受到道德,法律和监管限制的约束。互联网服务使得在没有有意义的同意的情况下收集大量敏感信息变得容易得多,并且在没有用户理解他们的私人数据正在发生的情况下大规模使用它。 #### 数据作为资产和权力 -由于行为数据是用户与服务交互的副产品,因此有时称为“数据耗尽” - 表明数据是毫无价值的浪费材料。从这个角度来看,行为和预测分析可以被看作是一种从数据中提取价值的回收形式,否则这些数据会被抛弃。 +由于行为数据是用户与服务交互的副产品,因此有时称为“数据耗尽” —— 表明数据是毫无价值的浪费材料。从这个角度来看,行为和预测分析可以被看作是一种从数据中提取价值的回收形式,否则这些数据会被抛弃。 更准确的说是反过来看:从经济的角度来看,如果有针对性的广告是为服务付费的话,那么关于人的行为数据就是服务的核心资产。在这种情况下,用户与之交互的应用程序仅仅是一种诱骗用户将更多的个人信息提供给监控基础设施的手段【99】。数据提取机器人讥讽地发现,在线服务中常常表现出令人愉快的人类创造力和社交关系。 -个人数据是宝贵资产的说法得到了数据中介的支持,这个数据中介是一个隐蔽的行业,它主要是为了市场目的而采购,收集,分析,推断和转售侵入性个人数据[ 90。初创公司通过他们的用户数量,通过“眼球”,即通过他们的监视能力来估价。 +个人数据是宝贵资产的说法得到了数据中介的支持,这个数据中介是一个隐蔽的行业,它主要是为了市场目的而采购,收集,分析,推断和转售侵入性个人数据【90】。初创公司通过他们的用户数量,通过“眼球”,即通过他们的监视能力来估价。 -因为数据很有价值,所以很多人都想要它。当然,公司需要它 - 这就是为什么他们收集它的原因。但政府也想获得它:通过秘密交易,胁迫,法律强制或者只是偷窃【101】。当公司破产时,收集到的个人资料就是被出售的资产之一。而且,数据难以确保,因此违规事件经常令人不安地发生【102】。 +因为数据很有价值,所以很多人都想要它。当然,公司需要它 —— 这就是为什么他们收集它的原因。但政府也想获得它:通过秘密交易,胁迫,法律强制或者只是偷窃【101】。当公司破产时,收集到的个人资料就是被出售的资产之一。而且,数据难以确保,因此违规事件经常令人不安地发生【102】。 这些观察已经导致批评者说数据不仅仅是一种资产,而是一种“有毒资产”【101】,或者至少是“有害物质”【103】。即使我们认为我们有能力防止滥用数据,但是每当我们收集数据时,我们都需要平衡这些好处和落入他们手中的风险:计算机系统可能会被犯罪分子或敌对的外国情报服务,数据可能会被内部人士泄露,公司可能会落入不合法的管理层之中,而这些管理层不会分享我们的价值观,或者这个国家可能会被毫无疑问迫使我们交出数据的政权所接管。 @@ -844,7 +846,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 就像工业革命有一个黑暗的一面需要管理一样,我们对信息时代的转变也有我们需要面对和解决的重大问题。我相信收集和使用数据是其中的一个问题。用布鲁斯·施奈尔(Bruce Schneier)【96】的话来说: -> 数据是信息时代的污染问题,保护隐私是环境挑战。几乎所有的电脑都能生成信息。它停留在周围,溃烂。我们如何处理它 - 我们如何控制它以及如何处理它 - 对我们信息经济的健康至关重要。正如我们今天回顾工业时代的早期十年,并想知道我们的祖先在匆忙建立一个工业世界的过程中如何忽略污染,我们的孙辈在信息时代的前几十年将回顾我们,就我们如何应对数据收集和滥用的挑战来判断我们。 +> 数据是信息时代的污染问题,保护隐私是环境挑战。几乎所有的电脑都能生成信息。它停留在周围,溃烂。我们如何处理它 —— 我们如何控制它以及如何处理它 —— 对我们信息经济的健康至关重要。正如我们今天回顾工业时代的早期十年,并想知道我们的祖先在匆忙建立一个工业世界的过程中如何忽略污染,我们的孙辈在信息时代的前几十年将回顾我们,就我们如何应对数据收集和滥用的挑战来判断我们。 > > 我们应该设法让他们感到骄傲。 @@ -858,7 +860,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 从根本上说,我认为我们需要在个人数据方面进行科技行业的文化转变。我们应该停止将用户作为度量标准进行优化,并记住他们是值得尊重,尊严和代理的人。我们应该自我调节我们的数据收集和处理实践,以建立和维持依赖我们软件的人们的信任【111】。我们应该自己去教育最终用户如何使用他们的数据,而不是让他们处于黑暗中。 -我们应该允许每个人保持他们的隐私 - 即他们控制自己的数据 - 而不是通过监视来窃取他们的控制权。我们控制数据的个人权利就像是一个国家公园的自然环境:如果我们没有明确的保护和关心,它将被破坏。这将是公地的悲剧,我们都会因此而变得更糟。无所不在的监视不是不可避免的,我们仍然能够阻止它。 +我们应该允许每个人保持他们的隐私 —— 即他们控制自己的数据 —— 而不是通过监视来窃取他们的控制权。我们控制数据的个人权利就像是一个国家公园的自然环境:如果我们没有明确的保护和关心,它将被破坏。这将是公地的悲剧,我们都会因此而变得更糟。无所不在的监视不是不可避免的,我们仍然能够阻止它。 我们究竟能做到这一点是一个悬而未决的问题。首先,我们不应该永久保留数据,但一旦不再需要就立即清除数据【111,112】。清除数据与不变性的想法背道而驰(请参阅第463页的“不变性的限制”),但可以解决该问题。我所看到的一种很有前途的方法是通过加密协议来实施访问控制,而不仅仅是通过策略【113,114】。总的来说,文化和态度的变化是必要的。 @@ -890,8 +892,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 “[Postgres Full-Text Search is Good Enough!](http://rachbelaid.com/postgres-full-text-search-is-good-enough/),” *rachbelaid.com*, July 13, 2015. 1. Philippe Ajoux, Nathan Bronson, Sanjeev Kumar, et al.: - “[Challenges to Adopting Stronger Consistency at Scale](https://www.usenix.org/system/files/conference/hotos15/hotos15-paper-ajoux.pdf),” at *15th USENIX Workshop on Hot Topics - in Operating Systems* (HotOS), May 2015. + “[Challenges to Adopting Stronger Consistency at Scale](https://www.usenix.org/system/files/conference/hotos15/hotos15-paper-ajoux.pdf),” at *15th USENIX Workshop on Hot Topics in Operating Systems* (HotOS), May 2015. 1. Pat Helland and Dave Campbell: “[Building on Quicksand](https://database.cs.wisc.edu/cidr/cidr2009/Paper_133.pdf),” at @@ -911,8 +912,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 *engineering.linkedin.com*, December 16, 2013. 1. Pat Helland: - “[Life Beyond Distributed Transactions: An Apostate’s Opinion](http://www-db.cs.wisc.edu/cidr/cidr2007/papers/cidr07p15.pdf),” at *3rd Biennial Conference on Innovative Data - Systems Research* (CIDR), January 2007. + “[Life Beyond Distributed Transactions: An Apostate’s Opinion](http://www-db.cs.wisc.edu/cidr/cidr2007/papers/cidr07p15.pdf),” at *3rd Biennial Conference on Innovative Data Systems Research* (CIDR), January 2007. 1. “[Great Western Railway (1835–1948)](https://www.networkrail.co.uk/VirtualArchive/great-western/),” Network Rail Virtual Archive, *networkrail.co.uk*. @@ -924,19 +924,16 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 “[Agile Architecture](http://conferences.oreilly.com/software-architecture/sa2015/public/schedule/detail/40388),” at *O'Reilly Software Architecture Conference*, March 2015. 1. Nathan Marz and James Warren: - *Big Data: Principles and Best Practices of - Scalable Real-Time Data Systems*. Manning, 2015. ISBN: 978-1-617-29034-3 + *Big Data: Principles and Best Practices of Scalable Real-Time Data Systems*. Manning, 2015. ISBN: 978-1-617-29034-3 1. Oscar Boykin, Sam Ritchie, Ian O'Connell, and - Jimmy Lin: “[Summingbird: A Framework for Integrating Batch and Online MapReduce Computations](http://www.vldb.org/pvldb/vol7/p1441-boykin.pdf),” at *40th International Conference on - Very Large Data Bases* (VLDB), September 2014. + Jimmy Lin: “[Summingbird: A Framework for Integrating Batch and Online MapReduce Computations](http://www.vldb.org/pvldb/vol7/p1441-boykin.pdf),” at *40th International Conference on Very Large Data Bases* (VLDB), September 2014. 1. Jay Kreps: “[Questioning the Lambda Architecture](https://www.oreilly.com/ideas/questioning-the-lambda-architecture),” *oreilly.com*, July 2, 2014. 1. Raul Castro Fernandez, Peter Pietzuch, - Jay Kreps, et al.: “[Liquid: Unifying Nearline and Offline Big Data Integration](http://www.cidrdb.org/cidr2015/Papers/CIDR15_Paper25u.pdf),” at *7th Biennial Conference on - Innovative Data Systems Research* (CIDR), January 2015. + Jay Kreps, et al.: “[Liquid: Unifying Nearline and Offline Big Data Integration](http://www.cidrdb.org/cidr2015/Papers/CIDR15_Paper25u.pdf),” at *7th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2015. 1. Dennis M. Ritchie and Ken Thompson: “[The UNIX Time-Sharing System](http://www.cs.virginia.edu/~zaher/classes/CS656/p365-ritchie.pdf),” *Communications of the ACM*, volume 17, number 7, pages 365–375, July 1974. @@ -959,8 +956,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 “[Foreign Data Wrappers for PostgreSQL](http://www.vertabelo.com/blog/technical-articles/foreign-data-wrappers-for-postgresql),” *vertabelo.com*, March 24, 2015. 1. David B. Lomet, Alan Fekete, Gerhard Weikum, and Mike Zwilling: - “[Unbundling Transaction Services in the Cloud](https://www.microsoft.com/en-us/research/publication/unbundling-transaction-services-in-the-cloud/),” at *4th Biennial Conference on Innovative Data Systems - Research* (CIDR), January 2009. + “[Unbundling Transaction Services in the Cloud](https://www.microsoft.com/en-us/research/publication/unbundling-transaction-services-in-the-cloud/),” at *4th Biennial Conference on Innovative Data Systems Research* (CIDR), January 2009. 1. Martin Kleppmann and Jay Kreps: “[Kafka, Samza and the Unix Philosophy of Distributed Data](http://martin.kleppmann.com/papers/kafka-debull15.pdf),” *IEEE Data Engineering Bulletin*, volume 38, number 4, pages 4–14, @@ -985,8 +981,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 “[Turning the Database Inside-out with Apache Samza,](http://martin.kleppmann.com/2015/03/04/turning-the-database-inside-out.html)” at *Strange Loop*, September 2014. 1. Peter Van Roy and Seif Haridi: - *Concepts, - Techniques, and Models of Computer Programming*. MIT Press, 2004. + *Concepts, Techniques, and Models of Computer Programming*. MIT Press, 2004. ISBN: 978-0-262-22069-9 1. “[Juttle Documentation](http://juttle.github.io/juttle/),” *juttle.github.io*, 2016. @@ -1049,8 +1044,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 1. Sebastian Burckhardt, Daan Leijen, Jonathan Protzenko, and Manuel Fähndrich: - “[Global Sequence Protocol: A Robust Abstraction for Replicated Shared State](http://drops.dagstuhl.de/opus/volltexte/2015/5238/),” at *29th European Conference on Object-Oriented - Programming* (ECOOP), July 2015. + “[Global Sequence Protocol: A Robust Abstraction for Replicated Shared State](http://drops.dagstuhl.de/opus/volltexte/2015/5238/),” at *29th European Conference on Object-Oriented Programming* (ECOOP), July 2015. [doi:10.4230/LIPIcs.ECOOP.2015.568](http://dx.doi.org/10.4230/LIPIcs.ECOOP.2015.568) 1. Mark Soper: @@ -1063,8 +1057,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 “[Dataflow as Database](https://github.com/frankmcsherry/blog/blob/master/posts/2016-07-17.md),” *github.com*, July 17, 2016. 1. Peter Alvaro: - “[I See What You Mean](https://www.youtube.com/watch?v=R2Aa4PivG0g),” at *Strange - Loop*, September 2015. + “[I See What You Mean](https://www.youtube.com/watch?v=R2Aa4PivG0g),” at *Strange Loop*, September 2015. 1. Nathan Marz: “[Trident: A High-Level Abstraction for Realtime Computation](https://blog.twitter.com/2012/trident-a-high-level-abstraction-for-realtime-computation),” *blog.twitter.com*, August 2, 2012. @@ -1078,13 +1071,11 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 October 2, 2016. 1. Arthur J. Bernstein, Philip M. Lewis, and Shiyong Lu: - “[Semantic Conditions for Correctness at Different Isolation Levels](http://db.cs.berkeley.edu/cs286/papers/isolation-icde2000.pdf),” at *16th International Conference on Data - Engineering* (ICDE), February 2000. + “[Semantic Conditions for Correctness at Different Isolation Levels](http://db.cs.berkeley.edu/cs286/papers/isolation-icde2000.pdf),” at *16th International Conference on Data Engineering* (ICDE), February 2000. [doi:10.1109/ICDE.2000.839387](http://dx.doi.org/10.1109/ICDE.2000.839387) 1. Sudhir Jorwekar, Alan Fekete, Krithi Ramamritham, and - S. Sudarshan: “[Automating the Detection of Snapshot Isolation Anomalies](http://www.vldb.org/conf/2007/papers/industrial/p1263-jorwekar.pdf),” at *33rd International Conference on Very - Large Data Bases* (VLDB), September 2007. + S. Sudarshan: “[Automating the Detection of Snapshot Isolation Anomalies](http://www.vldb.org/conf/2007/papers/industrial/p1263-jorwekar.pdf),” at *33rd International Conference on Very Large Data Bases* (VLDB), September 2007. 1. Kyle Kingsbury: [Jepsen blog post series](https://aphyr.com/tags/jepsen), *aphyr.com*, 2013–2016. @@ -1123,8 +1114,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 “[Memories, Guesses, and Apologies](http://blogs.msdn.com/b/pathelland/archive/2007/05/15/memories-guesses-and-apologies.aspx),” *blogs.msdn.com*, May 15, 2007. 1. Yoongu Kim, Ross Daly, Jeremie Kim, et al.: - “[Flipping Bits in Memory Without Accessing Them: An Experimental Study of DRAM Disturbance Errors](https://users.ece.cmu.edu/~yoonguk/papers/kim-isca14.pdf),” at *41st Annual - International Symposium on Computer Architecture* (ISCA), June 2014. + “[Flipping Bits in Memory Without Accessing Them: An Experimental Study of DRAM Disturbance Errors](https://users.ece.cmu.edu/~yoonguk/papers/kim-isca14.pdf),” at *41st Annual International Symposium on Computer Architecture* (ISCA), June 2014. [doi:10.1145/2678373.2665726](http://dx.doi.org/10.1145/2678373.2665726) 1. Mark Seaborn and Thomas Dullien: @@ -1169,8 +1159,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 [doi:10.1007/3-540-48184-2_32](http://dx.doi.org/10.1007/3-540-48184-2_32) 1. Ben Laurie: - “[Certificate Transparency](http://queue.acm.org/detail.cfm?id=2668154),” *ACM - Queue*, volume 12, number 8, pages 10-19, August 2014. + “[Certificate Transparency](http://queue.acm.org/detail.cfm?id=2668154),” *ACM Queue*, volume 12, number 8, pages 10-19, August 2014. [doi:10.1145/2668152.2668154](http://dx.doi.org/10.1145/2668152.2668154) 1. Mark D. Ryan: @@ -1178,8 +1167,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 Security Symposium* (NDSS), February 2014. [doi:10.14722/ndss.2014.23379](http://dx.doi.org/10.14722/ndss.2014.23379) -1. “Software Engineering Code of Ethics and Professional +1. “Software Engineering Code of Ethics and Professional Practice,” Association for Computing Machinery, *acm.org*, 1999. 1. François Chollet: @@ -1213,8 +1201,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 *idlewords.com*, June 2016. 1. Cathy O'Neil: - *Weapons of Math Destruction: How Big Data - Increases Inequality and Threatens Democracy*. Crown Publishing, 2016. + *Weapons of Math Destruction: How Big Data Increases Inequality and Threatens Democracy*. Crown Publishing, 2016. ISBN: 978-0-553-41881-1 1. Julia Angwin: @@ -1224,8 +1211,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 “[European Union Regulations on Algorithmic Decision-Making and a ‘Right to Explanation’](https://arxiv.org/abs/1606.08813),” *arXiv:1606.08813*, August 31, 2016. -1. “[A Review of the Data Broker Industry: Collection, Use, and Sale of Consumer Data for Marketing Purposes](https://www.commerce.senate.gov/public/index.cfm/reports?ID=57C428EC-8F20-44EE-BFB8-A570E9BE0CCC),” Staff Report, *United States Senate Committee on Commerce, Science, and - Transportation*, *commerce.senate.gov*, December 2013. +1. “[A Review of the Data Broker Industry: Collection, Use, and Sale of Consumer Data for Marketing Purposes](https://www.commerce.senate.gov/public/index.cfm/reports?ID=57C428EC-8F20-44EE-BFB8-A570E9BE0CCC),” Staff Report, *United States Senate Committee on Commerce, Science, and Transportation*, *commerce.senate.gov*, December 2013. 1. Olivia Solon: “[Facebook’s Failure: Did Fake News and Polarized Politics Get Trump Elected?](https://www.theguardian.com/technology/2016/nov/10/facebook-fake-news-election-conspiracy-theories)” *theguardian.com*, November 10, @@ -1244,8 +1230,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 “[‘Internet of Things’ Security Is Hilariously Broken and Getting Worse](http://arstechnica.com/security/2016/01/how-to-search-the-internet-of-things-for-photos-of-sleeping-babies/),” *arstechnica.com*, January 23, 2016. 1. Bruce Schneier: - *Data and Goliath: The Hidden Battles - to Collect Your Data and Control Your World*. W. W. Norton, 2015. + *Data and Goliath: The Hidden Battles to Collect Your Data and Control Your World*. W. W. Norton, 2015. ISBN: 978-0-393-35217-7 1. The Grugq: @@ -1294,8 +1279,7 @@ ACID意义上的一致性(请参阅第224页的“一致性”)基于数据 Thesis, KU Leuven Centre for IT and IP Law, August 2016. 1. Michiel Rhoen: - “[Beyond Consent: Improving Data Protection Through Consumer Protection Law](http://policyreview.info/articles/analysis/beyond-consent-improving-data-protection-through-consumer-protection-law),” *Internet Policy - Review*, volume 5, number 1, March 2016. + “[Beyond Consent: Improving Data Protection Through Consumer Protection Law](http://policyreview.info/articles/analysis/beyond-consent-improving-data-protection-through-consumer-protection-law),” *Internet Policy Review*, volume 5, number 1, March 2016. [doi:10.14763/2016.1.404](http://dx.doi.org/10.14763/2016.1.404) 1. Jessica Leber: From 9dc5a3fe8fac3c574410ccd9996752e02c0e5232 Mon Sep 17 00:00:00 2001 From: Vonng Date: Thu, 29 Mar 2018 01:15:26 +0800 Subject: [PATCH 07/17] ch9 75% --- ch5.md | 30 +++++------ ch8.md | 2 +- ch9.md | 156 +++++++++++++++++++++++++++++---------------------------- 3 files changed, 95 insertions(+), 93 deletions(-) diff --git a/ch5.md b/ch5.md index f4ee4ca..187b64d 100644 --- a/ch5.md +++ b/ch5.md @@ -606,19 +606,19 @@ Riak将客户端和数据库节点之间的所有通信保持在一个数据中 ### 检测并发写入 -Dynamo风格的数据库允许多个客户端同时写入相同的Key,这意味着即使使用严格的法定人数也会发生冲突。这种情况与多领导者复制相似(请参阅第171页的“处理写冲突”),但在Dynamo样式的数据库中,在**读修复**或**带提示的接力**期间也可能会产生冲突。 +Dynamo风格的数据库允许多个客户端同时写入相同的Key,这意味着即使使用严格的法定人数也会发生冲突。这种情况与多领导者复制相似(参阅“[处理写入冲突](#处理写入冲突)”),但在Dynamo样式的数据库中,在**读修复**或**带提示的接力**期间也可能会产生冲突。 -问题在于,由于可变的网络延迟和部分故障,事件可能在不同的节点以不同的顺序到达。例如,图5-12显示了两个客户机A和B同时写入三节点数据存储区中的键X: +问题在于,由于可变的网络延迟和部分故障,事件可能在不同的节点以不同的顺序到达。例如,[图5-12](img/fig5-12.png)显示了两个客户机A和B同时写入三节点数据存储区中的键X: -* 节点1接收来自A的写入,但由于暂时中断,从不接收来自B的写入。 -* 节点2首先接收来自A的写入,然后接收来自B的写入。 -* 节点3首先接收来自B的写入,然后从A写入。 +* 节点 1 接收来自 A 的写入,但由于暂时中断,从不接收来自 B 的写入。 +* 节点 2 首先接收来自 A 的写入,然后接收来自 B 的写入。 +* 节点 3 首先接收来自 B 的写入,然后从 A 写入。 ![](img/fig5-12.png) **图5-12 并发写入Dynamo风格的数据存储:没有明确定义的顺序。** -如果每个节点只要接收到来自客户端的写入请求就简单地覆盖了某个键的值,那么节点就会永久地不一致,如[图5-12](img/fig5-12.png)中的最终获取请求所示:节点2认为X的最终值是B,而其他节点认为值是A. +如果每个节点只要接收到来自客户端的写入请求就简单地覆盖了某个键的值,那么节点就会永久地不一致,如[图5-12](img/fig5-12.png)中的最终获取请求所示:节点2认为 X 的最终值是 B,而其他节点认为值是 A 。 为了最终达成一致,副本应该趋于相同的值。如何做到这一点?有人可能希望复制的数据库能够自动处理,但不幸的是,大多数的实现都很糟糕:如果你想避免丢失数据,你(应用程序开发人员)需要知道很多有关数据库冲突处理的内部信息。 @@ -632,7 +632,7 @@ Dynamo风格的数据库允许多个客户端同时写入相同的Key,这意 即使写入没有自然的排序,我们也可以强制任意排序。例如,可以为每个写入附加一个时间戳,挑选最**“最近”**的最大时间戳,并丢弃具有较早时间戳的任何写入。这种冲突解决算法被称为**最后写入为准(LWW, last write wins)**,是Cassandra 【53】唯一支持的冲突解决方法,也是Riak 【35】中的一个可选特征。 -LWW实现了最终收敛的目标,但以**持久性**为代价:如果同一个Key有多个并发写入,即使它们都被报告为客户端成功(因为它们被写入w个副本),其中一个写道会生存下来,其他的将被无声丢弃。此外,LWW甚至可能会删除不是并发的写入,我们将在的“[有序事件的时间戳](ch8.md#有序事件的时间戳)”中讨论。 +LWW实现了最终收敛的目标,但以**持久性**为代价:如果同一个Key有多个并发写入,即使它们都被报告为客户端成功(因为它们被写入 w 个副本),其中一个写道会生存下来,其他的将被无声丢弃。此外,LWW甚至可能会删除不是并发的写入,我们将在的“[有序事件的时间戳](ch8.md#有序事件的时间戳)”中讨论。 有一些情况,如缓存,其中丢失的写入可能是可以接受的。如果丢失数据不可接受,LWW是解决冲突的一个很烂的选择。 @@ -661,17 +661,17 @@ LWW实现了最终收敛的目标,但以**持久性**为代价:如果同一 -#### 捕捉"此前发生"关系 +#### 捕获"此前发生"关系 来看一个算法,它确定两个操作是否为并发的,还是一个在另一个之前。为了简单起见,我们从一个只有一个副本的数据库开始。一旦我们已经制定了如何在单个副本上完成这项工作,我们可以将该方法概括为具有多个副本的无领导者数据库。 [图5-13]()显示了两个客户端同时向同一购物车添加项目。 (如果这样的例子让你觉得太麻烦了,那么可以想象,两个空中交通管制员同时把飞机添加到他们正在跟踪的区域)最初,购物车是空的。在它们之间,客户端向数据库发出五次写入: -1. 客户端1将牛奶加入购物车。这是该键的第一次写入,服务器成功存储了它并为其分配版本号1,最后将值与版本号一起回送给客户端。 -2. 客户端2将鸡蛋加入购物车,不知道客户端1同时添加了牛奶(客户端2认为它的鸡蛋是购物车中的唯一物品)。服务器为此写入分配版本号2,并将鸡蛋和牛奶存储为两个单独的值。然后它将这两个值**都**反回给客户端2,并附上版本号2。 -3. 客户端1不知道客户端2的写入,想要将面粉加入购物车,因此认为当前的购物车内容应该是 [牛奶,面粉]。它将此值与服务器先前向客户端1提供的版本号1一起发送到服务器。服务器可以从版本号中知道[牛奶,面粉]的写入取代了[牛奶]的先前值,但与[鸡蛋]的值是**并发**的。因此,服务器将版本3分配给[牛奶,面粉],覆盖版本1值[牛奶],但保留版本2的值[蛋],并将所有的值返回给客户端1。 -4. 同时,客户端2想要加入火腿,不知道客端户1刚刚加了面粉。客户端2在最后一个响应中从服务器收到了两个值[牛奶]和[蛋],所以客户端2现在合并这些值,并添加火腿形成一个新的值,[鸡蛋,牛奶,火腿]。它将这个值发送到服务器,带着之前的版本号2。服务器检测到新值会覆盖版本2 [eggs],但新值也会与v3 [牛奶,面粉]**并发**,所以剩下的两个值是v3 [milk,flour],和v4:[鸡蛋,牛奶,火腿]。 -5. 最后,客户端1想要加培根。它以前在v3中从服务器接收[牛奶,面粉]和[鸡蛋],所以它合并这些,添加培根,并将最终值[牛奶,面粉,鸡蛋,培根]连同版本号v3发往服务器。这会覆盖v3[牛奶,面粉](请注意[鸡蛋]已经在最后一步被覆盖),但与v4[鸡蛋,牛奶,火腿]并发,所以服务器保留这两个并发值。 +1. 客户端 1 将牛奶加入购物车。这是该键的第一次写入,服务器成功存储了它并为其分配版本号1,最后将值与版本号一起回送给客户端。 +2. 客户端 2 将鸡蛋加入购物车,不知道客户端 1 同时添加了牛奶(客户端 2 认为它的鸡蛋是购物车中的唯一物品)。服务器为此写入分配版本号 2,并将鸡蛋和牛奶存储为两个单独的值。然后它将这两个值**都**反回给客户端 2 ,并附上版本号 2 。 +3. 客户端 1 不知道客户端 2 的写入,想要将面粉加入购物车,因此认为当前的购物车内容应该是 [牛奶,面粉]。它将此值与服务器先前向客户端 1 提供的版本号 1 一起发送到服务器。服务器可以从版本号中知道[牛奶,面粉]的写入取代了[牛奶]的先前值,但与[鸡蛋]的值是**并发**的。因此,服务器将版本 3 分配给[牛奶,面粉],覆盖版本1值[牛奶],但保留版本 2 的值[蛋],并将所有的值返回给客户端 1 。 +4. 同时,客户端 2 想要加入火腿,不知道客端户 1 刚刚加了面粉。客户端 2 在最后一个响应中从服务器收到了两个值[牛奶]和[蛋],所以客户端 2 现在合并这些值,并添加火腿形成一个新的值,[鸡蛋,牛奶,火腿]。它将这个值发送到服务器,带着之前的版本号 2 。服务器检测到新值会覆盖版本 2 [eggs],但新值也会与版本 3 [牛奶,面粉]**并发**,所以剩下的两个值是v3 [milk,flour],和v4:[鸡蛋,牛奶,火腿]。 +5. 最后,客户端 1 想要加培根。它以前在v3中从服务器接收[牛奶,面粉]和[鸡蛋],所以它合并这些,添加培根,并将最终值[牛奶,面粉,鸡蛋,培根]连同版本号v3发往服务器。这会覆盖v3[牛奶,面粉](请注意[鸡蛋]已经在最后一步被覆盖),但与v4[鸡蛋,牛奶,火腿]并发,所以服务器保留这两个并发值。 ![](img/fig5-13.png) @@ -690,13 +690,13 @@ LWW实现了最终收敛的目标,但以**持久性**为代价:如果同一 * 客户端写入键时,必须包含之前读取的版本号,并且必须将之前读取的所有值合并在一起。 (来自写入请求的响应可以像读取一样,返回所有当前值,这使得我们可以像购物车示例那样连接多个写入。) * 当服务器接收到具有特定版本号的写入时,它可以覆盖该版本号或更低版本的所有值(因为它知道它们已经被合并到新的值中),但是它必须保持所有值更高版本号(因为这些值与传入的写入同时发生)。 -当一个写入包含前一次读取的版本号时,它会告诉我们写入的是哪一种状态。如果在不包含版本号的情况下进行写操作,则与所有其他写操作并发,因此它不会覆盖任何内容 - 只会在随后的读取中作为其中一个值返回。 +当一个写入包含前一次读取的版本号时,它会告诉我们写入的是哪一种状态。如果在不包含版本号的情况下进行写操作,则与所有其他写操作并发,因此它不会覆盖任何内容 —— 只会在随后的读取中作为其中一个值返回。 #### 合并同时写入的值 这种算法可以确保没有数据被无声地丢弃,但不幸的是,客户端需要做一些额外的工作:如果多个操作并发发生,则客户端必须通过合并并发写入的值来擦屁股。 Riak称这些并发值**兄弟(siblings)**。 -合并兄弟值,本质上是与多领导者复制中的冲突解决相同的问题,我们先前讨论过(请参阅第171页的“[处理写冲突](#处理写冲突)”)。一个简单的方法是根据版本号或时间戳(最后写入胜利)选择一个值,但这意味着丢失数据。所以,你可能需要在应用程序代码中做更聪明的事情。 +合并兄弟值,本质上是与多领导者复制中的冲突解决相同的问题,我们先前讨论过(参阅“[处理写入冲突](#处理写入冲突)”)。一个简单的方法是根据版本号或时间戳(最后写入胜利)选择一个值,但这意味着丢失数据。所以,你可能需要在应用程序代码中做更聪明的事情。 以购物车为例,一种合理的合并兄弟方法就是集合求并。在[图5-14](img/fig5-14.png)中,最后的两个兄弟是[牛奶,面粉,鸡蛋,熏肉]和[鸡蛋,牛奶,火腿]。注意牛奶和鸡蛋出现在两个,即使他们每个只写一次。合并的价值可能是像[牛奶,面粉,鸡蛋,培根,火腿],没有重复。 diff --git a/ch8.md b/ch8.md index 2b2a4a8..f291598 100644 --- a/ch8.md +++ b/ch8.md @@ -464,7 +464,7 @@ while(true){ 最常见的法定人数是超过一半的绝对多数(尽管其他类型的法定人数也是可能的)。多数法定人数允许系统继续工作,如果单个节点发生故障(三个节点可以容忍单节点故障;五个节点可以容忍双节点故障)。系统仍然是安全的,因为在这个制度中只能有一个多数——不能同时存在两个相互冲突的多数决定。当我们在[第9章](ch9.md)中讨论**共识算法(consensus algorithms)**时,我们将更详细地讨论法定人数的应用。 -#### 领导和锁 +#### 领导者与锁定 通常情况下,一些东西在一个系统中只能有一个。例如: diff --git a/ch9.md b/ch9.md index 8eac16c..8012371 100644 --- a/ch9.md +++ b/ch9.md @@ -311,9 +311,9 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 ## 顺序保证 -之前说过,线性一致寄存器的行为就好像只有单个数据副本一样,且每个操作在某个时间点似乎都是原子性生效的。这个定义意味着,操作是按照某种良好定义的顺序执行的。我们通过操作(似乎)执行完毕的顺序来连接操作,以此说明[图9-4](img/fig9-4.png)中的顺序。 +之前说过,线性一致寄存器的行为就好像只有单个数据副本一样,且每个操作似乎都是在某个时间点以原子性的方式生效的。这个定义意味着操作是按照某种良好定义的顺序执行的。我们通过操作(似乎)执行完毕的顺序来连接操作,以此说明[图9-4](img/fig9-4.png)中的顺序。 -**顺序(ordering)**这一主题在本书中反复出现,这表明它可能是一个重要的基础性概念。让我们简要回顾一下其他**顺序**曾经出现过的上下文: +**顺序(ordering)**这一主题在本书中反复出现,这表明它可能是一个重要的基础性概念。让我们简要回顾一下其它**顺序**曾经出现过的上下文: * 在[第5章](ch5.md)中我们看到,领导者在单主复制中的主要目的就是,在复制日志中确定**写入顺序(order of write)**——也就是从库应用这些写入的顺序。如果不存在一个领导者,则并发操作可能导致冲突(参阅“[处理写入冲突](ch5.md#处理写入冲突)”)。 * 在[第7章](ch7.md)中讨论的**可序列化**,是关于事务表现的像按**某种序列顺序(some sequential order)**执行的保证。它可以通过字面意义上地**序列顺序(serial order)**执行事务来实现,或者通过允许并行执行,同时防止序列化冲突来实现(通过锁或中止事务)。 @@ -325,14 +325,14 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 **顺序**反复出现有几个原因,其中一个原因是,它有助于保持**因果关系(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不知道彼此。 * 在事务快照隔离的上下文中(“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”),我们说事务是从一致性快照中读取的。但此语境中“一致”到底又是什么意思?这意味着**与因果关系保持一致(consistent with causality)**:如果快照包含答案,它也必须包含被回答的问题【48】。在某个时间点观察整个数据库,与因果关系保持一致意味着:因果上在该时间点之前发生的所有操作,其影响都是可见的,但因果上在该时间点之后发生的操作,其影响对观察者不可见。**读偏差(read skew)**意味着读取的数据处于违反因果关系的状态(不可重复读,如[图7-6](img/fig7-6)所示)。 * 事务之间**写偏差(write skew)**的例子(参见“[写偏差和幻象](ch7.md#写偏差和幻象)”)也说明了因果依赖:在[图7-8](img/fig7-8.png)中,爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。[可序列化的快照隔离](ch7.md#可序列化的快照隔离(SSI))通过跟踪事务之间的因果依赖来检测写偏差。 * 在爱丽丝和鲍勃看球的例子中([图9-1](img/fig9-1.png)),在听到爱丽丝惊呼比赛结果后,鲍勃从服务器得到陈旧结果的事实违背了因果关系:爱丽丝的惊呼因果依赖于得分宣告,所以鲍勃应该也能在听到爱丽斯惊呼后查询到比分。相同的模式在“[跨信道的时序依赖](#跨信道的时序依赖)”一节中,以“图像大小调整服务”的伪装再次出现。 -因果关系对事件施加了一种顺序:因在果之前;消息发送在消息收取之前。而且就像现实生活中一样,一件事会导致另一件事:某个节点读取了一些数据然后写入一些结果,另一个节点读取其写入的内容,并依次写入一些其他内容,等等。这些因果依赖的操作链定义了系统中的因果顺序,即,什么在什么之前发生。 +因果关系对事件施加了一种**顺序**:因在果之前;消息发送在消息收取之前。而且就像现实生活中一样,一件事会导致另一件事:某个节点读取了一些数据然后写入一些结果,另一个节点读取其写入的内容,并依次写入一些其他内容,等等。这些因果依赖的操作链定义了系统中的因果顺序,即,什么在什么之前发生。 如果一个系统服从因果关系所规定的顺序,我们说它是**因果一致(causally)**的。例如,快照隔离提供了因果一致性:当你从数据库中读取到一些数据时,你一定还能够看到其因果前驱(假设在此期间这些数据还没有被删除)。 @@ -366,173 +366,175 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性**隐含着(implies)**因果关系:任何线性一致的系统都能正确保持因果性【7】。特别是,如果系统中有多个通信通道(如[图9-5](img/fig9-5.png)中的消息队列和文件存储服务),线性一致性可以自动保证因果性,系统无需任何特殊操作(如在不同组件间传递时间戳)。 -线性一致性确保因果性的事实使线性一致系统变得简单易懂,更有吸引力。然而,正如“[线性一致性的代价](#线性一致性的代价)”中所讨论的,使系统线性一致可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果系统在地理上散布)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,从而获得更好的性能,但它们用起来却更困难。 +线性一致性确保因果性的事实使线性一致系统变得简单易懂,更有吸引力。然而,正如“[线性一致性的代价](#线性一致性的代价)”中所讨论的,使系统线性一致可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果系统在地理上散布)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,从而获得更好的性能,但它们用起来也更为困难。 -好消息是,折衷是可能的。线性一致性并不是保持因果性的唯一途径——还有其他方法。一个系统可以是因果一致的,而无需承担线性一致带来的性能折损(尤其是,CAP定理不适用的情况)。实际上在所有的不会被网络延迟拖慢的一致性模型中,因果一致性是可行的最强的一致性模型。而且在网络故障时仍能保持可用【2,42】。 +好消息是存在折衷的可能性。线性一致性并不是保持因果性的唯一途径 —— 还有其他方法。一个系统可以是因果一致的,而无需承担线性一致带来的性能折损(尤其对于CAP定理不适用的情况)。实际上在所有的不会被网络延迟拖慢的一致性模型中,因果一致性是可行的最强的一致性模型。而且在网络故障时仍能保持可用【2,42】。 -在许多情况下,似乎需要线性一致性的系统实际上只需要因果一致性,因果一致性可以更高效地实现。基于这种观察,研究人员正在探索新型的数据库,既能保证因果一致性,且性能与可用性与最终一致系统类似【49,50,51】。 +在许多情况下,看上去需要线性一致性的系统,实际上需要的只是因果一致性,因果一致性可以更高效地实现。基于这种观察结果,研究人员正在探索新型的数据库,既能保证因果一致性,且性能与可用性与最终一致的系统类似【49,50,51】。 这方面的研究相当新鲜,其中很多尚未应用到生产系统,仍然有不少挑战需要克服【52,53】。但对于未来的系统而言,这是一个有前景的方向。 #### 捕获因果关系 -我们不会在这里讨论非线性系统如何保证因果一致性的细节,而只是简要地探讨一些关键的思想。 +我们不会在这里讨论非线性一致的系统如何保证因果性的细节,而只是简要地探讨一些关键的思想。 -为了保持因果性,你需要知道哪个操作发生在哪个其他操作之前(**happened before**)。这是一个偏序:并发操作可以以任意顺序进行,但如果一个操作发生在另一个操作之前,那它们必须在所有副本上以那个顺序被处理。因此,当一个副本处理一个操作时,它必须确保所有因果前驱的操作(之前发生的所有操作)已经被处理;如果前面的某个操作丢失了,后面的操作必须等待,直到前面的操作被处理完毕。 +为了维持因果性,你需要知道哪个操作发生在哪个其他操作之前(**happened before**)。这是一个偏序:并发操作可以以任意顺序进行,但如果一个操作发生在另一个操作之前,那它们必须在所有副本上以那个顺序被处理。因此,当一个副本处理一个操作时,它必须确保所有因果前驱的操作(之前发生的所有操作)已经被处理;如果前面的某个操作丢失了,后面的操作必须等待,直到前面的操作被处理完毕。 -为了确定因果依赖关系,我们需要一些方法来描述系统中节点的“知识”。如果节点在发出写入Y时已经看到X值,则X和Y可能是因果关系的。这个分析使用了你在欺诈指控的刑事调查中所期望的那些问题:CEO在做出决定时是否知道X? +为了确定因果依赖,我们需要一些方法来描述系统中节点的“知识”。如果节点在发出写入Y 的请求时已经看到了 X的值,则 X 和 Y 可能存在因果关系。这个分析使用了那些在欺诈指控刑事调查中常见的问题:CEO在做出决定 Y 时是否**知道** X ? -在其他操作之前确定哪些操作发生的技术与我们在“[检测并发写入](ch5.md#检测并发写入)”中所讨论的内容类似。该节讨论无领导者数据存储区中的因果关系,我们需要检测到同一个关键字为了防止丢失更新。因果关系更进一步:它需要跟踪整个数据库的因果关系,而不仅仅是一个关键。版本向量可以被推广到做这个【54】。 +用于确定*哪些操作发生在其他操作之前* 的技术,与我们在“[检测并发写入](ch5.md#检测并发写入)”中所讨论的内容类似。那一节讨论了无领导者数据存储中的因果性:为了防止丢失更新,我们需要检测到对同一个键的并发写入。因果一致性则更进一步:它需要跟踪整个数据库中的因果依赖,而不仅仅是一个键。可以推广版本向量以解决此类问题【54】。 -为了确定因果顺序,数据库需要知道应用程序读取哪个版本的数据。这就是为什么在[图5-13](img/fig5-13.png)中,来自先前操作的版本号在写入时被传回到数据库的原因。在SSI的冲突检测中会出现类似的想法,如“[可序列化的快照隔离(SSI)]()”中所述:当事务要提交时,数据库将检查它读取的数据版本是否仍然运行至今。为此,数据库跟踪哪个数据已经被哪个事务读取。 +为了确定因果顺序,数据库需要知道应用读取了哪个版本的数据。这就是为什么在 [图5-13 ](img/fig5-13.png)中,来自先前操作的版本号在写入时被传回到数据库的原因。在SSI 的冲突检测中会出现类似的想法,如“[可序列化的快照隔离(SSI)]()”中所述:当事务要提交时,数据库将检查它所读取的数据版本是否仍然是最新的。为此,数据库跟踪哪些数据被哪些事务所读取。 ### 序列号顺序 -虽然因果关系是一个重要的理论概念,但实际上跟踪所有的因果关系是不切实际的。在许多应用程序中,客户端在写入内容之前会先读取大量数据,然后不清楚写入是因果关系依赖于全部还是仅仅一些先前的读取。显式跟踪所有已读取的数据将意味着很大的开销。 +虽然因果是一个重要的理论概念,但实际上跟踪所有的因果关系是不切实际的。在许多应用中,客户端在写入内容之前会先读取大量数据,我们无法弄清写入因果依赖于先前全部的读取内容,还是仅包括其中一部分。显式跟踪所有已读数据意味着巨大的额外开销。 -但是,还有一个更好的方法:我们可以使用序列号或时间戳来排序事件。时间戳不一定来自时钟(或物理时钟,有很多问题,如“[不可靠时钟](ch8.md#不可靠的时钟)”)。它可以来自一个逻辑时钟,这是一个算法来产生一个数字序列来识别操作,通常使用计数器,每个操作增加计数器。 +但还有一个更好的方法:我们可以使用**序列号(sequence nunber)**或**时间戳(timestamp)**来排序事件。时间戳不一定来自时钟(或物理时钟,存在许多问题,如 “[不可靠时钟](ch8.md#不可靠的时钟)” 中所述)。它可以来自一个**逻辑时钟(logical clock)**,这是一个用来生成标识操作的数字序列的算法,典型实现是使用一个每次操作自增的计数器。 -这样的序列号或时间戳是紧凑的(只有几个字节大小),它们提供了一个总的顺序:也就是说,每一个操作都有一个唯一的序列号,你总是可以比较两个序列号来确定哪个更大(即,哪些操作发生在后面)。 -特别是,我们可以按照与因果关系一致的顺序创建序列号[^vii]:我们保证,如果操作A因果关系发生在B之前,那么A在总顺序之前发生在B之前(A具有比B更小的序列号)。并行操作可以任意命令。这样一个总的秩序捕获所有的因果信息,但也强加了比由于因果关系所严格要求的更多的秩序。 +这样的序列号或时间戳是紧凑的(只有几个字节大小),它提供了一个全序关系:也就是说每操作都有一个唯一的序列号,而且总是可以比较两个序列号,确定哪一个更大(即哪些操作后发生)。 -[^vii]: 与因果关系不一致的整个顺序很容易创建,但不是很有用。例如,你可以为每个操作生成随机UUID,并按照字典顺序比较UUID以定义操作的总顺序。这是一个有效的总顺序,但是随机的UUID并不告诉你哪个操作首先实际发生,或者操作是否是并发的。 +特别是,我们可以使用**与因果一致(consistent with causality)**的全序来生成序列号[^vii]:我们保证,如果操作 A 因果后继于操作 B,那么在这个全序中 A 在 B 前( A 具有比 B 更小的序列号)。并行操作之间可以任意排序。这样一个全序关系捕获了所有关于因果的信息,但也施加了一个比因果性要求更为严格的顺序。 -在单主复制的数据库中(请参见“[领导者与追随者](ch5.md#领导者与追随者)”),复制日志定义了与因果关系一致的写操作总顺序。领导者可以简单地为每个操作增加一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个追随者按照他们在复制日志中出现的顺序来应用写入,那么追随者的状态始终是因果一致的(即使它落后于领导者)。 +[^vii]: 与因果关系不一致的全序很容易创建,但没啥用。例如你可以为每个操作生成随机的UUID,并按照字典序比较UUID,以定义操作的全序。这是一个有效的全序,但是随机的UUID并不能告诉你哪个操作先发生,或者操作是否为并发的。 -#### 非因果序列号发生器 +在单主复制的数据库中(参见“[领导者与追随者](ch5.md#领导者与追随者)”),复制日志定义了与因果一致的写操作。主库可以简单地为每个操作自增一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个从库按照它们在复制日志中出现的顺序来应用写操作,那么从库的状态始终是因果一致的(即使它落后于领导者)。 -如果没有一个领导者(可能是因为你使用的是多领导者或无领导者的数据库,或者是因为数据库是分区的),那么如何为操作生成序列号还不太清楚。实践中使用了各种方法: +#### 非因果序列号生成器 -* 每个节点都可以生成自己独立的一组序列号。例如,如果有两个节点,一个节点只能生成奇数,而另一个节点只能生成偶数。通常,可以在序列号的二进制表示中保留一些位以包含唯一的节点标识符,这将确保两个不同的节点永远不会生成相同的序列号。 -* 你可以将时间戳从时钟(物理时钟)附加到每个操作【55】。这样的时间戳是不连续的,但是如果它们具有足够高的分辨率,那么它们可能足以完成命令操作。这个事实用于最后写入为准的冲突解决方法(参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。 -* 你可以预先分配序列号的块。例如,节点A可能要求从1到1,000的序列号的块,并且节点B可能要求该区块从1,001到2,000。然后,每个节点可以独立分配其块的序列号,并在序列号的提供开始变低时分配一个新的块。 +如果主库不存在(可能因为使用了多主数据库或无主数据库,或者因为使用了分区的数据库),如何为操作生成序列号就没有那么明显了。在实践中有各种各样的方法: -这三个选项都比单独的领导者增加一个计数器的表现更好,并且更具可扩展性。它们为每个操作生成一个唯一的,大约增加的序列号。然而,他们都有一个问题:他们产生的序列号与因果关系不一致。 +* 每个节点都可以生成自己独立的一组序列号。例如有两个节点,一个节点只能生成奇数,而另一个节点只能生成偶数。通常,可以在序列号的二进制表示中预留一些位,用于唯一的节点标识符,这样可以确保两个不同的节点永远不会生成相同的序列号。 +* 可以将时钟(物理时钟)时间戳附加到每个操作上【55】。这种时间戳并不连续,但是如果它具有足够高的分辨率,那也许足以提供一个操作的全序关系。这一事实应用于 *最后写入为准* 的冲突解决方法中(参阅“[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。 +* 可以预先分配序列号区块。例如,节点 A 可能要求从序列号1到1,000区块的所有权,而节点 B 可能要求序列号1,001到2,000区块的所有权。然后每个节点可以独立分配所属区块中的序列号,并在序列号告急时请求分配一个新的区块。 -因为这些序列号发生器不能正确地捕获不同节点上操作的顺序,所以会出现因果关系问题: +这三个选项都比单一主库的自增计数器表现要好,并且更具可扩展性。它们为每个操作生成一个唯一的,近似自增的序列号。然而它们都有同一个问题:生成的序列号与因果不一致。 -* 每个节点可以每秒处理不同数量的操作。因此,如果一个节点产生偶数而另一个产生奇数,则偶数的计数器可能落后于奇数的计数器,反之亦然。如果你有一个奇数的操作和一个偶数的操作,你不能准确地说出哪一个因果关系发生了。 +因为这些序列号生成器不能正确地捕获跨节点的操作顺序,所以会出现因果关系的问题: -* 来自物理时钟的时间戳会受到时钟偏移的影响,这可能会使其与因果性不一致。例如,见[图8-3](img/fig8-3.png),其中显示了一个情况,其中后来发生因果关系的操作实际上被分配了较低的时间戳。[^vii] +* 每个节点每秒可以处理不同数量的操作。因此,如果一个节点产生偶数序列号而另一个产生奇数序列号,则偶数计数器可能落后于奇数计数器,反之亦然。如果你有一个奇数编号的操作和一个偶数编号的操作,你无法准确地说出哪一个操作在因果上先发生。 - [^viii]: 可以使物理时钟时间戳与因果关系一致:在第294页的“用于全局快照的同步时钟”中,我们讨论了Google的Spanner,它可以估计预期的时钟偏差,并在提交写入之前等待不确定性间隔。 这个方法确保了一个事实上的后续事务得到了更大的时间戳。 但是,大多数时钟不能提供所需的不确定性度量。 +* 来自物理时钟的时间戳会受到时钟偏移的影响,这可能会使其与因果不一致。例如[图8-3](img/fig8-3.png) 展示了一个例子,其中因果上晚发生的操作,却被分配了一个更早的时间戳。[^vii] -* 在块分配器的情况下,一个操作可能会被赋予一个范围从1,001到2,000的序列号,而一个因果较晚的操作可能被赋予一个范围从1到1,000的数字。在这里,序列号与因果关系也是不一致的。 + [^viii]: 可以使物理时钟时间戳与因果关系保持一致:在“[用于全局快照的同步时钟](#用于全局快照的同步时钟)”中,我们讨论了Google的Spanner,它可以估计预期的时钟偏差,并在提交写入之前等待不确定性间隔。 这中方法确保了实际上靠后的事务会有更大的时间戳。 但是大多数时钟不能提供这种所需的不确定性度量。 + +* 在分配区块的情况下,某个操作可能会被赋予一个范围在1,001到2,000内的序列号,然而一个因果上更晚的操作可能被赋予一个范围在1到1,000之间的数字。这里序列号与因果关系也是不一致的。 #### 兰伯特时间戳 -尽管刚才描述的三个序列号发生器与因果关系不一致,但实际上有一个简单的方法来产生与因果关系一致的序列号。它被称为兰伯特时间戳,莱斯利·兰伯特(Leslie Lamport)于1978年提出【56】,现在是分布式系统领域中被引用最多的论文之一。 +尽管刚才描述的三个序列号生成器与因果不一致,但实际上有一个简单的方法来产生与因果关系一致的序列号。它被称为兰伯特时间戳,莱斯利·兰伯特(Leslie Lamport)于1978年提出【56】,现在是分布式系统领域中被引用最多的论文之一。 -[图9-8](img/fig9-8.png)说明了兰伯特时间戳的使用。每个节点都有一个唯一的标识符,每个节点都保存一个处理操作数量的计数器。 兰伯特时间戳然后是一对(计数器,节点ID)。二节点有时可能具有相同的计数器值,但通过在时间戳中包含节点ID,每个时间戳都是唯一的。 +[图9-8](img/fig9-8.png) 说明了兰伯特时间戳的应用。每个节点都有一个唯一标识符,和一个保存自己执行操作数量的计数器。 兰伯特时间戳就是两者的简单组合:(计数器,节点ID)$(counter, node ID)$。两个节点有时可能具有相同的计数器值,但通过在时间戳中包含节点ID,每个时间戳都是唯一的。 ![](img/fig9-8.png) **图9-8 Lamport时间戳提供了与因果关系一致的总排序。** -兰伯特时间戳与物理时间时钟没有任何关系,但是它提供了总计次数:如果你有两个时间戳,则计数器值较大的时间戳是较大的时间戳。如果计数器值相同,则节点ID越大的时间戳越大。 +兰伯特时间戳与物理时间时钟没有任何关系,但是它提供了一个全序:如果你有两个时间戳,则**计数器**值大者是更大的时间戳。如果计数器值相同,则节点ID越大的,时间戳越大。 -到目前为止,这个描述与上一节描述的偶数/奇数计数器基本相同。关于兰伯特时间戳的关键思想,使它们与因果关系一致,如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大计数器值,并在每个请求中包含最大计数器值。当一个节点接收到一个最大计数器值大于其自身计数器值的请求或响应时,它立即增加自己的计数器到最大值。 +迄今,这个描述与上节所述的奇偶计数器基本类似。使兰伯特时间戳因果一致的关键思想如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大**计数器**值,并在每个请求中包含这个最大计数器值。当一个节点收到最大计数器值大于自身计数器值的请求或响应时,它立即将自己的计数器设置为这个最大值。 -这如[图9-8](img/fig9-8.png)所示,其中客户端A从节点2接收计数器值5,然后将最大值5发送到节点1.此时,节点1的计数器仅为1,但是它立即向前移动到5,所以下一个操作的计数器值增加了6。 +这如 [图9-8](img/fig9-8.png) 所示,其中客户端 A 从节点 2 接收计数器值 5 ,然后将最大值 5 发送到节点 1 。此时,节点 1 的计数器仅为 1 ,但是它立即前移至 5 ,所以下一个操作的计数器的值为 6 。 -只要最大计数器值与每一个操作一起进行,这个方案确保兰伯特时间戳的排序与因果性一致,因为每个因果关系导致时间戳增加。 +只要每一个操作都携带着最大计数器值,这个方案确保兰伯特时间戳的排序与因果一致,因为每个因果依赖都会导致时间戳增长。 -兰伯特时间戳有时会与版本向量混淆,我们在第184页上的“检测并发写入”中看到了这些向量戳。虽然存在一些相似之处,但它们具有不同的目的:版本向量可以区分两个操作是并发还是因果依赖另一个,而兰伯特时间戳总是执行一个总的顺序。从兰伯特的全部顺序时间戳,你不能分辨两个操作是并行还是因果关系。 兰伯特时间戳优于版本向量的优点是它们更紧凑。 +兰伯特时间戳有时会与我们在 “[检测并发写入](ch5.md#检测并发写入)” 中看到的版本向量相混淆。虽然两者有一些相似之处,但它们有着不同的目的:版本向量可以区分两个操作是并发的,还是一个因果依赖另一个;而兰伯特时间戳总是施行一个全序。从兰伯特时间戳的全序中,你无法分辨两个操作是并发的还是因果依赖的。 兰伯特时间戳优于版本向量的地方是,它更加紧凑。 #### 光有时间戳排序还不够 -虽然兰伯特时间戳定义了与因果关系一致的操作总顺序,但它们还不足以解决分布式系统中的许多常见问题。 +虽然兰伯特时间戳定义了一个与因果一致的全序,但它还不足以解决分布式系统中的许多常见问题。 -例如,考虑一个需要确保用户名唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败。 (我们之前在“[领导和锁定](#领导和锁定)”中提到过这个问题。) +例如,考虑一个需要确保用户名能唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败。 (我们之前在“[领导者与锁定]](ch8.md#领导者与锁定)”中提到过这个问题。) -乍看之下,似乎总的操作顺序(例如,使用兰伯特时间戳)应该足以解决此问题:如果创建了两个具有相同用户名的帐户,请选择时间戳较低的那个作为获胜者(一个谁先抓住用户名),并让更大的时间戳失败。由于时间戳是完全有序的,所以这个比较总是有效的。 +乍看之下,似乎操作的全序关系足以解决这一问题(例如使用兰伯特时间戳):如果创建了两个具有相同用户名的帐户,选择时间戳较小的那个作为胜者(第一个抓到用户名的人),并让带有更大时间戳者失败。由于时间戳上有全序关系,所以这个比较总是可行的。 -这种方法适用于事后确定胜利者:一旦收集了系统中的所有用户名创建操作,就可以比较他们的时间戳。然而,当一个节点刚刚收到用户的一个请求来创建一个用户名,并且现在需要决定这个请求是成功还是失败,这是不够的。此时,节点不知道另一个节点是否正在同时创建具有相同用户名的帐户,以及其他节点可以分配给该操作的时间戳。 +这种方法适用于事后确定胜利者:一旦你收集了系统中的所有用户名创建操作,就可以比较它们的时间戳。然而当某个节点需要实时处理用户创建用户名的请求时,这样的方法就无法满足了。节点需要**马上(right now)**决定这个请求是成功还是失败。在那个时刻,节点并不知道是否存其他节点正在并发执行创建同样用户名的操作,罔论其它节点可能分配给那个操作的时间戳。 -为了确保没有其他节点正在使用相同的用户名和较低的时间戳同时创建一个帐户,你必须检查每个节点,看看它在做什么【56】。如果其中一个节点由于网络问题而出现故障或无法到达,则该系统将停止工作。这不是我们需要的那种容错系统。 +为了确保没有其他节点正在使用相同的用户名和较小的时间戳并发创建同名账户,你必须检查其它每个节点,看看它在做什么【56】。如果其中一个节点由于网络问题出现故障或不可达,则整个系统可能被拖至停机。这不是我们需要的那种容错系统。 -这里的问题是,只有在收集了所有的操作之后,操作的总顺序才会出现。如果另一个节点已经产生了一些操作,但是你还不知道它们是什么,那么就不能构造最终的操作顺序:来自另一个节点的未知操作可能需要被插入到总数的不同位置订购。 +这里的问题是,只有在所有的操作都被收集之后,操作的全序才会出现。如果另一个节点已经产生了一些操作,但你还不知道那些操作是什么,那就无法构造所有操作最终的全序关系:来自另一个节点的未知操作可能需要被插入到全序中的不同位置。 -总之:为了实现像用户名的唯一性约束这样的事情,仅仅对操作进行全面的排序是不够的,你还需要知道该命令何时完成。如果你有创建用户名的操作,并且你确定没有其他节点可以在你的操作之前为全部顺序插入相同用户名的声明,则可以安全地声明操作成功。 +总之:为了实诸如如用户名上的唯一约束这种东西,仅有操作的全序是不够的,你还需要知道这个全序何时会尘埃落定。如果你有一个创建用户名的操作,并且确定在全序中,没有任何其他节点可以在你的操作之前插入对同一用户名的声称,那么你就可以安全地宣告操作执行成功。 -这个知道什么时候你的总顺序被完成的概念被记录在全序广播的话题中。 +如何知道你的全序关系已经尘埃落定,这个想法将在[全序广播](#全序广播)一节中详细说明。 ### 全序广播 -如果你的程序只运行在一个CPU内核上,那么定义一个操作总的顺序是很容易的:它只是CPU执行的顺序。但是,在分布式系统中,让所有节点在相同的操作顺序上达成一致是非常棘手的。在最后一节中,我们讨论了按时间戳或序列号进行排序,但发现它不如单主复制(如果使用时间戳排序来实现唯一性约束,则不能容忍任何错误)。 +如果你的程序只运行在单个CPU核上,那么定义一个操作全序是很容易的:可以简单地就是CPU执行这些操作的顺序。但是在分布式系统中,让所有节点对同一个全局操作顺序达成一致可能相当棘手。在上一节中,我们讨论了按时间戳或序列号进行排序,但发现它还不如单主复制给力(如果你使用时间戳排序来实现唯一性约束,而且不能容忍任何错误)。 -如前所述,单引导程序复制通过选择一个节点作为引导程序来确定操作的总顺序,并对引导程序上的单个CPU核心上的所有操作进行排序。接下来的挑战是如果吞吐量大于单个领导者可以处理的情况下如何扩展系统,以及如果领导者失败(“[处理节点宕机](#处理节点宕机)”),如何处理故障转移。在分布式系统文献中,这个问题被称为全序广播或原子广播[^ix]【25,57,58】。 +如前所述,单主复制通过选择一个节点作为主库来确定操作的全序,并在主库的单个CPU核上对所有操作进行排序。接下来的挑战是,如果吞吐量超出单个主库的处理能力,这种情况下如何扩展系统;以及,如果主库失效(“[处理节点宕机](#处理节点宕机)”),如何处理故障转移。在分布式系统文献中,这个问题被称为**全序广播(total order broadcast)**或**原子广播(atomic broadcast)**[^ix]【25,57,58】。 -[^ix]: “原子广播”这个术语是传统的,但是它是非常混乱的,因为它与原子的其他用法不一致:它与ACID事务中的原子性没有任何关系,只是与原子操作(在多线程编程的意义上 )或原子寄存器(线性一致性存储)。 总的顺序组播是另一个同义词。 +[^ix]: “原子广播”是一个传统的术语,非常混乱,而且与“原子”一词的其他用法不一致:它与ACID事务中的原子性没有任何关系,只是与原子操作(在多线程编程的意义上 )或原子寄存器(线性一致存储)有间接的联系。全序广播是另一个同义词。 > #### 顺序保证的范围 > -> 每个分区有一个单独的引导程序的分区数据库通常只对每个分区进行排序,这意味着它们不能提供跨分区的一致性保证(例如,一致的快照,外键引用)。 所有分区的总排序是可能的,但需要额外的协调【59】。 +> 每个分区各有一个主库的分区数据库,通常只在每个分区内维持顺序,这意味着它们不能提供跨分区的一致性保证(例如,一致性快照,外键引用)。 跨所有分区的全序是可能的,但需要额外的协调【59】。 -全序广播通常被描述为在节点之间交换消息的协议。 非正式地,它要求总是满足两个安全属性: +全序广播通常被描述为在节点间交换消息的协议。 非正式地讲,它要满足两个安全属性: ***可靠交付(reliable delivery)*** -没有消息丢失:如果消息被传递到一个节点,它将被传递到所有节点。 +​ 没有消息丢失:如果消息被传递到一个节点,它将被传递到所有节点。 -***完全有序交付(totally ordered delivery)*** +**全序交付(totally ordered delivery)*** -消息以相同的顺序传递给每个节点。 +​ 消息以相同的顺序传递给每个节点。 -全序广播的正确算法必须确保始终满足可靠性和订购属性,即使节点或网络出现故障。当然,在网络中断的时候,消息不会被传送,但是一个算法可以继续重试,以便在网络被最终修复的时候消息能够通过(然后它们仍然必须按照正确的顺序传送)。 +正确的全序广播算法必须始终保证可靠性和有序性,即使节点或网络出现故障。当然在网络中断的时候,消息是传不出去的,但是算法可以不断重试,以便在网络最终修复时,消息能及时通过并送达(当然它们必须仍然按照正确的顺序传递)。 #### 使用全序广播 -像ZooKeeper和etcd这样的共识服务实际上是实现全面的顺序播放。这个事实暗示了整个命令广播和共识之间有着密切的联系,我们将在本章后面进行探讨。 +像ZooKeeper和etcd这样的共识服务实际上实现了全序广播。这一事实暗示了全序广播与共识之间有着紧密联系,我们将在本章稍后进行探讨。 -全序广播正是你所需的数据库复制:如果每封邮件都表示写入数据库,并且每个副本按相同的顺序处理相同的写入,则副本将保持一致(除了临时复制滞后)。这个原则被称为状态机复制【60】,我们将在[第11章](ch11.md)中回到它。 -类似地,可以使用全序广播来实现可序列化的事务:如“[真的串行执行](ch7.md#真的串行执行)”中所述,如果每个消息表示一个确定性事务作为存储过程来执行,并且每个节点都处理这些消息相同的顺序,那么数据库的分区和副本保持一致【61】。 +全序广播正是数据库复制所需的:如果每个消息都代表一次数据库的写入,且每个副本都按相同的顺序处理相同的写入,那么副本间将相互保持一致(除了临时的复制延迟)。这个原理被称为**状态机复制(state machine replication)**【60】,我们将在[第11章](ch11.md)中重新回到这个概念。 -全序广播的一个重要方面是顺序在交付消息时是固定的:如果后续消息已经交付,节点不允许追溯地将消息插入顺序中的较早位置。这个事实使得全序广播比时间戳命令更强。 +与之类似,可以使用全序广播来实现可序列化的事务:如“[真的串行执行](ch7.md#真的串行执行)”中所述,如果每个消息都表示一个确定性事务,以存储过程的形式来执行,且每个节点都以相同的顺序处理这些消息,那么数据库的分区和副本就可以相互保持一致【61】。 -查看全序广播的另一种方式是创建日志(如在复制日志,事务日志或预写日志中):传递消息就像附加到日志。由于所有节点必须以相同的顺序传递相同的消息,因此所有节点都可以读取日志并看到相同的消息序列。 +全序广播的一个重要表现是,顺序在消息送达时被固化:如果后续的消息已经送达,节点就不允许追溯地将(先前)消息插入顺序中的较早位置。这个事实使得全序广播比时间戳命令更强。 -全面订购广播对于实施提供防护令牌的锁定服务也很有用(参见“[防护令牌](ch8.md#防护令牌)”)。每个获取锁的请求都作为消息添加到日志中,并且所有消息都按它们在日志中出现的顺序依次编号。序列号可以作为一个击剑标记,因为它是单调递增的。在ZooKeeper中,这个序列号被称为`zxid` 【15】。 +考量全序广播的另一种方式是,这是一种创建日志的方式(如在复制日志,事务日志或预写式日志中):传递消息就像附加写入日志。由于所有节点必须以相同的顺序传递相同的消息,因此所有节点都可以读取日志,并看到相同的消息序列。 -#### 使用全序广播实现线性一致性的存储 +全序广播对于实现提供防护令牌的锁服务也很有用(参见“[防护令牌](ch8.md#防护令牌)”)。每个获取锁的请求都作为一条消息追加到日志末尾,并且所有的消息都按它们在日志中出现的顺序依次编号。序列号可以当成防护令牌用,因为它是单调递增的。在ZooKeeper中,这个序列号被称为`zxid` 【15】。 -如[图9-4](img/fig9-4.png)所示,在线性一致性的系统中,有一个操作的全序。这是否意味着线性一致性与全序播放相同?不完全,但两者之间有密切的联系[^x]。 +#### 使用全序广播实现线性一致的存储 -[^x]: 从形式上讲,线性读写寄存器是一个“更容易”的问题。 全序广播等同于共识【67】,在异步崩溃停止模型【68】中没有确定性的解决方案,而线性一致性的读写寄存器可以在同一系统模型中实现【23,24,25】。 然而,支持原子操作,如比较和设置,或者在寄存器中增加和获取,使得它相当于共识【28】。 因此,共识问题和线性一致性的注册问题密切相关。 +如 [图9-4](img/fig9-4.png) 所示,在线性一致的系统中,存在操作的全序。这是否意味着线性一致与全序广播一样?不尽然,但两者之间有者密切的联系[^x]。 -全部顺序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息何时被传送(所以一个接收者可能落后于其他接收者)。相比之下,线性一致性是最近的保证:读取保证看到写入的最新值。 +[^x]: 从形式上讲,线性一致读写寄存器是一个“更容易”的问题。 全序广播等价于共识【67】,而共识问题在异步的崩溃-停止模型【68】中没有确定性的解决方案,而线性一致的读写寄存器**可以**在这种模型中实现【23,24,25】。 然而,支持诸如**比较并设置(CAS, compare-and-set)**,或**自增并返回(increment-and-get)**的原子操作使它等价于共识问题【28】。 因此,共识问题与线性一致寄存器问题密切相关。 -但是,如果你有全序广播,则可以在其上构建线性一致性存储。例如,你可以确保用户名唯一标识用户帐户。 +全序广播是异步的:消息被保证以固定的顺序可靠地传送,但是不能保证消息**何时**被送达(所以一个接收者可能落后于其他接收者)。相比之下,线性一致性是新鲜性的保证:读取一定能看见最新的写入值。 -想象一下,对于每一个可能的用户名,你都可以拥有一个带有原子比较和设置操作的线性一致性寄存器。每个寄存器最初的值为空值(表示不使用用户名)。当用户想要创建一个用户名时,对该用户名的注册表执行比较设置操作,在前一个注册值为空的情况下,将其设置为用户账号。如果多个用户试图同时获取相同的用户名,则只有一个比较和设置操作会成功,因为其他用户将看到非空值(由于线性一致性)。 +但如果有了全序广播,你就可以在此基础上构建线性一致的存储。例如,你可以确保用户名能唯一标识用户帐户。 -你可以通过使用全序广播作为仅追加日志【62,63】来执行如下线性一致性的比较和设置操作: +设想对于每一个可能的用户名,你都可以有一个带有CAS原子操作的线性一致寄存器。每个寄存器最初的值为空值(表示不使用用户名)。当用户想要创建一个用户名时,对该用户名的寄存器执行CAS操作,在先前寄存器值为空的条件,将其值设置为用户的账号ID。如果多个用户试图同时获取相同的用户名,则只有一个CAS操作会成功,因为其他用户会看到非空的值(由于线性一致性)。 -1. 在日志中添加一条消息,暂时指明你要声明的用户名。 -2. 阅读日志,并等待你附加的信息被传回给你。[^xi] -3. 检查是否有任何消息声称你想要的用户名。如果所需用户名的第一条消息是你自己的消息,那么你是成功的:你可以提交用户名声明(也许通过附加另一条消息到日志)并确认给客户端。如果所需用户名的第一条消息来自其他用户,则中止操作。 +你可以通过将全序广播当成仅追加日志【62,63】的方式来实现这种线性一致的CAS操作: -[^xi]: 如果你不等待,但是在入队之后立即确认写入,则会得到类似于多核x86处理器的内存一致性模型【43】。 该模型既不是线性的也不是连续的。 +1. 在日志中追加一条消息,试探性地指明你要声明的用户名。 +2. 读日志,并等待你所附加的信息被回送。[^xi] +3. 检查是否有任何消息声称目标用户名的所有权。如果这些消息中的第一条就你自己的消息,那么你就成功了:你可以提交声称的用户名(也许是通过向日志追加另一条消息)并向客户端确认。如果所需用户名的第一条消息来自其他用户,则中止操作。 -由于日志条目以相同顺序传递到所有节点,因此如果有多个并发写入,则所有节点将首先同意哪个节点。选择第一个冲突的写入作为胜利者,并中止后面的写入,确保所有节点都同意写入是提交还是中止。一个类似的方法可以用来在一个日志之上实现可序列化的多对象事务【62】。 +[^xi]: 如果你不等待,而是在消息入队之后立即确认写入,则会得到类似于多核x86处理器内存的一致性模型【43】。 该模型既不是线性一致的也不是顺序一致的。 -虽然此过程确保线性写入,但不能保证线性一致性读取 - 如果你从与日志异步更新的存储中读取数据,则可能是陈旧的。 (具体来说,这里描述的过程提供了顺序一致性【47,64】,有时也称为时间线一致性【65,66】,这比线性一致性要弱一些)。为了使读取线性一致,有几个选项: +由于日志项是以相同顺序送达至所有节点,因此如果有多个并发写入,则所有节点会对最先到达者达成一致。选择冲突写入中的第一个作为胜利者,并中止后来者,以此确定所有节点对某个写入是提交还是中止达成一致。类似的方法可以在一个日志的基础上实现可序列化的多对象事务【62】。 -* 你可以通过附加消息,读取日志以及在消息被传回给你时执行实际读取来对日志进行排序。消息在日志中的位置因此定义了读取发生的时间点。 (法定读取etcd的工作有点像这样【16】。) -* 如果日志允许以线性方式获取最新日志消息的位置,则可以查询该位置,等待直到该位置的所有条目传送给你,然后执行读取。 (这是Zookeeper的`sync()`操作背后的思想【15】)。 -* 你可以从写入时同步更新的副本进行读取,因此可以确保最新。 (这种技术用于链式复制【63】;参阅“[复制研究](ch5.md#设置新从库)”。) +尽管这一过程保证写入是线性一致的,但它并不保证读取也是线性一致的 —— 如果你从与日志异步更新的存储中读取数据,结果可能是陈旧的。 (精确地说,这里描述的过程提供了**顺序一致性(sequential consistency)**【47,64】,有时也称为**时间线一致性(timeline consistency)**【65,66】,比线性一致性稍微弱一些的保证)。为了使读取也线性一致,有几个选项: + +* 你可以通过追加一条消息,当消息回送时读取日志,执行实际的读取。消息在日志中的位置因此定义了读取发生的时间点。 (etcd的法定人数读取有些类似这种情况【16】。) +* 如果日志允许以线性一致的方式获取最新日志消息的位置,则可以查询该位置,等待直到该位置前的所有消息都传达到你,然后执行读取。 (这是Zookeeper `sync()` 操作背后的思想【15】)。 +* 你可以从同步更新的副本中进行读取,因此可以确保结果是最新的。 (这种技术用于链式复制【63】;参阅“[复制研究](ch5.md#设置新从库)”。) #### 使用线性一致性存储实现全序广播 -最后一节介绍了如何从全序广播中构建一个线性一致性的比较和设置操作。我们也可以把它转过来,假设我们有线性一致性的存储,并展示如何从它构建全部命令播放。 +上一节介绍了如何从全序广播构建一个线性一致的CAS操作。我们也可以把它反过来,假设我们有线性一致的存储,接下来会展示如何在此基础上构建全序广播。 -最简单的方法是假设你有一个线性一致性的寄存器来存储一个整数,并且有一个原子增量和获取操作【28】。或者,原子比较和设置操作也可以完成这项工作。 +最简单的方法是假设你有一个线性一致的寄存器来存储一个整数,并且有一个原子**增量和获取**操作【28】。或者CAS操作也可以完成这项工作。 该算法很简单:对于每个要通过全部顺序广播发送的消息,你将递增并获取线性一致性的整数,然后将从寄存器获得的值作为序号附加到消息中。然后,你可以将消息发送到所有节点(重新发送任何丢失的消息),并且收件人将按序号连续发送消息。 From 11e8c80a64483a25c1b140f03c42395eb5a24992 Mon Sep 17 00:00:00 2001 From: Vonng Date: Thu, 29 Mar 2018 23:12:00 +0800 Subject: [PATCH 08/17] ch9 80% --- README.md | 2 +- ch7.md | 48 ++++++----- ch8.md | 2 +- ch9.md | 194 ++++++++++++++++++++++----------------------- img/ddia-wexin.JPG | Bin 109537 -> 108440 bytes 5 files changed, 121 insertions(+), 125 deletions(-) diff --git a/README.md b/README.md index 0067825..e7d1849 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ | 第六章:分区 | 初翻 | | | 第七章:事务 | 精翻 60% | Vonng | | 第八章:分布式系统中的问题 | 初翻 | | -| 第九章:一致性与共识 | 初翻 65% | Vonng | +| 第九章:一致性与共识 | 初翻 80% | Vonng | | 第三部分:前言 | 精翻 | | | 第十章:批处理 | 草翻 | | | 第十一章:流处理 | 草翻 | | diff --git a/ch7.md b/ch7.md index 93da2e4..464a341 100644 --- a/ch7.md +++ b/ch7.md @@ -82,7 +82,7 @@ ACID一致性的概念是,**对数据的一组特定陈述必须始终成立** 但是,一致性的这种概念取决于应用程序对不变量的观念,应用程序负责正确定义它的事务,并保持一致性。这并不是数据库可以保证的事情:如果你写入违反不变量的脏数据,数据库也无法阻止你。 (一些特定类型的不变量可以由数据库检查,例如外键约束或唯一约束,但是一般来说,是应用程序来定义什么样的数据是有效的,什么样是无效的。—— 数据库只管存储。) -原子性,隔离性和持久性是数据库的属性,而一致性(在ACID意义上)是应用程序的属性。应用程序可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因此,字母C不属于ACID[^i]。 +原子性,隔离性和持久性是数据库的属性,而一致性(在ACID意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因此,字母C不属于ACID[^i]。 [^i]: 乔·海勒斯坦(Joe Hellerstein)指出,在论Härder与Reuter的论文中,“ACID中的C”是被“扔进去凑缩写单词的”【7】,而且那时候大家都不怎么在乎一致性。 @@ -90,7 +90,7 @@ ACID一致性的概念是,**对数据的一组特定陈述必须始终成立** 大多数数据库都会同时被多个客户端访问。如果它们各自读写数据库的不同部分,这是没有问题的,但是如果它们访问相同的数据库记录,则可能会遇到**并发**问题(**竞争条件(race conditions)**)。 -[图7-1](img/fig7-1.png)是这类问题的一个简单例子。假设你有两个客户端同时在数据库中增长一个计数器。(假设数据库中没有自增操作)每个客户端需要读取计数器的当前值,加1,再回写新值。[图7-1](img/fig7-1.png)中,因为发生了两次增长,计数器应该从42增至44;但由于竞态条件,实际上只增至43。 +[图7-1](img/fig7-1.png)是这类问题的一个简单例子。假设你有两个客户端同时在数据库中增长一个计数器。(假设数据库中没有自增操作)每个客户端需要读取计数器的当前值,加 1 ,再回写新值。[图7-1](img/fig7-1.png) 中,因为发生了两次增长,计数器应该从42增至44;但由于竞态条件,实际上只增至 43 。 ACID意义上的隔离性意味着,**同时执行的事务是相互隔离的**:它们不能相互冒犯。传统的数据库教科书将隔离性形式化为**可序列化(Serializability)**,这意味着每个事务可以假装它是唯一在整个数据库上运行的事务。数据库确保当事务已经提交时,结果与它们按顺序运行(一个接一个)是一样的,尽管实际上它们可能是并发运行的【10】。 @@ -131,7 +131,7 @@ ACID意义上的隔离性意味着,**同时执行的事务是相互隔离的** ***原子性*** -如果在一系列写操作的中途发生错误,则应中止事务处理,并丢弃当前事务的所有写入。换句话说,数据库免去了用户对部分失败的担忧——通过提供“**宁为玉碎,不为瓦全**(all-or-nothing)”的保证。 +如果在一系列写操作的中途发生错误,则应中止事务处理,并丢弃当前事务的所有写入。换句话说,数据库免去了用户对部分失败的担忧——通过提供“**宁为玉碎,不为瓦全(all-or-nothing)**”的保证。 ***隔离性*** @@ -145,7 +145,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true 但如果邮件太多,你可能会觉得这个查询太慢,并决定用单独的字段存储未读邮件的数量(一种反规范化)。现在每当一个新消息写入时,必须也增长未读计数器,每当一个消息被标记为已读时,也必须减少未读计数器。 -在[图7-2](img/fig7-2.png)中,用户2遇到异常情况:邮件列表里显示有未读消息,但计数器显示为零未读消息,因为计数器增长还没有发生[^ii]。隔离性可以避免这个问题:通过确保用户2要么同时看到新邮件和增长后的计数器,要么都看不到。反正不会看到执行到一半的中间结果。 +在[图7-2](img/fig7-2.png)中,用户2 遇到异常情况:邮件列表里显示有未读消息,但计数器显示为零未读消息,因为计数器增长还没有发生[^ii]。隔离性可以避免这个问题:通过确保用户2 要么同时看到新邮件和增长后的计数器,要么都看不到。反正不会看到执行到一半的中间结果。 [^ii]: 可以说邮件应用中的错误计数器并不是什么特别重要的问题。但换种方式来看,你可以把未读计数器换成客户账户余额,把邮件收发看成支付交易。 @@ -159,7 +159,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true **图7-3 原子性确保发生错误时,事务先前的任何写入都会被撤消,以避免状态不一致** -多对象事务需要某种方式来确定哪些读写操作属于同一个事务。在关系型数据库中,通常基于客户端与数据库服务器的TCP连接:在任何特定连接上,`BEGIN TRANSACTION`和`COMMIT`语句之间的所有内容,被认为是同一事务的一部分.[^iii] +多对象事务需要某种方式来确定哪些读写操作属于同一个事务。在关系型数据库中,通常基于客户端与数据库服务器的TCP连接:在任何特定连接上,`BEGIN TRANSACTION` 和 `COMMIT` 语句之间的所有内容,被认为是同一事务的一部分.[^iii] [^iii]: 这并不完美。如果TCP连接中断,则事务必须中止。如果中断发生在客户端请求提交之后,但在服务器确认提交发生之前,客户端并不知道事务是否已提交。为了解决这个问题,事务管理器可以通过一个唯一事务标识符来对操作进行分组,这个标识符并未绑定到特定TCP连接。后续再“[数据库端到端的争论](ch12.md#数据库端到端的争论)”一节将回到这个主题。 @@ -167,30 +167,30 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true #### 单对象写入 -当单个对象发生改变时,原子性和隔离也是适用的。例如,假设您正在向数据库写入一个20 KB的JSON文档: +当单个对象发生改变时,原子性和隔离也是适用的。例如,假设您正在向数据库写入一个 20 KB的 JSON文档: -- 如果在发送第一个10 KB之后网络连接中断,数据库是否存储了不可解析的10 KB JSON片段? +- 如果在发送第一个10 KB之后网络连接中断,数据库是否存储了不可解析的10KB JSON片段? - 如果在数据库正在覆盖磁盘上的前一个值的过程中电源发生故障,是否最终将新旧值拼接在一起? - 如果另一个客户端在写入过程中读取该文档,是否会看到部分更新的值? -这些问题非常让人头大,故存储引擎一个几乎普遍的目标是:对单节点上的单个对象(例如键值对)上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复(请参阅第82页的“[使B树可靠]()”),并且可以使用每个对象上的锁来实现隔离(每次只允许一个线程访问对象) )。 +这些问题非常让人头大,故存储引擎一个几乎普遍的目标是:对单节点上的单个对象(例如键值对)上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复(参阅“[使B树可靠]()”),并且可以使用每个对象上的锁来实现隔离(每次只允许一个线程访问对象) )。 -一些数据库也提供更复杂的原子操作,例如自增操作,这样就不再需要像[图7-1](img/fig7-1.png)那样的读取-修改-写入序列了。同样流行的是**[比较和设置(CAS, compare-and-set)](#比较并设置(CAS))**操作,当值没有并发被其他人修改过时,才允许执行写操作。 +一些数据库也提供更复杂的原子操作,例如自增操作,这样就不再需要像 [图7-1](img/fig7-1.png) 那样的读取-修改-写入序列了。同样流行的是**[比较和设置(CAS, compare-and-set)](#比较并设置(CAS))**操作,当值没有并发被其他人修改过时,才允许执行写操作。 -这些单对象操作很有用,因为它们可以防止在多个客户端尝试同时写入同一个对象时丢失更新(参阅“[防止丢失更新](#防止丢失更新)”)。但是,它们不是通常意义上的事务。CAS以及其他单一对象操作被称为“轻量级事务”,甚至出于营销目的被称为“ACID”【20,21,22】,但是这个术语是误导性的。事务通常被理解为,**将多个对象上的多个操作合并为一个执行单元的机制**。[^iv] +这些单对象操作很有用,因为它们可以防止在多个客户端尝试同时写入同一个对象时丢失更新(参阅“[防止丢失更新](#防止丢失更新)”)。但它们不是通常意义上的事务。CAS以及其他单一对象操作被称为“轻量级事务”,甚至出于营销目的被称为“ACID”【20,21,22】,但是这个术语是误导性的。事务通常被理解为,**将多个对象上的多个操作合并为一个执行单元的机制**。[^iv] [^iv]: 严格地说,**原子自增(atomic increment)**这个术语在多线程编程的意义上使用了原子这个词。 在ACID的情况下,它实际上应该被称为**孤立(isolated)**的或**可序列化(serializable)**的增量。 但这就太吹毛求疵了。 #### 多对象事务的需求 -许多分布式数据存储已经放弃了多对象事务,因为多对象事务很难跨分区实现,而且在需要高可用性或高性能的情况下,它们可能会碍事。但说到底,在分布式数据库中实现事务,并没有什么根本性的障碍。[第9章](ch9.md)将讨论分布式事务的实现。 +许多分布式数据存储已经放弃了多对象事务,因为多对象事务很难跨分区实现,而且在需要高可用性或高性能的情况下,它们可能会碍事。但说到底,在分布式数据库中实现事务,并没有什么根本性的障碍。[第9章](ch9.md) 将讨论分布式事务的实现。 但是我们是否需要多对象事务?**是否有可能只用键值数据模型和单对象操作来实现任何应用程序?** 有一些场景中,单对象插入,更新和删除是足够的。但是许多其他场景需要协调写入几个不同的对象: * 在关系数据模型中,一个表中的行通常具有对另一个表中的行的外键引用。 (类似的是,在一个图数据模型中,一个顶点有着到其他顶点的边)。多对象事务使你确信这些引用始终有效:当插入几个相互引用的记录时,外键必须是正确的,最新的,不然数据就没有意义。 -* 在文档数据模型中,需要一起更新的字段通常在同一个文档中,这被视为单个对象——更新单个文档时不需要多对象事务。但是,缺乏连接功能的文档数据库会鼓励非规范化(参阅“[关系型数据库与文档数据库在今日的对比](ch2.md#关系型数据库与文档数据库在今日的对比)”)。当需要更新非规范化的信息时,如[图7-2](img/fig7-2.png)所示,需要一次更新多个文档。事务在这种情况下非常有用,可以防止非规范化的数据不同步。 +* 在文档数据模型中,需要一起更新的字段通常在同一个文档中,这被视为单个对象——更新单个文档时不需要多对象事务。但是,缺乏连接功能的文档数据库会鼓励非规范化(参阅“[关系型数据库与文档数据库在今日的对比](ch2.md#关系型数据库与文档数据库在今日的对比)”)。当需要更新非规范化的信息时,如 [图7-2](img/fig7-2.png) 所示,需要一次更新多个文档。事务在这种情况下非常有用,可以防止非规范化的数据不同步。 * 在具有二级索引的数据库中(除了纯粹的键值存储以外几乎都有),每次更改值时都需要更新索引。从事务角度来看,这些索引是不同的数据库对象:例如,如果没有事务隔离性,记录可能出现在一个索引中,但没有出现在另一个索引中,因为第二个索引的更新还没有发生。 这些应用仍然可以在没有事务的情况下实现。然而,**没有原子性,错误处理就要复杂得多,缺乏隔离性,就会导致并发问题**。我们将在“[弱隔离级别](#弱隔离级别)”中讨论这些问题,并在[第12章]()中探讨其他方法。 @@ -248,7 +248,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true 设想一个事务已经将一些数据写入数据库,但事务还没有提交或中止。另一个事务可以看到未提交的数据吗?如果是的话,那就叫做**脏读(dirty reads)**【2】。 -在**读已提交**隔离级别运行的事务必须防止脏读。这意味着事务的任何写入操作只有在该事务提交时才能被其他人看到(然后所有的写入操作都会立即变得可见)。如[图7-4]()所示,用户1设置了`x = 3`,但用户2的`get x`仍旧返回旧值2,而用户1尚未提交。 +在**读已提交**隔离级别运行的事务必须防止脏读。这意味着事务的任何写入操作只有在该事务提交时才能被其他人看到(然后所有的写入操作都会立即变得可见)。如[图7-4]()所示,用户1 设置了`x = 3`,但用户2 的 `get x `仍旧返回旧值2 ,而用户1 尚未提交。 ![](img/fig7-4.png) @@ -267,7 +267,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true 通过防止脏写,这个隔离级别避免了一些并发问题: -- 如果事务更新多个对象,脏写会导致不好的结果。例如,考虑[图7-5](img/fig7-5.png),[图7-5](img/fig7-5.png)以一个二手车销售网站为例,Alice和Bob两个人同时试图购买同一辆车。购买汽车需要两次数据库写入:网站上的商品列表需要更新,以反映买家的购买,销售发票需要发送给买家。在[图7-5](img/fig7-5.png)的情况下,销售是属于Bob的(因为他成功更新了商品列表),但发票却寄送给了爱丽丝(因为她成功更新了发票表)。读已提交会阻止这样这样的事故。 +- 如果事务更新多个对象,脏写会导致不好的结果。例如,考虑 [图7-5](img/fig7-5.png),[图7-5](img/fig7-5.png) 以一个二手车销售网站为例,Alice和Bob两个人同时试图购买同一辆车。购买汽车需要两次数据库写入:网站上的商品列表需要更新,以反映买家的购买,销售发票需要发送给买家。在[图7-5](img/fig7-5.png)的情况下,销售是属于Bob的(因为他成功更新了商品列表),但发票却寄送给了爱丽丝(因为她成功更新了发票表)。读已提交会阻止这样这样的事故。 - 但是,提交读取并不能防止[图7-1]()中两个计数器增量之间的竞争状态。在这种情况下,第二次写入发生在第一个事务提交后,所以它不是一个脏写。这仍然是不正确的,但是出于不同的原因,在“[防止更新丢失](#防止丢失更新)”中将讨论如何使这种计数器增量安全。 ![](img/fig7-5.png) @@ -338,7 +338,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true 表中的每一行都有一个`created_by`字段,其中包含将该行插入到表中的的事务ID。此外,每行都有一个`deleted_by`字段,最初是空的。如果某个事务删除了一行,那么该行实际上并未从数据库中删除,而是通过将`deleted_by`字段设置为请求删除的事务的ID来标记为删除。在稍后的时间,当确定没有事务可以再访问已删除的数据时,数据库中的垃圾收集过程会将所有带有删除标记的行移除,并释放其空间。[^译注ii] -[^译注ii]: 在PostgreSQL中,`created_by`实际名称为`xmin`,`deleted_by`实际名称为`xmax` +[^译注ii]: 在PostgreSQL中,`created_by` 实际名称为`xmin`,`deleted_by` 实际名称为`xmax` `UPDATE`操作在内部翻译为`DELETE`和`INSERT`。例如,在[图7-7]()中,事务13从账户2中扣除100美元,将余额从500美元改为400美元。实际上包含两条账户2的记录:余额为\$500的行被标记为**被事务13删除**,余额为\$400的行**由事务13创建**。 @@ -626,7 +626,7 @@ COMMIT; 在这种交互式的事务方式中,应用程序和数据库之间的网络通信耗费了大量的时间。如果不允许在数据库中进行并发处理,且一次只处理一个事务,则吞吐量将会非常糟糕,因为数据库大部分的时间都花费在等待应用程序发出当前事务的下一个查询。在这种数据库中,为了获得合理的性能,需要同时处理多个事务。 -出于这个原因,具有单线程串行事务处理的系统不允许交互式的多语句事务。取而代之,应用程序必须提前将整个事务代码作为存储过程提交给数据库。这些方法之间的差异如[图7-9](img/fig7-9.png)所示。如果事务所需的所有数据都在内存中,则存储过程可以非常快地执行,而不用等待任何网络或磁盘I/O。 +出于这个原因,具有单线程串行事务处理的系统不允许交互式的多语句事务。取而代之,应用程序必须提前将整个事务代码作为存储过程提交给数据库。这些方法之间的差异如[图7-9](img/fig7-9.png) 所示。如果事务所需的所有数据都在内存中,则存储过程可以非常快地执行,而不用等待任何网络或磁盘I/O。 ![](img/fig7-9.png) @@ -673,7 +673,7 @@ VoltDB还使用存储过程进行复制:但不是将事务的写入结果从 大约30年来,在数据库中只有一种广泛使用的序列化算法:**两阶段锁定(2PL,two-phase locking)**[^xi] -[^xi]: 有时也称为严格两阶段锁定(SS2PL, strict two-phas locking),以便和其他2PL变体区分。 +[^xi]: 有时也称为**严格两阶段锁定(SS2PL, strict two-phas locking)**,以便和其他2PL变体区分。 > #### 2PL不是2PC > @@ -692,8 +692,6 @@ VoltDB还使用存储过程进行复制:但不是将事务的写入结果从 2PL用于MySQL(InnoDB)和SQL Server中的可序列化隔离级别,以及DB2中的可重复读隔离级别【23,36】。 -[^xi]: 有时被称为强有力的严格的两阶段锁定(SS2PL),以区别于2PL的其他变种。 - 读与写的阻塞是通过为数据库中每个对象添加锁来实现的。锁可以处于**共享模式(shared mode)**或**独占模式(exclusive mode)**。锁使用如下: - 若事务要读取对象,则须先以共享模式获取锁。允许多个事务同时持有共享锁。但如果另一个事务已经在对象上持有排它锁,则这些事务必须等待。 @@ -719,7 +717,7 @@ VoltDB还使用存储过程进行复制:但不是将事务的写入结果从 在前面关于锁的描述中,我们掩盖了一个微妙而重要的细节。在“[导致写入偏差的幻读](#导致写入偏差的幻读)”中,我们讨论了**幻读(phantoms)**的问题。即一个事务改变另一个事务的搜索查询的结果。具有可序列化隔离级别的数据库必须防止**幻读**。 -在会议室预订的例子中,这意味着如果一个事务在某个时间窗口内搜索了一个房间的现有预订(见例7-2),则另一个事务不能同时插入或更新同一时间窗口与同一房间的另一个预订 (可以同时插入其他房间的预订,或在不影响另一个预定的条件下预定同一房间的其他时间段)。 +在会议室预订的例子中,这意味着如果一个事务在某个时间窗口内搜索了一个房间的现有预订(见[例7-2]()),则另一个事务不能同时插入或更新同一时间窗口与同一房间的另一个预订 (可以同时插入其他房间的预订,或在不影响另一个预定的条件下预定同一房间的其他时间段)。 如何实现这一点?从概念上讲,我们需要一个**谓词锁(predicate lock)**【3】。它类似于前面描述的共享/排它锁,但不属于特定的对象(例如,表中的一行),它属于所有符合某些搜索条件的对象,如: @@ -732,7 +730,7 @@ WHERE room_id = 123 AND 谓词锁限制访问,如下所示: -- 如果事务A想要读取匹配某些条件的对象,就像在这个`SELECT`查询中那样,它必须获取查询条件上的**共享谓词锁(shared-mode predicate lock)**。如果另一个事务B持有任何满足这一查询条件对象的排它锁,那么A必须等到B释放它的锁之后才允许进行查询。 +- 如果事务A想要读取匹配某些条件的对象,就像在这个 `SELECT` 查询中那样,它必须获取查询条件上的**共享谓词锁(shared-mode predicate lock)**。如果另一个事务B持有任何满足这一查询条件对象的排它锁,那么A必须等到B释放它的锁之后才允许进行查询。 - 如果事务A想要插入,更新或删除任何对象,则必须首先检查旧值或新值是否与任何现有的谓词锁匹配。如果事务B持有匹配的谓词锁,那么A必须等到B已经提交或中止后才能继续。 这里的关键思想是,谓词锁甚至适用于数据库中尚不存在,但将来可能会添加的对象(幻象)。如果两阶段锁定包含谓词锁,则数据库将阻止所有形式的写入偏差和其他竞争条件,因此其隔离实现了可串行化。 @@ -743,7 +741,7 @@ WHERE room_id = 123 AND 通过使谓词匹配到一个更大的集合来简化谓词锁是安全的。例如,如果你有在中午和下午1点之间预订123号房间的谓词锁,则锁定123号房间的所有时间段,或者锁定12:00~13:00时间段的所有房间(不只是123号房间)是一个安全的近似,因为任何满足原始谓词的写入也一定会满足这种更松散的近似。 -在房间预订数据库中,您可能会在`room_id`列上有一个索引,并且/或者在`start_time`和`end_time`上有索引(否则前面的查询在大型数据库上的速度会非常慢): +在房间预订数据库中,您可能会在`room_id`列上有一个索引,并且/或者在`start_time` 和 `end_time`上有索引(否则前面的查询在大型数据库上的速度会非常慢): - 假设您的索引位于`room_id`上,并且数据库使用此索引查找123号房间的现有预订。现在数据库可以简单地将共享锁附加到这个索引项上,指示事务已搜索123号房间用于预订。 - 或者,如果数据库使用基于时间的索引来查找现有预订,那么它可以将共享锁附加到该索引中的一系列值,指示事务已经将12:00~13:00时间段标记为用于预定。 @@ -817,7 +815,7 @@ WHERE room_id = 123 AND 当事务写入数据库时,它必须在索引中查找最近曾读取受影响数据的其他事务。这个过程类似于在受影响的键范围上获取写锁,但锁并不会阻塞事务到其他事务完成,而是像一个引线一样只是简单通知其他事务:你们读过的数据可能不是最新的啦。 -在[图7-11]()中,事务43通知事务42其先前读已过时,反之亦然。事务42首先提交并成功,尽管事务43的写影响了42,但因为事务43尚未提交,所以写入尚未生效。然而当事务43想要提交时,来自事务42的冲突写入已经被提交,所以43必须中止。 +在[图7-11]()中,事务43 通知事务42 其先前读已过时,反之亦然。事务42首先提交并成功,尽管事务43 的写影响了42 ,但因为事务43 尚未提交,所以写入尚未生效。然而当事务43 想要提交时,来自事务42 的冲突写入已经被提交,所以事务43 必须中止。 #### 可序列化的快照隔离的性能 @@ -829,7 +827,7 @@ WHERE room_id = 123 AND 与串行执行相比,可序列化快照隔离并不局限于单个CPU核的吞吐量:FoundationDB将检测到的序列化冲突分布在多台机器上,允许扩展到很高的吞吐量。即使数据可能跨多台机器进行分区,事务也可以在保证可序列化隔离等级的同时读写多个分区中的数据【54】。 -中止率显着影响SSI的整体表现。例如,长时间读取和写入数据的事务很可能会发生冲突并中止,因此SSI要求同时读写的事务尽量短(长时间运行的只读事务可能没问题)。对于慢事务,SSI可能比两阶段锁定或串行执行更不敏感。 +中止率显着影响SSI的整体表现。例如,长时间读取和写入数据的事务很可能会发生冲突并中止,因此SSI要求同时读写的事务尽量短(只读长事务可能没问题)。对于慢事务,SSI可能比两阶段锁定或串行执行更不敏感。 @@ -871,7 +869,7 @@ WHERE room_id = 123 AND ***字面意义上的串行执行*** -​ 如果每个事务的执行速度非常快,并且事务吞吐量足够低,足以在单个CPU内核上处理,这是一个简单而有效的选择。 +​ 如果每个事务的执行速度非常快,并且事务吞吐量足够低,足以在单个CPU核上处理,这是一个简单而有效的选择。 ***两阶段锁定*** diff --git a/ch8.md b/ch8.md index f291598..ff9ad0c 100644 --- a/ch8.md +++ b/ch8.md @@ -588,7 +588,7 @@ Web应用程序确实需要预期受终端用户控制的客户端(如Web浏 ***单调序列*** -​ 如果请求$x$返回了令牌$t_x$,并且请求$y$返回了令牌$t_y$,并且$x$在$y$开始之前已经完成,那么$t_x n$)可以获得“强一致性”。这取决于法定人数的具体配置,以及强一致性如何定义(通常不完全正确)。 +对于无领导者复制的系统(Dynamo风格;参阅“[无主复制](ch5.md#无主复制)”),有时候人们会声称通过要求法定人数读写( $w + r> n$ )可以获得“强一致性”。这取决于法定人数的具体配置,以及强一致性如何定义(通常不完全正确)。 -基于时钟(例如,在Cassandra中;参见“[依赖同步时钟](ch8.md#依赖同步时钟)”)的“最后写入胜利”冲突解决方法几乎可以确定是非线性的,由于时钟偏差,不能保证时钟的时间戳与实际事件顺序一致。松散的法定人数(第183页的“[松散法定人数与暗示接力](#马虎法定人数和暗示交接)”)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也是可能的,如下一节所示。 +基于时钟(例如,在Cassandra中;参见“[依赖同步时钟](ch8.md#依赖同步时钟)”)的“最后写入胜利”冲突解决方法几乎可以确定是非线性的,由于时钟偏差,不能保证时钟的时间戳与实际事件顺序一致。[松散的法定人数](ch5.md#马虎法定人数和暗示交接)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也是可能的,如下节所示。 #### 线性一致性和法定人数 @@ -236,9 +236,9 @@ **图9-6 非线性一致的执行,尽管使用了严格的法定人数** -在[图9-6](img/fig9-6.png)中,$x$ 的初始值为0,写入客户端通过向所有三个副本($n = 3, w = 3$)发送写入将 $x$ 更新为1。客户端A并发地从两个节点组成的法定人群($r = 2$)中读取数据,并在其中一个节点上看到新值1。客户端B也并发地从两个不同的节点组成的法定人数中读取,并从两个节点中取回了旧值0。 +在[图9-6](img/fig9-6.png)中,$x$ 的初始值为0,写入客户端通过向所有三个副本( $n = 3, w = 3$ )发送写入将 $x$ 更新为 `1`。客户端A并发地从两个节点组成的法定人群( $r = 2$ )中读取数据,并在其中一个节点上看到新值 `1` 。客户端B也并发地从两个不同的节点组成的法定人数中读取,并从两个节点中取回了旧值 `0` 。 -仲裁条件满足($w + r> n$),但是这个执行是非线性一致的:B的请求在A的请求完成后开始,但是B返回旧值,而A返回新值。 (又一次,如同Alice和Bob的例子 [图9-1]()) +仲裁条件满足( $w + r> n$ ),但是这个执行是非线性一致的:B的请求在A的请求完成后开始,但是B返回旧值,而A返回新值。 (又一次,如同Alice和Bob的例子 [图9-1]()) 有趣的是,通过牺牲性能,可以使Dynamo风格的法定人数线性化:读取者必须在将结果返回给应用程序之前,同步执行读取修复(参阅“[读时修复与反熵过程](ch5.md#读时修复与反熵过程)”) ,并且写入者必须在发送写入之前,读取法定数量节点的最新状态【24,25】。然而,由于性能损失,Riak不执行同步读取修复【26】。 Cassandra在进行法定人数读取时,**确实**在等待读取修复完成【27】;但是由于使用了最后写入为准的冲突解决方案,当同一个键有多个并发写入时,将不能保证线性一致性。 @@ -279,7 +279,7 @@ 因此不需要线性一致性的应用对网络问题有更强的容错能力。这种见解通常被称为CAP定理【29,30,31,32】,由Eric Brewer于2000年命名,尽管70年代的分布式数据库设计者早就知道了这种权衡【33,34,35,36】。 -CAP最初是作为一个经验法则提出的,没有准确的定义,目的是开始讨论数据库的权衡。那时候许多分布式数据库侧重于在共享存储的集群上提供线性一致性的语义【18】,CAP定理鼓励数据库工程师向分布式无共享系统的设计领域深入探索,这类架构更适合实现大规模的网络服务【37】。 对于这种文化上的转变,CAP值得赞扬——它见证了自00年代中期以来新数据库的技术爆炸(即NoSQL)。 +CAP最初是作为一个经验法则提出的,没有准确的定义,目的是开始讨论数据库的权衡。那时候许多分布式数据库侧重于在共享存储的集群上提供线性一致性的语义【18】,CAP定理鼓励数据库工程师向分布式无共享系统的设计领域深入探索,这类架构更适合实现大规模的网络服务【37】。 对于这种文化上的转变,CAP值得赞扬 —— 它见证了自00年代中期以来新数据库的技术爆炸(即NoSQL)。 > ### CAP定理没有帮助 > @@ -344,7 +344,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 然而数学集合并不完全是全序的:`{a, b}` 比 `{b, c}` 更大吗?好吧,你没法真正比较它们,因为二者都不是对方的子集。我们说它们是**无法比较(incomparable)**的,因此数学集合是**偏序(partially order)**的:在某些情况下,可以说一个集合大于另一个(如果一个集合包含另一个集合的所有元素),但在其他情况下它们是无法比较的[^译注i]。 -[^译注i]: 设R为非空集合A上的关系,如果R是自反的、反对称的和可传递的,则称R为A上的偏序关系。简称偏序,通常记作≦。一个集合A与A上的偏序关系R一起叫作偏序集,记作$$或$$。全序、偏序、关系、集合,这些概念的精确定义可以参考任意一本离散数学教材。 +[^译注i]: 设R为非空集合A上的关系,如果R是自反的、反对称的和可传递的,则称R为A上的偏序关系。简称偏序,通常记作≦。一个集合A与A上的偏序关系R一起叫作偏序集,记作$(A,R)$或$(A, ≦)$。全序、偏序、关系、集合,这些概念的精确定义可以参考任意一本离散数学教材。 全序和偏序之间的差异反映在不同的数据库一致性模型中: @@ -358,13 +358,13 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 因此,根据这个定义,在线性一致的数据存储中是不存在并发操作的:必须有且仅有一条时间线,所有的操作都在这条时间线上,构成一个全序关系。可能有几个请求在等待处理,但是数据存储确保了每个请求都是在唯一时间线上的某个时间点自动处理的,不存在任何并发。 -并发意味着时间线会分岔然后合并——在这种情况下,不同分支上的操作是无法比较的(即并发操作)。在[第五章](ch5.md)中我们看到了这种现象:例如,[图5-14](img/fig5-14.md)并不是一条直线的全序关系,而是一堆不同的操作并发进行。图中的箭头指明了因果依赖——操作的偏序。 +并发意味着时间线会分岔然后合并——在这种情况下,不同分支上的操作是无法比较的(即并发操作)。在[第五章](ch5.md)中我们看到了这种现象:例如,[图5-14](img/fig5-14.md) 并不是一条直线的全序关系,而是一堆不同的操作并发进行。图中的箭头指明了因果依赖——操作的偏序。 如果你熟悉像Git这样的分布式版本控制系统,那么其版本历史与因果关系图极其相似。通常,一个**提交(Commit)**发生在另一个提交之后,在一条直线上。但是有时你会遇到分支(当多个人同时在一个项目上工作时),**合并(Merge)**会在这些并发创建的提交相融合时创建。 #### 线性一致性强于因果一致性 -那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性**隐含着(implies)**因果关系:任何线性一致的系统都能正确保持因果性【7】。特别是,如果系统中有多个通信通道(如[图9-5](img/fig9-5.png)中的消息队列和文件存储服务),线性一致性可以自动保证因果性,系统无需任何特殊操作(如在不同组件间传递时间戳)。 +那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性**隐含着(implies)**因果关系:任何线性一致的系统都能正确保持因果性【7】。特别是,如果系统中有多个通信通道(如[图9-5](img/fig9-5.png) 中的消息队列和文件存储服务),线性一致性可以自动保证因果性,系统无需任何特殊操作(如在不同组件间传递时间戳)。 线性一致性确保因果性的事实使线性一致系统变得简单易懂,更有吸引力。然而,正如“[线性一致性的代价](#线性一致性的代价)”中所讨论的,使系统线性一致可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果系统在地理上散布)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,从而获得更好的性能,但它们用起来也更为困难。 @@ -440,7 +440,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 迄今,这个描述与上节所述的奇偶计数器基本类似。使兰伯特时间戳因果一致的关键思想如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大**计数器**值,并在每个请求中包含这个最大计数器值。当一个节点收到最大计数器值大于自身计数器值的请求或响应时,它立即将自己的计数器设置为这个最大值。 -这如 [图9-8](img/fig9-8.png) 所示,其中客户端 A 从节点 2 接收计数器值 5 ,然后将最大值 5 发送到节点 1 。此时,节点 1 的计数器仅为 1 ,但是它立即前移至 5 ,所以下一个操作的计数器的值为 6 。 +这如 [图9-8](img/fig9-8.png) 所示,其中客户端 A 从节点2 接收计数器值 `5` ,然后将最大值 `5` 发送到节点1 。此时,节点1 的计数器仅为 `1` ,但是它立即前移至 `5` ,所以下一个操作的计数器的值为 `6` 。 只要每一个操作都携带着最大计数器值,这个方案确保兰伯特时间戳的排序与因果一致,因为每个因果依赖都会导致时间戳增长。 @@ -534,17 +534,15 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 上一节介绍了如何从全序广播构建一个线性一致的CAS操作。我们也可以把它反过来,假设我们有线性一致的存储,接下来会展示如何在此基础上构建全序广播。 -最简单的方法是假设你有一个线性一致的寄存器来存储一个整数,并且有一个原子**增量和获取**操作【28】。或者CAS操作也可以完成这项工作。 +最简单的方法是假设你有一个线性一致的寄存器来存储一个整数,并且有一个原子**自增并返回**操作【28】。或者原子CAS操作也可以完成这项工作。 -该算法很简单:对于每个要通过全部顺序广播发送的消息,你将递增并获取线性一致性的整数,然后将从寄存器获得的值作为序号附加到消息中。然后,你可以将消息发送到所有节点(重新发送任何丢失的消息),并且收件人将按序号连续发送消息。 +该算法很简单:每个要通过全序广播发送的消息首先对线性一致寄存器执行**自增并返回**操作。然后将从寄存器获得的值作为序列号附加到消息中。然后你可以将消息发送到所有节点(重新发送任何丢失的消息),而收件人将按序列号连续发送消息。 -请注意,与兰伯特时间戳不同,你通过递增线性一致性寄存器获得的数字形成一个没有间隙的序列。因此,如果一个节点已经发送了消息4并且接收到序列号为6的传入消息,则它知道它在传递消息6之前必须等待消息5.同样的情况并非如此 +请注意,与兰伯特时间戳不同,通过自增线性一致性寄存器获得的数字形式上是一个没有间隙的序列。因此,如果一个节点已经发送了消息 4 并且接收到序列号为 6 的传入消息,则它知道它在传递消息 6 之前必须等待消息 5 。兰伯特时间戳则与之不同 ——事实上,这是全序广播和时间戳排序间的关键区别。 -与兰伯特时间戳——事实上,这是全序广播和时间戳顺序之间的关键区别。 +实现一个带有原子性**自增并返回**操作的线性一致寄存器有多困难?像往常一样,如果事情从来不出差错,那很容易:你可以简单地把它保存在单个节点内的变量中。问题在于处理当该节点的网络连接中断时的情况,并在该节点失效时能恢复这个值【59】。一般来说,如果你对线性一致性的序列号生成器进行深入过足够深入的思考,你不可避免地会得出一个共识算法。 -使用原子增量和获取操作来创建线性一致性整数有多困难?像往常一样,如果事情从来没有失败过,那很容易:你可以把它保存在一个节点的变量中。问题在于处理当该节点的网络连接中断时的情况,并在该节点失败时恢复该值【59】。一般来说,如果你对线性一致性序列号的产生者认真思考,你不可避免地会得出一个一致的算法。 - -这并非巧合:可以证明,线性一致性的比较和设置(或增量和取得)寄存器和全序广播都相当于【28,67】。也就是说,如果你能解决其中的一个问题,你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察力! +这并非巧合:可以证明,线性一致的CAS(或自增并返回)寄存器与全序广播都都等价于**共识**问题【28,67】。也就是说,如果你能解决其中的一个问题,你可以把它转化成为其他问题的解决方案。这是相当深刻和令人惊讶的洞察! 现在是时候正面处理共识问题了,我们将在本章的其余部分进行讨论。 @@ -552,125 +550,125 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 ## 分布式事务与共识 -共识是分布式计算中最重要也是最基本的问题之一。从表面上看,似乎很简单:非正式地说,目标只是让几个节点达成一致。你可能会认为这不应该太难。不幸的是,许多破损的系统已经被误认为这个问题很容易解决。 +**共识**是分布式计算中最重要也是最基本的问题之一。从表面上看似乎很简单:非正式地讲,目标只是**让几个节点达成一致(get serveral nodes to agree on something)**。你也许会认为这不会太难。不幸的是,许多出故障的系统都是因为错误地轻信这个问题很容易解决。 -虽然共识是非常重要的,但关于它的部分在本书的后半部分已经出现了,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术研究界,对共识的理解也只是在几十年的时间内逐渐显现出来,一路上有许多误解。现在我们已经讨论了复制([第5章](ch5.md)),事务([第7章](ch7.md)),系统模型([第8章](ch8.md)),线性一致性以及全序([本章](ch9.md)),我们终于准备好解决共识问题了。 +尽管共识非常重要,但关于它的内容出现在本书的后半部分,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术界,对共识的理解也是在几十年的过程中逐渐沉淀而来,一路上也有着许多误解。现在我们已经讨论了复制([第5章](ch5.md)),事务([第7章](ch7.md)),系统模型([第8章](ch8.md)),线性一致以及全序([本章](ch9.md)),我们终于准备好解决共识问题了。 -在节点达成一致的情况下,有许多情况是很重要的。例如: +节点能达成一致,在很多场景下都非常重要,例如: ***领导选举*** -​ 在具有单引导程序复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会引起争议。在这种情况下,一致性对于避免错误的故障切换非常重要,从而导致两个节点都认为自己是领导者的脑裂情况(参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。如果有两个领导者,它们都会接受写入,他们的数据会发生分歧,导致不一致和数据丢失。 +​ 在单主复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会对领导权的归属引起争议。在这种情况下,共识对于避免错误的故障切换非常重要。错误的故障切换会导致两个节点都认为自己是领导者(**脑裂**,参阅“[处理节点宕机](ch5.md#处理节点宕机)”)。如果有两个领导者,它们都会接受写入,它们的数据会发生分歧,从而导致不一致和数据丢失。 ***原子提交*** -在支持跨越多个节点或分区的事务的数据库中,有一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就ACID而言,请参“[原子性](ch7.md#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误)或者他们都承诺(如果没有出错)。这个共识的例子被称为原子提交问题[^xii]。 +在支持跨多节点或跨多分区事务的数据库中,一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就ACID而言,请参“[原子性](ch7.md#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止/回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为**原子提交(atomic commit)**问题[^xii]。 -[^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在所有参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 允许共识决定其中一位参与者提出的任何值。 然而,原子的承诺和共识是可以相互压缩的【70,71】。 非阻塞原子提交比共识更难——参阅“[三阶段提交](#三阶段提交)”。 +[^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在**所有**参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。 共识则允许就**任意一个**被参与者提出的候选值达成一致。 然而,原子提交和共识可以相互简化为对方【70,71】。 **非阻塞**原子提交则要比共识更为困难 —— 参阅“[三阶段提交](#三阶段提交)”。 > ### 共识的不可能性 > -> 你可能已经听说过作者Fischer,Lynch和Paterson之后的FLP结果【68】,这证明如果存在节点可能崩溃的风险,则不存在总是能够达成一致的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而,在这里,我们正在讨论达成共识的算法。这里发生了什么? +> 你可能已经听说过作者Fischer,Lynch和Paterson之后的FLP结果【68】,它证明,如果存在节点可能崩溃的风险,则不存在**总是**能够达成共识的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而这里我们正在讨论达成共识的算法,到底是怎么回事? > -> 答案是FLP结果在异步系统模型中得到了证明(参阅“[系统模型与现实](#系统模型与现实)”),这是一个非常有限的模型,它假定确定性算法不能使用任何时钟或超时。如果算法被允许使用超时或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),那么共识就变得可以解决了【67】。即使只允许算法使用随机数也足以绕过不可能的结果【69】。 +> 答案是FLP结果在**异步系统模型**中得到了证明(参阅“[系统模型与现实](ch8.md#系统模型与现实)”),这是一种限制性很强的模型,它假定确定性算法不能使用任何时钟或超时。如果允许算法使用**超时**或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),则共识变为一个可解的问题【67】。即使仅仅允许算法使用随机数,也足以绕过这个不可能的结果【69】。 > -> 因此,FLP虽然不可能达成共识,但理论上具有重要意义,但实际上分布式系统通常可以达成共识。 +> 因此,FLP是关于共识不可能性的重要理论结果,但现实中的分布式系统通常是可以达成共识的。 -在本节中,我们将首先更详细地检查原子提交问题。具体来说,我们将讨论两阶段提交(2PC)算法,这是解决原子提交最常见的方法,并在各种数据库、消息传递系统和应用服务器中实现。事实证明,2PC是一种一致的算法,但并不是一个很好的算法【70,71】。 +在本节中,我们将首先更详细地研究**原子提交**问题。具体来说,我们将讨论**两阶段提交(2PC, two-phase commit)**算法,这是解决原子提交问题最常见的办法,并在各种数据库、消息队列和应用服务器中实现。事实证明2PC是一种共识算法,但不是一个非常好的算法【70,71】。 -通过对2PC学习,我们将继续努力实现更好的一致性算法,比如ZooKeeper(Zab)和etcd(Raft)中使用的算法。 +通过对2PC的学习,我们将继续努力实现更好的一致性算法,比如ZooKeeper(Zab)和etcd(Raft)中使用的算法。 ### 原子提交与二阶段提交(2PC) -在[第7章](ch7.md)中,我们了解到事务原子性的目的是在出现几次写错的情况下提供简单的语义。事务的结果要么是成功的提交,在这种情况下,所有事务的写入都是持久的,或者中止,在这种情况下,所有事务的写入都被回滚(即撤消或丢弃)。 +在[第7章](ch7.md)中我们了解到,事务原子性的目的是在多次写操作中途出错的情况下,提供一种简单的语义。事务的结果要么是成功提交,在这种情况下,事务的所有写入都是持久化的;要么是中止,在这种情况下,事务的所有写入都被回滚(即撤消或丢弃)。 -原子性可以防止失败的事务搅乱数据库,其结果是半成品和半更新状态。这对于多对象事务(参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护二级索引的数据库尤其重要。每个辅助索引都是与主数据分离的数据结构——因此,如果你修改了一些数据,则还需要在辅助索引中进行相应的更改。原子性确保二级索引与主数据保持一致(如果索引与主数据不一致,则不会很有用)。 +原子性可以防止失败的事务搅乱数据库,避免数据库陷入半成品结果和半更新状态。这对于多对象事务(参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护次级索引的数据库尤其重要。每个辅助索引都是与主数据相分离的数据结构—— 因此,如果你修改了一些数据,则还需要在辅助索引中进行相应的更改。原子性确保二级索引与主数据保持一致(如果索引与主数据不一致,就没什么用了)。 #### 从单节点到分布式原子提交 -对于在单个数据库节点执行的事务,原子性通常由存储引擎执行。当客户端请求数据库节点提交事务时,数据库使事务的写入持久化(通常在预写式日志中:参阅“[使B树可靠](ch3.md#使B树可靠)”),然后将提交记录追加到日志中磁盘。如果数据库在这个过程中间崩溃,当节点重新启动时,事务从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;否则来自该事务的任何写入都被回滚。 +对于在单个数据库节点执行的事务,原子性通常由存储引擎实现。当客户端请求数据库节点提交事务时,数据库将使事务的写入持久化(通常在预写式日志中:参阅“[使B树可靠](ch3.md#使B树可靠)”),然后将提交记录追加到磁盘中的日志里。如果数据库在这个过程中间崩溃,当节点重启时,事务会从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;否则来自该事务的任何写入都被回滚。 -因此,在单个节点上,事务承诺主要取决于数据持久写入磁盘的顺序:首先是数据,然后是提交记录【72】。事务提交或放弃的关键决定时刻是磁盘完成写入提交记录的时刻:在此之前,仍有可能中止(由于崩溃),但在此之后,事务已经提交(即使数据库崩溃)。因此,这是一个单一的设备(一个特定的磁盘驱动器的控制器,连接到一个特定的节点),使得提交具有原子性。 +因此,在单个节点上,事务的提交主要取决于数据持久化落盘的**顺序**:首先是数据,然后是提交记录【72】。事务提交或终止的关键决定时刻是磁盘完成写入提交记录的时刻:在此之前,仍有可能中止(由于崩溃),但在此之后,事务已经提交(即使数据库崩溃)。因此,是单一的设备(连接到单个磁盘驱动的控制器,且挂载在单台机器上)使得提交具有原子性。 -但是,如果一个事务中涉及多个节点呢?例如,也许在分区数据库中有一个多对象事务,或者是一个由关键词分区的二级索引(其中索引条目可能位于与主数据不同的节点上;参阅“[分区和二级索引](ch6.md#分区和二级索引)”)。大多数“NoSQL”分布式数据存储不支持这种分布式事务,而是各种集群关系系统(请参见“[实践中的分布式事务](#实践中的分布式事务)”)。 +但是,如果一个事务中涉及多个节点呢?例如,你也许在分区数据库中会有一个多对象事务,或者是一个按关键词分区的二级索引(其中索引条目可能位于与主数据不同的节点上;参阅“[分区和二级索引](ch6.md#分区和二级索引)”)。大多数“NoSQL”分布式数据存储不支持这种分布式事务,但是很多关系型数据库集群支持(参见“[实践中的分布式事务](#实践中的分布式事务)”)。 -在这些情况下,仅向所有节点发送提交请求并且独立提交每个节点的事务是不够的。这样做很容易发生:提交在某些节点上成功,在其他节点上失败,这违反了原子性保证: +在这些情况下,仅向所有节点发送提交请求并独立提交每个节点的事务是不够的。这样很容易发生违反原子性的情况:提交在某些节点上成功,而在其他节点上失败: * 某些节点可能会检测到约束冲突或冲突,因此需要中止,而其他节点则可以成功进行提交。 * 某些提交请求可能在网络中丢失,最终由于超时而中止,而其他提交请求则通过。 * 在提交记录完全写入之前,某些节点可能会崩溃,并在恢复时回滚,而其他节点则成功提交。 -如果某些节点提交了事务,但其他节点却放弃了这些事务,那么这些节点就会彼此不一致(如[图7-3](img/fig7-3.png)所示)。而且一旦在一个节点上提交了一个事务,如果事后证明它在另一个节点上被中止,它将不能被撤回。出于这个原因,一旦确定事务中的所有其他节点也将提交,节点就必须进行提交。 +如果某些节点提交了事务,但其他节点却放弃了这些事务,那么这些节点就会彼此不一致(如 [图7-3](img/fig7-3.png) 所示)。而且一旦在某个节点上提交了一个事务,如果事后发现它在其它节点上被中止了,它是无法撤回的。出于这个原因,一旦确定事务中的所有其他节点也将提交,节点就必须进行提交。 -事务提交必须是不可撤销的——你不能改变主意,并在事务提交后追溯中止事务。这个规则的原因是,一旦数据被提交,其他事务就可以看到,因此其他客户可能会开始依赖这些数据。这个原则构成了读已提交隔离等级的基础,在“[读已提交](ch7.md#读已提交)”一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有读取提交数据的事务将基于被追溯声明不存在的数据,所以它们也必须恢复。 +事务提交必须是不可撤销的 —— 事务提交之后,你不能改变主意,并追溯性地中止事务。这个规则的原因是,一旦数据被提交,其结果就对其他事务可见,因此其他客户端可能会开始依赖这些数据。这个原则构成了**读已提交**隔离等级的基础,在“[读已提交](ch7.md#读已提交)”一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有那些读取了**已提交却又被追溯声明不存在数据**的事务也必须回滚。 -(提交事务的效果有可能后续通过另一个补偿事务取消【73,74】,但从数据库的角度来看,这是一个单独的事务,因此任何关于交叉事务正确性的保证是应用自己的问题。) +(提交事务的结果有可能通过事后执行另一个补偿事务来取消【73,74】,但从数据库的角度来看,这是一个单独的事务,因此任何关于跨事务正确性的保证都是应用自己的问题。) -#### 介绍两阶段提交 +#### 两阶段提交简介 -两阶段提交是一种用于实现跨多个节点的原子事务提交的算法,即确保所有节点提交或所有节点中止。 它是分布式数据库中的经典算法【13,35,75】。 2PC在某些数据库内部使用,并且还以XA事务【76,77】(例如Java Transaction API支持)或通过`WS-AtomicTransaction for SOAP Web`服务的形式提供给应用程序【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)说明了2PC的基本流程。 与单节点事务一样,2PC中的提交/终止进程分为两个阶段(因此而得名),而不是单个提交请求。 +[图9-9](img/fig9-9)说明了2PC的基本流程。2PC中的提交/中止过程分为两个阶段(因此而得名),而不是单节点事务中的单个提交请求。 ![](img/fig9-9.png) **图9-9 两阶段提交(2PC)的成功执行** -> #### 不要混淆2PC和2PL +> #### 不要把2PC和2PL搞混了 > -> 两阶段提交(2PC)和两阶段锁定(参阅“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)是两个完全不同的事情。 2PC在分布式数据库中提供原子提交,而2PL提供可序列化的隔离。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中的不幸的相似性。 +> 两阶段提交(2PC)和两阶段锁定(参阅“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)是两个完全不同的东西。 2PC在分布式数据库中提供原子提交,而2PL提供可序列化的隔离等级。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中不幸的相似性。 -2PC使用一个通常不会出现在单节点事务中的新组件:**协调者(coordinator)**(也称为事务管理器)。协调者通常在请求事务的相同应用程序进程(例如,嵌入在Java EE容器中)中实现为库,但也可以是单独的进程或服务。这种协调者的例子包括Narayana,JOTM,BTM或MSDTC。 +2PC使用一个通常不会出现在单节点事务中的新组件:**协调者(coordinator)**(也称为**事务管理器(transaction manager)**)。协调者通常在请求事务的相同应用程序进程中以库的形式实现(例如,嵌入在Java EE容器中),但也可以是单独的进程或服务。这种协调者的例子包括Narayana,JOTM,BTM或MSDTC。 -正常情况下,2PC事务从应用程序在多个数据库节点上读写数据开始。我们把这些数据库节点称为事务参与者。当应用程序准备提交时,协调者开始阶段1:它发送一个准备请求到每个节点,询问他们是否能够提交。协调者然后跟踪参与者的回应: +正常情况下,2PC事务以应用程序在多个数据库节点上读写数据开始。我们称这些数据库节点为**参与者(participants)**。当应用程序准备提交时,协调者开始阶段 1 :它发送一个**准备(prepare)**请求到每个节点,询问它们是否能够提交。然后协调者会跟踪参与者的响应: -* 如果所有参与者都回答“是”,表示他们已经准备好提交,那么协调者在阶段2发出**提交(commit)**请求,实际发生提交。 -* 如果任何参与者回复“否”,则协调者在阶段2中向所有节点发送**中止(abort)**请求。 +* 如果所有参与者都回答“是”,表示它们已经准备好提交,那么协调者在阶段 2 发出**提交(commit)**请求,然后提交真正发生。 +* 如果任意一个参与者回复了“否”,则协调者在阶段2 中向所有节点发送**中止(abort)**请求。 这个过程有点像西方传统婚姻仪式:司仪分别询问新娘和新郎是否要结婚,通常是从两方都收到“我愿意”的答复。收到两者的回复后,司仪宣布这对情侣成为夫妻:事务就提交了,这一幸福事实会广播至所有的参与者中。如果新娘与新郎之一没有回复”我愿意“,婚礼就会中止【73】。 #### 承诺系统 -从这个简短的描述可能不清楚为什么两阶段提交确保了原子性,而跨几个节点的一阶段提交没有。准备和提交请求当然可以在两阶段的情况下轻易地丢失。 2PC有什么不同? +这个简短的描述可能并没有说清楚为什么两阶段提交保证了原子性,而跨多个节点的一阶段提交却没有。在两阶段提交的情况下,准备请求和提交请求当然也可以轻易丢失。 2PC又有什么不同呢? 为了理解它的工作原理,我们必须更详细地分解这个过程: -1. 当应用想要开启一个分布式事务时,它向协调者请求一个事务ID。此事务ID是全局唯一的。 -2. 应用在每个参与者上开始单节点事务,并将全局唯一事务ID附到单节点事务上。所有的读写都是在这些单节点事务之一中完成的。如果在这个阶段出现任何问题(例如,节点崩溃或请求超时),则协调者或任何参与者都可以中止。 -3. 当应用程序准备提交时,协调者向所有参与者发送一个准备请求,标记为全局事务ID。如果这些请求中的任何一个失败或超时,则协调者向所有参与者发送针对该事务ID的放弃请求。 -4. 参与者收到准备请求时,确保在任何情况下都可以明确地进行事务。这包括将所有事务数据写入磁盘(出现故障,电源故障或硬盘空间不足以拒绝稍后提交)以及检查是否存在任何冲突或约束违规。通过向协调者回答“是”,节点承诺在没有错误的情况下提交事务。换句话说,参与者放弃了中止事务的权利,但没有实际提交。 -5. 当协调者收到所有准备请求的答复时,就是否提交或中止事务作出明确的决定(只有在所有参与者投赞成票的情况下才提交)。协调者必须把这个决定写到磁盘上的事务日志中,以便它知道它决定的方式,以防随后发生崩溃。这被称为**提交点(commit point)**。 -6. 一旦协调者的决定写入磁盘,提交或放弃请求被发送给所有参与者。如果此请求失败或超时,则协调者必须一直重试,直到成功为止。没有更多的事情要做,如果做出决定,那么决定必须执行,不管它需要多少次重试。如果参与者在此期间崩溃,事务将在恢复时进行——由于参与者投票“是”,因此恢复时不能拒绝提交。 +1. 当应用想要启动一个分布式事务时,它向协调者请求一个事务ID。此事务ID是全局唯一的。 +2. 应用在每个参与者上启动单节点事务,并在单节点事务上捎带上这个全局事务ID。所有的读写都是在这些单节点事务中各自完成的。如果在这个阶段出现任何问题(例如,节点崩溃或请求超时),则协调者或任何参与者都可以中止。 +3. 当应用准备提交时,协调者向所有参与者发送一个**准备**请求,并打上全局事务ID的标记。如果任意一个请求失败或超时,则协调者向所有参与者发送针对该事务ID的中止请求。 +4. 参与者收到准备请求时,需要确保在任意情况下都的确可以提交事务。这包括将所有事务数据写入磁盘(出现故障,电源故障,或硬盘空间不足都不能是稍后拒绝提交的理由)以及检查是否存在任何冲突或违反约束。通过向协调者回答“是”,节点承诺,只要请求,这个事务一定可以不出差错地提交。换句话说,参与者放弃了中止事务的权利,但没有实际提交。 +5. 当协调者收到所有准备请求的答复时,会就提交或中止事务作出明确的决定(只有在所有参与者投赞成票的情况下才会提交)。协调者必须把这个决定写到磁盘上的事务日志中,如果它随后就崩溃,恢复后也能知道自己所做的决定。这被称为**提交点(commit point)**。 +6. 一旦协调者的决定落盘,提交或放弃请求会发送给所有参与者。如果这个请求失败或超时,协调者必须永远保持重试,直到成功为止。没有回头路:如果已经做出决定,不管需要多少次重试它都必须被执行。如果参与者在此期间崩溃,事务将在其恢复后提交——由于参与者投了赞成,因此恢复后它不能拒绝提交。 -因此,该协议包含两个关键的“不归路”点:当参与者投票“是”时,它承诺它肯定能够稍后提交(尽管协调者可能仍然选择放弃)。一旦协调者决定,这个决定是不可撤销的。这些承诺保证了2PC的原子性。 (单节点原子提交将这两个事件合并为一个:将提交记录写入事务日志。) +因此,该协议包含两个关键的“不归路”点:当参与者投票“是”时,它承诺它稍后肯定能够提交(尽管协调者可能仍然选择放弃)。一旦协调者做出决定,这一决定是不可撤销的。这些承诺保证了2PC的原子性。 (单节点原子提交将这两个事件混为一谈:将提交记录写入事务日志。) -回到婚姻的比喻,在说“我是”之前,你和你的新娘/新郎有“中止”这个事务的自由,通过回复“不行!”(或者差不多效果的话)。然而,在说“我愿意”之后,你就不能收回那个声明了。如果你说“我愿意”后晕倒了,而你没有听到司仪说“你们现在是夫妻了”,那并不会改变事务已经提交的事实。当你稍后恢复意识时,你可以通过查询司仪的全局事务ID状态来查明你是否已婚,或者你可以等待司仪重试下一次提交请求(因为重试将在你无意识期间一直持续)。 +回到婚姻的比喻,在说“我是”之前,你和你的新娘/新郎有中止这个事务的自由,通过回复 “没门!”(或者有类似效果的话)。然而在说了“我愿意”之后,你就不能撤回那个声明了。如果你说“我愿意”后晕倒了,没有听到司仪说“你们现在是夫妻了”,那也并不会改变事务已经提交的现实。当你稍后恢复意识时,可以通过查询司仪的全局事务ID状态来确定你是否已经成婚,或者你可以等待司仪重试下一次提交请求(因为重试将在你无意识期间一直持续)。 #### 协调者失效 -我们已经讨论了在2PC期间如果其中一个参与者或网络发生故障会发生什么情况:如果任何一个准备请求失败或者超时,协调者就中止事务。如果任何提交或中止请求失败,协调者将无条件重试。但是,如果协调者崩溃,会发生什么情况并不太清楚。 +我们已经讨论了在2PC期间,如果参与者之一或网络发生故障时会发生什么情况:如果任何一个**准备**请求失败或者超时,协调者就会中止事务。如果任何提交或中止请求失败,协调者将无条件重试。但是如果协调者崩溃,会发生什么情况就不太清楚了。 -如果协调者在发送准备请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃——必须等待协调者回答事务是否已经发生或中止。如果此时协调者崩溃或网络出现故障,参与者只能等待。参与者在这个状态下的事务是存疑的或不确定的。 +如果协调者在发送**准备**请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了“是”,就不能再单方面放弃 —— 必须等待协调者回答事务是否已经提交或中止。如果此时协调者崩溃或网络出现故障,参与者什么也做不了只能等待。参与者的这种事务状态称为**存疑(in doubt)**的或**不确定(uncertain)**的。 -情况如[图9-10](img/fig9-10)所示。在这个特定的例子中,协调者实际上决定提交,数据库2收到提交请求。但是,协调者在将提交请求发送到数据库1之前发生崩溃,因此数据库1不知道是否提交或中止。即使超时在这里也没有帮助:如果数据库1在超时后单方面中止,它将最终与提交的数据库2不一致。同样,单方面犯也是不安全的,因为另一个参与者可能已经中止了。 +情况如[图9-10](img/fig9-10) 所示。在这个特定的例子中,协调者实际上决定提交,数据库2 收到提交请求。但是,协调者在将提交请求发送到数据库1 之前发生崩溃,因此数据库1 不知道是否提交或中止。即使**超时**在这里也没有帮助:如果数据库1 在超时后单方面中止,它将最终与执行提交的数据库2 不一致。同样,单方面提交也是不安全的,因为另一个参与者可能已经中止了。 ![](img/fig9-10.png)  **图9-10 参与者投赞成票后,协调者崩溃。数据库1不知道是否提交或中止** -没有协调者的消息,参与者无法知道是否承诺或放弃。原则上,参与者可以相互沟通,找出每个参与者如何投票并达成一致,但这不是2PC协议的一部分。 +没有协调者的消息,参与者无法知道是提交还是放弃。原则上参与者可以相互沟通,找出每个参与者是如何投票的,并达成一致,但这不是2PC协议的一部分。 -2PC可以完成的唯一方法是等待协调者恢复。这就是为什么协调者必须在向参与者发送提交或中止请求之前将其提交或中止决定写入磁盘上的事务日志:协调者恢复后,通过读取其事务日志来确定所有存疑事务的状态。任何在协调者日志中没有提交记录的事务都会中止。因此,2PC的**提交点**归结为协调者上的常规单节点原子提交。 +可以完成2PC的唯一方法是等待协调者恢复。这就是为什么协调者必须在向参与者发送提交或中止请求之前,将其提交或中止决定写入磁盘上的事务日志:协调者恢复后,通过读取其事务日志来确定所有存疑事务的状态。任何在协调者日志中没有提交记录的事务都会中止。因此,2PC的**提交点**归结为协调者上的常规单节点原子提交。 #### 三阶段提交 -两阶段提交被称为阻塞原子提交协议,因为2PC可能卡住,等待协调者恢复。理论上,可以使一个原子提交协议非阻塞,以便在节点失败时不会卡住。但在实践中这个工作并不那么简单。 +两阶段提交被称为**阻塞(blocking)**原子提交协议,因为存在2PC可能卡住并等待协调者恢复的情况。理论上,可以使一个原子提交协议变为**非阻塞(nonblocking)**的,以便在节点失败时不会卡住。但是让这个协议能在实践中工作并没有那么简单。 -作为2PC的替代方案,已经提出了一种称为三阶段提交(3PC)的算法【13,80】。然而,3PC假定一个有界延迟的网络和有限响应时间的节点;在大多数具有无限网络延迟和进程暂停的实际系统中(见[第8章](ch8.md)),它并不能保证原子性。 +作为2PC的替代方案,已经提出了一种称为**三阶段提交(3PC)**的算法【13,80】。然而,3PC假定网络延迟有界,节点响应时间有限;在大多数具有无限网络延迟和进程暂停的实际系统中(见[第8章](ch8.md)),它并不能保证原子性。 -通常,非阻塞原子提交需要一个完美的故障检测器【67,71】—— 即一个可靠的机制来判断一个节点是否已经崩溃。在无限延迟的网络中,超时不是可靠的故障检测器,因为即使没有节点崩溃,请求也可能由于网络问题而超时。出于这个原因,2PC仍然被使用,尽管大家都清楚可能存在协调者故障的问题。 +通常,非阻塞原子提交需要一个**完美的故障检测器(perfect failure detector)**【67,71】—— 即一个可靠的机制来判断一个节点是否已经崩溃。在具有无限延迟的网络中,超时并不是一种可靠的故障检测机制,因为即使没有节点崩溃,请求也可能由于网络问题而超时。出于这个原因,2PC仍然被使用,尽管大家都清楚可能存在协调者故障的问题。 @@ -678,7 +676,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 分布式事务,尤其是那些通过两阶段提交实现的事务,毁誉参半。一方面,它们被看作是提供一个难以实现的重要的安全保证;另一方面,他们被批评为造成运维问题,造成性能下降,承诺超过他们能够实现的目标【81,82,83,84】。许多云服务由于其导致的运维问题而选择不实现分布式事务【85,86】。 -分布式事务的某些实现会带来严重的性能损失——例如,MySQL中的分布式事务被报告比单节点事务慢10倍以上【87】,所以当人们建议不要使用这些事务时就不足为奇了。两阶段提交所固有的大部分性能成本是由于崩溃恢复所需的额外强制刷盘(`fsync`)【88】以及额外的网络往返。 +分布式事务的某些实现会带来严重的性能损失—— 例如,MySQL中的分布式事务被报告比单节点事务慢10倍以上【87】,所以当人们建议不要使用这些事务时就不足为奇了。两阶段提交所固有的大部分性能成本是由于崩溃恢复所需的额外强制刷盘(`fsync`)【88】以及额外的网络往返。 但是,我们不应该直接抛弃分布式事务,而应该更加仔细地审视这些事务,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明“**分布式事务**”的含义。两种截然不同的分布式事务类型经常被混淆: @@ -739,11 +737,11 @@ XA假定你的应用程序使用网络驱动程序或客户端库来与参与者 XA事务解决了保持多个参与者数据系统一致的真实而重要的问题,但正如我们所看到的那样,它们也引入了主要的运维问题。特别是,关键的实现是事务协调者本身就是一种数据库(在其中存储事务结果),因此需要像其他重要数据库一样小心: * 如果协调者没有被复制,而是只在一台机器上运行,那么整个系统是一个失效的单点(因为它的失效会导致其他应用程序服务器阻塞在有问题的事务处理的锁上)。令人惊讶的是,许多协调者实现默认情况下不是高度可用,或者只有基本的复制支持。 -* 许多服务器端应用程序都是在无状态模式下开发的(受到HTTP的青睐),所有持久状态都存储在数据库中,具有应用程序服务器可随意添加和删除的优点。但是,当协调者是应用程序服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分——与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复疑问事务所必需的。这样的应用程序服务器不再是无状态的。 +* 许多服务器端应用程序都是在无状态模式下开发的(受到HTTP的青睐),所有持久状态都存储在数据库中,具有应用程序服务器可随意添加和删除的优点。但是,当协调者是应用程序服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分—— 与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复疑问事务所必需的。这样的应用程序服务器不再是无状态的。 * 由于XA需要与各种数据系统兼容,因此它是必须的最小公分母。例如,它不能检测到不同系统间的死锁(因为这将需要一个标准化的协议来让系统交换每个事务正在等待的锁的信息),而且它不适用于[SSI](ch7.md#可串行快照隔离(SSI) ),因为这需要一个协议来识别不同系统之间的冲突。 * 对于数据库内部的分布式事务(而不是XA),限制不是很大,例如SSI的分布式版本是可能的。然而,仍然存在2PC成功进行事务的问题,所有参与者都必须作出回应。因此,如果系统的任何部分损坏,事务也会失败。因此,分布式事务有扩大故障的趋势,这与我们构建容错系统的目标背道而驰。 -这些事实是否意味着我们应该放弃保持几个系统一致的所有希望?不完全是——有其他的方法可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第十一章](ch11.md)和[第十二章](ch12.md)回到这些章节。但首先,我们应该总结共识的话题。 +这些事实是否意味着我们应该放弃保持几个系统一致的所有希望?不完全是——有其他的方法可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第11章](ch11.md) 和[第12章](ch12.md) 回到这些章节。但首先,我们应该总结共识的话题。 @@ -784,7 +782,7 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 因此,终止属性受到不到一半的节点崩溃或不可达的假设。然而,即使大多数节点出现故障或存在严重的网络问题,大多数共识的实施都能确保始终满足安全属性——同意,完整性和有效性【92】。因此,大规模的中断可能会阻止系统处理请求,但是它不能通过使系统做出无效的决定来破坏共识系统。 -大多数一致性算法假定没有**拜占庭式错误**,正如在“[拜占庭式错误](#拜占庭式错误)”一节中所讨论的那样。也就是说,如果一个节点没有正确地遵循协议(例如,如果它发送矛盾的消息到不同的节点),它可能会破坏协议的安全属性。只要少于三分之一的节点是拜占庭故障【25,93】,就可以对拜占庭故障形成共识,但我们没有地方在本书中详细讨论这些算法。 +大多数一致性算法假定没有**拜占庭式错误**,正如在“[拜占庭式错误](#拜占庭式错误)”一节中所讨论的那样。也就是说,如果一个节点没有正确地遵循协议(例如,如果它发送矛盾的消息到不同的节点),它可能会破坏协议的安全属性。只要少于三分之一的节点是拜占庭故障【25,93】,就可以对拜占庭故障达成共识,但我们没有地方在本书中详细讨论这些算法。 #### 共识算法和全序广播 @@ -792,7 +790,7 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 这些算法中的大多数实际上并不直接使用这里描述的形式化模型(建议和决定单个值,一致同意,完整性,有效性和终止性质)。相反,它们决定了值的顺序,这使得它们成为了全序广播算法,正如本章前面所讨论的那样(参阅“[全序广播](#全序广播)”)。 -请记住,全序广播要求将消息按照相同的顺序准确传送到所有节点。如果仔细思考,这相当于进行了几轮的共识:在每一轮中,节点提出下一个要发送的消息,然后决定下一个要发送的消息总数【67】。 +请记住,全序广播要求将消息按照相同的顺序准确传送到所有节点。如果仔细思考,这相当于进行了几轮共识:在每一轮中,节点提出下一个要发送的消息,然后决定下一个要发送的消息总数【67】。 所以,全序广播相当于重复的一轮共识(每个共同的决定对应于一个消息传递): @@ -809,7 +807,7 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 在[第5章](ch5.md)中,我们讨论了单领导者复制(参见“[领导者和追随者](ch5.md#领导者和追随者)”),它将所有的写入操作都交给领导者,并以相同的顺序将他们应用到追随者,从而使副本保持最新状态。这不是基本上全序广播?我们怎么不用担心[第五章](ch5.md)的共识? -答案取决于如何选择领导者。如果领导人是由你的运营团队中的人员手动选择和配置的,那么你基本上拥有独裁种类的“一致性算法”:只允许一个节点接受写入(即,决定写入的顺序复制日志),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为主管。这样的制度在实践中可以很好地发挥作用,但是不能达到共识的终止属性,因为它需要人为干预才能取得进展。 +答案取决于如何选择领导者。如果领导人是由你的运维团队人员手动选择和配置的,那么你基本上拥有独裁类型的“一致性算法”:只允许一个节点接受写入(即,决定写入的顺序复制日志),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为领导者。这样的制度在实践中效果很好,但是无法达到共识的**终止**属性,因为它需要人为干预才能取得进展。 一些数据库执行自动领导者选举和故障转移,如果旧领导者失败,则促使追随者成为新的领导者(参见“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们更接近容错的全面命令播出,从而达成共识。 @@ -839,13 +837,13 @@ XA事务解决了保持多个参与者数据系统一致的真实而重要的问 节点在决定之前对节点进行投票的过程是一种同步复制。如“[同步与异步复制](ch5.md#同步与异步复制)”中所述,通常将数据库配置为使用异步复制。在这种配置中,一些承诺的数据在故障转移时可能会丢失——但是为了获得更好的性能,许多人选择接受这种风险。 -共识体系总是需要严格的多数来操作。这意味着你至少需要三个节点才能容忍一个故障(其余三个为大多数),或者至少有五个节点容忍两个故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另参阅“[线性一致性的代价](#线性一致性的代价)”(第295页))。 +共识体系总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余三个为大多数),或者至少有五个节点来容忍两个节点发生故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另参阅“[线性一致性的代价](#线性一致性的代价)”)。 大多数一致性算法假定一组参与投票的节点,这意味着你不能只添加或删除集群中的节点。对共识算法的动态成员扩展允许集群中的节点集随着时间的推移而变化,但是它们比静态成员算法要好得多。 共识系统通常依靠超时来检测失败的节点。在网络延迟高度变化的环境中,特别是在地理上分布的系统中,经常发生一个节点错误地认为由于暂时的网络问题,导致失败的原因。虽然这个错误不会损害安全属性,但频繁的领导者选举会导致糟糕的表现,因为系统最终会花费更多的时间来选择领导者而不是做任何有用的工作。 -有时,共识算法对网络问题特别敏感。例如,Raft已被证明有不愉快的边缘情况【106】:如果整个网络工作正常,除了一个特定的网络连接一直不可靠,Raft可以进入领导层不断在两个节点之间弹跳的情况,或者目前的领导者不断被迫辞职,所以这个制度从来没有取得进展。其他一致性算法也存在类似的问题,而对不可靠网络更具鲁棒性的设计算法仍然是一个开放的研究问题。 +有时共识算法对网络问题特别敏感。例如Raft已被证明存在不愉快的边缘情况【106】:如果整个网络工作正常,除了一个特定的网络连接一直不可靠,Raft可以进入领导层不断在两个节点之间弹跳的情况,或者目前的领导者不断被迫辞职,所以这个制度从来没有取得进展。其他一致性算法也存在类似的问题,而对不可靠网络更具鲁棒性的设计算法仍然是一个开放的研究问题。 ### 成员与协调服务 @@ -859,7 +857,7 @@ ZooKeeper模仿Google的Chubby锁定服务【14,98】,不仅实现了全序广 ***线性一致性的原子操作*** -使用原子比较和设置操作,可以实现锁定:如果多个节点同时尝试执行相同的操作,则只有其中一个节点会成功。共识协议保证了操作将是原子性和线性一致性的,即使节点发生故障或网络在任何时候都被中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(参阅“[进程暂停](ch8.md#进程暂停)”)。 +使用原子CAS操作可以实现锁:如果多个节点同时尝试执行相同的操作,则只有一个节点会成功。共识协议保证了操作的原子性和线性一致性,即使节点发生故障或网络在任意时刻中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(参阅“[进程暂停](ch8.md#进程暂停)”)。 ***操作的全序*** @@ -883,11 +881,11 @@ ZooKeeper/Chubby模型运行良好的一个例子是,如果你有几个流程 另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式参与者系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入群集时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(参阅“[重新平衡分区](ch6.md#重新平衡分区)”)。当节点被移除或失败时,其他节点需要接管失败节点的工作。 -这些类型的任务可以通过在ZooKeeper中明智地使用原子操作,各种节点和通知来实现。如果正确完成,这种方法允许应用程序自动从故障中恢复,无需人工干预。尽管Apache Curator 【17】等库已经出现在ZooKeeper客户端API的顶层提供了更高级别的工具,但这样做并不容易,但它仍然比尝试从头开始实现必要的一致性算法要好得多,成绩不佳【107】。 +这些类型的任务可以通过在ZooKeeper中明智地使用原子操作,各种节点和通知来实现。如果正确完成,这种方法允许应用程序自动从故障中恢复,无需人工干预。尽管Apache Curator 【17】等库已经出现在ZooKeeper客户端API的顶层提供了更高层的工具,但这样做并不容易,但它仍然比尝试从头开始实现必要的一致性算法要好得多,成绩不佳【107】。 -应用最初只能在单个节点上运行,但最终可能会增长到数千个节点。试图在如此之多的节点上进行多数选票将是非常低效的。相反,ZooKeeper在固定数量的节点(通常是三到五个)上运行,并在这些节点之间执行其多数票,同时支持潜在的大量客户端。因此,ZooKeeper提供了一种将协调节点(共识,操作排序和故障检测)的一些工作“外包”到外部服务的方式。 +应用最初只能在单个节点上运行,但最终可能会增长到数千个节点。试图在如此之多的节点上进行多数投票将是非常低效的。相反,ZooKeeper在固定数量的节点(通常是三到五个)上运行,并在这些节点之间执行其多数票,同时支持潜在的大量客户端。因此,ZooKeeper提供了一种将协调节点(共识,操作排序和故障检测)的一些工作“外包”到外部服务的方式。 -通常,由ZooKeeper管理的数据的类型变化十分缓慢:代表“分区7中的节点运行在`10.1.1.23`上”的信息可能会在几分钟或几小时的时间内发生变化。它不是用来存储应用程序的运行时状态的,每秒可能会改变数千甚至数百万次。如果应用程序状态需要从一个节点复制到另一个节点,则可以使用其他工具(如Apache BookKeeper 【108】)。 +通常,由ZooKeeper管理的数据的类型变化十分缓慢:代表“分区 7 中的节点运行在 `10.1.1.23` 上”的信息可能会在几分钟或几小时的时间内发生变化。它不是用来存储应用程序的运行时状态的,每秒可能会改变数千甚至数百万次。如果应用程序状态需要从一个节点复制到另一个节点,则可以使用其他工具(如Apache BookKeeper 【108】)。 #### 服务发现 @@ -903,7 +901,7 @@ ZooKeeper和它的小伙伴们可以看作是成员服务研究的悠久历史 成员资格服务确定哪些节点当前处于活动状态并且是群集的活动成员。正如我们在[第8章](ch8.md)中看到的那样,由于无限的网络延迟,无法可靠地检测到另一个节点是否发生故障。但是,如果你通过一致的方式进行故障检测,那么节点可以就哪些节点应该被认为是存在或不存在达成一致。 -即使它确实存在,仍然可能发生一个节点被错误地宣布死于共识。但是对于一个系统来说,在哪些节点构成当前的成员关系方面是非常有用的。例如,选择领导者可能意味着简单地选择当前成员中编号最小的成员,但如果不同的节点对现有成员的成员有不同意见,则这种方法将不起作用。 +即使它确实存在,仍然可能发生一个节点被共识错误地宣告死亡。但是对于一个系统来说,在哪些节点构成当前的成员关系方面是非常有用的。例如,选择领导者可能意味着简单地选择当前成员中编号最小的成员,但如果不同的节点对现有成员的成员有不同意见,则这种方法将不起作用。 diff --git a/img/ddia-wexin.JPG b/img/ddia-wexin.JPG index 0e114085c96c8284675a0337e088f311412a4506..ade3f3d39c3d56a9b5501aaabaede814146d3a4e 100644 GIT binary patch delta 75091 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 delta 76245 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 C^8Hr; From 849eefcc24d30ccaa3e1c3e3683836ecb0be1df9 Mon Sep 17 00:00:00 2001 From: Vonng Date: Fri, 30 Mar 2018 02:05:28 +0800 Subject: [PATCH 09/17] ch9 90% --- ch9.md | 104 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/ch9.md b/ch9.md index 93312e9..4e27ac6 100644 --- a/ch9.md +++ b/ch9.md @@ -630,7 +630,7 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 这个过程有点像西方传统婚姻仪式:司仪分别询问新娘和新郎是否要结婚,通常是从两方都收到“我愿意”的答复。收到两者的回复后,司仪宣布这对情侣成为夫妻:事务就提交了,这一幸福事实会广播至所有的参与者中。如果新娘与新郎之一没有回复”我愿意“,婚礼就会中止【73】。 -#### 承诺系统 +#### 系统承诺 这个简短的描述可能并没有说清楚为什么两阶段提交保证了原子性,而跨多个节点的一阶段提交却没有。在两阶段提交的情况下,准备请求和提交请求当然也可以轻易丢失。 2PC又有什么不同呢? @@ -674,127 +674,127 @@ CAP定理的正式定义仅限于很狭隘的范围【30】,它只考虑了一 ### 实践中的分布式事务 -分布式事务,尤其是那些通过两阶段提交实现的事务,毁誉参半。一方面,它们被看作是提供一个难以实现的重要的安全保证;另一方面,他们被批评为造成运维问题,造成性能下降,承诺超过他们能够实现的目标【81,82,83,84】。许多云服务由于其导致的运维问题而选择不实现分布式事务【85,86】。 +分布式事务的名声毁誉参半,尤其是那些通过两阶段提交实现的。一方面,它被视作提供了一个难以实现的重要的安全性保证;另一方面,它们因为导致运维问题,造成性能下降,做出超过能力范围的承诺而饱受批评【81,82,83,84】。许多云服务由于其导致的运维问题,而选择不实现分布式事务【85,86】。 -分布式事务的某些实现会带来严重的性能损失—— 例如,MySQL中的分布式事务被报告比单节点事务慢10倍以上【87】,所以当人们建议不要使用这些事务时就不足为奇了。两阶段提交所固有的大部分性能成本是由于崩溃恢复所需的额外强制刷盘(`fsync`)【88】以及额外的网络往返。 +分布式事务的某些实现会带来严重的性能损失 —— 例如据报告称,MySQL中的分布式事务比单节点事务慢10倍以上【87】,所以当人们建议不要使用它们时就不足为奇了。两阶段提交所固有的性能成本,大部分是由于崩溃恢复所需的额外强制刷盘(`fsync`)【88】以及额外的网络往返。 -但是,我们不应该直接抛弃分布式事务,而应该更加仔细地审视这些事务,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明“**分布式事务**”的含义。两种截然不同的分布式事务类型经常被混淆: +但我们不应该直接忽视分布式事务,而应当更加仔细地审视这些事务,因为从中可以汲取重要的经验教训。首先,我们应该精确地说明“**分布式事务**”的含义。两种截然不同的分布式事务类型经常被混淆: ***数据库内部的分布式事务*** -一些分布式数据库(即在其标准配置中使用复制和分区的数据库)支持该数据库节点之间的内部事务。例如,VoltDB和MySQL Cluster的NDB存储引擎就有这样的内部事务支持。在这种情况下,所有参与事务的节点都运行相同的数据库软件。 +一些分布式数据库(即在其标准配置中使用复制和分区的数据库)支持数据库节点之间的内部事务。例如,VoltDB和MySQL Cluster的NDB存储引擎就有这样的内部事务支持。在这种情况下,所有参与事务的节点都运行相同的数据库软件。 ***异构分布式事务*** -在异构事务中,参与者有两种或两种以上不同的技术:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。 +在**异构(heterogeneous)**事务中,参与者是两种或以上不同技术:例如来自不同供应商的两个数据库,甚至是非数据库系统(如消息代理)。跨系统的分布式事务必须确保原子提交,尽管系统可能完全不同。 -数据库内部事务不必与任何其他系统兼容,因此他们可以使用任何协议并应用特定技术的特定优化。因此,数据库内部的分布式事务通常工作地很好。另一方面,跨越异构技术的事务则更有挑战性。 +数据库内部事务不必与任何其他系统兼容,因此它们可以使用任何协议,并能针对特定技术进行特定的优化。因此数据库内部的分布式事务通常工作地很好。另一方面,跨异构技术的事务则更有挑战性。 #### 恰好一次的消息处理 -异构的分布式事务处理能够以强大的方式集成不同的系统。例如,当且仅当用于处理消息的数据库事务处理时,来自消息队列的消息才能被确认为已处理成功承诺。这是通过自动提交消息确认和数据库写入单个事务来实现的。使用分布式事务支持,即使消息代理和数据库是在不同机器上运行的两个不相关技术,也是可能的。 +异构的分布式事务处理能够以强大的方式集成不同的系统。例如:消息队列中的一条消息可以被确认为已处理,当且仅当用于处理消息的数据库事务成功提交。这是通过在同一个事务中原子提交**消息确认**和**数据库写入**两个操作来实现的。藉由分布式事务的支持,即使消息代理和数据库是在不同机器上运行的两种不相关的技术,这种操作也是可能的。 -如果消息传递或数据库事务失败,两者都会中止,因此消息代理可能会稍后安全地重新传递消息。因此,通过自动提交消息及其处理的副作用,即使在成功之前需要几次重试,也可以确保消息被有效处理一次。中止放弃部分完成的事务的任何副作用。 +如果消息传递或数据库事务任意一者失败,两者都会中止,因此消息代理可能会在稍后安全地重传消息。因此,通过原子提交**消息处理及其副作用**,即使在成功之前需要几次重试,也可以确保消息被**有效地(effectively)**恰好处理一次。中止会抛弃部分完成事务所导致的任何副作用。 -这样的分布式事务只有在所有受事务影响的系统都能够使用相同的原子提交协议的情况下才是可能的。例如,处理消息的副作用是发送邮件,而邮件服务器不支持两阶段提交:如果邮件处理失败并重试,可能会发送两次或更多次的邮件。但是,如果处理消息的所有副作用在事务中止时回滚,那么可以安全地重新尝试处理步骤,就好像什么都没发生过一样。 +然而,只有当所有受事务影响的系统都使用同样的**原子提交协议(atomic commit protocl)**时,这样的分布式事务才是可能的。例如,假设处理消息的副作用是发送一封邮件,而邮件服务器并不支持两阶段提交:如果消息处理失败并重试,则可能会发送两次或更多次的邮件。但如果处理消息的所有副作用都可以在事务中止时回滚,那么这样的处理流程就可以安全地重试,就好像什么都没有发生过一样。 -我们将在[第11章](ch11.md)中再次回到”恰好一次“消息处理的主题。让我们首先看看允许这种异构分布式事务的原子提交协议。 +在[第11章](ch11.md)中将再次回到”恰好一次“消息处理的主题。让我们先来看看允许这种异构分布式事务的原子提交协议。 #### XA事务 -*X/Open XA*(**扩展架构(eXtended Architecture)**的缩写)是跨异构技术实现两阶段提交的标准【76,77】。它于1991年推出并得到了广泛的实施:许多传统关系数据库(包括PostgreSQL,MySQL,DB2,SQL Server和Oracle)和消息代理(包括ActiveMQ,HornetQ,MSMQ和IBM MQ) 。 +*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事务API(JTA)则由许多用于使用Java数据库连接(JDBC)的数据库驱动程序以及使用Java消息服务(JMS)API。 +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以查明操作是否应该是分布式事务的一部分——如果是,则将必要的信息发送到数据库服务器。驱动还会提供回调,协调者可以通过回调来要求参与者准备,提交或中止。 +XA假定你的应用使用网络驱动或客户端库来与**参与者**进行通信(数据库或消息服务)。如果驱动支持XA,则意味着它会调用XA API 以查明操作是否为分布式事务的一部分 —— 如果是,则将必要的信息发往数据库服务器。驱动还会向协调者暴露回调接口,协调者可以通过回调来要求参与者准备,提交或中止。 -事务协调者实现XA API。标准没有指定应该如何实现,但实际上,协调者通常只是一个库,与发出事务的应用程序(不是单独的服务)一起被加载到相同的进程中。它跟踪事务的参与者,在要求他们准备(通过回调驱动程序)之后收集参与者的回答,并使用本地磁盘上的日志记录每次事务的提交/中止决定。 +事务协调者需要实现XA API。标准没有指明应该如何实现,但实际上协调者通常只是一个库,被加载到发起事务的应用的同一个进程中(而不是单独的服务)。它在事务中个跟踪所有的参与者,并在要求它们**准备**之后收集参与者的响应(通过驱动回调),并使用本地磁盘上的日志记录每次事务的决定(提交/中止)。 -如果应用程序进程崩溃,或者运行应用程序的机器死亡,协调者就会使用它。然后任何有准备但未提交的事务的参与者都被怀疑。由于协调程序的日志位于应用程序服务器的本地磁盘上,因此必须重新启动该服务器,并且协调程序库必须读取日志以恢复每个事务的提交/中止结果。只有这样,协调者才能使用数据库驱动程序的XA回调来要求参与者提交或中止。数据库服务器不能直接联系协调者,因为所有通信都必须通过其客户端库。 +如果应用进程崩溃,或者运行应用的机器报销了,协调者也随之往生极乐。然后任何带有**准备了**但未提交事务的参与者都会在疑虑中卡死。由于协调程序的日志位于应用服务器的本地磁盘上,因此必须重启该服务器,且协调程序库必须读取日志以恢复每个事务的提交/中止结果。只有这样,协调者才能使用数据库驱动的XA回调来要求参与者提交或中止。数据库服务器不能直接联系协调者,因为所有通信都必须通过客户端库。 #### 怀疑时持有锁 -为什么我们非常关心存疑事务?系统的其他部分不能继续工作,而忽视最终将被清理的有问题的事务吗? +为什么我们这么关心存疑事务?系统的其他部分就不能继续正常工作,无视那些终将被清理的存疑事务吗? -问题在于**锁**。正如在“[读已提交](ch7.md#读已提交)”中所讨论的那样,数据库事务通常对其修改的行进行行级别的排他锁定,以防止脏写。此外,如果要使用可序列化的隔离等级,则使用两阶段锁定的数据库也必须对事务读取的任何行执行共享锁定(参见“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)。 +问题在于**锁(locking)**。正如在“[读已提交](ch7.md#读已提交)”中所讨论的那样,数据库事务通常获取待修改的行上的**行级排他锁**,以防止脏写。此外,如果要使用可序列化的隔离等级,则使用两阶段锁定的数据库也必须为事务所读取的行加上共享锁(参见“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”)。 -在事务提交或中止之前,数据库不能释放这些锁(如[图9-9](img/fig9-9.png)中的阴影区域所示)。因此,在使用两阶段提交时,事务必须在整个时间内保持锁定状态。如果协调者已经崩溃,需要20分钟才能重新启动,这些锁将会持有20分钟。如果协调者的日志由于某种原因完全丢失,这些锁将永久保存,或者至少在管理员手动解决该情况之前。 +在事务提交或中止之前,数据库不能释放这些锁(如[图9-9](img/fig9-9.png)中的阴影区域所示)。因此,在使用两阶段提交时,事务必须在整个存疑期间持有这些锁。如果协调者已经崩溃,需要20分钟才能重启,那么这些锁将会被持有20分钟。如果协调者的日志由于某种原因彻底丢失,这些锁将被永久持有 —— 或至少在管理员手动解决该情况之前。 -当这些锁被保留时,其他事务不能修改这些行。根据数据库的不同,其他事务甚至可能被阻止读取这些行。因此,其他事务不能简单地继续他们的业务 - 如果他们想访问相同的数据,他们将被阻止。这可能会导致大部分应用程序变得不可用,直到有问题的事务得到解决。 +当这些锁被持有时,其他事务不能修改这些行。根据数据库的不同,其他事务甚至可能因为读取这些行而被阻塞。因此,其他事务没法儿简单地继续它们的业务了 —— 如果它们要访问同样的数据,就会被阻塞。这可能会导致应用大面积进入不可用状态,直到存疑事务被解决。 #### 从协调者故障中恢复 -理论上,如果协调者崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何有问题的事务。然而,在实践中,孤立的不确定事务确实发生【89,90】,也就是说,协调者不能以任何理由决定结果的事务(例如,因为事务日志已经由于软件错误)。这些事务不能自动解决,所以他们永远待在数据库中,持有锁并阻止其他事务。 +理论上,如果协调者崩溃并重新启动,它应该干净地从日志中恢复其状态,并解决任何存疑事务。然而在实践中,**孤立(orphaned)**的存疑事务确实会出现【89,90】,即无论出于何种理由,协调者无法确定事务的结果(例如事务日志已经由于软件错误丢失或损坏)。这些事务无法自动解决,所以它们永远待在数据库中,持有锁并阻塞其他事务。 -即使重新启动数据库服务器也不能解决这个问题,因为2PC的正确实现必须在重新启动时保留一个有问题的事务的锁(否则就会冒有违反原子性保证的风险)。这是一个棘手的情况。 +即使重启数据库服务器也无法解决这个问题,因为在2PC的正确实现中,即使重启也必须保留存疑事务的锁(否则就会冒有违反原子性保证的风险)。这是一种棘手的情况。 -唯一的出路是让管理员手动决定是提交还是回滚事务。管理员必须检查每个有问题的事务的参与者,确定是否有任何参与者已经提交或中止,然后将相同的结果应用于其他参与者。解决这个问题潜在地需要大量的人力,并且可能发生在严重的生产中断期间(否则,为什么协调者处于这样一个糟糕的状态),很可能需要在高压力和时间压力下完成。 +唯一的出路是让管理员手动决定提交还是回滚事务。管理员必须检查每个存疑事务的参与者,确定是否有任何参与者已经提交或中止,然后将相同的结果应用于其他参与者。解决这个问题潜在地需要大量的人力,并且可能发生在严重的生产中断期间(不然为什么协调者处于这种糟糕的状态),并很可能要在巨大精神压力和时间压力下完成。 -许多XA的实现都有一个叫做**启发式决策(heuristic decistions)**的紧急逃生舱口:允许参与者单方面决定放弃或提交一个有疑问的事务,而不需要协调者做出明确的决定【76,77,91】。要清楚的是,这里的启发式是可能破坏原子性的委婉说法,因为它违背了两阶段承诺的承诺体系。因此,启发式决策只是为了逃出灾难性的情况,而不是经常性的使用。 +许多XA的实现都有一个叫做**启发式决策(heuristic decistions)**的紧急逃生舱口:允许参与者单方面决定放弃或提交一个存疑事务,而无需协调者做出最终决定【76,77,91】。要清楚的是,这里**启发式**是**可能破坏原子性(probably breaking atomicity)**的委婉说法,因为它违背了两阶段提交的系统承诺。因此,启发式决策只是为了逃出灾难性的情况而准备的,而不是为了日常使用的。 #### 分布式事务的限制 -XA事务解决了保持多个参与者数据系统一致的真实而重要的问题,但正如我们所看到的那样,它们也引入了主要的运维问题。特别是,关键的实现是事务协调者本身就是一种数据库(在其中存储事务结果),因此需要像其他重要数据库一样小心: +XA事务解决了保持多个参与者(数据系统)相互一致的现实的重要问题,但正如我们所看到的那样,它也引入了严重的运维问题。特别来讲,这里的核心认识是:事务协调者本身就是一种数据库(存储了事务的结果),因此需要像其他重要数据库一样小心地打交道: -* 如果协调者没有被复制,而是只在一台机器上运行,那么整个系统是一个失效的单点(因为它的失效会导致其他应用程序服务器阻塞在有问题的事务处理的锁上)。令人惊讶的是,许多协调者实现默认情况下不是高度可用,或者只有基本的复制支持。 -* 许多服务器端应用程序都是在无状态模式下开发的(受到HTTP的青睐),所有持久状态都存储在数据库中,具有应用程序服务器可随意添加和删除的优点。但是,当协调者是应用程序服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分—— 与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复疑问事务所必需的。这样的应用程序服务器不再是无状态的。 -* 由于XA需要与各种数据系统兼容,因此它是必须的最小公分母。例如,它不能检测到不同系统间的死锁(因为这将需要一个标准化的协议来让系统交换每个事务正在等待的锁的信息),而且它不适用于[SSI](ch7.md#可串行快照隔离(SSI) ),因为这需要一个协议来识别不同系统之间的冲突。 -* 对于数据库内部的分布式事务(而不是XA),限制不是很大,例如SSI的分布式版本是可能的。然而,仍然存在2PC成功进行事务的问题,所有参与者都必须作出回应。因此,如果系统的任何部分损坏,事务也会失败。因此,分布式事务有扩大故障的趋势,这与我们构建容错系统的目标背道而驰。 +* 如果协调者没有复制,而是只在单台机器上运行,那么它是整个系统的失效单点(因为它的失效会导致其他应用服务器阻塞在存疑事务持有的锁上)。令人惊讶的是,许多协调者实现默认情况下并不是高可用的,或者只有基本的复制支持。 +* 许多服务器端应用都是使用无状态模式开发的(受HTTP的青睐),所有持久状态都存储在数据库中,因此具有应用服务器可随意按需添加删除的优点。但是,当协调者成为应用服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分—— 与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复存疑事务所必需的。这样的应用服务器不再是无状态的了。 +* 由于XA需要兼容各种数据系统,因此它必须是所有系统的最小公分母。例如,它不能检测不同系统间的死锁(因为这将需要一个标准协议来让系统交换每个事务正在等待的锁的信息),而且它无法与[SSI ](ch7.md#可串行快照隔离(SSI) )协同工作,因为这需要一个跨系统定位冲突的协议。 +* 对于数据库内部的分布式事务(不是XA),限制没有这么大,例如,分布式版本的SSI 是可能的。然而仍然存在问题:2PC成功提交一个事务需要所有参与者的响应。因此,如果系统的**任何**部分损坏,事务也会失败。因此,分布式事务又有**扩大失效(amplifying failures)**的趋势,这又与我们构建容错系统的目标背道而驰。 -这些事实是否意味着我们应该放弃保持几个系统一致的所有希望?不完全是——有其他的方法可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第11章](ch11.md) 和[第12章](ch12.md) 回到这些章节。但首先,我们应该总结共识的话题。 +这些事实是否意味着我们应该放弃保持几个系统相互一致的所有希望?不完全是 —— 还有其他的办法,可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在[第11章](ch11.md) 和[第12章](ch12.md) 回到这些章节。但首先,我们应该概括一下关于**共识**的话题。 -### 容错的共识 +### 容错共识 -非正式地,共识意味着让几个节点达成一致。例如,如果有几个人同时尝试预订飞机上的最后一个座位或剧院中的同一个座位,或者尝试使用相同的用户名注册一个帐户,则可以使用一个一致的算法来确定哪个其中一个互不相容的行动应该是赢家。 +非正式地,共识意味着让几个节点就某事达成一致。例如,如果有几个人**同时(concurrently)**尝试预订飞机上的最后一个座位,或剧院中的同一个座位,或者尝试使用相同的用户名注册一个帐户。共识算法可以用来确定这些**互不相容(mutually incompatible)**的操作中,哪一个才是赢家。 -共识问题通常形式化如下:一个或多个节点可以**提出(propose)**某个值,而共识算法**决定(decides)**采用其中的一个值。在座位预订的例子中,当几个顾客同时试图购买最后一个座位时,处理顾客请求的每个节点可以提出正在服务的顾客的ID,且决定指明了哪个顾客获得座位。 +共识问题通常形式化如下:一个或多个节点可以**提议(propose)**某些值,而共识算法**决定(decides)**采用其中的某个值。在座位预订的例子中,当几个顾客同时试图订购最后一个座位时,处理顾客请求的每个节点可以**提议**正在服务的顾客的ID,而**决定**指明了哪个顾客获得了座位。 -在这种形式中,共识算法必须满足以下性质【25】:[^xiii] +在这种形式下,共识算法必须满足以下性质【25】:[^xiii] -[^xiii]: 这种共识的特殊形式被称为统一共识,相当于在具有不可靠故障检测器的异步系统中的常规共识【71】。学术文献通常指的是过程而不是节点,但我们在这里使用节点来与本书的其余部分保持一致。 +[^xiii]: 这种共识的特殊形式被称为**统一共识(uniform consensus)**,相当于在具有不可靠故障检测器的异步系统中的**常规共识(regular consensus)**【71】。学术文献通常指的是**进程(process)**而不是节点,但我们在这里使用**节点(node)**来与本书的其余部分保持一致。 -***一致同意*** +***一致同意(Uniform agreement)*** ​ 没有两个节点的决定不同。 -***完整性*** +***完整性(Integrity)*** ​ 没有节点决定两次。 -***有效性*** +***有效性(Validity)*** -​ 如果一个节点决定值 `v`,则`v`由某个节点提出。 +​ 如果一个节点决定了值 `v` ,则 `v` 由某个节点所提议。 -***终止*** +***终止(Termination)*** 由所有未崩溃的节点来最终决定值。 -一致同意和完整性属性定义了共识的核心思想:每个人都决定了相同的结果,一旦决定了,你就不能改变主意。有效性属性主要是为了排除平凡的解决方案:例如,无论提出什么建议,都可以有一个总是决定为空的算法;该算法将满足协议和完整性属性,但不符合有效性属性。 +**一致同意**和**完整性**属性定义了共识的核心思想:所有人都决定了相同的结果,一旦决定了,你就不能改变主意。**有效性**属性主要是为了排除平凡的解决方案:例如,无论提议了什么值,你都可以有一个始终决定值为`null`的算法。;该算法满足**一致同意**和**完整性**属性,但不满足**有效性**属性。 -如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但是,如果该节点失效,那么系统就不能再做出任何决定。事实上,这就是我们在两阶段提交的情况下所看到的:如果协调者失败了,那么不确定的参与者就不能决定是否提交或中止。 +如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为“独裁者”,并让该节点做出所有的决定。但如果该节点失效,那么系统就无法再做出任何决定。事实上,这就是我们在两阶段提交的情况中所看到的:如果协调者失效,那么存疑的参与者就无法决定提交还是中止。 -终止属性正式形成了容错的思想。它基本上说的是,一个共识算法不能简单地坐下来,永远不做任何事——换句话说,它必须取得进展。即使有些节点出现故障,其他节点也必须做出决定。 (终止是一种**活性属性**,而另外三种是安全属性——参见“[安全性和活性](ch8.md#安全性和活性)”。) +**终止**属性正式形成了容错的思想。它实质上说的是,一个共识算法不能简单地永远闲坐着等死 —— 换句话说,它必须取得进展。即使部分节点出现故障,其他节点也必须达成一项决定。 (**终止**是一种**活性属性**,而另外三种是安全属性 —— 参见“[安全性和活性](ch8.md#安全性和活性)”。) -共识的系统模型假设,当一个节点“崩溃”时,它突然消失且永远不会回来。(而不是软件崩溃,想象一下地震,包含你的节点的数据中心被山体滑坡所摧毁,你必须假设节点被埋在30英尺以下的泥土中,并且永远不会回到在线状态。)这个系统模型,任何等待节点恢复的算法都不能满足终止属性。特别是2PC不符合终止的要求。 +共识的系统模型假设,当一个节点“崩溃”时,它会突然消失而且永远不会回来。(不像软件崩溃,想象一下地震,包含你的节点的数据中心被山体滑坡所摧毁,你必须假设节点被埋在30英尺以下的泥土中,并且永远不会重新上线)在这个系统模型中,任何需要等待节点恢复的算法都不能满足**终止**属性。特别是,2PC不符合终止属性的要求。 -当然,如果所有的节点都崩溃,而且没有一个正在运行,那么任何算法都不可能决定什么。算法可以容忍的失败次数有一个限制:事实上,可以证明任何一致性算法都需要至少大部分节点正确运行,以确保终止属性【67】。大多数人可以安全地形成法定人数(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。 +当然如果**所有**的节点都崩溃了,没有一个在运行,那么所有算法都不可能决定任何事情。算法可以容忍的失效数量是有限的:事实上可以证明,任何共识算法都需要至少占总体**多数(majority)**的节点正确工作,以确保终止属性【67】。多数可以安全地组成法定人数(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。 -因此,终止属性受到不到一半的节点崩溃或不可达的假设。然而,即使大多数节点出现故障或存在严重的网络问题,大多数共识的实施都能确保始终满足安全属性——同意,完整性和有效性【92】。因此,大规模的中断可能会阻止系统处理请求,但是它不能通过使系统做出无效的决定来破坏共识系统。 +因此**终止**属性取决于一个假设,**不超过一半的节点崩溃或不可达**。然而即使多数节点出现故障或存在严重的网络问题,绝大多数共识的实现都能始终确保安全属性得到满足—— 一致同意,完整性和有效性【92】。因此,大规模的中断可能会阻止系统处理请求,但是它不能通过使系统做出无效的决定来破坏共识系统。 -大多数一致性算法假定没有**拜占庭式错误**,正如在“[拜占庭式错误](#拜占庭式错误)”一节中所讨论的那样。也就是说,如果一个节点没有正确地遵循协议(例如,如果它发送矛盾的消息到不同的节点),它可能会破坏协议的安全属性。只要少于三分之一的节点是拜占庭故障【25,93】,就可以对拜占庭故障达成共识,但我们没有地方在本书中详细讨论这些算法。 +大多数共识算法假设不存在**拜占庭式错误**,正如在“[拜占庭式错误](#拜占庭式错误)”一节中所讨论的那样。也就是说,如果一个节点没有正确地遵循协议(例如,如果它向不同节点发送矛盾的消息),它就可能会破坏协议的安全属性。克服拜占庭故障,稳健地达成共识是可能的,只要少于三分之一的节点存在拜占庭故障【25,93】。但我们没有地方在本书中详细讨论这些算法了。 #### 共识算法和全序广播 -最着名的容错一致性算法是**视图戳复制(viewstamped replication)**(VSR)【94,95】,Paxos 【96,97,98,99】,Raft 【22,100,101】和Zab 【15,21,102】 。这些算法之间有相当多的相似之处,但它们并不相同【103】。在本书中,我们不会详细介绍不同的算法:只要了解一些共同的高级思想就足够了,除非你准备自己实现一个共识系统。(这可能不是一个明智的做法,相当困难【98,104】) +最著名的容错共识算法是**视图戳复制(VSR, viewstamped replication)**【94,95】,Paxos 【96,97,98,99】,Raft 【22,100,101】以及 Zab 【15,21,102】 。这些算法之间有不少相似之处,但它们并不相同【103】。在本书中我们不会介绍各种算法的详细细节:了解一些它们共通的高级思想通常已经足够了,除非你准备自己实现一个共识系统。(可能并不明智,相当难【98,104】) -这些算法中的大多数实际上并不直接使用这里描述的形式化模型(建议和决定单个值,一致同意,完整性,有效性和终止性质)。相反,它们决定了值的顺序,这使得它们成为了全序广播算法,正如本章前面所讨论的那样(参阅“[全序广播](#全序广播)”)。 +大多数这些算法实际上并不直接使用这里描述的形式化模型(提议与决定单个值,一致同意,完整性,有效性和终止属性)。取而代之的是,它们决定了值的**顺序(sequence)**,这使它们成为全序广播算法,正如本章前面所讨论的那样(参阅“[全序广播](#全序广播)”)。 -请记住,全序广播要求将消息按照相同的顺序准确传送到所有节点。如果仔细思考,这相当于进行了几轮共识:在每一轮中,节点提出下一个要发送的消息,然后决定下一个要发送的消息总数【67】。 +请记住,全序广播要求将消息按照相同的顺序,恰好传递一次,准确传送到所有节点。如果仔细思考,这相当于进行了几轮共识:在每一轮中,节点提议下一条要发送的消息,然后决定在全序中下一条要发送的消息【67】。 -所以,全序广播相当于重复的一轮共识(每个共同的决定对应于一个消息传递): +所以,全序广播相当于重复进行多轮共识(每个共识决定与一次消息传递相对应): -* 由于协商一致意见,所有节点决定以相同的顺序传递相同的消息。 +* 由于一致同意属性,所有节点决定以相同的顺序传递相同的消息。 * 由于完整性属性,消息不重复。 From b12f63edef2b70016dbac2a143355e038eb4b6fb Mon Sep 17 00:00:00 2001 From: "jiajia.debug" Date: Fri, 30 Mar 2018 21:52:00 +0800 Subject: [PATCH 10/17] minor change to improve translation of preface --- preface.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/preface.md b/preface.md index 8cc4a80..ed84019 100644 --- a/preface.md +++ b/preface.md @@ -4,26 +4,26 @@ 在最近十年中,我们看到了很多有趣的进展,关于数据库,分布式系统,以及在此基础上构建应用程序的方式。这些进展有着各种各样的驱动力: -* 谷歌,雅虎,亚马逊,脸书,领英,微软和推特等互联网公司正在和巨大的流量/数据打交道,这迫使它们创造能有效应对这种规模的新工具。 -* 企业需要敏捷,廉价地测试假设,通过缩短开发周期和保持数据模型的灵活性,快速响应新的市场洞察。 +* 谷歌,雅虎,亚马逊,脸书,领英,微软和推特等互联网公司正在和巨大的流量/数据打交道,这迫使他们去创造能有效应对如此规模的新工具。 +* 企业需要变得敏捷,需要低成本地检验假设,需要通过缩短开发周期和保持数据模型的灵活性,快速地响应新的市场洞察。 * 免费和开源软件变得非常成功,在许多环境中比商业软件和定制软件更受欢迎。 -* 处理器主频几乎没有增长,但是多核处理器已经成为标配,网络也越来越快。这意味着并行程度只增不减。 +* 处理器主频几乎没有增长,但是多核处理器已经成为标配,网络也越来越快。这意味着并行化只增不减。 * 即使您在一个小团队中工作,现在也可以构建分布在多台计算机甚至多个地理区域的系统,这要归功于譬如亚马逊网络服务(AWS)等基础设施即服务(IaaS)概念的践行者。 * 许多服务都要求高可用,因停电或维护导致的服务不可用,变得越来越难以接受。 -数据密集型应用正在通过利用这些技术进步推动可能性的边界。如果**数据是其主要挑战**(数据量,数据复杂度或数据变化速度),我们称这类应用为**数据密集型**,与之相对的是**计算密集型**,即处理器速度是瓶颈。 +数据密集型应用正在通过利用这些技术进步推动可能性的边界。如果**数据是其主要挑战**(数据量,数据复杂度或数据变化速度),这类应用被称为**数据密集型**,与之相对的是**计算密集型**,即处理器速度是其瓶颈。 -帮助数据密集型应用程序存储和处理数据的工具和技术已经迅速适应这些变化。新型数据库系统(“NoSQL”)已经引起了人们的关注,但消息队列,缓存,搜索索引,批处理和流处理框架以及相关技术也非常重要。许多应用程序组合使用这些技术。 +工具和技术迅速适应着这些变化,帮助数据密集型应用程序来存储和处理数据。新型数据库系统(“NoSQL”)已经引起了众多关注,然而消息队列,缓存,搜索索引,批处理和流处理框架以及相关技术也非常重要。这些技术被组合使用在很多应用程序上。 -这些充满整个空间的流行语是对新的可能性的热情的表现,这是一件好事。但是,作为软件工程师和架构师,如果我们想要建立良好的应用程序,我们还需要对各种层出不穷的技术持有准确而严谨的技术性理解,以及它们之间的取舍权衡。为此,我们必须深入挖掘流行语背后的内容。 +这些充满整个空间的流行语体现出人们对新可能性的热情,这是一件好事。但是,作为软件工程师和架构师,如果要建立良好的应用程序,我们还需要对各种层出不穷的技术及其利弊持有准确而严谨的技术性理解。为此,必须深入挖掘流行语背后的内容。 -幸运的是,在技术快速变化的背后,无论您使用的是什么版本的特定工具,都存在一些持久的原则。如果你了解这些原则,你就可以看到每个工具的适用位置,如何充分利用它们,以及如何避免其中的陷阱。这是本书的初衷。 +幸运的是,在技术快速变化的背后,无论您使用的是哪个版本的特定工具,存在一些持久的原则。如果您了解这些原则,您就可以领会这些工具适用于何处,如何充分利用它们,以及如何避免其中的陷阱。这正是本书的初衷。 本书的目标是帮助您在多样而快速变化的处理和存储数据技术的大观园中找到方向。这本书不是一个特定工具的教程,也不是一本充满干枯理论的教科书。相反,我们将看到一些成功的数据系统示例:一些构成了许多流行应用程序基础,必须在每天的生产中满足可伸缩性,性能和可靠性需求的技术。 -我们将深入这些系统的内部,梳理他们的关键算法,讨论他们的原则和它们必须做出的权衡。在这个过程中,我们将尝试寻找有用的思考数据系统的方式 —— 不仅仅关于它们是如何工作的,还包括为什么它们以这种方式工作,以及我们需要问什么问题。 +我们将深入这些系统的内部,梳理他们的关键算法,讨论他们的原则和他们必须做出的权衡。在这个过程中,我们将尝试寻找有用的方式来思考数据系统 —— 不仅仅关于它们是如何工作的,还包括为什么它们以这种方式工作,以及我们需要问什么问题。 -阅读本书后,您将能够很好地决定哪种技术适合哪种用途,并了解如何将工具组合起来,形成良好应用架构的基础。您不会获得从头开始构建自己的数据库存储引擎的能力,但幸运的是,这是很少有必要的。您将会获得的是,对你的系统在底层做什么有一个很好的直觉,这样您就可以推断它们的行为,做出好的设计决定,并追踪任何可能出现的问题。 +阅读本书后,您将能够很好地决定哪种技术适合哪种用途,并了解如何将工具组合起来,为一个良好应用架构奠定基础。您并不能够因此准备好从头开始构建自己的数据库存储引擎的技能,不过幸运地很,这很少是必要的。您将会获得的是,建立起对系统底层运行的敏锐直觉,这样您就有能力推理它们的行为,做出好的设计方案,并追踪任何可能出现的问题。 @@ -35,7 +35,7 @@ 您应该具有构建基于Web的应用程序或网络服务的一些经验,并且您应该熟悉关系数据库和SQL。任何您了解的非关系型数据库和其他与数据相关的工具都会更有帮助,但不是必需的。常见的网络协议如TCP和HTTP的一般理解是有帮助的。编程语言或框架的选择对阅读本书没有任何不同影响。 -如果以下任何一条对你是真的,你会发现这本书很有价值: +如果以下任何一条对您是真的,您会发现这本书很有价值: * 您想了解如何使数据系统可扩展,例如,支持拥有数百万用户的Web或移动应用程序。 * 您需要提高应用程序的可用性(最大限度地减少停机时间)和运行稳定。 From 0381c15a334dca0bfa7cf0229afe5140523d3304 Mon Sep 17 00:00:00 2001 From: "jiajia.debug" Date: Fri, 30 Mar 2018 22:01:17 +0800 Subject: [PATCH 11/17] minor change to improve translation of part-i and ch1 --- ch1.md | 30 +++++++++++++++--------------- part-i.md | 6 +++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/ch1.md b/ch1.md index 9ea2dc1..4778252 100644 --- a/ch1.md +++ b/ch1.md @@ -2,7 +2,7 @@ ![](img/ch1.png) -> 互联网做得太棒了,以至于多数人将它看作像海洋这样的天然资源,而不是什么人工产物。上一次出现这种大规模且无差错的技术, 你还记得是什么时候吗? +> 互联网做得太棒了,以至于多数人将它看作像太平洋这样的自然资源,而不是什么人工产物。上一次出现这种大规模且无差错的技术, 你还记得是什么时候吗? > > ——阿兰·凯在接受Dobb博士杂志采访时说(2012年) @@ -32,11 +32,11 @@ ***批处理(batch processing)*** -​ 定期压缩累积的大批量数据 +​ 定期处理累积的大批量数据 -如果这些功能听上去平淡无奇,那是因为这些**数据系统(data system)**是非常成功的抽象,我们一直不假思索地使用它们并习以为常。绝大多数工程师不会想从零开始编写存储引擎,因为在开发应用时,数据库已经是足够完美的工具了。 +上述组件如果听上去显而易见,那是因为这些**数据系统(data system)**是非常成功的抽象:我们一直不假思索地使用它们并习以为常。绝大多数工程师不会想从零开始编写存储引擎,因为在开发应用时,数据库已经是足够完美的工具了。 -但事实并没有这么简单。不同的应用有着不同的需求,所以数据库系统也是百花齐放,有着各式各样的特性。我们有很多种手段可以实现缓存,也有好几种方法可以搞定搜索索引,诸如此类。因此在开发应用前,我们有必要先弄清楚最适合手头工作的工具和方法。而且当单个工具解决不了你的问题时,你会发现组合使用这些工具还是挺有难度的。 +但现实并没有这么简单。不同的应用有着不同的需求,因而数据库系统也是百花齐放,有着各式各样的特性。实现缓存有很多种手段,创建搜索索引也有好几种方法,诸如此类。因此在开发应用前,我们依然有必要先弄清楚最适合手头工作的工具和方法。而且当单个工具解决不了你的问题时,你会意识到组合使用这些工具是挺有难度的。 本书将是一趟关于数据系统原理、实践与应用的旅程,并讲述了设计数据密集型应用的方法。我们将探索不同工具之间的共性与特性,以及各自的实现原理。 @@ -141,7 +141,7 @@ 尽管人类不可靠,但怎么做才能让系统变得可靠?最好的系统会组合使用以下几种办法: -* 以最小化犯错机会的方式设计系统。例如,精心设计的抽象、API和管理后台使做对事情更容易,搞砸事情更困难。但如果接口限制太多,人们就会否定它们的好处而想办法绕开。这是一个很难正确把握的棘手平衡。 +* 以最小化犯错机会的方式设计系统。例如,精心设计的抽象、API和管理后台使做对事情更容易,搞砸事情更困难。但如果接口限制太多,人们就会忽略它们的好处而想办法绕开。很难正确把握这种微妙的平衡。 * 将人们最容易犯错的地方与可能导致失效的地方**解耦(decouple)**。特别是提供一个功能齐全的非生产环境**沙箱(sandbox)**,使人们可以在不影响真实用户的情况下,使用真实数据安全地探索和实验。 * 在各个层次进行彻底的测试【3】,从单元测试、全系统集成测试到手动测试。自动化测试易于理解,已经被广泛使用,特别适合用来覆盖正常情况中少见的**边缘场景(corner case)**。 * 允许从人为错误中简单快速地恢复,以最大限度地减少失效情况带来的影响。 例如,快速回滚配置变更,分批发布新代码(以便任何意外错误只影响一小部分用户),并提供数据重算工具(以备旧的计算出错)。 @@ -150,7 +150,7 @@ ### 可靠性有多重要? -可靠性不仅仅是针对核电站和空中交通管制软件而言,我们也期望许多平凡的应用能可靠地运行。商务应用中的错误会导致生产力损失(也许数据报告不完整还会有法律风险),而电商网站的中断则可能会导致收入和声誉的巨大损失。 +可靠性不仅仅是针对核电站和空中交通管制软件而言,我们也期望更多平凡的应用能可靠地运行。商务应用中的错误会导致生产力损失(也许数据报告不完整还会有法律风险),而电商网站的中断则可能会导致收入和声誉的巨大损失。 即使在“非关键”应用中,我们也对用户负有责任。试想一位家长把所有的照片和孩子的视频储存在你的照片应用里【15】。如果数据库突然损坏,他们会感觉如何?他们可能会知道如何从备份恢复吗? @@ -166,7 +166,7 @@ ### 描述负载 -讨论增长问题(如果负载加倍会发生什么?)前,首先要能简要描述系统的当前负载。负载可以用一些称为**负载参数(load parameters)**的数字来描述。参数的最佳选择取决于系统架构,它可能是每秒向Web服务器发出的请求、数据库中的读写比率、聊天室中同时活跃的用户数量、缓存命中率或其他东西。除此之外,也许平均情况对你很重要,也许你的瓶颈是少数极端场景。 +在讨论增长问题(如果负载加倍会发生什么?)前,首先要能简要描述系统的当前负载。负载可以用一些称为**负载参数(load parameters)**的数字来描述。参数的最佳选择取决于系统架构,它可能是每秒向Web服务器发出的请求、数据库中的读写比率、聊天室中同时活跃的用户数量、缓存命中率或其他东西。除此之外,也许平均情况对你很重要,也许你的瓶颈是少数极端场景。 为了使这个概念更加具体,我们以推特在2012年11月发布的数据【16】为例。推特的两个主要业务是: @@ -207,29 +207,29 @@ 然而方法2的缺点是,发推现在需要大量的额外工作。平均来说,一条推文会发往约75个关注者,所以每秒4.6k的发推写入,变成了对主页时间线缓存每秒345k的写入。但这个平均值隐藏了用户粉丝数差异巨大这一现实,一些用户有超过3000万的粉丝,这意味着一条推文就可能会导致主页时间线缓存的3000万次写入!及时完成这种操作是一个巨大的挑战——推特尝试在5秒内向粉丝发送推文。 -在推特的例子中,每个用户粉丝数的分布(可能按这些用户的发推频率来加权)是讨论可扩展性的关键负载参数,因为它决定了扇出负载。你的应用程序可能具有非常不同的特征,但可以使用相似的原则来考虑你的负载。 +在推特的例子中,每个用户粉丝数的分布(可能按这些用户的发推频率来加权)是探讨可扩展性的一个关键负载参数,因为它决定了扇出负载。你的应用程序可能具有非常不同的特征,但可以采用相似的原则来考虑它的负载。 -推特轶事的最终转折:现在方法2已经稳健地实现了,但推特又转向了两种方法的混合。大多数用户发推时仍然是扇出写入粉丝的主页时间线缓存中。但是少数拥有海量粉丝的用户(即名流)被排除在外。当用户读取主页时间线时,来自所关注名流的推文都会单独拉取,并与用户的主页时间线缓存合并,如方法1所示。这种混合方法能始终如一地提供良好性能。在[第12章](ch12.md)中将重新讨论这个例子,那时我们已经覆盖了更多的技术层面。 +推特轶事的最终转折:现在已经稳健地实现了方法2,推特逐步转向了两种方法的混合。大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中。但是少数拥有海量粉丝的用户(即名流)会被排除在外。当用户读取主页时间线时,分别地获取出该用户所关注的每位名流的推文,再与用户的主页时间线缓存合并,如方法1所示。这种混合方法能始终如一地提供良好性能。在[第12章](ch12.md)中我们将重新讨论这个例子,这在覆盖更多技术层面之后。 ### 描述性能 -一旦系统的负载可以被描述,就可以研究当负载增加会发生什么。我们可以从两种角度来看: +一旦系统的负载被描述好,就可以研究当负载增加会发生什么。我们可以从两种角度来看: -* 增加负载参数并保持系统资源(CPU、内存、网络带宽等)不变时,系统性能将有什么影响? +* 增加负载参数并保持系统资源(CPU、内存、网络带宽等)不变时,系统性能将受到什么影响? * 增加负载参数并希望保持性能不变时,需要增加多少系统资源? 这两个问题都需要性能数据,所以让我们简单地看一下如何描述系统性能。 -对于Hadoop这样的批处理系统,通常关心的是**吞吐量(throughput)**,即每秒可以处理的记录数量,或者在特定规模数据集上运行作业的总时间[^iii]。对于在线系统,通常更重要的是服务的**响应时间(response time)**,即客户端发送请求和接收响应之间的时间。 +对于Hadoop这样的批处理系统,通常关心的是**吞吐量(throughput)**,即每秒可以处理的记录数量,或者在特定规模数据集上运行作业的总时间[^iii]。对于在线系统,通常更重要的是服务的**响应时间(response time)**,即客户端发送请求到接收响应之间的时间。 [^iii]: 理想情况下,批量作业的运行时间是数据集的大小除以吞吐量。 在实践中由于数据倾斜(数据不是均匀分布在每个工作进程中),需要等待最慢的任务完成,所以运行时间往往更长。 > #### 延迟和响应时间 > -> **延迟(latency)**和**响应时间(response time)**通常当成同义词用,但实际上它们并不一样。响应时间是客户所看到的,除了实际处理请求的时间(**服务时间(service time)**)之外,还包括网络延迟和排队延迟。延迟是某个请求等待处理的**持续时长**,在此期间它处于**休眠(latent)**状态,并等待服务【17】。 +> **延迟(latency)**和**响应时间(response time)**通常互当成同义词来使用,但实际上它们并不一样。响应时间是客户所看到的,除了实际处理请求的时间(**服务时间(service time)**)之外,还包括网络延迟和排队延迟。延迟是某个请求等待处理的**持续时长**,在此期间它处于**休眠(latent)**状态,并等待服务【17】。 > -即使不断重复发送同样的请求,每次得到的响应时间也都会略有不同。现实世界的系统会处理各式各样的请求,响应时间可能会有很大差异。因此我们需要将响应时间视为一个可以测量的**分布(distribution)**,而不是单个数值。 +即使不断重复发送同样的请求,每次得到的响应时间也都会略有不同。现实世界的系统会处理各式各样的请求,响应时间可能会有很大差异。因此我们需要将响应时间视为一个可以测量的数值**分布(distribution)**,而不是单个数值。 在[图1-4](img/fig1-4.png)中,每个灰条表代表一次对服务的请求,其高度表示请求花费了多长时间。大多数请求是相当快的,但偶尔会出现需要更长的时间的异常值。这也许是因为缓慢的请求实质上开销更大,例如它们可能会处理更多的数据。但即使(你认为)所有请求都花费相同时间的情况下,随机的附加延迟也会导致结果变化,例如:上下文切换到后台进程,网络数据包丢失与TCP重传,垃圾收集暂停,强制从磁盘读取的页面错误,服务器机架中的震动【18】,还有很多其他原因。 @@ -245,7 +245,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 percentil)**)非常重要,因为它们直接影响用户的服务体验。例如亚马逊在描述内部服务的响应时间要求时以99.9百分位点为准,即使它只影响一千个请求中的一个。这是因为请求响应最慢的客户往往也是数据最多的客户,也可以说是最有价值的客户——因为他们掏钱了【19】。保证网站响应迅速对于保持客户的满意度非常重要,亚马逊观察到:响应时间增加100毫秒,销售量就减少1%【20】;而另一些报告说:慢1秒钟会让客户满意度指标减少16%【21,22】。 +响应时间的高百分位点(也称为**尾部延迟(tail latencies)**)非常重要,因为它们直接影响用户的服务体验。例如亚马逊在描述内部服务的响应时间要求时以99.9百分位点为准,即使它只影响一千个请求中的一个。这是因为请求响应最慢的客户往往也是数据最多的客户,也可以说是最有价值的客户——因为他们掏钱了【19】。保证网站响应迅速对于保持客户的满意度非常重要,亚马逊观察到:响应时间增加100毫秒,销售量就减少1%【20】;而另一些报告说:慢1秒钟会让客户满意度指标减少16%【21,22】。 另一方面,优化第99.99百分位点(一万个请求中最慢的一个)被认为太昂贵了,不能为亚马逊的目标带来足够好处。减小高百分位点处的响应时间相当困难,因为它很容易受到随机事件的影响,这超出了控制范围,而且效益也很小。 diff --git a/part-i.md b/part-i.md index 14679f7..f23ba26 100644 --- a/part-i.md +++ b/part-i.md @@ -4,10 +4,10 @@ 1. [第一章](ch1.md)将介绍本书使用的术语和方法。**可靠性,可扩展性和可维护性** ,这些词汇到底意味着什么?如何实现这些目标? 2. [第二章](ch2.md)将对几种不同的**数据模型和查询语言**进行比较。从程序员的角度看,这是数据库之间最明显的区别。不同的数据模型适用于不同的应用场景。 -3. [第三章](ch3.md)将深**存储引擎**内部,并研究数据库如何在磁盘上摆放数据。不同的存储引擎针对不同的负载进行优化,选择合适的存储引擎对系统性能有巨大影响。 -4. [第四章](ch4)将对几种不同的 **数据编码**进行比较。特别讨论了在应用需求经常变化、模式需要随时间演化的环境中,这些格式的适用情况。 +3. [第三章](ch3.md)将深入**存储引擎**内部,研究数据库如何在磁盘上摆放数据。不同的存储引擎针对不同的负载进行优化,选择合适的存储引擎对系统性能有巨大影响。 +4. [第四章](ch4)将对几种不同的 **数据编码**进行比较。特别地分析了他们在应用需求经常变化、模式需要随着时间演变的环境中的表现情况。 -第二部分将专门讨论在**分布式数据系统**中才有的问题。 +第二部分将专门讨论在**分布式数据系统**中特有的问题。 From 028df0de4bc037ca9ddf5c1d1cc025ba40e6e126 Mon Sep 17 00:00:00 2001 From: Vonng Date: Sat, 31 Mar 2018 02:03:16 +0800 Subject: [PATCH 12/17] ch9 done --- README.md | 2 +- ch2.md | 4 +- ch9.md | 118 ++++++++++++++++++++++++++---------------------------- 3 files changed, 60 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index e7d1849..4d81323 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ | 第六章:分区 | 初翻 | | | 第七章:事务 | 精翻 60% | Vonng | | 第八章:分布式系统中的问题 | 初翻 | | -| 第九章:一致性与共识 | 初翻 80% | Vonng | +| 第九章:一致性与共识 | 初翻 | Vonng | | 第三部分:前言 | 精翻 | | | 第十章:批处理 | 草翻 | | | 第十一章:流处理 | 草翻 | | diff --git a/ch2.md b/ch2.md index 4d0c303..cca022d 100644 --- a/ch2.md +++ b/ch2.md @@ -633,9 +633,9 @@ RETURN person.name 对于声明性查询语言来说,典型的情况是,在编写查询语句时,您不需要指定执行细节:查询优化程序会自动选择预测效率最高的策略,因此您可以继续编写其余的应用程序。 -### SQL中的图表查询 +### SQL中的图查询 -[例2-2]()建议可以在关系数据库中表示图形数据。但是,如果我们把图形数据放入关系结构中,我们是否也可以使用SQL查询它?答案是肯定的,但有些困难。在关系数据库中,您通常会事先知道在查询中需要哪些连接。在图表查询中,您可能需要在找到要查找的顶点之前,遍历可变数量的边。也就是说,连接的数量事先并不确定。 +[例2-2]()建议可以在关系数据库中表示图数据。但是,如果我们把图数据放入关系结构中,我们是否也可以使用SQL查询它?答案是肯定的,但有些困难。在关系数据库中,您通常会事先知道在查询中需要哪些连接。在图查询中,您可能需要在找到要查找的顶点之前,遍历可变数量的边。也就是说,连接的数量事先并不确定。 在我们的例子中,这发生在Cypher查询中的`() -[:WITHIN*0..]-> ()`规则中。一个人的`LIVES_IN`边缘可以指向任何类型的位置:街道,城市,地区,地区,国家等。城市可以在一个地区,在一个州内的一个地区,在一个国家内的一个州等等。`LIVES_IN`边可以直接指向你正在查找的位置,或者可以在位置层次结构中删除几个级别。 在Cypher中,`WITHIN * 0`表示这个事实非常简洁:意思是“沿着一个`WITHIN`边,零次或多次”。它就像正则表达式中的`*`运算符。 diff --git a/ch9.md b/ch9.md index 4e27ac6..f6f8c1b 100644 --- a/ch9.md +++ b/ch9.md @@ -792,96 +792,94 @@ XA事务解决了保持多个参与者(数据系统)相互一致的现实的 请记住,全序广播要求将消息按照相同的顺序,恰好传递一次,准确传送到所有节点。如果仔细思考,这相当于进行了几轮共识:在每一轮中,节点提议下一条要发送的消息,然后决定在全序中下一条要发送的消息【67】。 -所以,全序广播相当于重复进行多轮共识(每个共识决定与一次消息传递相对应): +所以,全序广播相当于重复进行多轮共识(每次共识决定与一次消息传递相对应): -* 由于一致同意属性,所有节点决定以相同的顺序传递相同的消息。 +* 由于**一致同意**属性,所有节点决定以相同的顺序传递相同的消息。 -* 由于完整性属性,消息不重复。 -* 由于有效性属性,消息不会被破坏,也不是凭空制造的。 -* 由于终止属性,消息不会丢失。 +* 由于**完整性**属性,消息不会重复。 +* 由于**有效性**属性,消息不会被损坏,也不能凭空编造。 +* 由于**终止**属性,消息不会丢失。 -已加密的复制,Raft和Zab直接执行全序广播,因为这样做比重复一轮一次一致的共识更有效。在Paxos的情况下,这种优化被称为Multi-Paxos。 +视图戳复制,Raft和Zab直接实现了全序广播,因为这样做比重复**一次一值(one value a time)**的共识更高效。在Paxos的情况下,这种优化被称为Multi-Paxos。 #### 单领导者复制和共识 -在[第5章](ch5.md)中,我们讨论了单领导者复制(参见“[领导者和追随者](ch5.md#领导者和追随者)”),它将所有的写入操作都交给领导者,并以相同的顺序将他们应用到追随者,从而使副本保持最新状态。这不是基本上全序广播?我们怎么不用担心[第五章](ch5.md)的共识? +在[第5章](ch5.md)中,我们讨论了单领导者复制(参见“[领导者和追随者](ch5.md#领导者和追随者)”),它将所有的写入操作都交给主库,并以相同的顺序将它们应用到从库,从而使副本保持在最新状态。这实际上不就是一个全序广播吗?为什么我们在[第五章](ch5.md)里一点都没担心过共识问题呢? -答案取决于如何选择领导者。如果领导人是由你的运维团队人员手动选择和配置的,那么你基本上拥有独裁类型的“一致性算法”:只允许一个节点接受写入(即,决定写入的顺序复制日志),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为领导者。这样的制度在实践中效果很好,但是无法达到共识的**终止**属性,因为它需要人为干预才能取得进展。 +答案取决于如何选择领导者。如果主库是由运维人员手动选择和配置的,那么你实际上拥有一种**独裁类型**的“共识算法”:只有一个节点被允许接受写入(即决定写入复制日志的顺序),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为主库。这样的系统在实践中可以表现良好,但它无法满足共识的**终止**属性,因为它需要人为干预才能取得**进展**。 -一些数据库执行自动领导者选举和故障转移,如果旧领导者失败,则促使追随者成为新的领导者(参见“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们更接近容错的全面命令播出,从而达成共识。 +一些数据库会自动执行领导者选举和故障转移,如果旧主库失效,会提拔一个从库为新主库(参见“[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们向容错的全序广播更进一步,从而达成共识。 -但是,有一个问题。我们之前曾经讨论过脑裂的问题,并且说过,所有的节点都需要同意领导者是谁,否则两个不同的节点都会认为自己是领导者,从而导致数据库进入不一致的状态。因此,我们需要达成共识才能选出一位领导人。但是,如果这里描述的一致性算法实际上是全序广播算法,并且全序广播就像单主复制,单主复制需要领导,那么... +但是还有一个问题。我们之前曾经讨论过脑裂的问题,并且说过所有的节点都需要同意是谁领导,否则两个不同的节点都会认为自己是领导者,从而导致数据库进入不一致的状态。因此,选出一位领导者需要共识。但如果这里描述的共识算法实际上是全序广播算法,并且全序广播就像单主复制,而单主复制需要一个领导者,那么... -看来要选出一个领导者,我们首先需要一个领导者。要解决共识问题,首先要解决共识问题。我们如何摆脱这个难题? +这样看来,要选出一个领导者,我们首先需要一个领导者。要解决共识问题,我们首先需要解决共识问题。我们如何跳出这个先有鸡还是先有蛋的问题? #### 时代编号和法定人数 -迄今为止所讨论的所有共识协议在内部都以某种形式使用领导者,但是并不能保证领导者是独一无二的。相反,他们可以做出较弱的保证:协议定义了一个纪元号码(称为Paxos中的选票号码,视图戳复制中的视图号码,以及Raft中的术语号码),并确保在每个纪元中领导者是唯一的。 +迄今为止所讨论的所有共识协议,在内部都以某种形式使用一个领导者,但它们并不能保证领导者是独一无二的。相反,它们可以做出更弱的保证:协议定义了一个**时代编号(epoch number)**(在Paxos中称为**投票编号(ballot number)**,视图戳复制中的**视图编号(view number)**,以及Raft中的**任期号码(term number)**),并确保在每个时代中,领导者都是唯一的。 -每当现在的领导被认为是死的时候,就会在节点之间开始投票选出一个新领导。这次选举被赋予了一个递增的时代号码,因此时代号码是完全有序的,单调递增的。如果在两个不同的时代,两个不同的领导者之间有冲突(也许是因为前一个领导者实际上并没有死亡),那么具有更高时代的领导者就占上风了。 +每次当现任领导被认为挂掉的时候,节点间就会开始一场投票,以选出一个新领导。这次选举被赋予一个递增的时代编号,因此时代编号是全序且单调递增的。如果两个不同的时代的领导者之间出现冲突(也许是因为前任领导者实际上并未死亡),那么带有更高时代编号的领导说了算。 -在任何领导人被允许决定任何事情之前,必须首先检查是否没有其他具有较高时代的领导者,这可能会采取相互冲突的决定。领导者如何知道它没有被另一个节点赶下?回想一下在“[真理是由多数所定义](ch8.md#真理是由多数所定义)”中:一个节点不一定能相信自己的判断——因为只有节点自己认为它是领导者,并不一定意味着其他节点接受它作为它们的领导者。 +在任何领导者被允许决定任何事情之前,必须先检查是否存在其他带有更高时代编号的领导者,它们可能会做出相互冲突的决定。领导者如何知道自己没有被另一个节点赶下台?回想一下在“[真理在多数人手中](ch8.md#真理在多数人手中)”中提到的:一个节点不一定能相信自己的判断—— 因为只有节点自己认为自己是领导者,并不一定意味着其他节点接受它作为它们的领导者。 -相反,它必须从节点法定人数中获取选票(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对于领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应提案。法定人数通常但不总是由大部分节点组成【105】。一个节点只有在没有意识到任何具有更高纪元的其他领导者的时候才投票赞成。 +相反,它必须从**法定人数(quorum)**的节点中获取选票(参阅“[读写的法定人数](ch5.md#读写的法定人数)”)。对领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应并赞成提案。法定人数通常(但不总是)由多数节点组成【105】。只有在没有意识到任何带有更高时代编号的领导者的情况下,一个节点才会投票赞成提议。 -因此,我们有两轮投票:一次是选一位领导人,二是投票领导人的提议。关键的看法是,这两票的法定人数必须重叠:如果一个提案的投票成功,至少有一个投票的节点也必须参加最近的领导人选举【105】。因此,如果一个提案的投票没有显示任何更高的时代,那么现在的领导者就可以得出这样的结论:没有一个更高时代的领袖选举发生了,因此可以确定它仍然是领导。然后它可以安全地决定提出的价值。 +因此,我们有两轮投票:第一次是为了选出一位领导者,第二次是对领导者的提议进行表决。关键的洞察在于,这两次投票的**法定人群**必须相互**重叠(overlap)**:如果一个提案的表决通过,则至少得有一个参与投票的节点也必须参加过最近的领导者选举【105】。因此,如果在一个提案的表决过程中没有出现更高的时代编号。那么现任领导者就可以得出这样的结论:没有发生过更高时代的领导选举,因此可以确定自己仍然在领导。然后它就可以安全地对提议值做出决定。 -这个投票过程看起来很像两阶段提交。最大的区别是在2PC中协调者不是选出来的,而容错协议算法只需要大部分节点的投票,而2PC则要求每个参与者都做“是”的投票。而且,共识算法定义了一个恢复过程,通过这个过程,节点可以在选举出新的领导者之后进入一个一致的状态,确保总是满足安全属性。这些差异是共识算法的正确性和容错性的关键。 +这一投票过程表面上看起来很像两阶段提交。最大的区别在于,2PC中协调者不是由选举产生的,而且2PC则要求**所有**参与者都投赞成票,而容错共识算法只需要多数节点的投票。而且,共识算法还定义了一个恢复过程,节点可以在选举出新的领导者之后进入一个一致的状态,确保始终能满足安全属性。这些区别正是共识算法正确性和容错性的关键。 #### 共识的局限性 -共识算法对于分布式系统来说是一个巨大的突破:它为具有其他各种不确定性的系统带来了具体的安全属性(一致性,完整性和有效性),而且它们仍然是容错的(只要能够进行处理大多数节点正在工作和可达)。它们提供全序广播,因此它们也可以容错的方式实现线性一致性的原子操作(参见“[使用全序广播实现线性一致性存储](#使用全序广播实现线性一致性存储)”)。 +共识算法对于分布式系统来说是一个巨大的突破:它为其他充满不确定性的系统带来了基础的安全属性(一致同意,完整性和有效性),然而它们还能保持容错(只要多数节点正常工作且可达,就能取得进展)。它们提供了全序广播,因此也可以它们也可以以一种容错的方式实现线性一致的原子操作(参见“[使用全序广播实现线性一致性存储](#使用全序广播实现线性一致性存储)”)。 -尽管如此,它们并没有到处使用,因为它的好处是有代价的。 +尽管如此,它们并不是在所有地方都用上了,因为好处总是有代价的。 -节点在决定之前对节点进行投票的过程是一种同步复制。如“[同步与异步复制](ch5.md#同步与异步复制)”中所述,通常将数据库配置为使用异步复制。在这种配置中,一些承诺的数据在故障转移时可能会丢失——但是为了获得更好的性能,许多人选择接受这种风险。 +节点在做出决定之前对提议进行投票的过程是一种同步复制。如“[同步与异步复制](ch5.md#同步与异步复制)”中所述,通常数据库会配置为异步复制模式。在这种配置中发生故障切换时,一些已经提交的数据可能会丢失 —— 但是为了获得更好的性能,许多人选择接受这种风险。 -共识体系总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余三个为大多数),或者至少有五个节点来容忍两个节点发生故障(其余三个为五分之一)。如果网络故障切断了其余节点的某些节点,则只有大部分网络可以继续工作,其余部分将被阻塞(另参阅“[线性一致性的代价](#线性一致性的代价)”)。 +共识系统总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余两个构成多数),或者至少有五个节点来容忍两个节点发生故障(其余三个构成多数)。如果网络故障切断了某些节点同其他节点的连接,则只有多数节点所在的网络可以继续工作,其余部分将被阻塞(参阅“[线性一致性的代价](#线性一致性的代价)”)。 -大多数一致性算法假定一组参与投票的节点,这意味着你不能只添加或删除集群中的节点。对共识算法的动态成员扩展允许集群中的节点集随着时间的推移而变化,但是它们比静态成员算法要好得多。 +大多数共识算法假定参与投票的节点是固定的集合,这意味着你不能简单的在集群中添加或删除节点。共识算法的**动态成员扩展(dynamic membership extension)**允许集群中的节点集随时间推移而变化,但是它们比静态成员算法要难理解得多。 -共识系统通常依靠超时来检测失败的节点。在网络延迟高度变化的环境中,特别是在地理上分布的系统中,经常发生一个节点错误地认为由于暂时的网络问题,导致失败的原因。虽然这个错误不会损害安全属性,但频繁的领导者选举会导致糟糕的表现,因为系统最终会花费更多的时间来选择领导者而不是做任何有用的工作。 +共识系统通常依靠超时来检测失效的节点。在网络延迟高度变化的环境中,特别是在地理上散布的系统中,经常发生一个节点由于暂时的网络问题,错误地认为领导者已经失效。虽然这种错误不会损害安全属性,但频繁的领导者选举会导致糟糕的性能表现,因系统最后可能花在权力倾扎上的时间要比花在建设性工作的多得多。 -有时共识算法对网络问题特别敏感。例如Raft已被证明存在不愉快的边缘情况【106】:如果整个网络工作正常,除了一个特定的网络连接一直不可靠,Raft可以进入领导层不断在两个节点之间弹跳的情况,或者目前的领导者不断被迫辞职,所以这个制度从来没有取得进展。其他一致性算法也存在类似的问题,而对不可靠网络更具鲁棒性的设计算法仍然是一个开放的研究问题。 +有时共识算法对网络问题特别敏感。例如Raft已被证明存在让人不悦的极端情况【106】:如果整个网络工作正常,但只有一条特定的网络连接一直不可靠,Raft可能会进入领导频繁二人转的局面,或者当前领导者不断被迫辞职以致系统实质上毫无进展。其他一致性算法也存在类似的问题,而设计能健壮应对不可靠网络的算法仍然是一个开放的研究问题。 ### 成员与协调服务 -像ZooKeeper或etcd这样的项目通常被描述为“分布式键值存储”或“协调和配置服务”。这种服务的API看起来非常像数据库:你可以读写给定键的值,并遍历键。所以如果他们基本上是数据库的话,他们为什么要全力实施一个共识算法呢?是什么使他们不同于任何其他类型的数据库? +像ZooKeeper或etcd这样的项目通常被描述为“分布式键值存储”或“协调与配置服务”。这种服务的API看起来非常像数据库:你可以读写给定键的值,并遍历键。所以如果它们基本上算是数据库的话,为什么它们要把工夫全花在实现一个共识算法上呢?是什么使它们区别于其他任意类型的数据库? -为了理解这一点,简单探讨如何使用像ZooKeeper这样的服务是有帮助的。作为应用程序开发人员,你很少需要直接使用ZooKeeper,因为它实际上不适合作为通用数据库。更有可能的是,通过其他项目间接依赖它,例如HBase,Hadoop YARN,OpenStack Nova和Kafka都依赖ZooKeeper在后台运行。这些项目从中得到什么? +为了理解这一点,简单了解如何使用ZooKeeper这类服务是很有帮助的。作为应用开发人员,你很少需要直接使用ZooKeeper,因为它实际上不适合当成通用数据库来用。更有可能的是,你会通过其他项目间接依赖它,例如HBase,Hadoop YARN,OpenStack Nova和Kafka都依赖ZooKeeper在后台运行。这些项目从它那里得到了什么? -ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据(虽然它们仍然写入磁盘以保持持久性),所以你不希望在这里存储所有的应用程序的数据。使用容错全序广播算法在所有节点上复制少量的数据。正如前面所讨论的那样,全序广播就是数据库复制所需要的:如果每条消息代表对数据库的写入,则以相同的顺序应用相同的写入操作可以保持副本之间的一致性。 +ZooKeeper和etcd被设计为容纳少量完全可以放在内存中的数据(虽然它们仍然会写入磁盘以保证持久性),所以你不会想着把所有应用数据放到这里。这些少量数据会通过容错的全序广播算法复制到所有节点上。正如前面所讨论的那样,数据库复制需要的就是全序广播:如果每条消息代表对数据库的写入,则以相同的顺序应用相同的写入操作可以使副本之间保持一致。 -ZooKeeper模仿Google的Chubby锁定服务【14,98】,不仅实现了全序广播(因此也实现了共识),而且还构建了一组有趣的其他特性,这些特性在构建分布式系统时变得特别有用: +ZooKeeper模仿了Google的Chubby锁服务【14,98】,不仅实现了全序广播(因此也实现了共识),而且还构建了一组有趣的其他特性,这些特性在构建分布式系统时变得特别有用: ***线性一致性的原子操作*** -使用原子CAS操作可以实现锁:如果多个节点同时尝试执行相同的操作,则只有一个节点会成功。共识协议保证了操作的原子性和线性一致性,即使节点发生故障或网络在任意时刻中断。分布式锁通常作为一个租约来实现,这个租约有一个到期时间,以便在客户端失败的情况下最终被释放(参阅“[进程暂停](ch8.md#进程暂停)”)。 +使用原子CAS操作可以实现锁:如果多个节点同时尝试执行相同的操作,只有一个节点会成功。共识协议保证了操作的原子性和线性一致性,即使节点发生故障或网络在任意时刻中断。分布式锁通常以**租约(lease)**的形式实现,租约有一个到期时间,以便在客户端失效的情况下最终能被释放(参阅“[进程暂停](ch8.md#进程暂停)”)。 -***操作的全序*** +***操作的全序排序*** -如“页首301和锁定”中所述,当某个资源受到锁定或租约的保护时,你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。击剑标记是每次获得锁定时单调增加的数字。 ZooKeeper通过完全排序所有操作,并为每个操作提供一个单调递增的事务ID(`zxid`)和版本号(`cversion`)来提供这个功能【15】。 +如“[领导者与锁定](ch8.md#领导者与锁定)”中所述,当某个资源受到锁或租约的保护时,你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。防护令牌是每次锁被获取时单调增加的数字。 ZooKeeper通过全局排序操作来提供这个功能,它为每个操作提供一个单调递增的事务ID(`zxid`)和版本号(`cversion`)【15】。 ***失效检测*** -客户端在ZooKeeper服务器上维护一个长期的会话,客户端和服务器周期性地交换心跳来检查另一个节点是否还活着。即使连接暂时中断,或者ZooKeeper节点失败,会话仍保持活动状态。但是,如果心跳停止持续时间超过会话超时,ZooKeeper会声明该会话已经死亡。当会话超时(ZooKeeper调用这些临时节点)时,会话持有的任何锁都可以配置为自动释放。 +客户端在ZooKeeper服务器上维护一个长期会话,客户端和服务器周期性地交换心跳包来检查节点是否还活着。即使连接暂时中断,或者ZooKeeper节点失效,会话仍保持在活跃状态。但如果心跳停止的持续时间超出会话超时,ZooKeeper会宣告该会话已死亡。当会话超时(ZooKeeper调用这些临时节点)时,会话持有的任何锁都可以配置为自动释放(ZooKeeper称之为**临时节点(ephemeral nodes)**)。 ***变更通知*** -一个客户端不仅可以读取其他客户端创建的锁和值,还可以监视其中的更改。因此,客户端可以找出另一个客户端何时加入集群(基于它写入ZooKeeper的值),还是另一个客户端发生故障(因为其会话超时并且其临时节点消失)。通过订阅通知,客户避免了不得不经常轮询以找出变化。 - -在这些特征中,只有线性一致性的原子操作才需要达成共识。但是,这些功能的结合使得像ZooKeeper这样的系统在分布式协调中非常有用。 - +客户端不仅可以读取其他客户端创建的锁和值,还可以监听它们的变更。因此,客户端可以知道另一个客户端何时加入集群(基于新客户端写入ZooKeeper的值),或发生故障(因其会话超时,而其临时节点消失)。通过订阅通知,客户端不用再通过频繁轮询的方式来找出变更。 +在这些功能中,只有线性一致的原子操作才真的需要共识。但正是这些功能的组合,使得像ZooKeeper这样的系统在分布式协调中非常有用。 #### 将工作分配给节点 -ZooKeeper/Chubby模型运行良好的一个例子是,如果你有几个流程或服务的实例,并且需要选择其中一个实例作为主库或首要。如果领导失败,其他节点之一应该接管。这对于单主数据库当然是有用的,但对于作业调度程序和类似的有状态系统也是有用的。 +ZooKeeper/Chubby模型运行良好的一个例子是,如果你有几个进程实例或服务,需要选择其中一个实例作为主库或首选服务。如果领导者失败,其他节点之一应该接管。这对单主数据库当然非常实用,但对作业调度程序和类似的有状态系统也很好用。 -另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式参与者系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入群集时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(参阅“[重新平衡分区](ch6.md#重新平衡分区)”)。当节点被移除或失败时,其他节点需要接管失败节点的工作。 +另一个例子是,当你有一些分区资源(数据库,消息流,文件存储,分布式Actor系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入集群时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(参阅“[重新平衡分区](ch6.md#重新平衡分区)”)。当节点被移除或失效时,其他节点需要接管失效节点的工作。 -这些类型的任务可以通过在ZooKeeper中明智地使用原子操作,各种节点和通知来实现。如果正确完成,这种方法允许应用程序自动从故障中恢复,无需人工干预。尽管Apache Curator 【17】等库已经出现在ZooKeeper客户端API的顶层提供了更高层的工具,但这样做并不容易,但它仍然比尝试从头开始实现必要的一致性算法要好得多,成绩不佳【107】。 +这类任务可以通过在ZooKeeper中明智地使用原子操作,临时节点与通知来实现。如果设计得当,这种方法允许应用自动从故障中恢复而无需人工干预。不过这并不容易,尽管已经有不少在ZooKeeper客户端API基础之上提供更高层工具的库,例如Apache Curator 【17】。但它仍然要比尝试从头实现必要的共识算法要好得多,这样的尝试鲜有成功记录【107】。 应用最初只能在单个节点上运行,但最终可能会增长到数千个节点。试图在如此之多的节点上进行多数投票将是非常低效的。相反,ZooKeeper在固定数量的节点(通常是三到五个)上运行,并在这些节点之间执行其多数票,同时支持潜在的大量客户端。因此,ZooKeeper提供了一种将协调节点(共识,操作排序和故障检测)的一些工作“外包”到外部服务的方式。 @@ -909,57 +907,55 @@ ZooKeeper和它的小伙伴们可以看作是成员服务研究的悠久历史 ## 本章小结 -在本章中,我们从几个不同的角度研究了一致性和共识的主题。我们深入研究了线性一致性(一种流行的一致性模型):其目标是使复制的数据看起来好像只有一个副本,并使所有操作都以原子方式运行。虽然线性一致性因为易于理解而变得很吸引人——它使数据库在单线程程序中表现得像一个变量一样,但它具有速度慢的缺点,特别是在网络延迟较大的环境中。 +在本章中,我们从几个不同的角度审视了关于一致性与共识的话题。我们深入研究了线性一致性(一种流行的一致性模型):其目标是使多副本数据看起来好像只有一个副本一样,并使其上所有操作都原子性地生效。虽然线性一致性因为简单易懂而很吸引人 —— 它使数据库表现的好像单线程程序中的一个变量一样,但它有着速度缓慢的缺点,特别是在网络延迟很大的环境中。 -我们还探讨了因果性,因果性对系统中的事件进行了排序(根据因和过,以及什么发生在什么之前)。与线性一致性不同,线性一致性将所有操作放在单一的完全有序的时间线中,因果一致性为我们提供了一个较弱的一致性模型:某些东西可以是并发的,所以版本历史就像是一个不断分叉与合并的时间线。因果一致性没有线性一致性的协调开销,并且对网络问题的敏感性要低得多。 +我们还探讨了因果性,因果性对系统中的事件施加了顺序(什么发生在什么之前,基于因与果)。与线性一致不同,线性一致性将所有操作放在单一的全序时间线中,因果一致性为我们提供了一个较弱的一致性模型:某些事件可以是**并发**的,所以版本历史就像是一条不断分叉与合并的时间线。因果一致性没有线性一致性的协调开销,而且对网络问题的敏感性要低得多。 -但是,即使我们捕捉到因果顺序(例如使用兰伯特时间戳),有些事情也不能以这种方式实现:在“[光有时间戳排序还不够](#光有时间戳排序还不够)”中,我们考虑了确保用户名是唯一的,并拒绝同一用户名的并发注册。如果一个节点要接受注册,则需要知道另一个节点不是同时注册相同名称的过程。这个问题导致我们需要**共识**。 +但即使捕获到因果顺序(例如使用兰伯特时间戳),我们发现有些事情也不能通过这种方式实现:在“[光有时间戳排序还不够](#光有时间戳排序还不够)”一节的例子中,我们需要确保用户名是唯一的,并拒绝同一用户名的其他并发注册。如果一个节点要通过注册,则需要知道其他的节点没有在并发抢注同一用户名的过程中。这个问题引领我们走向**共识**。 -我们看到,达成共识意味着决定一件事情,使所有节点对所做决定达成一致,从而决定是不可撤销的。通过一些挖掘,事实证明,很广泛的一系列问题实际上都可以归结为共识问题,并且彼此是等价的(从这个意义上来讲,如果你有一个问题的解决方案,你可以容易地将它转换为其他问题的解决方案)。这种等价的问题包括: +我们看到,达成共识意味着以这样一种方式决定某件事:所有节点一致同意所做决定,且这一决定不可撤销。通过深入挖掘,结果我们发现很广泛的一系列问题实际上都可以归结为共识问题,并且彼此等价(从这个意义上来讲,如果你有其中之一的解决方案,就可以轻易将它转换为其他问题的解决方案)。这些等价的问题包括: ***线性一致性的CAS寄存器*** -寄存器需要基于当前值是否等于操作中给定的参数,自动决定是否设置其值。 +​ 寄存器需要基于当前值是否等于操作给出的参数,原子地**决定**是否设置新值。 ***原子事务提交*** -数据库必须决定是否提交或中止分布式事务。 +​ 数据库必须**决定**是否提交或中止分布式事务。 ***全序广播*** -消息传递系统必须决定传递消息的顺序。 +​ 消息系统必须**决定**传递消息的顺序。 ***锁和租约*** -当几个客户争抢锁或租约时,决定哪个客户端成功获得锁。 +​ 当几个客户端争抢锁或租约时,由锁来**决定**哪个客户端成功获得锁。 ***成员/协调服务*** -给定故障检测器(例如,超时),系统必须决定哪些节点处于活动状态,哪些应该被认为是死的,因为它们的会话超时。 +​ 给定某种故障检测器(例如超时),系统必须**决定**哪些节点活着,哪些节点因为会话超时需要被宣告死亡。 ***唯一性约束*** -当多个事务同时尝试使用相同的键创建冲突记录时,约束必须决定哪一个允许,哪个会违反约束而失败。 +​ 当多个事务同时尝试使用相同的键创建冲突记录时,约束必须**决定**哪一个被允许,哪些因为违反约束而失败。 +如果你只有一个节点,或者你愿意将决策的权能分配给单个节点,所有这些事都很简单。这就是在单领导者数据库中发生的事情:所有决策权归属于领导者,这就是为什么这样的数据库能够提供线性一致的操作,唯一性约束,完全有序的复制日志,以及更多。 +但如果该领导者失效,或者如果网络中断导致领导者不可达,这样的系统就无法取得任何进展。应对这种情况可以有三种方法: -如果你只有一个节点,或者你愿意将决策功能分配给单个节点,所有这些都很简单。这就是在一个单独的领导者数据库中发生的事情:决策的所有权力归属于领导者,这就是为什么这样的数据库能够提供线性一致性操作,唯一性约束,完全有序的复制日志等等。 +1. 等待领导者恢复,接受系统将在这段时间阻塞的事实。许多XA/JTA事务协调者选择这个选项。这种方法并不能完全达成共识,因为它不能满足**终止**属性的要求:如果领导者续命失败,系统可能会永久阻塞。 +2. 人工故障切换,让人类选择一个新的领导者节点,并重新配置系统使之生效,许多关系型数据库都采用这种方方式。这是一种来自“天意”的共识 —— 由计算机系统之外的运维人员做出决定。故障切换的速度受到人类行动速度的限制,通常要比计算机慢(得多)。 +3. 使用算法自动选择一个新的领导者。这种方法需要一种共识算法,使用成熟的算法来正确处理恶劣的网络条件是明智之举【107】。 -但是,如果单个领导失败,或者如果网络中断导致主库不可达,这样的系统变得无法取得进展。处理这种情况有三种方法: +尽管单领导者数据库可以提供线性一致性,且无需对每个写操作都执行共识算法,但共识对于保持及变更领导权仍然是必须的。因此从某种意义上说,使用单个领导者不过是“缓兵之计”:共识仍然是需要的,只是在另一个地方,而且没那么频繁。好消息是,容错的共识算法与容错的共识系统是存在的,我们在本章中简要地讨论了它们。 -1. 等待领导者恢复,同时系统将阻止接受新请求。许多XA/JTA事务协调者选择这个选项。这种方法并不能完全解决共识,因为它不能满足终止属性的要求:如果领导者没有恢复,系统可能会被永久封锁。 -2. 通过让人类选择一个新的领导者节点,并重新配置系统,执行手动故障切换来使用它。许多关系数据库都采用这种方法。这是一种“上帝的行为”的共识——计算机系统之外的运维人员做出决定。故障转移的速度受到人类行动速度的限制,通常比计算机慢。 -3. 使用算法自动选择一个新的领导。这种方法需要一个共识算法,建议使用经过验证的算法来正确处理不利的网络条件【107】。 +像ZooKeeper这样的工具为应用提供了“外包”的共识、故障检测和成员服务。它们扮演了重要的角色,虽说使用不易,但总比自己去开发一个能经受[第8章](ch8.md)中所有问题考验的算法要好得多。如果你发现自己想要解决的问题可以归结为共识,并且希望它能容错,使用一个类似ZooKeeper的东西是明智之举。 -尽管单主数据库可以提供线性一致性,而不需要在每个写操作上执行共识算法,但对于保持领导权和领导权的变更,共识仍然是需要的。因此从某种意义上说,使用单个领导者不过是“缓兵之计”:共识还是需要的,只是在不同的地方,而且没那么频繁。好消息是,容错算法和共识系统是存在的,我们在本章中简要地讨论了它们。 +尽管如此,并不是所有系统都需要共识:例如,无领导者复制和多领导者复制系统通常不会使用全局的共识。这些系统中出现的冲突(参见“[处理冲突](ch5.md#处理冲突)”)正是不同领导者之间没有达成共识的结果,但也这并没有关系:也许我们只是需要接受没有线性一致性的事实,并学会更好地与具有分支与合并版本历史的数据打交道。 -像ZooKeeper这样的工具为应用提供了“外包”的共识、故障检测和成员服务。它们扮演了重要的角色,虽说用起来并不容易,但总比自己去开发一个能经受[第8章](ch8.md)中所有问题考验的算法要好得多。如果你发现自己想要做的是一个可以归结为共识的东西,并且希望它能容错,使用一个类似ZooKeeper的东西是明智之举。 +本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于指导这一领域的实践有着极其重要的价值:它们帮助我们推理什么可以做,什么不可以做,帮助我们找到反直觉的分布式系统缺陷。如果你有时间,这些参考资料值得探索。 -尽管如此,并不是每个系统都需要共识:例如,无主复制和多主复制系统通常不会使用全局共识。这些系统中出现的冲突(参见“[处理冲突](ch5.md#处理冲突)”)是不同领导者之间没有达成共识的结果,但也许没关系:也许我们只需要简单地在没有线性一致性的环境下应对,并学会更好地与具有分支和合并版本历史的数据打交道。 - -本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于通知这一领域的实际工作是非常有价值的:它们帮助我们推理什么可以做,什么不可以做,帮助我们找到违反直觉的方法,其中分布式系统往往是有缺陷的。如果你有时间,这些参考资料是值得探索的。 - -这里就到了本书[第二部分](part-ii.md)的末尾,第二部介绍了复制([第5章](ch5.md)),分区([第6章](ch6.md)),事务([第7章](ch7.md)),分布式系统故障模型([第8章](ch8.md))以及最后的一致性和共识([第9章](ch9.md))。现在我们已经奠定了坚实的理论基础,在[第三部分](part-iii.md)我们将再次转向更实用的系统,并讨论如何从异构的组件积木块中构建强大的应用。 +这里已经到了本书[第二部分](part-ii.md)的末尾,第二部介绍了复制([第5章](ch5.md)),分区([第6章](ch6.md)),事务([第7章](ch7.md)),分布式系统的故障模型([第8章](ch8.md))以及最后的一致性与共识([第9章](ch9.md))。现在我们已经奠定了扎实的理论基础,我们将在[第三部分](part-iii.md)再次转向更实际的系统,并讨论如何使用异构的组件积木块构建强大的应用。 From 0aaf94ff0d50293a6627d31b0e9f80900f7596c8 Mon Sep 17 00:00:00 2001 From: Vonng Date: Sat, 31 Mar 2018 14:15:03 +0800 Subject: [PATCH 13/17] merge & fix preface --- ch1.md | 16 ++++++------ colophon.md | 10 ++++---- part-i.md | 2 +- preface.md | 74 +++++++++++++++++++++++++++-------------------------- 4 files changed, 52 insertions(+), 50 deletions(-) diff --git a/ch1.md b/ch1.md index 4778252..d9be488 100644 --- a/ch1.md +++ b/ch1.md @@ -2,7 +2,7 @@ ![](img/ch1.png) -> 互联网做得太棒了,以至于多数人将它看作像太平洋这样的自然资源,而不是什么人工产物。上一次出现这种大规模且无差错的技术, 你还记得是什么时候吗? +> 互联网做得太棒了,以至于大多数人将它看作像太平洋这样的自然资源,而不是什么人工产物。上一次出现这种大规模且无差错的技术, 你还记得是什么时候吗? > > ——阿兰·凯在接受Dobb博士杂志采访时说(2012年) @@ -34,9 +34,9 @@ ​ 定期处理累积的大批量数据 -上述组件如果听上去显而易见,那是因为这些**数据系统(data system)**是非常成功的抽象:我们一直不假思索地使用它们并习以为常。绝大多数工程师不会想从零开始编写存储引擎,因为在开发应用时,数据库已经是足够完美的工具了。 +如果这些功能听上去平淡无奇,那是因为这些**数据系统(data system)**是非常成功的抽象:我们一直不假思索地使用它们并习以为常。绝大多数工程师不会幻想从零开始编写存储引擎,因为在开发应用时,数据库已经是足够完美的工具了。 -但现实并没有这么简单。不同的应用有着不同的需求,因而数据库系统也是百花齐放,有着各式各样的特性。实现缓存有很多种手段,创建搜索索引也有好几种方法,诸如此类。因此在开发应用前,我们依然有必要先弄清楚最适合手头工作的工具和方法。而且当单个工具解决不了你的问题时,你会意识到组合使用这些工具是挺有难度的。 +但现实没有这么简单。不同的应用有着不同的需求,因而数据库系统也是百花齐放,有着各式各样的特性。实现缓存有很多种手段,创建搜索索引也有好几种方法,诸如此类。因此在开发应用前,我们依然有必要先弄清楚最适合手头工作的工具和方法。而且当单个工具解决不了你的问题时,组合使用这些工具可能还是有些难度的。 本书将是一趟关于数据系统原理、实践与应用的旅程,并讲述了设计数据密集型应用的方法。我们将探索不同工具之间的共性与特性,以及各自的实现原理。 @@ -205,7 +205,7 @@ 推特的第一个版本使用了方法1,但系统很难跟上主页时间线查询的负载。所以公司转向了方法2,方法2的效果更好,因为发推频率比查询主页时间线的频率几乎低了两个数量级,所以在这种情况下,最好在写入时做更多的工作,而在读取时做更少的工作。 -然而方法2的缺点是,发推现在需要大量的额外工作。平均来说,一条推文会发往约75个关注者,所以每秒4.6k的发推写入,变成了对主页时间线缓存每秒345k的写入。但这个平均值隐藏了用户粉丝数差异巨大这一现实,一些用户有超过3000万的粉丝,这意味着一条推文就可能会导致主页时间线缓存的3000万次写入!及时完成这种操作是一个巨大的挑战——推特尝试在5秒内向粉丝发送推文。 +然而方法2的缺点是,发推现在需要大量的额外工作。平均来说,一条推文会发往约75个关注者,所以每秒4.6k的发推写入,变成了对主页时间线缓存每秒345k的写入。但这个平均值隐藏了用户粉丝数差异巨大这一现实,一些用户有超过3000万的粉丝,这意味着一条推文就可能会导致主页时间线缓存的3000万次写入!及时完成这种操作是一个巨大的挑战 —— 推特尝试在5秒内向粉丝发送推文。 在推特的例子中,每个用户粉丝数的分布(可能按这些用户的发推频率来加权)是探讨可扩展性的一个关键负载参数,因为它决定了扇出负载。你的应用程序可能具有非常不同的特征,但可以采用相似的原则来考虑它的负载。 @@ -226,7 +226,7 @@ > #### 延迟和响应时间 > -> **延迟(latency)**和**响应时间(response time)**通常互当成同义词来使用,但实际上它们并不一样。响应时间是客户所看到的,除了实际处理请求的时间(**服务时间(service time)**)之外,还包括网络延迟和排队延迟。延迟是某个请求等待处理的**持续时长**,在此期间它处于**休眠(latent)**状态,并等待服务【17】。 +> **延迟(latency)**和**响应时间(response time)**经常用作同义词,但实际上它们并不一样。响应时间是客户所看到的,除了实际处理请求的时间(**服务时间(service time)**)之外,还包括网络延迟和排队延迟。延迟是某个请求等待处理的**持续时长**,在此期间它处于**休眠(latent)**状态,并等待服务【17】。 > 即使不断重复发送同样的请求,每次得到的响应时间也都会略有不同。现实世界的系统会处理各式各样的请求,响应时间可能会有很大差异。因此我们需要将响应时间视为一个可以测量的数值**分布(distribution)**,而不是单个数值。 @@ -237,7 +237,7 @@ **图1-4 展示了一个服务100次请求响应时间的均值与百分位数** -通常报表都会展示服务的平均响应时间。 (严格来讲“平均”一词并不指代任何特定公式,但实际上它通常被理解为**算术平均值(arithmetic mean)**:给定n个值,加起来除以n)。然而如果你想知道“**典型(typical)**”响应时间,那么平均值并不是一个非常好的指标,因为它不能告诉你有多少用户实际上经历了这个延迟。 +通常报表都会展示服务的平均响应时间。 (严格来讲“平均”一词并不指代任何特定公式,但实际上它通常被理解为**算术平均值(arithmetic mean)**:给定 n 个值,加起来除以 n )。然而如果你想知道“**典型(typical)**”响应时间,那么平均值并不是一个非常好的指标,因为它不能告诉你有多少用户实际上经历了这个延迟。 通常使用**百分位点(percentiles)**会更好。如果将响应时间列表按最快到最慢排序,那么**中位数(median)**就在正中间:举个例子,如果你的响应时间中位数是200毫秒,这意味着一半请求的返回时间少于200毫秒,另一半比这个要长。 @@ -245,7 +245,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百分位点(一万个请求中最慢的一个)被认为太昂贵了,不能为亚马逊的目标带来足够好处。减小高百分位点处的响应时间相当困难,因为它很容易受到随机事件的影响,这超出了控制范围,而且效益也很小。 @@ -386,7 +386,7 @@ 不幸的是,使应用可靠、可扩展或可持续并不容易。但是某些模式和技术会不断重新出现在不同的应用中。在接下来的几章中,我们将看到一些数据系统的例子,并分析它们如何实现这些目标。 -在本书后面的[第三部分](part-iii.md)中,我们将看到一种模式:几个组件协同工作以构成一个完整的系统(例如[图1-1](img/fig1-1.png)中的) +在本书后面的[第三部分](part-iii.md)中,我们将看到一种模式:几个组件协同工作以构成一个完整的系统(如[图1-1](img/fig1-1.png)中的例子) diff --git a/colophon.md b/colophon.md index 56b290e..0c963e4 100644 --- a/colophon.md +++ b/colophon.md @@ -16,18 +16,18 @@ Martin是一位常规会议演讲者,博主和开源贡献者。他认为, PostgreSQL DBA @ TanTan -前Alibaba+-Finplus 架构师/全栈工程师 (15.08 ~ 17.12) +Alibaba+-Finplus 架构师/全栈工程师 (2015 ~ 2017) ## 后记 -设计数据密集型应用程序封面上的动物是印度野猪(Sus scrofa cristatus),它是在印度,缅甸,尼泊尔,斯里兰卡和泰国发现的一种野猪的亚种。它们与欧洲公猪不同,它们具有较高的背部刷毛,没有羊毛底毛,以及更大,更直的头骨。 +《设计数据密集型应用》封面上的动物是**印度野猪(Sus scrofa cristatus)**,它是在印度,缅甸,尼泊尔,斯里兰卡和泰国发现的一种野猪的亚种。它们与欧洲公猪不同,它们具有较高的背部刷毛,没有羊毛底毛,以及更大,更直的头骨。 -印度的野猪有一头灰色或黑色的头发,脊椎上有僵硬的硬毛。男性有突出的犬齿(称为t),用来与对手战斗或抵御掠食者。男性比女性大,但这些物种平均肩高33-35英寸,体重200-300磅。他们的天敌包括熊,老虎和各种大型猫科动物。 +印度的野猪有一头灰色或黑色的头发,脊椎上有僵硬的硬毛。雄性有突出的犬齿(称为T),用来与对手战斗或抵御掠食者。雄性比雌性大,但这些物种平均肩高33-35英寸,体重200-300磅。他们的天敌包括熊,老虎和各种大型猫科动物。 -这些动物夜行和杂食 - 他们吃各种各样的东西,包括根,昆虫,腐肉,坚果,浆果和小动物。野猪也被称为通过垃圾和作物田地,造成大量的破坏,并赢得农民的仇恨。他们需要每天吃4,000-4,500卡路里。公猪有一个发达的嗅觉,这有助于他们寻找地下植物材料和挖掘动物。但是,他们的视力很差。 +这些动物夜行且杂食 —— 它们吃各种各样的东西,包括根,昆虫,腐肉,坚果,浆果和小动物。野猪也被称为通过垃圾和作物田地,造成大量的破坏,并赢得农民的仇恨。他们需要每天吃4,000 ~ 4,500卡路里。公猪有着发达的嗅觉,这有助于它们寻找地下的植物材料和挖掘动物。但是它们的视力很差。 -野猪在人类文化中一直具有重要意义。在印度教传说中,野猪是毗湿奴神的化身。在古希腊的丧葬纪念碑中,它是一个勇敢的失败者的象征(与胜利的狮子相反)。由于它的侵略,它被描绘在斯堪的纳维亚,日耳曼和盎格鲁 - 撒克逊战士的盔甲和武器上。在中国十二生肖中,它象征着决心和急躁。 +野猪在人类文化中一直具有重要意义。在印度教传说中,野猪是毗湿奴神的化身。在古希腊的丧葬纪念碑中,它是一个勇敢失败者的象征(与胜利的狮子相反)。由于它的侵略,它被描绘在斯堪的纳维亚,日耳曼和盎格鲁 ~ 撒克逊战士的盔甲和武器上。在中国十二生肖中,它象征着决心和急躁。 O'Reilly封面上的许多动物都受到威胁;所有这些对世界都很重要。要了解有关如何提供帮助的更多信息,请访问animals.oreilly.com。封面图片来自Shaw's Zoology。封面字体是URW Typewriter和Guardian Sans。文字字体是Adobe Minion Pro;图中的字体是Adobe Myriad Pro;标题字体是Adobe Myriad Condensed;代码字体是Dalton Maag的Ubuntu Mono。 \ No newline at end of file diff --git a/part-i.md b/part-i.md index f23ba26..97cf05a 100644 --- a/part-i.md +++ b/part-i.md @@ -5,7 +5,7 @@ 1. [第一章](ch1.md)将介绍本书使用的术语和方法。**可靠性,可扩展性和可维护性** ,这些词汇到底意味着什么?如何实现这些目标? 2. [第二章](ch2.md)将对几种不同的**数据模型和查询语言**进行比较。从程序员的角度看,这是数据库之间最明显的区别。不同的数据模型适用于不同的应用场景。 3. [第三章](ch3.md)将深入**存储引擎**内部,研究数据库如何在磁盘上摆放数据。不同的存储引擎针对不同的负载进行优化,选择合适的存储引擎对系统性能有巨大影响。 -4. [第四章](ch4)将对几种不同的 **数据编码**进行比较。特别地分析了他们在应用需求经常变化、模式需要随着时间演变的环境中的表现情况。 +4. [第四章](ch4)将对几种不同的 **数据编码**进行比较。特别研究了这些格式在应用需求经常变化、模式需要随时间演变的环境中表现如何。 第二部分将专门讨论在**分布式数据系统**中特有的问题。 diff --git a/preface.md b/preface.md index ed84019..6dbe527 100644 --- a/preface.md +++ b/preface.md @@ -1,4 +1,4 @@ -# 序 +# 序言 如果近几年从业于软件工程,特别是服务器端和后端系统开发,那么您很有可能已经被大量关于数据存储和处理的时髦词汇轰炸过了: NoSQL!大数据!Web-Scale!分片!最终一致性!ACID! CAP定理!云服务!MapReduce!实时! @@ -7,94 +7,96 @@ * 谷歌,雅虎,亚马逊,脸书,领英,微软和推特等互联网公司正在和巨大的流量/数据打交道,这迫使他们去创造能有效应对如此规模的新工具。 * 企业需要变得敏捷,需要低成本地检验假设,需要通过缩短开发周期和保持数据模型的灵活性,快速地响应新的市场洞察。 * 免费和开源软件变得非常成功,在许多环境中比商业软件和定制软件更受欢迎。 -* 处理器主频几乎没有增长,但是多核处理器已经成为标配,网络也越来越快。这意味着并行化只增不减。 +* 处理器主频几乎没有增长,但是多核处理器已经成为标配,网络也越来越快。这意味着并行化程度只增不减。 * 即使您在一个小团队中工作,现在也可以构建分布在多台计算机甚至多个地理区域的系统,这要归功于譬如亚马逊网络服务(AWS)等基础设施即服务(IaaS)概念的践行者。 * 许多服务都要求高可用,因停电或维护导致的服务不可用,变得越来越难以接受。 -数据密集型应用正在通过利用这些技术进步推动可能性的边界。如果**数据是其主要挑战**(数据量,数据复杂度或数据变化速度),这类应用被称为**数据密集型**,与之相对的是**计算密集型**,即处理器速度是其瓶颈。 +**数据密集型应用(data-intensive applications)**正在通过使用这些技术进步来推动可能性的边界。一个应用被称为**数据密集型**的,如果**数据是其主要挑战**(数据量,数据复杂度或数据变化速度)—— 与之相对的是**计算密集型**,即处理器速度是其瓶颈。 -工具和技术迅速适应着这些变化,帮助数据密集型应用程序来存储和处理数据。新型数据库系统(“NoSQL”)已经引起了众多关注,然而消息队列,缓存,搜索索引,批处理和流处理框架以及相关技术也非常重要。这些技术被组合使用在很多应用程序上。 +帮助数据密集型应用存储和处理数据的工具与技术,正迅速地适应这些变化。新型数据库系统(“NoSQL”)已经备受关注,而消息队列,缓存,搜索索引,批处理和流处理框架以及相关技术也非常重要。很多应用组合使用这些工具与技术。 -这些充满整个空间的流行语体现出人们对新可能性的热情,这是一件好事。但是,作为软件工程师和架构师,如果要建立良好的应用程序,我们还需要对各种层出不穷的技术及其利弊持有准确而严谨的技术性理解。为此,必须深入挖掘流行语背后的内容。 +这些生意盎然的时髦词汇体现出人们对新的可能性的热情,这是一件好事。但是作为软件工程师和架构师,如果要开发优秀的应用,我们还需要对各种层出不穷的技术及其利弊权衡有精准的技术理解。为了获得这种洞察,我们需要深挖时髦词汇背后的内容。 -幸运的是,在技术快速变化的背后,无论您使用的是哪个版本的特定工具,存在一些持久的原则。如果您了解这些原则,您就可以领会这些工具适用于何处,如何充分利用它们,以及如何避免其中的陷阱。这正是本书的初衷。 +幸运的是,在技术迅速变化的背后总是存在一些持续成立的原则,无论您使用了特定工具的哪个版本。如果您理解了这些原则,就可以领会这些工具的适用场景,如何充分利用它们,以及如何避免其中的陷阱。这正是本书的初衷。 -本书的目标是帮助您在多样而快速变化的处理和存储数据技术的大观园中找到方向。这本书不是一个特定工具的教程,也不是一本充满干枯理论的教科书。相反,我们将看到一些成功的数据系统示例:一些构成了许多流行应用程序基础,必须在每天的生产中满足可伸缩性,性能和可靠性需求的技术。 +本书的目标是帮助您在飞速变化的数据处理和数据存储技术大观园中找到方向。本书并不是某个特定工具的教程,也不是一本充满枯燥理论的教科书。相反,我们将看到一些成功数据系统的样例:许多流行应用每天都要在生产中会满足可扩展性、性能、以及可靠性的要求,而这些技术构成了这些应用的基础。 -我们将深入这些系统的内部,梳理他们的关键算法,讨论他们的原则和他们必须做出的权衡。在这个过程中,我们将尝试寻找有用的方式来思考数据系统 —— 不仅仅关于它们是如何工作的,还包括为什么它们以这种方式工作,以及我们需要问什么问题。 +我们将深入这些系统的内部,理清它们的关键算法,讨论背后的原则和它们必须做出的权衡。在这个过程中,我们将尝试寻找**思考**数据系统的有效方式 —— 不仅关于它们**如何**工作,还包括它们**为什么**以这种方式工作,以及哪些问题是我们需要问的。 -阅读本书后,您将能够很好地决定哪种技术适合哪种用途,并了解如何将工具组合起来,为一个良好应用架构奠定基础。您并不能够因此准备好从头开始构建自己的数据库存储引擎的技能,不过幸运地很,这很少是必要的。您将会获得的是,建立起对系统底层运行的敏锐直觉,这样您就有能力推理它们的行为,做出好的设计方案,并追踪任何可能出现的问题。 +阅读本书后,你能很好地决定哪种技术适合哪种用途,并了解如何将工具组合起来,为一个良好应用架构奠定基础。本书并不足以使你从头开始构建自己的数据库存储引擎,不过幸运的是这基本上很少有必要。你将获得对系统底层发生事情的敏锐直觉,这样你就有能力推理它们的行为,做出优秀的设计决策,并追踪任何可能出现的问题。 ## 本书的目标读者 -如果您开发的应用程序具有用于存储或处理数据的某种服务器/后端系统,并且您的应用程序使用互联网(例如,Web应用程序,移动应用程序或连接到互联网的传感器),那么本书就是为您准备的。 +如果你开发的应用具有用于存储或处理数据的某种服务器/后端系统,而且使用网络(例如,Web应用,移动应用或连接到互联网的传感器),那么本书就是为你准备的。 -本书适用于热爱编写代码的软件工程师,软件架构师和技术经理。如果您需要就您所从事的系统架构做出决定,例如您需要选择解决某个特定问题的工具,并找出如何最好地应用这些工具来解决问题,那么这本书对您尤其重要。但即使您不能选择您的工具,这本书仍将帮助您更好地了解您所使用工具的长处和短处。 +本书是为软件工程师,软件架构师,以及喜欢写代码的技术经理准备的。如果您需要对所从事系统的架构做出决策 —— 例如您需要选择解决某个特定问题的工具,并找出如何最好地使用这些工具,那么这本书对您尤有价值。但即使你无法选择你的工具,本书仍将帮助你更好地了解所使用工具的长处和短处。 -您应该具有构建基于Web的应用程序或网络服务的一些经验,并且您应该熟悉关系数据库和SQL。任何您了解的非关系型数据库和其他与数据相关的工具都会更有帮助,但不是必需的。常见的网络协议如TCP和HTTP的一般理解是有帮助的。编程语言或框架的选择对阅读本书没有任何不同影响。 +您应当具有一些开发Web应用或网络服务的经验,且应当熟悉关系型数据库和SQL。任何您了解的非关系型数据库和其他与数据相关工具都会有所帮助,但不是必需的。对常见网络协议如TCP和HTTP的大概理解是有帮助的。编程语言或框架的选择对阅读本书没有任何不同影响。 -如果以下任何一条对您是真的,您会发现这本书很有价值: +如果以下任意一条对您为真,你会发现这本书很有价值: -* 您想了解如何使数据系统可扩展,例如,支持拥有数百万用户的Web或移动应用程序。 -* 您需要提高应用程序的可用性(最大限度地减少停机时间)和运行稳定。 -* 您正在寻找使系统长期易于维护的方法,即使随着需求和技术的变化而变化。 -* 您对事物的运作方式有着天然的好奇心,并且希望知道一些主要网站和在线服务背后发生的事情。这本书打破了各种数据库和数据处理系统的内幕,探索他们设计中的智慧是非常有趣的。 +* 您想了解如何使数据系统可扩展,例如,支持拥有数百万用户的Web或移动应用。 +* 您需要提高应用程序的可用性(最大限度地减少停机时间),保持稳定运行。 +* 您正在寻找使系统在长期运行过程易于维护的方法,即使系统规模增长,需求与技术也发生变化。 +* 您对事物的运作方式有着天然的好奇心,并且希望知道一些主流网站和在线服务背后发生的事情。这本书打破了各种数据库和数据处理系统的内幕,探索这些系统设计中的智慧是非常有趣的。 -有时在讨论可扩展的数据系统时,人们会评论:“你又不在谷歌或亚马逊,别操心可扩展性了,直接上关系数据库。“这个陈述有一定的道理:为了您不需要的规模而构建程序不仅会浪费不必要的精力,并且可能会把您锁死在一个不灵活的设计中。实际上这是“过早优化”的一种形式。不过,选择合适的工具确实很重要,而不同的技术各有优缺点。我们将会看到,关系数据库虽然很重要,但绝不是数据处理的终章。 +有时在讨论可扩展的数据系统时,人们会说:“你又不在谷歌或亚马逊,别操心可扩展性了,直接上关系型数据库”。这个陈述有一定的道理:为了不必要的扩展性而设计程序,不仅会浪费不必要的精力,并且可能会把你锁死在一个不灵活的设计中。实际上这是一种“过早优化”的形式。不过,选择合适的工具确实很重要,而不同的技术各有优缺点。我们将看到,关系数据库虽然很重要,但绝不是数据处理的终章。 ## 本书涉及的领域 -本书不会试图给出详细的指导,说明如何安装或使用特定的软件包或API,因为已经有大量的文档。相反,我们讨论了基本的数据系统的各种原则和权衡,并探讨了不同产品所做出的不同设计决策。 +本书并不会尝试告诉读者如何安装或使用特定的软件包或API,因为已经有大量文档给出了详细的使用说明。相反,我们会讨论数据系统的基石——各种原则与利弊权衡,并探讨了不同产品所做出的不同设计决策。 -在电子书版本中,我们包含了在线资源全文的链接。所有链接都在发布时进行了验证,但不幸的是,由于网络的性质,链接往往频繁地中断。如果您遇到链接断开的情况,或者您正在阅读本书的打印副本,则可以使用搜索引擎查找引用。对于学术论文,您可以在Google学术搜索中搜索标题以查找开放获取的PDF文件。或者,您可以在[DDIA-Reference](https://github.com/ept/ddia-references)上找到所有的参考资料,我们在那里维护最新的链接。 +在电子书中包含了在线资源全文的链接。所有链接在出版时都进行了验证,但不幸的是,由于网络的自然规律,链接往往会频繁地破损。如果您遇到链接断开的情况,或者正在阅读本书的打印副本,可以使用搜索引擎查找参考文献。对于学术论文,您可以在Google学术中搜索标题,查找可以公开获取的PDF文件。或者,您也可以在 https://github.com/ept/ddia-references 中找到所有的参考资料,我们在那儿维护最新的链接。 -我们主要关注数据系统的体系结构以及它们被集成到数据密集型应用程序中的方式。本书没有讨论部署,操作,安全,管理等领域的空间 —— 这些都是复杂而重要的话题,仅仅在本书中用肤浅的注解讨论它们对它们不公平。它们各自配得上一本单独的书。 - -本书中描述的许多技术都被涵盖在**大数据**这个时髦词的范畴中。然而“大数据”这个术语被滥用,缺乏明确定义,以至于在严肃的工程讨论中没有用处。这本书使用更少歧义的术语,如“单点系统”之于”分布式系统“,或”在线/交互式“之于”离线/批处理系统“。 - -本书对自由和开放源码的软件(FOSS)有一定偏好,因为阅读,修改和执行源代码是了解一个东西详细工作原理的好方法。开放平台还可以降低供应商垄断的风险。然而,在适当的情况下,我们也讨论专有软件(封闭源码软件,软件即服务,或一些公司内部的仅在文献中描述但未公开发布的软件)。 +我们主要关注的是数据系统的**架构(architecture)**,以及它们被集成到数据密集型应用中的方式。本书没有足够的空间覆盖部署,运维,安全,管理等领域 —— 这些都是复杂而重要的主题,仅仅在本书中用粗略的注解讨论这些对它们很不公平。每个领域都值得用单独的书去讲。 +本书中描述的许多技术都被涵盖在**大数据(Big Data)**这个时髦词的范畴中。然而“大数据”这个术语被滥用,缺乏明确定义,以至于在严肃的工程讨论中没有用处。这本书使用歧义更小的术语,如“单节点”之于”分布式系统“,或”在线/交互式系统“之于”离线/批处理系统“。 +本书对自由和开源软件(FOSS)有一定偏好,因为阅读,修改和执行源码是了解一样东西详细工作原理的好方法。开放的平台也可以降低供应商垄断的风险。然而在适当的情况下,我们也会讨论专利软件(闭源软件,软件即服务 SaaS,或一些在文献中描述过但未公开发行的公司内部软件)。 ## 本书纲要 本书分为三部分: -1. 在[第一部分](part-i.md)中,我们会讨论设计数据密集型应用所赖的基本思想。我们从[第1章](ch1.md)开始,讨论我们实际要达到的目标:可靠性,可扩展性和可维护性;我们该如何考虑它们;以及如何实现它们。在[第2章](ch2.md)中,我们比较了几种不同的数据模型和查询语言,看看它们是如何适用于不同的场景。在[第3章](ch3.md)中将讨论存储引擎:数据库如何在磁盘上摆放数据,以便能高效地再次找到它。[第4章](ch4.md)转向数据编码(序列化),以及随时间演化的模式。 +1. 在[第一部分](part-i.md)中,我们会讨论设计数据密集型应用所赖的基本思想。我们从[第1章](ch1.md)开始,讨论我们实际要达到的目标:可靠性,可扩展性和可维护性;我们该如何思考这些概念;以及如何实现它们。在[第2章](ch2.md)中,我们比较了几种不同的数据模型和查询语言,看看它们如何适用于不同的场景。在[第3章](ch3.md)中将讨论存储引擎:数据库如何在磁盘上摆放数据,以便能高效地再次找到它。[第4章](ch4.md)转向数据编码(序列化),以及随时间演化的模式。 -2. 在[第二部分](part-ii.md)中,我们从讨论存储在一台机器上的数据转向讨论分布在多台机器上的数据。这对于可扩展性通常是必需的,但带来了各种独特的挑战。我们首先讨论复制([第5章](ch5.md)),分区/分片([第6章](ch6.md))和事务([第7章](ch7.md))。我们然后探索关于分布式系统的问题的更多细节([第8章](ch8.md)),以及在分布式系统中实现共识和一致性意味着什么([第9章](ch9.md))。 +2. 在[第二部分](part-ii.md)中,我们从讨论存储在一台机器上的数据转向讨论分布在多台机器上的数据。这对于可扩展性通常是必需的,但带来了各种独特的挑战。我们首先讨论复制([第5章](ch5.md)),分区/分片([第6章](ch6.md))和事务([第7章](ch7.md))。然后我们将探索关于分布式系统问题的更多细节([第8章](ch8.md)),以及在分布式系统中实现一致性与共识意味着什么([第9章](ch9.md))。 -3. 在[第三部分](part-iii.md)中,我们讨论从其他数据集衍生一些数据集的系统。派生数据经常发生在异构系统中:当没有一个数据库可以把所有事都做好时,应用程序需要集成几个不同的数据库,缓存,索引等等。在[第10章](ch10.md)中我们将从一种批处理派生数据的方法开始,然后我们将在第11章中在此基础上用流处理构建。最后,在[第12章](ch12.md)中,我们将所有内容放在一起,并讨论未来构建可靠,可伸缩和可维护的应用程序的方法。 +3. 在[第三部分](part-iii.md)中,我们讨论那些从其他数据集衍生出一些数据集的系统。衍生数据经常出现在异构系统中:当没有单个数据库可以把所有事情都做的很好时,应用需要集成几种不同的数据库,缓存,索引等。在[第10章](ch10.md)中我们将从一种衍生数据的批处理方法开始,然后在此基础上建立在[第11章](ch11.md)中讨论的流处理。最后,在[第12章](ch12.md)中,我们将所有内容汇总,讨论在将来构建可靠,可伸缩和可维护的应用程序的方法。 ## 参考文献与延伸阅读 -本书中讨论的大部分内容已经在其它地方以某种形式出现过了 —— 会议演示文稿,研究论文,博客文章,代码,BUG跟踪器,邮件列表,以及工程习惯中。本书总结了不同来源资料中最重要的想法,并在文本中包含了指向原始文献的指针。 如果你想更深入地探索一个领域,那么每章末尾的参考文献都是很好的资源,其中大部分可以免费在线获取。 +本书中讨论的大部分内容已经在其它地方以某种形式出现过了 —— 会议演示文稿,研究论文,博客文章,代码,BUG跟踪器,邮件列表,以及工程习惯中。本书总结了不同来源资料中最重要的想法,并在文本中包含了指向原始文献的链接。 如果你想更深入地探索一个领域,那么每章末尾的参考文献都是很好的资源,其中大部分可以免费在线获取。 ## O‘Reilly Safari -[Safari](http://oreilly.com/safari)赛高! +[Safari](http://oreilly.com/safari) (formerly Safari Books Online) is a membership-based training and reference platform for enterprise, government, educators, and individuals. + +Members have access to thousands of books, training videos, Learning Paths, interac‐ tive tutorials, and curated playlists from over 250 publishers, including O’Reilly Media, Harvard Business Review, Prentice Hall Professional, Addison-Wesley Pro‐ fessional, Microsoft Press, Sams, Que, Peachpit Press, Adobe, Focal Press, Cisco Press, John Wiley & Sons, Syngress, Morgan Kaufmann, IBM Redbooks, Packt, Adobe Press, FT Press, Apress, Manning, New Riders, McGraw-Hill, Jones & Bartlett, and Course Technology, among others. + +For more information, please visit http://oreilly.com/safari. ## 致谢 -本书融合了学术研究和工业实践的经验,是大量其他人的思想和知识的融合与系统化。在计算中,我们往往会被新的东西所吸引,但是我认为我们有很多东西可以从以前的东西中学习。这本书有超过800篇文章,博客文章,讲座,文档等参考资料,对我来说这是一个宝贵的学习资源。我非常感谢这些材料的作者分享他们的知识。 +本书融合了学术研究和工业实践的经验,融合并系统化了大量其他人的想法与知识。在计算领域,我们往往会被各种新鲜花样所吸引,但我认为前人完成的工作中,有太多值得我们学习的地方了。本书有800多处引用:文章,博客,讲座,文档等,对我来说这些都是宝贵的学习资源。我非常感谢这些材料的作者分享他们的知识。 -我也从个人对话中学到了很多东西,这要感谢大量的人花时间讨论想法,耐心地向我解释。特别感谢Joe Adler, Ross Anderson, Peter Bailis, Márton Balassi, Alastair Beresford, Mark Callaghan, Mat Clayton, Patrick Collison, Sean Cribbs, Shirshanka Das, Niklas Ekström, Stephan Ewen, Alan Fekete, Gyula Fóra, Camille Fournier, Andres Freund, John Garbutt, Seth Gilbert, Tom Haggett, Pat Hel‐ land, Joe Hellerstein, Jakob Homan, Heidi Howard, John Hugg, Julian Hyde, Conrad Irwin, Evan Jones, Flavio Junqueira, Jessica Kerr, Kyle Kingsbury, Jay Kreps, Carl Lerche, Nicolas Liochon, Steve Loughran, Lee Mallabone, Nathan Marz, Caitie McCaffrey, Josie McLellan, Christopher Meiklejohn, Ian Meyers, Neha Narkhede, Neha Narula, Cathy O’Neil, Onora O’Neill, Ludovic Orban, Zoran Perkov, Julia Powles, Chris Riccomini, Henry Robinson, David Rosenthal, Jennifer Rullmann, Matthew Sackman, Martin Scholl, Amit Sela, Gwen Shapira, Greg Spurrier, Sam Stokes, Ben Stopford, Tom Stuart, Diana Vasile, Rahul Vohra, Pete Warden, 以及 Brett Wooldridge. +我也从与人交流中学到了很多东西,很多人花费了宝贵的时间与我讨论想法并耐心解释。特别感谢 Joe Adler, Ross Anderson, Peter Bailis, Márton Balassi, Alastair Beresford, Mark Callaghan, Mat Clayton, Patrick Collison, Sean Cribbs, Shirshanka Das, Niklas Ekström, Stephan Ewen, Alan Fekete, Gyula Fóra, Camille Fournier, Andres Freund, John Garbutt, Seth Gilbert, Tom Haggett, Pat Hel‐ land, Joe Hellerstein, Jakob Homan, Heidi Howard, John Hugg, Julian Hyde, Conrad Irwin, Evan Jones, Flavio Junqueira, Jessica Kerr, Kyle Kingsbury, Jay Kreps, Carl Lerche, Nicolas Liochon, Steve Loughran, Lee Mallabone, Nathan Marz, Caitie McCaffrey, Josie McLellan, Christopher Meiklejohn, Ian Meyers, Neha Narkhede, Neha Narula, Cathy O’Neil, Onora O’Neill, Ludovic Orban, Zoran Perkov, Julia Powles, Chris Riccomini, Henry Robinson, David Rosenthal, Jennifer Rullmann, Matthew Sackman, Martin Scholl, Amit Sela, Gwen Shapira, Greg Spurrier, Sam Stokes, Ben Stopford, Tom Stuart, Diana Vasile, Rahul Vohra, Pete Warden, 以及 Brett Wooldridge. -通过审阅草案并提供反馈意见,更多的人对本书的撰写非常有价值。对于这些贡献,我特别感谢Raul Agepati,Tyler Akidau,Mattias Andersson,Sasha Baranov,Veena Basavaraj,David Beyer,Jim Brikman,Paul Carey,Raul Castro Fernandez,Joseph Chow,Derek Elkins,Sam Elliott,Alexander Gallego,Mark Grover ,斯图尔·万洛威(Stu Hallow Halloway),海蒂·霍华德(Heidi Howard),尼科拉·克莱普曼(Nicola Kleppmann),斯特凡·克鲁帕(Stefan Kruppa),比约恩·马德森(Bjorn Madsen),麦克尔·桑德(Sandder Mak),斯特凡·波德科维斯基(Stefan Podkowinski),菲尔·波特(Phil Potter)当然,对于本书中的任何遗留错误或不愉快的意见,我承担全部责任。 +更多人通过审阅草稿并提供反馈意见在本书的创作过程中做出了无价的贡献。我要特别感谢Raul Agepati, Tyler Akidau, Mattias Andersson, Sasha Baranov, Veena Basavaraj, David Beyer, Jim Brikman, Paul Carey, Raul Castro Fernandez, Joseph Chow, Derek Elkins, Sam Elliott, Alexander Gallego, Mark Grover, Stu Halloway, Heidi Howard, Nicola Kleppmann, Stefan Kruppa, Bjorn Madsen, Sander Mak, Stefan Podkowinski, Phil Potter, Hamid Ramazani, Sam Stokes, 以及Ben Summers。当然对于本书中的任何遗留错误或难以接受的见解,我都承担全部责任。 -为了帮助这本书出版,并且耐心地处理我缓慢的写作和不寻常的要求,我感谢编辑Marie Beaugureau,Mike Loukides,Ann Spencer和O'Reilly的所有团队。为了帮助找到合适的单词,我要感谢Rachel Head。尽管有其他的工作承诺给了我写作的时间和自由,但我要感谢Alastair Beresford,Susan Goodhue,Neha Narkhede和Kevin Scott。 +为了帮助这本书落地,并且耐心地处理我缓慢的写作和不寻常的要求,我要对编辑Marie Beaugureau,Mike Loukides,Ann Spencer和O'Reilly的所有团队表示感谢。我要感谢Rachel Head帮我找到了合适的术语。我要感谢Alastair Beresford,Susan Goodhue,Neha Narkhede和Kevin Scott,在其他工作事务之外给了我充分地创作时间和自由。 -非常特别的感谢是Shabbir Diwan和Edie Freedman,他们非常小心的说明了各章的地图。他们以非常规的创作地图的想法,使他们如此美丽和令人兴奋,真是太棒了。 +特别感谢Shabbir Diwan和Edie Freedman,他们非常用心地为各章配了地图。他们提出了不落俗套的灵感,创作了这些地图,美丽而引人入胜,真是太棒了。 -最后,我的爱情传到了我的家人和朋友身上,没有他,我将无法完成这个花了近四年时间的写作过程。你是最好的。 \ No newline at end of file +最后我要表达对家人和朋友们的爱,没有他们,我将无法走完这个将近四年的写作历程。你们是最棒的。 \ No newline at end of file From c6821d8a14af93a65bfd57cbde6057d8b76cbe21 Mon Sep 17 00:00:00 2001 From: Vonng Date: Sat, 31 Mar 2018 20:22:34 +0800 Subject: [PATCH 14/17] fmt fix --- ch2.md | 26 ++++++++++----------- ch3.md | 74 +++++++++++++++++++++++++++++++--------------------------- ch4.md | 4 ++-- ch7.md | 24 +++++++++---------- 4 files changed, 66 insertions(+), 62 deletions(-) diff --git a/ch2.md b/ch2.md index cca022d..924b912 100644 --- a/ch2.md +++ b/ch2.md @@ -11,18 +11,18 @@ [TOC] -数据模型可能是开发软件最重要的部分,因为它们有着深远的影响:不仅影响软件的编写方式,而且会影响我们的解题思路。 +数据模型可能是软件开发中最重要的部分了,因为它有着深远的影响:不仅影响软件的编写方式,而且会影响我们的**解题思路**。 -大多数应用程序是通过将一个数据模型叠加在另一个之上来构建的。对于每一层,关键问题是:它是如何用下一层来表示的?例如: +多数应用使用层层叠加的数据模型构建。对于每层数据模型的关键问题是:它是如何用低一层数据模型来表示的?例如: -1. 作为一名应用程序开发人员,您将看到现实世界(包括人员,组织,货物,行为,资金流向,传感器等),并根据对象或数据结构以及API进行建模,操纵这些数据结构。这些结构通常是应用程序特定的。 -2. 如果要存储这些数据结构,可以使用通用数据模型(如JSON或XML文档,关系数据库中的表、或图模型)来表示它们。 -3. 构建数据库软件的工程师决定以内存,磁盘或网络上的字节表示JSON/XML/关系/图数据。该表示可以允许以各种方式查询,搜索,操纵和处理数据。 -4. 在更低的层面上,硬件工程师已经计算出如何用电流,光脉冲,磁场等来表示字节。 +1. 作为一名应用开发人员,你对现实世界进行观察(包括人员,组织,货物,行为,资金流向,传感器等),并使用对象或数据结构建模,提供操纵这些数据结构的API。这些结构通常是应用特定的。 +2. 当你想要存储这些存储这些数据结构时,你可以使用通用数据模型来表示它们,例如JSON或XML文档,关系型数据库中的表、或者图数据模型。 +3. 开发数据库软件的工程师需要决定如何使用内存、磁盘或网络中的字节来表示这些数据结构(JSON/XML/关系/图)。这种表示可以允许数据以各种方式被查询,搜索,操纵和处理。 +4. 在更低的层次上,硬件工程师已经想出了使用电流,光脉冲,磁场或其他东西来表示这些字节的方法。 -在一个复杂的应用程序中,可能会有更多的中间层次,比如基于API的API,但是基本思想仍然是一样的:每个层都通过提供一个干净的数据模型来隐藏下面层的复杂性。这些抽象允许不同的人群(例如数据库供应商的工程师和使用他们的数据库的应用程序开发人员)有效地协作。 +在一个复杂的应用中,可能会有更多的中间层,比如基于API 的API,但是基本思想仍然是一样的:每个层都通过提供一个干净整洁的数据模型来隐藏更低层次中的复杂度。这些抽象允许不同的人群有效地协作(例如数据库厂商的工程师,与使用其数据库的应用开发者)。 -有许多不同类型的数据模型,每个数据模型都体现了如何使用它的假设。某些用法很容易,有些不被支持;一些操作很快,一些操作不好;一些数据转换感觉自然,有些是尴尬的。 +有许多不同类型的数据模型,每个数据模型都带有如何使用的假设。某些用法很容易,有些不被支持;一些操作很快,一些操作不好;一些数据转换感觉自然,有些是尴尬的。 掌握一个数据模型可能需要很多努力(想想关系数据建模有多少本书)。即使只使用一种数据模型,而不用担心其内部工作,构建软件也是非常困难的。但是由于数据模型对软件的功能有很大的影响,因此选择适合应用程序的软件是非常重要的。 @@ -32,7 +32,7 @@ ## 关系模型与文档模型 -现在最着名的数据模型可能是SQL,它基于Edgar Codd在1970年提出的关系模型【1】:数据被组织到关系中(称为SQL表),其中每个关系是元组的无序集合SQL中的行)。 +现在最著名的数据模型可能是SQL,它基于Edgar Codd在1970年提出的关系模型【1】:数据被组织到关系中(称为SQL表),其中每个关系是元组的无序集合SQL中的行)。 关系模型是一个理论上的提议,当时很多人都怀疑是否能够有效实现。然而到了20世纪80年代中期,关系数据库管理系统(RDBMSes)和SQL已成为大多数需要存储和查询具有某种规模结构的数据的人们的首选工具。关系数据库的优势已经持续了大约25~30年——计算史中的永恒。 @@ -908,19 +908,19 @@ Datalog方法需要对本章讨论的其他查询语言采取不同的思维方 1. 文档数据库的应用场景是:数据通常是自我包含的,而且文档之间的关系非常罕见。 2. 图形数据库用于相反的场景: 任何东西都可能和任何东西相关联。 -所有这三种模型(文档,关系和图形)今天都被广泛使用,并且在各自的领域都是很好用的。一个模型可以用另一个模型来模拟 - 例如,图形数据可以在关系数据库中表示 - 但结果往往是尴尬的。这就是为什么我们有不同的系统用于不同的目的,而不是一个单一的万能解决方案。 +所有这三种模型(文档,关系和图形)今天都被广泛使用,并且在各自的领域都是很好用的。一个模型可以用另一个模型来模拟 —— 例如,图数据可以在关系数据库中表示 —— 但结果往往是尴尬的。这就是为什么我们有着用于不同目的的不同系统,而不是一个单一的万能解决方案。 文档数据库和图数据库有一个共同点,那就是它们通常不会为存储的数据强制实施一个模式,这可以使应用程序更容易适应不断变化的需求。但是应用程序很可能仍假定数据具有一定的结构:这只是模式是明确的(强制写入)还是隐含的(在读取时处理)的问题。 -每个数据模型都带有自己的查询语言或框架,我们讨论了几个例子:SQL,MapReduce,MongoDB的聚合管道,Cypher,SPARQL和Datalog。我们也谈到了CSS和XSL/XPath,它们不是数据库查询语言,而包含有趣的相似之处。 +每个数据模型都带有自己的查询语言或框架,我们讨论了几个例子:SQL,MapReduce,MongoDB的聚合管道,Cypher,SPARQL和Datalog。我们也谈到了CSS和 XSL/XPath,它们不是数据库查询语言,而包含有趣的相似之处。 虽然我们已经覆盖了很多地方,但仍然有许多数据模型没有提到。举几个简单的例子: * 研究人员使用基因组数据通常需要执行序列相似性搜索,这意味着需要一个很长的字符串(代表一个DNA分子),并将其与一个类似但不完全相同的大型字符串数据库进行匹配。这里所描述的数据库都不能处理这种用法,这就是为什么研究人员编写了像GenBank这样的专门的基因组数据库软件的原因【48】。 * 粒子物理学家数十年来一直在进行大数据类型的大规模数据分析,像大型强子对撞机(LHC)这样的项目现在可以工作在数百亿兆字节的范围内!在这样的规模下,需要定制解决方案来阻止硬件成本从失控中解脱出来【49】。 -* 全文搜索可以说是一种经常与数据库一起使用的数据模型。信息检索是一个大的专业课题,在本书中我们不会详细介绍,但是我们将在第三章和第三章中介绍搜索指标。 +* 全文搜索可以说是一种经常与数据库一起使用的数据模型。信息检索是一个大的专业课题,在本书中我们不会详细介绍,但是我们将在第三章和第三章中介绍搜索索引。 -在下一章中,我们将讨论在实现本章描述的数据模型时会发挥的一些权衡。 +在[下一章](ch3.md)中,我们将讨论在实现本章描述的数据模型时会遇到的一些权衡。 diff --git a/ch3.md b/ch3.md index 575e50d..1570e6e 100644 --- a/ch3.md +++ b/ch3.md @@ -13,7 +13,7 @@ 在最基本的层次上,一个数据库需要完成两件事情:当你给它数据时,它应该存储起来,而当你提问时,它应该把数据返回给你。 -在第二章中,我们讨论了数据模型和查询语言,即程序员录入数据库的数据格式,以及你可以再次获取它的机制。在本章中,我们讨论同样的问题,却是从数据库的视角:数据库如何存储我们提供的数据,以及如何在我们需要时重新找到数据。 +在[第2章](ch2.md)中,我们讨论了数据模型和查询语言,即程序员录入数据库的数据格式,以及你可以再次获取它的机制。在本章中,我们讨论同样的问题,却是从数据库的视角:数据库如何存储我们提供的数据,以及如何在我们需要时重新找到数据。 作为程序员,为什么要关心数据库如何在内部处理存储和检索?你可能不会从头开始实现自己的存储引擎,但是您需要从可用的许多存储引擎中选择适合应用程序的存储引擎。为了调谐一个存储引擎以适应应用工作负载,你需要大致了解存储引擎在做什么。 @@ -21,7 +21,7 @@ 但是,我们将从您最可能熟悉的两大类数据库:传统关系型数据库与所谓的“NoSQL”数据库开始,通过介绍它们的存储引擎来开始本章的内容。 -我们会研究两大类存储引擎:日志结构(log-structured)的存储引擎,以及面向页面(page-oriented)的存储引擎(如B树)。 +我们会研究两大类存储引擎:**日志结构(log-structured)**的存储引擎,以及**面向页面(page-oriented)**的存储引擎(如B树)。 @@ -40,7 +40,7 @@ db_get () { } ``` -这两个函数实现了键值存储的功能。执行`db_set key value`,会将`key`和`value`存储在数据库中。键和值可以是(几乎)任何你喜欢的东西,例如,值可以是JSON文档。然后调用`db_get key`,查找与该键关联的最新值并将其返回。麻雀虽小,五脏俱全: +这两个函数实现了键值存储的功能。执行 `db_set key value` ,会将 `key` 和 `value` 存储在数据库中。键和值可以是(几乎)任何你喜欢的东西,例如,值可以是JSON文档。然后调用 `db_get key` ,查找与该键关联的最新值并将其返回。麻雀虽小,五脏俱全: ```bash $ db_set 123456 '{"name":"London","attractions":["Big Ben","London Eye"]}' $ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}' @@ -48,16 +48,19 @@ $ db_get 42 {"name":"San Francisco","attractions":["Golden Gate Bridge"]} ``` -底层的存储格式非常简单:一个文本文件,每行包含一条逗号分隔的键值对(忽略转义问题的话,大致类似于CSV文件)。每次对`db_set`的调用都会追加记录到文件末尾,所以更新键的时候旧版本的值不会被覆盖。需要查看文件中最后一次出现的键以查找最新值(因此`db_get`中使用了`tail -n 1 `。) +底层的存储格式非常简单:一个文本文件,每行包含一条逗号分隔的键值对(忽略转义问题的话,大致类似于CSV文件)。每次对 `db_set` 的调用都会追加记录到文件末尾,所以更新键的时候旧版本的值不会被覆盖。需要查看文件中最后一次出现的键以查找最新值(因此 `db_get` 中使用了 `tail -n 1 ` 。) ```bash -$ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}' $ db_get 42 - {"name":"San Francisco","attractions":["Exploratorium"]} +$ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}' + +$ db_get 42 +{"name":"San Francisco","attractions":["Exploratorium"]} + $ cat database 123456,{"name":"London","attractions":["Big Ben","London Eye"]} 42,{"name":"San Francisco","attractions":["Golden Gate Bridge"]} 42,{"name":"San Francisco","attractions":["Exploratorium"]} ``` -`db_set`函数对于极其简单的场景其实有非常好的性能,因为在文件尾部追加写入通常是非常高效的。与`db_set`做的事情类似,许多数据库内部使用日志(log),也就是一个Append-Only的数据文件。真正的数据库有更多的问题需要处理(如并发控制,回收磁盘空间以免日志无限增长,处理错误和部分写入记录),但基本原理是一样的。日志非常有用,我们还将在本书的其它部分见到它。 +`db_set` 函数对于极其简单的场景其实有非常好的性能,因为在文件尾部追加写入通常是非常高效的。与`db_set`做的事情类似,许多数据库内部使用**日志(log)**,也就是一个**仅追加(append-only)**的数据文件。真正的数据库有更多的问题需要处理(如并发控制,回收磁盘空间以免日志无限增长,处理错误和部分写入记录),但基本原理是一样的。日志非常有用,我们还将在本书的其它部分见到它。 > 日志这个词通常用来指应用程序日志,应用程序输出描述发生事情的文本。本书中,日志用于更一般的含义上:一个只有追加记录的序列。它不一定是人类可读的记录,它可能是只能由其他程序读取的二进制记录。 @@ -73,7 +76,7 @@ $ cat database ### 哈希索引 -让我们从键值数据(key-value Data)的索引开始。这不是您可以索引的唯一一种数据类型,但键值数据是非常常见的。对于更复杂的索引来说,这是一个有用的构建模块。 +让我们从**键值数据(key-value Data)**的索引开始。这不是您可以索引的唯一一种数据类型,但键值数据是非常常见的。对于更复杂的索引来说,这是一个有用的构建模块。 键值存储与在大多数编程语言中可以找到的字典类型非常相似,通常字典都是用散列表(哈希表)实现的。哈希映射在许多算法教科书中都有描述【1,2】,所以我们不会详细讨论它们工作方式。既然我们已经有内存数据结构——HashMap,为什么不使用它们来索引在磁盘上的数据呢? @@ -158,7 +161,7 @@ $ cat database 如果在几个输入段中出现相同的键,该怎么办?请记住,每个段都包含在一段时间内写入数据库的所有值。这意味着一个输入段中的所有值必须比另一个段中的所有值更新(假设我们总是合并相邻的段)。当多个段包含相同的键时,我们可以保留最近段的值,并丢弃旧段中的值。 -2. 为了在文件中找到一个特定的键,你不再需要保存内存中所有键的索引。以[图3-5](img/fig3-5.png)为例:假设你正在内存中寻找键`handiwork`,但是你不知道段文件中该关键字的确切偏移量。然而,你知道`handbag`和`handsome`的偏移,而且由于排序特性,你知道`handiwork`必须出现在这两者之间。这意味着您可以跳到`handbag`的偏移位置并从那里扫描,直到您找到`handiwork`(或没找到,如果该文件中没有该键)。 +2. 为了在文件中找到一个特定的键,你不再需要保存内存中所有键的索引。以[图3-5](img/fig3-5.png)为例:假设你正在内存中寻找键 `handiwork`,但是你不知道段文件中该关键字的确切偏移量。然而,你知道 `handbag` 和 `handsome` 的偏移,而且由于排序特性,你知道 `handiwork` 必须出现在这两者之间。这意味着您可以跳到 `handbag` 的偏移位置并从那里扫描,直到您找到 `handiwork`(或没找到,如果该文件中没有该键)。 ![](img/fig3-5.png) @@ -174,7 +177,7 @@ $ cat database 到目前为止,但是如何让你的数据首先被按键排序呢?我们的传入写入可以以任何顺序发生。 -在磁盘上维护有序结构是可能的(参阅“B-Tree”),但在内存保存则要容易得多。有许多可以使用的众所周知的树形数据结构,例如红黑树或AVL树【2】。使用这些数据结构,您可以按任何顺序插入键,并按排序顺序读取它们。 +在磁盘上维护有序结构是可能的(参阅“[B树](#B树)”),但在内存保存则要容易得多。有许多可以使用的众所周知的树形数据结构,例如红黑树或AVL树【2】。使用这些数据结构,您可以按任何顺序插入键,并按排序顺序读取它们。 现在我们可以使我们的存储引擎工作如下: @@ -211,7 +214,7 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使 我们前面看到的日志结构索引将数据库分解为可变大小的段,通常是几兆字节或更大的大小,并且总是按顺序编写段。相比之下,B树将数据库分解成固定大小的块或页面,传统上大小为4 KB(有时会更大),并且一次只能读取或写入一个页面。这种设计更接近于底层硬件,因为磁盘也被安排在固定大小的块中。 -每个页面都可以使用地址或位置来标识,这允许一个页面引用另一个页面 - 类似于指针,但在磁盘而不是在内存中。我们可以使用这些页面引用来构建一个页面树,如[图3-6](img/fig3-6.png)所示。 +每个页面都可以使用地址或位置来标识,这允许一个页面引用另一个页面 —— 类似于指针,但在磁盘而不是在内存中。我们可以使用这些页面引用来构建一个页面树,如[图3-6](img/fig3-6.png)所示。 ![](img/fig3-6.png) @@ -231,17 +234,17 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使 **图3-7 通过分割页面来生长B树** -该算法确保树保持平衡:具有n个键的B树总是具有O(log n)的深度。大多数数据库可以放入一个三到四层的B树,所以你不需要遵循许多页面引用来找到你正在查找的页面。 (分支因子为500的4 KB页面的四级树可以存储多达256 TB。) +该算法确保树保持平衡:具有 n 个键的B树总是具有$O(log n)$的深度。大多数数据库可以放入一个三到四层的B树,所以你不需要遵循许多页面引用来找到你正在查找的页面。 (分支因子为500的4KB页面的四级树可以存储多达256 TB。) #### 让B树更可靠 -B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆盖不改变页面的位置;即,当页面被覆盖时,对该页面的所有引用保持完整。这与日志结构索引(如LSM-trees)形成鲜明对比,后者只附加到文件(并最终删除过时的文件),但从不修改文件。 +B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆盖不改变页面的位置;即,当页面被覆盖时,对该页面的所有引用保持完整。这与日志结构索引(如LSM树)形成鲜明对比,后者只附加到文件(并最终删除过时的文件),但从不修改文件。 您可以考虑将硬盘上的页面覆盖为实际的硬件操作。在磁性硬盘驱动器上,这意味着将磁头移动到正确的位置,等待旋转盘上的正确位置出现,然后用新的数据覆盖适当的扇区。在固态硬盘上,由于SSD必须一次擦除和重写相当大的存储芯片块,所以会发生更复杂的事情【19】。 而且,一些操作需要覆盖几个不同的页面。例如,如果因为插入导致页面过度而拆分页面,则需要编写已拆分的两个页面,并覆盖其父页面以更新对两个子页面的引用。这是一个危险的操作,因为如果数据库在仅有一些页面被写入后崩溃,那么最终将导致一个损坏的索引(例如,可能有一个孤儿页面不是任何父项的子项) 。 -为了使数据库对崩溃具有韧性,B树实现通常会带有一个额外的磁盘数据结构:预写式日志(WAL,也称为重做日志)。这是一个只能追加的文件,每个B树修改都可以应用到树本身的页面上。当数据库在崩溃后恢复时,这个日志被用来恢复B树回到一致的状态【5,20】。 +为了使数据库对崩溃具有韧性,B树实现通常会带有一个额外的磁盘数据结构:**预写式日志(write-ahead-log)**(WAL,也称为重做日志)。这是一个只能追加的文件,每个B树修改都可以应用到树本身的页面上。当数据库在崩溃后恢复时,这个日志被用来使B树恢复到一致的状态【5,20】。 更新页面的一个额外的复杂情况是,如果多个线程要同时访问B树,则需要仔细的并发控制 - 否则线程可能会看到树处于不一致的状态。这通常通过使用**锁存器(latches)**(轻量级锁)保护树的数据结构来完成。日志结构化的方法在这方面更简单,因为它们在后台进行所有的合并,而不会干扰传入的查询,并且不时地将旧的分段原子交换为新的分段。 @@ -291,9 +294,9 @@ B树在数据库体系结构中是非常根深蒂固的,为许多工作负载 到目前为止,我们只讨论了关键值索引,它们就像关系模型中的**主键(primary key)**索引。主键唯一标识关系表中的一行,或文档数据库中的一个文档或图形数据库中的一个顶点。数据库中的其他记录可以通过其主键(或ID)引用该行/文档/顶点,并且索引用于解析这样的引用。 -有二级索引也很常见。在关系数据库中,您可以使用`CREATE INDEX`命令在同一个表上创建多个二级索引,而且这些索引通常对于有效地执行联接而言至关重要。例如,在[第2章](ch2.md)中的[图2-1](img/fig2-1.png)中,很可能在`user_id`列上有一个二级索引,以便您可以在每个表中找到属于同一用户的所有行。 +有二级索引也很常见。在关系数据库中,您可以使用 `CREATE INDEX` 命令在同一个表上创建多个二级索引,而且这些索引通常对于有效地执行联接而言至关重要。例如,在[第2章](ch2.md)中的[图2-1](img/fig2-1.png)中,很可能在 `user_id` 列上有一个二级索引,以便您可以在每个表中找到属于同一用户的所有行。 -一个二级索引可以很容易地从一个键值索引构建。主要的不同是Key不是唯一的。即可能有许多行(文档,顶点)具有相同的键。这可以通过两种方式来解决:或者通过使索引中的每个值,成为匹配行标识符的列表(如全文索引中的发布列表),或者通过向每个索引添加行标识符来使每个关键字唯一。无论哪种方式,B树和日志结构索引都可以用作辅助索引。 +一个二级索引可以很容易地从一个键值索引构建。主要的不同是键不是唯一的。即可能有许多行(文档,顶点)具有相同的键。这可以通过两种方式来解决:或者通过使索引中的每个值,成为匹配行标识符的列表(如全文索引中的发布列表),或者通过向每个索引添加行标识符来使每个关键字唯一。无论哪种方式,B树和日志结构索引都可以用作辅助索引。 #### 将值存储在索引中 @@ -302,7 +305,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】。 与任何类型的数据重复一样,聚簇和覆盖索引可以加快读取速度,但是它们需要额外的存储空间,并且会增加写入开销。数据库还需要额外的努力来执行事务保证,因为应用程序不应该因为重复而导致不一致。 @@ -321,7 +324,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079 一个标准的B树或者LSM树索引不能够高效地响应这种查询:它可以返回一个纬度范围内的所有餐馆(但经度可能是任意值),或者返回在同一个经度范围内的所有餐馆(但纬度可能是北极和南极之间的任意地方),但不能同时满足。 -一种选择是使用空间填充曲线将二维位置转换为单个数字,然后使用常规B树索引【34】。更普遍的是,使用特殊化的空间索引,例如R树。例如,PostGIS使用PostgreSQL的通用Gist工具【35】将地理空间索引实现为R-树。这里我们没有足够的地方来描述R树,但是有大量的文献可供参考。 +一种选择是使用空间填充曲线将二维位置转换为单个数字,然后使用常规B树索引【34】。更普遍的是,使用特殊化的空间索引,例如R树。例如,PostGIS使用PostgreSQL的通用Gist工具【35】将地理空间索引实现为R树。这里我们没有足够的地方来描述R树,但是有大量的文献可供参考。 一个有趣的主意是,多维索引不仅可以用于地理位置。例如,在电子商务网站上可以使用维度(红色,绿色,蓝色)上的三维索引来搜索特定颜色范围内的产品,也可以在天气观测数据库中搜索二维(日期,温度)的指数,以便有效地搜索2013年的温度在25至30°C之间的所有观测资料。使用一维索引,你将不得不扫描2013年的所有记录(不管温度如何),然后通过温度进行过滤,反之亦然。 二维索引可以同时通过时间戳和温度来收窄数据集。这个技术被HyperDex使用【36】。 @@ -362,14 +365,15 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079 在业务数据处理的早期,对数据库的写入通常对应于正在进行的商业交易:进行销售,向供应商下订单,支付员工工资等等。随着数据库扩展到那些没有不涉及钱易手,术语交易仍然卡住,指的是形成一个逻辑单元的一组读写。 事务不一定具有ACID(原子性,一致性,隔离性和持久性)属性。事务处理只是意味着允许客户端进行低延迟读取和写入 - 而不是批量处理作业,而这些作业只能定期运行(例如每天一次)。我们在[第7章](ch7.md)中讨论ACID属性,在[第10章](ch10.md)中讨论批处理。 -即使数据库开始被用于许多不同类型的博客文章,游戏中的动作,地址簿中的联系人等等,基本访问模式仍然类似于处理业务事务。应用程序通常使用索引通过某个键查找少量记录。根据用户的输入插入或更新记录。由于这些应用程序是交互式的,因此访问模式被称为在线事务处理(OLTP)。 +即使数据库开始被用于许多不同类型的博客文章,游戏中的动作,地址簿中的联系人等等,基本访问模式仍然类似于处理业务事务。应用程序通常使用索引通过某个键查找少量记录。根据用户的输入插入或更新记录。由于这些应用程序是交互式的,因此访问模式被称为**在线事务处理(OLTP, OnLine Transaction Processing)**。 + 但是,数据库也开始越来越多地用于数据分析,这些数据分析具有非常不同的访问模式。通常,分析查询需要扫描大量记录,每个记录只读取几列,并计算汇总统计信息(如计数,总和或平均值),而不是将原始数据返回给用户。例如,如果您的数据是一个销售交易表,那么分析查询可能是: * 一月份我们每个商店的总收入是多少? * 我们在最近的推广活动中销售多少香蕉? * 哪种品牌的婴儿食品最常与X品牌的尿布一起购买? -这些查询通常由业务分析师编写,并提供给帮助公司管理层做出更好决策(商业智能)的报告。为了区分这种使用数据库的事务处理模式,它被称为在线分析处理(OLAP)。【47】。OLTP和OLAP之间的区别并不总是清晰的,但是一些典型的特征在[表3-1]()中列出。 +这些查询通常由业务分析师编写,并提供给帮助公司管理层做出更好决策(商业智能)的报告。为了区分这种使用数据库的事务处理模式,它被称为**在线分析处理(OLAP, OnLine Analytice Processing)**。【47】。OLTP和OLAP之间的区别并不总是清晰的,但是一些典型的特征在[表3-1]()中列出。 **表3-1 比较交易处理和分析系统的特点** @@ -389,7 +393,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079 这些OLTP系统通常具有高度的可用性,并以低延迟处理事务,因为这些系统往往对业务运作至关重要。因此数据库管理员密切关注他们的OLTP数据库他们通常不愿意让业务分析人员在OLTP数据库上运行临时分析查询,因为这些查询通常很昂贵,扫描大部分数据集,这会损害同时执行的事务的性能。 -相比之下,数据仓库是一个独立的数据库,分析人员可以查询他们心中的内容,而不影响OLTP操作【48】。数据仓库包含公司所有各种OLTP系统中的只读数据副本。从OLTP数据库中提取数据(使用定期的数据转储或连续的更新流),转换成适合分析的模式,清理并加载到数据仓库中。将数据存入仓库的过程称为“抽取-转换-加载(ETL)”,如[图3-8](img/fig3-8)所示。 +相比之下,数据仓库是一个独立的数据库,分析人员可以查询他们心中的内容,而不影响OLTP操作【48】。数据仓库包含公司所有各种OLTP系统中的只读数据副本。从OLTP数据库中提取数据(使用定期的数据转储或连续的更新流),转换成适合分析的模式,清理并加载到数据仓库中。将数据存入仓库的过程称为“**抽取-转换-加载(ETL)**”,如[图3-8](img/fig3-8)所示。 ![](img/fig3-8.png) @@ -401,7 +405,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079 #### OLTP数据库和数据仓库之间的分歧 -数据仓库的数据模型通常是关系型的,因为SQL通常很适合分析查询。有许多图形数据分析工具可以生成SQL查询,可视化结果,并允许分析人员(通过下钻,切片和切块等操作)探索数据。 +数据仓库的数据模型通常是关系型的,因为SQL通常很适合分析查询。有许多图形数据分析工具可以生成SQL查询,可视化结果,并允许分析人员探索数据(通过下钻,切片和切块等操作)。 表面上,一个数据仓库和一个关系OLTP数据库看起来很相似,因为它们都有一个SQL查询接口。然而,系统的内部看起来可能完全不同,因为它们针对非常不同的查询模式进行了优化。现在许多数据库供应商都将重点放在支持事务处理或分析工作负载上,而不是两者都支持。 @@ -423,7 +427,7 @@ Teradata,Vertica,SAP HANA和ParAccel等数据仓库供应商通常使用昂 事实表中的一些列是属性,例如产品销售的价格和从供应商那里购买的成本(允许计算利润余额)。事实表中的其他列是对其他表(称为维表)的外键引用。由于事实表中的每一行都表示一个事件,因此这些维度代表事件的发生地点,时间,方式和原因。 -例如,在[图3-9](img/fig3-9.md)中,其中一个维度是已售出的产品。 `dim_product`表中的每一行代表一种待售产品,包括库存单位(SKU),说明,品牌名称,类别,脂肪含量,包装尺寸等。`fact_sales`表中的每一行都使用外部表明在特定交易中销售了哪些产品。 (为了简单起见,如果客户一次购买几种不同的产品,则它们在事实表中被表示为单独的行)。 +例如,在[图3-9](img/fig3-9.md)中,其中一个维度是已售出的产品。 `dim_product`表中的每一行代表一种待售产品,包括**库存单位(SKU)**,说明,品牌名称,类别,脂肪含量,包装尺寸等。`fact_sales`表中的每一行都使用外部表明在特定交易中销售了哪些产品。 (为了简单起见,如果客户一次购买几种不同的产品,则它们在事实表中被表示为单独的行)。 即使日期和时间通常使用维度表来表示,因为这允许对日期(诸如公共假期)的附加信息进行编码,从而允许查询区分假期和非假期的销售。 @@ -439,7 +443,7 @@ Teradata,Vertica,SAP HANA和ParAccel等数据仓库供应商通常使用昂 如果事实表中有万亿行和数PB的数据,那么高效地存储和查询它们就成为一个具有挑战性的问题。维度表通常要小得多(数百万行),所以在本节中我们将主要关注事实的存储。 -尽管事实表通常超过100列,但典型的数据仓库查询一次只能访问4个或5个查询(“`SELECT *`”查询很少用于分析)【51】。以[例3-1]()中的查询为例:它访问了大量的行(在2013日历年中每次都有人购买水果或糖果),但只需访问`fact_sales`表的三列:`date_key, product_sk, quantity`。查询忽略所有其他列。 +尽管事实表通常超过100列,但典型的数据仓库查询一次只能访问4个或5个查询( “`SELECT *`” 查询很少用于分析)【51】。以[例3-1]()中的查询为例:它访问了大量的行(在2013日历年中每次都有人购买水果或糖果),但只需访问`fact_sales`表的三列:`date_key, product_sk, quantity`。查询忽略所有其他列。 **例3-1 分析人们是否更倾向于购买新鲜水果或糖果,这取决于一周中的哪一天** @@ -462,7 +466,7 @@ GROUP BY 在大多数OLTP数据库中,存储都是以面向行的方式进行布局的:表格的一行中的所有值都相邻存储。文档数据库是相似的:整个文档通常存储为一个连续的字节序列。你可以在[图3-1](img/fig3-1.png)的CSV例子中看到这个。 -为了处理像[例3-1]()这样的查询,您可能在`fact_sales.date_key` `fact_sales.product_sk`上有索引,它们告诉存储引擎在哪里查找特定日期或特定产品的所有销售情况。但是,面向行的存储引擎仍然需要将所有这些行(每个包含超过100个属性)从磁盘加载到内存中,解析它们,并过滤掉那些不符合要求的条件。这可能需要很长时间。 +为了处理像[例3-1]()这样的查询,您可能在 `fact_sales.date_key`, `fact_sales.product_sk`上有索引,它们告诉存储引擎在哪里查找特定日期或特定产品的所有销售情况。但是,面向行的存储引擎仍然需要将所有这些行(每个包含超过100个属性)从磁盘加载到内存中,解析它们,并过滤掉那些不符合要求的条件。这可能需要很长时间。 面向列的存储背后的想法很简单:不要将所有来自一行的值存储在一起,而是将来自每一列的所有值存储在一起。如果每个列存储在一个单独的文件中,查询只需要读取和解析查询中使用的那些列,这可以节省大量的工作。这个原理如[图3-10](img/fig3-10.png)所示。 @@ -486,9 +490,9 @@ GROUP BY **图3-11 压缩位图索引存储布局** -通常情况下,一列中不同值的数量与行数相比较小(例如,零售商可能有数十亿的销售交易,但只有100,000个不同的产品)。现在我们可以得到一个有n个不同值的列,并把它转换成n个独立的位图:每个不同值的一个位图,每行一位。如果该行具有该值,则该位为1,否则为0。 +通常情况下,一列中不同值的数量与行数相比较小(例如,零售商可能有数十亿的销售交易,但只有100,000个不同的产品)。现在我们可以得到一个有 n 个不同值的列,并把它转换成 n 个独立的位图:每个不同值的一个位图,每行一位。如果该行具有该值,则该位为1,否则为0。 -如果n非常小(例如,国家/地区列可能有大约200个不同的值),则这些位图可以每行存储一位。但是,如果n更大,大部分位图中将会有很多的零(我们说它们是稀疏的)。在这种情况下,位图可以另外进行游程编码,如[图3-11](fig3-11.png)底部所示。这可以使列的编码非常紧凑。 +如果 n 非常小(例如,国家/地区列可能有大约200个不同的值),则这些位图可以每行存储一位。但是,如果n更大,大部分位图中将会有很多的零(我们说它们是稀疏的)。在这种情况下,位图可以另外进行游程编码,如[图3-11](fig3-11.png)底部所示。这可以使列的编码非常紧凑。 这些位图索引非常适合数据仓库中常见的各种查询。例如: @@ -496,13 +500,13 @@ GROUP BY WHERE product_sk IN(30,68,69) ``` -加载`product_sk = 30, product_sk = 68, product_sk = 69`的三个位图,并计算三个位图的按位或,这可以非常有效地完成。 +加载 `product_sk = 30` , `product_sk = 68` , `product_sk = 69` 的三个位图,并计算三个位图的按位或,这可以非常有效地完成。 ```sql WHERE product_sk = 31 AND store_sk = 3 ``` -加载`product_sk = 31`和`store_sk = 3`的位图,并逐位计算AND。 这是因为列按照相同的顺序包含行,因此一列的位图中的第k位对应于与另一列的位图中的第k位相同的行。 +加载 `product_sk = 31` 和 `store_sk = 3` 的位图,并逐位计算AND。 这是因为列按照相同的顺序包含行,因此一列的位图中的第 k 位对应于与另一列的位图中的第 k 位相同的行。 对于不同种类的数据,也有各种不同的压缩方案,但我们不会详细讨论它们,参见【58】的概述。 @@ -525,11 +529,11 @@ WHERE product_sk = 31 AND store_sk = 3 注意,每列独自排序是没有意义的,因为那样我们就不会知道列中的哪些项属于同一行。我们只能重建一行,因为我们知道一列中的第k项与另一列中的第k项属于同一行。 -相反,即使按列存储数据,也需要一次对整行进行排序。数据库的管理员可以使用他们对常见查询的知识来选择表格应该被排序的列。例如,如果查询通常以日期范围为目标,例如上个月,则可以将`date_key`作为第一个排序键。然后,查询优化器只能扫描上个月的行,这比扫描所有行要快得多。 +相反,即使按列存储数据,也需要一次对整行进行排序。数据库的管理员可以使用他们对常见查询的知识来选择表格应该被排序的列。例如,如果查询通常以日期范围为目标,例如上个月,则可以将 `date_key` 作为第一个排序键。然后,查询优化器只能扫描上个月的行,这比扫描所有行要快得多。 -第二列可以确定第一列中具有相同值的任何行的排序顺序。例如,如果`date_key`是[图3-10](img/fig3-10.png)中的第一个排序关键字,那么`product_sk`可能是第二个排序关键字,因此同一天的同一产品的所有销售都将在存储中组合在一起。这将有助于需要在特定日期范围内按产品对销售进行分组或过滤的查询。 +第二列可以确定第一列中具有相同值的任何行的排序顺序。例如,如果 `date_key` 是[图3-10](img/fig3-10.png)中的第一个排序关键字,那么 `product_sk` 可能是第二个排序关键字,因此同一天的同一产品的所有销售都将在存储中组合在一起。这将有助于需要在特定日期范围内按产品对销售进行分组或过滤的查询。 -排序顺序的另一个好处是它可以帮助压缩列。如果主要排序列没有多个不同的值,那么在排序之后,它将具有很长的序列,其中相同的值连续重复多次。一个简单的运行长度编码(就像我们用于[图3-11](img/fig3-11.png)中的位图一样)可以将该列压缩到几千字节——即使表中有数十亿行。 +排序顺序的另一个好处是它可以帮助压缩列。如果主要排序列没有多个不同的值,那么在排序之后,它将具有很长的序列,其中相同的值连续重复多次。一个简单的运行长度编码(就像我们用于[图3-11](img/fig3-11.png)中的位图一样)可以将该列压缩到几千字节 —— 即使表中有数十亿行。 第一个排序键的压缩效果最强。第二和第三个排序键会更混乱,因此不会有这么长时间的重复值。排序优先级下面的列以基本上随机的顺序出现,所以它们可能不会被压缩。但前几列排序仍然是一个整体。 @@ -559,7 +563,7 @@ WHERE product_sk = 31 AND store_sk = 3 当底层数据发生变化时,物化视图需要更新,因为它是数据的非规范化副本。数据库可以自动完成,但是这样的更新使得写入成本更高,这就是在OLTP数据库中不经常使用物化视图的原因。在读取繁重的数据仓库中,它们可能更有意义(不管它们是否实际上改善了读取性能取决于个别情况)。 -物化视图的常见特例称为数据立方体或OLAP立方体【64】。它是按不同维度分组的聚合网格。[图3-12](img/fig3-12.png)显示了一个例子。 +物化视图的常见特例称为数据立方体或OLAP立方【64】。它是按不同维度分组的聚合网格。[图3-12](img/fig3-12.png)显示了一个例子。 ![](img/fig3-12.png) @@ -567,7 +571,7 @@ WHERE product_sk = 31 AND store_sk = 3 想象一下,现在每个事实都只有两个维度表的外键——在[图3-12](img/fig-3-12.png)中,这些是日期和产品。您现在可以绘制一个二维表格,一个轴线上的日期和另一个轴上的产品。每个单元包含具有该日期 - 产品组合的所有事实的属性(例如,`net_price`)的聚集(例如,`SUM`)。然后,您可以沿着每行或每列应用相同的汇总,并获得一个维度减少的汇总(按产品的销售额,无论日期,还是按日期销售,无论产品如何)。 -一般来说,事实往往有两个以上的维度。在图3-9中有五个维度:日期,产品,商店,促销和客户。要想象一个五维超立方体是什么样子是很困难的,但是原理是一样的:每个单元格都包含特定日期(产品 - 商店 - 促销 - 客户组合)的销售。这些值可以在每个维度上重复概括。 +一般来说,事实往往有两个以上的维度。在图3-9中有五个维度:日期,产品,商店,促销和客户。要想象一个五维超立方体是什么样子是很困难的,但是原理是一样的:每个单元格都包含特定日期(产品-商店-促销-客户)组合的销售。这些值可以在每个维度上重复概括。 物化数据立方体的优点是某些查询变得非常快,因为它们已经被有效地预先计算了。例如,如果您想知道每个商店的总销售额,则只需查看合适维度的总计,无需扫描数百万行。 @@ -586,7 +590,7 @@ WHERE product_sk = 31 AND store_sk = 3 在OLTP方面,我们看到了来自两大主流学派的存储引擎: -* 日志结构学派,只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 Bitcask,SSTables,LSM-tree,LevelDB,Cassandra,HBase,Lucene等都属于这个组。 +* 日志结构学派,只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。 Bitcask,SSTables,LSM树,LevelDB,Cassandra,HBase,Lucene等都属于这个组。 * 就地更新学派,将磁盘视为一组可以覆盖的固定大小的页面。 B树是这种哲学的最大的例子,被用在所有主要的关系数据库中,还有许多非关系数据库。 日志结构的存储引擎是相对较新的发展。他们的主要想法是,他们系统地将随机访问写入顺序写入磁盘,由于硬盘驱动器和固态硬盘的性能特点,可以实现更高的写入吞吐量。在完成OLTP方面,我们通过一些更复杂的索引结构和为保留所有数据而优化的数据库做了一个简短的介绍。 diff --git a/ch4.md b/ch4.md index 1f76560..a7421b5 100644 --- a/ch4.md +++ b/ch4.md @@ -428,7 +428,7 @@ Web服务仅仅是通过网络进行API请求的一系列技术的最新版本 尽管有这样那样的问题,RPC不会消失。在本章提到的所有编码的基础上构建了各种RPC框架:例如,Thrift和Avro带有RPC支持,gRPC是使用Protocol Buffers的RPC实现,Finagle也使用Thrift,Rest.li使用JSON over HTTP。 -这种新一代的RPC框架更加明确的是,远程请求与本地函数调用不同。例如,Finagle和Rest.li使用futures(promises)来封装可能失败的异步操作。`Futures`还可以简化需要并行发出多项服务的情况,并将其结果合并【45】。 gRPC支持流,其中一个调用不仅包括一个请求和一个响应,还包括一系列的请求和响应【46】。 +这种新一代的RPC框架更加明确的是,远程请求与本地函数调用不同。例如,Finagle和Rest.li 使用futures(promises)来封装可能失败的异步操作。`Futures`还可以简化需要并行发出多项服务的情况,并将其结果合并【45】。 gRPC支持流,其中一个调用不仅包括一个请求和一个响应,还包括一系列的请求和响应【46】。 其中一些框架还提供服务发现,即允许客户端找出在哪个IP地址和端口号上可以找到特定的服务。我们将在“[请求路由](ch6.md#请求路由)”中回到这个主题。 @@ -489,7 +489,7 @@ actor模型是单个进程中并发的编程模型。逻辑被封装在角色中 三个流行的分布式actor框架处理消息编码如下: * 默认情况下,Akka使用Java的内置序列化,不提供前向或后向兼容性。 但是,你可以用类似缓冲区的东西替代它,从而获得滚动升级的能力【50】。 -* `Orleans`默认使用不支持滚动升级部署的自定义数据编码格式; 要部署新版本的应用程序,您需要设置一个新的群集,将流量从旧群集迁移到新群集,然后关闭旧群集【51,52】。 像Akka一样,可以使用自定义序列化插件。 +* Orleans 默认使用不支持滚动升级部署的自定义数据编码格式; 要部署新版本的应用程序,您需要设置一个新的群集,将流量从旧群集迁移到新群集,然后关闭旧群集【51,52】。 像Akka一样,可以使用自定义序列化插件。 * 在Erlang OTP中,对记录模式进行更改是非常困难的(尽管系统具有许多为高可用性设计的功能)。 滚动升级是可能的,但需要仔细计划【53】。 一个新的实验性的`maps`数据类型(2014年在Erlang R17中引入的类似于JSON的结构)可能使得这个数据类型在未来更容易【54】。 diff --git a/ch7.md b/ch7.md index 464a341..4442bea 100644 --- a/ch7.md +++ b/ch7.md @@ -227,7 +227,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true 实际上不幸的是:隔离并没有那么简单。**可序列化**会有性能损失,许多数据库不愿意支付这个代价【8】。因此,系统通常使用较弱的隔离级别来防止一部分,而不是全部的并发问题。这些隔离级别难以理解,并且会导致微妙的错误,但是它们仍然在实践中被使用【23】。 -并发性错误导致的并发性错误不仅仅是一个理论问题。他们造成了很多的资金损失【24,25】,耗费了财务审计人员的调查【26】,并导致客户数据被破坏【27】。关于这类问题的一个流行的评论是“如果你正在处理财务数据,请使用ACID数据库!” ——但是这一点没有提到。即使是很多流行的关系型数据库系统(通常被认为是“ACID”)也使用弱隔离级别,所以它们也不一定能防止这些错误的发生。 +并发性错误导致的并发性错误不仅仅是一个理论问题。它们造成了很多的资金损失【24,25】,耗费了财务审计人员的调查【26】,并导致客户数据被破坏【27】。关于这类问题的一个流行的评论是“如果你正在处理财务数据,请使用ACID数据库!” —— 但是这一点没有提到。即使是很多流行的关系型数据库系统(通常被认为是“ACID”)也使用弱隔离级别,所以它们也不一定能防止这些错误的发生。 比起盲目地依赖工具,我们应该对存在的并发问题的种类,以及如何防止这些问题有深入的理解。然后就可以使用我们所掌握的工具来构建可靠和正确的应用程序。 @@ -336,11 +336,11 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true **图7-7 使用多版本对象实现快照隔离** -表中的每一行都有一个`created_by`字段,其中包含将该行插入到表中的的事务ID。此外,每行都有一个`deleted_by`字段,最初是空的。如果某个事务删除了一行,那么该行实际上并未从数据库中删除,而是通过将`deleted_by`字段设置为请求删除的事务的ID来标记为删除。在稍后的时间,当确定没有事务可以再访问已删除的数据时,数据库中的垃圾收集过程会将所有带有删除标记的行移除,并释放其空间。[^译注ii] +表中的每一行都有一个 `created_by` 字段,其中包含将该行插入到表中的的事务ID。此外,每行都有一个 `deleted_by` 字段,最初是空的。如果某个事务删除了一行,那么该行实际上并未从数据库中删除,而是通过将 `deleted_by` 字段设置为请求删除的事务的ID来标记为删除。在稍后的时间,当确定没有事务可以再访问已删除的数据时,数据库中的垃圾收集过程会将所有带有删除标记的行移除,并释放其空间。[^译注ii] -[^译注ii]: 在PostgreSQL中,`created_by` 实际名称为`xmin`,`deleted_by` 实际名称为`xmax` +[^译注ii]: 在PostgreSQL中,`created_by` 的实际名称为`xmin`,`deleted_by` 的实际名称为`xmax` -`UPDATE`操作在内部翻译为`DELETE`和`INSERT`。例如,在[图7-7]()中,事务13从账户2中扣除100美元,将余额从500美元改为400美元。实际上包含两条账户2的记录:余额为\$500的行被标记为**被事务13删除**,余额为\$400的行**由事务13创建**。 +`UPDATE` 操作在内部翻译为 `DELETE` 和 `INSERT` 。例如,在[图7-7]()中,事务13 从账户2 中扣除100美元,将余额从500美元改为400美元。实际上包含两条账户2 的记录:余额为 \$500 的行被标记为**被事务13删除**,余额为 \$400 的行**由事务13创建**。 #### 观察一致性快照的可见性规则 @@ -351,7 +351,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true 3. 由具有较晚事务ID(即,在当前事务开始之后开始的)的事务所做的任何写入都被忽略,而不管这些事务是否已经提交。 4. 所有其他写入,对应用都是可见的。 -这些规则适用于创建和删除对象。在[图7-7]()中,当事务12从账户2读取时,它会看到\$500的余额,因为\$500余额的删除是由事务13完成的(根据规则3,事务12看不到事务13执行的删除),且400美元记录的创建也是不可见的(按照相同的规则)。 +这些规则适用于创建和删除对象。在[图7-7]()中,当事务12 从账户2 读取时,它会看到 \$500 的余额,因为 \$500 余额的删除是由事务13 完成的(根据规则3,事务12 看不到事务13 执行的删除),且400美元记录的创建也是不可见的(按照相同的规则)。 换句话说,如果以下两个条件都成立,则可见一个对象: @@ -439,7 +439,7 @@ COMMIT; 这种方法的一个优点是,数据库可以结合快照隔离高效地执行此检查。事实上,PostgreSQL的可重复读,Oracle的可串行化和SQL Server的快照隔离级别,都会自动检测到丢失更新,并中止惹麻烦的事务。但是,MySQL/InnoDB的可重复读并不会检测**丢失更新**【23】。一些作者【28,30】认为,数据库必须能防止丢失更新才称得上是提供了**快照隔离**,所以在这个定义下,MySQL下不提供快照隔离。 -丢失更新检测是一个很好的功能,因为它不需要应用代码使用任何特殊的数据库功能,你可能会忘记使用锁或原子操作,从而引入一个错误;但丢失更新的检测是自动发生的,因此不太容易出错。 +丢失更新检测是一个很好的功能,因为它不需要应用代码使用任何特殊的数据库功能,你可能会忘记使用锁或原子操作,从而引入错误;但丢失更新的检测是自动发生的,因此不太容易出错。 #### 比较并设置(CAS) @@ -760,7 +760,7 @@ WHERE room_id = 123 AND 也许不是:一个称为**可序列化快照隔离(SSI, serializable snapshot isolation)**的算法是非常有前途的。它提供了完整的可序列化隔离级别,但与快照隔离相比只有只有很小的性能损失。 SSI是相当新的:它在2008年首次被描述【40】,并且是Michael Cahill的博士论文【51】的主题。 -今天,SSI既用于单节点数据库(PostgreSQL9.1以后的可序列化隔离级别)和分布式数据库(FoundationDB使用类似的算法)。由于SSI与其他并发控制机制相比还很年轻,还处于在实践中证明自己表现的阶段。但它有可能因为足够快而在未来成为新的默认选项。 +今天,SSI既用于单节点数据库(PostgreSQL9.1 以后的可序列化隔离级别)和分布式数据库(FoundationDB使用类似的算法)。由于SSI与其他并发控制机制相比还很年轻,还处于在实践中证明自己表现的阶段。但它有可能因为足够快而在未来成为新的默认选项。 #### 悲观与乐观的并发控制 @@ -791,7 +791,7 @@ WHERE room_id = 123 AND #### 检测旧MVCC读取 -回想一下,快照隔离通常是通过多版本并发控制(MVCC;见[图7-10]())来实现的。当一个事务从MVCC数据库中的一致快照读时,它将忽略取快照时尚未提交的任何其他事务所做的写入。在[图7-10]()中,事务43认为Alice的`on_call = true`,因为事务42(修改Alice的待命状态)未被提交。然而,在事务43想要提交时,事务42已经提交。这意味着在读一致性快照时被忽略的写入已经生效,事务43的前提不再为真。 +回想一下,快照隔离通常是通过多版本并发控制(MVCC;见[图7-10]())来实现的。当一个事务从MVCC数据库中的一致快照读时,它将忽略取快照时尚未提交的任何其他事务所做的写入。在[图7-10]()中,事务43 认为Alice的 `on_call = true` ,因为事务42(修改Alice的待命状态)未被提交。然而,在事务43想要提交时,事务42 已经提交。这意味着在读一致性快照时被忽略的写入已经生效,事务43 的前提不再为真。 ![](img/fig7-10.png) @@ -799,19 +799,19 @@ WHERE room_id = 123 AND 为了防止这种异常,数据库需要跟踪一个事务由于MVCC可见性规则而忽略另一个事务的写入。当事务想要提交时,数据库检查是否有任何被忽略的写入现在已经被提交。如果是这样,事务必须中止。 -为什么要等到提交?当检测到陈旧的读取时,为什么不立即中止事务43?因为如果事务43是只读事务,则不需要中止,因为没有写入偏差的风险。当事务43进行读取时,数据库还不知道事务是否要稍后执行写操作。此外,事务42可能在事务43被提交的时候中止或者可能仍然未被提交,因此读取可能终究不是陈旧的。通过避免不必要的中止,SSI保留快照隔离对从一致快照中长时间运行的读取的支持。 +为什么要等到提交?当检测到陈旧的读取时,为什么不立即中止事务43 ?因为如果事务43 是只读事务,则不需要中止,因为没有写入偏差的风险。当事务43 进行读取时,数据库还不知道事务是否要稍后执行写操作。此外,事务42 可能在事务43 被提交的时候中止或者可能仍然未被提交,因此读取可能终究不是陈旧的。通过避免不必要的中止,SSI 保留快照隔离对从一致快照中长时间运行的读取的支持。 #### 检测影响之前读取的写入 -第二种情况要考虑的是另一个事务在读取数据之后修改数据。这种情况如图7-11所示。 +第二种情况要考虑的是另一个事务在读取数据之后修改数据。这种情况如[图7-11](img/fig7-11.png)所示。 ![](img/fig7-11.png) **图7-11 在可序列化快照隔离中,检测一个事务何时修改另一个事务的读取。** -在两阶段锁定的上下文中,我们讨论了[索引范围锁]()(请参阅“[索引范围锁]()”),它允许数据库锁定与某个搜索查询匹配的所有行的访问权,例如`WHERE shift_id = 1234`。可以在这里使用类似的技术,除了SSI锁不会阻塞其他事务。 +在两阶段锁定的上下文中,我们讨论了[索引范围锁]()(请参阅“[索引范围锁]()”),它允许数据库锁定与某个搜索查询匹配的所有行的访问权,例如 `WHERE shift_id = 1234`。可以在这里使用类似的技术,除了SSI锁不会阻塞其他事务。 -在[图7-11]()中,事务42和43都在班次1234查找值班医生。如果在`shift_id`上有索引,则数据库可以使用索引项1234来记录事务42和43读取这个数据的事实。 (如果没有索引,这个信息可以在表级别进行跟踪)。这个信息只需要保留一段时间:在一个事务完成(提交或中止)之后,所有的并发事务完成之后,数据库就可以忘记它读取的数据了。 +在[图7-11]()中,事务42 和43 都在班次1234 查找值班医生。如果在`shift_id`上有索引,则数据库可以使用索引项1234 来记录事务42 和43 读取这个数据的事实。 (如果没有索引,这个信息可以在表级别进行跟踪)。这个信息只需要保留一段时间:在一个事务完成(提交或中止)之后,所有的并发事务完成之后,数据库就可以忘记它读取的数据了。 当事务写入数据库时,它必须在索引中查找最近曾读取受影响数据的其他事务。这个过程类似于在受影响的键范围上获取写锁,但锁并不会阻塞事务到其他事务完成,而是像一个引线一样只是简单通知其他事务:你们读过的数据可能不是最新的啦。 From 360087cb07f94b33faaf702e9519d13d4c394eba Mon Sep 17 00:00:00 2001 From: "jiajia.debug" Date: Sun, 1 Apr 2018 01:13:49 +0800 Subject: [PATCH 15/17] ch2 20% --- ch2.md | 206 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/ch2.md b/ch2.md index 924b912..baf156e 100644 --- a/ch2.md +++ b/ch2.md @@ -2,80 +2,81 @@ ![](img/ch2.png) -> 语言的极限即世界的极限 +> 语言的边界就是思想的边界。 > -> —— 路德维奇·维特根斯坦, 《逻辑哲学》(1922) +> —— 路德维奇·维特根斯坦,《逻辑哲学》(1922) > ------------------- [TOC] -数据模型可能是软件开发中最重要的部分了,因为它有着深远的影响:不仅影响软件的编写方式,而且会影响我们的**解题思路**。 +数据模型可能是软件开发中最重要的部分了,因为它们的影响如此深远:不仅仅影响着软件的编写方式,而且影响着我们的**解题思路**。 -多数应用使用层层叠加的数据模型构建。对于每层数据模型的关键问题是:它是如何用低一层数据模型来表示的?例如: +多数应用使用层层叠加的数据模型构建。对于每层数据模型的关键问题是:它是如何用低一层数据模型来**表示**的?例如: -1. 作为一名应用开发人员,你对现实世界进行观察(包括人员,组织,货物,行为,资金流向,传感器等),并使用对象或数据结构建模,提供操纵这些数据结构的API。这些结构通常是应用特定的。 -2. 当你想要存储这些存储这些数据结构时,你可以使用通用数据模型来表示它们,例如JSON或XML文档,关系型数据库中的表、或者图数据模型。 -3. 开发数据库软件的工程师需要决定如何使用内存、磁盘或网络中的字节来表示这些数据结构(JSON/XML/关系/图)。这种表示可以允许数据以各种方式被查询,搜索,操纵和处理。 -4. 在更低的层次上,硬件工程师已经想出了使用电流,光脉冲,磁场或其他东西来表示这些字节的方法。 +1. 作为一名应用开发人员,你观察现实世界(里面有人员,组织,货物,行为,资金流向,传感器等),并采用对象或数据结构,以及操控那些数据结构的API来进行建模。那些结构通常是特定于应用程序的。 +2. 当要存储那些数据结构时,你可以利用通用数据模型来表示它们,如JSON或XML文档,关系数据库中的表、或图模型。 +3. 数据库软件的工程师选定如何以内存、磁盘或网络上的字节来表示JSON/XML/关系/图数据。这类表示形式使数据有可能以各种方式来查询,搜索,操纵和处理。 +4. 在更低的层次上,硬件工程师已经想出了使用电流,光脉冲,磁场或者其他东西来表示字节的方法。 -在一个复杂的应用中,可能会有更多的中间层,比如基于API 的API,但是基本思想仍然是一样的:每个层都通过提供一个干净整洁的数据模型来隐藏更低层次中的复杂度。这些抽象允许不同的人群有效地协作(例如数据库厂商的工程师,与使用其数据库的应用开发者)。 +一个复杂的应用程序可能会有更多的中间层次,比如基于API的API,不过基本思想仍然是一样的:每个层都通过提供一个明确的数据模型来隐藏更低层次中的复杂性。这些抽象允许不同的人群有效地协作(例如数据库厂商的工程师和使用数据库的应用程序开发人员)。 -有许多不同类型的数据模型,每个数据模型都带有如何使用的假设。某些用法很容易,有些不被支持;一些操作很快,一些操作不好;一些数据转换感觉自然,有些是尴尬的。 +数据模型种类繁多,每个数据模型都带有如何使用的设想。有些用法很容易,有些则不支持如此;有些操作运行很快,有些则表现很差;有些数据转换非常自然,有些则很麻烦。 -掌握一个数据模型可能需要很多努力(想想关系数据建模有多少本书)。即使只使用一种数据模型,而不用担心其内部工作,构建软件也是非常困难的。但是由于数据模型对软件的功能有很大的影响,因此选择适合应用程序的软件是非常重要的。 +掌握一个数据模型需要花费很多精力(想想关系数据建模有多少本书)。即便只使用一个数据模型,不用操心其内部工作机制,构建软件也是非常困难的。然而,因为数据模型对上层软件的功能(能做什么,不能做什么)有着至深的影响,所以选择一个适合的数据模型是非常重要的。 -在本章中,我们将研究一系列用于数据存储和查询的通用数据模型(前面列表中的第2点)。特别是,我们将比较关系模型,文档模型和一些基于图形的数据模型。我们还将查看各种查询语言并比较它们的用例。在第3章中,我们将讨论存储引擎是如何工作的。也就是说,这些数据模型是如何实际实现的(列表中的第3点)。 +在本章中,我们将研究一系列用于数据存储和查询的通用数据模型(前面列表中的第2点)。特别地,我们将比较关系模型,文档模型和少量基于图形的数据模型。我们还将查看各种查询语言并比较它们的用例。在第3章中,我们将讨论存储引擎是如何工作的。也就是说,这些数据模型实际上是如何实现的(列表中的第3点)。 ## 关系模型与文档模型 -现在最著名的数据模型可能是SQL,它基于Edgar Codd在1970年提出的关系模型【1】:数据被组织到关系中(称为SQL表),其中每个关系是元组的无序集合SQL中的行)。 +现在最著名的数据模型可能是SQL。它基于Edgar Codd在1970年提出的关系模型【1】:数据被组织成**关系(relation)**(SQL中称作**表(table)**),其中每个关系是**元组(tuple)**(SQL中称作**行(row)**的无序集合。 -关系模型是一个理论上的提议,当时很多人都怀疑是否能够有效实现。然而到了20世纪80年代中期,关系数据库管理系统(RDBMSes)和SQL已成为大多数需要存储和查询具有某种规模结构的数据的人们的首选工具。关系数据库的优势已经持续了大约25~30年——计算史中的永恒。 +关系模型曾是一个理论性的提议,当时很多人都怀疑是否能够有效实现它。然而到了20世纪80年代中期,关系数据库管理系统(RDBMSes)和SQL已成为大多数人们存储和查询某些常规结构的数据的首选工具。关系数据库已经持续称霸了大约25~30年——这对计算机史来说是极其漫长的时间。 -关系数据库起源于商业数据处理,这是在20世纪60年代和70年代在大型计算机上进行的。从今天的角度来看,用例显得很平常:通常是交易处理(进入销售或银行交易,航空公司预订,仓库库存)和批处理(客户发票,工资单,报告)。 +关系数据库起源于商业数据处理,在20世纪60年代和70年代用大型计算机来执行。从今天的角度来看,那些用例显得很平常:典型的**事务处理(transaction processing)**(将销售或银行交易,航空公司预订,库存管理信息记录在库)和**批处理(batch processing)**(客户发票,工资单,报告)。 -当时的其他数据库迫使应用程序开发人员考虑数据库内部的数据表示。关系模型的目标是将实现细节隐藏在更简洁的界面之后。 +当时的其他数据库迫使应用程序开发人员必须考虑数据库内部的数据表示形式。关系模型致力于将上述实现细节隐藏在更简洁的接口之后。 -多年来,在数据存储和查询方面存在着许多相互竞争的方法。在20世纪70年代和80年代初,网络模型和分层模型是主要的选择,但关系模型占据了主导地位。对象数据库在二十世纪八十年代末和九十年代初再次出现。 XML数据库出现在二十一世纪初,但只有小众采用。关系模型的每个竞争者都在其时代产生了大量的炒作,但从来没有持续【2】。 +多年来,在数据存储和查询方面存在着许多相互竞争的方法。在20世纪70年代和80年代初,网络模型和分层模型曾是主要的选择,但关系模型随后占据了主导地位。对象数据库在20世纪80年代末和90年代初来了又去。 XML数据库在二十一世纪初出现,但只有小众采用过。关系模型的每个竞争者都在其时代产生了大量的炒作,但从来没有持续【2】。 -随着电脑越来越强大和联网,它们开始被用于日益多样化的目的。值得注意的是,关系数据库在业务数据处理的原始范围之外被推广到很广泛的用例。您今天在网上看到的大部分内容仍然是由关系数据库提供支持,无论是在线发布,讨论,社交网络,电子商务,游戏,软件即服务生产力应用程序等等。 +随着电脑越来越强大和互联,它们开始用于日益多样化的目的。关系数据库非常成功地被推广到业务数据处理的原始范围之外更为广泛的用例上。您今天在网上看到的大部分内容依旧是由关系数据库来提供支持,无论是在线发布,讨论,社交网络,电子商务,游戏,软件即服务生产力应用程序等等内容。 ### NoSQL的诞生 -现在在2010年代,NoSQL是推翻关系模式主导地位的最新尝试。 “NoSQL”这个名字非常不幸,因为它实际上并没有涉及到任何特定的技术,它最初只是作为一个吸引人的Twitter标签在2009年的一个关于分布式,非关系数据库上的开源聚会。无论如何,这个术语触动了某些神经,并迅速通过网络启动社区和更远的地方传播开来。一些有趣的数据库系统现在与*#NoSQL#*标签相关联,并被追溯性地重新解释为不仅是SQL 【4】。 +现在 - 2010年代,NoSQL开始了最新一轮尝试,试图推翻关系模型的统治地位。 “NoSQL”这个名字让人遗憾,因为实际上它并没有涉及到任何特定的技术。最初它只是作为一个醒目的Twitter标签,用在2009年一个关于分布式,非关系数据库上的开源聚会上。无论如何,这个术语触动了某些神经,并迅速在网络创业社区内外传播开来。好些有趣的数据库系统现在都与*#NoSQL#*标签相关联,并且NoSQL被追溯性地重新解释为**不仅是SQL(Not Only SQL)** 【4】。 -采用NoSQL数据库有几个驱动力,其中包括: +采用NoSQL数据库的背后有几个驱动因素,其中包括: * 需要比关系数据库更好的可扩展性,包括非常大的数据集或非常高的写入吞吐量 -* 相比商业数据库产品,偏爱免费和开源软件 +* 相比商业数据库产品,免费和开源软件更受偏爱。 * 关系模型不能很好地支持一些特殊的查询操作 -* 对关系模型限制性感到受挫,对更多动态性与表现力的渴望 +* 受挫于关系模型的限制性,渴望一种更具多动态性与表现力的数据模型【5】 -不同的应用程序有不同的要求,一个用例的最佳技术选择可能不同于另一个用例的最佳选择。因此,在可预见的未来,关系数据库似乎可能会继续与各种非关系数据库一起使用 - 这种想法有时也被称为**混合持久化(Polyglot Persistences)** +不同的应用程序有不同的需求,一个用例的最佳技术选择可能不同于另一个用例的最佳技术选择。因此,在可预见的未来,关系数据库似乎可能会继续与各种非关系数据库一起使用 - 这种想法有时也被称为**混合持久化(polyglot persistence)** ### 对象关系不匹配 -现在大多数应用程序开发都是在面向对象的编程语言中完成的,这导致了对SQL数据模型的普遍批评:如果数据存储在关系表中,那么应用程序代码中的对象之间需要一个笨拙的转换层,表,行和列的数据库模型。模型之间的不连贯有时被称为**阻抗不匹配(impedance mismatch)**[^i]。 +目前大多数应用程序开发都使用面向对象的编程语言来开发,这导致了对SQL数据模型的普遍批评:如果数据存储在关系表中,那么需要一个笨拙的转换层,处于应用程序代码中的对象和表,行,列的数据库模型之间。模型之间的不连贯有时被称为**阻抗不匹配(impedance mismatch)**[^i]。 -[^i]: 从电子学借用一个术语。每个电路的输入和输出都有一定的阻抗(交流电阻)。当您将一个电路的输出连接到另一个电路的输入时,如果两个电路的输出和输入阻抗匹配,则连接上的功率传输将被最大化。阻抗不匹配可能导致信号反射和其他问题 +[^i]: 一个从电子学借用的术语。每个电路的输入和输出都有一定的阻抗(交流电阻)。当您将一个电路的输出连接到另一个电路的输入时,如果两个电路的输出和输入阻抗匹配,则连接上的功率传输将被最大化。阻抗不匹配会导致信号反射及其他问题。 -像ActiveRecord和Hibernate这样的**对象关系映射(object-relational mapping, ORM)**框架减少了这个翻译层需要的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。 +像ActiveRecord和Hibernate这样的**对象关系映射(object-relational mapping, ORM)**框架可以减少这个转换层所需的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。 ![](img/fig2-1.png) -**图2-1 使用关系型模式来表示领英简历** +**图2-1 使用关系型模式来表示领英简介** -例如,[图2-1](img/fig2-1.png)展示了如何在关系模式中表达简历(一个LinkedIn简介)。整个配置文件可以通过一个唯一的标识符`user_id`来标识。像`first_name`和`last_name`这样的字段每个用户只出现一次,所以他们可以在用户表上建模为列。但是,大多数人的职业(职位)多于一份工作,人们可能有不同的教育期限和不同数量的联系信息。从用户到这些项目之间存在一对多的关系,可以用多种方式来表示: +例如,[图2-1](img/fig2-1.png)展示了如何在关系模式中表示简历(一个LinkedIn简介)。整个简介可以通过一个唯一的标识符`user_id`来标识。像`first_name`和`last_name`这样的字段每个用户只出现一次,所以他们可以在用户表上建模为列。但是,大多数人在职业生涯中拥有多于一份的工作,人们可能有不同样的教育阶段和任意数量的联系信息。从用户到这些项目之间存在一对多的关系,可以用多种方式来表示: -* 在传统SQL模型(SQL:1999之前)中,最常见的规范化表示形式是将职位,培训和联系信息放在单独的表中,对用户表提供外键引用,如[图2-1](img/fig2-1.png)所示。 -* 更高版本的SQL标准增加了对结构化数据类型和XML数据的支持;这允许将多值数据存储在单行内,支持在这些文档内查询和索引。这些功能在Oracle,IBM DB2,MS SQL Server和PostgreSQL中都有不同程度的支持【6,7】。 JSON数据类型也受到几个数据库的支持,包括IBM DB2,MySQL和PostgreSQL 【8】。 -* 第三种选择是将职业,教育和联系信息编码为JSON或XML文档,将其存储在数据库的文本列中,并让应用程序解释其结构和内容。在这种配置中,通常不能使用数据库查询该编码列中的值。 +* 传统SQL模型(SQL:1999之前)中,最常见的规范化表示形式是将职位,教育和联系信息放在单独的表中,对用户表提供外键引用,如[图2-1](img/fig2-1.png)所示。 +* 后续的SQL标准增加了对结构化数据类型和XML数据的支持;这允许将多值数据存储在单行内,并支持在这些文档内查询和索引。这些功能在Oracle,IBM DB2,MS SQL Server和PostgreSQL中都有不同程度的支持【6,7】。 JSON数据类型也得到多个数据库的支持,包括IBM DB2,MySQL和PostgreSQL 【8】。 +* 第三种选择是将职业,教育和联系信息编码为JSON或XML文档,将其存储在数据库的文本列中,并让应用程序解析其结构和内容。这种配置下,通常不能使用数据库来查询该编码列中的值。 -对于一个像简历这样自包含的数据结构而言,JSON表示是非常合适的:参见[例2-1]()。 JSON比XML更简单。 面向文档的数据库(如MongoDB 【9】,RethinkDB 【10】,CouchDB 【11】和Espresso【12】)支持这种数据模型。 +对于一个像简历这样自包含文档的数据结构而言,JSON表示是非常合适的:参见[例2-1]()。JSON比XML更简单。面向文档的数据库(如MongoDB 【9】,RethinkDB 【10】,CouchDB 【11】和Espresso【12】)支持这种数据模型。 +**例2-1. 用JSON文档表示一个LinkedIn简介** ```json { @@ -115,11 +116,11 @@ } ``` -一些开发人员认为JSON模型减少了应用程序代码和存储层之间的阻抗不匹配。但是,正如我们将在[第4章](ch4.md)中看到的那样,JSON作为数据编码格式也存在问题。缺乏一个模式往往被认为是一个优势;我们将在“[文档模型中的模式灵活性](#文档模型中的模式灵活性)”中讨论这个问题。 +有一些开发人员认为JSON模型减少了应用程序代码和存储层之间的阻抗不匹配。不过,正如我们将在[第4章](ch4.md)中看到的那样,JSON作为数据编码格式也存在问题。缺乏一个模式往往被认为是一个优势;我们将在“[文档模型中的模式灵活性](#文档模型中的模式灵活性)”中讨论这个问题。 -JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的局部性。如果要在关系示例中获取配置文件,则需要执行多个查询(通过`user_id`查询每个表),或者在用户表与其下属表之间执行混乱的多路连接。在JSON表示中,所有的相关信息都在一个地方,一个查询就足够了。 +JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的**局部性(locality)**。如果在上面的关系型示例中获取简介,那需要执行多个查询(通过`user_id`查询每个表),或者在用户表与其下属表之间混乱地执行多路连接。而在JSON表示中,所有相关信息都在同一个地方,一个查询就足够了。 -从用户配置文件到用户位置,教育历史和联系信息的一对多关系意味着数据中的树状结构,而JSON表示使得这个树状结构变得明确(见[图2-2](img/fig2-2.png))。 +从用户简介文件到用户职位,教育历史和联系信息,这种一对多关系隐含了数据中的树状结构,而JSON表示使得这个树状结构变得明确(见[图2-2](img/fig2-2.png))。 ![](img/fig2-2.png) @@ -127,148 +128,148 @@ JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的局部性 ### 多对一和多对多的关系 -在上一节的[例2-1]()中,`region_id`和`industry_id`是以ID而不是纯字符串“大西雅图地区”和“慈善”的形式给出的。为什么? +在上一节的[例2-1]()中,`region_id`和`industry_id`是以ID,而不是纯字符串“Greater Seattle Area”和“Philanthropy”的形式给出的。为什么? -如果用户界面具有用于输入区域和行业的自由文本字段,则将其存储为纯文本字符串是有意义的。但是,对地理区域和行业进行标准化,并让用户从下拉列表或自动填充器中进行选择是有好处的: +如果用户界面用一个自由文本字段来输入区域和行业,那么将他们存储为纯文本字符串是合理的。另一方式是给出地理区域和行业的标准化的列表,并让用户从下拉列表或自动填充器中进行选择,其优势如下: -* 统一的样式和拼写 +* 各个简介之间样式和拼写统一 * 避免歧义(例如,如果有几个同名的城市) -* 易于更新——名称只存储在一个地方,所以如果需要更改(例如,由于政治事件而改变城市名称),便于全面更新。 -* 本地化支持——当网站翻译成其他语言时,标准化的名单可以被本地化,所以地区和行业可以使用用户的语言来表示 -* 更好的搜索——例如,搜索华盛顿州的慈善家可以匹配这份简历,因为地区列表可以编码记录西雅图在华盛顿的事实(从“大西雅图地区”这个字符串中看不出来) +* 易于更新——名称只存储在一个地方,如果需要更改(例如,由于政治事件而改变城市名称),很容易进行全面更新。 +* 本地化支持——当网站翻译成其他语言时,标准化的列表可以被本地化,使得地区和行业可以使用用户的语言来显示 +* 更好的搜索——例如,搜索华盛顿州的慈善家就会匹配这份简介,因为地区列表可以编码记录西雅图在华盛顿这一事实(从“Greater Seattle Area”这个字符串中看不出来) -无论是存储一个ID还是一个文本字符串,都是一个关于**重复**的问题。当你使用一个ID时,对人类有意义的信息(比如单词:慈善)只存储在一个地方,引用它的所有信息都使用一个ID(ID只在数据库中有意义)。当你直接存储文本时,每个使用它的记录中,都存储的是有意义的信息。 +存储ID还是文本字符串,这是个**复制(duplication)**问题。当使用ID时,对人类有意义的信息(比如单词:Philanthropy)只存储在一处,所有引用它的地方使用ID(ID只在数据库中有意义)。当直接存储文本时,对人类有意义的信息会复制在每处使用记录中。 -使用ID的好处是,因为它对人类没有任何意义,所以永远不需要改变:ID可以保持不变,即使它标识的信息发生变化。任何对人类有意义的东西都可能需要在将来某个时候改变——如果这些信息被复制,所有的冗余副本都需要更新。这会导致写入开销,并且存在不一致的风险(信息的一些副本被更新,但其他信息的副本不被更新)。去除这种重复是数据库规范化的关键思想。(关系模型区分了几种不同的范式,但这些区别实际上并不重要。 作为一个经验法则,如果您重复只能存储在一个地方的值,那么架构不会被**规范化(normalized)**[^ii]。) +使用ID的好处是,ID对人类没有任何意义,永远不需要改变:ID可以保持不变,即使它标识的信息发生变化。任何对人类有意义的东西都可能需要在将来某个时候改变——如果这些信息被复制,所有的冗余副本都需要更新。这会导致写入开销,也存在不一致的风险(一些副本被更新了,还有些副本没有被更新)。去除此类重复是数据库**规范化(normalization)**的关键思想。[^ii] -[^ii]: 关于关系模型的文献区分了几种不同的规范形式,但这些区别几乎没有实际意义。 作为一个经验法则,如果重复存储了只能存储在一个地方的值,则模式就不是规范化的。 +[^ii]: 关于关系模型的文献区分了几种不同的规范形式,但这些区别几乎没有实际意义。一个经验法则是,如果重复存储了可以存储在一个地方的值,则模式就不是**规范化(normalized)**的。 -> 数据库管理员和开发人员喜欢争论规范化和非规范化,但我们现在暂停判断。 在本书的[第三部分](part-iii.md),我们将回到这个话题,探讨处理缓存,非规范化和派生数据的系统方法。 +> 数据库管理员和开发人员喜欢争论规范化和非规范化,让我们暂时保留判断吧。在本书的[第三部分](part-iii.md),我们将回到这个话题,探讨系统的方法用以处理缓存,非规范化和派生数据。 -不幸的是,对这些数据进行规范化需要多对一的关系(许多人生活在一个特定的地区,许多人在一个特定的行业工作),这与文档模型不太吻合。在关系数据库中,通过ID来引用其他表中的行是正常的,因为连接很容易。在文档数据库中,一对多树结构不需要连接,对连接的支持通常很弱[^iii]。 +不幸的是,对这些数据进行规范化需要多对一的关系(许多人生活在一个特定的地区,许多人在一个特定的行业工作),这与文档模型不太吻合。在关系数据库中,通过ID来引用其他表中的行是正常的,因为连接很容易。在文档数据库中,一对多树结构没有必要用连接,对连接的支持通常很弱[^iii]。 -[^iii]: 在撰写本文时,RethinkDB支持连接,MongoDB不支持连接,并且只支持CouchDB中的预先声明的视图。 +[^iii]: 在撰写本文时,RethinkDB支持连接,MongoDB不支持连接,而CouchDB只支持预先声明的视图。 -如果数据库本身不支持连接,则必须通过对数据库进行多个查询来模拟应用程序代码中的连接。 (在这种情况下,地区和行业的名单可能很小,变化不大,应用程序可以简单地将它们留在内存中,但是,联接的工作从数据库转移到应用程序代码。 +如果数据库本身不支持连接,那就不得不在应用程序代码中通过对数据库进行多个查询来模拟连接。(在这种情况中,地区和行业的列表可能很小,改动很少,应用程序可以简单地将其保存在内存中。不过,执行连接的工作从数据库被转移到应用程序代码上。 -而且,即使应用程序的初始版本适合无连接的文档模型,随着功能添加到应用程序中,数据也会变得更加互联。例如,考虑一下我们可以对简历例子进行的一些修改: +此外,即便应用程序的最初版本适合无连接的文档模型,随着功能添加到应用程序中,数据会变得更加互联。例如,考虑一下对简历例子进行的一些修改: ***组织和学校作为实体*** -在前面的描述中,组织(用户工作的公司)和`school_name`(他们学习的地方)只是字符串。也许他们应该是对实体的引用呢?然后,每个组织,学校或大学都可以拥有自己的网页(标识,新闻提要等)。每个简历可以链接到它所提到的组织和学校,并且包括他们的标识和其他信息(参见[图2-3](img/fig2-3.png),来自LinkedIn的一个例子)。 +在前面的描述中,`organization`(用户工作的公司)和`school_name`(他们学习的地方)只是字符串。也许他们应该是对实体的引用呢?然后,每个组织,学校或大学都可以拥有自己的网页(标识,新闻提要等)。每个简历可以链接到它所提到的组织和学校,并且包括他们的图标和其他信息(参见[图2-3](img/fig2-3.png),来自LinkedIn的一个例子)。 ***推荐*** -假设你想添加一个新的功能:一个用户可以为另一个用户写一个推荐。推荐在用户的简历上显示,并附上推荐用户的姓名和照片。如果推荐人更新他们的照片,他们写的任何建议都需要反映新的照片。因此,推荐应该引用作者的个人资料。 - +假设你想添加一个新的功能:一个用户可以为另一个用户写一个推荐。在用户的简历上显示推荐,并附上推荐用户的姓名和照片。如果推荐人更新他们的照片,他们写的任何建议都需要显示新的照片。因此,推荐应该有作者个人简介的引用。 ![](img/fig2-3.png) -**图2-3 公司名不仅是字符串,还是一个指向公司实体的连接(领英截图)** +**图2-3 公司名不仅是字符串,还是一个指向公司实体的链接(LinkedIn截图)** -[图2-4](img/fig2-4.png)阐明了这些新功能怎样使用多对多关系。 每个虚线矩形内的数据可以分组成一个文档,但是对单位,学校和其他用户的引用需要表示为引用,并且在查询时需要连接。 +[图2-4](img/fig2-4.png)阐明了这些新功能怎样使用多对多关系。 每个虚线矩形内的数据可以分组成一个文档,但是对单位,学校和其他用户的引用需要表示成引用,并且在查询时需要连接。 ![](img/fig2-4.png) **图2-4 使用多对多关系扩展简历** -### 文档数据库是否在重蹈覆辙? +### 文档数据库是否在重演历史? -虽然关系数据库中经常使用多对多的关系和连接,但文档数据库和NoSQL重新讨论了如何最好地在数据库中表示这种关系的争论。这个辩论比NoSQL早得多,事实上,它可以追溯到最早的计算机化数据库系统。 +在多对多的关系和连接已常规用在关系数据库时,文档数据库和NoSQL重启了辩论:如何最好地在数据库中表示多对多关系。这个辩论可比NoSQL古老得多,事实上,最早可以追溯到计算机化数据库系统。 -20世纪70年代最受欢迎的业务数据处理数据库是IBM的信息管理系统(IMS),最初是为了在阿波罗太空计划中进行库存管理而开发的,并于1968年首次商业发布【13】。目前它仍在使用和维护,在IBM大型机的OS/390上运行【14】。 -IMS的设计使用了一个相当简单的数据模型,称为层次模型,它与文档数据库使用的JSON模型有一些显着的相似之处【2】。它将所有数据表示为嵌套在记录中的记录树,就像[图2-2](img/fig2-2.png)的JSON结构一样。 +20世纪70年代最受欢迎的业务数据处理数据库是IBM的信息管理系统(IMS),最初是为了阿波罗太空计划的库存管理而开发的,并于1968年有了首次商业发布【13】。目前它仍在使用和维护,运行在IBM大型机的OS/390上【14】。 -像文档数据库一样,IMS在一对多的关系中运行良好,但是它使多对多的关系变得困难,并且不支持连接。开发人员必须决定是否冗余(非规范化)数据或手动解决从一个记录到另一个记录的引用。这些二十世纪六七十年代的问题与开发人员今天遇到的文档数据库问题非常相似【15】。 +IMS的设计中使用了一个相当简单的数据模型,称为**层次模型(hierarchical model)**,它与文档数据库使用的JSON模型有一些惊人的相似之处【2】。它将所有数据表示为嵌套在记录中的记录树,这很像[图2-2](img/fig2-2.png)的JSON结构。 -提出了各种解决方案来解决层次模型的局限性。其中最突出的两个是关系模型(它变成了SQL,接管了世界)和网络模型(最初很受关注,但最终变得模糊)。这两个阵营之间的“大辩论”持续了70年代的大部分时间【2】。 +同文档数据库一样,IMS能良好处理一对多的关系,但是很难应对多对多的关系,并且不支持连接。开发人员必须决定是否复制(非规范化)数据或手动解决从一个记录到另一个记录的引用。这些二十世纪六七十年代的问题与现在开发人员遇到的文档数据库问题非常相似【15】。 -由于这两个模式解决的问题今天仍然如此相关,今天的辩论值得简要回顾一下。 +那时人们提出了各种不同的解决方案来解决层次模型的局限性。其中最突出的两个是**关系模型(relational model)**(它变成了SQL,统治了世界)和**网络模型()network model**(最初很受关注,但最终变得冷门)。这两个阵营之间的“大辩论”在70年代持续了很久时间【2】。 + +那两个模式解决的问题与当前的问题相关,因此值得简要回顾一下那场辩论。 #### 网络模型 -网络模型由一个称为数据系统语言会议(CODASYL)的委员会进行了标准化,并由几个不同的数据源进行实施;它也被称为CODASYL模型【16】。 +网络模型由一个称为数据系统语言会议(CODASYL)的委员会进行了标准化,并被数个不同的数据库商实现;它也被称为CODASYL模型【16】。 -CODASYL模型是层次模型的推广。在分层模型的树结构中,每条记录只有一个父节点,在网络模式中,一个记录可能有多个父母。例如,“大西雅图地区”地区可能有一条记录,而且每个居住在该地区的用户都可以与之相关联。这允许对多对一和多对多的关系进行建模。 +CODASYL模型是层次模型的推广。在层次模型的树结构中,每条记录只有一个父节点;在网络模式中,每条记录可能有多个父节点。例如,“Greater Seattle Area”地区可能是一条记录,每个居住在该地区的用户都可以与之相关联。这允许对多对一和多对多的关系进行建模。 -网络模型中记录之间的链接不是外键,而更像编程语言中的指针(同时仍然存储在磁盘上)。访问记录的唯一方法是沿着这些链路链上的根记录进行路径。这被称为**访问路径**。 +网络模型中记录之间的链接不是外键,而更像编程语言中的指针(同时仍然存储在磁盘上)。访问记录的唯一方法是跟随从根记录起沿这些链路所形成的路径。这被称为**访问路径(access path)**。 -在最简单的情况下,访问路径可能类似于遍历链表:从列表头开始,一次查看一条记录,直到找到所需的记录。但在一个多对多关系的世界里,几条不同的路径可能会导致相同的记录,一个使用网络模型的程序员必须跟踪这些不同的访问路径。 +最简单的情况下,访问路径类似遍历链表:从列表头开始,每次查看一条记录,直到找到所需的记录。但在多对多关系的情况中,数条不同的路径可以到达相同的记录,网络模型的程序员必须跟踪这些不同的访问路径。 -CODASYL中的查询是通过遍历记录列表和访问路径后,通过在数据库中移动游标来执行的。如果记录有多个父母(即来自其他记录的多个传入指针),则应用程序代码必须跟踪所有的各种关系。甚至CODASYL委员会成员也承认,这就像在一个n维数据空间中进行导航【17】。 +CODASYL中的查询是通过利用遍历记录列和跟随访问路径表在数据库中移动游标来执行的。如果记录有多个父结点(即多个来自其他记录的传入指针),则应用程序代码必须跟踪所有的各种关系。甚至CODASYL委员会成员也承认,这就像在n维数据空间中进行导航【17】。 -尽管手动访问路径选择能够最有效地利用20世纪70年代非常有限的硬件功能(如磁带驱动器,其搜索速度非常慢),但问题是他们使查询和更新数据库的代码变得复杂不灵活。无论是分层还是网络模型,如果你没有一个你想要的数据的路径,那么你就处于一个困难的境地。你可以改变访问路径,但是你必须经过大量的手写数据库查询代码,并重写它来处理新的访问路径。很难对应用程序的数据模型进行更改。 +尽管手动选择访问路径够能最有效地利用20世纪70年代非常有限的硬件功能(如磁带驱动器,其搜索速度非常慢),但这使得查询和更新数据库的代码变得复杂不灵活。无论是分层还是网络模型,如果你没有所需数据的路径,就会陷入困境。你可以改变访问路径,但是必须浏览大量手写数据库查询代码,并重写来处理新的访问路径。更改应用程序的数据模型是很难的。 #### 关系模型 -相比之下,关系模型做的就是将所有的数据放在光天化日之下:一个**关系(table)**只是一个**元组(行)**的集合,就是这样。没有迷宫似的嵌套结构,如果你想看看数据,没有复杂的访问路径。您可以读取表中的任何或所有行,选择符合任意条件的行。您可以通过指定某些列作为关键字并匹配这些关键字来读取特定行。您可以在任何表中插入一个新的行,而不必担心与其他表的外键关系[^iv]。 +相比之下,关系模型做的就是将所有的数据放在光天化日之下:一个**关系(表)**只是一个**元组(行)**的集合,仅此而已。如果你想读取数据,它没有迷宫似的嵌套结构,也没有复杂的访问路径。你可以选中符合任意条件的行,读取表中的任何或所有行。你可以通过指定某些列作为匹配关键字来读取特定行。你可以在任何表中插入一个新的行,而不必担心与其他表的外键关系[^iv]。 -[^iv]: 外键约束允许对修改做限制,对于关系模型这并不是必选项。 即使有约束,在查询时执行外键连接,而在CODASYL中,连接在插入时高效完成。 +[^iv]: 外键约束允许对修改做限制,对于关系模型这并不是必选项。即使有约束,查询时会执行外键连接,而在CODASYL中,连接在插入时高效完成。 在关系数据库中,查询优化器自动决定查询的哪些部分以哪个顺序执行,以及使用哪些索引。这些选择实际上是“访问路径”,但最大的区别在于它们是由查询优化器自动生成的,而不是由程序员生成,所以我们很少需要考虑它们。 -如果你想以新的方式查询你的数据,你可以声明一个新的索引,查询会自动使用哪个索引是最合适的。您不需要更改查询来利用新的索引。 (请参阅“[用于数据的查询语言](#用于数据的查询语言)”。)关系模型因此使向应用程序添加新功能变得更加容易。 +如果想按新的方式查询数据,可以声明一个新的索引,查询会自动使用最合适的那些索引。无需更改查询来利用新的索引。 (请参阅“[用于数据的查询语言](#用于数据的查询语言)”。)关系模型因此使添加应用程序新功能变得更加容易。 -关系数据库的查询优化器是复杂的,他们已经耗费了多年的研究和开发工作【18】。但关系模型的一个关键洞察是:只需构建一次查询优化器,然后使用该数据库的所有应用程序都可以从中受益。如果您没有查询优化器,那么为特定查询手动编写访问路径比编写通用优化器更容易——但通用解决方案从长期看更好。 +关系数据库的查询优化器是复杂的,耗费多年的研究和开发精力【18】。关系模型的一个关键洞察是:只需构建一次查询优化器,随后使用该数据库的所有应用程序都可以从中受益。如果你没有查询优化器的话,那么为特定查询手动编写访问路径比编写通用优化器更容易——不过通用解决方案从长期看更好。 #### 与文档数据库相比 -文档数据库在一个方面还原为层次模型:在其父记录中存储嵌套记录([图2-1]()中的一对多关系,如位置,教育和`contact_info`),而不是在单独的表中。 +在一个方面,文档数据库还原为层次模型:在其父记录中存储嵌套记录([图2-1]()中的一对多关系,如`positions`,`education`和`contact_info`),而不是在单独的表中。 -但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都被一个唯一的标识符引用,这个标识符在关系模型中被称为外键,在文档模型中称为文档引用【9】。该标识符在读取时通过使用加入或后续查询来解决。迄今为止,文档数据库没有遵循CODASYL的路径。 +但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都被一个唯一的标识符引用,这个标识符在关系模型中被称为**外键**,在文档模型中称为**文档引用**【9】。该标识符在读取时通过连接或后续查询来解析。迄今为止,文档数据库没有遵循CODASYL的路数。 ### 关系型数据库与文档数据库在今日的对比 -将关系数据库与文档数据库进行比较时,需要考虑许多差异,包括它们的容错属性(参阅[第5章](ch5.md))和处理并发性(参阅[第7章](ch7.md))。在本章中,我们将只关注数据模型中的差异。 +将关系数据库与文档数据库进行比较时,可以考虑许多方面的差异,包括它们的容错属性(参阅[第5章](ch5.md))和处理并发性(参阅[第7章](ch7.md))。本章将只关注数据模型中的差异。 -支持文档数据模型的主要论据是架构灵活性,由于局部性而导致的更好的性能,对于某些应用程序而言更接近于应用程序使用的数据结构。关系模型通过为连接提供更好的支持以及支持多对一和多对多的关系来反击。 +支持文档数据模型的主要论据是架构灵活性,因局部性而拥有更好的性能,以及对于某些应用程序而言更接近于应用程序使用的数据结构。关系模型通过为连接提供更好的支持以及支持多对一和多对多的关系来反击。 #### 哪个数据模型更方便写代码? -如果应用程序中的数据具有类似文档的结构(即,一对多关系树,通常整个树被一次加载),那么使用文档模型可能是一个好主意。将类似文档的结构分解成多个表(如[图2-1](img/fig2-1.png)中的位置,教育和`contact_info`)的关系技术可能导致繁琐的模式和不必要的复杂的应用程序代码。 +如果应用程序中的数据具有类似文档的结构(即,一对多关系树,通常一次性加载整个树),那么使用文档模型可能是一个好主意。将类似文档的结构分解成多个表(如[图2-1](img/fig2-1.png)中的位置,教育和`contact_info`)的关系技术可能导致繁琐的模式和不必要的复杂的应用程序代码。 -文档模型有一定的局限性:例如,你不能直接引用文档中的需要的项目,而是需要说“用户251的位置列表中的第二项”(很像访问路径在分层模型中)。但是,只要文件嵌套不太深,通常不是问题。 +文档模型有一定的局限性:例如,不能直接引用文档中的嵌套的项目,而是需要说“用户251的位置列表中的第二项”(很像分层模型中的访问路径)。但是,只要文件嵌套不太深,通常不是问题。 -应用程序对文档数据库连接的垃圾支持也许或也许不是一个问题。例如,在使用文档数据库记录 哪个事件发生在哪儿 的分析应用程序中,可能永远不需要多对多的关系【19】。 +对文档数据库连接的糟糕支持也许或也许不是一个问题,这取决于应用程序。例如,分析应用程可能永远不需要多对多的关系,如果它使用文档数据库来记录何事发生于何时【19】。 但是,如果您的应用程序确实使用多对多关系,那么文档模型就没有那么吸引人了。通过反规范化可以减少对连接的需求,但是应用程序代码需要做额外的工作来保持数据的一致性。通过向数据库发出多个请求,可以在应用程序代码中模拟连接,但是这也将复杂性转移到应用程序中,并且通常比由数据库内的专用代码执行的连接慢。在这种情况下,使用文档模型会导致更复杂的应用程序代码和更差的性能【15】。 -说哪个数据模型在一般情况下导致更简单的应用程序代码是不可能的;它取决于数据项之间存在的关系种类。对于高度相互关联的数据,文档模型很尴尬,关系模型是可接受的,而图形模型(参见“[图数据模型](#图数据模型)”)是最自然的。 +很难说在一般情况下哪个数据模型让应用程序代码更简单;它取决于数据项之间存在的关系种类。对于高度相联的数据,文档模型是糟糕的,关系模型是可接受的,而图形模型(参见“[图数据模型](#图数据模型)”)是最自然的。 #### 文档模型中的架构灵活性 -大多数文档数据库以及关系数据库中的JSON支持都不会对文档中的数据执行任何模式。关系数据库中的XML支持通常带有可选的模式验证。没有模式意味着可以将任意的键和值添加到文档中,并且在读取时,客户端对于文档可能包含的字段没有保证。 +大多数文档数据库以及关系数据库中的JSON支持都不会强制文档中的数据采用何种模式。关系数据库中的XML支持通常带有可选的模式验证。没有模式意味着可以将任意的键和值添加到文档中,并且当读取时,客户端对无法保证文档可能包含的字段。 -文档数据库有时称为**无模式(schemaless)**,但这是误导性的,因为读取数据的代码通常采用某种结构——即存在隐式模式,但不由数据库强制执行【20】。一个更精确的术语是**读时模式(schema-on-read)**(数据的结构是隐含的,只有在数据被读取时才被解释),相应的是**写时模式(schema-on-write)**(传统的关系数据库方法,模式是明确,数据库确保所有的数据都符合它的形式)【21】。 +文档数据库有时称为**无模式(schemaless)**,但这是误导性的,因为读取数据的代码通常假定某种结构——即存在隐式模式,但不由数据库强制执行【20】。一个更精确的术语是**读时模式(schema-on-read)**(数据的结构是隐含的,只有在数据被读取时才被解释),相应的是**写时模式(schema-on-write)**(传统的关系数据库方法,模式明确,且数据库确保所有的数据都符合其模式)【21】。 -读取模式类似于编程语言中的动态(运行时)类型检查,而模式写入类似于静态(编译时)类型检查。就像静态和动态类型检查的倡导者对于它们的相对优点有很大的争议【22】,数据库中模式的执行是一个有争议的话题,一般来说没有正确或错误的答案。 +读取模式类似于编程语言中的动态(运行时)类型检查,而模式写入类似于静态(编译时)类型检查。就像静态和动态类型检查的相对优点具有很大的争议性【22】,数据库中模式的强制性是一个具有争议的话题,一般来说没有正确或错误的答案。 -在应用程序想要改变其数据格式的情况下,这些方法之间的区别特别明显。例如,假设你正在将每个用户的全名存储在一个字段中,而你想分别存储名字和姓氏【23】。在文档数据库中,只需开始使用新字段写入新文档,并在应用程序中使用代码来处理读取旧文档时的情况。例如: +在应用程序想要改变其数据格式的情况下,这些方法之间的区别特别明显。例如,假设你把每个用户的全名存储在一个字段中,而现在想分别存储名字和姓氏【23】。在文档数据库中,只需开始写入具有新字段的新文档,并在应用程序中使用代码来处理读取旧文档时的情况。例如: ```go if (user && user.name && !user.first_name) { - // Documents written before Dec 8, 2013 don't have first_name + // Documents written before Dec 8, 2013 don't have first_name user.first_name = user.name.split(" ")[0]; } ``` -另一方面,在“静态类型”数据库模式中,通常会执行以下操作: +另一方面,在“静态类型”数据库模式中,通常会执行以下**迁移(migration)**操作: ```sql ALTER TABLE users ADD COLUMN first_name text; -UPDATE users SET first_name = split_part(name, ' ', 1); -- PostgreSQL +UPDATE users SET first_name = split_part(name, ' ', 1); -- PostgreSQL UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL ``` -模式变更的速度很慢,而且需要停机。这种声誉并不是完全应得的:大多数关系数据库系统在几毫秒内执行`ALTER TABLE`语句。 MySQL是一个值得注意的例外,它执行`ALTER TABLE`时会复制整个表,这可能意味着在更改一个大表时会花费几分钟甚至几个小时的停机时间,尽管存在各种工具可以解决这个限制【24,25,26】。 +模式变更的速度很慢,而且需要停运。这种坏声誉并不是完全应得的:大多数关系数据库系统可在几毫秒内执行`ALTER TABLE`语句。 MySQL是一个值得注意的例外,它执行`ALTER TABLE`时会复制整个表,这可能意味着在更改一个大表时会花费几分钟甚至几个小时的停机时间,尽管存在各种工具可以解决这个限制【24,25,26】。 -在大型表上运行`UPDATE`语句在任何数据库上都可能会很慢,因为每一行都需要重写。如果这是不可接受的,应用程序可以将`first_name`设置为默认值`NULL`,并在读取时填充它,就像使用文档数据库一样。 +在大型表上运行`UPDATE`语句在任何数据库上都可能会很慢,因为每一行都需要重写。如果这是不可接受的,应用程序可以将`first_name`设置为默认值`NULL`,并在读取时再填充,就像使用文档数据库一样。 -如果由于某种原因(例如,数据是异构的)集合中的项目并不都具有相同的结构,例如,因为: +读时模式更具优势,当由于某种原因(例如,数据是异构的)集合中的项目并不都具有相同的结构,例如,因为: * 有许多不同类型的对象,将每种类型的对象放在自己的表中是不现实的。 -* 数据的结构由您无法控制,且随时可能更改的外部系统决定。 +* 数据的结构由您无法控制且随时可能变化的外部系统决定。 -在这样的情况下,模式的伤害远大于它的帮助,无模式文档可能是一个更加自然的数据模型。但是,如果所有记录都有相同的结构,那么模式就是记录和强制这种结构的有用机制。我们将在第四章更详细地讨论模式和模式演化。 +在这样的情况下,模式的伤害远大于它的帮助,无模式文档可能是一个更加自然的数据模型。但是,如果所有记录都具有相同的结构,那么模式就是记录并强制这种结构的有用机制。第四章将更详细地讨论模式和模式演化。 #### 查询的数据局部性 @@ -367,7 +368,7 @@ SQL示例不保证任何特定的顺序,所以它不介意顺序是否改变 现在想让当前所选页面的标题有一个蓝色的背景,以便在视觉上突出显示。 使用CSS实现起来非常简单: ```css -li.selected > p { +li.selected > p { background-color: blue; } ``` @@ -552,7 +553,7 @@ db.observations.aggregate([ * 唯一标识符 * **边的起点/尾点(tail vertex)** -* **边的终点/头点(head vertex)** +* **边的终点/头点(head vertex)** * 描述两个顶点之间关系类型的标签 * 一组属性(键值对) @@ -613,7 +614,7 @@ CREATE ```cypher MATCH (person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (us:Location {name:'United States'}), - (person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (eu:Location {name:'Europe'}) + (person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (eu:Location {name:'Europe'}) RETURN person.name ``` @@ -649,24 +650,24 @@ WITH RECURSIVE in_usa(vertex_id) AS ( SELECT vertex_id FROM vertices WHERE properties ->> 'name' = 'United States' UNION - SELECT edges.tail_vertex FROM edges + SELECT edges.tail_vertex FROM edges JOIN in_usa ON edges.head_vertex = in_usa.vertex_id WHERE edges.label = 'within' ), -- in_europe 包含所有的欧洲境内的地点ID in_europe(vertex_id) AS ( SELECT vertex_id FROM vertices WHERE properties ->> 'name' = 'Europe' - UNION + UNION SELECT edges.tail_vertex FROM edges JOIN in_europe ON edges.head_vertex = in_europe.vertex_id WHERE edges.label = 'within' ), - + -- born_in_usa 包含了所有类型为Person,且出生在美国的顶点 born_in_usa(vertex_id) AS ( SELECT edges.tail_vertex FROM edges JOIN in_usa ON edges.head_vertex = in_usa.vertex_id WHERE edges.label = 'born_in' ), - + -- lives_in_europe 包含了所有类型为Person,且居住在欧洲的顶点。 lives_in_europe(vertex_id) AS ( SELECT edges.tail_vertex FROM edges @@ -816,7 +817,7 @@ SPARQL是一种很好的查询语言——即使语义网从来没有出现, > #### 图形数据库与网络模型相比较 > -> 在“[文档数据库是否在重蹈覆辙?](#文档数据库是否在重蹈覆辙?)”中,我们讨论了CODASYL和关系模型如何竞争解决IMS中的多对多关系问题。乍一看,CODASYL的网络模型看起来与图模型相似。 CODASYL是否是图形数据库的第二个变种? +> 在“[文档数据库是否在重演历史?](#文档数据库是否在重演历史?)”中,我们讨论了CODASYL和关系模型如何竞争解决IMS中的多对多关系问题。乍一看,CODASYL的网络模型看起来与图模型相似。 CODASYL是否是图形数据库的第二个变种? > > 不,他们在几个重要方面有所不同: > @@ -862,15 +863,15 @@ born_in(lucy, idaho). ``` within_recursive(Location, Name) :- name(Location, Name). /* Rule 1 */ -within_recursive(Location, Name) :- within(Location, Via), /* Rule 2 */ +within_recursive(Location, Name) :- within(Location, Via), /* Rule 2 */ within_recursive(Via, Name). -migrated(Name, BornIn, LivingIn) :- name(Person, Name), /* Rule 3 */ +migrated(Name, BornIn, LivingIn) :- name(Person, Name), /* Rule 3 */ born_in(Person, BornLoc), within_recursive(BornLoc, BornIn), lives_in(Person, LivingLoc), within_recursive(LivingLoc, LivingIn). - + ?- migrated(Who, 'United States', 'Europe'). /* Who = 'Lucy'. */ ``` @@ -1035,4 +1036,3 @@ Datalog方法需要对本章讨论的其他查询语言采取不同的思维方 | 上一章 | 目录 | 下一章 | | -------------------------------------- | ------------------------------- | ---------------------------- | | [第一章:可靠、可扩展、可维护](ch1.md) | [设计数据密集型应用](README.md) | [第三章:存储与检索](ch3.md) | - From 3d3c8b05bf17b228c1a90d97fcdfa083fbac7e1e Mon Sep 17 00:00:00 2001 From: "jiajia.debug" Date: Sun, 1 Apr 2018 01:24:11 +0800 Subject: [PATCH 16/17] minor fix for ch2 --- ch2.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ch2.md b/ch2.md index baf156e..69904e5 100644 --- a/ch2.md +++ b/ch2.md @@ -32,11 +32,11 @@ ## 关系模型与文档模型 -现在最著名的数据模型可能是SQL。它基于Edgar Codd在1970年提出的关系模型【1】:数据被组织成**关系(relation)**(SQL中称作**表(table)**),其中每个关系是**元组(tuple)**(SQL中称作**行(row)**的无序集合。 +现在最著名的数据模型可能是SQL。它基于Edgar Codd在1970年提出的关系模型【1】:数据被组织成**关系**(SQL中称作**表**),其中每个关系是**元组**(SQL中称作**行**)的无序集合。 关系模型曾是一个理论性的提议,当时很多人都怀疑是否能够有效实现它。然而到了20世纪80年代中期,关系数据库管理系统(RDBMSes)和SQL已成为大多数人们存储和查询某些常规结构的数据的首选工具。关系数据库已经持续称霸了大约25~30年——这对计算机史来说是极其漫长的时间。 -关系数据库起源于商业数据处理,在20世纪60年代和70年代用大型计算机来执行。从今天的角度来看,那些用例显得很平常:典型的**事务处理(transaction processing)**(将销售或银行交易,航空公司预订,库存管理信息记录在库)和**批处理(batch processing)**(客户发票,工资单,报告)。 +关系数据库起源于商业数据处理,在20世纪60年代和70年代用大型计算机来执行。从今天的角度来看,那些用例显得很平常:典型的**事务处理**(将销售或银行交易,航空公司预订,库存管理信息记录在库)和**批处理**(客户发票,工资单,报告)。 当时的其他数据库迫使应用程序开发人员必须考虑数据库内部的数据表示形式。关系模型致力于将上述实现细节隐藏在更简洁的接口之后。 @@ -150,7 +150,7 @@ JSON表示比[图2-1](img/fig2-1.png)中的多表模式具有更好的**局部 [^iii]: 在撰写本文时,RethinkDB支持连接,MongoDB不支持连接,而CouchDB只支持预先声明的视图。 -如果数据库本身不支持连接,那就不得不在应用程序代码中通过对数据库进行多个查询来模拟连接。(在这种情况中,地区和行业的列表可能很小,改动很少,应用程序可以简单地将其保存在内存中。不过,执行连接的工作从数据库被转移到应用程序代码上。 +如果数据库本身不支持连接,则必须在应用程序代码中通过对数据库进行多个查询来模拟连接。(在这种情况中,地区和行业的列表可能很小,改动很少,应用程序可以简单地将其保存在内存中。不过,执行连接的工作从数据库被转移到应用程序代码上。 此外,即便应用程序的最初版本适合无连接的文档模型,随着功能添加到应用程序中,数据会变得更加互联。例如,考虑一下对简历例子进行的一些修改: @@ -181,7 +181,7 @@ IMS的设计中使用了一个相当简单的数据模型,称为**层次模型 同文档数据库一样,IMS能良好处理一对多的关系,但是很难应对多对多的关系,并且不支持连接。开发人员必须决定是否复制(非规范化)数据或手动解决从一个记录到另一个记录的引用。这些二十世纪六七十年代的问题与现在开发人员遇到的文档数据库问题非常相似【15】。 -那时人们提出了各种不同的解决方案来解决层次模型的局限性。其中最突出的两个是**关系模型(relational model)**(它变成了SQL,统治了世界)和**网络模型()network model**(最初很受关注,但最终变得冷门)。这两个阵营之间的“大辩论”在70年代持续了很久时间【2】。 +那时人们提出了各种不同的解决方案来解决层次模型的局限性。其中最突出的两个是**关系模型(relational model)**(它变成了SQL,统治了世界)和**网络模型(network model)**(最初很受关注,但最终变得冷门)。这两个阵营之间的“大辩论”在70年代持续了很久时间【2】。 那两个模式解决的问题与当前的问题相关,因此值得简要回顾一下那场辩论。 From bae11bab5ab6d208026da1ea95da0cfd1634cbf9 Mon Sep 17 00:00:00 2001 From: Vonng Date: Mon, 2 Apr 2018 00:38:01 +0800 Subject: [PATCH 17/17] ch3 35% --- ch3.md | 107 ++++++++++++++++++++++++++++++--------------------------- ch4.md | 8 ++--- 2 files changed, 60 insertions(+), 55 deletions(-) diff --git a/ch3.md b/ch3.md index 1570e6e..0a0e29b 100644 --- a/ch3.md +++ b/ch3.md @@ -11,21 +11,17 @@ [TOC] -在最基本的层次上,一个数据库需要完成两件事情:当你给它数据时,它应该存储起来,而当你提问时,它应该把数据返回给你。 +一个数据库在最基础的层次上需要完成两件事情:当你把数据交给数据库时,它应当把数据存储起来;而后当你向数据库要数据时,它应当把数据返回给你。 -在[第2章](ch2.md)中,我们讨论了数据模型和查询语言,即程序员录入数据库的数据格式,以及你可以再次获取它的机制。在本章中,我们讨论同样的问题,却是从数据库的视角:数据库如何存储我们提供的数据,以及如何在我们需要时重新找到数据。 +在[第2章](ch2.md)中,我们讨论了数据模型和查询语言,即程序员将数据录入数据库的格式,以及再次要回数据的机制。在本章中我们会从数据库的视角来讨论同样的问题:数据库如何存储我们提供的数据,以及如何在我们需要时重新找到数据。 -作为程序员,为什么要关心数据库如何在内部处理存储和检索?你可能不会从头开始实现自己的存储引擎,但是您需要从可用的许多存储引擎中选择适合应用程序的存储引擎。为了调谐一个存储引擎以适应应用工作负载,你需要大致了解存储引擎在做什么。 +作为程序员,为什么要关心数据库内部存储与检索的机理?你可能不会去从头开始实现自己的存储引擎,但是你**确实**需要从许多可用的存储引擎中选择一个合适的。而且为了调谐存储引擎以适配应用工作负载,你也需要大致了解存储引擎在底层究竟做什么。 -特别需要注意,针对事务性工作负载优化的存储引擎,与针对分析优化的存储引擎之间存在着巨大差异。稍后我们将在 “[事务处理还是分析?](#事务处理还是分析?)” 和 “[列存储](#列存储)”中探讨这个区别,那里将讨论针对分析优化的一系列存储引擎。 +特别需要注意,针对**事务**性负载和**分析性**负载优化的存储引擎之间存在巨大差异。稍后我们将在 “[事务处理还是分析?](#事务处理还是分析?)” 一节中探讨这一区别,并在 “[列存储](#列存储)”中讨论一系列针对分析优化存储引擎。 -但是,我们将从您最可能熟悉的两大类数据库:传统关系型数据库与所谓的“NoSQL”数据库开始,通过介绍它们的存储引擎来开始本章的内容。 +但是,我们将从您最可能熟悉的两大类数据库:传统关系型数据库与很多所谓的“NoSQL”数据库开始,通过介绍它们的**存储引擎**来开始本章的内容。我们会研究两大类存储引擎:**日志结构(log-structured)**的存储引擎,以及**面向页面(page-oriented)**的存储引擎(例如B树)。 -我们会研究两大类存储引擎:**日志结构(log-structured)**的存储引擎,以及**面向页面(page-oriented)**的存储引擎(如B树)。 - - - -## 数据库的底层数据结构 +## 驱动数据库的数据结构 世界上最简单的数据库可以用两个Bash函数实现: @@ -40,15 +36,20 @@ db_get () { } ``` -这两个函数实现了键值存储的功能。执行 `db_set key value` ,会将 `key` 和 `value` 存储在数据库中。键和值可以是(几乎)任何你喜欢的东西,例如,值可以是JSON文档。然后调用 `db_get key` ,查找与该键关联的最新值并将其返回。麻雀虽小,五脏俱全: +这两个函数实现了键值存储的功能。执行 `db_set key value` ,会将 **键(key)**和**值(value)** 存储在数据库中。键和值(几乎)可以是你喜欢的任何东西,例如,值可以是JSON文档。然后调用 `db_get key` ,查找与该键关联的最新值并将其返回。 + +麻雀虽小,五脏俱全: ```bash -$ db_set 123456 '{"name":"London","attractions":["Big Ben","London Eye"]}' $ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}' +$ db_set 123456 '{"name":"London","attractions":["Big Ben","London Eye"]}' $ + +$ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}' + $ db_get 42 {"name":"San Francisco","attractions":["Golden Gate Bridge"]} ``` -底层的存储格式非常简单:一个文本文件,每行包含一条逗号分隔的键值对(忽略转义问题的话,大致类似于CSV文件)。每次对 `db_set` 的调用都会追加记录到文件末尾,所以更新键的时候旧版本的值不会被覆盖。需要查看文件中最后一次出现的键以查找最新值(因此 `db_get` 中使用了 `tail -n 1 ` 。) +底层的存储格式非常简单:一个文本文件,每行包含一条逗号分隔的键值对(忽略转义问题的话,大致与CSV文件类似)。每次对 `db_set` 的调用都会向文件末尾追加记录,所以更新键的时候旧版本的值不会被覆盖 —— 因而查找最新值的时候,需要找到文件中键最后一次出现的位置(因此 `db_get` 中使用了 `tail -n 1 ` 。) ```bash $ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}' @@ -57,48 +58,48 @@ $ db_get 42 {"name":"San Francisco","attractions":["Exploratorium"]} $ cat database -123456,{"name":"London","attractions":["Big Ben","London Eye"]} 42,{"name":"San Francisco","attractions":["Golden Gate Bridge"]} 42,{"name":"San Francisco","attractions":["Exploratorium"]} +123456,{"name":"London","attractions":["Big Ben","London Eye"]} +42,{"name":"San Francisco","attractions":["Golden Gate Bridge"]} +42,{"name":"San Francisco","attractions":["Exploratorium"]} ``` -`db_set` 函数对于极其简单的场景其实有非常好的性能,因为在文件尾部追加写入通常是非常高效的。与`db_set`做的事情类似,许多数据库内部使用**日志(log)**,也就是一个**仅追加(append-only)**的数据文件。真正的数据库有更多的问题需要处理(如并发控制,回收磁盘空间以免日志无限增长,处理错误和部分写入记录),但基本原理是一样的。日志非常有用,我们还将在本书的其它部分见到它。 +`db_set` 函数对于极其简单的场景其实有非常好的性能,因为在文件尾部追加写入通常是非常高效的。与`db_set`做的事情类似,许多数据库在内部使用了**日志(log)**,也就是一个**仅追加(append-only)**的数据文件。真正的数据库有更多的问题需要处理(如并发控制,回收磁盘空间以避免日志无限增长,处理错误与部分写入的记录),但基本原理是一样的。日志极其有用,我们还将在本书的其它部分重复见到它好几次。 -> 日志这个词通常用来指应用程序日志,应用程序输出描述发生事情的文本。本书中,日志用于更一般的含义上:一个只有追加记录的序列。它不一定是人类可读的记录,它可能是只能由其他程序读取的二进制记录。 +> **日志(log)**这个词通常指应用日志:即应用程序输出的描述发生事情的文本。本书在更普遍的意义下使用**日志**这一词:一个仅追加的记录序列。它可能压根就不是给人类看的,使用二进制格式,并仅能由其他程序读取。 -另一方面,如果这个数据库中有大量记录,则我们的`db_get`函数的性能会非常糟糕。每次你想查找一个键时,`db_get`必须从头到尾扫描整个数据库文件来查找键的出现。在算法方面,查找的成本是`O(n)`:如果数据库的记录数量n增加了一倍,查找也需要一倍的时间。这就不好了。 +另一方面,如果这个数据库中有着大量记录,则这个`db_get` 函数的性能会非常糟糕。每次你想查找一个键时,`db_get` 必须从头到尾扫描整个数据库文件来查找键的出现。用算法的语言来说,查找的开销是 `O(n)` :如果数据库记录数量 n 翻了一倍,查找时间也要翻一倍。这就不好了。 -为了高效地找到数据库中特定键的值,我们需要一个数据结构:索引。本章将介绍一系列的索引结构并对它们进行对比。索引的通用思路是保存一些额外的元数据作为路标,帮助你找到你想要的数据。如果您想以几种不同的方式在相同的数据中搜索,也许需要在数据的不同部分使用多个不同的索引。 +为了高效查找数据库中特定键的值,我们需要一个数据结构:**索引(index)**。本章将介绍一系列的索引结构,并它们进行对比。索引背后的大致思想是,保存一些额外的元数据作为路标,帮助你找到想要的数据。如果您想在同一份数据中以几种不同的方式进行搜索,那么你也许需要不同的索引,建在数据的不同部分上。 -索引是从主数据派生的附加结构。许多数据库允许添加和删除索引,这不会影响数据的内容,它只影响查询的性能。维护额外的结构会产生开销,特别是在写入时。写入性能很难超过简单地追加写入文件,因为这是最简单的写入操作。任何类型的索引通常都会减慢写入速度,因为每次写入数据时都需要更新索引。 +索引是从主数据衍生的**附加(additional)**结构。许多数据库允许添加与删除索引,这不会影响数据的内容,它只影响查询的性能。维护额外的结构会产生开销,特别是在写入时。写入性能很难超过简单地追加写入文件,因为追加写入是最简单的写入操作。任何类型的索引通常都会减慢写入速度,因为每次写入数据时都需要更新索引。 -这是存储系统中一个重要的权衡:精心选取的索引加快了读取查询速度,但是每个索引都会减慢写入速度。因此数据库通常不会索引所有内容,需要程序员或DBA通过对应用查询模式的了解来手动选择索引。你可以选择能为应用带来最大收益,同时又不会引入超必要开销的索引。 +这是存储系统中一个重要的权衡:精心选择的索引加快了读查询的速度,但是每个索引都会拖慢写入速度。因为这个原因,数据库默认并不会索引所有的内容,而需要你(程序员或DBA)通过对应用查询模式的了解来手动选择索引。你可以选择能为应用带来最大收益,同时又不会引入超出必要开销的索引。 ### 哈希索引 -让我们从**键值数据(key-value Data)**的索引开始。这不是您可以索引的唯一一种数据类型,但键值数据是非常常见的。对于更复杂的索引来说,这是一个有用的构建模块。 +让我们从**键值数据(key-value Data)**的索引开始。这不是您可以索引的唯一数据类型,但键值数据是很常见的。对于更复杂的索引来说,这是一个有用的构建模块。 -键值存储与在大多数编程语言中可以找到的字典类型非常相似,通常字典都是用散列表(哈希表)实现的。哈希映射在许多算法教科书中都有描述【1,2】,所以我们不会详细讨论它们工作方式。既然我们已经有内存数据结构——HashMap,为什么不使用它们来索引在磁盘上的数据呢? +键值存储与在大多数编程语言中可以找到的**字典(dictionary)**类型非常相似,通常字典都是用**散列映射(hash map)**(或**哈希表(hash table)**)实现的。哈希映射在许多算法教科书中都有描述【1,2】,所以这里我们不会讨论它的工作细节。既然我们已经有**内存中**数据结构 —— 哈希映射,为什么不使用它来索引在**磁盘上**的数据呢? -假设我们的数据存储只包含一个文件,就像前面的例子一样。然后,最简单的索引策略是:保留一个内存HashMap,其中每个键映射到数据文件中的一个字节偏移量,即该值可以被找到的位置。 - -如[图3-1](img/fig3-1.png)所示。无论何时将新的键值对添加到文件中,还要更新散列映射以反映刚刚写入的数据的偏移量(这适用于插入新键和更新现有键)。查找一个值时,使用哈希映射来查找数据文件中的偏移量,寻找该位置并读取该值。 +假设我们的数据存储只是一个追加写入的文件,就像前面的例子一样。那么最简单的索引策略就是:保留一个内存中的哈希映射,其中每个键都映射到一个数据文件中的字节偏移量,指明了可以找到对应值的位置,如[图3-1](img/fig3-1.png)所示。当你将新的键值对追加写入文件中时,还要更新散列映射,以反映刚刚写入的数据的偏移量(这同时适用于插入新键与更新现有键)。当你想查找一个值时,使用哈希映射来查找数据文件中的偏移量,**寻找(seek)**该位置并读取该值。 ![](img/fig3-1.png) **图3-1 以类CSV格式存储键值对的日志,并使用内存哈希映射进行索引。** -听上去简单,但这是一个可行的方法。这实际上就是Bitcask做的事情(Riak中默认的存储引擎)【3】。 Bitcask提供高性能的读取和写入操作,但所有键必须能合适地放入在内存,因为哈希映射完全保留在内存中。这些值可以使用比可用内存更多的空间,因为可以从磁盘上通过一次`seek`加载所需部分,如果数据文件的那部分已经在文件系统缓存中,则读取根本不需要任何磁盘I/O。 +听上去简单,但这是一个可行的方法。现实中,Bitcask实际上就是这么做的(Riak中默认的存储引擎)【3】。 Bitcask提供高性能的读取和写入操作,但所有键必须能放入可用内存中,因为哈希映射完全保留在内存中。这些值可以使用比可用内存更多的空间,因为可以从磁盘上通过一次`seek`加载所需部分,如果数据文件的那部分已经在文件系统缓存中,则读取根本不需要任何磁盘I/O。 像Bitcask这样的存储引擎非常适合每个键的值经常更新的情况。例如,键可能是视频的URL,值可能是它播放的次数(每次有人点击播放按钮时递增)。在这种类型的工作负载中,有很多写操作,但是没有太多不同的键——每个键有很多的写操作,但是将所有键保存在内存中是可行的。 -直到现在,我们只是追加写一个文件 - 所以我们如何避免最终用完磁盘空间?一个好的解决方案是通过在达到一定大小时关闭一个段文件,然后将其写入一个新的段文件来将日志分割成特定大小的段。然后我们可以对这些段进行压缩,如[图3-2](img/fig3-2.png)所示。压缩意味着在日志中丢弃重复的键,只保留每个键的最新更新。 +直到现在,我们只是追加写入一个文件 —— 所以如何避免最终用完磁盘空间?一种好的解决方案是,将日志分为特定大小的段,当日志增长到特定尺寸时关闭当前段文件,并开始写入一个新的段文件。然后,我们就可以对这些段进行**压缩(compaction)**,如[图3-2](img/fig3-2.png)所示。压缩意味着在日志中丢弃重复的键,只保留每个键的最近更新。 ![](img/fig3-2.png) **图3-2 压缩键值更新日志(统计猫视频的播放次数),只保留每个键的最近值** -而且,由于压缩经常会使得段更小(假设在一个段内键被平均重写了好几次),我们也可以在执行压缩的同时将多个段合并在一起,如[图3-3](img/fig3-3.png)所示。段被写入后永远不会被修改,所以合并的段被写入一个新的文件。冻结段的合并和压缩可以在后台线程中完成,在进行时,我们仍然可以继续使用旧的段文件来正常提供读写请求。合并过程完成后,我们将读取请求转换为使用新的合并段而不是旧段 - 然后可以简单地删除旧的段文件。 +而且,由于压缩经常会使得段变得很小(假设在一个段内键被平均重写了好几次),我们也可以在执行压缩的同时将多个段合并在一起,如[图3-3](img/fig3-3.png)所示。段被写入后永远不会被修改,所以合并的段被写入一个新的文件。冻结段的合并和压缩可以在后台线程中完成,在进行时,我们仍然可以继续使用旧的段文件来正常提供读写请求。合并过程完成后,我们将读取请求转换为使用新的合并段而不是旧段 —— 然后可以简单地删除旧的段文件。 ![](img/fig3-3.png) @@ -129,7 +130,7 @@ $ cat database 乍一看,只有追加日志看起来很浪费:为什么不更新文件,用新值覆盖旧值?但是只能追加设计的原因有几个: -* 追加和分段合并是顺序写入操作,通常比随机写入快得多,尤其是在磁盘旋转硬盘上。在某种程度上,顺序写入在基于闪存的固态硬盘(SSD)上也是优选的【4】。我们将在第83页的“[比较B-树和LSM-树](#比较B-树和LSM-树)”中进一步讨论这个问题。 +* 追加和分段合并是顺序写入操作,通常比随机写入快得多,尤其是在磁盘旋转硬盘上。在某种程度上,顺序写入在基于闪存的**固态硬盘(SSD)**上也是优选的【4】。我们将在第83页的“[比较B-树和LSM-树](#比较B-树和LSM-树)”中进一步讨论这个问题。 * 如果段文件是附加的或不可变的,并发和崩溃恢复就简单多了。例如,您不必担心在覆盖值时发生崩溃的情况,而将包含旧值和新值的一部分的文件保留在一起。 * 合并旧段可以避免数据文件随着时间的推移而分散的问题。 @@ -151,9 +152,9 @@ $ cat database 现在我们可以对段文件的格式做一个简单的改变:我们要求键值对的序列按键排序。乍一看,这个要求似乎打破了我们使用顺序写入的能力,但是我们马上就会明白这一点。 -我们把这个格式称为Sorted String Table,简称SSTable。我们还要求每个键只在每个合并的段文件中出现一次(压缩过程已经确保)。与使用散列索引的日志段相比,SSTable有几个很大的优势: +我们把这个格式称为**排序字符串表(Sorted String Table)**,简称SSTable。我们还要求每个键只在每个合并的段文件中出现一次(压缩过程已经保证)。与使用散列索引的日志段相比,SSTable有几个很大的优势: -1. 合并段是简单而高效的,即使文件大于可用内存。这种方法就像mergesort算法中使用的方法一样,如图3-4所示:您开始并排读取输入文件,查看每个文件中的第一个键,复制最低键(根据排序顺序)到输出文件,并重复。这产生一个新的合并段文件,也按键排序。 +1. 合并段是简单而高效的,即使文件大于可用内存。这种方法就像归并排序算法中使用的方法一样,如[图3-4](img/fig3-4.png)所示:您开始并排读取输入文件,查看每个文件中的第一个键,复制最低键(根据排序顺序)到输出文件,并重复。这产生一个新的合并段文件,也按键排序。 ![](img/fig3-4.png) @@ -167,12 +168,14 @@ $ cat database **图3-5 具有内存索引的SSTable** - 您仍然需要一个内存中索引来告诉您一些键的偏移量,但它可能很稀疏:每几千字节的段文件就有一个键就足够了,因为几千字节可以很快被扫描。 + 您仍然需要一个内存中索引来告诉您一些键的偏移量,但它可能很稀疏:每几千字节的段文件就有一个键就足够了,因为几千字节可以很快被扫描[^i]。 -3. 由于读取请求无论如何都需要扫描所请求范围内的多个键值对,因此可以将这些记录分组到块中,并在将其写入磁盘之前对其进行压缩(如图3-5中的阴影区域所示) 。稀疏内存中索引的每个条目都指向压缩块的开始处。除了节省磁盘空间之外,压缩还可以减少IO带宽的使用。 +3. 由于读取请求无论如何都需要扫描所请求范围内的多个键值对,因此可以将这些记录分组到块中,并在将其写入磁盘之前对其进行压缩(如[图3-5](img/fig3-5.png)中的阴影区域所示) 。稀疏内存中索引的每个条目都指向压缩块的开始处。除了节省磁盘空间之外,压缩还可以减少IO带宽的使用。 +[^i]: 如果所有的键与值都是定长的,你可以使用段文件上的二分查找并完全避免使用内存索引。然而实践中键值通常都是变长的,因此如果没有索引,就很难知道记录的分界点(前一条记录结束,后一条记录开始的地方) + #### 构建和维护SSTables 到目前为止,但是如何让你的数据首先被按键排序呢?我们的传入写入可以以任何顺序发生。 @@ -181,24 +184,24 @@ $ cat database 现在我们可以使我们的存储引擎工作如下: -* 写入时,将其添加到内存中的平衡树数据结构(例如,红黑树)。这个内存树有时被称为memtable。 -* 当memtable大于某个阈值(通常为几兆字节)时,将其作为SSTable文件写入磁盘。这可以高效地完成,因为树已经维护了按键排序的键值对。新的SSTable文件成为数据库的最新部分。当SSTable被写入磁盘时,写入可以继续到一个新的memtable实例。 -* 为了提供读取请求,首先尝试在memtable中找到关键字,然后在最近的磁盘段中,然后在下一个较旧的段中找到该关键字。 +* 写入时,将其添加到内存中的平衡树数据结构(例如,红黑树)。这个内存树有时被称为**内存表(memtable)**。 +* 当**内存表**大于某个阈值(通常为几兆字节)时,将其作为SSTable文件写入磁盘。这可以高效地完成,因为树已经维护了按键排序的键值对。新的SSTable文件成为数据库的最新部分。当SSTable被写入磁盘时,写入可以继续到一个新的内存表实例。 +* 为了提供读取请求,首先尝试在内存表中找到关键字,然后在最近的磁盘段中,然后在下一个较旧的段中找到该关键字。 * 有时会在后台运行合并和压缩过程以组合段文件并丢弃覆盖或删除的值。 -这个方案效果很好。它只会遇到一个问题:如果数据库崩溃,则最近的写入(在memtable中,但尚未写入磁盘)将丢失。为了避免这个问题,我们可以在磁盘上保存一个单独的日志,每个写入都会立即被附加到磁盘上,就像在前一节中一样。该日志不是按排序顺序,但这并不重要,因为它的唯一目的是在崩溃后恢复memtable。每当Memtable写出到SSTable时,相应的日志都可以被丢弃。 +这个方案效果很好。它只会遇到一个问题:如果数据库崩溃,则最近的写入(在内存表中,但尚未写入磁盘)将丢失。为了避免这个问题,我们可以在磁盘上保存一个单独的日志,每个写入都会立即被附加到磁盘上,就像在前一节中一样。该日志不是按排序顺序,但这并不重要,因为它的唯一目的是在崩溃后恢复内存表。每当内存表写出到SSTable时,相应的日志都可以被丢弃。 #### 用SSTables制作LSM树 这里描述的算法本质上是LevelDB 【6】和RocksDB 【7】中使用的关键值存储引擎库,被设计嵌入到其他应用程序中。除此之外,LevelDB可以在Riak中用作Bitcask的替代品。在Cassandra和HBase中使用了类似的存储引擎【8】,这两种引擎都受到了Google的Bigtable文档【9】(引入了SSTable和memtable)的启发。 -最初这种索引结构是由Patrick O'Neil等人描述的。在日志结构合并树(或LSM-Tree)【10】的基础上,建立在以前的工作上日志结构的文件系统【11】。基于这种合并和压缩排序文件原理的存储引擎通常被称为LSM存储引擎。 +最初这种索引结构是由Patrick O'Neil等人描述的。在日志结构合并树(或LSM树)【10】的基础上,建立在以前的工作上日志结构的文件系统【11】。基于这种合并和压缩排序文件原理的存储引擎通常被称为LSM存储引擎。 -Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使用类似的方法来存储它的词典【12,13】。全文索引比键值索引复杂得多,但是基于类似的想法:在搜索查询中给出一个单词,找到提及单词的所有文档(网页,产品描述等)。这是通过键值结构实现的,其中键是单词(术语),值是包含单词(发布列表)的所有文档的ID的列表。在Lucene中,从术语到发布列表的这种映射保存在SSTable类的有序文件中,根据需要在后台合并【14】。 +Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使用类似的方法来存储它的词典【12,13】。全文索引比键值索引复杂得多,但是基于类似的想法:在搜索查询中给出一个单词,找到提及单词的所有文档(网页,产品描述等)。这是通过键值结构实现的,其中键是单词(**关键词(term)**),值是包含单词(文章列表)的所有文档的ID的列表。在Lucene中,从术语到发布列表的这种映射保存在SSTable类的有序文件中,根据需要在后台合并【14】。 #### 性能优化 -与往常一样,大量的细节使得存储引擎在实践中表现良好。例如,当查找数据库中不存在的键时,LSM树算法可能会很慢:您必须检查memtable,然后将这些段一直回到最老的(可能必须从磁盘读取每一个),然后才能确定键不存在。为了优化这种访问,存储引擎通常使用额外的Bloom过滤器【15】。 (布隆过滤器是用于近似集合内容的内存高效数据结构,它可以告诉您数据库中是否出现键,从而为不存在的键节省许多不必要的磁盘读取操作。 +与往常一样,大量的细节使得存储引擎在实践中表现良好。例如,当查找数据库中不存在的键时,LSM树算法可能会很慢:您必须检查内存表,然后将这些段一直回到最老的(可能必须从磁盘读取每一个),然后才能确定键不存在。为了优化这种访问,存储引擎通常使用额外的Bloom过滤器【15】。 (布隆过滤器是用于近似集合内容的内存高效数据结构,它可以告诉您数据库中是否出现键,从而为不存在的键节省许多不必要的磁盘读取操作。 还有不同的策略来确定SSTables如何被压缩和合并的顺序和时间。最常见的选择是大小分层压实。 LevelDB和RocksDB使用平坦压缩(LevelDB因此得名),HBase使用大小分层,Cassandra同时支持【16】。在规模级别的调整中,更新和更小的SSTables先后被合并到更老的和更大的SSTable中。在水平压实中,关键范围被拆分成更小的SSTables,而较旧的数据被移动到单独的“水平”,这使得压缩能够更加递增地进行,并且使用更少的磁盘空间。 @@ -226,9 +229,11 @@ Lucene是Elasticsearch和Solr使用的一种全文搜索的索引引擎,它使 最后,我们可以看到包含单个键(叶页)的页面,该页面包含每个键的内联值,或者包含对可以找到值的页面的引用。 -在B树的一个页面中对子页面的引用的数量称为分支因子。例如,在[图3-6](img/fig3-6.png)中,分支因子是六。在实践中,分支因子取决于存储页面参考和范围边界所需的空间量,但通常是几百个。 +在B树的一个页面中对子页面的引用的数量称为分支因子。例如,在[图3-6](img/fig3-6.png)中,分支因子是6 。在实践中,分支因子取决于存储页面参考和范围边界所需的空间量,但通常是几百个。 -如果要更新B树中现有键的值,则搜索包含该键的叶页,更改该页中的值,并将该页写回到磁盘(对该页的任何引用保持有效) 。如果你想添加一个新的键,你需要找到其范围包含新键的页面,并将其添加到该页面。如果页面中没有足够的可用空间容纳新键,则将其分成两个半满页面,并更新父页面以解释键范围的新分区,如[图3-7](img/fig3-7.png)所示。 +如果要更新B树中现有键的值,则搜索包含该键的叶页,更改该页中的值,并将该页写回到磁盘(对该页的任何引用保持有效) 。如果你想添加一个新的键,你需要找到其范围包含新键的页面,并将其添加到该页面。如果页面中没有足够的可用空间容纳新键,则将其分成两个半满页面,并更新父页面以解释键范围的新分区,如[图3-7](img/fig3-7.png)所示[^ii]。 + +[^ii]: 向B树中插入一个新的键是相当符合直觉的,但删除一个键(同时保持树平衡)就会牵扯很多其他东西了。 ![](img/fig3-7.png) @@ -350,7 +355,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079 诸如VoltDB,MemSQL和Oracle TimesTen等产品是具有关系模型的内存数据库,供应商声称,通过消除与管理磁盘上的数据结构相关的所有开销,他们可以提供巨大的性能改进【41,42】。 RAM Cloud是一个开源的内存键值存储器,具有持久性(对存储器中的数据以及磁盘上的数据使用日志结构化方法)【43】。 Redis和Couchbase通过异步写入磁盘提供了较弱的持久性。 -**反直觉的是,内存数据库的性能优势并不是因为它们不需要从磁盘读取的事实。即使是基于磁盘的存储引擎也可能永远不需要从磁盘读取,因为操作系统缓存最近在内存中使用了磁盘块。相反,它们更快的原因在于省去了将内存数据结构编码为磁盘数据结构的开销**。【44】。 +反直觉的是,内存数据库的性能优势并不是因为它们不需要从磁盘读取的事实。即使是基于磁盘的存储引擎也可能永远不需要从磁盘读取,因为操作系统缓存最近在内存中使用了磁盘块。相反,它们更快的原因在于省去了将内存数据结构编码为磁盘数据结构的开销。【44】。 除了性能,内存数据库的另一个有趣的领域是提供难以用基于磁盘的索引实现的数据模型。例如,Redis为各种数据结构(如优先级队列和集合)提供了类似数据库的接口。因为它将所有数据保存在内存中,所以它的实现相对简单。 @@ -363,7 +368,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079 ## 事务处理还是分析? 在业务数据处理的早期,对数据库的写入通常对应于正在进行的商业交易:进行销售,向供应商下订单,支付员工工资等等。随着数据库扩展到那些没有不涉及钱易手,术语交易仍然卡住,指的是形成一个逻辑单元的一组读写。 -事务不一定具有ACID(原子性,一致性,隔离性和持久性)属性。事务处理只是意味着允许客户端进行低延迟读取和写入 - 而不是批量处理作业,而这些作业只能定期运行(例如每天一次)。我们在[第7章](ch7.md)中讨论ACID属性,在[第10章](ch10.md)中讨论批处理。 +事务不一定具有ACID(原子性,一致性,隔离性和持久性)属性。事务处理只是意味着允许客户端进行低延迟读取和写入 —— 而不是批量处理作业,而这些作业只能定期运行(例如每天一次)。我们在[第7章](ch7.md)中讨论ACID属性,在[第10章](ch10.md)中讨论批处理。 即使数据库开始被用于许多不同类型的博客文章,游戏中的动作,地址簿中的联系人等等,基本访问模式仍然类似于处理业务事务。应用程序通常使用索引通过某个键查找少量记录。根据用户的输入插入或更新记录。由于这些应用程序是交互式的,因此访问模式被称为**在线事务处理(OLTP, OnLine Transaction Processing)**。 @@ -385,11 +390,11 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079 | 处理的数据 | 数据的最新状态(当前时间点) | 随时间推移的历史事件 | | 数据集尺寸 | GB ~ TB | TB ~ PB | -起初,相同的数据库用于事务处理和分析查询。 SQL在这方面证明是非常灵活的:对于OLTP类型的查询以及OLAP类型的查询来说,效果很好。尽管如此,在二十世纪八十年代末和九十年代初期,公司有停止使用OLTP系统进行分析的趋势,而是在单独的数据库上运行分析。这个单独的数据库被称为数据仓库。 +起初,相同的数据库用于事务处理和分析查询。 SQL在这方面证明是非常灵活的:对于OLTP类型的查询以及OLAP类型的查询来说效果很好。尽管如此,在二十世纪八十年代末和九十年代初期,公司有停止使用OLTP系统进行分析的趋势,而是在单独的数据库上运行分析。这个单独的数据库被称为**数据仓库**。 ### 数据仓库 -一个企业可能有几十个不同的交易处理系统:系统为面向客户的网站提供动力,控制实体商店的销售点(checkout)系统,跟踪仓库中的库存,规划车辆路线,管理供应商,管理员工等。这些系统中的每一个都是复杂的,需要一个人员去维护,所以系统最终都是自动运行的。 +一个企业可能有几十个不同的交易处理系统:系统为面向客户的网站提供动力,控制实体商店的**销售点(checkout)**系统,跟踪仓库中的库存,规划车辆路线,管理供应商,管理员工等。这些系统中的每一个都是复杂的,需要一个人员去维护,所以系统最终都是自动运行的。 这些OLTP系统通常具有高度的可用性,并以低延迟处理事务,因为这些系统往往对业务运作至关重要。因此数据库管理员密切关注他们的OLTP数据库他们通常不愿意让业务分析人员在OLTP数据库上运行临时分析查询,因为这些查询通常很昂贵,扫描大部分数据集,这会损害同时执行的事务的性能。 @@ -417,7 +422,7 @@ Teradata,Vertica,SAP HANA和ParAccel等数据仓库供应商通常使用昂 正如[第2章](ch2.md)所探讨的,根据应用程序的需要,在事务处理领域中使用了大量不同的数据模型。另一方面,在分析中,数据模型的多样性则少得多。许多数据仓库都以相当公式化的方式使用,被称为星型模式(也称为维度建模【55】)。 -图3-9中的示例模式显示了可能在食品零售商处找到的数据仓库。在模式的中心是一个所谓的事实表(在这个例子中,它被称为`fact_sales`)。事实表的每一行代表在特定时间发生的事件(这里,每一行代表客户购买的产品)。如果我们分析的是网站流量而不是零售量,则每行可能代表一个用户的页面浏览量或点击量。 +图3-9中的示例模式显示了可能在食品零售商处找到的数据仓库。在模式的中心是一个所谓的事实表(在这个例子中,它被称为 `fact_sales`)。事实表的每一行代表在特定时间发生的事件(这里,每一行代表客户购买的产品)。如果我们分析的是网站流量而不是零售量,则每行可能代表一个用户的页面浏览量或点击量。 ![](img/fig3-9.png) @@ -427,15 +432,15 @@ Teradata,Vertica,SAP HANA和ParAccel等数据仓库供应商通常使用昂 事实表中的一些列是属性,例如产品销售的价格和从供应商那里购买的成本(允许计算利润余额)。事实表中的其他列是对其他表(称为维表)的外键引用。由于事实表中的每一行都表示一个事件,因此这些维度代表事件的发生地点,时间,方式和原因。 -例如,在[图3-9](img/fig3-9.md)中,其中一个维度是已售出的产品。 `dim_product`表中的每一行代表一种待售产品,包括**库存单位(SKU)**,说明,品牌名称,类别,脂肪含量,包装尺寸等。`fact_sales`表中的每一行都使用外部表明在特定交易中销售了哪些产品。 (为了简单起见,如果客户一次购买几种不同的产品,则它们在事实表中被表示为单独的行)。 +例如,在[图3-9](img/fig3-9.md)中,其中一个维度是已售出的产品。 `dim_product` 表中的每一行代表一种待售产品,包括**库存单位(SKU)**,说明,品牌名称,类别,脂肪含量,包装尺寸等。`fact_sales` 表中的每一行都使用外部表明在特定交易中销售了哪些产品。 (为了简单起见,如果客户一次购买几种不同的产品,则它们在事实表中被表示为单独的行)。 即使日期和时间通常使用维度表来表示,因为这允许对日期(诸如公共假期)的附加信息进行编码,从而允许查询区分假期和非假期的销售。 “星型模式”这个名字来源于这样一个事实,即当表关系可视化时,事实表在中间,由维表包围;与这些表的连接就像星星的光芒。 -这个模板的变体被称为雪花模式,其中尺寸被进一步分解为子尺寸。例如,品牌和产品类别可能有单独的表格,并且`dim_product`表格中的每一行都可以将品牌和类别作为外键引用,而不是将它们作为字符串存储在`dim_product`表格中。雪花模式比星形模式更规范化,但是星形模式通常是首选,因为分析师使用它更简单【55】。 +这个模板的变体被称为雪花模式,其中尺寸被进一步分解为子尺寸。例如,品牌和产品类别可能有单独的表格,并且 `dim_product` 表格中的每一行都可以将品牌和类别作为外键引用,而不是将它们作为字符串存储在 `dim_product` 表格中。雪花模式比星形模式更规范化,但是星形模式通常是首选,因为分析师使用它更简单【55】。 -在典型的数据仓库中,表格通常非常宽泛:事实表格通常有100列以上,有时甚至有数百列【51】。维度表也可以是非常宽的,因为它们包括可能与分析相关的所有元数据——例如,`dim_store`表可以包括在每个商店提供哪些服务的细节,它是否具有店内面包房,方形镜头,商店第一次开幕的日期,最后一次改造的时间,离最近的高速公路的距离等等。 +在典型的数据仓库中,表格通常非常宽泛:事实表格通常有100列以上,有时甚至有数百列【51】。维度表也可以是非常宽的,因为它们包括可能与分析相关的所有元数据——例如,`dim_store` 表可以包括在每个商店提供哪些服务的细节,它是否具有店内面包房,方形镜头,商店第一次开幕的日期,最后一次改造的时间,离最近的高速公路的距离等等。 @@ -443,7 +448,7 @@ Teradata,Vertica,SAP HANA和ParAccel等数据仓库供应商通常使用昂 如果事实表中有万亿行和数PB的数据,那么高效地存储和查询它们就成为一个具有挑战性的问题。维度表通常要小得多(数百万行),所以在本节中我们将主要关注事实的存储。 -尽管事实表通常超过100列,但典型的数据仓库查询一次只能访问4个或5个查询( “`SELECT *`” 查询很少用于分析)【51】。以[例3-1]()中的查询为例:它访问了大量的行(在2013日历年中每次都有人购买水果或糖果),但只需访问`fact_sales`表的三列:`date_key, product_sk, quantity`。查询忽略所有其他列。 +尽管事实表通常超过100列,但典型的数据仓库查询一次只能访问4个或5个查询( “ `SELECT *` ” 查询很少用于分析)【51】。以[例3-1]()中的查询为例:它访问了大量的行(在2013日历年中每次都有人购买水果或糖果),但只需访问`fact_sales`表的三列:`date_key, product_sk, quantity`。查询忽略所有其他列。 **例3-1 分析人们是否更倾向于购买新鲜水果或糖果,这取决于一周中的哪一天** diff --git a/ch4.md b/ch4.md index a7421b5..1b6be85 100644 --- a/ch4.md +++ b/ch4.md @@ -410,7 +410,7 @@ REST风格的API倾向于更简单的方法,通常涉及较少的代码生成 #### 远程过程调用(RPC)的问题 -Web服务仅仅是通过网络进行API请求的一系列技术的最新版本,其中许多技术受到了大量的炒作,但是存在严重的问题。 Enterprise JavaBeans(EJB)和Java的**远程方法调用(RMI)**仅限于Java。**分布式组件对象模型(DCOM)**仅限于Microsoft平台。**公共对象请求代理体系结构(CORBA)**过于复杂,不提供前向或后向兼容性[41]。 +Web服务仅仅是通过网络进行API请求的一系列技术的最新版本,其中许多技术受到了大量的炒作,但是存在严重的问题。 Enterprise JavaBeans(EJB)和Java的**远程方法调用(RMI)**仅限于Java。**分布式组件对象模型(DCOM)**仅限于Microsoft平台。**公共对象请求代理体系结构(CORBA)**过于复杂,不提供前向或后向兼容性【41】。 所有这些都是基于**远程过程调用(RPC)**的思想,该过程调用自20世纪70年代以来一直存在【42】。 RPC模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。尽管RPC起初看起来很方便,但这种方法根本上是有缺陷的【43,44】。网络请求与本地函数调用非常不同: @@ -420,7 +420,7 @@ Web服务仅仅是通过网络进行API请求的一系列技术的最新版本 * 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的:在不到一毫秒的时间内它可能会完成,但是当网络拥塞或者远程服务超载时,可能需要几秒钟的时间完全一样的东西。 * 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。没关系,如果参数是像数字或字符串这样的基本类型,但是对于较大的对象很快就会变成问题。 -客户端和服务可以用不同的编程语言实现,所以RPC框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子,因为不是所有的语言都具有相同的类型 - 例如回想一下JavaScript的数字大于$2^{53}$的问题(参阅“[JSON,XML和二进制变体](#JSON,XML和二进制变体)”)。用单一语言编写的单个进程中不存在此问题。 +客户端和服务可以用不同的编程语言实现,所以RPC框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子,因为不是所有的语言都具有相同的类型 —— 例如回想一下JavaScript的数字大于$2^{53}$的问题(参阅“[JSON,XML和二进制变体](#JSON,XML和二进制变体)”)。用单一语言编写的单个进程中不存在此问题。 所有这些因素意味着尝试使远程服务看起来像编程语言中的本地对象一样毫无意义,因为这是一个根本不同的事情。 REST的部分吸引力在于,它并不试图隐藏它是一个网络协议的事实(尽管这似乎并没有阻止人们在REST之上构建RPC库)。 @@ -440,7 +440,7 @@ Web服务仅仅是通过网络进行API请求的一系列技术的最新版本 RPC方案的前后向兼容性属性从它使用的编码方式中继承 -* Thrift,gRPC(协议缓冲区)和Avro RPC可以根据相应编码格式的兼容性规则进行演变。 +* Thrift,gRPC(Protobuf)和Avro RPC可以根据相应编码格式的兼容性规则进行演变。 * 在SOAP中,请求和响应是使用XML模式指定的。这些可以演变,但有一些微妙的陷阱【47】。 * RESTful API通常使用JSON(没有正式指定的模式)用于响应,以及用于请求的JSON或URI编码/表单编码的请求参数。添加可选的请求参数并向响应对象添加新的字段通常被认为是保持兼容性的改变。 @@ -478,7 +478,7 @@ RPC方案的前后向兼容性属性从它使用的编码方式中继承 #### 分布式的Actor框架 -actor模型是单个进程中并发的编程模型。逻辑被封装在角色中,而不是直接处理线程(以及竞争条件,锁定和死锁的相关问题)。每个角色通常代表一个客户或实体,它可能有一些本地状态(不与其他任何角色共享),它通过发送和接收异步消息与其他角色通信。消息传送不保证:在某些错误情况下,消息将丢失。由于每个角色一次只能处理一条消息,因此不需要担心线程,每个角色可以由框架独立调度。 +Actor模型是单个进程中并发的编程模型。逻辑被封装在角色中,而不是直接处理线程(以及竞争条件,锁定和死锁的相关问题)。每个角色通常代表一个客户或实体,它可能有一些本地状态(不与其他任何角色共享),它通过发送和接收异步消息与其他角色通信。消息传送不保证:在某些错误情况下,消息将丢失。由于每个角色一次只能处理一条消息,因此不需要担心线程,每个角色可以由框架独立调度。 在分布式的行为者框架中,这个编程模型被用来跨越多个节点来扩展应用程序。不管发送方和接收方是在同一个节点上还是在不同的节点上,都使用相同的消息传递机制。如果它们在不同的节点上,则该消息被透明地编码成字节序列,通过网络发送,并在另一侧解码。