diff --git a/ch12.md b/ch12.md index c6781450..2fbd2e57 100644 --- a/ch12.md +++ b/ch12.md @@ -93,15 +93,15 @@ * 如果您可以记录一个事件来记录用户在做出决定之前所看到的系统状态,并给该事件一个唯一的标识符,那么后面的任何事件都可以引用该事件标识符来记录因果关系【4】 。我们将在第513页的“[读也是事件](#读也是事件)”中回到这个想法。 * 冲突解决算法(请参阅“[自动冲突解决](ch5.md#自动冲突解决)”)有助于处理以意外顺序传递的事件。它们对于维护状态很有用,但如果行为有外部副作用(例如,也许,随着时间的推移,应用程序开发模式将出现,使得能够有效地捕获因果依赖关系,并且保持正确的衍生状态,而不会迫使所有事件经历全序广播的瓶颈。 -### 批量处理与流处理 +### 批处理与流处理 ​ 我会说数据集成的目标是,确保数据最终能在所有正确的地方表现出正确的形式。这样做需要消费输入,转换,连接,过滤,聚合,训练模型,评估,以及最终写出适当的输出。批处理和流处理是实现这一目标的工具。 -​ 批处理和流处理的输出是衍生的数据集,例如搜索索引,实例化视图,向用户显示的建议,聚合度量等(请参阅“[批处理工作流的输出](ch10.md#批处理工作流的输出)”和“[流处理的用法](ch11.md#流处理的用法)”)。 +​ 批处理和流处理的输出是衍生数据集,例如搜索索引,实例化视图,向用户显示的建议,聚合度量等(请参阅“[批处理工作流的输出](ch10.md#批处理工作流的输出)”和“[流处理的用法](ch11.md#流处理的用法)”)。 ​ 正如我们在[第10章](ch10.md)和[第11章](ch11.md)中看到的,批处理和流处理有许多共同的原则,主要的根本区别在于流处理器在无界数据集上运行,而批处理输入是已知的有限大小。处理引擎的实现方式也有很多细节上的差异,但是这些区别开始模糊。 -​ Spark在批处理引擎上执行流处理,将流分解为**微批量(microbatches)**,而Apache Flink则在流处理引擎上执行批处理【5】。原则上,一种类型的处理可以在另一种类型上仿真,但是性能特征会有所不同:例如,在跳跃或滑动窗口时,**小批量**可能表现不佳【6】。 +​ Spark在批处理引擎上执行流处理,将流分解为**微批次(microbatches)**,而Apache Flink则在流处理引擎上执行批处理【5】。原则上,一种类型的处理可以在另一种类型上仿真,但是性能特征会有所不同:例如,在跳跃或滑动窗口上微批次可能表现不佳【6】。 #### 维护衍生状态 @@ -111,7 +111,7 @@ ​ 原则上,衍生数据系统可以同步地维护,就像关系数据库在与被索引表写入操作相同的事务中同步更新辅助索引一样。然而,异步是基于事件日志的系统稳健的原因:它允许系统的一部分故障被抑制在本地,而如果任何一个参与者失败,分布式事务将中止,因此它们倾向于通过将故障传播到系统的其余部分来放大故障(请参阅“[分布式事务的限制](ch9.md#分布式事务的限制)”)。 -​ 我们在“[分区和二级索引](ch6.md#分区和二级索引)”中看到,二级索引经常跨越分区边界。具有二级索引的分区系统需要将写入发送到多个分区(如果索引是分词)或将读取发送到所有分区(如果索引是文档分区的话)。如果索引是异步维护的,这种交叉分区通信也是最可靠和可扩展的【8】(另请参阅“[多分区数据处理](ch11.md#多分区数据处理)”)。 +​ 我们在“[分区与次级索引](ch6.md#分区与次级索引)”中看到,二级索引经常跨越分区边界。具有二级索引的分区系统需要将写入发送到多个分区(如果索引是分词)或将读取发送到所有分区(如果索引是文档分区的话)。如果索引是异步维护的,这种交叉分区通信也是最可靠和可扩展的【8】(另请参阅“[多分区数据处理](ch11.md#多分区数据处理)”)。 #### 应用演化后重新处理数据 @@ -133,11 +133,11 @@ #### Lambda架构 -​ 如果批处理用于重新处理历史数据,并且流处理用于处理最近的更新,那么您如何将这两者结合起来?Lambda架构【12】是这方面的一个建议,引起了很多关注。 +​ 如果批处理用于重新处理历史数据,并且流处理用于处理最近的更新,那么如何将这两者结合起来?Lambda架构【12】是这方面的一个建议,引起了很多关注。 -​ lambda体系结构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件源(参阅“[事件源](ch11.md#事件源)”)。从这些事件中,推导出读取优化的视图。 Lambda体系结构建议并行运行两个不同的系统:批处理系统(如Hadoop MapReduce)和独立的流处理系统(如Storm)。 +​ Lambda体系结构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件源(参阅“[事件源](ch11.md#事件源)”)。从这些事件中,推导出读取优化的视图。 Lambda架构建议并行运行两个不同的系统:批处理系统(如Hadoop MapReduce)和独立的流处理系统(如Storm)。 -​ 在lambda方法中,流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成衍生视图的更正版本。这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的(请参阅“[故障容错]()”)。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。 +​ 在Lambda方法中,流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成衍生视图的更正版本。这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的(请参阅“[故障容错]()”)。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。 ​ Lambda架构是一个有影响力的想法,它将数据系统的设计变得更好,尤其是通过推广这样的原则:在不可变事件流上建立衍生视图,并在需要时重新处理事件。但是我也认为它有一些实际问题: @@ -147,13 +147,13 @@ #### 统一批处理和流处理 -​ 最近的工作使得lambda体系结构的优点在没有其缺点的情况下得以实现,允许批处理计算(重新处理历史数据)和流计算(处理事件到达时)在同一个系统中实现【15】。 +​ 最近的工作使得Lambda架构的优点在没有其缺点的情况下得以实现,允许批处理计算(重新处理历史数据)和流计算(处理事件到达时)在同一个系统中实现【15】。 在一个系统中统一批处理和流处理需要以下功能,这些功能越来越广泛: -* 通过处理最近事件流的相同处理引擎来重放历史事件的能力。例如,基于日志的消息代理可以重放消息(请参阅第451页的“[重放旧消息]()”),某些流处理器可以从HDFS等分布式文件系统读取输入。 -* 对于流处理器来说,只有一次语义 —— 即确保输出与未发生故障的输出相同,即使事实上发生故障(请参阅“[故障容错]()”(第476页))。与批处理一样,这需要丢弃任何失败任务的部分输出。 -* 按事件时间进行窗口化的工具,而不是按处理时间进行窗口化,因为处理历史事件时的处理时间毫无意义(请参阅第468页的“[关于时间的推理]()”)。例如,Apache Beam提供了用于表达这种计算的API,然后可以使用Apache Flink或Google Cloud Dataflow运行。 +* 通过处理最近事件流的相同处理引擎来重放历史事件的能力。例如,基于日志的消息代理可以重放消息(参阅“[重放旧消息](ch11.md#重放旧消息)”),某些流处理器可以从HDFS等分布式文件系统读取输入。 +* 对于流处理器来说,只有一次语义 —— 即确保输出与未发生故障的输出相同,即使事实上发生故障(参阅“[故障容错](ch11.md#故障容错)”)。与批处理一样,这需要丢弃任何失败任务的部分输出。 +* 按事件时间进行窗口化的工具,而不是按处理时间进行窗口化,因为处理历史事件时的处理时间毫无意义(参阅“[时间推理](ch11.md#时间推理)”)。例如,Apache Beam提供了用于表达这种计算的API,然后可以使用Apache Flink或Google Cloud Dataflow运行。 @@ -229,7 +229,7 @@ #### 分拆系统vs集成系统 -​ 如果分拆确实成为未来的方式,它也不会取代目前形式的数据库 —— 它们仍然会像以往一样被需要。为了维护流处理组件中的状态,数据库仍然是需要的,并且为批处理和流处理器的输出提供查询服务(请参阅第419页上的“[批处理工作流的输出](ch10.md#批处理工作流的输出)”和第464页上的“[流处理](ch11.md#流处理)”)。专用查询引擎对于特定的工作负载仍然非常重要:例如,MPP数据仓库中的查询引擎针对探索性分析查询进行了优化,并且能够很好地处理这种类型的工作负载(请参阅第417页的“[对比Hadoop与分布式数据库](ch10.md#对比Hadoop与分布式数据库)” 。 +​ 如果分拆确实成为未来的方式,它也不会取代目前形式的数据库 —— 它们仍然会像以往一样被需要。为了维护流处理组件中的状态,数据库仍然是需要的,并且为批处理和流处理器的输出提供查询服务(参阅“[批处理工作流的输出](ch10.md#批处理工作流的输出)”与“[流处理](ch11.md#流处理)”)。专用查询引擎对于特定的工作负载仍然非常重要:例如,MPP数据仓库中的查询引擎针对探索性分析查询进行了优化,并且能够很好地处理这种类型的工作负载(参阅“[对比Hadoop与分布式数据库](ch10.md#对比Hadoop与分布式数据库)” 。 ​ 运行几种不同基础设施的复杂性可能是一个问题:每种软件都有一个学习曲线,配置问题和操作怪癖,因此部署尽可能少的移动部件是很有必要的。比起使用应用代码拼接多个工具而成的系统,单一集成软件产品也可以在其设计应对的工作负载类型上实现更好,更可预测的性能【23】。正如在前言中所说的那样,为了不需要的规模而构建系统是白费精力,而且可能会将你锁死在一个不灵活的设计中。实际上,这是一种过早优化的形式。 @@ -434,29 +434,29 @@ ​ 虽然传统的事务方法并没有走远,但我也相信在使应用正确而灵活地处理错误方面上,事务并不是最后的遗言。在本节中,我将提出一些在数据流架构中考量正确性的方式。 -### 数据库端到端的争论 +### 为数据库使用端到端的参数 -​ 仅仅因为应用使用提供比较强的安全属性的数据系统(例如可序列化的事务),并不意味着应用可以保证没有数据丢失或损坏。例如,如果一个应用有一个错误导致它写入不正确的数据,或者从数据库中删除数据,那么可序列化的事务不会有什么帮助。 +​ 应用仅仅是使用具有相对较强安全属性的数据系统(例如可序列化的事务),并不意味着就可以保证没有数据丢失或损坏。例如,如果某个应用有个Bug,导致它写入不正确的数据,或者从数据库中删除数据,那么可序列化的事务也救不了你。 -​ 这个例子可能看起来很无聊,但值得认真对待:应用会出Bug,而人也会犯错误。我在“[状态,流与不可变性](ch11.md#状态,流与不可变性)”中使用了这个例子来支持不可变和只能追加的数据,因为如果删除错误代码的能力来破坏好的数据,更容易从这些错误中恢复数据。 +​ 这个例子可能看起来很无聊,但值得认真对待:应用会出Bug,而人也会犯错误。我在“[状态,流与不可变性](ch11.md#状态,流与不可变性)”中使用了这个例子来支持不可变和仅追加的数据,阉割掉错误代码摧毁良好数据的能力,能让从错误中恢复更为容易。 -​ 虽然不变性是有用的,但它本身并非万能的。让我们看看可能发生的数据损坏的一个更为简单的例子。 +​ 虽然不变性很有用,但它本身并非万灵药。让我们来看一个可能发生的,非常微妙的数据损坏案例。 #### 正好执行一次操作 -​ 在第476页的“[容错]()”中,我们遇到了一种精确调用一次(或有效一次)语义的想法。如果在处理消息时出现问题,您可以放弃(丢弃消息 —— 即导致数据丢失)或再次尝试。如果再试一次,第一次就有成功的风险,但是你没有发现成功,所以这个消息最终被处理了两次。 +​ 在“[容错](ch11.md#容错)”中,我们见到了**恰好一次**(或**等效一次**)语义的概念。如果在处理消息时出现问题,你可以选择放弃(丢弃消息 —— 导致数据丢失)或重试。如果重试,就会有这种风险:第一次实际上成功了,只不过你没有发现。结果这个消息就被处理了两次。 -​ 处理两次是数据损坏的一种形式:对于相同的服务向客户收费两次(计费太多)或增加计数器两次(夸大一些度量)是不可取的。在这种情况下,正好一次就意味着安排计算,使得最终效果与没有发生错误的情况相同,即使操作实际上由于某种错误而被重试。我们以前讨论过实现这一目标的几种方法。 +​ 处理两次是数据损坏的一种形式:为同样的服务向客户收费两次(收费太多)或增长计数器两次(夸大指标)都不是我们想要的。在这种情况下,恰好一次意味着安排计算,使得最终效果与没有发生错误的情况一样,即使操作实际上因为某种错误而重试。我们先前讨论过实现这一目标的几种方法。 -​ 最有效的方法之一是使幂等操作(参见第478页的“[幂等性]()”);即确保它具有相同的效果,无论是执行一次还是多次。但是,采取一种不自然是幂等的操作并使其具有幂等性需要付出一定的努力和关注:您可能需要维护一些额外的元数据(例如更新了值的操作ID集合),并在从一个节点到另一个节点(请参阅第295页上的“[领导和锁定]()”)。 +​ 最有效的方法之一是使操作**幂等(idempotent)**(参阅“[幂等性](ch11.md#幂等性)”);即确保它无论是执行一次还是执行多次都具有相同的效果。但是,将不是天生幂等的操作变为幂等的操作需要一些额外的努力与关注:你可能需要维护一些额外的元数据(例如更新了值的操作ID集合),并在从一个节点故障切换至另一个节点时做好防护(参阅的“[领导与锁定](ch9.md#领导与锁定)”)。 -#### 重复抑制 +#### 抑制重复 -​ 除了流处理之外,还需要抑制重复的相同模式出现在许多其他位置。例如,TCP使用数据包上的序列号将它们按正确的顺序排列在收件人处,并确定网络上是否有数据包丢失或重复。任何丢失的数据包都会被重新传输,并且在将数据交给应用程序之前,TCP堆栈会删除任何重复数据包。 +​ 除了流处理之外,其他许多地方也需要抑制重复的模式。例如,TCP使用数据包上的序列号,在接收方将它们正确排序。并确定网络上是否有数据包丢失或重复。任何丢失的数据包都会被重新传输,而在将数据交付应用前,TCP协议栈会移除任何重复数据包。 -​ 但是,此重复抑制仅适用于单个TCP连接的上下文中。假设TCP连接是一个客户端与数据库的连接,并且它正在执行[例12-1]()中的事务。在许多数据库中,事务与客户端连接有关(如果客户端发送了多个查询,数据库就知道它们属于同一个事务,因为它们是在同一个TCP连接上发送的)。如果客户端在发送`COMMIT`之后但在从数据库服务器回听之前遇到网络中断和连接超时,则不知道事务是否已被提交或中止([图8-1](img/fig8-1.png))。 +​ 但是,这种重复抑制仅适用于单条TCP连接的场景中。假设TCP连接是一个客户端与数据库的连接,并且它正在执行[例12-1]()中的事务。在许多数据库中,事务是绑定在客户端连接上的(如果客户端发送了多个查询,数据库就知道它们属于同一个事务,因为它们是在同一个TCP连接上发送的)。如果客户端在发送`COMMIT`之后但在从数据库服务器收到响应之前遇到网络中断与连接超时,客户端是不知道事务是否已经被提交的([图8-1](img/fig8-1.png))。 -**例12-1 将资金从一个账户转移到另一个账户的非赦免** +**例12-1 资金从一个账户到另一个账户的非幂等转移** ```sql BEGIN TRANSACTION; @@ -465,18 +465,20 @@ 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,但是信号在他们能够从服务器接收响应之前变得太弱。 +​ 两阶段提交(参阅“[原子提交与两阶段提交(2PC)](ch9.md#原子提交与两阶段提交(2PC))”)协议会破坏TCP连接与事务之间的1:1映射,因为它们必须在故障后允许事务协调器重连到数据库,告诉数据库将存疑事务提交还是中止。这足以确保事务只被恰好执行一次吗?不幸的是,并不能。 -​ 在这种情况下,用户可能会显示错误消息,并且可能会手动重试。 Web浏览器警告说,“你确定要再次提交这个表单吗?” —— 用户说是,因为他们希望操作发生。 (Post/Redirect/Get模式【54】可以避免在正常操作中出现此警告消息,但如果POST请求超时,它将无济于事。)从Web服务器的角度来看,重试是一个单独的请求,并且从数据库的角度来看,这是一个单独的事务。通常的重复数据删除机制无济于事。 +​ 即使我们可以抑制数据库客户端与服务器之间的重复事务,我们仍然需要担心终端用户设备与应用服务器之间的网络。例如,如果终端用户的客户端是Web浏览器,则它可能会使用HTTP POST请求向服务器提交指令。也许用户正处于一个信号微弱的蜂窝数据网络连接中,它们成功地发送了POST,但却在能够从服务器接收响应之前没了信号。 + +​ 在这种情况下,可能会向用户显示错误消息,而他们可能会手动重试。 Web浏览器警告说,“你确定要再次提交这个表单吗?” —— 用户选“是”,因为他们希望操作发生。 (Post/Redirect/Get模式【54】可以避免在正常操作中出现此警告消息,但POST请求超时就没办法了。)从Web服务器的角度来看,重试是一个独立的请求,而从数据库的角度来看,这是一个独立的事务。通常的除重机制无济于事。 #### 操作标识符 -​ 要通过几次网络通信使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的—— 您需要考虑请求的端到端流。 - 例如,您可以为操作(例如UUID)生成唯一的标识符,并将其作为隐藏的表单字段包含在客户机应用程序中,或计算所有相关表单字段的散列以衍生操作ID 【3】。如果Web浏览器提交两次POST请求,这两个请求将具有相同的操作ID。然后,您可以将该操作ID传递到数据库,并检查您是否只使用给定的ID执行一个操作,如[例12-2]()中所示。 +​ 要在通过几跳的网络通信上使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的 —— 你需要考虑**端到端(end-to-end)**的请求流。 + 例如,你可以为操作生成一个唯一的标识符(例如UUID),并将其作为隐藏表单字段包含在客户端应用中,或通过计算所有表单相关字段的散列来生成操作ID 【3】。如果Web浏览器提交了两次POST请求,这两个请求将具有相同的操作ID。然后,你可以将该操作ID一路传递到数据库,并检查你是否曾经使用给定的ID执行过一个操作,如[例12-2]()中所示。 -**例12-2 使用唯一的ID来抑制重复的请求** +**例12-2 使用唯一ID来抑制重复请求** ```sql ALTER TABLE requests ADD UNIQUE (request_id); @@ -489,166 +491,174 @@ BEGIN TRANSACTION; COMMIT; ``` -​ [例12-2]()依赖于`request_id`列上的唯一性约束。如果一个事务尝试插入一个已经存在的ID,那么`INSERT`失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束(而在第248页上的“[写入偏差和幻读]()”中讨论过,应用程序级别的check-then-insert可能会在不可序列化的隔离下失败)。 +​ [例12-2]()依赖于`request_id`列上的唯一约束。如果一个事务尝试插入一个已经存在的ID,那么`INSERT`失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束(而在“[写入偏差与幻读](ch7.md#写入偏差与幻读)”中讨论过,应用级别的**检查-然后-插入**可能会在不可序列化的隔离下失败)。 -​ 除了抑制重复的请求之外,示[例12-2]()中的请求表充当事件日志的一种,暗示着事件源的方向(请参阅第457页的“[事件源]()”)。账户余额的更新事实上不必与插入事件相同的事务发生,因为它们是多余的,并且可以从下游消费者中的请求事件衍生出来 —— 只要该事件只处理一次,这可以再次使用请求ID来执行。 +​ 除了抑制重复的请求之外,[例12-2]()中的请求表表现得就像一种事件日志,提示向着事件溯源的方向(参阅“[事件溯源](ch11.md#事件溯源)”)。更新账户余额事实上不必与插入事件发生在同一个事务中,因为它们是冗余的,而能由下游消费者从请求事件中衍生出来 —— 只要该事件被恰好处理一次,这又一次可以使用请求ID来强制执行。 -**端到端的论点** +**端到端的原则** -​ 抑制重复交易的这种情况只是一个更普遍的原则的一个例子,这个原则被称为端对端的论点,它在1984年由Saltzer,Reed和Clark阐述【55】: +​ 抑制重复事务的这种情况只是一个更普遍的原则的一个例子,这个原则被称为**端到端的原则(end-to-end argument)**,它在1984年由Saltzer,Reed和Clark阐述【55】: -​ 只有在通信系统端点的应用程序的知识和帮助下,所讨论的功能才能够完全正确地实现。因此,将这种被质疑的功能作为通信系统本身的功能是不可能的。 (有时,通信系统提供的功能的不完整版本可能有助于提高性能。) +> ​ 只有在通信系统两端应用的知识与帮助下,所讨论的功能才能完全地正确地实现。因而将这种被质疑的功能作为通信系统本身的功能是不可能的。 (有时,通信系统可以提供这种功能的不完备版本,可能有助于提高性能) +> -​ 在我们的例子中,有问题的函数是重复抑制。我们看到TCP在TCP连接级别抑制重复的数据包,一些流处理器在消息处理级别提供了所谓的唯一的语义,但是这不足以防止用户提交重复请求一次。 TCP,数据库事务和流处理器本身并不能完全排除这些重复。解决这个问题需要一个端到端的解决方案:从最终用户客户端一直传递到数据库的事务标识符。 +​ 在我们的例子中**所讨论的功能**是重复抑制。我们看到TCP在TCP连接层次抑制了重复的数据包,一些流处理器在消息处理层次提供了所谓的恰好一次语义,但这些都无法阻止当一个请求超时时,用户亲自提交重复的请求。TCP,数据库事务,以及流处理器本身并不能完全排除这些重复。解决这个问题需要一个端到端的解决方案:从终端用户的客户端一路传递到数据库的事务标识符。 -​ 端到端参数也适用于检查数据的完整性:以太网,TCP和TLS中内置的校验和可以检测网络中数据包的损坏情况,但是它们无法检测到发送和接收软件中的错误网络连接的末端,或数据存储在磁盘上的损坏。如果您想要捕获所有可能的数据损坏源,则还需要端到端的校验和。 +​ 端到端参数也适用于检查数据的完整性:以太网,TCP和TLS中内置的校验和可以检测网络中数据包的损坏情况,但是它们无法检测到由连接两端发送/接收软件中Bug导致的损坏。或数据存储所在磁盘上的损坏。如果你想捕获数据所有可能的损坏来源,你也需要端到端的校验和。 -​ 类似的说法适用于加密【55】:家庭WiFi网络上的密码可以防止人们窃听您的WiFi流量,但不会对互联网上其他地方的攻击者进行窥探;您的客户端和服务器之间的TLS / SSL可以得到保护对网络攻击者,而不是对服务器的妥协。只有端到端的加密和认证可以防止所有这些事情。 +​ 类似的原则也适用于加密【55】:家庭WiFi网络上的密码可以防止人们窃听你的WiFi流量,但无法阻止互联网上其他地方攻击者的窥探;客户端与服务器之间的TLS/SSL可以阻挡网络攻击者,但无法阻止恶意服务器。只有端到端的加密和认证可以防止所有这些事情。 -​ 尽管低级功能(TCP复制抑制,以太网校验和,WiFi加密)无法单独提供所需的端到端功能,但它们仍然很有用,因为它们可以降低较高级别出现问题的可能性。例如,如果我们没有TCP将数据包放回正确的顺序,那么HTTP请求通常会被破坏。我们只需要记住,低级别的可靠性功能本身并不足以确保端到端的正确性。 +​ 尽管低层级的功能(TCP复制抑制,以太网校验和,WiFi加密)无法单独提供所需的端到端功能,但它们仍然很有用,因为它们能降低较高层级出现问题的可能性。例如,如果我们没有TCP来将数据包排成正确的顺序,那么HTTP请求通常就会被搅烂。我们只需要记住,低级别的可靠性功能本身并不足以确保端到端的正确性。 #### 在数据系统中应用端到端思考 -​ 这使我回到我的原始论文:仅仅因为应用程序使用提供比较强的安全属性的数据系统(如可序列化事务),并不意味着应用程序保证不会丢失数据或损坏。应用程序本身也需要采取端到端的措施,例如重复压制。 +​ 这将我带回最初的论点·:仅仅因为应用使用了提供相对较强安全属性的数据系统,例如可序列化的事务,并不意味着应用的数据就不会丢失或损坏了。应用本身也需要采取端到端的措施,例如除重。 -​ 这是一个耻辱,因为容错机制很难得到正确的。低级可靠性机制(如TCP中的可靠性机制)运行良好,因此其余较高级别的故障发生得相当少。将抽象中的高级容错机制封装起来非常好,以便应用程序代码不必担心它 —— 但是我担心我们还没有找到合适的抽象。 +​ 这实在是一个遗憾,因为容错机制很难弄好。低层级的可靠机制(比如TCP中的那些)运行的相当好,因而剩下的高层级错误基本很少出现。如果能将这些剩下的高层级容错机制打包成抽象,而应用不需要再去操心,那该多好呀 —— 但恐怕我们还没有找到这一正确的抽象。 -​ 长期以来,事务被认为是一个很好的抽象,我相信它们是有用的。正如[第7章](ch7.md)介绍中所讨论的那样,它们会涉及各种可能的问题(并发写入,约束违规,崩溃,网络中断,磁盘故障),并将其折叠为两种可能的结果:提交或中止。这是编程模型的一个巨大的简化,但我担心这是不够的。 +​ 长期以来,事务被认为是一个很好的抽象,我相信它们确实是很有用的。正如[第7章](ch7.md)导言中所讨论的,它们将各种可能的问题(并发写入,违背约束,崩溃,网络中断,磁盘故障)合并为两种可能结果:提交或中止。这是对编程模型而言是一种巨大的简化,但恐怕这还不够。 -​ 事务处理非常昂贵,尤其是涉及异构存储技术时(请参阅第364页的“[实践中的分布式事务]()”)。当我们拒绝使用分布式事务是因为它们太昂贵时,我们最终不得不在应用程序代码中重新实现容错机制。正如本书中大量的例子所显示的那样,关于并发性和部分失败的推理是困难且违反直觉的,所以我怀疑大多数应用程序级别的机制不能正常工作。结果是丢失或损坏的数据。 +​ 事务是代价高昂的,当涉及异构存储技术时尤为甚(参阅的“[实践中的分布式事务](ch9.md#实践中的分布式事务)”)。我们拒绝使用分布式事务是因为它开销太大,结果我们最后不得不在应用代码中重新实现容错机制。正如本书中大量的例子所示,对并发性与部分失败的推理是困难且违反直觉的,所以我怀疑大多数应用级别的机制都不能正确工作,最终结果是数据丢失或损坏。 -​ 出于这些原因,我认为值得探索的容错抽象方法能够容易地提供特定于应用程序的端到端正确性属性,而且还可以在大型分布式环境中保持良好的性能和良好的操作特性。 +​ 出于这些原因,我认为探索对容错的抽象是很有价值的。它使提供应用特定的端到端的正确性属性变得更简单,而且还能在大规模分布式环境中提供良好的性能与运维特性。 -### 强制实施约束 +### 强制约束 -​ 让我们考虑分拆数据库的想法背景下的正确性(“[剥离数据库]()”,第499页)。我们看到,使用从客户端一直传递到记录写入的数据库的请求标识可以实现端到端的重复压缩。其他类型的限制呢? +​ 让我们思考一下在[分拆数据库](#分拆数据库)上下文中的**正确性(correctness)**。我们看到端到端的除重可以通过从客户端一路透传到数据库的请求ID实现。那么其他类型的约束呢? -​ 特别是让我们关注唯一性约束 —— 例如我们在[例12-2]()中所依赖的约束。在第330页的“[约束和唯一性保证]()”中,我们看到了几个其他需要强制实施唯一性的应用程序功能示例:用户名或电子邮件地址必须唯一标识用户,文件存储服务不能包含多个文件同名,两个人不能在航班或剧院预订同一个座位。 +​ 我们先来特别关注一下**唯一性约束** —— 例如我们在[例12-2]()中所依赖的约束。在“[约束和唯一性保证](ch9.md#约束和唯一性保证)”中,我们看到了几个其他需要强制实施唯一性的应用功能例子:用户名或电子邮件地址必须唯一标识用户,文件存储服务不能包含多个重名文件,两个人不能在航班或剧院预订同一个座位。 -​ 其他类型的约束非常相似:例如,确保帐户余额永远不会变为负数,您不会出售比仓库中的库存更多的物料,或者会议室没有重复的预订。执行唯一性的技术通常也可以用于这些约束。 +​ 其他类型的约束也非常类似:例如,确保帐户余额永远不会变为负数,你就不会超卖库存;或者会议室没有重复的预订。执行唯一性约束的技术通常也可以用于这些约束。 #### 唯一性约束需要达成共识 -​ 在[第9章](ch9.md)中我们看到,在分布式环境中,强制执行唯一性约束需要达成共识:如果存在多个具有相同值的并发请求,则系统需要决定哪个冲突操作被接受,并拒绝其他违规操作的约束。 +​ 在[第9章](ch9.md)中我们看到,在分布式环境中,强制执行唯一性约束需要共识:如果存在多个具有相同值的并发请求,则系统需要决定冲突操作中的哪一个被接受,并拒绝其他违背约束的操作。 -​ 达成这一共识的最常见方式是将单个节点作为领导者,并将其负责制定所有决策。只要您不介意通过单个节点发送所有请求(即使客户端位于世界的另一端),并且只要该节点没有失败,就可以正常工作。如果您需要容忍领导者失败,那么您又回到了共识问题(请参阅第367页上的“[单领导表示和共识]()”)。 +​ 达成这一共识的最常见方式是使单个节点作为领导,并使其负责所有决策。只要你不介意所有请求都挤过单个节点(即使客户端位于世界的另一端),只要该节点没有失效,系统就能正常工作。如果你需要容忍领导者失效,那么就又回到了共识问题(参阅“[单领导者复制与共识](ch9.md#单领导者复制与共识)”)。 -​ 唯一性检查可以根据需要唯一的值进行划分。例如,如果需要通过请求标识确保唯一性(如[例12-2]()所示),则可以确保具有相同请求标识的所有请求都路由到同一分区(请参阅[第6章](ch6.md))。如果您需要用户名是唯一的,您可以通过用户名的哈希分区。 +​ 唯一性检查可以通过对唯一性字段分区做横向扩展。例如,如果需要通过请求ID确保唯一性(如[例12-2]()所示),你可以确保所有具有相同请求ID的请求都被路由到同一分区(参阅[第6章](ch6.md))。如果你需要让用户名是唯一的,则可以按用户名的散列值做分区。 -​ 但是,排除了异步多主复制,因为它可能会导致不同的主设备同时接受冲突的写操作,因此这些值不再是唯一的(请参阅第295页的“[实现可线性化系统]()”)。如果你想立即拒绝任何违反约束的写入,同步协调是不可避免的【56】。 +​ 但异步多主复制排除在外,因为可能会发生不同主库同时接受冲突写操作的情况,因而这些值不再是唯一的(参阅“[实现可线性化系统](ch9.md#实现可线性化系统)”)。如果你想立刻拒绝任何违背约束的写入,同步协调是无法避免的【56】。 -#### 基于日志的消息传递的唯一性 +#### 基于日志消息传递中的唯一性 -​ 该日志确保所有消费者以相同的顺序查看消息 —— 这种保证在形式上被称为全部命令广播并且等同于共识(参见第346页上的“[全序广播]()”)。在使用基于日志的消息传递的分拆数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。 +​ 日志确保所有消费者以相同的顺序看见消息 —— 这种保证在形式上被称为**全序广播(total order boardcast)**并且等价于共识(参见“[全序广播](ch9.md#全序广播)”)。在使用基于日志的消息传递的分拆数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。 -​ 流处理器在单个线程上依次占用日志分区中的所有消息(请参见第448页的“[与传统消息传递相比的日志]()”)。因此,如果日志根据需要唯一的值进行分区,则流处理器可以明确并确定性地确定几个冲突操作中的哪一个先到达。例如,在多个用户尝试声明相同用户名的情况下【57】: +​ 流处理器在单个线程上依次消费单个日志分区中的所有消息(参阅“[与传统消息传递相比的日志](ch11.md#与传统消息传递相比的日志)”)。因此,如果日志是按有待确保唯一的值做的分区,则流处理器可以无歧义地,确定性地决定几个冲突操作中的哪一个先到达。例如,在多个用户尝试宣告相同用户名的情况下【57】: -1. 对用户名的每个请求都被编码为一条消息,并附加到由用户名散列确定的分区。 -2. 流处理器使用本地数据库连续读取日志中的请求,以跟踪使用哪些用户名。对于每个可用的用户名请求,它都会记录该名称并将成功消息发送到输出流。对于每个已经被使用的用户名请求,它都会向输出流发送拒绝消息。 -3. 请求用户名的客户端观察输出流并等待与其请求相对应的成功或拒绝消息。 +1. 每个对用户名的请求都被编码为一条消息,并追加到按用户名散列值确定的分区。 +2. 流处理器依序读取日志中的请求,并使用本地数据库来追踪哪些用户名已经被占用了。对于所有申请可用用户名的请求,它都会记录该用户名,并向输出流发送一条成功消息。对于所有申请已占用用户名的请求,它都会向输出流发送一条拒绝消息。 +3. 请求用户名的客户端监视输出流,等待与其请求相对应的成功或拒绝消息。 -该算法基本上与第363页上的“[使用全序广播实现线性化存储]()”中的算法相同。它可以通过增加分区数容易地扩展为较大的请求吞吐量,因为可以独立处理每个分区。 +该算法基本上与“[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)”中的算法相同。它可以简单地通过增加分区数扩展至较大的请求吞吐量,因为每个分区可以被独立处理。 -​ 该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如第174页上的“什么是冲突?”和第246页上的“[写入偏差和幻读]()”中所述,冲突的定义可能取决于应用程序,但流处理器可以使用任意逻辑来验证请求。这个想法与Bayou在20世纪90年代开创的方法类似【58】。 +​ 该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是,任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如“[什么是冲突?](ch5.md#什么是冲突?)”与“[写入偏差与幻读](ch7.md#写入偏差与幻读)”中所述,冲突的定义可能取决于应用,但流处理器可以使用任意逻辑来验证请求。这个想法与Bayou在90年代开创的方法类似【58】。 #### 多分区请求处理 -​ 当涉及多个分区时,确保操作以原子方式执行,同时满足约束条件变得更有趣。在示[例12-2]()中,可能有三个分区:一个包含请求ID,一个包含收款人账户,另一个包含付款人账户。没有理由把这三件事放在同一个分区,因为它们都是相互独立的。 +​ 当涉及多个分区时,确保操作以原子方式执行且同时满足约束就变得很有趣了。在[例12-2]()中,可能有三个分区:一个包含请求ID,一个包含收款人账户,另一个包含付款人账户。没有理由把这三种东西放入同一个分区,因为它们都是相互独立的。 + +​ 在数据库的传统方法中,执行此事务需要跨全部三个分区进行原子提交,这实质上是将该事务嵌入一个全序,就这些分区上的所有其他事务而言。而这样就要求跨分区协调,不同的分区无法再独立地进行处理,因此吞吐量可能会受到影响。 + +但事实证明,使用分区日志可以达到等价的正确性而无需原子提交: + +1. 从账户A向账户B转账的请求由客户端提供一个唯一的请求ID,并按请求ID追加写入相应日志分区。 +2. 流处理器读取请求日志。对于每个请求消息,它向输出流发出两条消息:付款人账户A的借记指令(按A分区),收款人B的贷记指令(按B分区)。被发出的消息中会带有原始的请求ID。 +3. 后续处理器消费借记/贷记指令流,按照请求ID除重,并将变更应用至账户余额。 -​ 在数据库的传统方法中,执行此事务需要跨所有三个分区进行原子提交,这实质上会将它强制为与任何这些分区上的所有其他事务的总顺序。由于现在存在跨分区协调,不能再独立处理不同的分区,因此吞吐量可能会受到影响。 -但是,事实证明,使用分区日志可以实现同等的正确性,并且不需要原子提交: +步骤1和步骤2是必要的,因为如果客户直接发送贷记与借记指令,则需要在这两个分区之间进行原子提交,以确保两者要么都发生或都不发生。为了避免对分布式事务的需要,我们首先将请求持久化记录为单条消息,然后从这第一条消息中衍生出贷记指令与借记指令。几乎在所有数据系统中,单对象写入都是原子性的(参阅“[单对象写入](ch7.md#单对象写入)),因此请求要么出现在日志中,要么就不出现,无需多分区原子提交。 -1. 从账户A向账户B转账的请求由客户端提供唯一的请求ID,并根据请求ID附加到日志分区。 -2. 流处理器读取请求的日志。对于每个请求消息,它发出两条消息以输出流:付款人账户A(由A分配)的借方指令和收款人账户B(由B分区)的信贷指令。原始的请求ID包含在那些发出的消息中。 -3. 其他处理器使用信用卡和借记指令流,通过请求ID进行扣除,并将变更应用于账户余额。 +​ 如果流处理器在步骤2中崩溃,则它会从上一个存档点恢复处理。这样做时,它不会跳过任何请求消息,但可能会多次处理请求并产生重复的贷记与借记指令。但由于它是确定性的,因此它只是再次生成相同的指令,而步骤3中的处理器可以使用端到端请求ID轻松地对其除重。 -步骤1和步骤2是必要的,因为如果客户直接发送信用和借记指令,则需要在这两个分区之间进行原子提交以确保两者都不发生。为了避免分布式事务的需要,我们首先将请求永久记录为单条消息,然后从第一条消息中获取信用和借记指令。单对象写入在几乎所有数据系统中都是原子性的(请参阅“[单对象写入]()”第213页),因此请求既可以出现在日志中,也可以不出现,而不需要多分区原子com-麻省理工学院。 +​ 如果你想确保付款人的帐户不会因此次转账而透支,则可以使用一个额外的流处理器来维护账户余额并校验事务(按付款人账户分区),只有有效的事务会被记录在步骤1中的请求日志中。 + +​ 通过将多分区事务分解为两个不同分区方式的阶段,并使用端到端的请求ID,我们实现了同样的正确性属性(每个请求对付款人与收款人都恰好生效一次),即使在出现故障,且没有使用原子提交协议的情况下依然如此。使用多个不同分区方式的阶段与我们在“[多分区数据处理](#多分区数据处理)”中讨论的想法类似(参阅“[并发控制](ch11.md#并发控制)”)。 -​ 如果步骤2中的流处理器崩溃,则从上一个检查点恢复处理。这样做时,它不会跳过任何请求消息,但可能会多次处理请求并产生重复的信用和借记指令。但是,由于它是确定性的,因此它只会再次生成相同的指令,并且步骤3中的处理器可以使用端到端请求ID轻松地对它们进行重复数据删除。 - 如果您想确保付款人帐户不会因此次转账而透支,您可以额外使用流处理器(分区)使用多个不同分区的阶段的想法与我们所讨论的类似“多分区数据处理”一节第514页(另请参阅“[并发控制]()”一节第462页)。 ### 及时性与完整性 -​ 事务的一个方便属性是它们通常是可线性化的(请参阅“[可用性]()”),也就是说,一个写入者等待事务提交,之后其所有读者立即可以看到它的写入。 +​ 事务的一个便利属性是,它们通常是线性一致的(参阅“[线性一致性](ch9.md#线性一致性)”),也就是说,写入者会等到事务提交,而之后其写入立刻对所有读取者可见。 -​ 在跨流处理器的多个阶段拆分操作时情况并非如此:日志的使用者在设计上是异步的,因此发送者不会等到其消息已经被消费者处理。但是,客户端可能会等待消息出现在输出流上。这是我们在检查是否满足唯一性约束时在“基于日志的消息传递中的唯一性”一节中所做的操作。 +​ 当我们把一个操作拆分为跨越多个阶段的流处理器时,却并非如此:日志的消费者在设计上就是异步的,因此发送者不会等其消息被消费者处理完。但是,客户端等待输出流中的特定消息是可能的。这正是我们在“[基于日志消息传递中的唯一性](#基于日志消息传递中的唯一性)”一节中检查唯一性约束时所做的事情。 -​ 在这个例子中,唯一性检查的正确性不取决于消息的发送者是否等待结果。等待仅具有同步通知发送者唯一性检查是否成功的目的,但是该通知可以与处理消息的效果分离。 +​ 在这个例子中,唯一性检查的正确性不取决于消息发送者是否等待结果。等待的目的仅仅是同步通知发送者唯一性检查是否成功。但该通知可以与消息处理的结果相解耦。 -更一般地说,我认为术语一致性这个术语将两个不同的需求分开考虑: +​ 更一般地来讲,我认为术语**一致性(consistency)**这个术语混淆了两个值得分别考虑的需求: -***及时性*** +***及时性(Timeliness)*** -​ 及时性意味着确保用户观察系统处于最新状态。我们之前看到,如果用户从数据的陈旧副本中读取数据,他们可能会以不一致的状态观察数据(请参阅第161页上的“[复制延迟问题]()”)。但是,这种不一致是暂时的,最终只能通过等待和再次尝试来解决。 +​ 及时性意味着确保用户观察到系统的最新状态。我们之前看到,如果用户从陈旧的数据副本中读取数据,它们可能会观察到系统处于不一致的状态(参阅“[复制延迟问题](ch5.md#复制延迟问题)”)。但这种不一致是暂时的,而最终会通过等待与重试简单地得到解决。 -​ CAP定理(参见第359页的“[线性一致性的代价]()”)使用线性一致的意义上的一致性,这是实现及时性的强有力的方法。像写后读一致性这样的时效性较弱的属性(请参阅第162页的“[读己之写]()”)也很有用。 +​ CAP定理(参阅“[线性一致性的代价](ch9.md#线性一致性的代价)”)使用**线性一致性(linearizability)**意义上的一致性,这是实现及时性的强有力方法。像**写后读**这样及时性更弱的一致性也很有用(参阅“[读己之写](ch5.md#读己之写)”)也很有用。 -***完整性*** +***完整性(Integrity)*** -​ 完整性意味着没有腐败;即没有数据丢失,并且没有矛盾或错误的数据。尤其是,如果将某些衍生数据集作为某些基础数据的视图进行维护(请参阅“[从事件日志导出当前状态]()”(第458页)),衍生必须正确。例如,数据库索引必须正确地反映数据库的内容 —— 缺少某些记录的索引不是很有用。如果完整性受到侵犯,这种不一致是永久性的:在大多数情况下,等待并再次尝试不会修复数据库损坏。相反,需要明确的检查和修理。在ACID事务的上下文中(参见第223页上的“[ACID的含义]()”),一致性通常被理解为某种特定于应用程序的完整性概念。原子性和耐久性是保持完整性的重要工具。 +​ 完整性意味着没有损坏;即没有数据丢失,并且没有矛盾或错误的数据。尤其是如果某些衍生数据集是作为底层数据之上的视图而维护的(参阅“[从事件日志导出当前状态](ch11.md#从事件日志导出当前状态)”),这种衍生必须是正确的。例如,数据库索引必须正确地反映数据库的内容 —— 缺失某些记录的索引并不是很有用。 -​ 口号形式:违反及时性是“最终一致性”,而违反诚信则是“永久不一致”。 +​ 如果完整性被违背,这种不一致是永久的:在大多数情况下,等待与重试并不能修复数据库损坏。相反的是,需要显式地检查与修复。在ACID事务的上下文中(参阅“[ACID的涵义](ch7.md#ACID的涵义)”),一致性通常被理解为某种特定于应用的完整性概念。原子性和持久性是保持完整性的重要工具。 -​ 我要断言,在大多数应用中,完整性比时间要重要得多。违反时效可能令人讨厌和混淆,但是对正直的侵犯可能是灾难性的。 -​ 例如,在您的信用卡对账单上,如果您在过去24小时内完成的交易尚未出现,这并不奇怪 —— 这些系统有一定的滞后是正常的。我们知道银行协调和异步结算交易,并且这里的及时性并不重要【3】。但是,如果报表余额不等于交易总和加上先前的报表余额(数额错误),或者交易是向您收取但未支付给商家的话,那将是非常糟糕的(消失的金钱)。这样的问题会违反系统的完整性。 -#### 数据流系统的正确性 +​ 口号形式:违反及时性,“最终一致性”;违反完整性,“永无一致性”。 + +​ 我断言在大多数应用中,完整性比及时性重要得多。违反及时性可能令人困惑与讨厌,但违反完整性的结果可能是灾难性的。 -​ ACID事务通常既提供时间性(例如线性化)又提供完整性(例如原子提交)保证。因此,如果从ACID交易的角度来看应用程序的正确性,那么时间性与完整性的区别就相当不重要了。 +​ 例如在你的信用卡对账单上,如果某一笔过去24小时内完成的交易尚未出现并不令人奇怪 —— 这些系统有一定的滞后是正常的。我们知道银行是异步核算与敲定交易的,而这里的及时性也并不是非常重要【3】。但果当期对账单余额与上期对账单余额加交易总额对不上(求和错误),或者出现一比向你收费但未向商家付款的交易(消失的钱),那实在是太糟糕了。这样的问题就违背了系统的完整性。 + +#### 数据流系统的正确性 -​ 另一方面,我们在本章中讨论的基于事件的数据流系统的一个有趣特性是它们将时间性和完整性分开。在异步处理事件流时,不能保证及时性,除非在返回之前明确地构建等待消息到达的消费者。但是,完整性实际上是流式传输系统的核心。 +​ ACID事务通常既提供及时性(例如线性一致性)也提供完整性保证(例如原子提交)。因此如果你从ACID事务的角度来看待应用的正确性,那么及时性与完整性的区别是无关紧要的。 -一次或一次有效的语义(请参阅“[故障容错]()”一节第437页)是一种保持完整性的机制。如果事件丢失,或者事件发生两次,数据系统的完整性可能被侵犯。因此,容错消息传递和重复抑制(例如,幂等操作)对于在面对故障时保持数据系统的完整性是重要的。 +​ 另一方面,对于在本章中讨论的基于事件的数据流系统而言,它们的一个有趣特性就是将及时性与完整性分开。在异步处理事件流时不能保证及时性,除非你显式构建一个在返回之前明确等待特定消息到达的消费者。但完整性实际上才是流处理系统的核心。 -正如我们在上一节看到的那样,可靠的流处理系统可以在不需要分布式事务和原子提交协议的情况下保持整体性,这意味着它们可以实现更好的可比较的正确性 +​ **恰好一次**或**等效一次**语义(参阅“[容错](ch11.md#容错)”)是一种保持完整性的机制。如果事件丢失或者生效两次,就有可能违背数据系统的完整性。因此在面对故障时,容错消息传递与重复抑制(例如,幂等操作)对于维护数据系统的完整性是很重要的。 -性能和运行稳健性。我们通过以下机制的结合实现了这一完整性: +​ 正如我们在上一节看到的那样,可靠的流处理系统可以在无需分布式事务与原子提交协议的情况下保持完整性,这意味着它们能潜在地实现好得多的性能与运维稳健性,在达到类似正确性的前提下。为了达成这种正确性,我们组合使用了多种机制: -* 将写入操作的内容表示为单个消息,可以轻松地以原子方式编写 —— 这种方法非常适合事件排序(请参阅第457页的“事件排序”)。 -* 使用确定性描述函数从该单个消息中获取所有其他状态更新,这与存储过程类似(请参见第252页的“[真的串行执行]()”和第479页的“[作为衍生函数的应用程序代码]()”) -* 通过所有这些级别的处理传递客户端生成的请求ID,启用端到端重复抑制和幂等性 -* 使消息不可变并允许衍生数据不时重新处理,这使得从错误中恢复变得更加容易(请参阅“[不可变事件的优点]()”第367页) +* 将写入操作的内容表示为单条消息,从而可以轻松地被原子写入 —— 与事件溯源搭配效果拔群(参阅“[事件溯源](ch11.md#事件溯源)”)。 +* 使用与存储过程类似的确定性衍生函数,从这一消息中衍生出所有其他的状态变更(参见“[真的串行执行](ch7.md#真的串行执行)”和“[作为衍生函数的应用代码](ch11.md#作为衍生函数的应用代码)”) +* 将客户端生成的请求ID传递通过所有的处理层次,从而启用端到端除重,带来幂等性。 +* 使消息不可变,并允许衍生数据能随时被重新处理,这使从错误中恢复更加容易(参阅“[不可变事件的优点](ch11.md#不可变事件的优点)”) -这种机制的组合在我看来是未来构建容错应用程序的一个非常有前途的方向。 +这种机制组合在我看来,是未来构建容错应用的一个非常有前景的方向。 -#### 松散的解释约束 +#### 宽松地解释约束 -​ 如前所述,执行唯一性约束需要达成共识,通常通过在单个节点中汇集特定分区中的所有事件来实现。如果我们想要传统的唯一性约束形式,并且流处理无法避免,这种限制是不可避免的。 +​ 如前所述,执行唯一性约束需要共识,通常通过在单个节点中汇集特定分区中的所有事件来实现。如果我们想要传统的唯一性约束形式,这种限制是不可避免的,流处理也不例外。 -然而,另一个要认识到的是,许多真正的应用程序实际上可以摆脱唯一性较弱的概念: +然而另一个需要了解的事实是,许多真实世界的应用实际上可以摆脱这种形式,接受弱得多的唯一性: -* 如果两个人同时注册相同的用户名或预订相同的座位,则可以发送其中一个消息来道歉,并要求他们选择不同的用户名。这种纠正错误的变化被称为补偿性事务【59,60】。 -* 如果客户订购的物品多于仓库中的物品,则可以订购更多库存,为延误向客户道歉,并向他们提供折扣。实际上,如果叉车在仓库中的某些物品上方跑过来,而库存的物品数量比您想象的要少,那么您就必须这样做【61】。因此,无论如何,道歉工作流程已经需要成为业务流程的一部分,因此可能不需要对库存中的项目数量进行线性化约束。 -* 同样地,许多航空公司预计飞行员会错过飞机,许多旅馆超额预订客房,预计部分客人将取消预订。在这些情况下,出于商业原因故意违反了“每个座位一人”的约束,并且处理补偿过程(退款,升级,在邻近酒店提供免费房间)以处理需求超过供应的情况。即使没有超额预订,为了应对由于恶劣天气而被取消的航班或者罢工的员工,这些问题的恢复仅仅是商业活动的正常组成部分,就需要道歉和赔偿流程。 -* 如果有人收取比他们账户中更多的钱,银行可以向他们收取透支费用,并要求他们偿还欠款。通过限制每天的提款总额,银行的风险是有限的。 +* 如果两个人同时注册了相同的用户名或预订了相同的座位,你可以发送其中一个发消息道歉,并要求他们选择一个不同的用户名。这种纠正错误的变化被称为**补偿性事务(compensating transaction)**【59,60】。 +* 如果客户订购的物品多于仓库中的物品,你可以下单补仓,并为延误向客户道歉,向他们提供折扣。实际上,这么说吧,如果在叉车在仓库中轧过了你的货物,剩下的货物比你想象的要少,那么你也是得这么做【61】。因此,既然道歉工作流无论如何已经成为你商业过程中的一部分了,那么对库存物品数目添加线性一致的约束可能就没必要了。 +* 与之类似,许多航空公司都会超卖机票,打着一些旅客可能会错过航班的算盘;许多旅馆也会超卖客房,抱着部分客人可能会取消预订的期望。在这些情况下,出于商业原因而故意违反了“一人一座”的约束;当需求超过供给的情况出现时,就会进入补偿流程(退款、升级舱位/房型、提供隔壁酒店的免费的房间)。即使没有超卖,为了应对由恶劣天气或员工罢工导致的航班取消,你还是需要道歉与补偿流程 —— 从这些问题中恢复仅仅是商业活动的正常组成部分。 +* 如果有人从账户超额取款,银行可以向他们收取透支费用,并要求他们偿还欠款。通过限制每天的提款总额,银行的风险是有限的。 -​ 在许多商业环境中,临时违反约束并稍后通过道歉修复它实际上是可以接受的。道歉的成本(金钱或报酬)各不相同,但通常很低:您无法取消发送电子邮件,但可以发送后续电子邮件并进行更正。如果您不小心向信用卡收取了两次费用,您可以退还其中一项费用,而您的费用仅仅是处理费用,并且可以处理客户投诉。一旦自动提款机支付了钱,就不能直接将其退回,尽管原则上如果账户透支并且客户不支付,原则上可以派遣收债员收回款项。 +​ 在许多商业场景中,临时违背约束并稍后通过道歉来修复,实际上是可以接受的。道歉的成本各不相同,但通常很低(以金钱或名声来算):你无法撤回已发送的电子邮件,但可以发送一封后续电子邮件进行更正。如果你不小心向信用卡收取了两次费用,则可以将其中一项收费退款,而代价仅仅是手续费,也许还有客户的投诉……。尽管一旦ATM吐了钱,你无法直接取回,但原则上如果账户透支而客户拒不支付,你可以派催收员收回欠款…。 -​ 道歉的成本是否可以接受是一个商业决策。如果可以接受的话,在写入数据之前检查所有约束的传统模型是不必要的限制,并且不需要线性化约束。乐观地继续写作,并在事实之后检查约束,这可能是一个合理的选择。您仍然可以确保验证发生在做恢复成本高昂的事情之前,但这并不意味着在您编写数据之前您必须先进行验证。 +​ 道歉的成本是否能接受是一个商业决策。如果可以接受的话,在写入数据之前检查所有约束的传统模型反而会带来不必要的限制,而线性一致性的约束也不是必须的。乐观写入,事后检查可能是一种合理的选择。你仍然可以在做一些挽回成本高昂的事情前确保验证发生,但这并不意味着写入数据之前必须先进行验证。 -​ 这些应用程序确实需要完整性:您不会希望失去预订,或者由于信用和借方不匹配而导致资金消失。但是他们并不需要及时执行约束:如果您销售的物品多于仓库中的物品,则可以在事后道歉后修补问题。这样做与我们在第171页上的“处理写入冲突”中讨论的冲突解决方法类似。 +​ 这些应用**确实**需要完整性:你不会希望丢失预订信息,或者由于借方贷方不匹配导致资金消失。但是它们在执行约束时**并不需要**及时性:如果你销售的货物多于仓库中的库存,可以在事后道歉后并弥补问题。这种做法与我们在“[处理写入冲突](ch5.md#处理写入冲突)”中讨论的冲突解决方法类似。 #### 无协调数据系统 我们现在做了两个有趣的观察: -1. 数据流系统可以保持对衍生数据的完整性保证,而无需原子提交,线性化或同步跨分区协调。 -2. 虽然严格的唯一性约束要求及时性和协调性,但许多应用程序实际上可以很好地处理宽松的约束,只要整个过程保持完整性,它们可能会被暂时违反并予以修复。 +1. 数据流系统可以维持衍生数据的完整性保证,而无需原子提交,线性一致性,或者同步跨分区协调。 +2. 虽然严格的唯一性约束要求及时性和协调,但许多应用实际上可以接受宽松的约束:只要整个过程保持完整性,这些约束可能会被临时违反并在稍后被修复。 -总之,这些观察意味着数据流系统可以为许多应用程序提供数据管理服务,而不需要协调,同时仍然提供强大的完整性保证。这种避免协调的数据系统具有很大的吸引力:它们可以比需要执行同步协调的系统获得更好的性能和容错能力【56】。 +总之这些观察意味着,数据流系统可以为许多应用提供无需协调的数据管理服务,且仍能给出很强的完整性保证。这种**无协调(coordination-avoiding)**的数据系统有着很大的吸引力:比起需要执行同步协调的系统,它们能达到更好的性能与更强的容错能力【56】。 -​ 例如,这种系统可以在多主配置中跨多个数据中心进行分布式操作,在区域之间异步复制。任何一个数据中心都可以继续独立运行,因为不需要同步跨区域协调。这样一个系统的时效性保证会很弱 —— 如果不引入协调,它就不可能线性化,但它仍然可以提供强有力的完整性保证。 +​ 例如,这种系统可以使用多领导者配置运维,跨越多个数据中心,在区域间异步复制。任何一个数据中心都可以持续独立运行,因为不需要同步的跨区域协调。这样的系统时效性保证会很弱 —— 如果不引入协调它是不可能是线性一致的 —— 但它仍然可以提供有力的完整性保证。 -​ 在这种情况下,序列化事务作为维护衍生状态的一部分仍然是有用的,但它们可以在小范围内运行,在那里它们工作得很好【8】。异构分布式事务(如XA事务)(请参阅“[实践中的分布式事务]()”(第367页))不是必需的。同步协调仍然可以在需要的地方引入(例如,在无法恢复的操作之前执行严格的限制),但是如果只有一个小的协议,则不需要任何东西来支付协调费用应用程序的一部分需要它【43】。 +​ 在这种情况下,可序列化事务作为维护衍生状态的一部分仍然是有用的,但它们可以在小范围内运行,在那里它们工作得很好【8】。异构分布式事务(如XA事务)(请参阅“[实践中的分布式事务](ch9.md#实践中的分布式事务)”)不是必需的。同步协调仍然可以在需要的地方引入(例如在无法恢复的操作之前强制执行严格的约束),但是如果只是应用的一小部分地方需要它,没必要让所有操作都付出协调的代价。【43】。 -​ 查看协调和约束的另一种方法是:它们减少了由于不一致而必须做出的道歉数量,但也可能会降低系统的性能和可用性,从而可能会增加必须制定的道歉数量中断。您不能将道歉数量减少到零,但您可以根据自己的需求寻找最佳平衡点 —— 这是既不存在太多不一致性又不存在太多可用性问题的最佳选择。 +​ 另一种审视协调与约束的角度是:它们减少了由于不一致而必须做出的道歉数量,但也可能会降低系统的性能和可用性,从而可能增加由于宕机中断而需要做出的道歉数量。你不可能将道歉数量减少到零,但可以根据自己的需求寻找最佳平衡点 —— 既不存在太多不一致性,又不存在太多可用性问题的最佳选择。 ### 信任,但需要验证 -​ 我们所有关于正确性,完整性和容错性的讨论都假设某些事情可能会出错,但其他事情则不会。我们将这些假设称为我们的系统模型(请参阅“[将系统模型映射到现实世界]()”一节第309页):例如,我们应该假设进程可能会崩溃,机器可能突然断电,网络可能会任意延迟或丢弃消息。但是我们也可以假设写入磁盘的数据在`fsync`之后不会丢失,内存中的数据没有损坏,并且CPU的乘法指令总是返回正确的结果。 +​ 我们所有关于正确性,完整性和容错性的讨论都假设某些事情可能会出错,但其他事情则不会。我们将这些假设称为我们的系统模型(参阅“[将系统模型映射到现实世界](ch8.md#将系统模型映射到现实世界)”):例如,我们应该假设进程可能会崩溃,机器可能突然断电,网络可能会任意延迟或丢弃消息。但是我们也可以假设写入磁盘的数据在`fsync`之后不会丢失,内存中的数据没有损坏,并且CPU的乘法指令总是返回正确的结果。 ​ 这些假设是非常合理的,因为大多数时候它们都是真实的,如果我们不得不经常担心计算机出错,那么很难完成任何事情。传统上,系统模型采用二元方法处理故障:我们假设有些事情会发生,而其他事情永远不会发生。实际上,这更像是一个概率问题:有些事情更可能,其他事情不太可能。问题是违反我们的假设是否经常发生,以至于我们可能在实践中遇到它们。 @@ -724,13 +734,13 @@ COMMIT; ​ 在本书的最后部分,我想退后一步。在本书中,我们考察了各种不同的数据系统体系结构,评估了它们的优缺点,并探讨了构建可靠,可扩展和可维护应用程序的技术。但是,我们忽略了现在我想填补的讨论中一个重要的基本部分。 -​ 每个系统都是为了一个目的而建造的我们采取的每一项行动都有既定的意义,也有无意义的后果。其目的可能与赚钱一样简单,但世界的后果可能会远远超出最初的目的。我们这些建立这些系统的工程师有责任仔细考虑这些后果并有意识地决定我们希望生活在哪一种世界。 +​ 每个系统都是为了一个目的而建造的,我们采取的每一项行动都有既定的意义,也有无意义的后果。其目的可能与赚钱一样简单,但世界的后果可能会远远超出最初的目的。我们这些建立这些系统的工程师有责任仔细考虑这些后果,并有意识地决定我们希望生活在哪一种世界。 ​ 我们将数据作为一个抽象的东西来讨论,但请记住,许多数据集都是关于人的:他们的行为,他们的兴趣,他们的身份。我们必须以人性和尊重来对待这些数据。用户也是人类,人的尊严是至高无上的。 ​ 软件开发日益涉及重要的道德选择。有一些指导原则可以帮助软件工程师解决这些问题,例如ACM的软件工程道德规范和专业实践【77】,但很少在实践中讨论,应用和实施。因此,工程师和产品经理有时会对隐私和产品潜在的负面后果采取非常傲慢的态度【78,79,80】。 -​ 技术本身并不好或不好,重要的是如何使用它以及它如何影响人。对于像搜索引擎这样的软件系统来说,就像使用像武器这样的武器非常相似。我认为,软件工程师仅仅专注于技术并忽视其后果是不够的:伦理责任也是我们的责任。有关道德的推理是困难的,但忽视这一点太重要了。 +​ 技术本身并无好坏之分,重要的是如何使用它以及它如何影响人。对于像搜索引擎这样的软件系统来说,就像使用像武器这样的武器非常相似。我认为,软件工程师仅仅专注于技术并忽视其后果是不够的:伦理责任也是我们的责任。有关道德的推理是困难的,但忽视这一点太重要了。 ### 预测分析 @@ -847,9 +857,9 @@ COMMIT; ​ 就像工业革命有一个黑暗的一面需要管理一样,我们对信息时代的转变也有我们需要面对和解决的重大问题。我相信收集和使用数据是其中的一个问题。用布鲁斯·施奈尔(Bruce Schneier)【96】的话来说: -> 数据是信息时代的污染问题,保护隐私是环境挑战。几乎所有的电脑都能生成信息。它停留在周围,溃烂。我们如何处理它 —— 我们如何控制它以及如何处理它 —— 对我们信息经济的健康至关重要。正如我们今天回顾工业时代的早期十年,并想知道我们的祖先在匆忙建立一个工业世界的过程中如何忽略污染,我们的孙辈在信息时代的前几十年将回顾我们,就我们如何应对数据收集和滥用的挑战来判断我们。 +> ​ 数据是信息时代的污染问题,保护隐私是环境挑战。几乎所有的电脑都能生成信息。它停留在周围,溃烂。我们如何处理它 —— 我们如何控制它以及如何处理它 —— 对我们信息经济的健康至关重要。正如我们今天回顾工业时代的早期十年,并想知道我们的祖先在匆忙建立一个工业世界的过程中如何忽略污染,我们的孙辈在信息时代的前几十年将回顾我们,就我们如何应对数据收集和滥用的挑战来评断我们。 > -> 我们应该设法让他们感到骄傲。 +> ​ 我们应该设法让他们感到骄傲。 #### 立法和自律