mirror of
https://github.com/Vonng/ddia.git
synced 2025-01-05 15:30:06 +08:00
translatin update for chapter 12 -- still in progress
This commit is contained in:
parent
9683eae3e7
commit
17b2f013e0
298
ch12.md
298
ch12.md
@ -14,11 +14,11 @@
|
||||
|
||||
对未来的看法与推测当然具有很大的主观性。所以在撰写本章时,当提及我个人的观点时会使用第一人称。您完全可以不同意这些观点并提出自己的看法,但我希望本章中的概念,至少能成为富有成效的讨论出发点,并澄清一些经常被混淆的概念。
|
||||
|
||||
[第1章](ch1.md)概述了本书的目标:探索如何创建**可靠**,**可伸缩**和**可维护**的应用与系统。这一主题贯穿了所有的章节:例如,我们讨论了许多有助于提高可靠性的容错算法,有助于提高可伸缩性的分区,以及有助于提高可维护性的演化与抽象机制。在本章中,我们将把所有这些想法结合在一起,并在它们的基础上展望未来。我们的目标是,发现如何设计出比现有应用更好的应用 —— 健壮,正确,可演化,且最终对人类有益。
|
||||
[第一章](ch1.md)概述了本书的目标:探索如何创建**可靠**,**可伸缩**和**可维护**的应用与系统。这一主题贯穿了所有的章节:例如,我们讨论了许多有助于提高可靠性的容错算法,有助于提高可伸缩性的分区,以及有助于提高可维护性的演化与抽象机制。在本章中,我们将把所有这些想法结合在一起,并在它们的基础上展望未来。我们的目标是,发现如何设计出比现有应用更好的应用 —— 健壮,正确,可演化,且最终对人类有益。
|
||||
|
||||
## 数据集成
|
||||
|
||||
本书中反复出现的主题是,对于任何给定的问题都会有好几种解决方案,所有这些解决方案都有不同的优缺点与利弊权衡。例如在[第3章](ch3.md)讨论存储引擎时,我们看到了日志结构存储,B树,以及列存储。在[第5章](ch5.md)讨论复制时,我们看到了单领导者,多领导者,和无领导者的方法。
|
||||
本书中反复出现的主题是,对于任何给定的问题都会有好几种解决方案,所有这些解决方案都有不同的优缺点与利弊权衡。例如在[第三章](ch3.md)讨论存储引擎时,我们看到了日志结构存储,B树,以及列存储。在[第五章](ch5.md)讨论复制时,我们看到了单领导者,多领导者,和无领导者的方法。
|
||||
|
||||
如果你有一个类似于“我想存储一些数据并稍后再查询”的问题,那么并没有一种正确的解决方案。但对于不同的具体环境,总会有不同的合适方法。软件实现通常必须选择一种特定的方法。使单条代码路径能做到稳定健壮且表现良好已经是一件非常困难的事情了 —— 尝试在单个软件中完成所有事情,几乎可以保证,实现效果会很差。
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
|
||||
例如,为了处理任意关键词的搜索查询,将OLTP数据库与全文搜索索引集成在一起是很常见的的需求。尽管一些数据库(例如PostgreSQL)包含了全文索引功能,对于简单的应用完全够了【1】,但更复杂的搜索能力就需要专业的信息检索工具了。相反的是,搜索索引通常不适合作为持久的记录系统,因此许多应用需要组合这两种不同的工具以满足所有需求。
|
||||
|
||||
我们在“[使系统保持同步](ch11.md#使系统保持同步)”中接触过集成数据系统的问题。随着数据不同表示形式的增加,集成问题变得越来越困难。除了数据库和搜索索引之外,也许你需要在分析系统(数据仓库,或批处理和流处理系统)中维护数据副本;维护从原始数据中衍生的缓存,或反规范化的数据版本;将数据灌入机器学习,分类,排名,或推荐系统中;或者基于数据变更发送通知。
|
||||
我们在“[保持系统同步](ch11.md#保持系统同步)”中接触过集成数据系统的问题。随着数据不同表示形式的增加,集成问题变得越来越困难。除了数据库和搜索索引之外,也许你需要在分析系统(数据仓库,或批处理和流处理系统)中维护数据副本;维护从原始数据中衍生的缓存,或反规范化的数据版本;将数据灌入机器学习,分类,排名,或推荐系统中;或者基于数据变更发送通知。
|
||||
|
||||
令人惊讶的是,我经常看到软件工程师做出这样的陈述:“根据我的经验,99%的人只需要X”或者 “......不需要X”(对于各种各样的X)。我认为这种陈述更像是发言人自己的经验,而不是技术实际上的实用性。可能对数据执行的操作,其范围极其宽广。某人认为鸡肋而毫无意义的功能可能是别人的核心需求。当你拉高视角,并考虑跨越整个组织范围的数据流时,数据集成的需求往往就会变得明显起来。
|
||||
|
||||
@ -40,84 +40,80 @@
|
||||
|
||||
当需要在多个存储系统中维护相同数据的副本以满足不同的访问模式时,你要对输入和输出了如指掌:哪些数据先写入,哪些数据表示衍生自哪些来源?如何以正确的格式,将所有数据导入正确的地方?
|
||||
|
||||
例如,你可能会首先将数据写入**记录数据库**系统,捕获对该数据库所做的变更(参阅“[捕获数据变更](ch11.md#捕获数据变更)”),然后将变更应用于数据库中的搜索索引相同的顺序。如果变更数据捕获(CDC)是更新索引的唯一方式,则可以确定该索引完全派生自记录系统,因此与其保持一致(除软件错误外)。写入数据库是向该系统提供新输入的唯一方式。
|
||||
例如,你可能会首先将数据写入**记录系统**数据库,捕获对该数据库所做的变更(请参阅“[变更数据捕获](ch11.md#变更数据捕获)”),然后将变更以相同的顺序应用于搜索索引。如果变更数据捕获(CDC)是更新索引的唯一方式,则可以确定该索引完全派生自记录系统,因此与其保持一致(除软件错误外)。写入数据库是向该系统提供新输入的唯一方式。
|
||||
|
||||
允许应用程序直接写入搜索索引和数据库引入了如[图11-4](img/fig11-4.png)所示的问题,其中两个客户端同时发送冲突的写入,且两个存储系统按不同顺序处理它们。在这种情况下,既不是数据库说了算,也不是搜索索引说了算,所以它们做出了相反的决定,进入彼此间持久性的不一致状态。
|
||||
|
||||
如果您可以通过单个系统来提供所有用户输入,从而决定所有写入的排序,则通过按相同顺序处理写入,可以更容易地衍生出其他数据表示。 这是状态机复制方法的一个应用,我们在“[全序广播](ch9.md#全序广播)”中看到。无论您使用变更数据捕获还是事件源日志,都不如仅对全局顺序达成共识更重要。
|
||||
如果您可以通过单个系统来提供所有用户输入,从而决定所有写入的排序,则通过按相同顺序处理写入,可以更容易地衍生出其他数据表示。 这是状态机复制方法的一个应用,我们在“[全序广播](ch9.md#全序广播)”中看到。无论您使用变更数据捕获还是事件溯源日志,都不如简单的基于全序的决策原则更重要。
|
||||
|
||||
基于事件日志来更新衍生数据的系统,通常可以做到**确定性**与**幂等性**(参见第478页的“[幂等性]()”),使得从故障中恢复相当容易。
|
||||
基于事件日志来更新衍生数据的系统,通常可以做到**确定性**与**幂等性**(请参阅[幂等性](ch11.md#幂等性)”),使得从故障中恢复相当容易。
|
||||
|
||||
#### 衍生数据与分布式事务
|
||||
|
||||
保持不同数据系统彼此一致的经典方法涉及分布式事务,如“[原子提交与两阶段提交(2PC)](ch9.md#原子提交与两阶段提交(2PC))”中所述。与分布式事务相比,使用衍生数据系统的方法如何?
|
||||
|
||||
在抽象层面,它们通过不同的方式达到类似的目标。分布式事务通过**锁**进行互斥来决定写入的顺序(参阅“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”),而CDC和事件溯源使用日志进行排序。分布式事务使用原子提交来确保变更只生效一次,而基于日志的系统通常基于**确定性重试**和**幂等性**。
|
||||
在抽象层面,它们通过不同的方式达到类似的目标。分布式事务通过**锁**进行互斥来决定写入的顺序(请参阅“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”),而CDC和事件溯源使用日志进行排序。分布式事务使用原子提交来确保变更只生效一次,而基于日志的系统通常基于**确定性重试**和**幂等性**。
|
||||
|
||||
最大的不同之处在于事务系统通常提供 **[线性一致性](ch9.md#线性一致性)**,这包含着有用的保证,例如[读己之写](ch5.md#读己之写)。另一方面,衍生数据系统通常是异步更新的,因此它们默认不会提供相同的时序保证。
|
||||
最大的不同之处在于事务系统通常提供[线性一致性](ch9.md#线性一致性),这包含着有用的保证,例如[读己之写](ch5.md#读己之写)。另一方面,衍生数据系统通常是异步更新的,因此它们默认不会提供相同的时序保证。
|
||||
|
||||
在愿意为分布式事务付出代价的有限场景中,它们已被成功应用。但是,我认为XA的容错能力和性能很差劲(参阅“[实践中的分布式事务](ch9.md#实践中的分布式事务)”),这严重限制了它的实用性。我相信为分布式事务设计一种更好的协议是可行的。但使这样一种协议被现有工具广泛接受是很有挑战的,且不是立竿见影的事。
|
||||
在愿意为分布式事务付出代价的有限场景中,它们已被成功应用。但是,我认为XA的容错能力和性能很差劲(请参阅“[实践中的分布式事务](ch9.md#实践中的分布式事务)”),这严重限制了它的实用性。我相信为分布式事务设计一种更好的协议是可行的。但使这样一种协议被现有工具广泛接受是很有挑战的,且不是立竿见影的事。
|
||||
|
||||
在没有广泛支持的良好分布式事务协议的情况下,我认为基于日志的衍生数据是集成不同数据系统的最有前途的方法。然而,诸如读己之写的保证是有用的,我认为告诉所有人“最终一致性是不可避免的 —— 忍一忍并学会和它打交道”是没有什么建设性的(至少在缺乏**如何**应对的良好指导时)。
|
||||
|
||||
在“[将事情做正确](#将事情做正确)”中,我们将讨论一些在异步衍生系统之上实现更强保障的方法,并迈向分布式事务和基于日志的异步系统之间的中间地带。
|
||||
|
||||
#### 全局有序的限制
|
||||
|
||||
对于足够小的系统,构建一个完全有序的事件日志是完全可行的(正如单主复制数据库的流行所证明的那样,这正好建立了这样一种日志)。但是,随着系统向更大更复杂的工作负载伸缩,限制开始出现:
|
||||
|
||||
* 在大多数情况下,构建完全有序的日志,需要所有事件汇集于决定顺序的单个领导节点。如果事件吞吐量大于单台计算机的处理能力,则需要将其分割到多台计算机上(参见“[分区日志](ch11.md#分区日志)”)。然后两个不同分区中的事件顺序关系就不明确了。
|
||||
#### 全序的限制
|
||||
|
||||
对于足够小的系统,构建一个完全有序的事件日志是完全可行的(正如单主复制数据库的流行所证明的那样,它正好建立了这样一种日志)。但是,随着系统向更大更复杂的工作负载伸缩,限制开始出现:
|
||||
|
||||
* 在大多数情况下,构建完全有序的日志,需要所有事件汇集于决定顺序的**单个领导者节点**。如果事件吞吐量大于单台计算机的处理能力,则需要将其分区到多台计算机上(请参阅“[分区日志](ch11.md#分区日志)”)。然后两个不同分区中的事件顺序关系就不明确了。
|
||||
* 如果服务器分布在多个**地理位置分散**的数据中心上,例如为了容忍整个数据中心掉线,您通常在每个数据中心都有单独的主库,因为网络延迟会导致同步的跨数据中心协调效率低下(请参阅“[多主复制](ch5.md#多主复制)“)。这意味着源自两个不同数据中心的事件顺序未定义。
|
||||
* 将应用程序部署为微服务时(请参阅“[服务中的数据流:REST与RPC](ch4.md#服务中的数据流:REST与RPC)”),常见的设计选择是将每个服务及其持久状态作为独立单元进行部署,服务之间不共享持久状态。当两个事件来自不同的服务时,这些事件间的顺序未定义。
|
||||
* 某些应用程序在客户端保存状态,该状态在用户输入时立即更新(无需等待服务器确认),甚至可以继续脱机工作(请参阅“[需要离线操作的客户端](ch5.md#需要离线操作的客户端)”)。对于这样的应用程序,客户端和服务器很可能以不同的顺序看到事件。
|
||||
|
||||
在形式上,决定事件的全局顺序称为**全序广播**,相当于**共识**(请参阅“[共识算法和全序广播](ch9.md#共识算法和全序广播)”)。大多数共识算法都是针对单个节点的吞吐量足以处理整个事件流的情况而设计的,并且这些算法不提供多个节点共享事件排序工作的机制。设计可以伸缩至单个节点的吞吐量之上,且在地理位置分散的环境中仍然工作良好的的共识算法仍然是一个开放的研究问题。
|
||||
|
||||
* 将应用程序部署为微服务时(请参阅第125页上的“[服务中的数据流:REST与RPC](ch4.md#服务中的数据流:REST与RPC)”),常见的设计选择是将每个服务及其持久状态作为独立单元进行部署,服务之间不共享持久状态。当两个事件来自不同的服务时,这些事件间的顺序未定义。
|
||||
#### 排序事件以捕获因果关系
|
||||
|
||||
|
||||
* 某些应用程序在客户端保存状态,该状态在用户输入时立即更新(无需等待服务器确认),甚至可以继续脱机工作(参阅“[脱机操作的客户端](ch5.md#脱机操作的客户端)”)。有了这样的应用程序,客户端和服务器很可能以不同的顺序看到事件。
|
||||
|
||||
在形式上,决定事件的全局顺序称为**全序广播**,相当于**共识**(参阅“[共识算法和全序广播](ch9.md#共识算法和全序广播)”)。大多数共识算法都是针对单个节点的吞吐量足以处理整个事件流的情况而设计的,并且这些算法不提供多个节点共享事件排序工作的机制。设计可以伸缩至单个节点的吞吐量之上,且在地理散布环境中仍然工作良好的的共识算法仍然是一个开放的研究问题。
|
||||
|
||||
#### 排序事件以捕捉因果关系
|
||||
|
||||
在事件之间不存在因果关系的情况下,缺乏全局顺序并不是一个大问题,因为并发事件可以任意排序。其他一些情况很容易处理:例如,当同一对象有多个更新时,它们可以通过将特定对象ID的所有更新路由到相同的日志分区来完全排序。然而,因果关系有时会以更微妙的方式出现(参阅“[顺序和因果关系](ch8.md#顺序和因果关系)”)。
|
||||
在事件之间不存在因果关系的情况下,全序的缺乏并不是一个大问题,因为并发事件可以任意排序。其他一些情况很容易处理:例如,当同一对象有多个更新时,它们可以通过将特定对象ID的所有更新路由到相同的日志分区来完全排序。然而,因果关系有时会以更微妙的方式出现(请参阅“[顺序与因果关系](ch9.md#顺序与因果关系)”)。
|
||||
|
||||
例如,考虑一个社交网络服务,以及一对曾处于恋爱关系但刚分手的用户。其中一个用户将另一个用户从好友中移除,然后向剩余的好友发送消息,抱怨他们的前任。用户的心思是他们的前任不应该看到这些粗鲁的消息,因为消息是在好友状态解除后发送的。
|
||||
|
||||
但是如果好友关系状态与消息存储在不同的地方,在这样一个系统中,可能会出现**解除好友**事件与**发送消息**事件之间的因果依赖丢失的情况。如果因果依赖关系没有被捕捉到,则发送有关新消息的通知的服务可能会在**解除好友**事件之前处理**发送消息**事件,从而错误地向前任发送通知。
|
||||
|
||||
在本例中,通知实际上是消息和好友列表之间的连接,使得它与我们先前讨论的连接的时间问题有关(请参阅第475页的“[连接的时间依赖性](ch11.md#连接的时间依赖性)”)。不幸的是,这个问题似乎并没有一个简单的答案【2,3】。起点包括:
|
||||
在本例中,通知实际上是消息和好友列表之间的连接,使得它与我们先前讨论的连接的时序问题有关(请参阅“[连接的时间依赖性](ch11.md#连接的时间依赖性)”)。不幸的是,这个问题似乎并没有一个简单的答案【2,3】。起点包括:
|
||||
|
||||
* 逻辑时间戳可以提供无需协调的全局顺序(参见“[序列号排序](ch8.md#序列号排序)”),因此它们可能有助于全序广播不可行的情况。但是,他们仍然要求收件人处理不按顺序发送的事件,并且需要传递其他元数据。
|
||||
* 如果你可以记录一个事件来记录用户在做出决定之前所看到的系统状态,并给该事件一个唯一的标识符,那么后面的任何事件都可以引用该事件标识符来记录因果关系【4】 。我们将在“[读也是事件](#读也是事件)”中回到这个想法。
|
||||
* 冲突解决算法(请参阅“[自动冲突解决](ch5.md#自动冲突解决)”)有助于处理以意外顺序传递的事件。它们对于维护状态很有用,但如果行为有外部副作用(例如,也许,随着时间的推移,应用开发模式将出现,使得能够有效地捕获因果依赖关系,并且保持正确的衍生状态,而不会迫使所有事件经历全序广播的瓶颈)。
|
||||
* 逻辑时间戳可以提供无需协调的全局顺序(请参阅“[序列号顺序](ch9.md#序列号顺序)”),因此它们可能有助于全序广播不可行的情况。但是,他们仍然要求收件人处理不按顺序发送的事件,并且需要传递其他元数据。
|
||||
* 如果你可以记录一个事件来记录用户在做出决定之前所看到的系统状态,并给该事件一个唯一的标识符,那么后面的任何事件都可以引用该事件标识符来记录因果关系【4】。我们将在“[读也是事件](#读也是事件)”中回到这个想法。
|
||||
* 冲突解决算法(请参阅“[自动冲突解决](ch5.md#自动冲突解决)”)有助于处理以意外顺序传递的事件。它们对于维护状态很有用,但如果行为有外部副作用(例如,给用户发送通知),就没什么帮助了。
|
||||
|
||||
也许,随着时间的推移,应用开发模式将出现,使得能够有效地捕获因果依赖关系,并且保持正确的衍生状态,而不会迫使所有事件经历全序广播的瓶颈)。
|
||||
|
||||
### 批处理与流处理
|
||||
|
||||
我会说数据集成的目标是,确保数据最终能在所有正确的地方表现出正确的形式。这样做需要消费输入,转换,连接,过滤,聚合,训练模型,评估,以及最终写出适当的输出。批处理和流处理是实现这一目标的工具。
|
||||
|
||||
批处理和流处理的输出是衍生数据集,例如搜索索引,物化视图,向用户显示的建议,聚合指标等(请参阅“[批处理工作流的输出](ch10.md#批处理工作流的输出)”和“[流处理的用法](ch11.md#流处理的用法)”)。
|
||||
批处理和流处理的输出是衍生数据集,例如搜索索引,物化视图,向用户显示的建议,聚合指标等(请参阅“[批处理工作流的输出](ch10.md#批处理工作流的输出)”和“[流处理的应用](ch11.md#流处理的应用)”)。
|
||||
|
||||
正如我们在[第10章](ch10.md)和[第11章](ch11.md)中看到的,批处理和流处理有许多共同的原则,主要的根本区别在于流处理器在无限数据集上运行,而批处理输入是已知的有限大小。处理引擎的实现方式也有很多细节上的差异,但是这些区别已经开始模糊。
|
||||
正如我们在[第十章](ch10.md)和[第十一章](ch11.md)中看到的,批处理和流处理有许多共同的原则,主要的根本区别在于流处理器在无限数据集上运行,而批处理输入是已知的有限大小。处理引擎的实现方式也有很多细节上的差异,但是这些区别已经开始模糊。
|
||||
|
||||
Spark在批处理引擎上执行流处理,将流分解为**微批次(microbatches)**,而Apache Flink则在流处理引擎上执行批处理【5】。原则上,一种类型的处理可以用另一种类型来模拟,但是性能特征会有所不同:例如,在跳跃或滑动窗口上,微批次可能表现不佳【6】。
|
||||
|
||||
#### 维护衍生状态
|
||||
|
||||
批处理有着很强的函数式风格(即使其代码不是用函数式语言编写的):它鼓励确定性的纯函数,其输出仅依赖于输入,除了显式输出外没有副作用,将输入视作不可变的,且输出是仅追加的。流处理与之类似,但它扩展了算子以允许受管理的,容错的状态(参阅“[失败后重建状态”](ch11.md#失败后重建状态))。
|
||||
批处理有着很强的函数式风格(即使其代码不是用函数式语言编写的):它鼓励确定性的纯函数,其输出仅依赖于输入,除了显式输出外没有副作用,将输入视作不可变的,且输出是仅追加的。流处理与之类似,但它扩展了算子以允许受管理的、容错的状态(请参阅“[失败后重建状态”](ch11.md#失败后重建状态))。
|
||||
|
||||
具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(参见“[幂等性](ch11.md#幂等性)”),也简化了有关组织中数据流的推理【7】。无论衍生数据是搜索索引,统计模型还是缓存,采用这种观点思考都是很有帮助的:将其视为从一个东西衍生出另一个的数据管道,将一个系统的状态变更推送至函数式应用代码中,并将其效果应用至衍生系统中。
|
||||
具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(请参阅“[幂等性](ch11.md#幂等性)”),也简化了有关组织中数据流的推理【7】。无论衍生数据是搜索索引、统计模型还是缓存,采用这种观点思考都是很有帮助的:将其视为从一个东西衍生出另一个的数据管道,通过函数式应用代码推送一个系统的状态变更,并将其效果应用至衍生系统中。
|
||||
|
||||
原则上,衍生数据系统可以同步地维护,就像关系数据库在与被索引表写入操作相同的事务中同步更新辅助索引一样。然而,异步是基于事件日志的系统稳健的原因:它允许系统的一部分故障被抑制在本地,而如果任何一个参与者失败,分布式事务将中止,因此它们倾向于通过将故障传播到系统的其余部分来放大故障(请参阅“[分布式事务的限制](ch9.md#分布式事务的限制)”)。
|
||||
原则上,衍生数据系统可以同步地维护,就像关系数据库在与索引表写入操作相同的事务中同步更新辅助索引一样。然而,异步是使基于事件日志的系统稳健的原因:它允许系统的一部分故障被抑制在本地。而如果任何一个参与者失败,分布式事务将中止,因此它们倾向于通过将故障传播到系统的其余部分来放大故障(请参阅“[分布式事务的限制](ch9.md#分布式事务的限制)”)。
|
||||
|
||||
我们在“[分区与次级索引](ch6.md#分区与次级索引)”中看到,二级索引经常跨越分区边界。具有二级索引的分区系统需要将写入发送到多个分区(如果索引按关键词分区的话)或将读取发送到所有分区(如果索引是按文档分区的话)。如果索引是异步维护的,这种交叉分区通信也是最可靠和最可伸缩的【8】(另请参阅“[多分区数据处理](ch11.md#多分区数据处理)”)。
|
||||
我们在“[分区与次级索引](ch6.md#分区与次级索引)”中看到,二级索引经常跨越分区边界。具有二级索引的分区系统需要将写入发送到多个分区(如果索引按关键词分区的话)或将读取发送到所有分区(如果索引是按文档分区的话)。如果索引是异步维护的,这种跨分区通信也是最可靠和最可伸缩的【8】(另请参阅“[多分区数据处理](多分区数据处理)”)。
|
||||
|
||||
#### 应用演化后重新处理数据
|
||||
|
||||
在维护衍生数据时,批处理和流处理都是有用的。流处理允许将输入中的变化以低延迟反映在衍生视图中,而批处理允许重新处理大量累积的历史数据以便将新视图导出到现有数据集上。
|
||||
|
||||
特别是,重新处理现有数据为维护系统提供了一个良好的机制,演化并支持新功能和需求变更(参见[第4章](ch4.md))。不需要重新进行处理,模式演化仅限于简单的变化,例如向记录中添加新的可选字段或添加新类型的记录。无论是在写模式还是在读模式中都是如此(参阅“[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”)。另一方面,通过重新处理,可以将数据集重组为一个完全不同的模型,以便更好地满足新的要求。
|
||||
特别是,重新处理现有数据为维护系统、演化并支持新功能和需求变更提供了一个良好的机制(请参阅[第四章](ch4.md))。没有重新进行处理,模式演化将仅限于简单的变化,例如向记录中添加新的可选字段或添加新类型的记录。无论是在写时模式还是在读时模式中都是如此(请参阅“[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”)。另一方面,通过重新处理,可以将数据集重组为一个完全不同的模型,以便更好地满足新的要求。
|
||||
|
||||
> ### 铁路上的模式迁移
|
||||
>
|
||||
@ -125,49 +121,46 @@
|
||||
>
|
||||
> 在1846年最终确定了一个标准轨距之后,其他轨距的轨道必须转换 —— 但是如何在不停运火车线路的情况下进行数月甚至数年的迁移?解决的办法是首先通过添加第三条轨道将轨道转换为**双轨距(dual guage)**或**混合轨距**。这种转换可以逐渐完成,当完成时,两种轨距的列车都可以在线路上跑,使用三条轨道中的两条。事实上,一旦所有的列车都转换成标准轨距,那么可以移除提供非标准轨距的轨道。
|
||||
>
|
||||
> 以这种方式“再加工”现有的轨道,让新旧版本并存,可以在几年的时间内逐渐改变轨距。然而,这是一项昂贵的事业,这就是今天非标准轨距仍然存在的原因。例如,旧金山湾区的BART系统使用与美国大部分地区不同的轨距。
|
||||
> 以这种方式“再加工”现有的轨道,让新旧版本并存,可以在几年的时间内逐渐改变轨距。然而,这是一项昂贵的事业,这就是今天非标准轨距仍然存在的原因。例如,旧金山湾区的BART系统使用了与美国大部分地区不同的轨距。
|
||||
|
||||
衍生视图允许**渐进演化(gradual evolution)**。如果你想重新构建数据集,不需要执行迁移,例如**突然切换**。取而代之的是,你可以将旧架构和新架构并排维护为相同基础数据上的两个独立衍生视图。然后可以开始将少量用户转移到新视图,以测试其性能并发现任何错误,而大多数用户仍然会被路由到旧视图。你可以逐渐地增加访问新视图的用户比例,最终可以删除旧视图【10】。
|
||||
衍生视图允许**渐进演化(gradual evolution)**。如果你想重新构建数据集,不需要执行突然切换式的迁移。取而代之的是,你可以将旧架构和新架构并排维护为相同基础数据上的两个独立衍生视图。然后可以开始将少量用户转移到新视图,以测试其性能并发现任何错误,而大多数用户仍然会被路由到旧视图。你可以逐渐地增加访问新视图的用户比例,最终可以删除旧视图【10】。
|
||||
|
||||
这种逐渐迁移的美妙之处在于,如果出现问题,每个阶段的过程都很容易逆转:你始终有一个可以回滚的可用系统。通过降低不可逆损害的风险,你能对继续前进更有信心,从而更快地改善系统【11】。
|
||||
|
||||
#### Lambda架构
|
||||
|
||||
如果批处理用于重新处理历史数据,并且流处理用于处理最近的更新,那么如何将这两者结合起来?Lambda架构【12】是这方面的一个建议,引起了很多关注。
|
||||
如果批处理用于重新处理历史数据,而流处理用于处理最近的更新,那么如何将这两者结合起来?Lambda架构【12】是这方面的一个建议,引起了很多关注。
|
||||
|
||||
Lambda架构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件溯源(参阅“[事件溯源](ch11.md#事件溯源)”)。为了从这些事件中衍生出读取优化的视图, Lambda架构建议并行运行两个不同的系统:批处理系统(如Hadoop MapReduce)和独立的流处理系统(如Storm)。
|
||||
Lambda架构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件溯源(请参阅“[事件溯源](ch11.md#事件溯源)”)。为了从这些事件中衍生出读取优化的视图, Lambda架构建议并行运行两个不同的系统:批处理系统(如Hadoop MapReduce)和独立的流处理系统(如Storm)。
|
||||
|
||||
在Lambda方法中,流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成衍生视图的更正版本。这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的(请参阅“[故障容错]()”)。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。
|
||||
在Lambda方法中,流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成衍生视图的更正版本。这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的(请参阅“[容错](ch11.md#容错)”)。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。
|
||||
|
||||
Lambda架构是一种有影响力的想法,它将数据系统的设计变得更好,尤其是通过推广这样的原则:在不可变事件流上建立衍生视图,并在需要时重新处理事件。但是我也认为它有一些实际问题:
|
||||
|
||||
* 在批处理和流处理框架中维护相同的逻辑是很显著的额外工作。虽然像Summingbird 【13】这样的库提供了一种可以在批处理和流处理的上下文中运行的计算抽象。调试,调整和维护两个不同系统的操作复杂性依然存在【14】。
|
||||
* 在批处理和流处理框架中维护相同的逻辑是很显著的额外工作。虽然像Summingbird【13】这样的库提供了一种可以在批处理和流处理的上下文中运行的计算抽象。调试、调整和维护两个不同系统的操作复杂性依然存在【14】。
|
||||
* 由于流管道和批处理管道产生独立的输出,因此需要合并它们以响应用户请求。如果计算是基于滚动窗口的简单聚合,则合并相当容易,但如果视图基于更复杂的操作(例如连接和会话化)而导出,或者输出不是时间序列,则会变得非常困难。
|
||||
* 尽管有能力重新处理整个历史数据集是很好的,但在大型数据集上这样做经常会开销巨大。因此,批处理流水线通常需要设置为处理增量批处理(例如,在每小时结束时处理一小时的数据),而不是重新处理所有内容。这引发了“[关于时间的推理](ch11.md#关于时间的推理)”中讨论的问题,例如处理分段器和处理跨批次边界的窗口。增加批量计算会增加复杂性,使其更类似于流式传输层,这与保持批处理层尽可能简单的目标背道而驰。
|
||||
* 尽管有能力重新处理整个历史数据集是很好的,但在大型数据集上这样做经常会开销巨大。因此,批处理流水线通常需要设置为处理增量批处理(例如,在每小时结束时处理一小时的数据),而不是重新处理所有内容。这引发了“[时间推理](ch11.md#时间推理)”中讨论的问题,例如处理滞留事件和处理跨批次边界的窗口。增量化批处理计算会增加复杂性,使其更类似于流式传输层,这与保持批处理层尽可能简单的目标背道而驰。
|
||||
|
||||
#### 统一批处理和流处理
|
||||
|
||||
最近的工作使得Lambda架构的优点在没有其缺点的情况下得以实现,允许批处理计算(重新处理历史数据)和流计算(处理事件到达时)在同一个系统中实现【15】。
|
||||
|
||||
在一个系统中统一批处理和流处理需要以下功能,这些功能越来越广泛:
|
||||
|
||||
* 通过处理最近事件流的相同处理引擎来重放历史事件的能力。例如,基于日志的消息代理可以重放消息(参阅“[重放旧消息](ch11.md#重放旧消息)”),某些流处理器可以从HDFS等分布式文件系统读取输入。
|
||||
* 对于流处理器来说,恰好一次语义 —— 即确保输出与未发生故障的输出相同,即使事实上发生故障(参阅“[故障容错](ch11.md#故障容错)”)。与批处理一样,这需要丢弃任何失败任务的部分输出。
|
||||
* 按事件时间进行窗口化的工具,而不是按处理时间进行窗口化,因为处理历史事件时,处理时间毫无意义(参阅“[时间推理](ch11.md#时间推理)”)。例如,Apache Beam提供了用于表达这种计算的API,然后可以使用Apache Flink或Google Cloud Dataflow运行。
|
||||
|
||||
最近的工作使得Lambda架构的优点在没有其缺点的情况下得以实现,允许批处理计算(重新处理历史数据)和流计算(在事件到达时即处理)在同一个系统中实现【15】。
|
||||
|
||||
在一个系统中统一批处理和流处理需要以下功能,这些功能也正在越来越广泛地被提供:
|
||||
|
||||
* 通过处理最近事件流的相同处理引擎来重播历史事件的能力。例如,基于日志的消息代理可以重播消息(请参阅“[重播旧消息](ch11.md#重播旧消息)”),某些流处理器可以从HDFS等分布式文件系统读取输入。
|
||||
* 对于流处理器来说,恰好一次语义 —— 即确保输出与未发生故障的输出相同,即使事实上发生故障(请参阅“[容错](ch11.md#容错)”)。与批处理一样,这需要丢弃任何失败任务的部分输出。
|
||||
* 按事件时间进行窗口化的工具,而不是按处理时间进行窗口化,因为处理历史事件时,处理时间毫无意义(请参阅“[时间推理](ch11.md#时间推理)”)。例如,Apache Beam提供了用于表达这种计算的API,可以在Apache Flink或Google Cloud Dataflow使用。
|
||||
|
||||
|
||||
## 分拆数据库
|
||||
|
||||
在最抽象的层面上,数据库,Hadoop和操作系统都发挥相同的功能:它们存储一些数据,并允许你处理和查询这些数据【16】。数据库将数据存储为特定数据模型的记录(表中的行、文档、图中的顶点等),而操作系统的文件系统则将数据存储在文件中 —— 但其核心都是“信息管理”系统【17】。正如我们在[第10章](ch10.md)中看到的,Hadoop生态系统有点像Unix的分布式版本。
|
||||
在最抽象的层面上,数据库,Hadoop和操作系统都发挥相同的功能:它们存储一些数据,并允许你处理和查询这些数据【16】。数据库将数据存储为特定数据模型的记录(表中的行、文档、图中的顶点等),而操作系统的文件系统则将数据存储在文件中 —— 但其核心都是“信息管理”系统【17】。正如我们在[第十章](ch10.md)中看到的,Hadoop生态系统有点像Unix的分布式版本。
|
||||
|
||||
当然,有很多实际的差异。例如,许多文件系统都不能很好地处理包含1000万个小文件的目录,而包含1000万个小记录的数据库完全是寻常而不起眼的。无论如何,操作系统和数据库之间的相似之处和差异值得探讨。
|
||||
|
||||
Unix和关系数据库以非常不同的哲学来处理信息管理问题。 Unix认为它的目的是为程序员提供一种相当低层次的硬件的逻辑抽象,而关系数据库则希望为应用程序员提供一种高层次的抽象,以隐藏磁盘上数据结构的复杂性,并发性,崩溃恢复以及等等。 Unix发展出的管道和文件只是字节序列,而数据库则发展出了SQL和事务。
|
||||
Unix和关系数据库以非常不同的哲学来处理信息管理问题。Unix认为它的目的是为程序员提供一种相当低层次的硬件的逻辑抽象,而关系数据库则希望为应用程序员提供一种高层次的抽象,以隐藏磁盘上数据结构的复杂性,并发性,崩溃恢复等等。Unix发展出的管道和文件只是字节序列,而数据库则发展出了SQL和事务。
|
||||
|
||||
哪种方法更好?当然这取决于你想要的是什么。 Unix是“简单的”,因为它是硬件资源相当薄的包装;关系数据库是“更简单”的,因为一个简短的声明性查询可以利用很多强大的基础设施(查询优化,索引,连接方法,并发控制,复制等),而不需要查询的作者理解其实现细节。
|
||||
哪种方法更好?当然这取决于你想要的是什么。 Unix是“简单的”,因为它是对硬件资源相当薄的包装;关系数据库是“更简单”的,因为一个简短的声明性查询可以利用很多强大的基础设施(查询优化,索引,连接方法,并发控制,复制等),而不需要查询的作者理解其实现细节。
|
||||
|
||||
这些哲学之间的矛盾已经持续了几十年(Unix和关系模型都出现在70年代初),仍然没有解决。例如,我将NoSQL运动解释为,希望将类Unix的低级别抽象方法应用于分布式OLTP数据存储的领域。
|
||||
|
||||
@ -177,12 +170,12 @@
|
||||
|
||||
在本书的过程中,我们讨论了数据库提供的各种功能及其工作原理,其中包括:
|
||||
|
||||
* 次级索引,使您可以根据字段的值有效地搜索记录(参阅“[其他索引结构](ch3.md#其他索引结构)”)
|
||||
* 物化视图,这是一种预计算的查询结果缓存(参阅“[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”)
|
||||
* 复制日志,保持其他节点上数据的副本最新(参阅“[复制日志的实现](ch5.md#复制日志的实现)”)
|
||||
* 全文搜索索引,允许在文本中进行关键字搜索(参见“[全文搜索和模糊索引](ch3.md#全文搜索和模糊索引)”)内置于某些关系数据库【1】
|
||||
* 次级索引,使您可以根据字段的值有效地搜索记录(请参阅“[其他索引结构](ch3.md#其他索引结构)”)
|
||||
* 物化视图,这是一种预计算的查询结果缓存(请参阅“[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”)
|
||||
* 复制日志,保持其他节点上数据的副本最新(请参阅“[复制日志的实现](ch5.md#复制日志的实现)”)
|
||||
* 全文搜索索引,允许在文本中进行关键字搜索(请参阅“[全文搜索和模糊索引](ch3.md#全文搜4索和模糊索引)”),也内置于某些关系数据库【1】
|
||||
|
||||
在[第10章](ch10.md)和[第11章](ch11.md)中,出现了类似的主题。我们讨论了如何构建全文搜索索引(请参阅第357页上的“[批处理工作流的输出]()”),了解有关实例化视图维护(请参阅“[维护实例化视图]()”一节第437页)以及有关将变更从数据库复制到衍生数据系统(请参阅第454页的“[变更数据捕获]()”)。
|
||||
在[第十章](ch10.md)和[第十一章](ch11.md)中,出现了类似的主题。我们讨论了如何构建全文搜索索引(请参阅“[批处理工作流的输出](ch10.md#批处理工作流的输出)”),了解了如何维护物化视图(请参阅“[维护物化视图](ch11.md#维护物化视图)”)以及如何将变更从数据库复制到衍生数据系统(请参阅“[变更数据捕获](ch11.md#变更数据捕获)”)。
|
||||
|
||||
数据库中内置的功能与人们用批处理和流处理器构建的衍生数据系统似乎有相似之处。
|
||||
|
||||
@ -190,70 +183,70 @@
|
||||
|
||||
想想当你运行`CREATE INDEX`在关系数据库中创建一个新的索引时会发生什么。数据库必须扫描表的一致性快照,挑选出所有被索引的字段值,对它们进行排序,然后写出索引。然后它必须处理自一致快照以来所做的写入操作(假设表在创建索引时未被锁定,所以写操作可能会继续)。一旦完成,只要事务写入表中,数据库就必须继续保持索引最新。
|
||||
|
||||
此过程非常类似于设置新的从库副本(参阅“[设置新的追随者]()”),也非常类似于流处理系统中的**引导(bootstrap)** 变更数据捕获(请参阅第455页的“[初始快照]()”)。
|
||||
此过程非常类似于设置新的从库副本(请参阅“[设置新从库](ch5.md#设置新从库)”),也非常类似于流处理系统中的**引导(bootstrap)** 变更数据捕获(请参阅“[初始快照](ch11.md#初始快照)”)。
|
||||
|
||||
无论何时运行`CREATE INDEX`,数据库都会重新处理现有数据集(如第494页的“[重新处理应用程序数据的演变数据]()”中所述),并将该索引作为新视图导出到现有数据上。现有数据可能是状态的快照,而不是所有发生变化的日志,但两者密切相关(请参阅“[状态,数据流和不变性]()”第459页)。
|
||||
无论何时运行`CREATE INDEX`,数据库都会重新处理现有数据集(如“[应用演化后重新处理数据](#应用演化后重新处理数据)”中所述),并将该索引作为新视图导出到现有数据上。现有数据可能是状态的快照,而不是所有发生变化的日志,但两者密切相关(请参阅“[状态、流和不变性](ch11.md#状态、流和不变性)”)。
|
||||
|
||||
#### 一切的元数据库
|
||||
|
||||
有鉴于此,我认为整个组织的数据流开始像一个巨大的数据库【7】。每当批处理,流或ETL过程将数据从一个地方传输到另一个地方并组装时,它表现地就像数据库子系统一样,使索引或物化视图保持最新。
|
||||
有鉴于此,我认为整个组织的数据流开始像一个巨大的数据库【7】。每当批处理、流或ETL过程将数据从一个地方传输到另一个地方并组装时,它表现地就像数据库子系统一样,使索引或物化视图保持最新。
|
||||
|
||||
从这种角度来看,批处理和流处理器就像触发器,存储过程和物化视图维护例程的精细实现。它们维护的衍生数据系统就像不同的索引类型。例如,关系数据库可能支持B树索引,散列索引,空间索引(请参阅第79页的“[多列索引]()”)以及其他类型的索引。在新兴的衍生数据系统架构中,不是将这些设施作为单个集成数据库产品的功能实现,而是由各种不同的软件提供,运行在不同的机器上,由不同的团队管理。
|
||||
从这种角度来看,批处理和流处理器就像精心实现的触发器、存储过程和物化视图维护例程。它们维护的衍生数据系统就像不同的索引类型。例如,关系数据库可能支持B树索引、散列索引、空间索引(请参阅“[多列索引](ch3.md#多列索引)”)以及其他类型的索引。在新兴的衍生数据系统架构中,不是将这些设施作为单个集成数据库产品的功能实现,而是由各种不同的软件提供,运行在不同的机器上,由不同的团队管理。
|
||||
|
||||
这些发展在未来将会把我们带到哪里?如果我们从没有适合所有访问模式的单一数据模型或存储格式的前提出发,我推测有两种途径可以将不同的存储和处理工具组合成一个有凝聚力的系统:
|
||||
|
||||
**联合数据库:统一读取**
|
||||
|
||||
可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 —— 一种称为**联合数据库(federated database)** 或**多态存储(polystore)** 的方法【18,19】。例如,PostgreSQL的外部数据包装器功能符合这种模式【20】。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。
|
||||
可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 —— 一种称为**联合数据库(federated database)**或**多态存储(polystore)**的方法【18,19】。例如,PostgreSQL的**外部数据包装器(foreign data wrapper)**功能符合这种模式【20】。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。
|
||||
|
||||
联合查询接口遵循着单一集成系统与关系型模型的传统,带有高级查询语言和优雅的语义,但实现起来非常复杂。
|
||||
联合查询接口遵循着单一集成系统的关系型传统,带有高级查询语言和优雅的语义,但实现起来非常复杂。
|
||||
|
||||
**分拆数据库:统一写入**
|
||||
|
||||
虽然联合能解决跨多个不同系统的只读查询问题,但它并没有很好的解决跨系统**同步**写入的问题。我们说过,在单个数据库中,创建一致的索引是一项内置功能。当我们构建多个存储系统时,我们同样需要确保所有数据变更都会在所有正确的位置结束,即使在出现故障时也是如此。将存储系统可靠地插接在一起(例如,通过变更数据捕获和事件日志)更容易,就像将数据库的索引维护功能以可以跨不同技术同步写入的方式分开【7,21】。
|
||||
虽然联合能解决跨多个不同系统的只读查询问题,但它并没有很好的解决跨系统**同步**写入的问题。我们说过,在单个数据库中,创建一致的索引是一项内置功能。当我们构建多个存储系统时,我们同样需要确保所有数据变更都会在所有正确的位置结束,即使在出现故障时也是如此。想要更容易地将存储系统可靠地插接在一起(例如,通过变更数据捕获和事件日志),就像将数据库的索引维护功能以可以跨不同技术同步写入的方式分开【7,21】。
|
||||
|
||||
分拆方法遵循Unix传统的小型工具,它可以很好地完成一件事【22】,通过统一的低级API(管道)进行通信,并且可以使用更高级的语言进行组合(shell)【16】 。
|
||||
分拆方法遵循Unix传统的小型工具,它可以很好地完成一件事【22】,通过统一的低层级API(管道)进行通信,并且可以使用更高层级的语言进行组合(shell)【16】 。
|
||||
|
||||
#### 开展分拆工作
|
||||
|
||||
联合和分拆是一个硬币的两面:用不同的组件构成可靠,可伸缩和可维护的系统。联合只读查询需要将一个数据模型映射到另一个数据模型,这需要一些思考,但最终还是一个可解决的问题。我认为同步写入到几个存储系统是更困难的工程问题,所以我将重点关注它。
|
||||
联合和分拆是一个硬币的两面:用不同的组件构成可靠、 可伸缩和可维护的系统。联合只读查询需要将一个数据模型映射到另一个数据模型,这需要一些思考,但最终还是一个可解决的问题。而我认为同步写入到几个存储系统是更困难的工程问题,所以我将重点关注它。
|
||||
|
||||
传统的同步写入方法需要跨异构存储系统的分布式事务【18】,我认为这是错误的解决方案(请参阅“[导出的数据与分布式事务]()”第495页)。单个存储或流处理系统内的事务是可行的,但是当数据跨越不同技术之间的边界时,我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法。
|
||||
传统的同步写入方法需要跨异构存储系统的分布式事务【18】,我认为这是错误的解决方案(请参阅“[衍生数据与分布式事务](#衍生数据与分布式事务)”)。单个存储或流处理系统内的事务是可行的,但是当数据跨越不同技术之间的边界时,我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法。
|
||||
|
||||
例如,分布式事务在某些流处理组件内部使用,以匹配**恰好一次(exactly-once)** 语义(请参阅第477页的“[重新访问原子提交]()”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理组件写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的事件的有序事件日志(参见第478页的“[幂等性]()”)是一种更简单的抽象,因此在异构系统中实现更加可行【7】。
|
||||
例如,分布式事务在某些流处理组件内部使用,以匹配**恰好一次(exactly-once)** 语义(请参阅“[原子提交再现](ch11.md#原子提交再现)”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理组件写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的有序事件日志(请参阅“[幂等性](ch11.md#幂等性)”)是一种更简单的抽象,因此在异构系统中实现更加可行【7】。
|
||||
|
||||
基于日志的集成的一大优势是各个组件之间的**松散耦合(loose coupling)**,这体现在两个方面:
|
||||
|
||||
1. 在系统级别,异步事件流使整个系统对各个组件的中断或性能下降更加稳健。如果使用者运行缓慢或失败,那么事件日志可以缓冲消息(请参阅“[磁盘空间使用情况]()”第369页),以便生产者和任何其他使用者可以继续不受影响地运行。有问题的消费者可以在固定时赶上,因此不会错过任何数据,并且包含故障。相比之下,分布式事务的同步交互往往会将本地故障升级为大规模故障(请参见第363页的“[分布式事务的限制]()”)。
|
||||
1. 在系统级别,异步事件流使整个系统在个别组件的中断或性能下降时更加稳健。如果消费者运行缓慢或失败,那么事件日志可以缓冲消息(请参阅“[磁盘空间使用](ch11.md#磁盘空间使用)”),以便生产者和任何其他消费者可以继续不受影响地运行。有问题的消费者可以在问题修复后赶上,因此不会错过任何数据,并且包含故障。相比之下,分布式事务的同步交互往往会将本地故障升级为大规模故障(请参阅“[分布式事务的限制](ch9.md#分布式事务的限制)”)。
|
||||
2. 在人力方面,分拆数据系统允许不同的团队独立开发,改进和维护不同的软件组件和服务。专业化使得每个团队都可以专注于做好一件事,并与其他团队的系统以明确的接口交互。事件日志提供了一个足够强大的接口,以捕获相当强的一致性属性(由于持久性和事件的顺序),但也足够普适于几乎任何类型的数据。
|
||||
|
||||
#### 分拆系统vs集成系统
|
||||
|
||||
如果分拆确实成为未来的方式,它也不会取代目前形式的数据库 —— 它们仍然会像以往一样被需要。为了维护流处理组件中的状态,数据库仍然是需要的,并且为批处理和流处理器的输出提供查询服务(参阅“[批处理工作流的输出](ch10.md#批处理工作流的输出)”与“[流处理](ch11.md#流处理)”)。专用查询引擎对于特定的工作负载仍然非常重要:例如,MPP数据仓库中的查询引擎针对探索性分析查询进行了优化,并且能够很好地处理这种类型的工作负载(参阅“[对比Hadoop与分布式数据库](ch10.md#对比Hadoop与分布式数据库)” 。
|
||||
如果分拆确实成为未来的方式,它也不会取代目前形式的数据库 —— 它们仍然会像以往一样被需要。为了维护流处理组件中的状态,数据库仍然是需要的,并且为批处理和流处理器的输出提供查询服务(请参阅“[批处理工作流的输出](ch10.md#批处理工作流的输出)”与“[流处理](ch11.md#流处理)”)。专用查询引擎对于特定的工作负载仍然非常重要:例如,MPP数据仓库中的查询引擎针对探索性分析查询进行了优化,并且能够很好地处理这种类型的工作负载(请参阅“[Hadoop与分布式数据库的对比](ch10.md#Hadoop与分布式数据库的对比)” 。
|
||||
|
||||
运行几种不同基础设施的复杂性可能是一个问题:每种软件都有一个学习曲线,配置问题和操作怪癖,因此部署尽可能少的移动部件是很有必要的。比起使用应用代码拼接多个工具而成的系统,单一集成软件产品也可以在其设计应对的工作负载类型上实现更好,更可预测的性能【23】。正如在前言中所说的那样,为了不需要的规模而构建系统是白费精力,而且可能会将你锁死在一个不灵活的设计中。实际上,这是一种过早优化的形式。
|
||||
运行几种不同基础设施的复杂性可能是一个问题:每种软件都有一个学习曲线,配置问题和操作怪癖,因此部署尽可能少的移动部件是很有必要的。比起使用应用代码拼接多个工具而成的系统,单一集成软件产品也可以在其设计应对的工作负载类型上实现更好、更可预测的性能【23】。正如在前言中所说的那样,为了不需要的规模而构建系统是白费精力,而且可能会将你锁死在一个不灵活的设计中。实际上,这是一种过早优化的形式。
|
||||
|
||||
分拆的目标不是要针对个别数据库与特定工作负载的性能进行竞争;我们的目标是允许您结合多个不同的数据库,以便在比单个软件可能实现的更广泛的工作负载范围内实现更好的性能。这是关于广度,而不是深度 —— 与我们在“[对比Hadoop与分布式数据库](ch10.md#对比Hadoop与分布式数据库)”中讨论的存储和处理模型的多样性一样。
|
||||
分拆的目标不是要针对个别数据库与特定工作负载的性能进行竞争;我们的目标是允许您结合多个不同的数据库,以便在比单个软件可能实现的更广泛的工作负载范围内实现更好的性能。这是关于广度,而不是深度 —— 与我们在“[Hadoop与分布式数据库的对比](ch10.md#Hadoop与分布式数据库的对比)”中讨论的存储和处理模型的多样性一样。
|
||||
|
||||
因此,如果有一项技术可以满足您的所有需求,那么最好使用该产品,而不是试图用低级组件重新实现它。只有当没有单一软件满足您的所有需求时,才会出现拆分和联合的优势。
|
||||
因此,如果有一项技术可以满足您的所有需求,那么最好使用该产品,而不是试图用更低层级的组件重新实现它。只有当没有单一软件满足您的所有需求时,才会出现拆分和联合的优势。
|
||||
|
||||
#### 少了什么?
|
||||
|
||||
用于组成数据系统的工具正在变得越来越好,但我认为还缺少一个主要的东西:我们还没有与Unix shell类似的分拆数据库(即,一种声明式的,简单的,用于组装存储和处理系统的高级语言)。
|
||||
用于组成数据系统的工具正在变得越来越好,但我认为还缺少一个主要的东西:我们还没有与Unix shell类似的分拆数据库等价物(即,一种声明式的、简单的、用于组装存储和处理系统的高级语言)。
|
||||
|
||||
例如,如果我们可以简单地声明`mysql |elasticsearch`,类似于Unix管道【22】,成为`CREATE INDEX`的分拆等价物:它将MySQL数据库中的所有文档并将其索引到Elasticsearch集群中。然后它会不断捕获对数据库所做的所有变更,并自动将它们应用于搜索索引,而无需编写自定义应用代码。这种集成应当支持几乎任何类型的存储或索引系统。
|
||||
例如,如果我们可以简单地声明`mysql | elasticsearch`,类似于Unix管道【22】,成为`CREATE INDEX`的分拆等价物:它将读取MySQL数据库中的所有文档并将其索引到Elasticsearch集群中。然后它会不断捕获对数据库所做的所有变更,并自动将它们应用于搜索索引,而无需编写自定义应用代码。这种集成应当支持几乎任何类型的存储或索引系统。
|
||||
|
||||
同样,能够更容易地预先计算和更新缓存将是一件好事。回想一下,物化视图本质上是一个预先计算的缓存,所以您可以通过为复杂查询声明指定物化视图来创建缓存,包括图上的递归查询(参阅“[图数据模型](ch2.md#图数据模型)”)和应用逻辑。在这方面有一些有趣的早期研究,如**差分数据流(differential dataflow)**【24,25】,我希望这些想法能够在生产系统中找到自己的方法。
|
||||
同样,能够更容易地预先计算和更新缓存将是一件好事。回想一下,物化视图本质上是一个预先计算的缓存,所以您可以通过为复杂查询声明指定物化视图来创建缓存,包括图上的递归查询(请参阅“[图数据模型](ch2.md#图数据模型)”)和应用逻辑。在这方面有一些有趣的早期研究,如**差分数据流(differential dataflow)**【24,25】,我希望这些想法能够在生产系统中找到自己的方法。
|
||||
|
||||
### 围绕数据流设计应用
|
||||
|
||||
使用应用代码组合专用存储与处理系统来分拆数据库的方法,也被称为“**数据库由内而外**”方法【26】,在我在2014年的一次会议演讲标题之后【27】。然而称它为“新架构”过于宏大。我将其看作是一种设计模式,一个讨论的起点,我们只是简单地给它起一个名字,以便我们能更好地讨论它。
|
||||
使用应用代码组合专用存储与处理系统来分拆数据库的方法,也被称为“**数据库由内而外(database inside-out)**”方法【26】,该名称来源于我在2014年的一次会议演讲标题【27】。然而称它为“新架构”过于夸大,我仅将其看作是一种设计模式,一个讨论的起点,我们只是简单地给它起一个名字,以便我们能更好地讨论它。
|
||||
|
||||
这些想法不是我的;它们是很多人的思想的融合,这些思想非常值得我们学习。尤其是,以Oz 【28】和Juttle 【29】为代表的数据流语言,以Elm【30,31】为代表的**函数式响应式编程(functional reactive programming, FRP)**,以Bloom【32】为代表的逻辑编程语言。在这一语境中的术语**分拆(unbundling)** 是由Jay Kreps 提出的【7】。
|
||||
这些想法不是我的;它们是很多人的思想的融合,这些思想非常值得我们学习。尤其是,以Oz【28】和Juttle【29】为代表的数据流语言,以Elm【30,31】为代表的**函数式响应式编程(functional reactive programming, FRP)**,以Bloom【32】为代表的逻辑编程语言。在这一语境中的术语**分拆(unbundling)** 是由Jay Kreps 提出的【7】。
|
||||
|
||||
即使是**电子表格**也在数据流编程能力上甩开大多数主流编程语言几条街【33】。在电子表格中,可以将公式放入一个单元格中(例如,另一列中的单元格求和值),并且只要公式的任何输入发生变更,公式的结果都会自动重新计算。这正是我们在数据系统层次所需要的:当数据库中的记录发生变更时,我们希望自动更新该记录的任何索引,并且自动刷新依赖于记录的任何缓存视图或聚合。您不必担心这种刷新如何发生的技术细节,但能够简单地相信它可以正常工作。
|
||||
即使是**电子表格**也在数据流编程能力上甩开大多数主流编程语言几条街【33】。在电子表格中,可以将公式放入一个单元格中(例如,对另一列中的单元格求和),并且只要公式的任何输入发生变更,公式的结果都会自动重新计算。这正是我们在数据系统层次所需要的:当数据库中的记录发生变更时,我们希望自动更新该记录的任何索引,并且自动刷新依赖于记录的任何缓存视图或聚合。您不必担心这种刷新如何发生的技术细节,但能够简单地相信它可以正常工作。
|
||||
|
||||
因此,我认为绝大多数数据系统仍然可以从VisiCalc在1979年已经具备的功能中学习【34】。与电子表格的不同之处在于,今天的数据系统需要具有容错性,可伸缩性以及持久存储数据。它们还需要能够整合不同人群编写的不同技术,并重用现有的库和服务:期望使用某种特定语言,框架或工具开发所有软件是不切实际的。
|
||||
因此,我认为绝大多数数据系统仍然可以从VisiCalc在1979年已经具备的功能中学习【34】。与电子表格的不同之处在于,今天的数据系统需要具有容错性,可伸缩性以及持久存储数据。它们还需要能够整合不同人群编写的不同技术,并重用现有的库和服务:期望使用某一种特定的语言、框架或工具来开发所有软件是不切实际的。
|
||||
|
||||
在本节中,我将详细介绍这些想法,并探讨一些围绕分拆数据库和数据流的想法构建应用的方法。
|
||||
|
||||
@ -261,21 +254,24 @@
|
||||
|
||||
当一个数据集衍生自另一个数据集时,它会经历某种转换函数。例如:
|
||||
|
||||
* 次级索引是由一种直白的转换函数生成的衍生数据集:对于基础表中的每行或每个文档,它挑选被索引的列或字段中的值,并按这些值排序(假设使用B树或SSTable索引,按键排序,如[第3章](ch3.md)所述)。
|
||||
* 全文搜索索引是通过应用各种自然语言处理函数而创建的,诸如语言检测,分词,词干或词汇化,拼写纠正和同义词识别)创建全文搜索索引,然后构建用于高效查找的数据结构(例如倒排索引)。
|
||||
* 在机器学习系统中,我们可以将模型视作从训练数据通过应用各种特征提取,统计分析函数衍生的数据,当模型应用于新的输入数据时,模型的输出是从输入和模型(因此间接地从训练数据)中衍生的。
|
||||
* 次级索引是由一种直白的转换函数生成的衍生数据集:对于基础表中的每行或每个文档,它挑选被索引的列或字段中的值,并按这些值排序(假设使用B树或SSTable索引,按键排序,如[第三章](ch3.md)所述)。
|
||||
* 全文搜索索引是通过应用各种自然语言处理函数而创建的,诸如语言检测、分词、词干或词汇化、拼写纠正和同义词识别,然后构建用于高效查找的数据结构(例如倒排索引)。
|
||||
* 在机器学习系统中,我们可以将模型视作从训练数据通过应用各种特征提取、统计分析函数衍生的数据,当模型应用于新的输入数据时,模型的输出是从输入和模型(因此间接地从训练数据)中衍生的。
|
||||
* 缓存通常包含将以用户界面(UI)显示的形式的数据聚合。因此填充缓存需要知道UI中引用的字段;UI中的变更可能需要更新缓存填充方式的定义,并重建缓存。
|
||||
|
||||
用于次级索引的衍生函数是如此常用的需求,以致于它作为核心功能被内建至许多数据库中,你可以简单地通过`CREATE INDEX`来调用它。对于全文索引,常见语言的基本语言特征可能内置到数据库中,但更复杂的特征通常需要领域特定的调整。在机器学习中,特征工程是众所周知的特定于应用的特征,通常需要包含很多关于用户交互与应用部署的详细知识【35】。
|
||||
当创建衍生数据集的函数不是像创建二级索引那样的标准搬砖函数时,需要自定义代码来处理特定于应用的东西。而这个自定义代码是让许多数据库挣扎的地方,虽然关系数据库通常支持触发器,存储过程和用户定义的函数,它们可以用来在数据库中执行应用代码,但它们有点像数据库设计里的事后反思。(参阅“[传输事件流](ch11.md#传输事件流)”)。
|
||||
用于次级索引的衍生函数是如此常用的需求,以致于它作为核心功能被内建至许多数据库中,你可以简单地通过`CREATE INDEX`来调用它。对于全文索引,常见语言的基本语言特征可能内置到数据库中,但更复杂的特征通常需要领域特定的调整。在机器学习中,特征工程是众所周知的特定于应用的特征,通常需要包含很多关于用户交互与应用部署的详细知识【35】。
|
||||
|
||||
当创建衍生数据集的函数不是像创建二级索引那样的标准搬砖函数时,需要自定义代码来处理特定于应用的东西。而这个自定义代码是让许多数据库挣扎的地方,虽然关系数据库通常支持触发器、存储过程和用户定义的函数,可以用它们来在数据库中执行应用代码,但它们有点像数据库设计里的事后反思。(请参阅“[传递事件流](ch11.md#传递事件流)”)。
|
||||
|
||||
#### 应用代码和状态的分离
|
||||
|
||||
理论上,数据库可以是任意应用代码的部署环境,就如同操作系统一样。然而实践中它们对这一目标适配的很差。它们不满足现代应用开发的要求,例如依赖性和软件包管理,版本控制,滚动升级,可演化性,监控,指标,对网络服务的调用以及与外部系统的集成。
|
||||
理论上,数据库可以是任意应用代码的部署环境,就如同操作系统一样。然而实践中它们对这一目标适配的很差。它们不满足现代应用开发的要求,例如依赖和软件包管理、版本控制、滚动升级、可演化性、监控、指标、对网络服务的调用以及与外部系统的集成。
|
||||
|
||||
另一方面,Mesos,YARN,Docker,Kubernetes等部署和集群管理工具专为运行应用代码而设计。通过专注于做好一件事情,他们能够做得比将数据库作为其众多功能之一执行用户定义的功能要好得多。我认为让系统的某些部分专门用于持久数据存储以及专门运行应用程序代码的其他部分是有意义的。这两者可以在保持独立的同时互动。
|
||||
另一方面,Mesos,YARN,Docker,Kubernetes等部署和集群管理工具专为运行应用代码而设计。通过专注于做好一件事情,他们能够做得比将数据库作为其众多功能之一执行用户定义的功能要好得多。
|
||||
|
||||
现在大多数Web应用程序都是作为无状态服务部署的,其中任何用户请求都可以路由到任何应用程序服务器,并且服务器在发送响应后会忘记所有请求。这种部署方式很方便,因为可以随意添加或删除服务器,但状态必须到某个地方:通常是数据库。趋势是将无状态应用程序逻辑与状态管理(数据库)分开:不将应用程序逻辑放入数据库中,也不将持久状态置于应用程序中【36】。正如职能规划界人士喜欢开玩笑说的那样,“我们相信**教会(Church)** 与**国家(state)** 的分离”【37】 [^i]
|
||||
我认为让系统的某些部分专门用于持久数据存储并让其他部分专门运行应用程序代码是有意义的。这两者可以在保持独立的同时互动。
|
||||
|
||||
现在大多数Web应用程序都是作为无状态服务部署的,其中任何用户请求都可以路由到任何应用程序服务器,并且服务器在发送响应后会忘记所有请求。这种部署方式很方便,因为可以随意添加或删除服务器,但状态必须到某个地方:通常是数据库。趋势是将无状态应用程序逻辑与状态管理(数据库)分开:不将应用程序逻辑放入数据库中,也不将持久状态置于应用程序中【36】。正如函数式编程社区喜欢开玩笑说的那样,“我们相信**教会(Church)** 与**国家(state)** 的分离”【37】 [^i]
|
||||
|
||||
[^i]: 解释笑话很少会让人感觉更好,但我不想让任何人感到被遗漏。 在这里,Church指代的是数学家的阿隆佐·邱奇,他创立了lambda演算,这是计算的早期形式,是大多数函数式编程语言的基础。 lambda演算不具有可变状态(即没有变量可以被覆盖),所以可以说可变状态与Church的工作是分离的。
|
||||
|
||||
@ -283,52 +279,47 @@
|
||||
|
||||
但是,在大多数编程语言中,你无法订阅可变变量中的变更 —— 你只能定期读取它。与电子表格不同,如果变量的值发生变化,变量的读者不会收到通知。 (你可以在自己的代码中实现这样的通知 —— 这被称为**观察者模式** —— 但大多数语言没有将这种模式作为内置功能。)
|
||||
|
||||
数据库继承了这种可变数据的被动方法:如果你想知道数据库的内容是否发生了变化,通常你唯一的选择就是轮询(即定期重复你的查询)。 订阅变更只是刚刚开始出现的功能(参阅“[变更流的API支持](ch11.md#变更流的API支持)”)。
|
||||
数据库继承了这种可变数据的被动方法:如果你想知道数据库的内容是否发生了变化,通常你唯一的选择就是轮询(即定期重复你的查询)。 订阅变更只是刚刚开始出现的功能(请参阅“[变更流的API支持](ch11.md#变更流的API支持)”)。
|
||||
|
||||
#### 数据流:应用代码与状态变化的交互
|
||||
|
||||
从数据流的角度思考应用,意味着重新协调应用代码和状态管理之间的关系。将数据库视作被应用操纵的被动变量,取而代之的是更多地考虑状态,状态变更和处理它们的代码之间的相互作用与协同关系。应用代码通过在另一个地方触发状态变更来响应状态变更。
|
||||
从数据流的角度思考应用程序,意味着重新协调应用代码和状态管理之间的关系。我们不再将数据库视作被应用操纵的被动变量,取而代之的是更多地考虑状态,状态变更和处理它们的代码之间的相互作用与协同关系。应用代码通过在另一个地方触发状态变更来响应状态变更。
|
||||
|
||||
我们在“[数据库与流](ch11.md#数据库与流)”中看到了这一思路,我们讨论了将数据库的变更日志视为一种我们可以订阅的事件流。诸如Actor的消息传递系统(参阅“[消息传递数据流](ch4.md#消息传递数据流)”)也具有响应事件的概念。早在20世纪80年代,**元组空间(tuple space)** 模型就已经探索了表达分布式计算的方式:观察状态变更并作出反应【38,39】。
|
||||
我们在“[数据库与流](ch11.md#数据库与流)”中看到了这一思路,我们讨论了将数据库的变更日志视为一种我们可以订阅的事件流。诸如Actor的消息传递系统(请参阅“[消息传递中的数据流 ](ch4.md#消息传递中的数据流)”)也具有响应事件的概念。早在20世纪80年代,**元组空间(tuple space)** 模型就已经探索了表达分布式计算的方式:观察状态变更并作出反应的过程【38,39】。
|
||||
|
||||
如前所述,当触发器由于数据变更而被触发时,或次级索引更新以反映索引表中的变更时,数据库内部也发生着类似的情况。分拆数据库意味着将这个想法应用于在主数据库之外,用于创建衍生数据集:缓存,全文搜索索引,机器学习或分析系统。我们可以为此使用流处理和消息传递系统。
|
||||
|
||||
需要记住的重要一点是,维护衍生数据不同于执行异步任务。传统的消息传递系统通常是为执行异步任务设计的(参阅“[与传统消息传递相比的日志](ch11.md#与传统消息传递相比的日志)”):
|
||||
|
||||
* 在维护衍生数据时,状态变更的顺序通常很重要(如果多个视图是从事件日志衍生的,则需要按照相同的顺序处理事件,以便它们之间保持一致)。如“[确认与重传](ch11.md#确认与重传)”中所述,许多消息代理在重传未确认消息时没有此属性,双写也被排除在外(参阅“[保持系统同步](ch11.md#保持系统同步)”)。
|
||||
如前所述,当触发器由于数据变更而被触发时,或次级索引更新以反映索引表中的变更时,数据库内部也发生着类似的情况。分拆数据库意味着将这个想法应用于在主数据库之外,用于创建衍生数据集:缓存、全文搜索索引、机器学习或分析系统。我们可以为此使用流处理和消息传递系统。
|
||||
|
||||
需要记住的重要一点是,维护衍生数据不同于执行异步任务。传统的消息传递系统通常是为执行异步任务设计的(请参阅“[日志与传统的消息传递相比](ch11.md#日志与传统的消息传递相比)”):
|
||||
|
||||
* 在维护衍生数据时,状态变更的顺序通常很重要(如果多个视图是从事件日志衍生的,则需要按照相同的顺序处理事件,以便它们之间保持一致)。如“[确认与重新传递](ch11.md#确认与重新传递)”中所述,许多消息代理在重传未确认消息时没有此属性,双写也被排除在外(请参阅“[保持系统同步](ch11.md#保持系统同步)”)。
|
||||
* 容错是衍生数据的关键:仅仅丢失单个消息就会导致衍生数据集永远与其数据源失去同步。消息传递和衍生状态更新都必须可靠。例如,许多Actor系统默认在内存中维护Actor的状态和消息,所以如果运行Actor的机器崩溃,状态和消息就会丢失。
|
||||
|
||||
|
||||
稳定的消息排序和容错消息处理是相当严格的要求,但与分布式事务相比,它们开销更小,运行更稳定。现代流处理组件可以提供这些排序和可靠性保证,并允许应用代码以流算子的形式运行。
|
||||
|
||||
这些应用代码可以执行任意处理,包括数据库内置衍生函数通常不提供的功能。就像通过管道链接的Unix工具一样,流算子可以围绕着数据流构建大型系统。每个算子接受状态变更的流作为输入,并产生其他状态变化的流作为输出。
|
||||
|
||||
#### 流处理器和服务
|
||||
|
||||
当今流行的应用开发风格涉及将功能分解为一组通过同步网络请求(如REST API)进行通信的**服务(service)**(参阅“[通过服务实现数据流:REST和RPC](ch4.md#通过服务实现数据流:REST和RPC)”)。这种面向服务的架构优于单一庞大应用的优势主要在于:通过松散耦合来提供组织上的可伸缩性:不同的团队可以专职于不同的服务上,从而减少团队之间的协调工作(因为服务可以独立部署和更新)。
|
||||
当今流行的应用开发风格涉及将功能分解为一组通过同步网络请求(如REST API)进行通信的**服务(service)**(请参阅“[服务中的数据流:REST与RPC](ch4.md#服务中的数据流:REST与RPC)”)。这种面向服务的架构优于单一庞大应用的优势主要在于:通过松散耦合来提供组织上的可伸缩性:不同的团队可以专职于不同的服务上,从而减少团队之间的协调工作(因为服务可以独立部署和更新)。
|
||||
|
||||
在数据流中组装流算子与微服务方法有很多相似之处【40】。但底层通信机制是有很大区别:数据流采用单向异步消息流,而不是同步的请求/响应式交互。
|
||||
|
||||
除了在“[消息传递数据流](ch4.md#消息传递数据流)”中列出的优点(如更好的容错性),数据流系统还能实现更好的性能。例如,假设客户正在购买以一种货币定价,但以另一种货币支付的商品。为了执行货币换算,你需要知道当前的汇率。这个操作可以通过两种方式实现【40,41】:
|
||||
除了在“[消息传递中的数据流](ch4.md#消息传递中的数据流)”中列出的优点(如更好的容错性),数据流系统还能实现更好的性能。例如,假设客户正在购买以一种货币定价,但以另一种货币支付的商品。为了执行货币换算,你需要知道当前的汇率。这个操作可以通过两种方式实现【40,41】:
|
||||
|
||||
1. 在微服务方法中,处理购买的代码可能会查询汇率服务或数据库,以获取特定货币的当前汇率。
|
||||
2. 在数据流方法中,处理订单的代码会提前订阅汇率变更流,并在汇率发生变动时将当前汇率存储在本地数据库中。处理订单时只需查询本地数据库即可。
|
||||
|
||||
|
||||
|
||||
第二种方法能将对另一服务的同步网络请求替换为对本地数据库的查询(可能在同一台机器甚至同一个进程中)[^ii]。数据流方法不仅更快,而且当其他服务失效时也更稳健。最快且最可靠的网络请求就是压根没有网络请求!我们现在不再使用RPC,而是在购买事件和汇率更新事件之间建立流联接(参阅“[流表联接](ch11.md#流表联接)”)。
|
||||
第二种方法能将对另一服务的同步网络请求替换为对本地数据库的查询(可能在同一台机器甚至同一个进程中)[^ii]。数据流方法不仅更快,而且当其他服务失效时也更稳健。最快且最可靠的网络请求就是压根没有网络请求!我们现在不再使用RPC,而是在购买事件和汇率更新事件之间建立流联接(请参阅“[流表连接(流扩充)](ch11.md#流表连接(流扩充))”)。
|
||||
|
||||
[^ii]: 在微服务方法中,你也可以通过在处理购买的服务中本地缓存汇率来避免同步网络请求。 但是为了保证缓存的新鲜度,你需要定期轮询汇率以获取其更新,或订阅变更流 —— 这恰好是数据流方法中发生的事情。
|
||||
|
||||
连接是时间相关的:如果购买事件在稍后的时间点被重新处理,汇率可能已经改变。如果要重建原始输出,则需要获取原始购买时的历史汇率。无论是查询服务还是订阅汇率更新流,你都需要处理这种时间相关性(参阅“[连接的时间相关性](ch11.md#连接的时间相关性)”)。
|
||||
连接是时间相关的:如果购买事件在稍后的时间点被重新处理,汇率可能已经改变。如果要重建原始输出,则需要获取原始购买时的历史汇率。无论是查询服务还是订阅汇率更新流,你都需要处理这种时间相关性(请参阅“[连接的时间依赖性](ch11.md#连接的时间依赖性)”)。
|
||||
|
||||
订阅变更流,而不是在需要时查询当前状态,使我们更接近类似电子表格的计算模型:当某些数据发生变更时,依赖于此的所有衍生数据都可以快速更新。还有很多未解决的问题,例如关于时间相关连接等问题,但我认为围绕数据流构建应用的想法是一个非常有希望的方向。
|
||||
|
||||
### 观察衍生数据状态
|
||||
|
||||
在抽象层面,上一节讨论的数据流系统提供了创建衍生数据集(例如搜索索引,物化视图和预测模型)并使其保持更新的过程。我们将这个过程称为**写路径(write path)**:只要某些信息被写入系统,它可能会经历批处理与流处理的多个阶段,而最终每个衍生数据集都会被更新,以适配写入的数据。[图12-1](img/fig12-1.png)显示了一个更新搜索索引的例子。
|
||||
在抽象层面,上一节讨论的数据流系统提供了创建衍生数据集(例如搜索索引、物化视图和预测模型)并使其保持更新的过程。我们将这个过程称为**写路径(write path)**:只要某些信息被写入系统,它可能会经历批处理与流处理的多个阶段,而最终每个衍生数据集都会被更新,以适配写入的数据。[图12-1](img/fig12-1.png)显示了一个更新搜索索引的例子。
|
||||
|
||||
![](img/fig12-1.png)
|
||||
|
||||
@ -350,21 +341,21 @@
|
||||
|
||||
[^iii]: 假设一个有限的语料库,那么返回非空搜索结果的搜索查询集合是有限的。然而,它是与语料库中的术语数量呈指数关系,这仍是一个坏消息。
|
||||
|
||||
另一个选择是只为一组固定的最常见的查询预先计算搜索结果,以便它们可以快速地服务而不必去走索引。不常见的查询仍然可以从走索引。这通常被称为常见查询的**缓存(cache)**,尽管我们也可以称之为**物化视图(materialized view)**,因为当新文档出现,且需要被包含在这些常见查询的搜索结果之中时,这些索引就需要更新。
|
||||
另一个选择是只为一组固定的最常见的查询预先计算搜索结果,以便它们可以快速地服务而不必去走索引。不常见的查询仍然可以通过索引来提供服务。这通常被称为常见查询的**缓存(cache)**,尽管我们也可以称之为**物化视图(materialized view)**,因为当新文档出现,且需要被包含在这些常见查询的搜索结果之中时,这些索引就需要更新。
|
||||
|
||||
从这个例子中我们可以看到,索引不是写路径和读路径之间唯一可能的边界;缓存常见搜索结果也是可行的;而在少量文档上使用没有索引的类grep扫描也是可行的。由此来看,缓存,索引和物化视图的作用很简单:它们改变了读路径与写路径之间的边界。通过预先计算结果,从而允许我们在写路径上做更多的工作,以节省读取路径上的工作量。
|
||||
从这个例子中我们可以看到,索引不是写路径和读路径之间唯一可能的边界;缓存常见搜索结果也是可行的;而在少量文档上使用没有索引的类grep扫描也是可行的。由此来看,缓存,索引和物化视图的作用很简单:它们改变了读路径与写路径之间的边界。通过预先计算结果,从而允许我们在写路径上做更多的工作,以节省读路径上的工作量。
|
||||
|
||||
在写路径上完成的工作和读路径之间的界限,实际上是本书开始处在“[描述负载](ch1.md#描述负载)”中推特例子里谈到的主题。在该例中,我们还看到了与普通用户相比,名流的写路径和读路径可能有所不同。在500页之后,我们已经走完一个大循环!
|
||||
在写路径上完成的工作和读路径之间的界限,实际上是本书开始处在“[描述负载](ch1.md#描述负载)”中推特例子里谈到的主题。在该例中,我们还看到了与普通用户相比,名人的写路径和读路径可能有所不同。在500页之后,我们已经绕回了起点!
|
||||
|
||||
#### 有状态,可离线的客户端
|
||||
#### 有状态、可离线的客户端
|
||||
|
||||
我发现写和读路径之间的边界很有趣,因为我们可以试着改变这个边界,并探讨这种改变的实际意义。我们来看看不同上下文中的这一想法。
|
||||
我发现写路径和读路径之间的边界很有趣,因为我们可以试着改变这个边界,并探讨这种改变的实际意义。我们来看看不同上下文中的这一想法。
|
||||
|
||||
过去二十年来,Web应用的火热让我们对应用开发作出了一些很容易视作理所当然的假设。具体来说就是,客户端/服务器模型 —— 客户端大多是无状态的,而服务器拥有数据的权威 —— 已经普遍到我们几乎忘掉了还有其他任何模型的存在。但是技术在不断地发展,我认为不时地质疑现状非常重要。
|
||||
|
||||
传统上,网络浏览器是无状态的客户端,只有当连接到互联网时才能做一些有用的事情(能离线执行的唯一事情基本上就是上下滚动之前在线时加载好的页面)。然而,最近的“单页面”JavaScript Web应用已经获得了很多有状态的功能,包括客户端用户界面交互,以及Web浏览器中的持久化本地存储。移动应用可以类似地在设备上存储大量状态,而且大多数用户交互都不需要与服务器往返交互。
|
||||
|
||||
这些不断变化的功能重新引发了对**离线优先(offline-first)** 应用的兴趣,这些应用尽可能地在同一设备上使用本地数据库,无需连接互联网,并在后台网络连接可用时与远程服务器同步【42】。由于移动设备通常具有缓慢且不可靠的蜂窝网络连接,因此,如果用户的用户界面不必等待同步网络请求,且应用主要是离线工作的,则这是一个巨大优势(参阅“[具有离线操作的客户端](ch5.md#具有离线操作的客户端)”)。
|
||||
这些不断变化的功能重新引发了对**离线优先(offline-first)** 应用的兴趣,这些应用尽可能地在同一设备上使用本地数据库,无需连接互联网,并在后台网络连接可用时与远程服务器同步【42】。由于移动设备通常具有缓慢且不可靠的蜂窝网络连接,因此,如果用户的用户界面不必等待同步网络请求,且应用主要是离线工作的,则这是一个巨大优势(请参阅“[需要离线操作的客户端](ch5.md#需要离线操作的客户端)”)。
|
||||
|
||||
当我们摆脱无状态客户端与中央数据库交互的假设,并转向在终端用户设备上维护状态时,这就开启了新世界的大门。特别是,我们可以将设备上的状态视为**服务器状态的缓存**。屏幕上的像素是客户端应用中模型对象的物化视图;模型对象是远程数据中心的本地状态副本【27】。
|
||||
|
||||
@ -374,19 +365,19 @@
|
||||
|
||||
最近的协议已经超越了HTTP的基本请求/响应模式:服务端发送的事件(EventSource API)和WebSockets提供了通信信道,通过这些信道,Web浏览器可以与服务器保持打开的TCP连接,只要浏览器仍然连接着,服务器就能主动向浏览器推送信息。这为服务器提供了主动通知终端用户客户端的机会,服务器能告知客户端其本地存储状态的任何变化,从而减少客户端状态的陈旧程度。
|
||||
|
||||
用我们的写路径与读路径模型来讲,主动将状态变更推至到客户端设备,意味着将写路径一直延伸到终端用户。当客户端首次初始化时,它仍然需要使用读路径来获取其初始状态,但此后它就可能依赖于服务器发送的状态变更流了。我们在流处理和消息传递部分讨论的想法并不局限于数据中心中:我们可以进一步采纳这些想法,并将它们一直延伸到终端用户设备【43】。
|
||||
用我们的写路径与读路径模型来讲,主动将状态变更推至到客户端设备,意味着将写路径一直延伸到终端用户。当客户端首次初始化时,它仍然需要使用读路径来获取其初始状态,但此后它就能够依赖服务器发送的状态变更流了。我们在流处理和消息传递部分讨论的想法并不局限于数据中心中:我们可以进一步采纳这些想法,并将它们一直延伸到终端用户设备【43】。
|
||||
|
||||
这些设备有时会离线,并在此期间无法收到服务器状态变更的任何通知。但是我们已经解决了这个问题:在“[消费者偏移量](ch11.md#消费者偏移量)”中,我们讨论了基于日志的消息代理的消费者能在失败或断开连接后重连,并确保它不会错过掉线期间任何到达的消息。同样的技术适用于单个用户,每个设备都是一个小事件流的小小订阅者。
|
||||
|
||||
#### 端到端的事件流
|
||||
|
||||
最近用于开发带状态客户端与用户界面的工具,例如如Elm语言【30】和Facebook的React,Flux和Redux工具链,已经通过订阅表示用户输入和服务器响应的事件流,来管理客户端的内部状态,其结构与事件溯源相似(请参阅第457页的“[事件溯源](ch11.md#事件溯源)”)。
|
||||
最近用于开发有状态的客户端与用户界面的工具,例如如Elm语言【30】和Facebook的React,Flux和Redux工具链,已经通过订阅表示用户输入或服务器响应的事件流来管理客户端的内部状态,其结构与事件溯源相似(请参阅“[事件溯源](ch11.md#事件溯源)”)。
|
||||
|
||||
将这种编程模型扩展为:允许服务器将状态变更事件,推送到客户端的事件管道中,是非常自然的。因此,状态变化可以通过**端到端(end-to-end)** 的写路径流动:从一个设备上的交互触发状态变更开始,经由事件日志,并穿过几个衍生数据系统与流处理器,一直到另一台设备上的用户界面,而有人正在观察用户界面上的状态变化。这些状态变化能以相当低的延迟传播 —— 比如说,在一秒内从一端到另一端。
|
||||
将这种编程模型扩展为:允许服务器将状态变更事件推送到客户端的事件管道中,是非常自然的。因此,状态变化可以通过**端到端(end-to-end)**的写路径流动:从一个设备上的交互触发状态变更开始,经由事件日志,并穿过几个衍生数据系统与流处理器,一直到另一台设备上的用户界面,而有人正在观察用户界面上的状态变化。这些状态变化能以相当低的延迟传播 —— 比如说,在一秒内从一端到另一端。
|
||||
|
||||
一些应用(如即时消息传递与在线游戏)已经具有这种“实时”架构(在低延迟交互的意义上,不是在“[响应时间保证](ch8.md#响应时间保证)”中的意义上)。但我们为什么不用这种方式构建所有的应用?
|
||||
|
||||
挑战在于,关于无状态客户端和请求/响应交互的假设已经根深蒂固地植入在在我们的数据库,库,框架,以及协议之中。许多数据存储支持读取与写入操作,为请求返回一个响应,但只有极少数提供订阅变更的能力 —— 为请求返回一个随时间推移返回响应的流(请参阅“[变更流的API支持](ch11.md#变更流的API支持)” )。
|
||||
挑战在于,关于无状态客户端和请求/响应交互的假设已经根深蒂固地植入在在我们的数据库、库、框架以及协议之中。许多数据存储支持读取与写入操作,为请求返回一个响应,但只有极少数提供订阅变更的能力 —— 请求返回一个随时间推移的响应流(请参阅“[变更流的API支持](ch11.md#变更流的API支持)” )。
|
||||
|
||||
为了将写路径延伸至终端用户,我们需要从根本上重新思考我们构建这些系统的方式:从请求/响应交互转向发布/订阅数据流【27】。更具响应性的用户界面与更好的离线支持,我认为这些优势值得我们付出努力。如果你正在设计数据系统,我希望您对订阅变更的选项留有印象,而不只是查询当前状态。
|
||||
|
||||
@ -394,37 +385,36 @@
|
||||
|
||||
我们讨论过,当流处理器将衍生数据写入存储(数据库,缓存或索引)时,以及当用户请求查询该存储时,存储将充当写路径和读路径之间的边界。该存储应当允许对数据进行随机访问的读取查询,否则这些查询将需要扫描整个事件日志。
|
||||
|
||||
在很多情况下,数据存储与流处理系统是分开的。但回想一下,流处理器还是需要维护状态以执行聚合和连接的(参阅“[流连接](ch11.md#流连接)”)。这种状态通常隐藏在流处理器内部,但一些框架也允许这些状态被外部客户端查询【45】,将流处理器本身变成一种简单的数据库。
|
||||
在很多情况下,数据存储与流处理系统是分开的。但回想一下,流处理器还是需要维护状态以执行聚合和连接的(请参阅“[流连接](ch11.md#流连接)”)。这种状态通常隐藏在流处理器内部,但一些框架也允许这些状态被外部客户端查询【45】,将流处理器本身变成一种简单的数据库。
|
||||
|
||||
我愿意进一步思考这个想法。正如到目前为止所讨论的那样,对存储的写入是通过事件日志进行的,而读取是临时的网络请求,直接流向存储着待查数据的节点。这是一个合理的设计,但不是唯一可行的设计。也可以将读取请求表示为事件流,并同时将读事件与写事件送往流处理器;流处理器通过将读取结果发送到输出流来响应读取事件【46】。
|
||||
|
||||
当写入和读取都被表示为事件,并且被路由到同一个流算子以便处理时,我们实际上是在读取查询流和数据库之间执行流表连接。读取事件需要被送往保存数据的数据库分区(参阅“[请求路由](ch6.md#请求路由)”),就像批处理和流处理器在连接时需要在同一个键上对输入分区一样(请参阅“[Reduce端连接与分组](ch10.md#Reduce端连接与分组)“)。
|
||||
当写入和读取都被表示为事件,并且被路由到同一个流算子以便处理时,我们实际上是在读取查询流和数据库之间执行流表连接。读取事件需要被送往保存数据的数据库分区(请参阅“[请求路由](ch6.md#请求路由)”),就像批处理和流处理器在连接时需要在同一个键上对输入分区一样(请参阅“[Reduce侧连接与分组](ch10.md#Reduce侧连接与分组)“)。
|
||||
|
||||
服务请求与执行连接之间的这种相似之处是非常关键的【47】。一次性读取请求只是将请求传过连接算子,然后请求马上就被忘掉了;而一个订阅请求,则是与连接另一侧过去与未来事件的持久化连接。
|
||||
|
||||
记录读取事件的日志可能对于追踪整个系统中的因果关系与数据来源也有好处:它可以让你重现出当用户做出特定决策之前看见了什么。例如在网商中,向客户显示的预测送达日期与库存状态,可能会影响他们是否选择购买一件商品【4】。要分析这种联系,则需要记录用户查询运输与库存状态的结果。
|
||||
|
||||
将读取事件写入持久存储可以更好地跟踪因果关系(参阅“[排序事件以捕获因果关系](ch9.md#排序事件以捕获因果关系)”),但会产生额外的存储与I/O成本。优化这些系统以减少开销仍然是一个开放的研究问题【2】。但如果你已经出于运维目的留下了读取请求日志,将其作为请求处理的副作用,那么将这份日志作为请求事件源并不是什么特别大的变更。
|
||||
将读取事件写入持久存储可以更好地跟踪因果关系(请参阅“[排序事件以捕获因果关系](#排序事件以捕获因果关系)”),但会产生额外的存储与I/O成本。优化这些系统以减少开销仍然是一个开放的研究问题【2】。但如果你已经出于运维目的留下了读取请求日志,将其作为请求处理的副作用,那么将这份日志作为请求事件源并不是什么特别大的变更。
|
||||
|
||||
#### 多分区数据处理
|
||||
|
||||
对于只涉及单个分区的查询,通过流来发送查询与收集响应可能是杀鸡用牛刀了。然而,这个想法开启了分布式执行复杂查询的可能性,这需要合并来自多个分区的数据,利用流处理器已经提供的消息路由,分区和连接的基础设施。
|
||||
对于只涉及单个分区的查询,通过流来发送查询与收集响应可能是杀鸡用牛刀了。然而,这个想法开启了分布式执行复杂查询的可能性,这需要合并来自多个分区的数据,利用了流处理器已经提供的消息路由、分区和连接的基础设施。
|
||||
|
||||
Storm的分布式RPC功能支持这种使用模式(参阅“[消息传递和RPC](ch11.md#消息传递和RPC)”)。例如,它已经被用来计算浏览过某个推特URL的人数 —— 即,转推该URL的粉丝集合的并集【48】。由于推特的用户是分区的,因此这种计算需要合并来自多个分区的结果。
|
||||
Storm的分布式RPC功能支持这种使用模式(请参阅“[消息传递和RPC](ch11.md#消息传递和RPC)”)。例如,它已经被用来计算浏览过某个推特URL的人数 —— 即,发推包含该URL的所有人的粉丝集合的并集【48】。由于推特的用户是分区的,因此这种计算需要合并来自多个分区的结果。
|
||||
|
||||
这种模式的另一个例子是欺诈预防:为了评估特定购买事件是否具有欺诈风险,你可以检查该用户IP地址,电子邮件地址,帐单地址,送货地址的信用分。这些信用数据库中的每一个自己都是一个分区,因此为特定购买事件采集分数需要连接一系列不同的分区数据集【49】。
|
||||
|
||||
MPP数据库的内部查询执行图有着类似的特征(参阅“[比较Hadoop与分布式数据库](ch10.md#比较Hadoop与分布式数据库)”)。如果需要执行这种多分区连接,则直接使用提供此功能的数据库,可能要比使用流处理器实现它要更简单。然而将查询视为流提供了一种选项,可以用于实现超出传统现成解决方案的大规模应用。
|
||||
这种模式的另一个例子是欺诈预防:为了评估特定购买事件是否具有欺诈风险,你可以检查该用户IP地址,电子邮件地址,帐单地址,送货地址的信用分。这些信用数据库中的每一个都是有分区的,因此为特定购买事件采集分数需要连接一系列不同的分区数据集【49】。
|
||||
|
||||
MPP数据库的内部查询执行图有着类似的特征(请参阅“[Hadoop与分布式数据库的对比](ch10.md#Hadoop与分布式数据库的对比)”)。如果需要执行这种多分区连接,则直接使用提供此功能的数据库,可能要比使用流处理器实现它要更简单。然而将查询视为流提供了一种选项,可以用于实现超出传统现成解决方案的大规模应用。
|
||||
|
||||
|
||||
## 将事情做正确
|
||||
|
||||
对于只读取数据的无状态服务,出问题也没什么大不了的:你可以修复该错误并重启服务,而一切都恢复正常。像数据库这样的有状态系统就没那么简单了:它们被设计为永远记住事物(或多或少),所以如果出现问题,这种(错误的)效果也将潜在地永远持续下去,这意味着它们需要更仔细的思考【50】。
|
||||
|
||||
我们希望构建可靠且**正确**的应用(即使面对各种故障,程序的语义也能被很好地定义与理解)。约四十年来,原子性,隔离性和持久性([第7章](ch7.md))等事务特性一直是构建正确应用的首选工具。然而这些地基没有看上去那么牢固:例如弱隔离级别带来的困惑可以佐证(请参见“[弱隔离级别](ch7.md#弱隔离级别)”)。
|
||||
我们希望构建可靠且**正确**的应用(即使面对各种故障,程序的语义也能被很好地定义与理解)。约四十年来,原子性,隔离性和持久性([第七章](ch7.md))等事务特性一直是构建正确应用的首选工具。然而这些地基没有看上去那么牢固:例如弱隔离级别带来的困惑可以佐证(请参阅“[弱隔离级别](ch7.md#弱隔离级别)”)。
|
||||
|
||||
事务在某些领域被完全抛弃,并被提供更好性能与可伸缩性的模型取代,但更复杂的语义(例如,参阅“[无领导者复制](ch5.md#无领导者复制)”)。**一致性(Consistency)** 经常被谈起,但其定义并不明确(“[一致性](ch5.md#一致性)”和[第9章](ch9.md))。有些人断言我们应当为了高可用而“拥抱弱一致性”,但却对这些概念实际上意味着什么缺乏清晰的认识。
|
||||
事务在某些领域被完全抛弃,并被提供更好性能与可伸缩性的模型取代,但更复杂的语义(例如,请参阅“[无领导者复制](ch5.md#无领导者复制)”)。**一致性(Consistency)** 经常被谈起,但其定义并不明确(“[一致性](ch5.md#一致性)”和[第九章](ch9.md))。有些人断言我们应当为了高可用而“拥抱弱一致性”,但却对这些概念实际上意味着什么缺乏清晰的认识。
|
||||
|
||||
对于如此重要的话题,我们的理解,以及我们的工程方法却是惊人地薄弱。例如,确定在特定事务隔离等级或复制配置下运行特定应用是否安全是非常困难的【51,52】。通常简单的解决方案似乎在低并发性的情况下工作正常,并且没有错误,但在要求更高的情况下却会出现许多微妙的错误。
|
||||
|
||||
@ -448,7 +438,7 @@
|
||||
|
||||
处理两次是数据损坏的一种形式:为同样的服务向客户收费两次(收费太多)或增长计数器两次(夸大指标)都不是我们想要的。在这种情况下,恰好一次意味着安排计算,使得最终效果与没有发生错误的情况一样,即使操作实际上因为某种错误而重试。我们先前讨论过实现这一目标的几种方法。
|
||||
|
||||
最有效的方法之一是使操作**幂等(idempotent)**(参阅“[幂等性](ch11.md#幂等性)”);即确保它无论是执行一次还是执行多次都具有相同的效果。但是,将不是天生幂等的操作变为幂等的操作需要一些额外的努力与关注:你可能需要维护一些额外的元数据(例如更新了值的操作ID集合),并在从一个节点故障切换至另一个节点时做好防护(参阅的“[领导与锁定](ch9.md#领导与锁定)”)。
|
||||
最有效的方法之一是使操作**幂等(idempotent)**(请参阅“[幂等性](ch11.md#幂等性)”);即确保它无论是执行一次还是执行多次都具有相同的效果。但是,将不是天生幂等的操作变为幂等的操作需要一些额外的努力与关注:你可能需要维护一些额外的元数据(例如更新了值的操作ID集合),并在从一个节点故障切换至另一个节点时做好防护(请参阅的“[领导与锁定](ch9.md#领导与锁定)”)。
|
||||
|
||||
#### 抑制重复
|
||||
|
||||
@ -467,7 +457,7 @@ COMMIT;
|
||||
|
||||
客户端可以重连到数据库并重试事务,但现在现在处于TCP重复抑制的范围之外了。因为[例12-1]()中的事务不是幂等的,可能会发生转了\$22而不是期望的\$11。因此,尽管[例12-1]()是一个事务原子性的标准样例,但它实际上并不正确,而真正的银行并不会这样办事【3】。
|
||||
|
||||
两阶段提交(参阅“[原子提交与两阶段提交(2PC)](ch9.md#原子提交与两阶段提交(2PC))”)协议会破坏TCP连接与事务之间的1:1映射,因为它们必须在故障后允许事务协调器重连到数据库,告诉数据库将存疑事务提交还是中止。这足以确保事务只被恰好执行一次吗?不幸的是,并不能。
|
||||
两阶段提交(请参阅“[原子提交与两阶段提交(2PC)](ch9.md#原子提交与两阶段提交(2PC))”)协议会破坏TCP连接与事务之间的1:1映射,因为它们必须在故障后允许事务协调器重连到数据库,告诉数据库将存疑事务提交还是中止。这足以确保事务只被恰好执行一次吗?不幸的是,并不能。
|
||||
|
||||
即使我们可以抑制数据库客户端与服务器之间的重复事务,我们仍然需要担心终端用户设备与应用服务器之间的网络。例如,如果终端用户的客户端是Web浏览器,则它可能会使用HTTP POST请求向服务器提交指令。也许用户正处于一个信号微弱的蜂窝数据网络连接中,它们成功地发送了POST,但却在能够从服务器接收响应之前没了信号。
|
||||
|
||||
@ -493,7 +483,7 @@ COMMIT;
|
||||
|
||||
[例12-2]()依赖于`request_id`列上的唯一约束。如果一个事务尝试插入一个已经存在的ID,那么`INSERT`失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束(而在“[写入偏斜与幻读](ch7.md#写入偏斜与幻读)”中讨论过,应用级别的**检查-然后-插入**可能会在不可序列化的隔离下失败)。
|
||||
|
||||
除了抑制重复的请求之外,[例12-2]()中的请求表表现得就像一种事件日志,提示向着事件溯源的方向(参阅“[事件溯源](ch11.md#事件溯源)”)。更新账户余额事实上不必与插入事件发生在同一个事务中,因为它们是冗余的,而能由下游消费者从请求事件中衍生出来 —— 只要该事件被恰好处理一次,这又一次可以使用请求ID来强制执行。
|
||||
除了抑制重复的请求之外,[例12-2]()中的请求表表现得就像一种事件日志,提示向着事件溯源的方向(请参阅“[事件溯源](ch11.md#事件溯源)”)。更新账户余额事实上不必与插入事件发生在同一个事务中,因为它们是冗余的,而能由下游消费者从请求事件中衍生出来 —— 只要该事件被恰好处理一次,这又一次可以使用请求ID来强制执行。
|
||||
|
||||
**端到端的原则**
|
||||
|
||||
@ -516,9 +506,9 @@ COMMIT;
|
||||
|
||||
这实在是一个遗憾,因为容错机制很难弄好。低层级的可靠机制(比如TCP中的那些)运行的相当好,因而剩下的高层级错误基本很少出现。如果能将这些剩下的高层级容错机制打包成抽象,而应用不需要再去操心,那该多好呀 —— 但恐怕我们还没有找到这一正确的抽象。
|
||||
|
||||
长期以来,事务被认为是一个很好的抽象,我相信它们确实是很有用的。正如[第7章](ch7.md)导言中所讨论的,它们将各种可能的问题(并发写入,违背约束,崩溃,网络中断,磁盘故障)合并为两种可能结果:提交或中止。这是对编程模型而言是一种巨大的简化,但恐怕这还不够。
|
||||
长期以来,事务被认为是一个很好的抽象,我相信它们确实是很有用的。正如[第七章](ch7.md)导言中所讨论的,它们将各种可能的问题(并发写入,违背约束,崩溃,网络中断,磁盘故障)合并为两种可能结果:提交或中止。这是对编程模型而言是一种巨大的简化,但恐怕这还不够。
|
||||
|
||||
事务是代价高昂的,当涉及异构存储技术时尤为甚(参阅的“[实践中的分布式事务](ch9.md#实践中的分布式事务)”)。我们拒绝使用分布式事务是因为它开销太大,结果我们最后不得不在应用代码中重新实现容错机制。正如本书中大量的例子所示,对并发性与部分失败的推理是困难且违反直觉的,所以我怀疑大多数应用级别的机制都不能正确工作,最终结果是数据丢失或损坏。
|
||||
事务是代价高昂的,当涉及异构存储技术时尤为甚(请参阅的“[实践中的分布式事务](ch9.md#实践中的分布式事务)”)。我们拒绝使用分布式事务是因为它开销太大,结果我们最后不得不在应用代码中重新实现容错机制。正如本书中大量的例子所示,对并发性与部分失败的推理是困难且违反直觉的,所以我怀疑大多数应用级别的机制都不能正确工作,最终结果是数据丢失或损坏。
|
||||
|
||||
出于这些原因,我认为探索对容错的抽象是很有价值的。它使提供应用特定的端到端的正确性属性变得更简单,而且还能在大规模分布式环境中提供良好的性能与运维特性。
|
||||
|
||||
@ -532,19 +522,19 @@ COMMIT;
|
||||
|
||||
#### 唯一性约束需要达成共识
|
||||
|
||||
在[第9章](ch9.md)中我们看到,在分布式环境中,强制执行唯一性约束需要共识:如果存在多个具有相同值的并发请求,则系统需要决定冲突操作中的哪一个被接受,并拒绝其他违背约束的操作。
|
||||
在[第九章](ch9.md)中我们看到,在分布式环境中,强制执行唯一性约束需要共识:如果存在多个具有相同值的并发请求,则系统需要决定冲突操作中的哪一个被接受,并拒绝其他违背约束的操作。
|
||||
|
||||
达成这一共识的最常见方式是使单个节点作为领导,并使其负责所有决策。只要你不介意所有请求都挤过单个节点(即使客户端位于世界的另一端),只要该节点没有失效,系统就能正常工作。如果你需要容忍领导者失效,那么就又回到了共识问题(参阅“[单领导者复制与共识](ch9.md#单领导者复制与共识)”)。
|
||||
达成这一共识的最常见方式是使单个节点作为领导,并使其负责所有决策。只要你不介意所有请求都挤过单个节点(即使客户端位于世界的另一端),只要该节点没有失效,系统就能正常工作。如果你需要容忍领导者失效,那么就又回到了共识问题(请参阅“[单领导者复制与共识](ch9.md#单领导者复制与共识)”)。
|
||||
|
||||
唯一性检查可以通过对唯一性字段分区做横向伸缩。例如,如果需要通过请求ID确保唯一性(如[例12-2]()所示),你可以确保所有具有相同请求ID的请求都被路由到同一分区(参阅[第6章](ch6.md))。如果你需要让用户名是唯一的,则可以按用户名的散列值做分区。
|
||||
唯一性检查可以通过对唯一性字段分区做横向伸缩。例如,如果需要通过请求ID确保唯一性(如[例12-2]()所示),你可以确保所有具有相同请求ID的请求都被路由到同一分区(请参阅[第六章](ch6.md))。如果你需要让用户名是唯一的,则可以按用户名的散列值做分区。
|
||||
|
||||
但异步多主复制排除在外,因为可能会发生不同主库同时接受冲突写操作的情况,因而这些值不再是唯一的(参阅“[实现可线性化系统](ch9.md#实现可线性化系统)”)。如果你想立刻拒绝任何违背约束的写入,同步协调是无法避免的【56】。
|
||||
但异步多主复制排除在外,因为可能会发生不同主库同时接受冲突写操作的情况,因而这些值不再是唯一的(请参阅“[实现可线性化系统](ch9.md#实现可线性化系统)”)。如果你想立刻拒绝任何违背约束的写入,同步协调是无法避免的【56】。
|
||||
|
||||
#### 基于日志消息传递中的唯一性
|
||||
|
||||
日志确保所有消费者以相同的顺序看见消息 —— 这种保证在形式上被称为**全序广播(total order boardcast)** 并且等价于共识(参见“[全序广播](ch9.md#全序广播)”)。在使用基于日志的消息传递的分拆数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。
|
||||
日志确保所有消费者以相同的顺序看见消息 —— 这种保证在形式上被称为**全序广播(total order boardcast)** 并且等价于共识(请参阅“[全序广播](ch9.md#全序广播)”)。在使用基于日志的消息传递的分拆数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。
|
||||
|
||||
流处理器在单个线程上依次消费单个日志分区中的所有消息(参阅“[与传统消息传递相比的日志](ch11.md#与传统消息传递相比的日志)”)。因此,如果日志是按有待确保唯一的值做的分区,则流处理器可以无歧义地,确定性地决定几个冲突操作中的哪一个先到达。例如,在多个用户尝试宣告相同用户名的情况下【57】:
|
||||
流处理器在单个线程上依次消费单个日志分区中的所有消息(请参阅“[与传统消息传递相比的日志](ch11.md#与传统消息传递相比的日志)”)。因此,如果日志是按有待确保唯一的值做的分区,则流处理器可以无歧义地,确定性地决定几个冲突操作中的哪一个先到达。例如,在多个用户尝试宣告相同用户名的情况下【57】:
|
||||
|
||||
1. 每个对用户名的请求都被编码为一条消息,并追加到按用户名散列值确定的分区。
|
||||
2. 流处理器依序读取日志中的请求,并使用本地数据库来追踪哪些用户名已经被占用了。对于所有申请可用用户名的请求,它都会记录该用户名,并向输出流发送一条成功消息。对于所有申请已占用用户名的请求,它都会向输出流发送一条拒绝消息。
|
||||
@ -567,17 +557,17 @@ COMMIT;
|
||||
2. 流处理器读取请求日志。对于每个请求消息,它向输出流发出两条消息:付款人账户A的借记指令(按A分区),收款人B的贷记指令(按B分区)。被发出的消息中会带有原始的请求ID。
|
||||
3. 后续处理器消费借记/贷记指令流,按照请求ID除重,并将变更应用至账户余额。
|
||||
|
||||
步骤1和步骤2是必要的,因为如果客户直接发送贷记与借记指令,则需要在这两个分区之间进行原子提交,以确保两者要么都发生或都不发生。为了避免对分布式事务的需要,我们首先将请求持久化记录为单条消息,然后从这第一条消息中衍生出贷记指令与借记指令。几乎在所有数据系统中,单对象写入都是原子性的(参阅“[单对象写入](ch7.md#单对象写入)),因此请求要么出现在日志中,要么就不出现,无需多分区原子提交。
|
||||
步骤1和步骤2是必要的,因为如果客户直接发送贷记与借记指令,则需要在这两个分区之间进行原子提交,以确保两者要么都发生或都不发生。为了避免对分布式事务的需要,我们首先将请求持久化记录为单条消息,然后从这第一条消息中衍生出贷记指令与借记指令。几乎在所有数据系统中,单对象写入都是原子性的(请参阅“[单对象写入](ch7.md#单对象写入)),因此请求要么出现在日志中,要么就不出现,无需多分区原子提交。
|
||||
|
||||
如果流处理器在步骤2中崩溃,则它会从上一个存档点恢复处理。这样做时,它不会跳过任何请求消息,但可能会多次处理请求并产生重复的贷记与借记指令。但由于它是确定性的,因此它只是再次生成相同的指令,而步骤3中的处理器可以使用端到端请求ID轻松地对其除重。
|
||||
|
||||
如果你想确保付款人的帐户不会因此次转账而透支,则可以使用一个额外的流处理器来维护账户余额并校验事务(按付款人账户分区),只有有效的事务会被记录在步骤1中的请求日志中。
|
||||
|
||||
通过将多分区事务分解为两个不同分区方式的阶段,并使用端到端的请求ID,我们实现了同样的正确性属性(每个请求对付款人与收款人都恰好生效一次),即使在出现故障,且没有使用原子提交协议的情况下依然如此。使用多个不同分区方式的阶段与我们在“[多分区数据处理](#多分区数据处理)”中讨论的想法类似(参阅“[并发控制](ch11.md#并发控制)”)。
|
||||
通过将多分区事务分解为两个不同分区方式的阶段,并使用端到端的请求ID,我们实现了同样的正确性属性(每个请求对付款人与收款人都恰好生效一次),即使在出现故障,且没有使用原子提交协议的情况下依然如此。使用多个不同分区方式的阶段与我们在“[多分区数据处理](#多分区数据处理)”中讨论的想法类似(请参阅“[并发控制](ch11.md#并发控制)”)。
|
||||
|
||||
### 及时性与完整性
|
||||
|
||||
事务的一个便利属性是,它们通常是线性一致的(参阅“[线性一致性](ch9.md#线性一致性)”),也就是说,写入者会等到事务提交,而之后其写入立刻对所有读取者可见。
|
||||
事务的一个便利属性是,它们通常是线性一致的(请参阅“[线性一致性](ch9.md#线性一致性)”),也就是说,写入者会等到事务提交,而之后其写入立刻对所有读取者可见。
|
||||
|
||||
当我们把一个操作拆分为跨越多个阶段的流处理器时,却并非如此:日志的消费者在设计上就是异步的,因此发送者不会等其消息被消费者处理完。但是,客户端等待输出流中的特定消息是可能的。这正是我们在“[基于日志消息传递中的唯一性](#基于日志消息传递中的唯一性)”一节中检查唯一性约束时所做的事情。
|
||||
|
||||
@ -587,15 +577,15 @@ COMMIT;
|
||||
|
||||
***及时性(Timeliness)***
|
||||
|
||||
及时性意味着确保用户观察到系统的最新状态。我们之前看到,如果用户从陈旧的数据副本中读取数据,它们可能会观察到系统处于不一致的状态(参阅“[复制延迟问题](ch5.md#复制延迟问题)”)。但这种不一致是暂时的,而最终会通过等待与重试简单地得到解决。
|
||||
及时性意味着确保用户观察到系统的最新状态。我们之前看到,如果用户从陈旧的数据副本中读取数据,它们可能会观察到系统处于不一致的状态(请参阅“[复制延迟问题](ch5.md#复制延迟问题)”)。但这种不一致是暂时的,而最终会通过等待与重试简单地得到解决。
|
||||
|
||||
CAP定理(参阅“[线性一致性的代价](ch9.md#线性一致性的代价)”)使用**线性一致性(linearizability)** 意义上的一致性,这是实现及时性的强有力方法。像**写后读**这样及时性更弱的一致性也很有用(参阅“[读己之写](ch5.md#读己之写)”)也很有用。
|
||||
CAP定理(请参阅“[线性一致性的代价](ch9.md#线性一致性的代价)”)使用**线性一致性(linearizability)** 意义上的一致性,这是实现及时性的强有力方法。像**写后读**这样及时性更弱的一致性也很有用(请参阅“[读己之写](ch5.md#读己之写)”)也很有用。
|
||||
|
||||
***完整性(Integrity)***
|
||||
|
||||
完整性意味着没有损坏;即没有数据丢失,并且没有矛盾或错误的数据。尤其是如果某些衍生数据集是作为底层数据之上的视图而维护的(参阅“[从事件日志导出当前状态](ch11.md#从事件日志导出当前状态)”),这种衍生必须是正确的。例如,数据库索引必须正确地反映数据库的内容 —— 缺失某些记录的索引并不是很有用。
|
||||
完整性意味着没有损坏;即没有数据丢失,并且没有矛盾或错误的数据。尤其是如果某些衍生数据集是作为底层数据之上的视图而维护的(请参阅“[从事件日志导出当前状态](ch11.md#从事件日志导出当前状态)”),这种衍生必须是正确的。例如,数据库索引必须正确地反映数据库的内容 —— 缺失某些记录的索引并不是很有用。
|
||||
|
||||
如果完整性被违背,这种不一致是永久的:在大多数情况下,等待与重试并不能修复数据库损坏。相反的是,需要显式地检查与修复。在ACID事务的上下文中(参阅“[ACID的涵义](ch7.md#ACID的涵义)”),一致性通常被理解为某种特定于应用的完整性概念。原子性和持久性是保持完整性的重要工具。
|
||||
如果完整性被违背,这种不一致是永久的:在大多数情况下,等待与重试并不能修复数据库损坏。相反的是,需要显式地检查与修复。在ACID事务的上下文中(请参阅“[ACID的涵义](ch7.md#ACID的涵义)”),一致性通常被理解为某种特定于应用的完整性概念。原子性和持久性是保持完整性的重要工具。
|
||||
|
||||
|
||||
|
||||
@ -611,14 +601,14 @@ COMMIT;
|
||||
|
||||
另一方面,对于在本章中讨论的基于事件的数据流系统而言,它们的一个有趣特性就是将及时性与完整性分开。在异步处理事件流时不能保证及时性,除非你显式构建一个在返回之前明确等待特定消息到达的消费者。但完整性实际上才是流处理系统的核心。
|
||||
|
||||
**恰好一次**或**等效一次**语义(参阅“[容错](ch11.md#容错)”)是一种保持完整性的机制。如果事件丢失或者生效两次,就有可能违背数据系统的完整性。因此在面对故障时,容错消息传递与重复抑制(例如,幂等操作)对于维护数据系统的完整性是很重要的。
|
||||
**恰好一次**或**等效一次**语义(请参阅“[容错](ch11.md#容错)”)是一种保持完整性的机制。如果事件丢失或者生效两次,就有可能违背数据系统的完整性。因此在面对故障时,容错消息传递与重复抑制(例如,幂等操作)对于维护数据系统的完整性是很重要的。
|
||||
|
||||
正如我们在上一节看到的那样,可靠的流处理系统可以在无需分布式事务与原子提交协议的情况下保持完整性,这意味着它们能潜在地实现好得多的性能与运维稳健性,在达到类似正确性的前提下。为了达成这种正确性,我们组合使用了多种机制:
|
||||
|
||||
* 将写入操作的内容表示为单条消息,从而可以轻松地被原子写入 —— 与事件溯源搭配效果拔群(参阅“[事件溯源](ch11.md#事件溯源)”)。
|
||||
* 使用与存储过程类似的确定性衍生函数,从这一消息中衍生出所有其他的状态变更(参见“[真的串行执行](ch7.md#真的串行执行)”和“[作为衍生函数的应用代码](ch11.md#作为衍生函数的应用代码)”)
|
||||
* 将写入操作的内容表示为单条消息,从而可以轻松地被原子写入 —— 与事件溯源搭配效果拔群(请参阅“[事件溯源](ch11.md#事件溯源)”)。
|
||||
* 使用与存储过程类似的确定性衍生函数,从这一消息中衍生出所有其他的状态变更(请参阅“[真的串行执行](ch7.md#真的串行执行)”和“[作为衍生函数的应用代码](ch11.md#作为衍生函数的应用代码)”)
|
||||
* 将客户端生成的请求ID传递通过所有的处理层次,从而启用端到端除重,带来幂等性。
|
||||
* 使消息不可变,并允许衍生数据能随时被重新处理,这使从错误中恢复更加容易(参阅“[不可变事件的优点](ch11.md#不可变事件的优点)”)
|
||||
* 使消息不可变,并允许衍生数据能随时被重新处理,这使从错误中恢复更加容易(请参阅“[不可变事件的优点](ch11.md#不可变事件的优点)”)
|
||||
|
||||
这种机制组合在我看来,是未来构建容错应用的一个非常有前景的方向。
|
||||
|
||||
@ -658,11 +648,11 @@ COMMIT;
|
||||
|
||||
### 信任但验证
|
||||
|
||||
我们所有关于正确性,完整性和容错的讨论都基于一些假设,假设某些事情可能会出错,但其他事情不会。我们将这些假设称为我们的**系统模型(system model)**(参阅“[将系统模型映射到现实世界](ch8.md#将系统模型映射到现实世界)”):例如,我们应该假设进程可能会崩溃,机器可能突然断电,网络可能会任意延迟或丢弃消息。但是我们也可能假设写入磁盘的数据在执行`fsync`后不会丢失,内存中的数据没有损坏,而CPU的乘法指令总是能返回正确的结果。
|
||||
我们所有关于正确性,完整性和容错的讨论都基于一些假设,假设某些事情可能会出错,但其他事情不会。我们将这些假设称为我们的**系统模型(system model)**(请参阅“[将系统模型映射到现实世界](ch8.md#将系统模型映射到现实世界)”):例如,我们应该假设进程可能会崩溃,机器可能突然断电,网络可能会任意延迟或丢弃消息。但是我们也可能假设写入磁盘的数据在执行`fsync`后不会丢失,内存中的数据没有损坏,而CPU的乘法指令总是能返回正确的结果。
|
||||
|
||||
这些假设是相当合理的,因为大多数时候它们都是成立的,如果我们不得不经常担心计算机出错,那么基本上寸步难行。在传统上,系统模型采用二元方法处理故障:我们假设有些事情可能会发生,而其他事情**永远**不会发生。实际上,这更像是一个概率问题:有些事情更有可能,其他事情不太可能。问题在于违反我们假设的情况是否经常发生,以至于我们可能在实践中遇到它们。
|
||||
|
||||
我们已经看到,数据可能会在尚未落盘时损坏(参阅“[复制与持久性](ch5.md#复制与持久性)”),而网络上的数据损坏有时可能规避了TCP校验和(参阅“[弱谎言形式](ch8.md#弱谎言形式)” )。也许我们应当更关注这些事情?
|
||||
我们已经看到,数据可能会在尚未落盘时损坏(请参阅“[复制与持久性](ch5.md#复制与持久性)”),而网络上的数据损坏有时可能规避了TCP校验和(请参阅“[弱谎言形式](ch8.md#弱谎言形式)” )。也许我们应当更关注这些事情?
|
||||
|
||||
我过去所从事的一个应用收集了来自客户端的崩溃报告,我们收到的一些报告,只有在这些设备内存中出现了随机位翻转才解释的通。这看起来不太可能,但是如果有足够多的设备运行你的软件,那么即使再不可能发生的事也确实会发生。除了由于硬件故障或辐射导致的随机存储器损坏之外,一些病态的存储器访问模式甚至可以在没有故障的存储器中翻转位【62】 —— 一种可用于破坏操作系统安全机制的效应【63】(这种技术被称为**Rowhammer**)。一旦你仔细观察,硬件并不是看上去那样完美的抽象。
|
||||
|
||||
@ -676,7 +666,7 @@ COMMIT;
|
||||
|
||||
而对于应用代码,我们不得不假设会有更多的错误,因为绝大多数应用的代码经受的评审与测试远远无法与数据库的代码相比。许多应用甚至没有正确使用数据库提供的用于维持完整性的功能,例如外键或唯一性约束【36】。
|
||||
|
||||
ACID意义下的一致性(参阅“[一致性](ch7.md#一致性)”)基于这样一种想法:数据库以一致的状态启动,而事务将其从一个一致状态转换至另一个一致的状态。因此,我们期望数据库始终处于一致状态。然而,只有当你假设事务没有Bug时,这种想法才有意义。如果应用以某种错误的方式使用数据库,例如,不安全地使用弱隔离等级,数据库的完整性就无法得到保证。
|
||||
ACID意义下的一致性(请参阅“[一致性](ch7.md#一致性)”)基于这样一种想法:数据库以一致的状态启动,而事务将其从一个一致状态转换至另一个一致的状态。因此,我们期望数据库始终处于一致状态。然而,只有当你假设事务没有Bug时,这种想法才有意义。如果应用以某种错误的方式使用数据库,例如,不安全地使用弱隔离等级,数据库的完整性就无法得到保证。
|
||||
|
||||
#### 不要盲目信任承诺
|
||||
|
||||
@ -698,11 +688,11 @@ COMMIT;
|
||||
|
||||
#### 为可审计性而设计
|
||||
|
||||
如果一个事务在一个数据库中改变了多个对象,在这一事实发生后,很难说清这个事务到底意味着什么。即使你捕获了事务日志(参阅“[变更数据捕获](ch11.md#变更数据捕获)”),各种表中的插入,更新和删除操作并不一定能清楚地表明**为什么**要执行这些变更。决定这些变更的是应用逻辑中的调用,而这一应用逻辑稍纵即逝,无法重现。
|
||||
如果一个事务在一个数据库中改变了多个对象,在这一事实发生后,很难说清这个事务到底意味着什么。即使你捕获了事务日志(请参阅“[变更数据捕获](ch11.md#变更数据捕获)”),各种表中的插入,更新和删除操作并不一定能清楚地表明**为什么**要执行这些变更。决定这些变更的是应用逻辑中的调用,而这一应用逻辑稍纵即逝,无法重现。
|
||||
|
||||
相比之下,基于事件的系统可以提供更好的可审计性。在事件溯源方法中,系统的用户输入被表示为一个单一不可变事件,而任何其导致的状态变更都衍生自该事件。衍生可以实现为具有确定性与可重复性,因而相同的事件日志通过相同版本的衍生代码时,会导致相同的状态变更。
|
||||
|
||||
显式处理数据流(参阅“[批处理输出的哲学](ch10.md#批处理输出的哲学)”)可以使数据的**来龙去脉(provenance)** 更加清晰,从而使完整性检查更具可行性。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理器与流处理器,以检查是否获得相同的结果,或者,甚至并行运行冗余的衍生流程。
|
||||
显式处理数据流(请参阅“[批处理输出的哲学](ch10.md#批处理输出的哲学)”)可以使数据的**来龙去脉(provenance)** 更加清晰,从而使完整性检查更具可行性。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理器与流处理器,以检查是否获得相同的结果,或者,甚至并行运行冗余的衍生流程。
|
||||
|
||||
具有确定性且定义良好的数据流,也使调试与跟踪系统的执行变得容易,以便确定它**为什么**做了某些事情【4,69】。如果出现意想之外的事情,那么重现导致意外事件的确切事故现场的诊断能力—— 一种时间旅行调试功能是非常有价值的。
|
||||
|
||||
@ -710,7 +700,7 @@ COMMIT;
|
||||
|
||||
如果我们不能完全相信系统的每个组件都不会损坏 —— 每一个硬件都没缺陷,每一个软件都没有Bug —— 那我们至少必须定期检查数据的完整性。如果我们不检查,我们就不能发现损坏,直到无可挽回地导致对下游的破坏时,那时候再去追踪问题就要难得多,且代价也要高的多。
|
||||
|
||||
检查数据系统的完整性,最好是以端到端的方式进行(参阅“[数据库端到端的争论](#数据库的端到端的争论)”):我们能在完整性检查中涵盖的系统越多,某些处理阶中出现不被察觉损坏的几率就越小。如果我们能检查整个衍生数据管道端到端的正确性,那么沿着这一路径的任何磁盘,网络,服务,以及算法的正确性检查都隐含在其中了。
|
||||
检查数据系统的完整性,最好是以端到端的方式进行(请参阅“[数据库端到端的争论](#数据库的端到端的争论)”):我们能在完整性检查中涵盖的系统越多,某些处理阶中出现不被察觉损坏的几率就越小。如果我们能检查整个衍生数据管道端到端的正确性,那么沿着这一路径的任何磁盘,网络,服务,以及算法的正确性检查都隐含在其中了。
|
||||
|
||||
持续的端到端完整性检查可以不断提高你对系统正确性的信心,从而使你能更快地进步【70】。与自动化测试一样,审计提高了快速发现错误的可能性,从而降低了系统变更或新存储技术可能导致损失的风险。如果你不害怕进行变更,就可以更好地充分演化一个应用,使其满足不断变化的需求。
|
||||
|
||||
@ -722,7 +712,7 @@ COMMIT;
|
||||
|
||||
我没有资格评论这些技术用于货币,或者合同商定机制的价值。但从数据系统的角度来看,它们包含了一些有趣的想法。实质上,它们是分布式数据库,具有数据模型与事务机制,而不同副本可以由互不信任的组织托管。副本不断检查其他副本的完整性,并使用共识协议对应当执行的事务达成一致。
|
||||
|
||||
我对这些技术的拜占庭容错方面有些怀疑(参阅“[拜占庭故障](ch8.md#拜占庭故障)”),而且我发现**工作证明(proof of work)** 技术非常浪费(比如,比特币挖矿)。比特币的交易吞吐量相当低,尽管是出于政治与经济原因而非技术上的原因。不过,完整性检查的方面是很有趣的。
|
||||
我对这些技术的拜占庭容错方面有些怀疑(请参阅“[拜占庭故障](ch8.md#拜占庭故障)”),而且我发现**工作证明(proof of work)** 技术非常浪费(比如,比特币挖矿)。比特币的交易吞吐量相当低,尽管是出于政治与经济原因而非技术上的原因。不过,完整性检查的方面是很有趣的。
|
||||
|
||||
密码学审计与完整性检查通常依赖**默克尔树(Merkle tree)**【74】,这是一颗散列值的树,能够用于高效地证明一条记录出现在一个数据集中(以及其他一些特性)。除了炒作的沸沸扬扬的加密货币之外,**证书透明性(certificate transparency)** 也是一种依赖Merkle树的安全技术,用来检查TLS/SSL证书的有效性【75,76】。
|
||||
|
||||
@ -876,7 +866,7 @@ COMMIT;
|
||||
|
||||
我们应该允许每个人保留自己的隐私 —— 即,对自己数据的控制 —— 而不是通过监视来窃取这种控制权。我们控制自己数据的个体权利就像是国家公园的自然环境:如果我们不去明确地保护它,关心它,它就会被破坏。这将是公地的悲剧,我们都会因此而变得更糟。无所不在的监视并非不可避免的 —— 我们现在仍然能阻止它。
|
||||
|
||||
我们究竟能做到哪一步,是一个开放的问题。首先,我们不应该永久保留数据,而是一旦不再需要就立即清除数据【111,112】。清除数据与不变性的想法背道而驰(参阅“[不变性的局限性](ch11.md#不变性的局限性)”),但这是可以解决该问题。我所看到的一种很有前景的方法是通过加密协议来实施访问控制,而不仅仅是通过策略【113,114】。总的来说,文化与态度的改变是必要的。
|
||||
我们究竟能做到哪一步,是一个开放的问题。首先,我们不应该永久保留数据,而是一旦不再需要就立即清除数据【111,112】。清除数据与不变性的想法背道而驰(请参阅“[不变性的局限性](ch11.md#不变性的局限性)”),但这是可以解决该问题。我所看到的一种很有前景的方法是通过加密协议来实施访问控制,而不仅仅是通过策略【113,114】。总的来说,文化与态度的改变是必要的。
|
||||
|
||||
|
||||
|
||||
|
298
zh-tw/ch12.md
298
zh-tw/ch12.md
@ -14,11 +14,11 @@
|
||||
|
||||
對未來的看法與推測當然具有很大的主觀性。所以在撰寫本章時,當提及我個人的觀點時會使用第一人稱。您完全可以不同意這些觀點並提出自己的看法,但我希望本章中的概念,至少能成為富有成效的討論出發點,並澄清一些經常被混淆的概念。
|
||||
|
||||
[第1章](ch1.md)概述了本書的目標:探索如何建立**可靠**,**可伸縮**和**可維護**的應用與系統。這一主題貫穿了所有的章節:例如,我們討論了許多有助於提高可靠性的容錯演算法,有助於提高可伸縮性的分割槽,以及有助於提高可維護性的演化與抽象機制。在本章中,我們將把所有這些想法結合在一起,並在它們的基礎上展望未來。我們的目標是,發現如何設計出比現有應用更好的應用 —— 健壯,正確,可演化,且最終對人類有益。
|
||||
[第一章](ch1.md)概述了本書的目標:探索如何建立**可靠**,**可伸縮**和**可維護**的應用與系統。這一主題貫穿了所有的章節:例如,我們討論了許多有助於提高可靠性的容錯演算法,有助於提高可伸縮性的分割槽,以及有助於提高可維護性的演化與抽象機制。在本章中,我們將把所有這些想法結合在一起,並在它們的基礎上展望未來。我們的目標是,發現如何設計出比現有應用更好的應用 —— 健壯,正確,可演化,且最終對人類有益。
|
||||
|
||||
## 資料整合
|
||||
|
||||
本書中反覆出現的主題是,對於任何給定的問題都會有好幾種解決方案,所有這些解決方案都有不同的優缺點與利弊權衡。例如在[第3章](ch3.md)討論儲存引擎時,我們看到了日誌結構儲存,B樹,以及列儲存。在[第5章](ch5.md)討論複製時,我們看到了單領導者,多領導者,和無領導者的方法。
|
||||
本書中反覆出現的主題是,對於任何給定的問題都會有好幾種解決方案,所有這些解決方案都有不同的優缺點與利弊權衡。例如在[第三章](ch3.md)討論儲存引擎時,我們看到了日誌結構儲存,B樹,以及列儲存。在[第五章](ch5.md)討論複製時,我們看到了單領導者,多領導者,和無領導者的方法。
|
||||
|
||||
如果你有一個類似於“我想儲存一些資料並稍後再查詢”的問題,那麼並沒有一種正確的解決方案。但對於不同的具體環境,總會有不同的合適方法。軟體實現通常必須選擇一種特定的方法。使單條程式碼路徑能做到穩定健壯且表現良好已經是一件非常困難的事情了 —— 嘗試在單個軟體中完成所有事情,幾乎可以保證,實現效果會很差。
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
|
||||
例如,為了處理任意關鍵詞的搜尋查詢,將OLTP資料庫與全文搜尋索引整合在一起是很常見的的需求。儘管一些資料庫(例如PostgreSQL)包含了全文索引功能,對於簡單的應用完全夠了【1】,但更復雜的搜尋能力就需要專業的資訊檢索工具了。相反的是,搜尋索引通常不適合作為持久的記錄系統,因此許多應用需要組合這兩種不同的工具以滿足所有需求。
|
||||
|
||||
我們在“[使系統保持同步](ch11.md#使系統保持同步)”中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中衍生的快取,或反規範化的資料版本;將資料灌入機器學習,分類,排名,或推薦系統中;或者基於資料變更傳送通知。
|
||||
我們在“[保持系統同步](ch11.md#保持系統同步)”中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中衍生的快取,或反規範化的資料版本;將資料灌入機器學習,分類,排名,或推薦系統中;或者基於資料變更傳送通知。
|
||||
|
||||
令人驚訝的是,我經常看到軟體工程師做出這樣的陳述:“根據我的經驗,99%的人只需要X”或者 “......不需要X”(對於各種各樣的X)。我認為這種陳述更像是發言人自己的經驗,而不是技術實際上的實用性。可能對資料執行的操作,其範圍極其寬廣。某人認為雞肋而毫無意義的功能可能是別人的核心需求。當你拉高視角,並考慮跨越整個組織範圍的資料流時,資料整合的需求往往就會變得明顯起來。
|
||||
|
||||
@ -40,84 +40,80 @@
|
||||
|
||||
當需要在多個儲存系統中維護相同資料的副本以滿足不同的訪問模式時,你要對輸入和輸出瞭如指掌:哪些資料先寫入,哪些資料表示衍生自哪些來源?如何以正確的格式,將所有資料匯入正確的地方?
|
||||
|
||||
例如,你可能會首先將資料寫入**記錄資料庫**系統,捕獲對該資料庫所做的變更(參閱“[捕獲資料變更](ch11.md#捕獲資料變更)”),然後將變更應用於資料庫中的搜尋索引相同的順序。如果變更資料捕獲(CDC)是更新索引的唯一方式,則可以確定該索引完全派生自記錄系統,因此與其保持一致(除軟體錯誤外)。寫入資料庫是向該系統提供新輸入的唯一方式。
|
||||
例如,你可能會首先將資料寫入**記錄系統**資料庫,捕獲對該資料庫所做的變更(請參閱“[變更資料捕獲](ch11.md#變更資料捕獲)”),然後將變更以相同的順序應用於搜尋索引。如果變更資料捕獲(CDC)是更新索引的唯一方式,則可以確定該索引完全派生自記錄系統,因此與其保持一致(除軟體錯誤外)。寫入資料庫是向該系統提供新輸入的唯一方式。
|
||||
|
||||
允許應用程式直接寫入搜尋索引和資料庫引入瞭如[圖11-4](../img/fig11-4.png)所示的問題,其中兩個客戶端同時傳送衝突的寫入,且兩個儲存系統按不同順序處理它們。在這種情況下,既不是資料庫說了算,也不是搜尋索引說了算,所以它們做出了相反的決定,進入彼此間永續性的不一致狀態。
|
||||
|
||||
如果您可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地衍生出其他資料表示。 這是狀態機複製方法的一個應用,我們在“[全序廣播](ch9.md#全序廣播)”中看到。無論您使用變更資料捕獲還是事件源日誌,都不如僅對全域性順序達成共識更重要。
|
||||
如果您可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地衍生出其他資料表示。 這是狀態機複製方法的一個應用,我們在“[全序廣播](ch9.md#全序廣播)”中看到。無論您使用變更資料捕獲還是事件溯源日誌,都不如簡單的基於全序的決策原則更重要。
|
||||
|
||||
基於事件日誌來更新衍生資料的系統,通常可以做到**確定性**與**冪等性**(參見第478頁的“[冪等性]()”),使得從故障中恢復相當容易。
|
||||
基於事件日誌來更新衍生資料的系統,通常可以做到**確定性**與**冪等性**(請參閱[冪等性](ch11.md#冪等性)”),使得從故障中恢復相當容易。
|
||||
|
||||
#### 衍生資料與分散式事務
|
||||
|
||||
保持不同資料系統彼此一致的經典方法涉及分散式事務,如“[原子提交與兩階段提交(2PC)](ch9.md#原子提交與兩階段提交(2PC))”中所述。與分散式事務相比,使用衍生資料系統的方法如何?
|
||||
|
||||
在抽象層面,它們透過不同的方式達到類似的目標。分散式事務透過**鎖**進行互斥來決定寫入的順序(參閱“[兩階段鎖定(2PL)](ch7.md#兩階段鎖定(2PL))”),而CDC和事件溯源使用日誌進行排序。分散式事務使用原子提交來確保變更只生效一次,而基於日誌的系統通常基於**確定性重試**和**冪等性**。
|
||||
在抽象層面,它們透過不同的方式達到類似的目標。分散式事務透過**鎖**進行互斥來決定寫入的順序(請參閱“[兩階段鎖定(2PL)](ch7.md#兩階段鎖定(2PL))”),而CDC和事件溯源使用日誌進行排序。分散式事務使用原子提交來確保變更只生效一次,而基於日誌的系統通常基於**確定性重試**和**冪等性**。
|
||||
|
||||
最大的不同之處在於事務系統通常提供 **[線性一致性](ch9.md#線性一致性)**,這包含著有用的保證,例如[讀己之寫](ch5.md#讀己之寫)。另一方面,衍生資料系統通常是非同步更新的,因此它們預設不會提供相同的時序保證。
|
||||
最大的不同之處在於事務系統通常提供[線性一致性](ch9.md#線性一致性),這包含著有用的保證,例如[讀己之寫](ch5.md#讀己之寫)。另一方面,衍生資料系統通常是非同步更新的,因此它們預設不會提供相同的時序保證。
|
||||
|
||||
在願意為分散式事務付出代價的有限場景中,它們已被成功應用。但是,我認為XA的容錯能力和效能很差勁(參閱“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”),這嚴重限制了它的實用性。我相信為分散式事務設計一種更好的協議是可行的。但使這樣一種協議被現有工具廣泛接受是很有挑戰的,且不是立竿見影的事。
|
||||
在願意為分散式事務付出代價的有限場景中,它們已被成功應用。但是,我認為XA的容錯能力和效能很差勁(請參閱“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”),這嚴重限制了它的實用性。我相信為分散式事務設計一種更好的協議是可行的。但使這樣一種協議被現有工具廣泛接受是很有挑戰的,且不是立竿見影的事。
|
||||
|
||||
在沒有廣泛支援的良好分散式事務協議的情況下,我認為基於日誌的衍生資料是整合不同資料系統的最有前途的方法。然而,諸如讀己之寫的保證是有用的,我認為告訴所有人“最終一致性是不可避免的 —— 忍一忍並學會和它打交道”是沒有什麼建設性的(至少在缺乏**如何**應對的良好指導時)。
|
||||
|
||||
在“[將事情做正確](#將事情做正確)”中,我們將討論一些在非同步衍生系統之上實現更強保障的方法,並邁向分散式事務和基於日誌的非同步系統之間的中間地帶。
|
||||
|
||||
#### 全域性有序的限制
|
||||
|
||||
對於足夠小的系統,構建一個完全有序的事件日誌是完全可行的(正如單主複製資料庫的流行所證明的那樣,這正好建立了這樣一種日誌)。但是,隨著系統向更大更復雜的工作負載伸縮,限制開始出現:
|
||||
|
||||
* 在大多數情況下,構建完全有序的日誌,需要所有事件彙集於決定順序的單個領導節點。如果事件吞吐量大於單臺計算機的處理能力,則需要將其分割到多臺計算機上(參見“[分割槽日誌](ch11.md#分割槽日誌)”)。然後兩個不同分割槽中的事件順序關係就不明確了。
|
||||
#### 全序的限制
|
||||
|
||||
對於足夠小的系統,構建一個完全有序的事件日誌是完全可行的(正如單主複製資料庫的流行所證明的那樣,它正好建立了這樣一種日誌)。但是,隨著系統向更大更復雜的工作負載伸縮,限制開始出現:
|
||||
|
||||
* 在大多數情況下,構建完全有序的日誌,需要所有事件彙集於決定順序的**單個領導者節點**。如果事件吞吐量大於單臺計算機的處理能力,則需要將其分割槽到多臺計算機上(請參閱“[分割槽日誌](ch11.md#分割槽日誌)”)。然後兩個不同分割槽中的事件順序關係就不明確了。
|
||||
* 如果伺服器分佈在多個**地理位置分散**的資料中心上,例如為了容忍整個資料中心掉線,您通常在每個資料中心都有單獨的主庫,因為網路延遲會導致同步的跨資料中心協調效率低下(請參閱“[多主複製](ch5.md#多主複製)“)。這意味著源自兩個不同資料中心的事件順序未定義。
|
||||
* 將應用程式部署為微服務時(請參閱“[服務中的資料流:REST與RPC](ch4.md#服務中的資料流:REST與RPC)”),常見的設計選擇是將每個服務及其持久狀態作為獨立單元進行部署,服務之間不共享持久狀態。當兩個事件來自不同的服務時,這些事件間的順序未定義。
|
||||
* 某些應用程式在客戶端儲存狀態,該狀態在使用者輸入時立即更新(無需等待伺服器確認),甚至可以繼續離線工作(請參閱“[需要離線操作的客戶端](ch5.md#需要離線操作的客戶端)”)。對於這樣的應用程式,客戶端和伺服器很可能以不同的順序看到事件。
|
||||
|
||||
在形式上,決定事件的全域性順序稱為**全序廣播**,相當於**共識**(請參閱“[共識演算法和全序廣播](ch9.md#共識演算法和全序廣播)”)。大多數共識演算法都是針對單個節點的吞吐量足以處理整個事件流的情況而設計的,並且這些演算法不提供多個節點共享事件排序工作的機制。設計可以伸縮至單個節點的吞吐量之上,且在地理位置分散的環境中仍然工作良好的的共識演算法仍然是一個開放的研究問題。
|
||||
|
||||
* 將應用程式部署為微服務時(請參閱第125頁上的“[服務中的資料流:REST與RPC](ch4.md#服務中的資料流:REST與RPC)”),常見的設計選擇是將每個服務及其持久狀態作為獨立單元進行部署,服務之間不共享持久狀態。當兩個事件來自不同的服務時,這些事件間的順序未定義。
|
||||
#### 排序事件以捕獲因果關係
|
||||
|
||||
|
||||
* 某些應用程式在客戶端儲存狀態,該狀態在使用者輸入時立即更新(無需等待伺服器確認),甚至可以繼續離線工作(參閱“[離線操作的客戶端](ch5.md#離線操作的客戶端)”)。有了這樣的應用程式,客戶端和伺服器很可能以不同的順序看到事件。
|
||||
|
||||
在形式上,決定事件的全域性順序稱為**全序廣播**,相當於**共識**(參閱“[共識演算法和全序廣播](ch9.md#共識演算法和全序廣播)”)。大多數共識演算法都是針對單個節點的吞吐量足以處理整個事件流的情況而設計的,並且這些演算法不提供多個節點共享事件排序工作的機制。設計可以伸縮至單個節點的吞吐量之上,且在地理散佈環境中仍然工作良好的的共識演算法仍然是一個開放的研究問題。
|
||||
|
||||
#### 排序事件以捕捉因果關係
|
||||
|
||||
在事件之間不存在因果關係的情況下,缺乏全域性順序並不是一個大問題,因為併發事件可以任意排序。其他一些情況很容易處理:例如,當同一物件有多個更新時,它們可以透過將特定物件ID的所有更新路由到相同的日誌分割槽來完全排序。然而,因果關係有時會以更微妙的方式出現(參閱“[順序和因果關係](ch8.md#順序和因果關係)”)。
|
||||
在事件之間不存在因果關係的情況下,全序的缺乏並不是一個大問題,因為併發事件可以任意排序。其他一些情況很容易處理:例如,當同一物件有多個更新時,它們可以透過將特定物件ID的所有更新路由到相同的日誌分割槽來完全排序。然而,因果關係有時會以更微妙的方式出現(請參閱“[順序與因果關係](ch9.md#順序與因果關係)”)。
|
||||
|
||||
例如,考慮一個社交網路服務,以及一對曾處於戀愛關係但剛分手的使用者。其中一個使用者將另一個使用者從好友中移除,然後向剩餘的好友傳送訊息,抱怨他們的前任。使用者的心思是他們的前任不應該看到這些粗魯的訊息,因為訊息是在好友狀態解除後傳送的。
|
||||
|
||||
但是如果好友關係狀態與訊息儲存在不同的地方,在這樣一個系統中,可能會出現**解除好友**事件與**傳送訊息**事件之間的因果依賴丟失的情況。如果因果依賴關係沒有被捕捉到,則傳送有關新訊息的通知的服務可能會在**解除好友**事件之前處理**傳送訊息**事件,從而錯誤地向前任傳送通知。
|
||||
|
||||
在本例中,通知實際上是訊息和好友列表之間的連線,使得它與我們先前討論的連線的時間問題有關(請參閱第475頁的“[連線的時間依賴性](ch11.md#連線的時間依賴性)”)。不幸的是,這個問題似乎並沒有一個簡單的答案【2,3】。起點包括:
|
||||
在本例中,通知實際上是訊息和好友列表之間的連線,使得它與我們先前討論的連線的時序問題有關(請參閱“[連線的時間依賴性](ch11.md#連線的時間依賴性)”)。不幸的是,這個問題似乎並沒有一個簡單的答案【2,3】。起點包括:
|
||||
|
||||
* 邏輯時間戳可以提供無需協調的全域性順序(參見“[序列號排序](ch8.md#序列號排序)”),因此它們可能有助於全序廣播不可行的情況。但是,他們仍然要求收件人處理不按順序傳送的事件,並且需要傳遞其他元資料。
|
||||
* 如果你可以記錄一個事件來記錄使用者在做出決定之前所看到的系統狀態,並給該事件一個唯一的識別符號,那麼後面的任何事件都可以引用該事件識別符號來記錄因果關係【4】 。我們將在“[讀也是事件](#讀也是事件)”中回到這個想法。
|
||||
* 衝突解決演算法(請參閱“[自動衝突解決](ch5.md#自動衝突解決)”)有助於處理以意外順序傳遞的事件。它們對於維護狀態很有用,但如果行為有外部副作用(例如,也許,隨著時間的推移,應用開發模式將出現,使得能夠有效地捕獲因果依賴關係,並且保持正確的衍生狀態,而不會迫使所有事件經歷全序廣播的瓶頸)。
|
||||
* 邏輯時間戳可以提供無需協調的全域性順序(請參閱“[序列號順序](ch9.md#序列號順序)”),因此它們可能有助於全序廣播不可行的情況。但是,他們仍然要求收件人處理不按順序傳送的事件,並且需要傳遞其他元資料。
|
||||
* 如果你可以記錄一個事件來記錄使用者在做出決定之前所看到的系統狀態,並給該事件一個唯一的識別符號,那麼後面的任何事件都可以引用該事件識別符號來記錄因果關係【4】。我們將在“[讀也是事件](#讀也是事件)”中回到這個想法。
|
||||
* 衝突解決演算法(請參閱“[自動衝突解決](ch5.md#自動衝突解決)”)有助於處理以意外順序傳遞的事件。它們對於維護狀態很有用,但如果行為有外部副作用(例如,給使用者傳送通知),就沒什麼幫助了。
|
||||
|
||||
也許,隨著時間的推移,應用開發模式將出現,使得能夠有效地捕獲因果依賴關係,並且保持正確的衍生狀態,而不會迫使所有事件經歷全序廣播的瓶頸)。
|
||||
|
||||
### 批處理與流處理
|
||||
|
||||
我會說資料整合的目標是,確保資料最終能在所有正確的地方表現出正確的形式。這樣做需要消費輸入,轉換,連線,過濾,聚合,訓練模型,評估,以及最終寫出適當的輸出。批處理和流處理是實現這一目標的工具。
|
||||
|
||||
批處理和流處理的輸出是衍生資料集,例如搜尋索引,物化檢視,向用戶顯示的建議,聚合指標等(請參閱“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”和“[流處理的用法](ch11.md#流處理的用法)”)。
|
||||
批處理和流處理的輸出是衍生資料集,例如搜尋索引,物化檢視,向用戶顯示的建議,聚合指標等(請參閱“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”和“[流處理的應用](ch11.md#流處理的應用)”)。
|
||||
|
||||
正如我們在[第10章](ch10.md)和[第11章](ch11.md)中看到的,批處理和流處理有許多共同的原則,主要的根本區別在於流處理器在無限資料集上執行,而批處理輸入是已知的有限大小。處理引擎的實現方式也有很多細節上的差異,但是這些區別已經開始模糊。
|
||||
正如我們在[第十章](ch10.md)和[第十一章](ch11.md)中看到的,批處理和流處理有許多共同的原則,主要的根本區別在於流處理器在無限資料集上執行,而批處理輸入是已知的有限大小。處理引擎的實現方式也有很多細節上的差異,但是這些區別已經開始模糊。
|
||||
|
||||
Spark在批處理引擎上執行流處理,將流分解為**微批次(microbatches)**,而Apache Flink則在流處理引擎上執行批處理【5】。原則上,一種型別的處理可以用另一種型別來模擬,但是效能特徵會有所不同:例如,在跳躍或滑動視窗上,微批次可能表現不佳【6】。
|
||||
|
||||
#### 維護衍生狀態
|
||||
|
||||
批處理有著很強的函式式風格(即使其程式碼不是用函式式語言編寫的):它鼓勵確定性的純函式,其輸出僅依賴於輸入,除了顯式輸出外沒有副作用,將輸入視作不可變的,且輸出是僅追加的。流處理與之類似,但它擴充套件了運算元以允許受管理的,容錯的狀態(參閱“[失敗後重建狀態”](ch11.md#失敗後重建狀態))。
|
||||
批處理有著很強的函式式風格(即使其程式碼不是用函式式語言編寫的):它鼓勵確定性的純函式,其輸出僅依賴於輸入,除了顯式輸出外沒有副作用,將輸入視作不可變的,且輸出是僅追加的。流處理與之類似,但它擴充套件了運算元以允許受管理的、容錯的狀態(請參閱“[失敗後重建狀態”](ch11.md#失敗後重建狀態))。
|
||||
|
||||
具有良好定義的輸入和輸出的確定性函式的原理不僅有利於容錯(參見“[冪等性](ch11.md#冪等性)”),也簡化了有關組織中資料流的推理【7】。無論衍生資料是搜尋索引,統計模型還是快取,採用這種觀點思考都是很有幫助的:將其視為從一個東西衍生出另一個的資料管道,將一個系統的狀態變更推送至函式式應用程式碼中,並將其效果應用至衍生系統中。
|
||||
具有良好定義的輸入和輸出的確定性函式的原理不僅有利於容錯(請參閱“[冪等性](ch11.md#冪等性)”),也簡化了有關組織中資料流的推理【7】。無論衍生資料是搜尋索引、統計模型還是快取,採用這種觀點思考都是很有幫助的:將其視為從一個東西衍生出另一個的資料管道,透過函式式應用程式碼推送一個系統的狀態變更,並將其效果應用至衍生系統中。
|
||||
|
||||
原則上,衍生資料系統可以同步地維護,就像關係資料庫在與被索引表寫入操作相同的事務中同步更新輔助索引一樣。然而,非同步是基於事件日誌的系統穩健的原因:它允許系統的一部分故障被抑制在本地,而如果任何一個參與者失敗,分散式事務將中止,因此它們傾向於透過將故障傳播到系統的其餘部分來放大故障(請參閱“[分散式事務的限制](ch9.md#分散式事務的限制)”)。
|
||||
原則上,衍生資料系統可以同步地維護,就像關係資料庫在與索引表寫入操作相同的事務中同步更新輔助索引一樣。然而,非同步是使基於事件日誌的系統穩健的原因:它允許系統的一部分故障被抑制在本地。而如果任何一個參與者失敗,分散式事務將中止,因此它們傾向於透過將故障傳播到系統的其餘部分來放大故障(請參閱“[分散式事務的限制](ch9.md#分散式事務的限制)”)。
|
||||
|
||||
我們在“[分割槽與次級索引](ch6.md#分割槽與次級索引)”中看到,二級索引經常跨越分割槽邊界。具有二級索引的分割槽系統需要將寫入傳送到多個分割槽(如果索引按關鍵詞分割槽的話)或將讀取傳送到所有分割槽(如果索引是按文件分割槽的話)。如果索引是非同步維護的,這種交叉分割槽通訊也是最可靠和最可伸縮的【8】(另請參閱“[多分割槽資料處理](ch11.md#多分割槽資料處理)”)。
|
||||
我們在“[分割槽與次級索引](ch6.md#分割槽與次級索引)”中看到,二級索引經常跨越分割槽邊界。具有二級索引的分割槽系統需要將寫入傳送到多個分割槽(如果索引按關鍵詞分割槽的話)或將讀取傳送到所有分割槽(如果索引是按文件分割槽的話)。如果索引是非同步維護的,這種跨分割槽通訊也是最可靠和最可伸縮的【8】(另請參閱“[多分割槽資料處理](多分割槽資料處理)”)。
|
||||
|
||||
#### 應用演化後重新處理資料
|
||||
|
||||
在維護衍生資料時,批處理和流處理都是有用的。流處理允許將輸入中的變化以低延遲反映在衍生檢視中,而批處理允許重新處理大量累積的歷史資料以便將新檢視匯出到現有資料集上。
|
||||
|
||||
特別是,重新處理現有資料為維護系統提供了一個良好的機制,演化並支援新功能和需求變更(參見[第4章](ch4.md))。不需要重新進行處理,模式演化僅限於簡單的變化,例如向記錄中新增新的可選欄位或新增新型別的記錄。無論是在寫模式還是在讀模式中都是如此(參閱“[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”)。另一方面,透過重新處理,可以將資料集重組為一個完全不同的模型,以便更好地滿足新的要求。
|
||||
特別是,重新處理現有資料為維護系統、演化並支援新功能和需求變更提供了一個良好的機制(請參閱[第四章](ch4.md))。沒有重新進行處理,模式演化將僅限於簡單的變化,例如向記錄中新增新的可選欄位或新增新型別的記錄。無論是在寫時模式還是在讀時模式中都是如此(請參閱“[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”)。另一方面,透過重新處理,可以將資料集重組為一個完全不同的模型,以便更好地滿足新的要求。
|
||||
|
||||
> ### 鐵路上的模式遷移
|
||||
>
|
||||
@ -125,49 +121,46 @@
|
||||
>
|
||||
> 在1846年最終確定了一個標準軌距之後,其他軌距的軌道必須轉換 —— 但是如何在不停運火車線路的情況下進行數月甚至數年的遷移?解決的辦法是首先透過新增第三條軌道將軌道轉換為**雙軌距(dual guage)**或**混合軌距**。這種轉換可以逐漸完成,當完成時,兩種軌距的列車都可以線上路上跑,使用三條軌道中的兩條。事實上,一旦所有的列車都轉換成標準軌距,那麼可以移除提供非標準軌距的軌道。
|
||||
>
|
||||
> 以這種方式“再加工”現有的軌道,讓新舊版本並存,可以在幾年的時間內逐漸改變軌距。然而,這是一項昂貴的事業,這就是今天非標準軌距仍然存在的原因。例如,舊金山灣區的BART系統使用與美國大部分地區不同的軌距。
|
||||
> 以這種方式“再加工”現有的軌道,讓新舊版本並存,可以在幾年的時間內逐漸改變軌距。然而,這是一項昂貴的事業,這就是今天非標準軌距仍然存在的原因。例如,舊金山灣區的BART系統使用了與美國大部分地區不同的軌距。
|
||||
|
||||
衍生檢視允許**漸進演化(gradual evolution)**。如果你想重新構建資料集,不需要執行遷移,例如**突然切換**。取而代之的是,你可以將舊架構和新架構並排維護為相同基礎資料上的兩個獨立衍生檢視。然後可以開始將少量使用者轉移到新檢視,以測試其效能並發現任何錯誤,而大多數使用者仍然會被路由到舊檢視。你可以逐漸地增加訪問新檢視的使用者比例,最終可以刪除舊檢視【10】。
|
||||
衍生檢視允許**漸進演化(gradual evolution)**。如果你想重新構建資料集,不需要執行突然切換式的遷移。取而代之的是,你可以將舊架構和新架構並排維護為相同基礎資料上的兩個獨立衍生檢視。然後可以開始將少量使用者轉移到新檢視,以測試其效能並發現任何錯誤,而大多數使用者仍然會被路由到舊檢視。你可以逐漸地增加訪問新檢視的使用者比例,最終可以刪除舊檢視【10】。
|
||||
|
||||
這種逐漸遷移的美妙之處在於,如果出現問題,每個階段的過程都很容易逆轉:你始終有一個可以回滾的可用系統。透過降低不可逆損害的風險,你能對繼續前進更有信心,從而更快地改善系統【11】。
|
||||
|
||||
#### Lambda架構
|
||||
|
||||
如果批處理用於重新處理歷史資料,並且流處理用於處理最近的更新,那麼如何將這兩者結合起來?Lambda架構【12】是這方面的一個建議,引起了很多關注。
|
||||
如果批處理用於重新處理歷史資料,而流處理用於處理最近的更新,那麼如何將這兩者結合起來?Lambda架構【12】是這方面的一個建議,引起了很多關注。
|
||||
|
||||
Lambda架構的核心思想是透過將不可變事件附加到不斷增長的資料集來記錄傳入資料,這類似於事件溯源(參閱“[事件溯源](ch11.md#事件溯源)”)。為了從這些事件中衍生出讀取最佳化的檢視, Lambda架構建議並行執行兩個不同的系統:批處理系統(如Hadoop MapReduce)和獨立的流處理系統(如Storm)。
|
||||
Lambda架構的核心思想是透過將不可變事件附加到不斷增長的資料集來記錄傳入資料,這類似於事件溯源(請參閱“[事件溯源](ch11.md#事件溯源)”)。為了從這些事件中衍生出讀取最佳化的檢視, Lambda架構建議並行執行兩個不同的系統:批處理系統(如Hadoop MapReduce)和獨立的流處理系統(如Storm)。
|
||||
|
||||
在Lambda方法中,流處理器消耗事件並快速生成對檢視的近似更新;批處理器稍後將使用同一組事件並生成衍生檢視的更正版本。這個設計背後的原因是批處理更簡單,因此不易出錯,而流處理器被認為是不太可靠和難以容錯的(請參閱“[故障容錯]()”)。而且,流處理可以使用快速近似演算法,而批處理使用較慢的精確演算法。
|
||||
在Lambda方法中,流處理器消耗事件並快速生成對檢視的近似更新;批處理器稍後將使用同一組事件並生成衍生檢視的更正版本。這個設計背後的原因是批處理更簡單,因此不易出錯,而流處理器被認為是不太可靠和難以容錯的(請參閱“[容錯](ch11.md#容錯)”)。而且,流處理可以使用快速近似演算法,而批處理使用較慢的精確演算法。
|
||||
|
||||
Lambda架構是一種有影響力的想法,它將資料系統的設計變得更好,尤其是透過推廣這樣的原則:在不可變事件流上建立衍生檢視,並在需要時重新處理事件。但是我也認為它有一些實際問題:
|
||||
|
||||
* 在批處理和流處理框架中維護相同的邏輯是很顯著的額外工作。雖然像Summingbird 【13】這樣的庫提供了一種可以在批處理和流處理的上下文中執行的計算抽象。除錯,調整和維護兩個不同系統的操作複雜性依然存在【14】。
|
||||
* 在批處理和流處理框架中維護相同的邏輯是很顯著的額外工作。雖然像Summingbird【13】這樣的庫提供了一種可以在批處理和流處理的上下文中執行的計算抽象。除錯、調整和維護兩個不同系統的操作複雜性依然存在【14】。
|
||||
* 由於流管道和批處理管道產生獨立的輸出,因此需要合併它們以響應使用者請求。如果計算是基於滾動視窗的簡單聚合,則合併相當容易,但如果檢視基於更復雜的操作(例如連線和會話化)而匯出,或者輸出不是時間序列,則會變得非常困難。
|
||||
* 儘管有能力重新處理整個歷史資料集是很好的,但在大型資料集上這樣做經常會開銷巨大。因此,批處理流水線通常需要設定為處理增量批處理(例如,在每小時結束時處理一小時的資料),而不是重新處理所有內容。這引發了“[關於時間的推理](ch11.md#關於時間的推理)”中討論的問題,例如處理分段器和處理跨批次邊界的視窗。增加批次計算會增加複雜性,使其更類似於流式傳輸層,這與保持批處理層儘可能簡單的目標背道而馳。
|
||||
* 儘管有能力重新處理整個歷史資料集是很好的,但在大型資料集上這樣做經常會開銷巨大。因此,批處理流水線通常需要設定為處理增量批處理(例如,在每小時結束時處理一小時的資料),而不是重新處理所有內容。這引發了“[時間推理](ch11.md#時間推理)”中討論的問題,例如處理滯留事件和處理跨批次邊界的視窗。增量化批處理計算會增加複雜性,使其更類似於流式傳輸層,這與保持批處理層儘可能簡單的目標背道而馳。
|
||||
|
||||
#### 統一批處理和流處理
|
||||
|
||||
最近的工作使得Lambda架構的優點在沒有其缺點的情況下得以實現,允許批處理計算(重新處理歷史資料)和流計算(處理事件到達時)在同一個系統中實現【15】。
|
||||
|
||||
在一個系統中統一批處理和流處理需要以下功能,這些功能越來越廣泛:
|
||||
|
||||
* 透過處理最近事件流的相同處理引擎來重放歷史事件的能力。例如,基於日誌的訊息代理可以重放訊息(參閱“[重放舊訊息](ch11.md#重放舊訊息)”),某些流處理器可以從HDFS等分散式檔案系統讀取輸入。
|
||||
* 對於流處理器來說,恰好一次語義 —— 即確保輸出與未發生故障的輸出相同,即使事實上發生故障(參閱“[故障容錯](ch11.md#故障容錯)”)。與批處理一樣,這需要丟棄任何失敗任務的部分輸出。
|
||||
* 按事件時間進行視窗化的工具,而不是按處理時間進行視窗化,因為處理歷史事件時,處理時間毫無意義(參閱“[時間推理](ch11.md#時間推理)”)。例如,Apache Beam提供了用於表達這種計算的API,然後可以使用Apache Flink或Google Cloud Dataflow執行。
|
||||
|
||||
最近的工作使得Lambda架構的優點在沒有其缺點的情況下得以實現,允許批處理計算(重新處理歷史資料)和流計算(在事件到達時即處理)在同一個系統中實現【15】。
|
||||
|
||||
在一個系統中統一批處理和流處理需要以下功能,這些功能也正在越來越廣泛地被提供:
|
||||
|
||||
* 透過處理最近事件流的相同處理引擎來重播歷史事件的能力。例如,基於日誌的訊息代理可以重播訊息(請參閱“[重播舊訊息](ch11.md#重播舊訊息)”),某些流處理器可以從HDFS等分散式檔案系統讀取輸入。
|
||||
* 對於流處理器來說,恰好一次語義 —— 即確保輸出與未發生故障的輸出相同,即使事實上發生故障(請參閱“[容錯](ch11.md#容錯)”)。與批處理一樣,這需要丟棄任何失敗任務的部分輸出。
|
||||
* 按事件時間進行視窗化的工具,而不是按處理時間進行視窗化,因為處理歷史事件時,處理時間毫無意義(請參閱“[時間推理](ch11.md#時間推理)”)。例如,Apache Beam提供了用於表達這種計算的API,可以在Apache Flink或Google Cloud Dataflow使用。
|
||||
|
||||
|
||||
## 分拆資料庫
|
||||
|
||||
在最抽象的層面上,資料庫,Hadoop和作業系統都發揮相同的功能:它們儲存一些資料,並允許你處理和查詢這些資料【16】。資料庫將資料儲存為特定資料模型的記錄(表中的行、文件、圖中的頂點等),而作業系統的檔案系統則將資料儲存在檔案中 —— 但其核心都是“資訊管理”系統【17】。正如我們在[第10章](ch10.md)中看到的,Hadoop生態系統有點像Unix的分散式版本。
|
||||
在最抽象的層面上,資料庫,Hadoop和作業系統都發揮相同的功能:它們儲存一些資料,並允許你處理和查詢這些資料【16】。資料庫將資料儲存為特定資料模型的記錄(表中的行、文件、圖中的頂點等),而作業系統的檔案系統則將資料儲存在檔案中 —— 但其核心都是“資訊管理”系統【17】。正如我們在[第十章](ch10.md)中看到的,Hadoop生態系統有點像Unix的分散式版本。
|
||||
|
||||
當然,有很多實際的差異。例如,許多檔案系統都不能很好地處理包含1000萬個小檔案的目錄,而包含1000萬個小記錄的資料庫完全是尋常而不起眼的。無論如何,作業系統和資料庫之間的相似之處和差異值得探討。
|
||||
|
||||
Unix和關係資料庫以非常不同的哲學來處理資訊管理問題。 Unix認為它的目的是為程式設計師提供一種相當低層次的硬體的邏輯抽象,而關係資料庫則希望為應用程式設計師提供一種高層次的抽象,以隱藏磁碟上資料結構的複雜性,併發性,崩潰恢復以及等等。 Unix發展出的管道和檔案只是位元組序列,而資料庫則發展出了SQL和事務。
|
||||
Unix和關係資料庫以非常不同的哲學來處理資訊管理問題。Unix認為它的目的是為程式設計師提供一種相當低層次的硬體的邏輯抽象,而關係資料庫則希望為應用程式設計師提供一種高層次的抽象,以隱藏磁碟上資料結構的複雜性,併發性,崩潰恢復等等。Unix發展出的管道和檔案只是位元組序列,而資料庫則發展出了SQL和事務。
|
||||
|
||||
哪種方法更好?當然這取決於你想要的是什麼。 Unix是“簡單的”,因為它是硬體資源相當薄的包裝;關係資料庫是“更簡單”的,因為一個簡短的宣告性查詢可以利用很多強大的基礎設施(查詢最佳化,索引,連線方法,併發控制,複製等),而不需要查詢的作者理解其實現細節。
|
||||
哪種方法更好?當然這取決於你想要的是什麼。 Unix是“簡單的”,因為它是對硬體資源相當薄的包裝;關係資料庫是“更簡單”的,因為一個簡短的宣告性查詢可以利用很多強大的基礎設施(查詢最佳化,索引,連線方法,併發控制,複製等),而不需要查詢的作者理解其實現細節。
|
||||
|
||||
這些哲學之間的矛盾已經持續了幾十年(Unix和關係模型都出現在70年代初),仍然沒有解決。例如,我將NoSQL運動解釋為,希望將類Unix的低級別抽象方法應用於分散式OLTP資料儲存的領域。
|
||||
|
||||
@ -177,12 +170,12 @@
|
||||
|
||||
在本書的過程中,我們討論了資料庫提供的各種功能及其工作原理,其中包括:
|
||||
|
||||
* 次級索引,使您可以根據欄位的值有效地搜尋記錄(參閱“[其他索引結構](ch3.md#其他索引結構)”)
|
||||
* 物化檢視,這是一種預計算的查詢結果快取(參閱“[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”)
|
||||
* 複製日誌,保持其他節點上資料的副本最新(參閱“[複製日誌的實現](ch5.md#複製日誌的實現)”)
|
||||
* 全文搜尋索引,允許在文字中進行關鍵字搜尋(參見“[全文搜尋和模糊索引](ch3.md#全文搜尋和模糊索引)”)內置於某些關係資料庫【1】
|
||||
* 次級索引,使您可以根據欄位的值有效地搜尋記錄(請參閱“[其他索引結構](ch3.md#其他索引結構)”)
|
||||
* 物化檢視,這是一種預計算的查詢結果快取(請參閱“[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”)
|
||||
* 複製日誌,保持其他節點上資料的副本最新(請參閱“[複製日誌的實現](ch5.md#複製日誌的實現)”)
|
||||
* 全文搜尋索引,允許在文字中進行關鍵字搜尋(請參閱“[全文搜尋和模糊索引](ch3.md#全文搜4索和模糊索引)”),也內置於某些關係資料庫【1】
|
||||
|
||||
在[第10章](ch10.md)和[第11章](ch11.md)中,出現了類似的主題。我們討論瞭如何構建全文搜尋索引(請參閱第357頁上的“[批處理工作流的輸出]()”),瞭解有關例項化檢視維護(請參閱“[維護例項化檢視]()”一節第437頁)以及有關將變更從資料庫複製到衍生資料系統(請參閱第454頁的“[變更資料捕獲]()”)。
|
||||
在[第十章](ch10.md)和[第十一章](ch11.md)中,出現了類似的主題。我們討論瞭如何構建全文搜尋索引(請參閱“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”),瞭解瞭如何維護物化檢視(請參閱“[維護物化檢視](ch11.md#維護物化檢視)”)以及如何將變更從資料庫複製到衍生資料系統(請參閱“[變更資料捕獲](ch11.md#變更資料捕獲)”)。
|
||||
|
||||
資料庫中內建的功能與人們用批處理和流處理器構建的衍生資料系統似乎有相似之處。
|
||||
|
||||
@ -190,70 +183,70 @@
|
||||
|
||||
想想當你執行`CREATE INDEX`在關係資料庫中建立一個新的索引時會發生什麼。資料庫必須掃描表的一致性快照,挑選出所有被索引的欄位值,對它們進行排序,然後寫出索引。然後它必須處理自一致快照以來所做的寫入操作(假設表在建立索引時未被鎖定,所以寫操作可能會繼續)。一旦完成,只要事務寫入表中,資料庫就必須繼續保持索引最新。
|
||||
|
||||
此過程非常類似於設定新的從庫副本(參閱“[設定新的追隨者]()”),也非常類似於流處理系統中的**引導(bootstrap)** 變更資料捕獲(請參閱第455頁的“[初始快照]()”)。
|
||||
此過程非常類似於設定新的從庫副本(請參閱“[設定新從庫](ch5.md#設定新從庫)”),也非常類似於流處理系統中的**引導(bootstrap)** 變更資料捕獲(請參閱“[初始快照](ch11.md#初始快照)”)。
|
||||
|
||||
無論何時執行`CREATE INDEX`,資料庫都會重新處理現有資料集(如第494頁的“[重新處理應用程式資料的演變資料]()”中所述),並將該索引作為新檢視匯出到現有資料上。現有資料可能是狀態的快照,而不是所有發生變化的日誌,但兩者密切相關(請參閱“[狀態,資料流和不變性]()”第459頁)。
|
||||
無論何時執行`CREATE INDEX`,資料庫都會重新處理現有資料集(如“[應用演化後重新處理資料](#應用演化後重新處理資料)”中所述),並將該索引作為新檢視匯出到現有資料上。現有資料可能是狀態的快照,而不是所有發生變化的日誌,但兩者密切相關(請參閱“[狀態、流和不變性](ch11.md#狀態、流和不變性)”)。
|
||||
|
||||
#### 一切的元資料庫
|
||||
|
||||
有鑑於此,我認為整個組織的資料流開始像一個巨大的資料庫【7】。每當批處理,流或ETL過程將資料從一個地方傳輸到另一個地方並組裝時,它表現地就像資料庫子系統一樣,使索引或物化檢視保持最新。
|
||||
有鑑於此,我認為整個組織的資料流開始像一個巨大的資料庫【7】。每當批處理、流或ETL過程將資料從一個地方傳輸到另一個地方並組裝時,它表現地就像資料庫子系統一樣,使索引或物化檢視保持最新。
|
||||
|
||||
從這種角度來看,批處理和流處理器就像觸發器,儲存過程和物化檢視維護例程的精細實現。它們維護的衍生資料系統就像不同的索引型別。例如,關係資料庫可能支援B樹索引,雜湊索引,空間索引(請參閱第79頁的“[多列索引]()”)以及其他型別的索引。在新興的衍生資料系統架構中,不是將這些設施作為單個整合資料庫產品的功能實現,而是由各種不同的軟體提供,執行在不同的機器上,由不同的團隊管理。
|
||||
從這種角度來看,批處理和流處理器就像精心實現的觸發器、儲存過程和物化檢視維護例程。它們維護的衍生資料系統就像不同的索引型別。例如,關係資料庫可能支援B樹索引、雜湊索引、空間索引(請參閱“[多列索引](ch3.md#多列索引)”)以及其他型別的索引。在新興的衍生資料系統架構中,不是將這些設施作為單個整合資料庫產品的功能實現,而是由各種不同的軟體提供,執行在不同的機器上,由不同的團隊管理。
|
||||
|
||||
這些發展在未來將會把我們帶到哪裡?如果我們從沒有適合所有訪問模式的單一資料模型或儲存格式的前提出發,我推測有兩種途徑可以將不同的儲存和處理工具組合成一個有凝聚力的系統:
|
||||
|
||||
**聯合資料庫:統一讀取**
|
||||
|
||||
可以為各種各樣的底層儲存引擎和處理方法提供一個統一的查詢介面 —— 一種稱為**聯合資料庫(federated database)** 或**多型儲存(polystore)** 的方法【18,19】。例如,PostgreSQL的外部資料包裝器功能符合這種模式【20】。需要專用資料模型或查詢介面的應用程式仍然可以直接訪問底層儲存引擎,而想要組合來自不同位置的資料的使用者可以透過聯合介面輕鬆完成操作。
|
||||
可以為各種各樣的底層儲存引擎和處理方法提供一個統一的查詢介面 —— 一種稱為**聯合資料庫(federated database)**或**多型儲存(polystore)**的方法【18,19】。例如,PostgreSQL的**外部資料包裝器(foreign data wrapper)**功能符合這種模式【20】。需要專用資料模型或查詢介面的應用程式仍然可以直接訪問底層儲存引擎,而想要組合來自不同位置的資料的使用者可以透過聯合介面輕鬆完成操作。
|
||||
|
||||
聯合查詢介面遵循著單一整合系統與關係型模型的傳統,帶有高階查詢語言和優雅的語義,但實現起來非常複雜。
|
||||
聯合查詢介面遵循著單一整合系統的關係型傳統,帶有高階查詢語言和優雅的語義,但實現起來非常複雜。
|
||||
|
||||
**分拆資料庫:統一寫入**
|
||||
|
||||
雖然聯合能解決跨多個不同系統的只讀查詢問題,但它並沒有很好的解決跨系統**同步**寫入的問題。我們說過,在單個數據庫中,建立一致的索引是一項內建功能。當我們構建多個儲存系統時,我們同樣需要確保所有資料變更都會在所有正確的位置結束,即使在出現故障時也是如此。將儲存系統可靠地插接在一起(例如,透過變更資料捕獲和事件日誌)更容易,就像將資料庫的索引維護功能以可以跨不同技術同步寫入的方式分開【7,21】。
|
||||
雖然聯合能解決跨多個不同系統的只讀查詢問題,但它並沒有很好的解決跨系統**同步**寫入的問題。我們說過,在單個數據庫中,建立一致的索引是一項內建功能。當我們構建多個儲存系統時,我們同樣需要確保所有資料變更都會在所有正確的位置結束,即使在出現故障時也是如此。想要更容易地將儲存系統可靠地插接在一起(例如,透過變更資料捕獲和事件日誌),就像將資料庫的索引維護功能以可以跨不同技術同步寫入的方式分開【7,21】。
|
||||
|
||||
分拆方法遵循Unix傳統的小型工具,它可以很好地完成一件事【22】,透過統一的低階API(管道)進行通訊,並且可以使用更高階的語言進行組合(shell)【16】 。
|
||||
分拆方法遵循Unix傳統的小型工具,它可以很好地完成一件事【22】,透過統一的低層級API(管道)進行通訊,並且可以使用更高層級的語言進行組合(shell)【16】 。
|
||||
|
||||
#### 開展分拆工作
|
||||
|
||||
聯合和分拆是一個硬幣的兩面:用不同的元件構成可靠,可伸縮和可維護的系統。聯合只讀查詢需要將一個數據模型對映到另一個數據模型,這需要一些思考,但最終還是一個可解決的問題。我認為同步寫入到幾個儲存系統是更困難的工程問題,所以我將重點關注它。
|
||||
聯合和分拆是一個硬幣的兩面:用不同的元件構成可靠、 可伸縮和可維護的系統。聯合只讀查詢需要將一個數據模型對映到另一個數據模型,這需要一些思考,但最終還是一個可解決的問題。而我認為同步寫入到幾個儲存系統是更困難的工程問題,所以我將重點關注它。
|
||||
|
||||
傳統的同步寫入方法需要跨異構儲存系統的分散式事務【18】,我認為這是錯誤的解決方案(請參閱“[匯出的資料與分散式事務]()”第495頁)。單個儲存或流處理系統內的事務是可行的,但是當資料跨越不同技術之間的邊界時,我認為具有冪等寫入的非同步事件日誌是一種更加健壯和實用的方法。
|
||||
傳統的同步寫入方法需要跨異構儲存系統的分散式事務【18】,我認為這是錯誤的解決方案(請參閱“[衍生資料與分散式事務](#衍生資料與分散式事務)”)。單個儲存或流處理系統內的事務是可行的,但是當資料跨越不同技術之間的邊界時,我認為具有冪等寫入的非同步事件日誌是一種更加健壯和實用的方法。
|
||||
|
||||
例如,分散式事務在某些流處理元件內部使用,以匹配**恰好一次(exactly-once)** 語義(請參閱第477頁的“[重新訪問原子提交]()”),這可以很好地工作。然而,當事務需要涉及由不同人群編寫的系統時(例如,當資料從流處理元件寫入分散式鍵值儲存或搜尋索引時),缺乏標準化的事務協議會使整合更難。有冪等消費者的事件的有序事件日誌(參見第478頁的“[冪等性]()”)是一種更簡單的抽象,因此在異構系統中實現更加可行【7】。
|
||||
例如,分散式事務在某些流處理元件內部使用,以匹配**恰好一次(exactly-once)** 語義(請參閱“[原子提交再現](ch11.md#原子提交再現)”),這可以很好地工作。然而,當事務需要涉及由不同人群編寫的系統時(例如,當資料從流處理元件寫入分散式鍵值儲存或搜尋索引時),缺乏標準化的事務協議會使整合更難。有冪等消費者的有序事件日誌(請參閱“[冪等性](ch11.md#冪等性)”)是一種更簡單的抽象,因此在異構系統中實現更加可行【7】。
|
||||
|
||||
基於日誌的整合的一大優勢是各個元件之間的**鬆散耦合(loose coupling)**,這體現在兩個方面:
|
||||
|
||||
1. 在系統級別,非同步事件流使整個系統對各個元件的中斷或效能下降更加穩健。如果使用者執行緩慢或失敗,那麼事件日誌可以緩衝訊息(請參閱“[磁碟空間使用情況]()”第369頁),以便生產者和任何其他使用者可以繼續不受影響地執行。有問題的消費者可以在固定時趕上,因此不會錯過任何資料,並且包含故障。相比之下,分散式事務的同步互動往往會將本地故障升級為大規模故障(請參見第363頁的“[分散式事務的限制]()”)。
|
||||
1. 在系統級別,非同步事件流使整個系統在個別元件的中斷或效能下降時更加穩健。如果消費者執行緩慢或失敗,那麼事件日誌可以緩衝訊息(請參閱“[磁碟空間使用](ch11.md#磁碟空間使用)”),以便生產者和任何其他消費者可以繼續不受影響地執行。有問題的消費者可以在問題修復後趕上,因此不會錯過任何資料,並且包含故障。相比之下,分散式事務的同步互動往往會將本地故障升級為大規模故障(請參閱“[分散式事務的限制](ch9.md#分散式事務的限制)”)。
|
||||
2. 在人力方面,分拆資料系統允許不同的團隊獨立開發,改進和維護不同的軟體元件和服務。專業化使得每個團隊都可以專注於做好一件事,並與其他團隊的系統以明確的介面互動。事件日誌提供了一個足夠強大的介面,以捕獲相當強的一致性屬性(由於永續性和事件的順序),但也足夠普適於幾乎任何型別的資料。
|
||||
|
||||
#### 分拆系統vs整合系統
|
||||
|
||||
如果分拆確實成為未來的方式,它也不會取代目前形式的資料庫 —— 它們仍然會像以往一樣被需要。為了維護流處理元件中的狀態,資料庫仍然是需要的,並且為批處理和流處理器的輸出提供查詢服務(參閱“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”與“[流處理](ch11.md#流處理)”)。專用查詢引擎對於特定的工作負載仍然非常重要:例如,MPP資料倉庫中的查詢引擎針對探索性分析查詢進行了最佳化,並且能夠很好地處理這種型別的工作負載(參閱“[對比Hadoop與分散式資料庫](ch10.md#對比Hadoop與分散式資料庫)” 。
|
||||
如果分拆確實成為未來的方式,它也不會取代目前形式的資料庫 —— 它們仍然會像以往一樣被需要。為了維護流處理元件中的狀態,資料庫仍然是需要的,並且為批處理和流處理器的輸出提供查詢服務(請參閱“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”與“[流處理](ch11.md#流處理)”)。專用查詢引擎對於特定的工作負載仍然非常重要:例如,MPP資料倉庫中的查詢引擎針對探索性分析查詢進行了最佳化,並且能夠很好地處理這種型別的工作負載(請參閱“[Hadoop與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)” 。
|
||||
|
||||
執行幾種不同基礎設施的複雜性可能是一個問題:每種軟體都有一個學習曲線,配置問題和操作怪癖,因此部署儘可能少的移動部件是很有必要的。比起使用應用程式碼拼接多個工具而成的系統,單一整合軟體產品也可以在其設計應對的工作負載型別上實現更好,更可預測的效能【23】。正如在前言中所說的那樣,為了不需要的規模而構建系統是白費精力,而且可能會將你鎖死在一個不靈活的設計中。實際上,這是一種過早最佳化的形式。
|
||||
執行幾種不同基礎設施的複雜性可能是一個問題:每種軟體都有一個學習曲線,配置問題和操作怪癖,因此部署儘可能少的移動部件是很有必要的。比起使用應用程式碼拼接多個工具而成的系統,單一整合軟體產品也可以在其設計應對的工作負載型別上實現更好、更可預測的效能【23】。正如在前言中所說的那樣,為了不需要的規模而構建系統是白費精力,而且可能會將你鎖死在一個不靈活的設計中。實際上,這是一種過早最佳化的形式。
|
||||
|
||||
分拆的目標不是要針對個別資料庫與特定工作負載的效能進行競爭;我們的目標是允許您結合多個不同的資料庫,以便在比單個軟體可能實現的更廣泛的工作負載範圍內實現更好的效能。這是關於廣度,而不是深度 —— 與我們在“[對比Hadoop與分散式資料庫](ch10.md#對比Hadoop與分散式資料庫)”中討論的儲存和處理模型的多樣性一樣。
|
||||
分拆的目標不是要針對個別資料庫與特定工作負載的效能進行競爭;我們的目標是允許您結合多個不同的資料庫,以便在比單個軟體可能實現的更廣泛的工作負載範圍內實現更好的效能。這是關於廣度,而不是深度 —— 與我們在“[Hadoop與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)”中討論的儲存和處理模型的多樣性一樣。
|
||||
|
||||
因此,如果有一項技術可以滿足您的所有需求,那麼最好使用該產品,而不是試圖用低階元件重新實現它。只有當沒有單一軟體滿足您的所有需求時,才會出現拆分和聯合的優勢。
|
||||
因此,如果有一項技術可以滿足您的所有需求,那麼最好使用該產品,而不是試圖用更低層級的元件重新實現它。只有當沒有單一軟體滿足您的所有需求時,才會出現拆分和聯合的優勢。
|
||||
|
||||
#### 少了什麼?
|
||||
|
||||
用於組成資料系統的工具正在變得越來越好,但我認為還缺少一個主要的東西:我們還沒有與Unix shell類似的分拆資料庫(即,一種宣告式的,簡單的,用於組裝儲存和處理系統的高階語言)。
|
||||
用於組成資料系統的工具正在變得越來越好,但我認為還缺少一個主要的東西:我們還沒有與Unix shell類似的分拆資料庫等價物(即,一種宣告式的、簡單的、用於組裝儲存和處理系統的高階語言)。
|
||||
|
||||
例如,如果我們可以簡單地宣告`mysql |elasticsearch`,類似於Unix管道【22】,成為`CREATE INDEX`的分拆等價物:它將MySQL資料庫中的所有文件並將其索引到Elasticsearch叢集中。然後它會不斷捕獲對資料庫所做的所有變更,並自動將它們應用於搜尋索引,而無需編寫自定義應用程式碼。這種整合應當支援幾乎任何型別的儲存或索引系統。
|
||||
例如,如果我們可以簡單地宣告`mysql | elasticsearch`,類似於Unix管道【22】,成為`CREATE INDEX`的分拆等價物:它將讀取MySQL資料庫中的所有文件並將其索引到Elasticsearch叢集中。然後它會不斷捕獲對資料庫所做的所有變更,並自動將它們應用於搜尋索引,而無需編寫自定義應用程式碼。這種整合應當支援幾乎任何型別的儲存或索引系統。
|
||||
|
||||
同樣,能夠更容易地預先計算和更新快取將是一件好事。回想一下,物化檢視本質上是一個預先計算的快取,所以您可以透過為複雜查詢宣告指定物化檢視來建立快取,包括圖上的遞迴查詢(參閱“[圖資料模型](ch2.md#圖資料模型)”)和應用邏輯。在這方面有一些有趣的早期研究,如**差分資料流(differential dataflow)**【24,25】,我希望這些想法能夠在生產系統中找到自己的方法。
|
||||
同樣,能夠更容易地預先計算和更新快取將是一件好事。回想一下,物化檢視本質上是一個預先計算的快取,所以您可以透過為複雜查詢宣告指定物化檢視來建立快取,包括圖上的遞迴查詢(請參閱“[圖資料模型](ch2.md#圖資料模型)”)和應用邏輯。在這方面有一些有趣的早期研究,如**差分資料流(differential dataflow)**【24,25】,我希望這些想法能夠在生產系統中找到自己的方法。
|
||||
|
||||
### 圍繞資料流設計應用
|
||||
|
||||
使用應用程式碼組合專用儲存與處理系統來分拆資料庫的方法,也被稱為“**資料庫由內而外**”方法【26】,在我在2014年的一次會議演講標題之後【27】。然而稱它為“新架構”過於巨集大。我將其看作是一種設計模式,一個討論的起點,我們只是簡單地給它起一個名字,以便我們能更好地討論它。
|
||||
使用應用程式碼組合專用儲存與處理系統來分拆資料庫的方法,也被稱為“**資料庫由內而外(database inside-out)**”方法【26】,該名稱來源於我在2014年的一次會議演講標題【27】。然而稱它為“新架構”過於誇大,我僅將其看作是一種設計模式,一個討論的起點,我們只是簡單地給它起一個名字,以便我們能更好地討論它。
|
||||
|
||||
這些想法不是我的;它們是很多人的思想的融合,這些思想非常值得我們學習。尤其是,以Oz 【28】和Juttle 【29】為代表的資料流語言,以Elm【30,31】為代表的**函式式響應式程式設計(functional reactive programming, FRP)**,以Bloom【32】為代表的邏輯程式語言。在這一語境中的術語**分拆(unbundling)** 是由Jay Kreps 提出的【7】。
|
||||
這些想法不是我的;它們是很多人的思想的融合,這些思想非常值得我們學習。尤其是,以Oz【28】和Juttle【29】為代表的資料流語言,以Elm【30,31】為代表的**函式式響應式程式設計(functional reactive programming, FRP)**,以Bloom【32】為代表的邏輯程式語言。在這一語境中的術語**分拆(unbundling)** 是由Jay Kreps 提出的【7】。
|
||||
|
||||
即使是**電子表格**也在資料流程式設計能力上甩開大多數主流程式語言幾條街【33】。在電子表格中,可以將公式放入一個單元格中(例如,另一列中的單元格求和值),並且只要公式的任何輸入發生變更,公式的結果都會自動重新計算。這正是我們在資料系統層次所需要的:當資料庫中的記錄發生變更時,我們希望自動更新該記錄的任何索引,並且自動重新整理依賴於記錄的任何快取檢視或聚合。您不必擔心這種重新整理如何發生的技術細節,但能夠簡單地相信它可以正常工作。
|
||||
即使是**電子表格**也在資料流程式設計能力上甩開大多數主流程式語言幾條街【33】。在電子表格中,可以將公式放入一個單元格中(例如,對另一列中的單元格求和),並且只要公式的任何輸入發生變更,公式的結果都會自動重新計算。這正是我們在資料系統層次所需要的:當資料庫中的記錄發生變更時,我們希望自動更新該記錄的任何索引,並且自動重新整理依賴於記錄的任何快取檢視或聚合。您不必擔心這種重新整理如何發生的技術細節,但能夠簡單地相信它可以正常工作。
|
||||
|
||||
因此,我認為絕大多數資料系統仍然可以從VisiCalc在1979年已經具備的功能中學習【34】。與電子表格的不同之處在於,今天的資料系統需要具有容錯性,可伸縮性以及持久儲存資料。它們還需要能夠整合不同人群編寫的不同技術,並重用現有的庫和服務:期望使用某種特定語言,框架或工具開發所有軟體是不切實際的。
|
||||
因此,我認為絕大多數資料系統仍然可以從VisiCalc在1979年已經具備的功能中學習【34】。與電子表格的不同之處在於,今天的資料系統需要具有容錯性,可伸縮性以及持久儲存資料。它們還需要能夠整合不同人群編寫的不同技術,並重用現有的庫和服務:期望使用某一種特定的語言、框架或工具來開發所有軟體是不切實際的。
|
||||
|
||||
在本節中,我將詳細介紹這些想法,並探討一些圍繞分拆資料庫和資料流的想法構建應用的方法。
|
||||
|
||||
@ -261,21 +254,24 @@
|
||||
|
||||
當一個數據集衍生自另一個數據集時,它會經歷某種轉換函式。例如:
|
||||
|
||||
* 次級索引是由一種直白的轉換函式生成的衍生資料集:對於基礎表中的每行或每個文件,它挑選被索引的列或欄位中的值,並按這些值排序(假設使用B樹或SSTable索引,按鍵排序,如[第3章](ch3.md)所述)。
|
||||
* 全文搜尋索引是透過應用各種自然語言處理函式而建立的,諸如語言檢測,分詞,詞幹或詞彙化,拼寫糾正和同義詞識別)建立全文搜尋索引,然後構建用於高效查詢的資料結構(例如倒排索引)。
|
||||
* 在機器學習系統中,我們可以將模型視作從訓練資料透過應用各種特徵提取,統計分析函式衍生的資料,當模型應用於新的輸入資料時,模型的輸出是從輸入和模型(因此間接地從訓練資料)中衍生的。
|
||||
* 次級索引是由一種直白的轉換函式生成的衍生資料集:對於基礎表中的每行或每個文件,它挑選被索引的列或欄位中的值,並按這些值排序(假設使用B樹或SSTable索引,按鍵排序,如[第三章](ch3.md)所述)。
|
||||
* 全文搜尋索引是透過應用各種自然語言處理函式而建立的,諸如語言檢測、分詞、詞幹或詞彙化、拼寫糾正和同義詞識別,然後構建用於高效查詢的資料結構(例如倒排索引)。
|
||||
* 在機器學習系統中,我們可以將模型視作從訓練資料透過應用各種特徵提取、統計分析函式衍生的資料,當模型應用於新的輸入資料時,模型的輸出是從輸入和模型(因此間接地從訓練資料)中衍生的。
|
||||
* 快取通常包含將以使用者介面(UI)顯示的形式的資料聚合。因此填充快取需要知道UI中引用的欄位;UI中的變更可能需要更新快取填充方式的定義,並重建快取。
|
||||
|
||||
用於次級索引的衍生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過`CREATE INDEX`來呼叫它。對於全文索引,常見語言的基本語言特徵可能內建到資料庫中,但更復雜的特徵通常需要領域特定的調整。在機器學習中,特徵工程是眾所周知的特定於應用的特徵,通常需要包含很多關於使用者互動與應用部署的詳細知識【35】。
|
||||
當建立衍生資料集的函式不是像建立二級索引那樣的標準搬磚函式時,需要自定義程式碼來處理特定於應用的東西。而這個自定義程式碼是讓許多資料庫掙扎的地方,雖然關係資料庫通常支援觸發器,儲存過程和使用者定義的函式,它們可以用來在資料庫中執行應用程式碼,但它們有點像資料庫設計裡的事後反思。(參閱“[傳輸事件流](ch11.md#傳輸事件流)”)。
|
||||
用於次級索引的衍生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過`CREATE INDEX`來呼叫它。對於全文索引,常見語言的基本語言特徵可能內建到資料庫中,但更復雜的特徵通常需要領域特定的調整。在機器學習中,特徵工程是眾所周知的特定於應用的特徵,通常需要包含很多關於使用者互動與應用部署的詳細知識【35】。
|
||||
|
||||
當建立衍生資料集的函式不是像建立二級索引那樣的標準搬磚函式時,需要自定義程式碼來處理特定於應用的東西。而這個自定義程式碼是讓許多資料庫掙扎的地方,雖然關係資料庫通常支援觸發器、儲存過程和使用者定義的函式,可以用它們來在資料庫中執行應用程式碼,但它們有點像資料庫設計裡的事後反思。(請參閱“[傳遞事件流](ch11.md#傳遞事件流)”)。
|
||||
|
||||
#### 應用程式碼和狀態的分離
|
||||
|
||||
理論上,資料庫可以是任意應用程式碼的部署環境,就如同作業系統一樣。然而實踐中它們對這一目標適配的很差。它們不滿足現代應用開發的要求,例如依賴性和軟體包管理,版本控制,滾動升級,可演化性,監控,指標,對網路服務的呼叫以及與外部系統的整合。
|
||||
理論上,資料庫可以是任意應用程式碼的部署環境,就如同作業系統一樣。然而實踐中它們對這一目標適配的很差。它們不滿足現代應用開發的要求,例如依賴和軟體包管理、版本控制、滾動升級、可演化性、監控、指標、對網路服務的呼叫以及與外部系統的整合。
|
||||
|
||||
另一方面,Mesos,YARN,Docker,Kubernetes等部署和叢集管理工具專為執行應用程式碼而設計。透過專注於做好一件事情,他們能夠做得比將資料庫作為其眾多功能之一執行使用者定義的功能要好得多。我認為讓系統的某些部分專門用於持久資料儲存以及專門執行應用程式程式碼的其他部分是有意義的。這兩者可以在保持獨立的同時互動。
|
||||
另一方面,Mesos,YARN,Docker,Kubernetes等部署和叢集管理工具專為執行應用程式碼而設計。透過專注於做好一件事情,他們能夠做得比將資料庫作為其眾多功能之一執行使用者定義的功能要好得多。
|
||||
|
||||
現在大多數Web應用程式都是作為無狀態服務部署的,其中任何使用者請求都可以路由到任何應用程式伺服器,並且伺服器在傳送響應後會忘記所有請求。這種部署方式很方便,因為可以隨意新增或刪除伺服器,但狀態必須到某個地方:通常是資料庫。趨勢是將無狀態應用程式邏輯與狀態管理(資料庫)分開:不將應用程式邏輯放入資料庫中,也不將持久狀態置於應用程式中【36】。正如職能規劃界人士喜歡開玩笑說的那樣,“我們相信**教會(Church)** 與**國家(state)** 的分離”【37】 [^i]
|
||||
我認為讓系統的某些部分專門用於持久資料儲存並讓其他部分專門執行應用程式程式碼是有意義的。這兩者可以在保持獨立的同時互動。
|
||||
|
||||
現在大多數Web應用程式都是作為無狀態服務部署的,其中任何使用者請求都可以路由到任何應用程式伺服器,並且伺服器在傳送響應後會忘記所有請求。這種部署方式很方便,因為可以隨意新增或刪除伺服器,但狀態必須到某個地方:通常是資料庫。趨勢是將無狀態應用程式邏輯與狀態管理(資料庫)分開:不將應用程式邏輯放入資料庫中,也不將持久狀態置於應用程式中【36】。正如函數語言程式設計社群喜歡開玩笑說的那樣,“我們相信**教會(Church)** 與**國家(state)** 的分離”【37】 [^i]
|
||||
|
||||
[^i]: 解釋笑話很少會讓人感覺更好,但我不想讓任何人感到被遺漏。 在這裡,Church指代的是數學家的阿隆佐·邱奇,他創立了lambda演算,這是計算的早期形式,是大多數函數語言程式設計語言的基礎。 lambda演算不具有可變狀態(即沒有變數可以被覆蓋),所以可以說可變狀態與Church的工作是分離的。
|
||||
|
||||
@ -283,52 +279,47 @@
|
||||
|
||||
但是,在大多數程式語言中,你無法訂閱可變變數中的變更 —— 你只能定期讀取它。與電子表格不同,如果變數的值發生變化,變數的讀者不會收到通知。 (你可以在自己的程式碼中實現這樣的通知 —— 這被稱為**觀察者模式** —— 但大多數語言沒有將這種模式作為內建功能。)
|
||||
|
||||
資料庫繼承了這種可變資料的被動方法:如果你想知道資料庫的內容是否發生了變化,通常你唯一的選擇就是輪詢(即定期重複你的查詢)。 訂閱變更只是剛剛開始出現的功能(參閱“[變更流的API支援](ch11.md#變更流的API支援)”)。
|
||||
資料庫繼承了這種可變資料的被動方法:如果你想知道資料庫的內容是否發生了變化,通常你唯一的選擇就是輪詢(即定期重複你的查詢)。 訂閱變更只是剛剛開始出現的功能(請參閱“[變更流的API支援](ch11.md#變更流的API支援)”)。
|
||||
|
||||
#### 資料流:應用程式碼與狀態變化的互動
|
||||
|
||||
從資料流的角度思考應用,意味著重新協調應用程式碼和狀態管理之間的關係。將資料庫視作被應用操縱的被動變數,取而代之的是更多地考慮狀態,狀態變更和處理它們的程式碼之間的相互作用與協同關係。應用程式碼透過在另一個地方觸發狀態變更來響應狀態變更。
|
||||
從資料流的角度思考應用程式,意味著重新協調應用程式碼和狀態管理之間的關係。我們不再將資料庫視作被應用操縱的被動變數,取而代之的是更多地考慮狀態,狀態變更和處理它們的程式碼之間的相互作用與協同關係。應用程式碼透過在另一個地方觸發狀態變更來響應狀態變更。
|
||||
|
||||
我們在“[資料庫與流](ch11.md#資料庫與流)”中看到了這一思路,我們討論了將資料庫的變更日誌視為一種我們可以訂閱的事件流。諸如Actor的訊息傳遞系統(參閱“[訊息傳遞資料流](ch4.md#訊息傳遞資料流)”)也具有響應事件的概念。早在20世紀80年代,**元組空間(tuple space)** 模型就已經探索了表達分散式計算的方式:觀察狀態變更並作出反應【38,39】。
|
||||
我們在“[資料庫與流](ch11.md#資料庫與流)”中看到了這一思路,我們討論了將資料庫的變更日誌視為一種我們可以訂閱的事件流。諸如Actor的訊息傳遞系統(請參閱“[訊息傳遞中的資料流 ](ch4.md#訊息傳遞中的資料流)”)也具有響應事件的概念。早在20世紀80年代,**元組空間(tuple space)** 模型就已經探索了表達分散式計算的方式:觀察狀態變更並作出反應的過程【38,39】。
|
||||
|
||||
如前所述,當觸發器由於資料變更而被觸發時,或次級索引更新以反映索引表中的變更時,資料庫內部也發生著類似的情況。分拆資料庫意味著將這個想法應用於在主資料庫之外,用於建立衍生資料集:快取,全文搜尋索引,機器學習或分析系統。我們可以為此使用流處理和訊息傳遞系統。
|
||||
|
||||
需要記住的重要一點是,維護衍生資料不同於執行非同步任務。傳統的訊息傳遞系統通常是為執行非同步任務設計的(參閱“[與傳統訊息傳遞相比的日誌](ch11.md#與傳統訊息傳遞相比的日誌)”):
|
||||
|
||||
* 在維護衍生資料時,狀態變更的順序通常很重要(如果多個檢視是從事件日誌衍生的,則需要按照相同的順序處理事件,以便它們之間保持一致)。如“[確認與重傳](ch11.md#確認與重傳)”中所述,許多訊息代理在重傳未確認訊息時沒有此屬性,雙寫也被排除在外(參閱“[保持系統同步](ch11.md#保持系統同步)”)。
|
||||
如前所述,當觸發器由於資料變更而被觸發時,或次級索引更新以反映索引表中的變更時,資料庫內部也發生著類似的情況。分拆資料庫意味著將這個想法應用於在主資料庫之外,用於建立衍生資料集:快取、全文搜尋索引、機器學習或分析系統。我們可以為此使用流處理和訊息傳遞系統。
|
||||
|
||||
需要記住的重要一點是,維護衍生資料不同於執行非同步任務。傳統的訊息傳遞系統通常是為執行非同步任務設計的(請參閱“[日誌與傳統的訊息傳遞相比](ch11.md#日誌與傳統的訊息傳遞相比)”):
|
||||
|
||||
* 在維護衍生資料時,狀態變更的順序通常很重要(如果多個檢視是從事件日誌衍生的,則需要按照相同的順序處理事件,以便它們之間保持一致)。如“[確認與重新傳遞](ch11.md#確認與重新傳遞)”中所述,許多訊息代理在重傳未確認訊息時沒有此屬性,雙寫也被排除在外(請參閱“[保持系統同步](ch11.md#保持系統同步)”)。
|
||||
* 容錯是衍生資料的關鍵:僅僅丟失單個訊息就會導致衍生資料集永遠與其資料來源失去同步。訊息傳遞和衍生狀態更新都必須可靠。例如,許多Actor系統預設在記憶體中維護Actor的狀態和訊息,所以如果執行Actor的機器崩潰,狀態和訊息就會丟失。
|
||||
|
||||
|
||||
穩定的訊息排序和容錯訊息處理是相當嚴格的要求,但與分散式事務相比,它們開銷更小,執行更穩定。現代流處理元件可以提供這些排序和可靠性保證,並允許應用程式碼以流運算元的形式執行。
|
||||
|
||||
這些應用程式碼可以執行任意處理,包括資料庫內建衍生函式通常不提供的功能。就像透過管道連結的Unix工具一樣,流運算元可以圍繞著資料流構建大型系統。每個運算元接受狀態變更的流作為輸入,併產生其他狀態變化的流作為輸出。
|
||||
|
||||
#### 流處理器和服務
|
||||
|
||||
當今流行的應用開發風格涉及將功能分解為一組透過同步網路請求(如REST API)進行通訊的**服務(service)**(參閱“[透過服務實現資料流:REST和RPC](ch4.md#透過服務實現資料流:REST和RPC)”)。這種面向服務的架構優於單一龐大應用的優勢主要在於:通過鬆散耦合來提供組織上的可伸縮性:不同的團隊可以專職於不同的服務上,從而減少團隊之間的協調工作(因為服務可以獨立部署和更新)。
|
||||
當今流行的應用開發風格涉及將功能分解為一組透過同步網路請求(如REST API)進行通訊的**服務(service)**(請參閱“[服務中的資料流:REST與RPC](ch4.md#服務中的資料流:REST與RPC)”)。這種面向服務的架構優於單一龐大應用的優勢主要在於:通過鬆散耦合來提供組織上的可伸縮性:不同的團隊可以專職於不同的服務上,從而減少團隊之間的協調工作(因為服務可以獨立部署和更新)。
|
||||
|
||||
在資料流中組裝流運算元與微服務方法有很多相似之處【40】。但底層通訊機制是有很大區別:資料流採用單向非同步訊息流,而不是同步的請求/響應式互動。
|
||||
|
||||
除了在“[訊息傳遞資料流](ch4.md#訊息傳遞資料流)”中列出的優點(如更好的容錯性),資料流系統還能實現更好的效能。例如,假設客戶正在購買以一種貨幣定價,但以另一種貨幣支付的商品。為了執行貨幣換算,你需要知道當前的匯率。這個操作可以透過兩種方式實現【40,41】:
|
||||
除了在“[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”中列出的優點(如更好的容錯性),資料流系統還能實現更好的效能。例如,假設客戶正在購買以一種貨幣定價,但以另一種貨幣支付的商品。為了執行貨幣換算,你需要知道當前的匯率。這個操作可以透過兩種方式實現【40,41】:
|
||||
|
||||
1. 在微服務方法中,處理購買的程式碼可能會查詢匯率服務或資料庫,以獲取特定貨幣的當前匯率。
|
||||
2. 在資料流方法中,處理訂單的程式碼會提前訂閱匯率變更流,並在匯率發生變動時將當前匯率儲存在本地資料庫中。處理訂單時只需查詢本地資料庫即可。
|
||||
|
||||
|
||||
|
||||
第二種方法能將對另一服務的同步網路請求替換為對本地資料庫的查詢(可能在同一臺機器甚至同一個程序中)[^ii]。資料流方法不僅更快,而且當其他服務失效時也更穩健。最快且最可靠的網路請求就是壓根沒有網路請求!我們現在不再使用RPC,而是在購買事件和匯率更新事件之間建立流聯接(參閱“[流表聯接](ch11.md#流表聯接)”)。
|
||||
第二種方法能將對另一服務的同步網路請求替換為對本地資料庫的查詢(可能在同一臺機器甚至同一個程序中)[^ii]。資料流方法不僅更快,而且當其他服務失效時也更穩健。最快且最可靠的網路請求就是壓根沒有網路請求!我們現在不再使用RPC,而是在購買事件和匯率更新事件之間建立流聯接(請參閱“[流表連線(流擴充)](ch11.md#流表連線(流擴充))”)。
|
||||
|
||||
[^ii]: 在微服務方法中,你也可以透過在處理購買的服務中本地快取匯率來避免同步網路請求。 但是為了保證快取的新鮮度,你需要定期輪詢匯率以獲取其更新,或訂閱變更流 —— 這恰好是資料流方法中發生的事情。
|
||||
|
||||
連線是時間相關的:如果購買事件在稍後的時間點被重新處理,匯率可能已經改變。如果要重建原始輸出,則需要獲取原始購買時的歷史匯率。無論是查詢服務還是訂閱匯率更新流,你都需要處理這種時間相關性(參閱“[連線的時間相關性](ch11.md#連線的時間相關性)”)。
|
||||
連線是時間相關的:如果購買事件在稍後的時間點被重新處理,匯率可能已經改變。如果要重建原始輸出,則需要獲取原始購買時的歷史匯率。無論是查詢服務還是訂閱匯率更新流,你都需要處理這種時間相關性(請參閱“[連線的時間依賴性](ch11.md#連線的時間依賴性)”)。
|
||||
|
||||
訂閱變更流,而不是在需要時查詢當前狀態,使我們更接近類似電子表格的計算模型:當某些資料發生變更時,依賴於此的所有衍生資料都可以快速更新。還有很多未解決的問題,例如關於時間相關連線等問題,但我認為圍繞資料流構建應用的想法是一個非常有希望的方向。
|
||||
|
||||
### 觀察衍生資料狀態
|
||||
|
||||
在抽象層面,上一節討論的資料流系統提供了建立衍生資料集(例如搜尋索引,物化檢視和預測模型)並使其保持更新的過程。我們將這個過程稱為**寫路徑(write path)**:只要某些資訊被寫入系統,它可能會經歷批處理與流處理的多個階段,而最終每個衍生資料集都會被更新,以適配寫入的資料。[圖12-1](../img/fig12-1.png)顯示了一個更新搜尋索引的例子。
|
||||
在抽象層面,上一節討論的資料流系統提供了建立衍生資料集(例如搜尋索引、物化檢視和預測模型)並使其保持更新的過程。我們將這個過程稱為**寫路徑(write path)**:只要某些資訊被寫入系統,它可能會經歷批處理與流處理的多個階段,而最終每個衍生資料集都會被更新,以適配寫入的資料。[圖12-1](../img/fig12-1.png)顯示了一個更新搜尋索引的例子。
|
||||
|
||||
![](../img/fig12-1.png)
|
||||
|
||||
@ -350,21 +341,21 @@
|
||||
|
||||
[^iii]: 假設一個有限的語料庫,那麼返回非空搜尋結果的搜尋查詢集合是有限的。然而,它是與語料庫中的術語數量呈指數關係,這仍是一個壞訊息。
|
||||
|
||||
另一個選擇是隻為一組固定的最常見的查詢預先計算搜尋結果,以便它們可以快速地服務而不必去走索引。不常見的查詢仍然可以從走索引。這通常被稱為常見查詢的**快取(cache)**,儘管我們也可以稱之為**物化檢視(materialized view)**,因為當新文件出現,且需要被包含在這些常見查詢的搜尋結果之中時,這些索引就需要更新。
|
||||
另一個選擇是隻為一組固定的最常見的查詢預先計算搜尋結果,以便它們可以快速地服務而不必去走索引。不常見的查詢仍然可以透過索引來提供服務。這通常被稱為常見查詢的**快取(cache)**,儘管我們也可以稱之為**物化檢視(materialized view)**,因為當新文件出現,且需要被包含在這些常見查詢的搜尋結果之中時,這些索引就需要更新。
|
||||
|
||||
從這個例子中我們可以看到,索引不是寫路徑和讀路徑之間唯一可能的邊界;快取常見搜尋結果也是可行的;而在少量文件上使用沒有索引的類grep掃描也是可行的。由此來看,快取,索引和物化檢視的作用很簡單:它們改變了讀路徑與寫路徑之間的邊界。透過預先計算結果,從而允許我們在寫路徑上做更多的工作,以節省讀取路徑上的工作量。
|
||||
從這個例子中我們可以看到,索引不是寫路徑和讀路徑之間唯一可能的邊界;快取常見搜尋結果也是可行的;而在少量文件上使用沒有索引的類grep掃描也是可行的。由此來看,快取,索引和物化檢視的作用很簡單:它們改變了讀路徑與寫路徑之間的邊界。透過預先計算結果,從而允許我們在寫路徑上做更多的工作,以節省讀路徑上的工作量。
|
||||
|
||||
在寫路徑上完成的工作和讀路徑之間的界限,實際上是本書開始處在“[描述負載](ch1.md#描述負載)”中推特例子裡談到的主題。在該例中,我們還看到了與普通使用者相比,名流的寫路徑和讀路徑可能有所不同。在500頁之後,我們已經走完一個大迴圈!
|
||||
在寫路徑上完成的工作和讀路徑之間的界限,實際上是本書開始處在“[描述負載](ch1.md#描述負載)”中推特例子裡談到的主題。在該例中,我們還看到了與普通使用者相比,名人的寫路徑和讀路徑可能有所不同。在500頁之後,我們已經繞回了起點!
|
||||
|
||||
#### 有狀態,可離線的客戶端
|
||||
#### 有狀態、可離線的客戶端
|
||||
|
||||
我發現寫和讀路徑之間的邊界很有趣,因為我們可以試著改變這個邊界,並探討這種改變的實際意義。我們來看看不同上下文中的這一想法。
|
||||
我發現寫路徑和讀路徑之間的邊界很有趣,因為我們可以試著改變這個邊界,並探討這種改變的實際意義。我們來看看不同上下文中的這一想法。
|
||||
|
||||
過去二十年來,Web應用的火熱讓我們對應用開發作出了一些很容易視作理所當然的假設。具體來說就是,客戶端/伺服器模型 —— 客戶端大多是無狀態的,而伺服器擁有資料的權威 —— 已經普遍到我們幾乎忘掉了還有其他任何模型的存在。但是技術在不斷地發展,我認為不時地質疑現狀非常重要。
|
||||
|
||||
傳統上,網路瀏覽器是無狀態的客戶端,只有當連線到網際網路時才能做一些有用的事情(能離線執行的唯一事情基本上就是上下滾動之前線上時載入好的頁面)。然而,最近的“單頁面”JavaScript Web應用已經獲得了很多有狀態的功能,包括客戶端使用者介面互動,以及Web瀏覽器中的持久化本地儲存。移動應用可以類似地在裝置上儲存大量狀態,而且大多數使用者互動都不需要與伺服器往返互動。
|
||||
|
||||
這些不斷變化的功能重新引發了對**離線優先(offline-first)** 應用的興趣,這些應用盡可能地在同一裝置上使用本地資料庫,無需連線網際網路,並在後臺網路連線可用時與遠端伺服器同步【42】。由於移動裝置通常具有緩慢且不可靠的蜂窩網路連線,因此,如果使用者的使用者介面不必等待同步網路請求,且應用主要是離線工作的,則這是一個巨大優勢(參閱“[具有離線操作的客戶端](ch5.md#具有離線操作的客戶端)”)。
|
||||
這些不斷變化的功能重新引發了對**離線優先(offline-first)** 應用的興趣,這些應用盡可能地在同一裝置上使用本地資料庫,無需連線網際網路,並在後臺網路連線可用時與遠端伺服器同步【42】。由於移動裝置通常具有緩慢且不可靠的蜂窩網路連線,因此,如果使用者的使用者介面不必等待同步網路請求,且應用主要是離線工作的,則這是一個巨大優勢(請參閱“[需要離線操作的客戶端](ch5.md#需要離線操作的客戶端)”)。
|
||||
|
||||
當我們擺脫無狀態客戶端與中央資料庫互動的假設,並轉向在終端使用者裝置上維護狀態時,這就開啟了新世界的大門。特別是,我們可以將裝置上的狀態視為**伺服器狀態的快取**。螢幕上的畫素是客戶端應用中模型物件的物化檢視;模型物件是遠端資料中心的本地狀態副本【27】。
|
||||
|
||||
@ -374,19 +365,19 @@
|
||||
|
||||
最近的協議已經超越了HTTP的基本請求/響應模式:服務端傳送的事件(EventSource API)和WebSockets提供了通訊通道,透過這些通道,Web瀏覽器可以與伺服器保持開啟的TCP連線,只要瀏覽器仍然連線著,伺服器就能主動向瀏覽器推送資訊。這為伺服器提供了主動通知終端使用者客戶端的機會,伺服器能告知客戶端其本地儲存狀態的任何變化,從而減少客戶端狀態的陳舊程度。
|
||||
|
||||
用我們的寫路徑與讀路徑模型來講,主動將狀態變更推至到客戶端裝置,意味著將寫路徑一直延伸到終端使用者。當客戶端首次初始化時,它仍然需要使用讀路徑來獲取其初始狀態,但此後它就可能依賴於伺服器傳送的狀態變更流了。我們在流處理和訊息傳遞部分討論的想法並不侷限於資料中心中:我們可以進一步採納這些想法,並將它們一直延伸到終端使用者裝置【43】。
|
||||
用我們的寫路徑與讀路徑模型來講,主動將狀態變更推至到客戶端裝置,意味著將寫路徑一直延伸到終端使用者。當客戶端首次初始化時,它仍然需要使用讀路徑來獲取其初始狀態,但此後它就能夠依賴伺服器傳送的狀態變更流了。我們在流處理和訊息傳遞部分討論的想法並不侷限於資料中心中:我們可以進一步採納這些想法,並將它們一直延伸到終端使用者裝置【43】。
|
||||
|
||||
這些裝置有時會離線,並在此期間無法收到伺服器狀態變更的任何通知。但是我們已經解決了這個問題:在“[消費者偏移量](ch11.md#消費者偏移量)”中,我們討論了基於日誌的訊息代理的消費者能在失敗或斷開連線後重連,並確保它不會錯過掉線期間任何到達的訊息。同樣的技術適用於單個使用者,每個裝置都是一個小事件流的小小訂閱者。
|
||||
|
||||
#### 端到端的事件流
|
||||
|
||||
最近用於開發帶狀態客戶端與使用者介面的工具,例如如Elm語言【30】和Facebook的React,Flux和Redux工具鏈,已經透過訂閱表示使用者輸入和伺服器響應的事件流,來管理客戶端的內部狀態,其結構與事件溯源相似(請參閱第457頁的“[事件溯源](ch11.md#事件溯源)”)。
|
||||
最近用於開發有狀態的客戶端與使用者介面的工具,例如如Elm語言【30】和Facebook的React,Flux和Redux工具鏈,已經透過訂閱表示使用者輸入或伺服器響應的事件流來管理客戶端的內部狀態,其結構與事件溯源相似(請參閱“[事件溯源](ch11.md#事件溯源)”)。
|
||||
|
||||
將這種程式設計模型擴充套件為:允許伺服器將狀態變更事件,推送到客戶端的事件管道中,是非常自然的。因此,狀態變化可以透過**端到端(end-to-end)** 的寫路徑流動:從一個裝置上的互動觸發狀態變更開始,經由事件日誌,並穿過幾個衍生資料系統與流處理器,一直到另一臺裝置上的使用者介面,而有人正在觀察使用者介面上的狀態變化。這些狀態變化能以相當低的延遲傳播 —— 比如說,在一秒內從一端到另一端。
|
||||
將這種程式設計模型擴充套件為:允許伺服器將狀態變更事件推送到客戶端的事件管道中,是非常自然的。因此,狀態變化可以透過**端到端(end-to-end)**的寫路徑流動:從一個裝置上的互動觸發狀態變更開始,經由事件日誌,並穿過幾個衍生資料系統與流處理器,一直到另一臺裝置上的使用者介面,而有人正在觀察使用者介面上的狀態變化。這些狀態變化能以相當低的延遲傳播 —— 比如說,在一秒內從一端到另一端。
|
||||
|
||||
一些應用(如即時訊息傳遞與線上遊戲)已經具有這種“實時”架構(在低延遲互動的意義上,不是在“[響應時間保證](ch8.md#響應時間保證)”中的意義上)。但我們為什麼不用這種方式構建所有的應用?
|
||||
|
||||
挑戰在於,關於無狀態客戶端和請求/響應互動的假設已經根深蒂固地植入在在我們的資料庫,庫,框架,以及協議之中。許多資料儲存支援讀取與寫入操作,為請求返回一個響應,但只有極少數提供訂閱變更的能力 —— 為請求返回一個隨時間推移返回響應的流(請參閱“[變更流的API支援](ch11.md#變更流的API支援)” )。
|
||||
挑戰在於,關於無狀態客戶端和請求/響應互動的假設已經根深蒂固地植入在在我們的資料庫、庫、框架以及協議之中。許多資料儲存支援讀取與寫入操作,為請求返回一個響應,但只有極少數提供訂閱變更的能力 —— 請求返回一個隨時間推移的響應流(請參閱“[變更流的API支援](ch11.md#變更流的API支援)” )。
|
||||
|
||||
為了將寫路徑延伸至終端使用者,我們需要從根本上重新思考我們構建這些系統的方式:從請求/響應互動轉向釋出/訂閱資料流【27】。更具響應性的使用者介面與更好的離線支援,我認為這些優勢值得我們付出努力。如果你正在設計資料系統,我希望您對訂閱變更的選項留有印象,而不只是查詢當前狀態。
|
||||
|
||||
@ -394,37 +385,36 @@
|
||||
|
||||
我們討論過,當流處理器將衍生資料寫入儲存(資料庫,快取或索引)時,以及當用戶請求查詢該儲存時,儲存將充當寫路徑和讀路徑之間的邊界。該儲存應當允許對資料進行隨機訪問的讀取查詢,否則這些查詢將需要掃描整個事件日誌。
|
||||
|
||||
在很多情況下,資料儲存與流處理系統是分開的。但回想一下,流處理器還是需要維護狀態以執行聚合和連線的(參閱“[流連線](ch11.md#流連線)”)。這種狀態通常隱藏在流處理器內部,但一些框架也允許這些狀態被外部客戶端查詢【45】,將流處理器本身變成一種簡單的資料庫。
|
||||
在很多情況下,資料儲存與流處理系統是分開的。但回想一下,流處理器還是需要維護狀態以執行聚合和連線的(請參閱“[流連線](ch11.md#流連線)”)。這種狀態通常隱藏在流處理器內部,但一些框架也允許這些狀態被外部客戶端查詢【45】,將流處理器本身變成一種簡單的資料庫。
|
||||
|
||||
我願意進一步思考這個想法。正如到目前為止所討論的那樣,對儲存的寫入是透過事件日誌進行的,而讀取是臨時的網路請求,直接流向儲存著待查資料的節點。這是一個合理的設計,但不是唯一可行的設計。也可以將讀取請求表示為事件流,並同時將讀事件與寫事件送往流處理器;流處理器透過將讀取結果傳送到輸出流來響應讀取事件【46】。
|
||||
|
||||
當寫入和讀取都被表示為事件,並且被路由到同一個流運算元以便處理時,我們實際上是在讀取查詢流和資料庫之間執行流表連線。讀取事件需要被送往儲存資料的資料庫分割槽(參閱“[請求路由](ch6.md#請求路由)”),就像批處理和流處理器在連線時需要在同一個鍵上對輸入分割槽一樣(請參閱“[Reduce端連線與分組](ch10.md#Reduce端連線與分組)“)。
|
||||
當寫入和讀取都被表示為事件,並且被路由到同一個流運算元以便處理時,我們實際上是在讀取查詢流和資料庫之間執行流表連線。讀取事件需要被送往儲存資料的資料庫分割槽(請參閱“[請求路由](ch6.md#請求路由)”),就像批處理和流處理器在連線時需要在同一個鍵上對輸入分割槽一樣(請參閱“[Reduce側連線與分組](ch10.md#Reduce側連線與分組)“)。
|
||||
|
||||
服務請求與執行連線之間的這種相似之處是非常關鍵的【47】。一次性讀取請求只是將請求傳過連線運算元,然後請求馬上就被忘掉了;而一個訂閱請求,則是與連線另一側過去與未來事件的持久化連線。
|
||||
|
||||
記錄讀取事件的日誌可能對於追蹤整個系統中的因果關係與資料來源也有好處:它可以讓你重現出當用戶做出特定決策之前看見了什麼。例如在網商中,向客戶顯示的預測送達日期與庫存狀態,可能會影響他們是否選擇購買一件商品【4】。要分析這種聯絡,則需要記錄使用者查詢運輸與庫存狀態的結果。
|
||||
|
||||
將讀取事件寫入持久儲存可以更好地跟蹤因果關係(參閱“[排序事件以捕獲因果關係](ch9.md#排序事件以捕獲因果關係)”),但會產生額外的儲存與I/O成本。最佳化這些系統以減少開銷仍然是一個開放的研究問題【2】。但如果你已經出於運維目的留下了讀取請求日誌,將其作為請求處理的副作用,那麼將這份日誌作為請求事件源並不是什麼特別大的變更。
|
||||
將讀取事件寫入持久儲存可以更好地跟蹤因果關係(請參閱“[排序事件以捕獲因果關係](#排序事件以捕獲因果關係)”),但會產生額外的儲存與I/O成本。最佳化這些系統以減少開銷仍然是一個開放的研究問題【2】。但如果你已經出於運維目的留下了讀取請求日誌,將其作為請求處理的副作用,那麼將這份日誌作為請求事件源並不是什麼特別大的變更。
|
||||
|
||||
#### 多分割槽資料處理
|
||||
|
||||
對於只涉及單個分割槽的查詢,透過流來發送查詢與收集響應可能是殺雞用牛刀了。然而,這個想法開啟了分散式執行復雜查詢的可能性,這需要合併來自多個分割槽的資料,利用流處理器已經提供的訊息路由,分割槽和連線的基礎設施。
|
||||
對於只涉及單個分割槽的查詢,透過流來發送查詢與收集響應可能是殺雞用牛刀了。然而,這個想法開啟了分散式執行復雜查詢的可能性,這需要合併來自多個分割槽的資料,利用了流處理器已經提供的訊息路由、分割槽和連線的基礎設施。
|
||||
|
||||
Storm的分散式RPC功能支援這種使用模式(參閱“[訊息傳遞和RPC](ch11.md#訊息傳遞和RPC)”)。例如,它已經被用來計算瀏覽過某個推特URL的人數 —— 即,轉推該URL的粉絲集合的並集【48】。由於推特的使用者是分割槽的,因此這種計算需要合併來自多個分割槽的結果。
|
||||
Storm的分散式RPC功能支援這種使用模式(請參閱“[訊息傳遞和RPC](ch11.md#訊息傳遞和RPC)”)。例如,它已經被用來計算瀏覽過某個推特URL的人數 —— 即,發推包含該URL的所有人的粉絲集合的並集【48】。由於推特的使用者是分割槽的,因此這種計算需要合併來自多個分割槽的結果。
|
||||
|
||||
這種模式的另一個例子是欺詐預防:為了評估特定購買事件是否具有欺詐風險,你可以檢查該使用者IP地址,電子郵件地址,帳單地址,送貨地址的信用分。這些信用資料庫中的每一個自己都是一個分割槽,因此為特定購買事件採集分數需要連線一系列不同的分割槽資料集【49】。
|
||||
|
||||
MPP資料庫的內部查詢執行圖有著類似的特徵(參閱“[比較Hadoop與分散式資料庫](ch10.md#比較Hadoop與分散式資料庫)”)。如果需要執行這種多分割槽連線,則直接使用提供此功能的資料庫,可能要比使用流處理器實現它要更簡單。然而將查詢視為流提供了一種選項,可以用於實現超出傳統現成解決方案的大規模應用。
|
||||
這種模式的另一個例子是欺詐預防:為了評估特定購買事件是否具有欺詐風險,你可以檢查該使用者IP地址,電子郵件地址,帳單地址,送貨地址的信用分。這些信用資料庫中的每一個都是有分割槽的,因此為特定購買事件採集分數需要連線一系列不同的分割槽資料集【49】。
|
||||
|
||||
MPP資料庫的內部查詢執行圖有著類似的特徵(請參閱“[Hadoop與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)”)。如果需要執行這種多分割槽連線,則直接使用提供此功能的資料庫,可能要比使用流處理器實現它要更簡單。然而將查詢視為流提供了一種選項,可以用於實現超出傳統現成解決方案的大規模應用。
|
||||
|
||||
|
||||
## 將事情做正確
|
||||
|
||||
對於只讀取資料的無狀態服務,出問題也沒什麼大不了的:你可以修復該錯誤並重啟服務,而一切都恢復正常。像資料庫這樣的有狀態系統就沒那麼簡單了:它們被設計為永遠記住事物(或多或少),所以如果出現問題,這種(錯誤的)效果也將潛在地永遠持續下去,這意味著它們需要更仔細的思考【50】。
|
||||
|
||||
我們希望構建可靠且**正確**的應用(即使面對各種故障,程式的語義也能被很好地定義與理解)。約四十年來,原子性,隔離性和永續性([第7章](ch7.md))等事務特性一直是構建正確應用的首選工具。然而這些地基沒有看上去那麼牢固:例如弱隔離級別帶來的困惑可以佐證(請參見“[弱隔離級別](ch7.md#弱隔離級別)”)。
|
||||
我們希望構建可靠且**正確**的應用(即使面對各種故障,程式的語義也能被很好地定義與理解)。約四十年來,原子性,隔離性和永續性([第七章](ch7.md))等事務特性一直是構建正確應用的首選工具。然而這些地基沒有看上去那麼牢固:例如弱隔離級別帶來的困惑可以佐證(請參閱“[弱隔離級別](ch7.md#弱隔離級別)”)。
|
||||
|
||||
事務在某些領域被完全拋棄,並被提供更好效能與可伸縮性的模型取代,但更復雜的語義(例如,參閱“[無領導者複製](ch5.md#無領導者複製)”)。**一致性(Consistency)** 經常被談起,但其定義並不明確(“[一致性](ch5.md#一致性)”和[第9章](ch9.md))。有些人斷言我們應當為了高可用而“擁抱弱一致性”,但卻對這些概念實際上意味著什麼缺乏清晰的認識。
|
||||
事務在某些領域被完全拋棄,並被提供更好效能與可伸縮性的模型取代,但更復雜的語義(例如,請參閱“[無領導者複製](ch5.md#無領導者複製)”)。**一致性(Consistency)** 經常被談起,但其定義並不明確(“[一致性](ch5.md#一致性)”和[第九章](ch9.md))。有些人斷言我們應當為了高可用而“擁抱弱一致性”,但卻對這些概念實際上意味著什麼缺乏清晰的認識。
|
||||
|
||||
對於如此重要的話題,我們的理解,以及我們的工程方法卻是驚人地薄弱。例如,確定在特定事務隔離等級或複製配置下執行特定應用是否安全是非常困難的【51,52】。通常簡單的解決方案似乎在低併發性的情況下工作正常,並且沒有錯誤,但在要求更高的情況下卻會出現許多微妙的錯誤。
|
||||
|
||||
@ -448,7 +438,7 @@
|
||||
|
||||
處理兩次是資料損壞的一種形式:為同樣的服務向客戶收費兩次(收費太多)或增長計數器兩次(誇大指標)都不是我們想要的。在這種情況下,恰好一次意味著安排計算,使得最終效果與沒有發生錯誤的情況一樣,即使操作實際上因為某種錯誤而重試。我們先前討論過實現這一目標的幾種方法。
|
||||
|
||||
最有效的方法之一是使操作**冪等(idempotent)**(參閱“[冪等性](ch11.md#冪等性)”);即確保它無論是執行一次還是執行多次都具有相同的效果。但是,將不是天生冪等的操作變為冪等的操作需要一些額外的努力與關注:你可能需要維護一些額外的元資料(例如更新了值的操作ID集合),並在從一個節點故障切換至另一個節點時做好防護(參閱的“[領導與鎖定](ch9.md#領導與鎖定)”)。
|
||||
最有效的方法之一是使操作**冪等(idempotent)**(請參閱“[冪等性](ch11.md#冪等性)”);即確保它無論是執行一次還是執行多次都具有相同的效果。但是,將不是天生冪等的操作變為冪等的操作需要一些額外的努力與關注:你可能需要維護一些額外的元資料(例如更新了值的操作ID集合),並在從一個節點故障切換至另一個節點時做好防護(請參閱的“[領導與鎖定](ch9.md#領導與鎖定)”)。
|
||||
|
||||
#### 抑制重複
|
||||
|
||||
@ -467,7 +457,7 @@ COMMIT;
|
||||
|
||||
客戶端可以重連到資料庫並重試事務,但現在現在處於TCP重複抑制的範圍之外了。因為[例12-1]()中的事務不是冪等的,可能會發生轉了\$22而不是期望的\$11。因此,儘管[例12-1]()是一個事務原子性的標準樣例,但它實際上並不正確,而真正的銀行並不會這樣辦事【3】。
|
||||
|
||||
兩階段提交(參閱“[原子提交與兩階段提交(2PC)](ch9.md#原子提交與兩階段提交(2PC))”)協議會破壞TCP連線與事務之間的1:1對映,因為它們必須在故障後允許事務協調器重連到資料庫,告訴資料庫將存疑事務提交還是中止。這足以確保事務只被恰好執行一次嗎?不幸的是,並不能。
|
||||
兩階段提交(請參閱“[原子提交與兩階段提交(2PC)](ch9.md#原子提交與兩階段提交(2PC))”)協議會破壞TCP連線與事務之間的1:1對映,因為它們必須在故障後允許事務協調器重連到資料庫,告訴資料庫將存疑事務提交還是中止。這足以確保事務只被恰好執行一次嗎?不幸的是,並不能。
|
||||
|
||||
即使我們可以抑制資料庫客戶端與伺服器之間的重複事務,我們仍然需要擔心終端使用者裝置與應用伺服器之間的網路。例如,如果終端使用者的客戶端是Web瀏覽器,則它可能會使用HTTP POST請求向伺服器提交指令。也許使用者正處於一個訊號微弱的蜂窩資料網路連線中,它們成功地傳送了POST,但卻在能夠從伺服器接收響應之前沒了訊號。
|
||||
|
||||
@ -493,7 +483,7 @@ COMMIT;
|
||||
|
||||
[例12-2]()依賴於`request_id`列上的唯一約束。如果一個事務嘗試插入一個已經存在的ID,那麼`INSERT`失敗,事務被中止,使其無法生效兩次。即使在較弱的隔離級別下,關係資料庫也能正確地維護唯一性約束(而在“[寫入偏斜與幻讀](ch7.md#寫入偏斜與幻讀)”中討論過,應用級別的**檢查-然後-插入**可能會在不可序列化的隔離下失敗)。
|
||||
|
||||
除了抑制重複的請求之外,[例12-2]()中的請求表表現得就像一種事件日誌,提示向著事件溯源的方向(參閱“[事件溯源](ch11.md#事件溯源)”)。更新賬戶餘額事實上不必與插入事件發生在同一個事務中,因為它們是冗餘的,而能由下游消費者從請求事件中衍生出來 —— 只要該事件被恰好處理一次,這又一次可以使用請求ID來強制執行。
|
||||
除了抑制重複的請求之外,[例12-2]()中的請求表表現得就像一種事件日誌,提示向著事件溯源的方向(請參閱“[事件溯源](ch11.md#事件溯源)”)。更新賬戶餘額事實上不必與插入事件發生在同一個事務中,因為它們是冗餘的,而能由下游消費者從請求事件中衍生出來 —— 只要該事件被恰好處理一次,這又一次可以使用請求ID來強制執行。
|
||||
|
||||
**端到端的原則**
|
||||
|
||||
@ -516,9 +506,9 @@ COMMIT;
|
||||
|
||||
這實在是一個遺憾,因為容錯機制很難弄好。低層級的可靠機制(比如TCP中的那些)執行的相當好,因而剩下的高層級錯誤基本很少出現。如果能將這些剩下的高層級容錯機制打包成抽象,而應用不需要再去操心,那該多好呀 —— 但恐怕我們還沒有找到這一正確的抽象。
|
||||
|
||||
長期以來,事務被認為是一個很好的抽象,我相信它們確實是很有用的。正如[第7章](ch7.md)導言中所討論的,它們將各種可能的問題(併發寫入,違背約束,崩潰,網路中斷,磁碟故障)合併為兩種可能結果:提交或中止。這是對程式設計模型而言是一種巨大的簡化,但恐怕這還不夠。
|
||||
長期以來,事務被認為是一個很好的抽象,我相信它們確實是很有用的。正如[第七章](ch7.md)導言中所討論的,它們將各種可能的問題(併發寫入,違背約束,崩潰,網路中斷,磁碟故障)合併為兩種可能結果:提交或中止。這是對程式設計模型而言是一種巨大的簡化,但恐怕這還不夠。
|
||||
|
||||
事務是代價高昂的,當涉及異構儲存技術時尤為甚(參閱的“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)。我們拒絕使用分散式事務是因為它開銷太大,結果我們最後不得不在應用程式碼中重新實現容錯機制。正如本書中大量的例子所示,對併發性與部分失敗的推理是困難且違反直覺的,所以我懷疑大多數應用級別的機制都不能正確工作,最終結果是資料丟失或損壞。
|
||||
事務是代價高昂的,當涉及異構儲存技術時尤為甚(請參閱的“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)。我們拒絕使用分散式事務是因為它開銷太大,結果我們最後不得不在應用程式碼中重新實現容錯機制。正如本書中大量的例子所示,對併發性與部分失敗的推理是困難且違反直覺的,所以我懷疑大多數應用級別的機制都不能正確工作,最終結果是資料丟失或損壞。
|
||||
|
||||
出於這些原因,我認為探索對容錯的抽象是很有價值的。它使提供應用特定的端到端的正確性屬性變得更簡單,而且還能在大規模分散式環境中提供良好的效能與運維特性。
|
||||
|
||||
@ -532,19 +522,19 @@ COMMIT;
|
||||
|
||||
#### 唯一性約束需要達成共識
|
||||
|
||||
在[第9章](ch9.md)中我們看到,在分散式環境中,強制執行唯一性約束需要共識:如果存在多個具有相同值的併發請求,則系統需要決定衝突操作中的哪一個被接受,並拒絕其他違背約束的操作。
|
||||
在[第九章](ch9.md)中我們看到,在分散式環境中,強制執行唯一性約束需要共識:如果存在多個具有相同值的併發請求,則系統需要決定衝突操作中的哪一個被接受,並拒絕其他違背約束的操作。
|
||||
|
||||
達成這一共識的最常見方式是使單個節點作為領導,並使其負責所有決策。只要你不介意所有請求都擠過單個節點(即使客戶端位於世界的另一端),只要該節點沒有失效,系統就能正常工作。如果你需要容忍領導者失效,那麼就又回到了共識問題(參閱“[單領導者複製與共識](ch9.md#單領導者複製與共識)”)。
|
||||
達成這一共識的最常見方式是使單個節點作為領導,並使其負責所有決策。只要你不介意所有請求都擠過單個節點(即使客戶端位於世界的另一端),只要該節點沒有失效,系統就能正常工作。如果你需要容忍領導者失效,那麼就又回到了共識問題(請參閱“[單領導者複製與共識](ch9.md#單領導者複製與共識)”)。
|
||||
|
||||
唯一性檢查可以透過對唯一性欄位分割槽做橫向伸縮。例如,如果需要透過請求ID確保唯一性(如[例12-2]()所示),你可以確保所有具有相同請求ID的請求都被路由到同一分割槽(參閱[第6章](ch6.md))。如果你需要讓使用者名稱是唯一的,則可以按使用者名稱的雜湊值做分割槽。
|
||||
唯一性檢查可以透過對唯一性欄位分割槽做橫向伸縮。例如,如果需要透過請求ID確保唯一性(如[例12-2]()所示),你可以確保所有具有相同請求ID的請求都被路由到同一分割槽(請參閱[第六章](ch6.md))。如果你需要讓使用者名稱是唯一的,則可以按使用者名稱的雜湊值做分割槽。
|
||||
|
||||
但非同步多主複製排除在外,因為可能會發生不同主庫同時接受衝突寫操作的情況,因而這些值不再是唯一的(參閱“[實現可線性化系統](ch9.md#實現可線性化系統)”)。如果你想立刻拒絕任何違背約束的寫入,同步協調是無法避免的【56】。
|
||||
但非同步多主複製排除在外,因為可能會發生不同主庫同時接受衝突寫操作的情況,因而這些值不再是唯一的(請參閱“[實現可線性化系統](ch9.md#實現可線性化系統)”)。如果你想立刻拒絕任何違背約束的寫入,同步協調是無法避免的【56】。
|
||||
|
||||
#### 基於日誌訊息傳遞中的唯一性
|
||||
|
||||
日誌確保所有消費者以相同的順序看見訊息 —— 這種保證在形式上被稱為**全序廣播(total order boardcast)** 並且等價於共識(參見“[全序廣播](ch9.md#全序廣播)”)。在使用基於日誌的訊息傳遞的分拆資料庫方法中,我們可以使用非常類似的方法來執行唯一性約束。
|
||||
日誌確保所有消費者以相同的順序看見訊息 —— 這種保證在形式上被稱為**全序廣播(total order boardcast)** 並且等價於共識(請參閱“[全序廣播](ch9.md#全序廣播)”)。在使用基於日誌的訊息傳遞的分拆資料庫方法中,我們可以使用非常類似的方法來執行唯一性約束。
|
||||
|
||||
流處理器在單個執行緒上依次消費單個日誌分割槽中的所有訊息(參閱“[與傳統訊息傳遞相比的日誌](ch11.md#與傳統訊息傳遞相比的日誌)”)。因此,如果日誌是按有待確保唯一的值做的分割槽,則流處理器可以無歧義地,確定性地決定幾個衝突操作中的哪一個先到達。例如,在多個使用者嘗試宣告相同使用者名稱的情況下【57】:
|
||||
流處理器在單個執行緒上依次消費單個日誌分割槽中的所有訊息(請參閱“[與傳統訊息傳遞相比的日誌](ch11.md#與傳統訊息傳遞相比的日誌)”)。因此,如果日誌是按有待確保唯一的值做的分割槽,則流處理器可以無歧義地,確定性地決定幾個衝突操作中的哪一個先到達。例如,在多個使用者嘗試宣告相同使用者名稱的情況下【57】:
|
||||
|
||||
1. 每個對使用者名稱的請求都被編碼為一條訊息,並追加到按使用者名稱雜湊值確定的分割槽。
|
||||
2. 流處理器依序讀取日誌中的請求,並使用本地資料庫來追蹤哪些使用者名稱已經被佔用了。對於所有申請可用使用者名稱的請求,它都會記錄該使用者名稱,並向輸出流傳送一條成功訊息。對於所有申請已佔用使用者名稱的請求,它都會向輸出流傳送一條拒絕訊息。
|
||||
@ -567,17 +557,17 @@ COMMIT;
|
||||
2. 流處理器讀取請求日誌。對於每個請求訊息,它向輸出流發出兩條訊息:付款人賬戶A的借記指令(按A分割槽),收款人B的貸記指令(按B分割槽)。被髮出的訊息中會帶有原始的請求ID。
|
||||
3. 後續處理器消費借記/貸記指令流,按照請求ID除重,並將變更應用至賬戶餘額。
|
||||
|
||||
步驟1和步驟2是必要的,因為如果客戶直接傳送貸記與借記指令,則需要在這兩個分割槽之間進行原子提交,以確保兩者要麼都發生或都不發生。為了避免對分散式事務的需要,我們首先將請求持久化記錄為單條訊息,然後從這第一條訊息中衍生出貸記指令與借記指令。幾乎在所有資料系統中,單物件寫入都是原子性的(參閱“[單物件寫入](ch7.md#單物件寫入)),因此請求要麼出現在日誌中,要麼就不出現,無需多分割槽原子提交。
|
||||
步驟1和步驟2是必要的,因為如果客戶直接傳送貸記與借記指令,則需要在這兩個分割槽之間進行原子提交,以確保兩者要麼都發生或都不發生。為了避免對分散式事務的需要,我們首先將請求持久化記錄為單條訊息,然後從這第一條訊息中衍生出貸記指令與借記指令。幾乎在所有資料系統中,單物件寫入都是原子性的(請參閱“[單物件寫入](ch7.md#單物件寫入)),因此請求要麼出現在日誌中,要麼就不出現,無需多分割槽原子提交。
|
||||
|
||||
如果流處理器在步驟2中崩潰,則它會從上一個存檔點恢復處理。這樣做時,它不會跳過任何請求訊息,但可能會多次處理請求併產生重複的貸記與借記指令。但由於它是確定性的,因此它只是再次生成相同的指令,而步驟3中的處理器可以使用端到端請求ID輕鬆地對其除重。
|
||||
|
||||
如果你想確保付款人的帳戶不會因此次轉賬而透支,則可以使用一個額外的流處理器來維護賬戶餘額並校驗事務(按付款人賬戶分割槽),只有有效的事務會被記錄在步驟1中的請求日誌中。
|
||||
|
||||
透過將多分割槽事務分解為兩個不同分割槽方式的階段,並使用端到端的請求ID,我們實現了同樣的正確性屬性(每個請求對付款人與收款人都恰好生效一次),即使在出現故障,且沒有使用原子提交協議的情況下依然如此。使用多個不同分割槽方式的階段與我們在“[多分割槽資料處理](#多分割槽資料處理)”中討論的想法類似(參閱“[併發控制](ch11.md#併發控制)”)。
|
||||
透過將多分割槽事務分解為兩個不同分割槽方式的階段,並使用端到端的請求ID,我們實現了同樣的正確性屬性(每個請求對付款人與收款人都恰好生效一次),即使在出現故障,且沒有使用原子提交協議的情況下依然如此。使用多個不同分割槽方式的階段與我們在“[多分割槽資料處理](#多分割槽資料處理)”中討論的想法類似(請參閱“[併發控制](ch11.md#併發控制)”)。
|
||||
|
||||
### 及時性與完整性
|
||||
|
||||
事務的一個便利屬性是,它們通常是線性一致的(參閱“[線性一致性](ch9.md#線性一致性)”),也就是說,寫入者會等到事務提交,而之後其寫入立刻對所有讀取者可見。
|
||||
事務的一個便利屬性是,它們通常是線性一致的(請參閱“[線性一致性](ch9.md#線性一致性)”),也就是說,寫入者會等到事務提交,而之後其寫入立刻對所有讀取者可見。
|
||||
|
||||
當我們把一個操作拆分為跨越多個階段的流處理器時,卻並非如此:日誌的消費者在設計上就是非同步的,因此傳送者不會等其訊息被消費者處理完。但是,客戶端等待輸出流中的特定訊息是可能的。這正是我們在“[基於日誌訊息傳遞中的唯一性](#基於日誌訊息傳遞中的唯一性)”一節中檢查唯一性約束時所做的事情。
|
||||
|
||||
@ -587,15 +577,15 @@ COMMIT;
|
||||
|
||||
***及時性(Timeliness)***
|
||||
|
||||
及時性意味著確保使用者觀察到系統的最新狀態。我們之前看到,如果使用者從陳舊的資料副本中讀取資料,它們可能會觀察到系統處於不一致的狀態(參閱“[複製延遲問題](ch5.md#複製延遲問題)”)。但這種不一致是暫時的,而最終會透過等待與重試簡單地得到解決。
|
||||
及時性意味著確保使用者觀察到系統的最新狀態。我們之前看到,如果使用者從陳舊的資料副本中讀取資料,它們可能會觀察到系統處於不一致的狀態(請參閱“[複製延遲問題](ch5.md#複製延遲問題)”)。但這種不一致是暫時的,而最終會透過等待與重試簡單地得到解決。
|
||||
|
||||
CAP定理(參閱“[線性一致性的代價](ch9.md#線性一致性的代價)”)使用**線性一致性(linearizability)** 意義上的一致性,這是實現及時性的強有力方法。像**寫後讀**這樣及時性更弱的一致性也很有用(參閱“[讀己之寫](ch5.md#讀己之寫)”)也很有用。
|
||||
CAP定理(請參閱“[線性一致性的代價](ch9.md#線性一致性的代價)”)使用**線性一致性(linearizability)** 意義上的一致性,這是實現及時性的強有力方法。像**寫後讀**這樣及時性更弱的一致性也很有用(請參閱“[讀己之寫](ch5.md#讀己之寫)”)也很有用。
|
||||
|
||||
***完整性(Integrity)***
|
||||
|
||||
完整性意味著沒有損壞;即沒有資料丟失,並且沒有矛盾或錯誤的資料。尤其是如果某些衍生資料集是作為底層資料之上的檢視而維護的(參閱“[從事件日誌匯出當前狀態](ch11.md#從事件日誌匯出當前狀態)”),這種衍生必須是正確的。例如,資料庫索引必須正確地反映資料庫的內容 —— 缺失某些記錄的索引並不是很有用。
|
||||
完整性意味著沒有損壞;即沒有資料丟失,並且沒有矛盾或錯誤的資料。尤其是如果某些衍生資料集是作為底層資料之上的檢視而維護的(請參閱“[從事件日誌匯出當前狀態](ch11.md#從事件日誌匯出當前狀態)”),這種衍生必須是正確的。例如,資料庫索引必須正確地反映資料庫的內容 —— 缺失某些記錄的索引並不是很有用。
|
||||
|
||||
如果完整性被違背,這種不一致是永久的:在大多數情況下,等待與重試並不能修復資料庫損壞。相反的是,需要顯式地檢查與修復。在ACID事務的上下文中(參閱“[ACID的涵義](ch7.md#ACID的涵義)”),一致性通常被理解為某種特定於應用的完整性概念。原子性和永續性是保持完整性的重要工具。
|
||||
如果完整性被違背,這種不一致是永久的:在大多數情況下,等待與重試並不能修復資料庫損壞。相反的是,需要顯式地檢查與修復。在ACID事務的上下文中(請參閱“[ACID的涵義](ch7.md#ACID的涵義)”),一致性通常被理解為某種特定於應用的完整性概念。原子性和永續性是保持完整性的重要工具。
|
||||
|
||||
|
||||
|
||||
@ -611,14 +601,14 @@ COMMIT;
|
||||
|
||||
另一方面,對於在本章中討論的基於事件的資料流系統而言,它們的一個有趣特性就是將及時性與完整性分開。在非同步處理事件流時不能保證及時性,除非你顯式構建一個在返回之前明確等待特定訊息到達的消費者。但完整性實際上才是流處理系統的核心。
|
||||
|
||||
**恰好一次**或**等效一次**語義(參閱“[容錯](ch11.md#容錯)”)是一種保持完整性的機制。如果事件丟失或者生效兩次,就有可能違背資料系統的完整性。因此在面對故障時,容錯訊息傳遞與重複抑制(例如,冪等操作)對於維護資料系統的完整性是很重要的。
|
||||
**恰好一次**或**等效一次**語義(請參閱“[容錯](ch11.md#容錯)”)是一種保持完整性的機制。如果事件丟失或者生效兩次,就有可能違背資料系統的完整性。因此在面對故障時,容錯訊息傳遞與重複抑制(例如,冪等操作)對於維護資料系統的完整性是很重要的。
|
||||
|
||||
正如我們在上一節看到的那樣,可靠的流處理系統可以在無需分散式事務與原子提交協議的情況下保持完整性,這意味著它們能潛在地實現好得多的效能與運維穩健性,在達到類似正確性的前提下。為了達成這種正確性,我們組合使用了多種機制:
|
||||
|
||||
* 將寫入操作的內容表示為單條訊息,從而可以輕鬆地被原子寫入 —— 與事件溯源搭配效果拔群(參閱“[事件溯源](ch11.md#事件溯源)”)。
|
||||
* 使用與儲存過程類似的確定性衍生函式,從這一訊息中衍生出所有其他的狀態變更(參見“[真的序列執行](ch7.md#真的序列執行)”和“[作為衍生函式的應用程式碼](ch11.md#作為衍生函式的應用程式碼)”)
|
||||
* 將寫入操作的內容表示為單條訊息,從而可以輕鬆地被原子寫入 —— 與事件溯源搭配效果拔群(請參閱“[事件溯源](ch11.md#事件溯源)”)。
|
||||
* 使用與儲存過程類似的確定性衍生函式,從這一訊息中衍生出所有其他的狀態變更(請參閱“[真的序列執行](ch7.md#真的序列執行)”和“[作為衍生函式的應用程式碼](ch11.md#作為衍生函式的應用程式碼)”)
|
||||
* 將客戶端生成的請求ID傳遞透過所有的處理層次,從而啟用端到端除重,帶來冪等性。
|
||||
* 使訊息不可變,並允許衍生資料能隨時被重新處理,這使從錯誤中恢復更加容易(參閱“[不可變事件的優點](ch11.md#不可變事件的優點)”)
|
||||
* 使訊息不可變,並允許衍生資料能隨時被重新處理,這使從錯誤中恢復更加容易(請參閱“[不可變事件的優點](ch11.md#不可變事件的優點)”)
|
||||
|
||||
這種機制組合在我看來,是未來構建容錯應用的一個非常有前景的方向。
|
||||
|
||||
@ -658,11 +648,11 @@ COMMIT;
|
||||
|
||||
### 信任但驗證
|
||||
|
||||
我們所有關於正確性,完整性和容錯的討論都基於一些假設,假設某些事情可能會出錯,但其他事情不會。我們將這些假設稱為我們的**系統模型(system model)**(參閱“[將系統模型對映到現實世界](ch8.md#將系統模型對映到現實世界)”):例如,我們應該假設程序可能會崩潰,機器可能突然斷電,網路可能會任意延遲或丟棄訊息。但是我們也可能假設寫入磁碟的資料在執行`fsync`後不會丟失,記憶體中的資料沒有損壞,而CPU的乘法指令總是能返回正確的結果。
|
||||
我們所有關於正確性,完整性和容錯的討論都基於一些假設,假設某些事情可能會出錯,但其他事情不會。我們將這些假設稱為我們的**系統模型(system model)**(請參閱“[將系統模型對映到現實世界](ch8.md#將系統模型對映到現實世界)”):例如,我們應該假設程序可能會崩潰,機器可能突然斷電,網路可能會任意延遲或丟棄訊息。但是我們也可能假設寫入磁碟的資料在執行`fsync`後不會丟失,記憶體中的資料沒有損壞,而CPU的乘法指令總是能返回正確的結果。
|
||||
|
||||
這些假設是相當合理的,因為大多數時候它們都是成立的,如果我們不得不經常擔心計算機出錯,那麼基本上寸步難行。在傳統上,系統模型採用二元方法處理故障:我們假設有些事情可能會發生,而其他事情**永遠**不會發生。實際上,這更像是一個概率問題:有些事情更有可能,其他事情不太可能。問題在於違反我們假設的情況是否經常發生,以至於我們可能在實踐中遇到它們。
|
||||
|
||||
我們已經看到,資料可能會在尚未落盤時損壞(參閱“[複製與永續性](ch5.md#複製與永續性)”),而網路上的資料損壞有時可能規避了TCP校驗和(參閱“[弱謊言形式](ch8.md#弱謊言形式)” )。也許我們應當更關注這些事情?
|
||||
我們已經看到,資料可能會在尚未落盤時損壞(請參閱“[複製與永續性](ch5.md#複製與永續性)”),而網路上的資料損壞有時可能規避了TCP校驗和(請參閱“[弱謊言形式](ch8.md#弱謊言形式)” )。也許我們應當更關注這些事情?
|
||||
|
||||
我過去所從事的一個應用收集了來自客戶端的崩潰報告,我們收到的一些報告,只有在這些裝置記憶體中出現了隨機位翻轉才解釋的通。這看起來不太可能,但是如果有足夠多的裝置執行你的軟體,那麼即使再不可能發生的事也確實會發生。除了由於硬體故障或輻射導致的隨機儲存器損壞之外,一些病態的儲存器訪問模式甚至可以在沒有故障的儲存器中翻轉位【62】 —— 一種可用於破壞作業系統安全機制的效應【63】(這種技術被稱為**Rowhammer**)。一旦你仔細觀察,硬體並不是看上去那樣完美的抽象。
|
||||
|
||||
@ -676,7 +666,7 @@ COMMIT;
|
||||
|
||||
而對於應用程式碼,我們不得不假設會有更多的錯誤,因為絕大多數應用的程式碼經受的評審與測試遠遠無法與資料庫的程式碼相比。許多應用甚至沒有正確使用資料庫提供的用於維持完整性的功能,例如外來鍵或唯一性約束【36】。
|
||||
|
||||
ACID意義下的一致性(參閱“[一致性](ch7.md#一致性)”)基於這樣一種想法:資料庫以一致的狀態啟動,而事務將其從一個一致狀態轉換至另一個一致的狀態。因此,我們期望資料庫始終處於一致狀態。然而,只有當你假設事務沒有Bug時,這種想法才有意義。如果應用以某種錯誤的方式使用資料庫,例如,不安全地使用弱隔離等級,資料庫的完整性就無法得到保證。
|
||||
ACID意義下的一致性(請參閱“[一致性](ch7.md#一致性)”)基於這樣一種想法:資料庫以一致的狀態啟動,而事務將其從一個一致狀態轉換至另一個一致的狀態。因此,我們期望資料庫始終處於一致狀態。然而,只有當你假設事務沒有Bug時,這種想法才有意義。如果應用以某種錯誤的方式使用資料庫,例如,不安全地使用弱隔離等級,資料庫的完整性就無法得到保證。
|
||||
|
||||
#### 不要盲目信任承諾
|
||||
|
||||
@ -698,11 +688,11 @@ COMMIT;
|
||||
|
||||
#### 為可審計性而設計
|
||||
|
||||
如果一個事務在一個數據庫中改變了多個物件,在這一事實發生後,很難說清這個事務到底意味著什麼。即使你捕獲了事務日誌(參閱“[變更資料捕獲](ch11.md#變更資料捕獲)”),各種表中的插入,更新和刪除操作並不一定能清楚地表明**為什麼**要執行這些變更。決定這些變更的是應用邏輯中的呼叫,而這一應用邏輯稍縱即逝,無法重現。
|
||||
如果一個事務在一個數據庫中改變了多個物件,在這一事實發生後,很難說清這個事務到底意味著什麼。即使你捕獲了事務日誌(請參閱“[變更資料捕獲](ch11.md#變更資料捕獲)”),各種表中的插入,更新和刪除操作並不一定能清楚地表明**為什麼**要執行這些變更。決定這些變更的是應用邏輯中的呼叫,而這一應用邏輯稍縱即逝,無法重現。
|
||||
|
||||
相比之下,基於事件的系統可以提供更好的可審計性。在事件溯源方法中,系統的使用者輸入被表示為一個單一不可變事件,而任何其導致的狀態變更都衍生自該事件。衍生可以實現為具有確定性與可重複性,因而相同的事件日誌透過相同版本的衍生程式碼時,會導致相同的狀態變更。
|
||||
|
||||
顯式處理資料流(參閱“[批處理輸出的哲學](ch10.md#批處理輸出的哲學)”)可以使資料的**來龍去脈(provenance)** 更加清晰,從而使完整性檢查更具可行性。對於事件日誌,我們可以使用雜湊來檢查事件儲存沒有被破壞。對於任何衍生狀態,我們可以重新執行從事件日誌中衍生它的批處理器與流處理器,以檢查是否獲得相同的結果,或者,甚至並行執行冗餘的衍生流程。
|
||||
顯式處理資料流(請參閱“[批處理輸出的哲學](ch10.md#批處理輸出的哲學)”)可以使資料的**來龍去脈(provenance)** 更加清晰,從而使完整性檢查更具可行性。對於事件日誌,我們可以使用雜湊來檢查事件儲存沒有被破壞。對於任何衍生狀態,我們可以重新執行從事件日誌中衍生它的批處理器與流處理器,以檢查是否獲得相同的結果,或者,甚至並行執行冗餘的衍生流程。
|
||||
|
||||
具有確定性且定義良好的資料流,也使除錯與跟蹤系統的執行變得容易,以便確定它**為什麼**做了某些事情【4,69】。如果出現意想之外的事情,那麼重現導致意外事件的確切事故現場的診斷能力—— 一種時間旅行除錯功能是非常有價值的。
|
||||
|
||||
@ -710,7 +700,7 @@ COMMIT;
|
||||
|
||||
如果我們不能完全相信系統的每個元件都不會損壞 —— 每一個硬體都沒缺陷,每一個軟體都沒有Bug —— 那我們至少必須定期檢查資料的完整性。如果我們不檢查,我們就不能發現損壞,直到無可挽回地導致對下游的破壞時,那時候再去追蹤問題就要難得多,且代價也要高的多。
|
||||
|
||||
檢查資料系統的完整性,最好是以端到端的方式進行(參閱“[資料庫端到端的爭論](#資料庫的端到端的爭論)”):我們能在完整性檢查中涵蓋的系統越多,某些處理階中出現不被察覺損壞的機率就越小。如果我們能檢查整個衍生資料管道端到端的正確性,那麼沿著這一路徑的任何磁碟,網路,服務,以及演算法的正確性檢查都隱含在其中了。
|
||||
檢查資料系統的完整性,最好是以端到端的方式進行(請參閱“[資料庫端到端的爭論](#資料庫的端到端的爭論)”):我們能在完整性檢查中涵蓋的系統越多,某些處理階中出現不被察覺損壞的機率就越小。如果我們能檢查整個衍生資料管道端到端的正確性,那麼沿著這一路徑的任何磁碟,網路,服務,以及演算法的正確性檢查都隱含在其中了。
|
||||
|
||||
持續的端到端完整性檢查可以不斷提高你對系統正確性的信心,從而使你能更快地進步【70】。與自動化測試一樣,審計提高了快速發現錯誤的可能性,從而降低了系統變更或新儲存技術可能導致損失的風險。如果你不害怕進行變更,就可以更好地充分演化一個應用,使其滿足不斷變化的需求。
|
||||
|
||||
@ -722,7 +712,7 @@ COMMIT;
|
||||
|
||||
我沒有資格評論這些技術用於貨幣,或者合同商定機制的價值。但從資料系統的角度來看,它們包含了一些有趣的想法。實質上,它們是分散式資料庫,具有資料模型與事務機制,而不同副本可以由互不信任的組織託管。副本不斷檢查其他副本的完整性,並使用共識協議對應當執行的事務達成一致。
|
||||
|
||||
我對這些技術的拜占庭容錯方面有些懷疑(參閱“[拜占庭故障](ch8.md#拜占庭故障)”),而且我發現**工作證明(proof of work)** 技術非常浪費(比如,比特幣挖礦)。比特幣的交易吞吐量相當低,儘管是出於政治與經濟原因而非技術上的原因。不過,完整性檢查的方面是很有趣的。
|
||||
我對這些技術的拜占庭容錯方面有些懷疑(請參閱“[拜占庭故障](ch8.md#拜占庭故障)”),而且我發現**工作證明(proof of work)** 技術非常浪費(比如,比特幣挖礦)。比特幣的交易吞吐量相當低,儘管是出於政治與經濟原因而非技術上的原因。不過,完整性檢查的方面是很有趣的。
|
||||
|
||||
密碼學審計與完整性檢查通常依賴**默克爾樹(Merkle tree)**【74】,這是一顆雜湊值的樹,能夠用於高效地證明一條記錄出現在一個數據集中(以及其他一些特性)。除了炒作的沸沸揚揚的加密貨幣之外,**證書透明性(certificate transparency)** 也是一種依賴Merkle樹的安全技術,用來檢查TLS/SSL證書的有效性【75,76】。
|
||||
|
||||
@ -876,7 +866,7 @@ COMMIT;
|
||||
|
||||
我們應該允許每個人保留自己的隱私 —— 即,對自己資料的控制 —— 而不是透過監視來竊取這種控制權。我們控制自己資料的個體權利就像是國家公園的自然環境:如果我們不去明確地保護它,關心它,它就會被破壞。這將是公地的悲劇,我們都會因此而變得更糟。無所不在的監視並非不可避免的 —— 我們現在仍然能阻止它。
|
||||
|
||||
我們究竟能做到哪一步,是一個開放的問題。首先,我們不應該永久保留資料,而是一旦不再需要就立即清除資料【111,112】。清除資料與不變性的想法背道而馳(參閱“[不變性的侷限性](ch11.md#不變性的侷限性)”),但這是可以解決該問題。我所看到的一種很有前景的方法是透過加密協議來實施訪問控制,而不僅僅是透過策略【113,114】。總的來說,文化與態度的改變是必要的。
|
||||
我們究竟能做到哪一步,是一個開放的問題。首先,我們不應該永久保留資料,而是一旦不再需要就立即清除資料【111,112】。清除資料與不變性的想法背道而馳(請參閱“[不變性的侷限性](ch11.md#不變性的侷限性)”),但這是可以解決該問題。我所看到的一種很有前景的方法是透過加密協議來實施訪問控制,而不僅僅是透過策略【113,114】。總的來說,文化與態度的改變是必要的。
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user