diff --git a/ch11.md b/ch11.md index 0202f3d1086389fc2e99674de6d2c242dac40a43..7f1ad29bb7044d43e72ac54e3ab7f612ee029b46 100644 --- a/ch11.md +++ b/ch11.md @@ -33,22 +33,13 @@ ​ 事件可能被编码为文本字符串或JSON,或者某种二进制编码,如[第4章](ch4.md)所述。这种编码允许你存储一个事件,例如将其附加到一个文件,将其插入关系表,或将其写入文档数据库。它还允许你通过网络将事件发送到另一个节点以进行处理。 -​ 在批处理领域,作业的输入和输出是文件(也许在分布式文件系统上)。什么是类似的流媒体? +​ 在批处理中,文件被写入一次,然后可能被多个作业读取。类似地,在流处理术语中,一个事件由**生产者(producer)**(也称为**发布者(publisher)**或**发送者(sender)**)生成一次,然后可能由多个**消费者(consumer)**(**订阅者(subscribers)**或**接收者(recipients)**)进行处理【3】。在文件系统中,文件名标识一组相关记录;在流式系统中,相关的事件通常被聚合为一个**主题(topic)**或**流(stream)**。 -​ 当输入是一个文件(一个字节序列)时,第一个处理步骤通常是将其解析为一系列记录。在流处理的上下文中,记录通常被称为事件,但它本质上是一样的:一个小的,自包含的,不可变的对象,包含某个时间点发生的事情的细节。一个事件通常包含一个时间戳,指示何时根据时钟来发生(参见“[单调钟与时钟](ch8.md#单调钟与时钟)”)。 - -​ 例如,发生的事情可能是用户采取的行动,例如查看页面或进行购买。它也可能来源于机器,例如来自温度传感器的周期性测量或者CPU利用率度量。在“[使用Unix工具进行批处理](ch10.md#使用Unix工具进行批处理)”的示例中,Web服务器日志的每一行都是一个事件。 - -​ 事件可能被编码为文本字符串或JSON,或者以某种二进制形式编码,如[第4章](ch4.md)所述。这种编码允许你存储一个事件,例如将其追加写入一个文件,将其插入关系型表,或将其写入文档数据库。它还允许你通过网络将事件发送到其他节点以进行处理。 - -​ 在批处理中,文件被写入一次,然后可能被多个作业读取。类似地,在流处理术语中,一个事件由**生产者(producer)**(也称为**发布者(publisher)**或**发送者(sender)**)生成一次,然后可能由多个**消费者(consumer)**(**订阅者(subscribers)**或**接收者(recipients)**)进行处理【3】。在文件系统中,文件名标识一组相关记录;在流媒体系统中,相关的事件通常被聚合为一个**主题(topic)**或**流(stream)**。 - -​ 原则上将,文件或数据库就足以连接生产者和消费者:生产者将其生成的每个事件写入数据存储,且每个消费者定期轮询数据存储,检查自上次运行以来新出现的事件。这实际上正是批处理在每天结束时处理当天数据时所做的事情。 +​ 原则上讲,文件或数据库就足以连接生产者和消费者:生产者将其生成的每个事件写入数据存储,且每个消费者定期轮询数据存储,检查自上次运行以来新出现的事件。这实际上正是批处理在每天结束时处理当天数据时所做的事情。 ​ 但当我们想要进行低延迟的连续处理时,如果数据存储不是为这种用途专门设计的,那么轮询开销就会很大。轮询的越频繁,能返回新事件的请求比例就越低,而额外开销也就越高。相比之下,最好能在新事件出现时直接通知消费者。 -​ 数据库在传统上对这种通知机制支持的并不好,关系型数据库通常有**触发器(trigger)**,它们可以对变化作出反应(如,插入表中的一行),但它们的功能非常有限,而且在数据库设计中算是一种事后反思【4,5】。相应的是,已经有为传递事件通知这一目开发的专用工具已经被开发出来。 - +​ 数据库在传统上对这种通知机制支持的并不好,关系型数据库通常有**触发器(trigger)**,它们可以对变化作出反应(如,插入表中的一行),但是它们的功能非常有限,并且在数据库设计中有些后顾之忧【4,5】。相应的是,已经开发了专门的工具来提供事件通知。 ### 消息系统 @@ -143,7 +134,7 @@ ​ 如果你将新的消费者添加到消息系统,通常只能接收到消费者注册之后开始发送的消息。先前的任何消息都随风而逝,一去不复返。作为对比,你可以随时为文件和数据库添加新的客户端,且能读取任意久远的数据(只要应用没有显式覆盖或删除这些数据)。 -​ 为什么我们不能把它俩杂交一下,既有数据库的持久存储方式,又有消息传递的低延迟通知?这就是**基于日志的消息代理(log-based message brokers)**背后的想法。 +​ 为什么我们不能把它俩杂交一下,既有数据库的持久存储方式,又有消息传递的低延迟通知?这就是**基于日志的消息代理(log-based message brokers)** 背后的想法。 #### 使用日志进行消息存储 @@ -252,7 +243,7 @@ ​ 数十年来,许多数据库根本没有记录在档的,获取变更日志的方式。由于这个原因,捕获数据库中所有的变更,然后将其复制到其他存储技术(搜索索引,缓存,数据仓库)中是相当困难的。 -​ 最近,人们对**变更数据捕获(change data capture, CDC)**越来越感兴趣,这是一种观察写入数据库的所有数据变更,并将其提取并转换为可以复制到其他系统中的形式的过程。 CDC是非常有意思的,尤其是当变更能在被写入后立刻用于流时。 +​ 最近,人们对**变更数据捕获(change data capture, CDC)** 越来越感兴趣,这是一种观察写入数据库的所有数据变更,并将其提取并转换为可以复制到其他系统中的形式的过程。 CDC是非常有意思的,尤其是当变更能在被写入后立刻用于流时。 ​ 例如,你可以捕获数据库中的变更,并不断将相同的变更应用至搜索索引。如果变更日志以相同的顺序应用,则可以预期搜索索引中的数据与数据库中的数据是匹配的。搜索索引和任何其他衍生数据系统只是变更流的消费者,如[图11-5](img/fig11-5.png)所示。 @@ -282,7 +273,7 @@ #### 日志压缩 -​ 如果你只能保留有限的历史日志,则每次要添加新的衍生数据系统时,都需要做一次快照。但**日志压缩(log compaction)**提供了一个很好的备选方案。 +​ 如果你只能保留有限的历史日志,则每次要添加新的衍生数据系统时,都需要做一次快照。但**日志压缩(log compaction)** 提供了一个很好的备选方案。 ​ 我们之前在日志结构存储引擎的上下文中讨论了“[Hash索引](ch3.md#Hash索引)”中的日志压缩(参见[图3-2](img/fig3-2.png)的示例)。原理很简单:存储引擎定期在日志中查找具有相同键的记录,丢掉所有重复的内容,并只保留每个键的最新更新。这个压缩与合并过程在后台运行。 @@ -304,18 +295,18 @@ ### 事件溯源 -​ 我们在这里讨论的想法和**事件溯源( Event Sourcing)**之间有一些相似之处,这是一个在**领域驱动设计(domain-driven design, DDD)**社区中折腾出来的技术。我们将简要讨论事件溯源,因为它包含了一些关于流处理系统的有用想法。 +​ 我们在这里讨论的想法和**事件溯源( Event Sourcing)** 之间有一些相似之处,这是一个在 **领域驱动设计(domain-driven design, DDD)** 社区中折腾出来的技术。我们将简要讨论事件溯源,因为它包含了一些关于流处理系统的有用想法。 -​ 与变更数据捕获类似,事件溯源涉及到**将所有对应用状态的变更**存储为变更事件日志。最大的区别是事件溯源将这一想法应用到了几个不同的抽象层次上: +​ 与变更数据捕获类似,事件溯源涉及到**将所有对应用状态的变更** 存储为变更事件日志。最大的区别是事件溯源将这一想法应用到了几个不同的抽象层次上: -* 在变更数据捕获中,应用以**可变方式(mutable way)**使用数据库,任意更新和删除记录。变更日志是从数据库的底层提取的(例如,通过解析复制日志),从而确保从数据库中提取的写入顺序与实际写入的顺序相匹配,从而避免[图11-4](img/fig11-4.png)中的竞态条件。写入数据库的应用不需要知道CDC的存在。 +* 在变更数据捕获中,应用以**可变方式(mutable way)** 使用数据库,任意更新和删除记录。变更日志是从数据库的底层提取的(例如,通过解析复制日志),从而确保从数据库中提取的写入顺序与实际写入的顺序相匹配,从而避免[图11-4](img/fig11-4.png)中的竞态条件。写入数据库的应用不需要知道CDC的存在。 * 在事件溯源中,应用逻辑显式构建在写入事件日志的不可变事件之上。在这种情况下,事件存储是仅追加写入的,更新与删除是不鼓励的或禁止的。事件被设计为旨在反映应用层面发生的事情,而不是底层的状态变更。 事件源是一种强大的数据建模技术:从应用的角度来看,将用户的行为记录为不可变的事件更有意义,而不是在可变数据库中记录这些行为的影响。事件代理使得应用随时间演化更为容易,通过事实更容易理解事情发生的原因,使得调试更为容易,并有利于防止应用Bug(请参阅“[不可变事件的优点](#不可变事件的优点)”)。 ​ 例如,存储“学生取消选课”事件以中性的方式清楚地表达了单个行为的意图,而副作用“从注册表中删除了一个条目,而一条取消原因被添加到学生反馈表“则嵌入了很多有关稍后数据使用方式的假设。如果引入一个新的应用功能,例如“将位置留给等待列表中的下一个人” —— 事件溯源方法允许将新的副作用轻松地链接至现有事件之后。 -​ 事件溯源类似于**编年史(chronicle)**数据模型【45】,事件日志与星型模式中的事实表之间也存在相似之处(参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。 +​ 事件溯源类似于**编年史(chronicle)** 数据模型【45】,事件日志与星型模式中的事实表之间也存在相似之处(参阅“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。 ​ 诸如Event Store【46】这样的专业数据库已经被开发出来,供使用事件溯源的应用使用,但总的来说,这种方法独立于任何特定的工具。传统的数据库或基于日志的消息代理也可以用来构建这种风格的应用。 @@ -365,7 +356,7 @@ $$ ​ 如果你持久存储了变更日志,那么重现状态就非常简单。如果你认为事件日志是你的记录系统,而所有的衍生状态都从它派生而来,那么系统中的数据流动就容易理解的多。正如帕特·赫兰(Pat Helland)所说的【52】: -> 事务日志记录了数据库的所有变更。高速追加下入是更改日志的唯一方法。从这个角度来看,数据库的内容其实是日志中记录最新值的缓存。日志才是真相,数据库是日志子集的缓存,这一缓存子集恰好来自日志中每条记录与索引值的最新值。 +> 事务日志记录了数据库的所有变更。高速追加是更改日志的唯一方法。从这个角度来看,数据库的内容其实是日志中记录最新值的缓存。日志才是真相,数据库是日志子集的缓存,这一缓存子集恰好来自日志中每条记录与索引值的最新值。 ​ 日志压缩(如“[日志压缩](#日志压缩)”中所述)是连接日志与数据库状态之间的桥梁:它只保留每条记录的最新版本,并丢弃被覆盖的版本。 @@ -444,7 +435,7 @@ $$ #### 复合事件处理 -​ **复合事件处理(complex, event processing, CEP)**是20世纪90年代为分析事件流而开发出的一种方法,尤其适用于需要搜索某些事件模式的应用【65,66】。与正则表达式允许你在字符串中搜索特定字符模式的方式类似,CEP允许你指定规则以在流中搜索某些事件模式。 +​ **复合事件处理(complex, event processing, CEP)** 是20世纪90年代为分析事件流而开发出的一种方法,尤其适用于需要搜索某些事件模式的应用【65,66】。与正则表达式允许你在字符串中搜索特定字符模式的方式类似,CEP允许你指定规则以在流中搜索某些事件模式。 ​ CEP系统通常使用高层次的声明式查询语言,比如SQL,或者图形用户界面,来描述应该检测到的事件模式。这些查询被提交给处理引擎,该引擎消费输入流,并在内部维护一个执行所需匹配的状态机。当发现匹配时,引擎发出一个**复合事件(complex event)**(因此得名),并附有检测到的事件模式详情【67】。 @@ -468,7 +459,7 @@ $$ #### 维护物化视图 -​ 我们在“[数据库和数据流](#数据库和数据流)”中看到,数据库的变更流可以用于维护衍生数据系统(如缓存,搜索索引和数据仓库),使其与源数据库保持最新。我们可以将这些示例视作维护**物化视图(materialized view)**的一种具体场景(参阅“[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”):在某个数据集上衍生出一个替代视图以便高效查询,并在底层数据变更时更新视图【50】。 +​ 我们在“[数据库和数据流](#数据库和数据流)”中看到,数据库的变更流可以用于维护衍生数据系统(如缓存,搜索索引和数据仓库),使其与源数据库保持最新。我们可以将这些示例视作维护**物化视图(materialized view)** 的一种具体场景(参阅“[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”):在某个数据集上衍生出一个替代视图以便高效查询,并在底层数据变更时更新视图【50】。 ​ 同样,在事件溯源中,应用程序的状态是通过**应用(apply)**事件日志来维护的;这里的应用状态也是一种物化视图。与流分析场景不同的是,仅考虑某个时间窗口内的事件通常是不够的:构建物化视图可能需要任意时间段内的**所有**事件,除了那些可能由日志压缩丢弃的过时事件(请参阅“[日志压缩](#日志压缩)“)。实际上,你需要一个可以一直延伸到时间开端的窗口。 @@ -512,7 +503,7 @@ $$ ​ 而且,消息延迟还可能导致无法预测消息顺序。例如,假设用户首先发出一个Web请求(由Web服务器A处理),然后发出第二个请求(由服务器B处理)。 A和B发出描述它们所处理请求的事件,但是B的事件在A的事件发生之前到达消息代理。现在,流处理器将首先看到B事件,然后看到A事件,即使它们实际上是以相反的顺序发生的。 -​ 有一个类比也许能帮助理解,“星球大战”电影:第四集于1977年发行,第五集于1980年,第六集于1983年,紧随其后的是1999年的第一集,2002年的第二集,和2005年的三集,以及2015年的第七集【80】[^ii]。如果你按照按照它们上映的顺序观看电影,你处理电影的顺序与它们叙事的顺序就是不一致的。 (集数编号就像事件时间戳,而你观看电影的日期就是处理时间)作为人类,我们能够应对这种不连续性,但是流处理算法需要专门写就,以适应这种时机与顺序的问题。 +​ 有一个类比也许能帮助理解,“星球大战”电影:第四集于1977年发行,第五集于1980年,第六集于1983年,紧随其后的是1999年的第一集,2002年的第二集,和2005年的三集,以及2015年的第七集【80】[^ii]。如果你按照按照它们上映的顺序观看电影,你处理电影的顺序与它们叙事的顺序就是不一致的。 (集数编号就像事件时间戳,而你观看电影的日期就是处理时间)作为人类,我们能够应对这种不连续性,但是流处理算法需要专门编写,以适应这种时机与顺序的问题。 [^ii]: 感谢Flink社区的Kostas Kloudas提出这个比喻。 @@ -528,7 +519,7 @@ $$ ​ 例如,假设你将事件分组为一分钟的窗口,以便统计每分钟的请求数。你已经计数了一些带有本小时内第37分钟时间戳的事件,时间流逝,现在进入的主要都是本小时内第38和第39分钟的事件。什么时候才能宣布你已经完成了第37分钟的窗口计数,并输出其计数器值? -​ 在一段时间没有看到任何新的事件之后,你可以超时并宣布一个窗口已经就绪,但仍然可能发生这种情况:某些事件被缓冲在另一台机器上,由于网络中断而延迟。你需要能够处理这种在窗口宣告完成之后到达的**滞留(straggler)**事件。大体上,你有两种选择【1】: +​ 在一段时间没有看到任何新的事件之后,你可以超时并宣布一个窗口已经就绪,但仍然可能发生这种情况:某些事件被缓冲在另一台机器上,由于网络中断而延迟。你需要能够处理这种在窗口宣告完成之后到达的 **滞留(straggler)** 事件。大体上,你有两种选择【1】: 1. 忽略这些滞留事件,因为在正常情况下它们可能只是事件中的一小部分。你可以将丢弃事件的数量作为一个监控指标,并在出现大量丢消息的情况时报警。 2. 发布一个**更正(correction)**,一个包括滞留事件的更新窗口值。更新的窗口与包含散兵队员的价值。你可能还需要收回以前的输出。 @@ -561,7 +552,7 @@ $$ ***跳动窗口(Hopping Window)*** -​ 跳动窗口也有着固定的长度,但允许窗口重叠以提供一些平滑。例如,一个带有1分钟跳跃步长的5分钟窗口将包含`10:03:00`至`10:07:59`之间的事件,而下一个窗口将覆盖`10:04:00`至`10:08`之间的事件: 59,等等。通过首先计算1分钟的滚动窗口,然后在几个相邻窗口上进行聚合,可以实现这种跳动窗口。 +​ 跳动窗口也有着固定的长度,但允许窗口重叠以提供一些平滑。例如,一个带有1分钟跳跃步长的5分钟窗口将包含`10:03:00`至`10:07:59`之间的事件,而下一个窗口将覆盖`10:04:00`至`10:08:59`之间的事件,等等。通过首先计算1分钟的滚动窗口,然后在几个相邻窗口上进行聚合,可以实现这种跳动窗口。 ***滑动窗口(Sliding Window)*** @@ -569,7 +560,7 @@ $$ ***会话窗口(Session window)*** -​ 与其他窗口类型不同,会话窗口没有固定的持续时间,而定义为:将同一用户出现时间相近的所有事件分组在一起,而当用户一段时间没有活动时(例如,如果30分钟内没有事件)窗口结束。会话切分是网站分析的常见需求(参阅“[GROUP BY](ch10.md#GROUP BY)”)。 +​ 与其他窗口类型不同,会话窗口没有固定的持续时间,而定义为:将同一用户出现时间相近的所有事件分组在一起,而当用户一段时间没有活动时(例如,如果30分钟内没有事件)窗口结束。会话切分是网站分析的常见需求(参阅“[GROUP BY](ch10.md#GROUP\ BY)”)。 ### 流式连接 @@ -589,7 +580,7 @@ $$ #### 流表连接(流扩展) -​ 在“[示例:用户活动事件分析](ch10.md#示例:用户活动事件分析)”([图10-2](img/fig10-2.png))中,我们看到了连接两个数据集的批处理作业示例:一组用户活动事件和一个用户档案数据库。将用户活动事件视为流,并在流处理器中连续执行相同的连接是很自然的想法:输入是包含用户ID的活动事件流,而输出还是活动事件流,但其中用户ID已经被扩展为用户的档案信息。这个过程有时被称为 使用数据库的信息来**扩充(enriching)**活动事件。 +​ 在“[示例:用户活动事件分析](ch10.md#示例:用户活动事件分析)”([图10-2](img/fig10-2.png))中,我们看到了连接两个数据集的批处理作业示例:一组用户活动事件和一个用户档案数据库。将用户活动事件视为流,并在流处理器中连续执行相同的连接是很自然的想法:输入是包含用户ID的活动事件流,而输出还是活动事件流,但其中用户ID已经被扩展为用户的档案信息。这个过程有时被称为 使用数据库的信息来**扩充(enriching)** 活动事件。 ​ 要执行此联接,流处理器需要一次处理一个活动事件,在数据库中查找事件的用户ID,并将档案信息添加到活动事件中。数据库查询可以通过查询远程数据库来实现。但正如在“[示例:分析用户活动事件](ch10.md#示例:分析用户活动事件)”一节中讨论的,此类远程查询可能会很慢,并且有可能导致数据库过载【75】。 @@ -644,7 +635,7 @@ GROUP BY follows.follower_id ​ 在本章的最后一节中,让我们看一看流处理是如何容错的。我们在[第10章](ch10.md)中看到,批处理框架可以很容易地容错:如果MapReduce作业中的任务失败,可以简单地在另一台机器上再次启动,并且丢弃失败任务的输出。这种透明的重试是可能的,因为输入文件是不可变的,每个任务都将其输出写入到HDFS上的独立文件中,而输出仅当任务成功完成后可见。 -​ 特别是,批处理容错方法可确保批处理作业的输出与没有出错的情况相同,即使实际上某些任务失败了。看起来好像每条输入记录都被处理了恰好一次 —— 没有记录被跳过,而且没有记录被处理两次。尽管重启任务意味着实际上可能会多次处理记录,但输出中的可见效果看上去就像只处理过一次。这个原则被称为**恰好一次语义(exactly-once semantics)**,尽管**有效一次(effectively-once)**可能会是一个更写实的术语【90】。 +​ 特别是,批处理容错方法可确保批处理作业的输出与没有出错的情况相同,即使实际上某些任务失败了。看起来好像每条输入记录都被处理了恰好一次 —— 没有记录被跳过,而且没有记录被处理两次。尽管重启任务意味着实际上可能会多次处理记录,但输出中的可见效果看上去就像只处理过一次。这个原则被称为**恰好一次语义(exactly-once semantics)**,尽管**有效一次(effectively-once)** 可能会是一个更写实的术语【90】。 ​ 在流处理中也出现了同样的容错问题,但是处理起来没有那么直观:等待某个任务完成之后再使其输出可见并不是一个可行选项,因为你永远无法处理完一个无限的流。 @@ -654,7 +645,7 @@ GROUP BY follows.follower_id ​ 微批次也隐式提供了一个与批次大小相等的滚动窗口(按处理时间而不是事件时间戳分窗)。任何需要更大窗口的作业都需要显式地将状态从一个微批次转移到下一个微批次。 -​ Apache Flink则使用不同的方法,它会定期生成状态的滚动存档点并将其写入持久存储【92,93】。如果流算子崩溃,它可以从最近的存档点重启,并丢弃从最近检查点到崩溃之间的所有输出。存档点会由消息流中的**壁障(barrier)**触发,类似于微批次之间的边界,但不会强制一个特定的窗口大小。 +​ Apache Flink则使用不同的方法,它会定期生成状态的滚动存档点并将其写入持久存储【92,93】。如果流算子崩溃,它可以从最近的存档点重启,并丢弃从最近检查点到崩溃之间的所有输出。存档点会由消息流中的 **壁障(barrier)** 触发,类似于微批次之间的边界,但不会强制一个特定的窗口大小。 ​ 在流处理框架的范围内,微批次与存档点方法提供了与批处理一样的**恰好一次语义**。但是,只要输出离开流处理器(例如,写入数据库,向外部消息代理发送消息,或发送电子邮件),框架就无法抛弃失败批次的输出了。在这种情况下,重启失败任务会导致外部副作用发生两次,只有微批次或存档点不足以阻止这一问题。 diff --git a/ch12.md b/ch12.md index 423331def86856d10c67bd22dbf17d965ab7939c..63b3fd0d1cfce13476f9e5fb4aee81eab54d1255 100644 --- a/ch12.md +++ b/ch12.md @@ -10,9 +10,9 @@ [TOC] -​ 到目前为止,本书主要描述的是**实然**问题:现在事情**是**什么样的。在这最后一章中,我们将放眼未来,讨论**应然**的问题:事情**应该**是什么样子的。我将提出一些想法与方法,我相信它们能从根本上改进我们设计与构建应用的方式。 +​ 到目前为止,本书主要描述的是**现状**。在这最后一章中,我们将放眼**未来**,讨论应该是怎么样的:我将提出一些想法与方法,我相信它们能从根本上改进我们设计与构建应用的方式。 -​ 对未来的看法与推测当然具有很大的主观性。所以在撰写本章时,当提及我个人的观点时会使用第一人称。您完全可以不同意这些观点并提出自己的看法,但我希望本章中的概念,至少能成为富有成效讨论的出发点,并澄清一些经常被混淆的概念。 +​ 对未来的看法与推测当然具有很大的主观性。所以在撰写本章时,当提及我个人的观点时会使用第一人称。您完全可以不同意这些观点并提出自己的看法,但我希望本章中的概念,至少能成为富有成效的讨论出发点,并澄清一些经常被混淆的概念。 ​ [第1章](ch1.md)概述了本书的目标:探索如何创建**可靠**,**可扩展**和**可维护**的应用与系统。这一主题贯穿了所有的章节:例如,我们讨论了许多有助于提高可靠性的容错算法,有助于提高可扩展性的分区,以及有助于提高可维护性的演化与抽象机制。在本章中,我们将把所有这些想法结合在一起,并在它们的基础上展望未来。我们的目标是,发现如何设计出比现有应用更好的应用 —— 健壮,正确,可演化,且最终对人类有益。 @@ -54,7 +54,7 @@ ​ 在抽象层面,它们通过不同的方式达到类似的目标。分布式事务通过**锁**进行互斥来决定写入的顺序(参阅“[两阶段锁定(2PL)](ch7.md#两阶段锁定(2PL))”),而CDC和事件溯源使用日志进行排序。分布式事务使用原子提交来确保变更只生效一次,而基于日志的系统通常基于**确定性重试**和**幂等性**。 -​ 最大的不同之处在于事务系统通常提供**[线性一致性](ch9.md#线性一致性)**,这包含着有用的保证,例如[读己之写](ch5.md#读己之写)。另一方面,衍生数据系统通常是异步更新的,因此它们默认不会提供相同的时序保证。 +​ 最大的不同之处在于事务系统通常提供 **[线性一致性](ch9.md#线性一致性)**,这包含着有用的保证,例如[读己之写](ch5.md#读己之写)。另一方面,衍生数据系统通常是异步更新的,因此它们默认不会提供相同的时序保证。 ​ 在愿意为分布式事务付出代价的有限场景中,它们已被成功应用。但是,我认为XA的容错能力和性能很差劲(参阅“[实践中的分布式事务](ch9.md#实践中的分布式事务)”),这严重限制了它的实用性。我相信为分布式事务设计一种更好的协议是可行的。但使这样一种协议被现有工具广泛接受是很有挑战的,且不是立竿见影的事。 @@ -77,7 +77,7 @@ * 某些应用程序在客户端保存状态,该状态在用户输入时立即更新(无需等待服务器确认),甚至可以继续脱机工作(参阅“[脱机操作的客户端](ch5.md#脱机操作的客户端)”)。有了这样的应用程序,客户端和服务器很可能以不同的顺序看到事件。 - 在形式上,决定事件的全局顺序称为**全序广播**,相当于**共识**(参阅“[共识算法和全序广播](ch9.md#共识算法和全序广播)”)。大多数共识算法都是针对单个节点的吞吐量足以处理整个事件流的情况而设计的,并且这些算法不提供多个节点共享事件排序工作的机制。设计可以扩展到单个节点的吞吐量之上,且在地理散布环境中仍然工作良好的的共识算法仍然是一个开放的研究问题。 +在形式上,决定事件的全局顺序称为**全序广播**,相当于**共识**(参阅“[共识算法和全序广播](ch9.md#共识算法和全序广播)”)。大多数共识算法都是针对单个节点的吞吐量足以处理整个事件流的情况而设计的,并且这些算法不提供多个节点共享事件排序工作的机制。设计可以扩展到单个节点的吞吐量之上,且在地理散布环境中仍然工作良好的的共识算法仍然是一个开放的研究问题。 #### 排序事件以捕捉因果关系 @@ -190,7 +190,7 @@ ​ 想想当你运行`CREATE INDEX`在关系数据库中创建一个新的索引时会发生什么。数据库必须扫描表的一致性快照,挑选出所有被索引的字段值,对它们进行排序,然后写出索引。然后它必须处理自一致快照以来所做的写入操作(假设表在创建索引时未被锁定,所以写操作可能会继续)。一旦完成,只要事务写入表中,数据库就必须继续保持索引最新。 -​ 此过程非常类似于设置新的从库副本(参阅“[设置新的追随者]()”),也非常类似于流处理系统中的**引导(bootstrap)**变更数据捕获(请参阅第455页的“[初始快照]()”)。 +​ 此过程非常类似于设置新的从库副本(参阅“[设置新的追随者]()”),也非常类似于流处理系统中的**引导(bootstrap)** 变更数据捕获(请参阅第455页的“[初始快照]()”)。 ​ 无论何时运行`CREATE INDEX`,数据库都会重新处理现有数据集(如第494页的“[重新处理应用程序数据的演变数据]()”中所述),并将该索引作为新视图导出到现有数据上。现有数据可能是状态的快照,而不是所有发生变化的日志,但两者密切相关(请参阅“[状态,数据流和不变性]()”第459页)。 @@ -204,7 +204,7 @@ **联合数据库:统一读取** -​ 可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 —— 一种称为**联合数据库(federated database)**或**多态存储(polystore)**的方法【18,19】。例如,PostgreSQL的外部数据包装器功能符合这种模式【20】。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。 +​ 可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 —— 一种称为**联合数据库(federated database)** 或**多态存储(polystore)** 的方法【18,19】。例如,PostgreSQL的外部数据包装器功能符合这种模式【20】。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。 ​ 联合查询接口遵循着单一集成系统与关系型模型的传统,带有高级查询语言和优雅的语义,但实现起来非常复杂。 @@ -220,7 +220,7 @@ ​ 传统的同步写入方法需要跨异构存储系统的分布式事务【18】,我认为这是错误的解决方案(请参阅“[导出的数据与分布式事务]()”第495页)。单个存储或流处理系统内的事务是可行的,但是当数据跨越不同技术之间的边界时,我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法。 -​ 例如,分布式事务在某些流处理组件内部使用,以匹配**恰好一次(exactly-once)**语义(请参阅第477页的“[重新访问原子提交]()”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理组件写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的事件的有序事件日志(参见第478页的“[幂等性]()”)是一种更简单的抽象,因此在异构系统中实现更加可行【7】。 +​ 例如,分布式事务在某些流处理组件内部使用,以匹配**恰好一次(exactly-once)** 语义(请参阅第477页的“[重新访问原子提交]()”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理组件写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的事件的有序事件日志(参见第478页的“[幂等性]()”)是一种更简单的抽象,因此在异构系统中实现更加可行【7】。 基于日志的集成的一大优势是各个组件之间的**松散耦合(loose coupling)**,这体现在两个方面: @@ -249,7 +249,7 @@ ​ 使用应用代码组合专用存储与处理系统来分拆数据库的方法,也被称为“**数据库由内而外**”方法【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】。在电子表格中,可以将公式放入一个单元格中(例如,另一列中的单元格求和值),并且只要公式的任何输入发生变更,公式的结果都会自动重新计算。这正是我们在数据系统层次所需要的:当数据库中的记录发生变更时,我们希望自动更新该记录的任何索引,并且自动刷新依赖于记录的任何缓存视图或聚合。您不必担心这种刷新如何发生的技术细节,但能够简单地相信它可以正常工作。 @@ -275,7 +275,7 @@ ​ 另一方面,Mesos,YARN,Docker,Kubernetes等部署和集群管理工具专为运行应用代码而设计。通过专注于做好一件事情,他们能够做得比将数据库作为其众多功能之一执行用户定义的功能要好得多。我认为让系统的某些部分专门用于持久数据存储以及专门运行应用程序代码的其他部分是有意义的。这两者可以在保持独立的同时互动。 -​ 现在大多数Web应用程序都是作为无状态服务部署的,其中任何用户请求都可以路由到任何应用程序服务器,并且服务器在发送响应后会忘记所有请求。这种部署方式很方便,因为可以随意添加或删除服务器,但状态必须到某个地方:通常是数据库。趋势是将无状态应用程序逻辑与状态管理(数据库)分开:不将应用程序逻辑放入数据库中,也不将持久状态置于应用程序中【36】。正如职能规划界人士喜欢开玩笑说的那样,“我们相信**教会(Church)**与**国家(state)**的分离”【37】 [^i] +​ 现在大多数Web应用程序都是作为无状态服务部署的,其中任何用户请求都可以路由到任何应用程序服务器,并且服务器在发送响应后会忘记所有请求。这种部署方式很方便,因为可以随意添加或删除服务器,但状态必须到某个地方:通常是数据库。趋势是将无状态应用程序逻辑与状态管理(数据库)分开:不将应用程序逻辑放入数据库中,也不将持久状态置于应用程序中【36】。正如职能规划界人士喜欢开玩笑说的那样,“我们相信**教会(Church)** 与**国家(state)** 的分离”【37】 [^i] [^i]: 解释笑话很少会让人感觉更好,但我不想让任何人感到被遗漏。 在这里,Church指代的是数学家的阿隆佐·邱奇,他创立了lambda演算,这是计算的早期形式,是大多数函数式编程语言的基础。 lambda演算不具有可变状态(即没有变量可以被覆盖),所以可以说可变状态与Church的工作是分离的。 @@ -289,7 +289,7 @@ ​ 从数据流的角度思考应用,意味着重新协调应用代码和状态管理之间的关系。将数据库视作被应用操纵的被动变量,取而代之的是更多地考虑状态,状态变更和处理它们的代码之间的相互作用与协同关系。应用代码通过在另一个地方触发状态变更来响应状态变更。 -​ 我们在“[流与数据库](ch11.md#流与数据库)”中看到了这一思路,我们讨论了将数据库的变更日志视为一种我们可以订阅的事件流。诸如Actor的消息传递系统(参阅“[消息传递数据流](ch4.md#消息传递数据流)”)也具有响应事件的概念。早在20世纪80年代,**元组空间(tuple space)**模型就已经探索了表达分布式计算的方式:观察状态变更并作出反应【38,39】。 +​ 我们在“[流与数据库](ch11.md#流与数据库)”中看到了这一思路,我们讨论了将数据库的变更日志视为一种我们可以订阅的事件流。诸如Actor的消息传递系统(参阅“[消息传递数据流](ch4.md#消息传递数据流)”)也具有响应事件的概念。早在20世纪80年代,**元组空间(tuple space)** 模型就已经探索了表达分布式计算的方式:观察状态变更并作出反应【38,39】。 ​ 如前所述,当触发器由于数据变更而被触发时,或次级索引更新以反映索引表中的变更时,数据库内部也发生着类似的情况。分拆数据库意味着将这个想法应用于在主数据库之外,用于创建衍生数据集:缓存,全文搜索索引,机器学习或分析系统。我们可以为此使用流处理和消息传递系统。 @@ -364,7 +364,7 @@ ​ 传统上,网络浏览器是无状态的客户端,只有当连接到互联网时才能做一些有用的事情(能离线执行的唯一事情基本上就是上下滚动之前在线时加载好的页面)。然而,最近的“单页面”JavaScript Web应用已经获得了很多有状态的功能,包括客户端用户界面交互,以及Web浏览器中的持久化本地存储。移动应用可以类似地在设备上存储大量状态,而且大多数用户交互都不需要与服务器往返交互。 -​ 这些不断变化的功能重新引发了对**离线优先(offline-first)**应用的兴趣,这些应用尽可能地在同一设备上使用本地数据库,无需连接互联网,并在后台网络连接可用时与远程服务器同步【42】。由于移动设备通常具有缓慢且不可靠的蜂窝网络连接,因此,如果用户的用户界面不必等待同步网络请求,且应用主要是离线工作的,则这是一个巨大优势(参阅“[具有离线操作的客户端](ch5.md#具有离线操作的客户端)”)。 +​ 这些不断变化的功能重新引发了对**离线优先(offline-first)** 应用的兴趣,这些应用尽可能地在同一设备上使用本地数据库,无需连接互联网,并在后台网络连接可用时与远程服务器同步【42】。由于移动设备通常具有缓慢且不可靠的蜂窝网络连接,因此,如果用户的用户界面不必等待同步网络请求,且应用主要是离线工作的,则这是一个巨大优势(参阅“[具有离线操作的客户端](ch5.md#具有离线操作的客户端)”)。 ​ 当我们摆脱无状态客户端与中央数据库交互的假设,并转向在终端用户设备上维护状态时,这就开启了新世界的大门。特别是,我们可以将设备上的状态视为**服务器状态的缓存**。屏幕上的像素是客户端应用中模型对象的物化视图;模型对象是远程数据中心的本地状态副本【27】。 @@ -382,7 +382,7 @@ ​ 最近用于开发带状态客户端与用户界面的工具,例如如Elm语言【30】和Facebook的React,Flux和Redux工具链,已经通过订阅表示用户输入和服务器响应的事件流,来管理客户端的内部状态,其结构与事件溯源相似(请参阅第457页的“[事件溯源](ch11.md#事件溯源)”)。 -​ 将这种编程模型扩展为:允许服务器将状态变更事件,推送到客户端的事件管道中,是非常自然的。因此,状态变化可以通过**端到端(end-to-end)**的写路径流动:从一个设备上的交互触发状态变更开始,经由事件日志,并穿过几个衍生数据系统与流处理器,一直到另一台设备上的用户界面,而有人正在观察用户界面上的状态变化。这些状态变化能以相当低的延迟传播 —— 比如说,在一秒内从一端到另一端。 +​ 将这种编程模型扩展为:允许服务器将状态变更事件,推送到客户端的事件管道中,是非常自然的。因此,状态变化可以通过**端到端(end-to-end)** 的写路径流动:从一个设备上的交互触发状态变更开始,经由事件日志,并穿过几个衍生数据系统与流处理器,一直到另一台设备上的用户界面,而有人正在观察用户界面上的状态变化。这些状态变化能以相当低的延迟传播 —— 比如说,在一秒内从一端到另一端。 ​ 一些应用(如即时消息传递与在线游戏)已经具有这种“实时”架构(在低延迟交互的意义上,不是在“[响应时间保证](ch8.md#响应时间保证)”中的意义上)。但我们为什么不用这种方式构建所有的应用? @@ -424,7 +424,7 @@ ​ 我们希望构建可靠且**正确**的应用(即使面对各种故障,程序的语义也能被很好地定义与理解)。约四十年来,原子性,隔离性和持久性([第7章](ch7.md))等事务特性一直是构建正确应用的首选工具。然而这些地基没有看上去那么牢固:例如弱隔离级别带来的困惑可以佐证(请参见“[弱隔离级别](ch7.md#弱隔离级别)”)。 -​ 事务在某些领域被完全抛弃,并被提供更好性能与可扩展性的模型取代,但更复杂的语义(例如,参阅“[无领导者复制](ch5.md#无领导者复制)”)。**一致性(Consistency)**经常被谈起,但其定义并不明确(“[一致性](ch5.md#一致性)”和[第9章](ch9.md))。有些人断言我们应当为了高可用而“拥抱弱一致性”,但却对这些概念实际上意味着什么缺乏清晰的认识。 +​ 事务在某些领域被完全抛弃,并被提供更好性能与可扩展性的模型取代,但更复杂的语义(例如,参阅“[无领导者复制](ch5.md#无领导者复制)”)。**一致性(Consistency)** 经常被谈起,但其定义并不明确(“[一致性](ch5.md#一致性)”和[第9章](ch9.md))。有些人断言我们应当为了高可用而“拥抱弱一致性”,但却对这些概念实际上意味着什么缺乏清晰的认识。 ​ 对于如此重要的话题,我们的理解,以及我们的工程方法却是惊人地薄弱。例如,确定在特定事务隔离等级或复制配置下运行特定应用是否安全是非常困难的【51,52】。通常简单的解决方案似乎在低并发性的情况下工作正常,并且没有错误,但在要求更高的情况下却会出现许多微妙的错误。 @@ -475,7 +475,7 @@ COMMIT; #### 操作标识符 -​ 要在通过几跳的网络通信上使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的 —— 你需要考虑**端到端(end-to-end)**的请求流。 +​ 要在通过几跳的网络通信上使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的 —— 你需要考虑**端到端(end-to-end)** 的请求流。 例如,你可以为操作生成一个唯一的标识符(例如UUID),并将其作为隐藏表单字段包含在客户端应用中,或通过计算所有表单相关字段的散列来生成操作ID 【3】。如果Web浏览器提交了两次POST请求,这两个请求将具有相同的操作ID。然后,你可以将该操作ID一路传递到数据库,并检查你是否曾经使用给定的ID执行过一个操作,如[例12-2]()中所示。 **例12-2 使用唯一ID来抑制重复请求** @@ -542,7 +542,7 @@ COMMIT; #### 基于日志消息传递中的唯一性 -​ 日志确保所有消费者以相同的顺序看见消息 —— 这种保证在形式上被称为**全序广播(total order boardcast)**并且等价于共识(参见“[全序广播](ch9.md#全序广播)”)。在使用基于日志的消息传递的分拆数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。 +​ 日志确保所有消费者以相同的顺序看见消息 —— 这种保证在形式上被称为**全序广播(total order boardcast)** 并且等价于共识(参见“[全序广播](ch9.md#全序广播)”)。在使用基于日志的消息传递的分拆数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。 ​ 流处理器在单个线程上依次消费单个日志分区中的所有消息(参阅“[与传统消息传递相比的日志](ch11.md#与传统消息传递相比的日志)”)。因此,如果日志是按有待确保唯一的值做的分区,则流处理器可以无歧义地,确定性地决定几个冲突操作中的哪一个先到达。例如,在多个用户尝试宣告相同用户名的情况下【57】: @@ -583,13 +583,13 @@ COMMIT; ​ 在这个例子中,唯一性检查的正确性不取决于消息发送者是否等待结果。等待的目的仅仅是同步通知发送者唯一性检查是否成功。但该通知可以与消息处理的结果相解耦。 -​ 更一般地来讲,我认为术语**一致性(consistency)**这个术语混淆了两个值得分别考虑的需求: +​ 更一般地来讲,我认为术语**一致性(consistency)** 这个术语混淆了两个值得分别考虑的需求: ***及时性(Timeliness)*** ​ 及时性意味着确保用户观察到系统的最新状态。我们之前看到,如果用户从陈旧的数据副本中读取数据,它们可能会观察到系统处于不一致的状态(参阅“[复制延迟问题](ch5.md#复制延迟问题)”)。但这种不一致是暂时的,而最终会通过等待与重试简单地得到解决。 -​ CAP定理(参阅“[线性一致性的代价](ch9.md#线性一致性的代价)”)使用**线性一致性(linearizability)**意义上的一致性,这是实现及时性的强有力方法。像**写后读**这样及时性更弱的一致性也很有用(参阅“[读己之写](ch5.md#读己之写)”)也很有用。 +​ CAP定理(参阅“[线性一致性的代价](ch9.md#线性一致性的代价)”)使用**线性一致性(linearizability)** 意义上的一致性,这是实现及时性的强有力方法。像**写后读**这样及时性更弱的一致性也很有用(参阅“[读己之写](ch5.md#读己之写)”)也很有用。 ***完整性(Integrity)*** @@ -648,7 +648,7 @@ COMMIT; 1. 数据流系统可以维持衍生数据的完整性保证,而无需原子提交,线性一致性,或者同步跨分区协调。 2. 虽然严格的唯一性约束要求及时性和协调,但许多应用实际上可以接受宽松的约束:只要整个过程保持完整性,这些约束可能会被临时违反并在稍后被修复。 -总之这些观察意味着,数据流系统可以为许多应用提供无需协调的数据管理服务,且仍能给出很强的完整性保证。这种**无协调(coordination-avoiding)**的数据系统有着很大的吸引力:比起需要执行同步协调的系统,它们能达到更好的性能与更强的容错能力【56】。 +总之这些观察意味着,数据流系统可以为许多应用提供无需协调的数据管理服务,且仍能给出很强的完整性保证。这种**无协调(coordination-avoiding)** 的数据系统有着很大的吸引力:比起需要执行同步协调的系统,它们能达到更好的性能与更强的容错能力【56】。 ​ 例如,这种系统可以使用多领导者配置运维,跨越多个数据中心,在区域间异步复制。任何一个数据中心都可以持续独立运行,因为不需要同步的跨区域协调。这样的系统时效性保证会很弱 —— 如果不引入协调它是不可能是线性一致的 —— 但它仍然可以提供有力的完整性保证。 @@ -690,7 +690,7 @@ COMMIT; #### 验证的文化 -​ 像HDFS和S3这样的系统仍然需要假设磁盘大部分时间都能正常工作 —— 这是一个合理的假设,但与它们**始终**能正常工作的假设并不相同。然而目前还没有多少系统采用这种“信任但是验证”的方式来持续审计自己。许多人认为正确性保证是绝对的,并且没有为罕见的数据损坏的可能性做过准备。我希望未来能看到更多的**自我验证(self-validating)**或**自我审计(self-auditing)**系统,不断检查自己的完整性,而不是依赖盲目的信任【68】。 +​ 像HDFS和S3这样的系统仍然需要假设磁盘大部分时间都能正常工作 —— 这是一个合理的假设,但与它们**始终**能正常工作的假设并不相同。然而目前还没有多少系统采用这种“信任但是验证”的方式来持续审计自己。许多人认为正确性保证是绝对的,并且没有为罕见的数据损坏的可能性做过准备。我希望未来能看到更多的**自我验证(self-validating)** 或**自我审计(self-auditing)** 系统,不断检查自己的完整性,而不是依赖盲目的信任【68】。 ​ 我担心ACID数据库的文化导致我们在盲目信任技术(如事务机制)的基础上开发应用,而忽视了这种过程中的任何可审计性。由于我们所信任的技术在大多数情况下工作得很好,通常会认为审计机制并不值得投资。 @@ -702,7 +702,7 @@ COMMIT; ​ 相比之下,基于事件的系统可以提供更好的可审计性。在事件溯源方法中,系统的用户输入被表示为一个单一不可变事件,而任何其导致的状态变更都衍生自该事件。衍生可以实现为具有确定性与可重复性,因而相同的事件日志通过相同版本的衍生代码时,会导致相同的状态变更。 -​ 显式处理数据流(参阅“[批处理输出的哲学](ch10.md#批处理输出的哲学)”)可以使数据的**来龙去脉(provenance)**更加清晰,从而使完整性检查更具可行性。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理器与流处理器,以检查是否获得相同的结果,或者,甚至并行运行冗余的衍生流程。 +​ 显式处理数据流(参阅“[批处理输出的哲学](ch10.md#批处理输出的哲学)”)可以使数据的**来龙去脉(provenance)** 更加清晰,从而使完整性检查更具可行性。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理器与流处理器,以检查是否获得相同的结果,或者,甚至并行运行冗余的衍生流程。 ​ 具有确定性且定义良好的数据流,也使调试与跟踪系统的执行变得容易,以便确定它**为什么**做了某些事情【4,69】。如果出现意想之外的事情,那么重现导致意外事件的确切事故现场的诊断能力—— 一种时间旅行调试功能是非常有价值的。 @@ -722,9 +722,9 @@ COMMIT; ​ 我没有资格评论这些技术用于货币,或者合同商定机制的价值。但从数据系统的角度来看,它们包含了一些有趣的想法。实质上,它们是分布式数据库,具有数据模型与事务机制,而不同副本可以由互不信任的组织托管。副本不断检查其他副本的完整性,并使用共识协议对应当执行的事务达成一致。 -​ 我对这些技术的拜占庭容错方面有些怀疑(参阅“[拜占庭故障](ch8.md#拜占庭故障)”),而且我发现**工作证明(proof of work)**技术非常浪费(比如,比特币挖矿)。比特币的交易吞吐量相当低,尽管是出于政治与经济原因而非技术上的原因。不过,完整性检查的方面是很有趣的。 +​ 我对这些技术的拜占庭容错方面有些怀疑(参阅“[拜占庭故障](ch8.md#拜占庭故障)”),而且我发现**工作证明(proof of work)** 技术非常浪费(比如,比特币挖矿)。比特币的交易吞吐量相当低,尽管是出于政治与经济原因而非技术上的原因。不过,完整性检查的方面是很有趣的。 -​ 密码学审计与完整性检查通常依赖**默克尔树(Merkle tree)**【74】,这是一颗散列值的树,能够用于高效地证明一条记录出现在一个数据集中(以及其他一些特性)。除了炒作的沸沸扬扬的加密货币之外,**证书透明性(certificate transparency)**也是一种依赖Merkle树的安全技术,用来检查TLS/SSL证书的有效性【75,76】。 +​ 密码学审计与完整性检查通常依赖**默克尔树(Merkle tree)**【74】,这是一颗散列值的树,能够用于高效地证明一条记录出现在一个数据集中(以及其他一些特性)。除了炒作的沸沸扬扬的加密货币之外,**证书透明性(certificate transparency)** 也是一种依赖Merkle树的安全技术,用来检查TLS/SSL证书的有效性【75,76】。 ​ 我可以想象,那些在证书透明度与分布式账本中使用的完整性检查和审计算法,将会在通用数据系统中得到越来越广泛的应用。要使得这些算法对于没有密码学审计的系统同样可伸缩,并尽可能降低性能损失还需要一些工作。 但我认为这是一个值得关注的有趣领域。 @@ -780,7 +780,7 @@ COMMIT; ​ 当预测性分析影响人们的生活时,自我强化的反馈循环会导致非常有害的问题。例如,考虑雇主使用信用分来评估候选人的例子。你可能是一个信用分不错的好员工,但因不可抗力的意外而陷入财务困境。由于不能按期付账单,你的信用分会受到影响,进而导致找到工作更为困难。失业使你陷入贫困,这进一步恶化了你的分数,使你更难找到工作【87】。在数据与数学严谨性的伪装背后,隐藏的是由恶毒假设导致的恶性循环。 -​ 我们无法预测这种反馈循环何时发生。然而通过对整个系统(不仅仅是计算机化的部分,而且还有与之互动的人)进行整体思考,许多后果是可以够预测的 —— 一种称为**系统思维(systems thinkin)**的方法【92】。我们可以尝试理解数据分析系统如何响应不同的行为,结构或特性。该系统是否加强和增大了人们之间现有的差异(例如,损不足以奉有余,富者愈富,贫者愈贫),还是试图与不公作斗争?而且即使有着最好的动机,我们也必须当心意想不到的后果。 +​ 我们无法预测这种反馈循环何时发生。然而通过对整个系统(不仅仅是计算机化的部分,而且还有与之互动的人)进行整体思考,许多后果是可以够预测的 —— 一种称为**系统思维(systems thinkin)** 的方法【92】。我们可以尝试理解数据分析系统如何响应不同的行为,结构或特性。该系统是否加强和增大了人们之间现有的差异(例如,损不足以奉有余,富者愈富,贫者愈贫),还是试图与不公作斗争?而且即使有着最好的动机,我们也必须当心意想不到的后果。 ### 隐私和追踪 @@ -796,7 +796,7 @@ COMMIT; #### 监视 -​ 让我们做一个思想实验,尝试用**监视(surveillance)**一词替换**数据(data)**,再看看常见的短语是不是听起来还那么漂亮【93】。比如:“在我们的监视驱动的组织中,我们收集实时监视流并将它们存储在我们的监视仓库中。我们的监视科学家使用高级分析和监视处理来获得新的见解。“ +​ 让我们做一个思想实验,尝试用**监视(surveillance)** 一词替换**数据(data)**,再看看常见的短语是不是听起来还那么漂亮【93】。比如:“在我们的监视驱动的组织中,我们收集实时监视流并将它们存储在我们的监视仓库中。我们的监视科学家使用高级分析和监视处理来获得新的见解。“ ​ 对于本书《设计监控密集型应用》而言,这个思想实验是罕见的争议性内容,但我认为需要激烈的言辞来强调这一点。在我们尝试制造软件“吞噬世界”的过程中【94】,我们已经建立了世界上迄今为止所见过的最伟大的大规模监视基础设施。我们正朝着万物互联迈进,我们正在迅速走近这样一个世界:每个有人居住的空间至少包含一个带互联网连接的麦克风,以智能手机,智能电视,语音控制助理设备,婴儿监视器甚至儿童玩具的形式存在,并使用基于云的语音识别。这些设备中的很多都有着可怕的安全记录【95】。 @@ -814,13 +814,13 @@ COMMIT; ​ 而且从用户身上挖掘数据是一个单向过程,而不是真正的互惠关系,也不是公平的价值交换。用户对能用多少数据换来什么样的服务,既没有没有发言权也没有选择权:服务与用户之间的关系是非常不对称与单边的。这些条款是由服务提出的,而不是由用户提出的【99】。 -​ 对于不同意监视的用户,唯一真正管用的备选项,就是简单地不使用服务。但这个选择也不是真正自由的:如果一项服务如此受欢迎,以至于“被大多数人认为是基本社会参与的必要条件”【99】,那么指望人们选择退出这项服务是不合理的 —— 使用它**事实上(de facto)**是强制性的。例如,在大多数西方社会群体中,携带智能手机,使用Facebook进行社交,以及使用Google查找信息已成为常态。特别是当一项服务具有网络效应时,人们选择**不**使用会产生社会成本。 +​ 对于不同意监视的用户,唯一真正管用的备选项,就是简单地不使用服务。但这个选择也不是真正自由的:如果一项服务如此受欢迎,以至于“被大多数人认为是基本社会参与的必要条件”【99】,那么指望人们选择退出这项服务是不合理的 —— 使用它**事实上(de facto)** 是强制性的。例如,在大多数西方社会群体中,携带智能手机,使用Facebook进行社交,以及使用Google查找信息已成为常态。特别是当一项服务具有网络效应时,人们选择**不**使用会产生社会成本。 ​ 因为跟踪用户而拒绝使用服务,这只是少数人才拥有的权力,他们有足够的时间与知识来了解隐私政策,并承受的起代价:错过社会参与,以及使用服务可能带来的专业机会。对于那些处境不太好的人而言,并没有真正意义上的选择:监控是不可避免的。 #### 隐私与数据使用 -​ 有时候,人们声称“隐私已死”,理由是有些用户愿意把各种关于他们生活的事情发布到社交媒体上,有时是平凡俗套,但有时是高度私密的。但这种说法是错误的,而且是对**隐私(privacy)**一词的误解。 +​ 有时候,人们声称“隐私已死”,理由是有些用户愿意把各种关于他们生活的事情发布到社交媒体上,有时是平凡俗套,但有时是高度私密的。但这种说法是错误的,而且是对**隐私(privacy)** 一词的误解。 ​ 拥有隐私并不意味着保密一切东西;它意味着拥有选择向谁展示哪些东西的自由,要公开什么,以及要保密什么。**隐私权是一项决定权**:在从保密到透明的光谱上,隐私使得每个人都能决定自己想要在什么地方位于光谱上的哪个位置【99】。这是一个人自由与自主的重要方面。 @@ -850,7 +850,7 @@ COMMIT; ​ 俗话说,“知识就是力量”。更进一步,“在避免自己被审视的同时审视他人,是权力最重要的形式之一”【105】。这就是极权政府想要监控的原因:这让它们有能力控制全体居民。尽管今天的科技公司并没有公开地寻求政治权力,但是它们积累的数据与知识却给它们带来了很多权力,其中大部分是在公共监督之外偷偷进行的【106】。 -#### 记着工业革命 +#### 回顾工业革命 ​ 数据是信息时代的决定性特征。互联网,数据存储,处理和软件驱动的自动化正在对全球经济和人类社会产生重大影响。我们的日常生活与社会组织在过去十年中发生了变化,而且在未来的十年中可能会继续发生根本性的变化,所以我们会想到与工业革命对比【87,96】。 @@ -882,13 +882,13 @@ COMMIT; ## 本章小结 -​ 在本章中,我们讨论了设计数据系统的新方式,而且也包括了我的个人观点,以及对未来的猜测。我们从这样一种观察开始:没有单种工具能高效服务所有可能的用例,因此应用必须组合使用几种不同的软件才能实现其目标。我们讨论了如何使用批处理与事件流来解决这一**数据集成(data integration)**问题,以便让数据变更在不同系统之间流动。 +​ 在本章中,我们讨论了设计数据系统的新方式,而且也包括了我的个人观点,以及对未来的猜测。我们从这样一种观察开始:没有单种工具能高效服务所有可能的用例,因此应用必须组合使用几种不同的软件才能实现其目标。我们讨论了如何使用批处理与事件流来解决这一**数据集成(data integration)** 问题,以便让数据变更在不同系统之间流动。 ​ 在这种方法中,某些系统被指定为记录系统,而其他数据则通过转换衍生自记录系统。通过这种方式,我们可以维护索引,物化视图,机器学习模型,统计摘要等等。通过使这些衍生和转换操作异步且松散耦合,能够防止一个区域中的问题扩散到系统中不相关部分,从而增加整个系统的稳健性与容错性。 ​ 将数据流表示为从一个数据集到另一个数据集的转换也有助于演化应用程序:如果你想变更其中一个处理步骤,例如变更索引或缓存的结构,则可以在整个输入数据集上重新运行新的转换代码,以便重新衍生输出。同样,出现问题时,你也可以修复代码并重新处理数据以便恢复。 -​ 这些过程与数据库内部已经完成的过程非常类似,因此我们将数据流应用的概念重新改写为,**分拆(unbundling)**数据库组件,并通过组合这些松散耦合的组件来构建应用程序。 +​ 这些过程与数据库内部已经完成的过程非常类似,因此我们将数据流应用的概念重新改写为,**分拆(unbundling)** 数据库组件,并通过组合这些松散耦合的组件来构建应用程序。 ​ 衍生状态可以通过观察底层数据的变更来更新。此外,衍生状态本身可以进一步被下游消费者观察。我们甚至可以将这种数据流一路传送至显示数据的终端用户设备,从而构建可动态更新以反映数据变更,并在离线时能继续工作的用户界面。