ch12 机翻完成

This commit is contained in:
Vonng 2018-02-12 18:48:33 +08:00
parent 6456a9c2a2
commit 9509cadf8a
2 changed files with 847 additions and 6 deletions

View File

@ -60,7 +60,7 @@
| 章节 | 文件 | 计划 | 进度 |
| ------ | ------ | ---- | ---- |
| 序 | [preface.md](preface.md) | | 机翻 |
| 序 | [preface.md](preface.md) | | 机翻 |
| 第一部分:数据系统基础 ——概览 | [part-i.md](part-i.md) | | 精翻 20% |
| 第一章:可靠性、可扩展性、可维护性 | [ch1.md](ch1.md) | | 初翻 |
| 第二章:数据模型与查询语言 | [ch2.md](ch2.md) | | 初翻 |
@ -74,8 +74,9 @@
| 第九章:一致性与共识 | [ch9.md](ch9.md) | | 机翻 |
| 第三部分:前言 | [part-iii.md](part-iii.md) | | 机翻 |
| 第十章:批处理 | [ch10.md](ch10.md) | | 机翻 |
| 第十一章:流处理 | [ch11.md](ch11.md) | | - |
| 第十二章:数据系统的未来 | [ch12.md](ch12.md) | | - |
| 第十一章:流处理 | [ch11.md](ch11.md) | | 机翻 |
| 第十二章:数据系统的未来 | [ch12.md](ch12.md) | | 机翻 |
| 后记 | [colophon.md](colophon.md) | | 机翻 |

View File

@ -10,46 +10,877 @@
[TOC]
到目前为止,本书主要描述的是目前的情况。在这最后一章中,我们将转向未来,讨论应该如何做:我将提出一些想法和方法,我相信这些方法可以从根本上改进我们设计和构建应用程序的方式。
对未来的看法和猜测当然是主观的,所以我在写这篇个人意见的时候会用到第一人称。我们欢迎您不同意并形成自己的观点,但我希望本章的观点至少可以成为一个富有成效的讨论的出发点,并为经常混淆的概念提供一些清晰。
第1章概述了本书的目标探索如何创建可靠可伸缩和可维护的应用程序和系统。这些主题贯穿了所有的章节例如我们讨论了许多有助于提高可靠性的容错算法提高可扩展性的分区以及提高可维护性的进化和抽象机制。在本章中我们将把所有这些想法结合在一起并以这些想法为基础来设想未来。我们的目标是发现如何设计比今天更好的应用程序 - 强大,正确,可演化,并最终对人类有益。
## 数据集成
本书中反复出现的主题是对于任何给定的问题都有几种解决方案所有这些解决方案都有不同的优点缺点和折衷。例如在第3章讨论存储引擎时我们看到了日志结构存储B树和列式存储。在第5章讨论复制时我们看到了单领导多领导和无领导的方法。
如果你有一个问题,例如“我想存储一些数据并稍后再查询”,那么没有一个正确的解决方案,但是在不同的情况下,每种方法都是适当的。软件实现通常必须选择一种特定的方法。要使一个代码路径健壮并且很好地尝试在一个软件中执行所有操作几乎可以保证实现效果很差,这很难。
因此,软件工具的最合适的选择也取决于情况。每一个软件,甚至是所谓的“通用”数据库,都是针对特定的使用模式而设计的。
面对如此众多的替代品,第一个挑战就是弄清楚软件产品与其所处的环境之间的映射关系。供应商可以理解地不愿意告诉你他们的软件不适合的工作负载类型,但是希望以前的章节能够给你提供一些问题,以便在各行之间阅读并更好地理解这些权衡。
但是,即使您完全理解工具与其使用环境之间的映射,还有一个挑战:在复杂的应用程序中,数据通常以多种不同的方式使用。不太可能存在适用于所有不同数据使用环境的软件,因此您不可避免地需要拼凑几个不同的软件以提供您的应用程序的功能。
### 组合使用派生数据的工具
例如为了处理任意关键字的查询需要将OLTP数据库与全文搜索索引集成在一起是很常见的。尽管一些数据库如PostgreSQL包含了全文索引功能可以满足简单的应用需求[1],但更复杂的搜索工具需要专业的信息检索工具。相反,搜索索引通常不适合作为一个持久的记录系统,因此许多应用程序需要结合两种不同的工具来满足所有要求。
我们谈到了将数据系统集成到“使系统保持同步”第452页的问题。随着数据的不同表示数量的增加集成问题变得更加困难。除了数据库和搜索索引之外也许您需要保留分析系统数据仓库或批处理和流处理系统中的数据副本。维护从原始数据派生的对象的高速缓存或非规范化版本;通过机器学习,分类,排名或推荐系统传递数据;或根据对数据的更改发送通知。
令人惊讶的是我经常看到软件工程师做出如下陈述“根据我的经验99的人只需要X”或“......不需要X”对于X的各种值。我认为这样的陈述更多地讲述了讲话者的经验而不是技术的实际有用性。您可能想要对数据执行的各种操作范围非常广泛。一个人认为是一个模糊和毫无意义的功能可能是别人的核心要求。如果缩小数据流并考虑整个组织的数据流那么对数据集成的需求往往就会变得明显。
#### 推理数据流
当需要在多个存储系统中维护相同数据的副本以满足不同的访问模式时,您需要非常清楚输入和输出:哪些数据先写入,哪些表示来自哪些源?如何以正确的格式将数据导入所有正确的地方?
例如您可能会首先将数据写入记录数据库系统捕获对该数据库所做的更改请参阅第454页上的“更改数据捕获”然后将更改应用于数据库中的搜索索引相同的顺序。如果更改数据捕获CDC是更新索引的唯一方式则可以确定该索引完全来自记录系统因此与其保持一致禁止软件中的错误。写入数据库是向该系统提供新输入的唯一方式。
允许应用程序直接写入搜索索引和数据库引入了图11-4所示的问题其中两个客户端同时发送冲突写入并且两个存储系统按不同顺序处理它们。在这种情况下既不是数据库也不是
基于事件日志更新派生数据系统通常可以做出决定性的和幂等的参见第478页的“幂等性”使得从故障中恢复相当容易。
#### 派生数据与分布式事务
保持不同数据系统彼此一致的经典方法涉及分布式事务如第354页的“原子提交和两阶段提交2PC”中所述。与分布式事务相比使用派生数据系统的方法如何
在抽象层面他们通过不同的方式达到类似的目标。分布式事务通过使用锁定进行互斥来决定写入的顺序请参阅第257页上的“两阶段锁定2PL而CDC和事件源使用日志进行排序。分布式事务使用原子提交来确保更改只生效一次而基于日志的系统通常基于确定性重试和幂等性。
最大的不同之处在于事务系统通常提供线性请参阅第324页的“线性化”这意味着有用的保证例如读取自己的写入请参阅第162页的“读取自己的写入”。另一方面派生数据系统通常是异步更新的因此它们不会默认提供相同的时间保证。
在愿意支付分布式交易成本的有限环境中它们已被成功使用。但是我认为XA的容错能力和性能特征较差请参阅第364页的“实践中的分布式事务”这严重限制了它的实用性。我相信有可能为分布式事务创建一个更好的协议但是将这样一个协议广泛采用并与现有工具集成将是具有挑战性的并且不太可能很快发生。
在没有广泛支持良好分布式事务协议的情况下,我认为基于日志的派生数据是集成不同数据系统的最有前途的方法。然而,诸如阅读自己写作的保证是有用的,我认为告诉每个人“最终的一致性是不可避免的 - 吸收它并学会处理它”是没有成果的(至少不是没有良好的指导如何处理它)。
在第51页的“瞄准正确性”中我们将讨论一些在异步派生系统之上实现更强保障的方法并在分布式事务和异步基于对数系统之间建立中间立场。
#### 全局有序的限制
对于足够小的系统,构建一个完全有序的事件日志是完全可行的(正如单引导程序复制数据库的流行所证明的那样,这正好构建了这样一个日志)。但是,随着系统向更大更复杂的工作负载扩展,限制开始出现:
* 在大多数情况下构建完全有序的日志需要所有事件通过决定订购的单个领导节点。如果事件吞吐量大于单台计算机可处理的事件则需要将其分割到多台计算机上请参见第446页的“分区日志”。然后两个不同分区中的事件顺序不明确。
* 如果服务器分布在多个地理位置分散的数据中心上例如为了容忍整个数据中心脱机您通常在每个数据中心都有单独的领导者因为网络延迟会导致同步的跨数据中心协调效率低下请参阅“Multi -Leader复制“。这意味着源自两个不同数据中心的事件的未定义排序。
* 将应用程序部署为微服务时请参阅第125页上的“通过服务进行数据流REST和RPC”常见的设计选择是将每个服务及其持久状态作为独立单元进行部署服务之间不共享持久状态。当两个事件来自不同的服务时这些事件没有定义的顺序。
* 某些应用程序保持客户端状态该状态在用户输入时立即更新无需等待服务器确认甚至可以继续脱机工作请参阅第170页的“脱机操作的客户端”。有了这样的应用程序客户端和服务器很可能以不同的顺序看到事件。
在形式上决定事件的总次序称为总次序广播相当于共识请参阅第366页上的“共识算法和总次序广播”。大多数共识算法都是针对单个节点的吞吐量足以处理整个事件流的情况而设计的并且这些算法不提供多个节点共享事件排序工作的机制。设计共识算法仍然是一个开放的研究问题它可以扩展到单个节点的吞吐量之外并且在地理上分散的环境中工作良好。
#### 订购事件以捕捉因果关系
在事件之间不存在因果关系的情况下缺乏全部命令并不是一个大问题因为并发事件可以任意排序。其他一些情况很容易处理例如当同一对象有多个更新时它们可以通过将特定对象ID的所有更新路由到相同的日志分区来完全排序。然而因果关系有时会以更微妙的方式出现另请参阅“订购和因果关系”第319页
例如,考虑一个社交网络服务,以及两个相互关系但刚分手的用户。其中一个用户将另一个作为朋友移除,然后向其余的朋友发送消息,抱怨他们的前伴侣。用户的意图是他们的前配偶不应该看到粗鲁的信息,因为信息是在朋友状态被撤销后发送的。
但是,在一个地方存储友谊状态并在另一个地方存储消息的系统中,不友好事件和消息发送事件之间的顺序依赖关系可能会丢失。如果未捕获到因果依赖关系,则发送有关新消息的通知的服务可能会在不友好事件之前处理消息发送事件,从而错误地向前伙伴发送通知。
在本例中通知实际上是消息和好友列表之间的连接使得它与我们先前讨论的连接的时间问题有关请参阅第475页的“连接的时间依赖性”。不幸的是这个问题似乎并没有一个简单的答案[2,3]。起点包括:
* 逻辑时间戳可以提供没有协调的全部订购请参见“序列号排序”第页343因此它们可能有助于总订单广播不可行的情况。但是他们仍然要求收件人处理不按顺序发送的事件并且需要传递其他元数据。
* 如果您可以记录一个事件来记录用户在做出决定之前所看到的系统状态,并给该事件一个唯一的标识符,那么后面的任何事件都可以引用该事件标识符来记录因果关系[4] 。我们将在第513页的“Reads are events too”中回到这个想法。
* 冲突解决算法请参阅“自动冲突解决”第165页有助于处理以意外顺序传递的事件。它们对于维护状态很有用但如果行为有外部副作用例如也许随着时间的推移应用程序开发模式将出现使得能够有效地捕获因果依赖关系并且保持正确的派生状态而不会迫使所有事件经历全部命令广播的瓶颈。
### 批量处理与流处理
我会说数据集成的目标是确保数据在所有正确的地方以正确的形式结束。这样做需要消耗投入,转化,加入,过滤,汇总,培训模型,评估并最终写入适当的输出。批处理和流处理器是实现这一目标的工具。
批处理和流处理的输出是派生的数据集例如搜索索引实例化视图向用户显示的建议聚合度量等请参阅“批处理工作流的输出”第417页和“流处理的用法”第465页
正如我们在第10章和第11章中看到的批处理和流处理有许多共同的原则主要的根本区别在于流处理器在无界数据集上运行而批处理输入是已知的有限大小。处理引擎的实现方式也有很多细节上的差异但是这些区别开始模糊。
Spark在批处理引擎上执行流处理将流分解为微格式而Apache Flink则在流处理引擎上执行批处理[5]。原则上,一种类型的处理可以在另一种类型上仿真,但是性能特征会有所不同:例如,在跳跃或滑动窗口时,微博可能表现不佳[6]。
#### 保持派生状态
批处理具有非常强大的功能特性即使代码不是用函数式编程语言编写的它鼓励确定性的纯函数其输出仅依赖于输入除了显式输出外没有副作用处理输入作为不可变的并作为附加的输出。流处理类似但它扩展了运算符以允许受管理的容错状态请参阅第478页的“重建失败后的状态”
具有良好定义的输入和输出的确定性函数的原理不仅有利于容错请参见第478页的“幂等性”但也简化了有关组织中数据流的推理[7]。无论派生数据是搜索索引,统计模型还是缓存,从数据管道角度来看,从另一个派生出一件事情,通过功能应用程序代码推送一个系统中的状态更改和应用对衍生系统的影响。
原则上派生数据系统可以同步维护就像关系数据库在与被索引表写入操作相同的事务中同步更新辅助索引一样。然而异步是基于事件日志的系统稳健的原因它允许系统的一部分故障被本地包含而如果任何一个参与者失败分布式事务将中止因此他们倾向于通过将故障扩展到系统的其余部分请参阅第363页的“分布式事务的限制”
我们在第206页的“分区和二级索引”中看到二级索引经常跨越分区边界。具有二级索引的分区系统需要将写入发送到多个分区如果索引是分词或将读取发送到所有分区如果索引是文档分区的话。如果索引是异步维护的这种交叉分区通信也是最可靠和可扩展的[8]另请参阅“多分区数据处理”第479页
#### 为应用程序演变重新处理数据
在维护派生数据时,批处理和流处理都是有用的。流处理允许将输入中的变化以低延迟反映在派生视图中,而批处理允许重新处理大量累积的历史数据以便将新视图导出到现有数据集上。
特别是重新处理现有数据为维护系统提供了一个良好的机制并将其发展为支持新功能和变更需求参见第4章。如果不进行重新处理模式演化就会局限于简单的变化例如向记录中添加新的可选字段或添加新类型的记录。无论是在写模式还是在读模式中都是如此请参阅第39页的“文档模型中的模式灵活性”。另一方面通过重新处理可以将数据集重组为一个完全不同的模型以便更好地满足新的要求。
> ### 在铁路上的模式迁移
>
> 大规模的“模式迁移”也发生在非计算机系统中。例如在19世纪英国铁路建设初期轨距两轨之间的距离就有了各种各样的交易标准。为一个测量仪而建的列车不能在另一个测量仪的轨道上运行这限制了火车网络中可能的相互连接[9]。
>
> 在1846年最终确定了一个标准仪表之后其他仪表的轨道必须转换 - 但是如何在不关闭火车线路的情况下进行数月甚至数年?解决的办法是首先将轨道转换为双轨或混合轨距,方法是增加第三轨。这种转换可以逐渐完成,当完成时,两个仪表的列车可以在三条轨道中的两条轨道上运行。事实上,一旦所有的列车都转换成标准轨距,那么可以移除提供非标准轨距的轨道。
>
> 以这种方式“再加工”现有的轨道让新旧版本并存可以在几年的时间内逐渐改变轨距。然而这是一项昂贵的事业这就是今天非标准仪表仍然存在的原因。例如旧金山湾区的BART系统使用与美国大部分地区不同的仪表。
派生视图允许逐步演变。如果您想重新构建数据集,则不需要执行迁移作为突然切换。相反,您可以将旧架构和新架构并排维护为相同基础数据上的两个独立派生视图。然后,您可以开始将少量用户转移到新视图,以测试其性能并发现任何错误,而大多数用户仍然会被路由到旧视图。逐渐地,您可以增加访问新视图的用户比例,最终您可以删除旧视图[10]。
这种逐渐迁移的美妙之处在于,如果出现问题,每个阶段的过程都很容易逆转:您始终有一个可以回到的工作系统。通过降低不可逆损害的风险,您可以更有信心继续前进,从而更快地改善您的系统[11]。
#### Lambda架构
如果批处理用于重新处理历史数据,并且流处理用于处理最近的更新,那么您如何将这两者结合起来?拉姆达体系结构[12]是这方面的一个建议,引起了很多关注。
lambda体系结构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据这类似于事件源请参阅第457页上的“事件源”。从这些事件中推导出读取优化的视图。 lambda体系结构建议并行运行两个不同的系统批处理系统如Hadoop MapReduce和独立的流处理系统如Storm
在lambda方法中流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成派生视图的更正版本。这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的(请参阅“故障容错”)。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。
拉姆达体系结构是一个有影响力的想法,它将数据系统的设计变得更好,尤其是通过推广将视图派生到不可变事件流和在需要时重新处理事件的原则。但是,我也认为它有一些实际问题:
* 必须保持相同的逻辑才能在批处理和流处理框架中运行这是额外的工作。虽然像Summingbird [13]这样的库提供了一个抽象的计算,可以在一个批处理或流的上下文中运行,调试,调整和维护两个不同系统的操作复杂性仍然[14]。
* 由于流管道和批处理管道产生单独的输出,因此需要合并它们以响应用户请求。如果计算是通过滚动窗口的简单聚合,则合并相当容易,但如果使用更复杂的操作(例如连接和会话化)导出视图,或者输出不是时间序列,则显得非常困难。
* 尽管有能力重新处理整个历史数据集是很好的但在大型数据集上这样做经常会很昂贵。因此批处理流水线通常需要设置为处理增量批处理例如在每小时结束时处理一小时的数据而不是重新处理所有内容。这引发了第468页的“关于时间的推理”中讨论的问题例如处理分段器和处理跨批次边界的窗口。增加批量计算会增加复杂性使其更类似于流式传输层这与保持批处理层尽可能简单的目标背道而驰。
#### 统一批处理和流处理
最近的工作使得lambda体系结构的优点在没有其缺点的情况下得以实现允许批处理计算重新处理历史数据和流计算处理事件到达时在同一个系统中实现[15]。
在一个系统中统一批处理和流处理需要以下功能,这些功能越来越广泛:
* 通过处理最近事件流的相同处理引擎来重放历史事件的能力。例如基于日志的消息代理可以重放消息请参阅第451页的“重放旧消息”某些流处理器可以从HDFS等分布式文件系统读取输入。
* 对于流处理器来说,只有一次语义 - 即确保输出与未发生故障的输出相同即使事实上发生故障请参阅“故障容错”第476页。与批处理一样这需要丢弃任何失败任务的部分输出。
* 按事件时间进行窗口化的工具而不是按处理时间进行窗口化因为处理历史事件时的处理时间毫无意义请参阅第468页的“关于时间的推理”。例如Apache Beam提供了用于表达这种计算的API然后可以使用Apache Flink或Google Cloud Dataflow运行。
## 拆分数据库
在最抽象的层面上数据库Hadoop和操作系统都执行相同的功能它们存储一些数据并允许您处理和查询数据[16]。数据库将数据存储在某些数据模型(表中的文档,文档中的顶点,图形中的顶点等)的记录中,而操作系统的文件系统则将数据存储在文件中 - 但在其核心上,都是“信息管理”系统[ 17。正如我们在第10章中看到的Hadoop生态系统有点像Unix的分布式版本。
当然有很多实际的差异。例如许多文件系统不能很好地处理包含1000万个小文件的目录而包含1000万个小记录的数据库是完全正常且不起眼的。无论如何操作系统和数据库之间的相似之处和差异值得探讨。
Unix和关系数据库以非常不同的哲学来处理信息管理问题。 Unix认为它的目的是为程序员提供一种逻辑但相当低层次的硬件抽象而关系数据库则希望为应用程序员提供一种高层次的抽象以隐藏磁盘上数据结构的复杂性并发性崩溃恢复以及等等。 Unix开发的管道和文件只是字节序列而数据库则开发了SQL和事务。
哪种方法更好?当然,这取决于你想要的。 Unix是“简单的”因为它是硬件资源相当薄的包装;关系数据库是“更简单”的,因为一个简短的声明性查询可以利用很多强大的基础设施(查询优化,索引,连接方法,并发控制,复制等),而不需要查询作者理解实施细节。
这些哲学之间的矛盾已经持续了几十年Unix和关系模型都出现在70年代初仍然没有解决。例如我将NoSQL运动解释为希望将低级别抽象方法应用于分布式OLTP数据存储领域。
在这一部分我将试图调和这两个哲学,希望我们可以结合两全其美。
### 组合使用数据存储技术
在本书的过程中,我们讨论了数据库提供的各种功能及其工作原理,其中包括:
* 二级索引使您可以根据字段的值有效地搜索记录请参阅第79页上的“其他索引结构”
* 物化视图这是一种预先计算的查询结果缓存请参阅“聚合数据立方体和物化视图”第101页
* 复制日志保持其他节点上数据的副本最新请参阅第158页中的“复制日志的实现”
* 全文搜索索引允许在文本中进行关键字搜索请参见第88页上的“全文搜索和模糊索引”以及内置于某些关系数据库[1]
在第十章和第十一章中出现了类似的主题。我们讨论了如何构建全文搜索索引请参阅第357页上的“批处理工作流的输出”了解有关实例化视图维护请参阅“维护实例化视图”一节第437页以及有关将更改从数据库复制到派生数据系统请参阅第454页的“更改数据捕获”
数据库中内置的功能与人们用批处理和流处理器构建的派生数据系统似乎有相似之处。
#### 创建一个索引
想想当你运行CREATE INDEX在关系数据库中创建一个新的索引时会发生什么。数据库必须扫描表的一致性快照挑选出所有被索引的字段值对它们进行排序然后写出索引。然后它必须处理自一致快照以来所做的写入操作假设表在创建索引时未被锁定所以写操作可能会继续。一旦完成只要事务写入表中数据库就必须继续保持索引最新。
此过程非常类似于设置新的追随者副本请参阅第155页的“设置新的追随者”也非常类似于流系统中的引导更改数据捕获请参阅第455页的“初始快照”
无论何时运行CREATE INDEX数据库都会重新处理现有数据集如第494页的“重新处理应用程序数据的演变数据”中所述并将该索引作为新视图导出到现有数据上。现有数据可能是状态的快照而不是所有发生变化的日志但两者密切相关请参阅“状态数据流和不变性”第459页
#### 一切的元数据库
有鉴于此,我认为整个组织的数据流开始像一个巨大的数据库[7]。每当批处理流或ETL过程将数据从一个地方传输到另一个地方并形成表单时就像数据库子系统一样使索引或物化视图保持最新。
像这样看批处理和流处理器就像触发器存储过程和物化视图维护例程的精细实现。他们维护的派生数据系统就像不同的索引类型。例如关系数据库可能支持B树索引散列索引空间索引请参阅第79页的“多列索引”以及其他类型的索引。在新兴的派生数据系统体系结构中不是将这些设施作为单个集成数据库产品的功能实现而是由各种不同的软件提供运行在不同的机器上由不同的团队管理。
这些发展在未来将会把我们带到哪里?如果我们从没有适合所有访问模式的单一数据模型或存储格式的前提出发,我推测有两种途径可以将不同的存储和处理工具组合成一个有凝聚力的系统:
**联合数据库:统一读取**
可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 - 一种称为联邦数据库或多存储的方法[18,19]。例如PostgreSQL的外部数据包装功能符合这种模式[20]。需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。
联合查询接口遵循单一集成系统的关系传统,具有高级查询语言和优雅的语义,但却是一个复杂的实现。
**非捆绑数据库:统一写入**
虽然联合会解决了跨多个不同系统的只读查询问题,但它并没有很好的解决跨系统同步写入的问题。我们说过,在单个数据库中,创建一致的索引是一项内置功能。当我们构建多个存储系统时,我们同样需要确保所有数据更改都会在所有正确的位置结束,即使在出现故障时也是如此。将存储系统可靠地插接在一起(例如,通过更改数据捕获和事件日志)更容易,就像将数据库的索引维护功能以可以跨不同技术同步写入的方式分开[7,21]。
unbundled方法遵循Unix传统的小型工具它可以很好地完成一件事[22]通过统一的低级API管道进行通信并且可以使用更高级别的语言shell[16] 。
#### 开展分拆工作
联邦和非捆绑是同一个硬币的两个方面:用不同的组件构成可靠,可扩展和可维护的系统。联合只读联邦和非捆绑是同一个硬币的两个方面:用不同的组件构成可靠,可扩展和可维护的系统。联合只读查询需要将一个数据模型映射到另一个数据模型,这需要一些思考,但最终还是一个可管理的问题。我认为保持写入到几个存储系统是同步的更困难的工程问题,所以我将重点关注它。
传统的同步写入方法需要跨异构存储系统的分布式事务[18]我认为这是错误的解决方案请参阅“导出的数据与分布式事务”第495页。单个存储或流处理系统内的事务是可行的但是当数据跨越不同技术之间的边界时我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法。
例如分布式事务在某些流处理器中使用以精确匹配一次语义请参阅第477页的“重新访问原子提交”这可以很好地工作。然而当事务需要涉及由不同人群编写的系统时例如当数据从流处理器写入分布式键值存储或搜索索引时缺乏标准化的事务协议会使集成更难。有幂等消费者的事件的有序日志参见第478页的“幂等性”是一种更简单的抽象因此在异构系统中实现更加可行[7]。
基于日志的集成的一大优势是各个组件之间的松散耦合,这体现在两个方面:
1. 在系统级别异步事件流使整个系统对各个组件的中断或性能下降更加稳健。如果使用者运行缓慢或失败那么事件日志可以缓冲消息请参阅“磁盘空间使用情况”第369页以便生产者和任何其他使用者可以继续不受影响地运行。有问题的消费者可以在固定时赶上因此不会错过任何数据并且包含故障。相比之下分布式事务的同步交互往往会将本地故障升级为大规模故障请参见第363页的“分布式事务的限制”
2. 在人力方面,分拆数据系统允许不同的团队独立开发,改进和维护不同的软件组件和服务。专业化使得每个团队都可以专注于做好一件事,并与其他团队的系统进行明确的界面。事件日志提供了一个足够强大的接口,以捕获相当强的一致性属性(由于持久性和事件的顺序),但也足够普遍适用于几乎任何类型的数据。
#### 非捆绑与集成系统
如果分拆确实成为未来的方式,它将不会取代目前形式的数据库 - 它们仍然会像以往一样需要。数据库仍然需要维护流处理器中的状态并且为批处理和流处理器的输出提供查询服务请参阅第419页上的“批处理工作流的输出”和第464页上的“处理流”。专门的查询引擎将继续对特定的工作负载非常重要例如MPP数据仓库中的查询引擎针对探索性分析查询进行了优化并且能够很好地处理这种类型的工作负载请参阅第417页的“将Hadoop与分布式数据库进行比较” 。
运行几个不同基础架构的复杂性可能是一个问题:每一个软件都有一个学习曲线,配置问题和操作怪癖,因此值得部署尽可能少的移动部件。与由应用程序代码[23]组成的多个工具组成的系统相比,单一集成软件产品也可以在其设计的工作负载类型上实现更好,更可预测的性能。正如我在前言中所说的那样,为了扩大规模而建设你不需要的是浪费精力,并且可能会将你锁定在一个不灵活的设计中。实际上,这是一种过早优化的形式。
分拆的目标不是要针对个别数据库与特定工作负载的性能进行竞争;我们的目标是允许您结合多个不同的数据库,以便在比单个软件可能实现的更广泛的工作负载范围内实现更好的性能。这是关于广度,而不是深度 - 与我们在第414页上的“比较Hadoop与分布式数据库”中讨论的存储和处理模型的多样性一样。
因此,如果有一项技术可以满足您的所有需求,那么您最好使用该产品,而不是试图用低级组件重新实现它。只有当没有单一软件满足您的所有需求时,才会出现拆分和合成的优势。
#### 少了什么东西?
用于组成数据系统的工具正在变得越来越好但我认为缺少一个主要部分我们还没有Unix shell的非捆绑式数据库用于组成存储和处理系统的高级语言简单和陈述的方式
例如如果我们可以简单地声明mysql |我就会喜欢它elasticsearch类似于Unix管道[22]这将成为CREATE INDEX的非捆绑等价物它将MySQL数据库中的所有文档并将其索引到Elasticsearch集群中。然后它会不断捕获对数据库所做的所有更改并自动将它们应用于搜索索引而无需编写自定义应用程序代码。几乎任何类型的存储或索引系统都可以实现这种集成。
同样能够更容易地预先计算和更新缓存将是一件好事。回想一下物化视图本质上是一个预先计算的缓存所以您可以通过为复杂查询声明指定物化视图来创建缓存包括图上的递归查询请参阅第49页上的“类图数据模型”和应用逻辑。在这方面有一些有趣的早期研究如差异数据流[24,25],我希望这些想法能够在生产系统中找到自己的方法。
### 围绕数据流设计应用
通过使用应用程序代码组成专门的存储和处理系统来分离数据库的方法也被称为“数据库内外”方法[26]在2014年的一次会议演讲标题之后[27]。然而,称它为“新建筑”太宏大。我把它看作是一个设计模式,一个讨论的起点,我们只是简单地给它起一个名字,以便我们可以更好地谈论它。
这些想法不是我的;他们只是我认为我们应该学习的其他人思想的融合。尤其是数据流语言如Oz [28]和Juttle [29]功能反应式编程FRP语言如Elm [30,31]和逻辑编程语言如Bloom [ 32。 Jay Kreps [7]提出了在这种背景下解绑的术语。
即使电子表格也具有比大多数主流编程语言遥远的数据流编程功能[33]。在电子表格中,可以将公式放入一个单元格中(例如,另一列中的单元格总和),并且只要公式的任何输入发生更改,公式的结果都会自动重新计算。这正是我们在数据系统级所需要的:当数据库中的记录发生更改时,我们希望自动更新该记录的任何索引,并且自动刷新依赖于记录的任何缓存视图或聚合。您不必担心这种刷新如何发生的技术细节,但能够简单地相信它可以正常工作。
因此我认为绝大多数数据系统仍然可以从VisiCalc在1979年已经具备的功能中学习[34]。与电子表格的不同之处在于,今天的数据系统需要具有容错性,可扩展性以及持久存储数据。他们还需要能够整合不同人群编写的不同技术,并重用现有的库和服务:期望使用某种特定语言,框架或工具开发所有软件是不切实际的。
在本节中,我将详细介绍这些想法,并探讨一些围绕非捆绑数据库和数据流的想法构建应用程序的方法。
#### 应用程序代码作为派生函数
当一个数据集来自另一个数据集时,它会经历某种转换函数。例如:
* 辅助索引是一种具有直接转换函数的派生数据集对于基表中的每一行或文档它挑选被索引的列或字段中的值并按这些值排序假设B - 树或SSTable索引按键排序如第3章所述
* 通过应用各种自然语言处理功能(如语言检测,分词,词干或词汇化,拼写纠正和同义词识别)创建全文搜索索引,然后构建用于高效查找的数据结构(例如作为倒排索引)。
* 在机器学习系统中,我们可以将模型视为通过应用各种特征提取和统计分析功能从训练数据中导出。当模型应用于新的输入数据时,模型的输出是从输入和模型(因此间接地从训练数据)中导出的。
* 缓存通常包含将以用户界面UI显示的形式的数据聚合。因此填充缓存需要知道UI中引用的字段; UI中的更改可能需要更新缓存填充方式的定义以及重建缓存。
辅助索引的派生函数通常是必需的因此它作为核心特性被构建到许多数据库中您可以仅通过说CREATE INDEX来调用它。对于全文索引常见语言的基本语言特征可能内置到数据库中但更复杂的特征通常需要特定于域的调整。在机器学习中特征工程是众所周知的特定于应用程序的特征并且通常必须包含关于应用程序的用户交互和部署的详细知识[35]。
当创建派生数据集的函数不是像创建二级索引那样的标准Cookie切割函数时需要自定义代码来处理特定于应用程序的方面。而这个自定义代码是许多数据库难以抗争虽然关系数据库通常支持触发器存储过程和用户定义的函数它们可以用来在数据库中执行应用程序代码但它们在数据库设计中已经有所反应了请参阅“传输事件流”第447页
#### 应用程序代码和状态的分离
理论上,数据库可以是任意应用程序代码的部署环境,如操作系统。但是,实际上他们已经变得不适合这个目的。它们不适合现代应用程序开发的要求,例如依赖性和软件包管理,版本控制,滚动升级,可演化性,监控,指标,对网络服务的调用以及与外部系统的集成。
另一方面MesosYARNDockerKubernetes等部署和集群管理工具专为运行应用程序代码而设计。通过专注于做好一件事情他们能够做得比将数据库作为其众多功能之一执行用户定义的功能要好得多。
我认为让系统的某些部分专门用于持久数据存储以及专门运行应用程序代码的其他部分是有意义的。这两者可以在保持独立的同时互动。
现在大多数Web应用程序都是作为无状态服务部署的其中任何用户请求都可以路由到任何应用程序服务器并且服务器在发送响应后会忘记所有请求。这种部署方式很方便因为可以随意添加或删除服务器但状态必须到某个地方通常是数据库。趋势是将无状态应用程序逻辑与状态管理数据库分开不将应用程序逻辑放入数据库中也不将持久状态置于应用程序中[36]。正如职能规划界人士喜欢开玩笑说的那样“我们相信教会与国家的分离”【37】 [^i]
[^i]: 解释一个笑话很少改进它,但我不想让任何人感到被遗漏。 在这里Church是数学家Alonzo Church的参考资料他创建了lambda演算这是早期的计算形式是大多数函数式编程语言的基础。 lambda演算不具有可变状态即没有变量可以被覆盖所以可以说可变状态与Church的工作是分开的。
在这个典型的Web应用程序模型中数据库充当一种可以通过网络同步访问的可变共享变量。应用程序可以读取和更新变量并且数据库负责保持持久性提供一些并发控制和容错。
但是,在大多数编程语言中,您无法订阅可变变量中的更改 - 您只能定期读取它。与电子表格不同,如果变量的值发生变化,变量的读者不会收到通知。 (您可以在自己的代码中实现这样的通知 - 这被称为观察者模式 - 但大多数语言没有将此模式作为内置功能。)
数据库继承了这种可变数据的被动方法:如果你想知道数据库的内容是否发生了变化,通常你唯一的选择就是轮询(即定期重复你的查询)。 订阅更改只是刚刚开始出现的功能请参阅第455页的“更改流的API支持”
#### 数据流:状态变化和应用程序代码之间的相互影响
从数据流的角度思考应用意味着重新谈判应用代码和状态管理之间的关系。我们不是将数据库视为被应用程序操纵的被动变量,而是更多地考虑状态,状态更改和处理它们的代码之间的相互作用和协作。应用程序代码通过在另一个地方触发状态更改来响应状态更改。
我们在第451页的“数据库和数据流”中看到了这一思路我们讨论将数据库更改的日志作为我们可以指定的事件流处理。消息传递系统如角色请参阅第136页的“消息传递数据流”也具有响应事件的概念。早在20世纪80年代元组空间模型探索表示分布式计算的过程观察状态变化并对它们做出反应[38,39]。
如前所述,当触发器由于数据更改而触发时,或者次级索引更新以反映索引表中的更改时,数据库内部会发生类似的情况。分解数据库意味着将此想法应用于在主数据库之外创建衍生数据集:缓存,全文搜索索引,机器学习或分析系统。我们可以为此使用流处理和消息传递系统。
需要记住的重要一点是维护派生数据与传统设计消息传递系统的异步作业执行不同请参阅第448页上的“与传统消息传递相比的日志”
•在维护派生数据时状态更改的顺序通常很重要如果多个视图是从事件日志派生的则需要按照相同的顺序处理事件以便它们保持一致。如第445页上的“确认和重新传递”中所述许多消息代理在重新传送未确认消息时没有此属性。双重写入也被排除请参阅第454页上的“保持系统同步”
•容错是导出数据的关键:仅丢失单个消息会导致派生数据集永远与其数据源不同步。消息传递和派生状态更新都必须可靠。例如,许多角色系统默认在内存中维护角色状态和消息,所以如果运行角色的机器崩溃,他们就会丢失。
稳定的消息排序和容错消息处理是相当严格的要求但与分布式事务相比它们更便宜操作更稳定。现代流处理器可以提供这些排序和可靠性保证并允许应用程序代码作为流操作符运行。此应用程序代码可以执行数据库中内置的派生函数通常不提供的任意处理。就像管道链接的Unix工具一样流操作符可以组成数据流周围的大型系统。每个运算符将状态变化流作为输入并产生其他状态变化流作为输出。
#### 流处理器和服务
当前流行的应用程序开发风格涉及将功能分解为一组通过同步网络请求如REST API进行通信的服务请参阅第121页的“通过服务实现数据流REST和RPC”。这种面向服务的体系结构优于单个单一应用程序的优势主要在于通过松散耦合的组织可伸缩性不同的团队可以在不同的服务上工作从而减少团队之间的协调工作只要服务可以独立部署和更新
将流操作符合并到数据流系统中与微服务方法有很多相似的特征[40]。但是,底层的通信机制是非常不同的:单向异步消息流而不是同步请求/响应交互。
除了第136页上的“消息传递数据流”中列出的优点如更好的容错性数据流系统还可以获得更好的性能。例如假设客户正在购买以一种货币定价但以另一种货币支付的商品。为了执行货币转换您需要知道当前的汇率。这个操作可以通过两种方式实现[40,41]
1. 在微服务方法中,处理购买的代码可能会查询汇率服务或数据库以获取特定货币的当前汇率。
2. 在数据流方法中,处理采购的代码将提前订阅汇率更新流,并在当地数据库发生更改时将当前汇率记录下来。处理采购时,只需查询本地数据库即可。
第二种方法已经将同步网络请求替换为对本地数据库进行查询的另一服务(即使在同一个进程中,该请求也可能在同一台机器上)[^ii]。数据流不仅方法更快而且更稳健到另一项服务的失败。最快和最可靠的网络请求根本就没有网络请求我们现在不使用RPC而是在购买事件和汇率更新事件之间建立流联接请参阅第473页的“流表联接流增强
[^ii]: 在微服务方法中,您可以通过在处理购买的服务中本地缓存汇率来避免同步网络请求。 但是,为了使缓存保持新鲜,您需要定期轮询更新的汇率,或订阅更改流 - 这正是数据流方法中发生的情况。
加入是时间相关的如果购买事件在稍后的时间点被重新处理汇率将会改变。如果要重建原始输出则需要获取原始购买时的历史汇率。无论您是查询服务还是订阅汇率更新流您都需要处理这种时间依赖性请参阅第475页的“连接的时间依赖性”
订阅一系列更改,而不是在需要时查询当前状态,使我们更接近类似电子表格的计算模型:当某些数据发生更改时,依赖于此的所有派生数据都可以快速更新。还有很多未解决的问题,例如围绕时间依赖连接等问题,但我认为围绕数据流想法构建应用程序是一个非常有希望的方向。
### 观察派生数据状态
在抽象层面上一节讨论的数据流系统为您提供了创建派生数据集例如搜索索引物化视图和预测模型并使其保持最新的过程。让我们称这个过程为写入路径只要某些信息被写入系统它可能会经历批处理和流处理的多个阶段并且最终每个派生数据集都会更新以合并写入的数据。图12-1显示了更新搜索索引的示例。
![](img/fig12-1.png)
**图12-1 在搜索索引中,写入(文档更新)符合读取(查询)**
但为什么你首先创建派生数据集?很可能是因为你想在以后再次查询它。这是读取路径:在提供从派生数据集中读取的用户请求时,可能会对结果执行一些更多处理,然后构建对用户的响应。
总而言之,写入路径和读取路径涵盖了数据的整个旅程,从收集数据的地步到使用数据(可能是由另一个人)。写入路径是预先计算的行程的一部分 - 即,一旦数据进入,即刻完成,无论是否有人要求查看它。阅读路径是旅程中只有当有人要求时才会发生的部分。如果您熟悉函数式编程语言,则可能会注意到写入路径类似于急切的评估,读取路径类似于懒惰评估。
如图12-1所示派生数据集是写入路径和读取路径相遇的地方。它代表了在写入时需要完成的工作量与在读取时需要完成的工作量之间的权衡。
#### 物化视图和缓存
全文搜索索引就是一个很好的例子写路径更新索引读路径在索引中搜索关键字。读写都需要做一些工作。写入需要更新文档中出现的所有术语的索引条目。阅读需要搜索查询中的每个单词并应用布尔逻辑来查找包含查询中所有单词AND运算符的文档或者每个单词OR运算符的任何同义词。
如果您没有索引搜索查询将不得不扫描所有文档如grep如果您有大量文档这将会非常昂贵。没有索引意味着写入路径上的工作量较少没有要更新的索引但是在读取路径上需要更多工作。
[^iii]: 假设一个有限的语料库,那么非空洞的搜索结果的非空搜索结果是有限的。然而,它在语料库中的术语数量是指数级的,这仍然是一个坏消息。
另一方面,您可以想象为所有可能的查询预先计算搜索结果。在这种情况下,读取路径上的工作量会减少:不需要布尔逻辑,只需查找查询结果并返回即可。但是,写入路径会更加昂贵:可能要求的可能的搜索查询集合是无限的,因此预先计算所有可能的搜索结果将需要无限的时间和存储空间。那不会很好[^iii]。
另一个选择是预先计算搜索结果,只对一组固定的最常见的查询进行计算,以便它们可以快速地服务而不必去索引。不寻常的查询仍然可以从索引提供。这通常会被称为常见查询缓存,尽管我们也可以称之为物化
视图,因为当新文档出现时,需要更新这些文档,这些文档应该包含在其中一个常见查询的结果中。
从这个例子中我们可以看到索引不是写路径和读路径之间唯一可能的边界。常见搜索结果的缓存是可能的并且在少量文档上也可以使用没有索引的类似grep的扫描。如此看来缓存索引和物化视图的作用很简单它们改变了读取路径和写入路径之间的边界。通过预先计算结果它们允许我们在写入路径上做更多的工作以节省读取路径上的工作量。
在写作路径上完成的工作和读取路径之间的界限实际上是本书开始处的Twitter示例的主题在第11页的“描述负载”中。在该示例中我们还看到了与普通用户相比名人的写作路径和阅读路径可能会有所不同。在500页之后我们已经到了整个圈子
#### 有状态,可离线的客户端
我发现写作和读取路径之间的边界很有趣,因为我们可以讨论移动这个边界并探讨实际意义上的这种转变意味着什么。我们来看看不同情况下的想法。
过去二十年来Web应用程序的巨大流行使我们对应用程序开发有了一定的假设这很容易被视为理所当然。特别是客户机/服务器模型(客户机主要是无状态的,服务器拥有数据的权限)非常普遍,我们几乎忘记了其他任何东西都存在。但是,技术不断发展,我认为不时质疑现状非常重要。
传统上网络浏览器是无状态的客户端只有在您连接到互联网时才能做有用的事情只有您可以离线执行的唯一的事情是在您之前在线加载的页面上下滚动。然而最近的“单页面”JavaScript Web应用程序已经获得了很多有状态的功能包括客户端用户界面交互和Web浏览器中的持久本地存储。移动应用程序可以类似地在设备上存储大量状态并且不需要往返于大多数用户交互的服务器。
这些不断变化的功能引发了对离线优先应用程序的重新兴趣,这些应用程序尽可能地在同一设备上使用本地数据库,无需连接互联网,并且在网络连接时与后台远程服务器同步可用[42]。由于移动设备通常具有缓慢且不可靠的蜂窝互联网连接因此如果用户的用户界面不必等待同步网络请求并且应用程序大多离线工作则对用户来说是一大优势请参阅“具有离线操作的客户端”第170页
当我们摆脱无国籍客户与中央数据库交谈的假设,并转向终端用户设备上维护的状态时,开启了一个全新的机会。特别是,我们可以将设备上的状态视为服务器上的状态缓存。屏幕上的像素是客户端应用程序中模型对象的物化视图;模型对象是远程数据中心的本地状态副本[27]。
#### 将状态更改推送给客户端
在典型的网页中如果您在Web浏览器中加载页面并且随后服务器上的数据发生更改则浏览器在重新加载页面之前不会查找有关更改。浏览器只能在一个时间点读取数据假设它是静态的 - 它不会订阅来自服务器的更新。因此,设备上的状态是一个陈旧的缓存,除非您明确轮询更改,否则不会更新。 像RSS这样的基于HTTP的订阅源订阅协议实际上只是一种基本的调查形式。
更新的协议已经超越了HTTP的基本请求/响应模式服务器发送的事件EventSource API和WebSockets提供了通信渠道通过这些通信渠道Web浏览器可以与服务器保持开放的TCP连接服务器可以只要保持连接状态就会主动将消息推送到浏览器。这为服务器提供了一个机会主动通知最终用户客户端本地存储状态的任何变化从而减少客户端状态的陈旧程度。
就我们的写入路径和读取路径模型而言,主动将状态改变到客户端设备意味着将写入路径一直延伸到最终用户。当客户端首次初始化时,它仍然需要使用读取路径来获取其初始状态,但此后可能依赖于服务器发送的状态更改流。我们在流处理和消息传递方面讨论的想法并不局限于仅在数据中心运行:我们可以进一步采用这些想法,并将它们一直延伸到终端用户设备[43]。
这些设备有时会脱机并且在此期间无法收到服务器状态更改的任何通知。但是我们已经解决了这个问题在第449页的“消费者偏移量”中我们讨论了基于日志的消息中介的使用者在失败或断开连接后可以重新连接并确保它不会错过任何到达的消息它被断开。同样的技术适用于单个用户每个设备都是小事件的小用户。
#### 端到端的事件流
最近用于开发有状态客户端和用户界面的工具如Elm语言[30]和Facebook的ReactFlux和Redux工具链已经通过订阅表示用户的事件流来管理内部客户端状态输入或来自服务器的响应其结构与事件源相似请参阅第457页的“事件源”
将这种编程模型扩展为允许服务器将状态改变事件推送到客户端事件管道中是非常自然的。因此,状态变化可以通过端到端的写入路径流动:从触发状态改变的一个设备上的交互,通过事件日志以及通过多个派生的数据系统和流处理器,一直到用户界面在另一台设备上观察状态的人。这些状态变化可以以相当低的延迟传播 - 比如说,在一秒内结束。
一些应用程序(如即时消息传递和在线游戏)已经具有这种“实时”体系结构(从低延迟的交互意义上说,不是“响应时间保证”在本页中的含义)。但为什么我们不用这种方式构建所有的应用程序?
挑战在于无状态客户端和请求/响应交互的假设在我们的数据库,库,框架和协议中非常深入。许多数据存储支持读取和写入操作,请求返回一个响应,但是少得多提供订阅更改的能力 - 即随着时间的推移返回响应流的请求请参阅“更改流的API支持” 。
为了将写入路径扩展到最终用户,我们需要从根本上重新思考我们构建这些系统的方式:从请求/响应交互转向发布/订阅数据流[27]。我认为更具响应性的用户界面和更好的离线支持的优势将使其值得付出努力。如果您正在设计数据系统,我希望您会记住订阅更改的选项,而不只是查询当前状态。
#### 读也是事件
我们讨论过,当流处理器将派生数据写入存储(数据库,缓存或索引)时,以及当用户请求查询该存储时,存储将充当写入路径和读取路径之间的边界。该商店允许对数据进行随机访问读取查询,否则这些查询将需要扫描整个事件日志。
在很多情况下数据存储与流式传输系统是分开的。但请记住流处理器还需要维护状态以执行聚合和连接请参阅第472页的“流连接”。这种状态通常隐藏在流处理器内部但是一些框架允许它也被外部客户端查询[45],将流处理器本身变成一种简单的数据库。
我想进一步考虑这个想法。正如到目前为止所讨论的那样,对商店的写入是通过事件日志进行的,而读取是瞬时网络请求,直接进入存储被查询数据的节点。这是一个合理的设计,但不是唯一可能的设计。也可以将读取请求表示为事件流,并通过流处理器发送读取事件和写入事件;处理器通过将读取结果发送到输出流来响应读取事件[46]。
当写入和读取都被表示为事件并且被路由到同一个流操作符以便处理时我们实际上是在读查询流和数据库之间执行流表连接。读取事件需要发送到保存数据的数据库分区请参阅第214页的“请求路由”就像批处理和流处理器在连接时需要在同一个键上共同输入一样请参阅“Reduce-Side连接和分组“
服务请求和正在执行的连接之间的这种对应关系是非常重要的[47]。一次性读取请求只是通过连接运算符传递请求,然后立即忘记它;订阅请求是与连接另一端的过去和未来事件的持续连接。
记录读取事件的日志可能对于追踪整个系统中的因果关系和数据来源也有好处:它可以让您在做出特定决策之前重建用户看到的内容。例如,在网上商店,向客户显示的预测出货日期和库存状态可能影响他们是否选择购买物品[4]。要分析此连接,您需要记录用户查询运输和库存状态的结果。
将读取事件写入持久存储器可以更好地跟踪因果关系请参阅第493页的“订购事件以捕获因果关系”但会产生额外的存储和I / O成本。优化这些系统以减少开销仍然是一个开放的研究问题[2]。但是,如果您已经为了操作目的而记录了读取请求,作为请求处理的副作用,将日志作为请求的来源并不是什么大的改变。
#### 多分区数据处理
对于只涉及单个分区的查询,通过流发送查询和收集响应流的努力可能是过度的。然而,这个想法打开了分布式执行复杂查询的可能性,这需要合并来自多个分区的数据,利用流处理器已经提供的消息路由,分区和加入的基础设施。
Storm的分布式RPC功能支持这种使用模式请参阅第468页的“消息传递和RPC”。例如它已被用于计算在Twitter上看到过网址的人数 - 即,每个人都推送了该网址的跟随者集合[48]。由于Twitter用户组是分区的因此这种计算需要合并来自多个分区的结果
这种模式的另一个例子是欺诈预防为了评估特定购买事件是否具有欺诈风险您可以检查用户的IP地址电子邮件地址帐单地址送货地址等的信誉分数。这些信誉数据库中的每一个都是自身分区的因此为特定购买事件收集分数需要一系列具有不同分区数据集的联合[49]。
MPP数据库的内部查询执行图具有相似的特征请参阅第417页的“比较Hadoop与分布式数据库”。如果您需要执行这种多分区连接使用提供此功能的数据库可能比使用流处理器实现它更简单。但是将查询视为流提供了一个选项可以实现大规模应用程序这些应用程序可以在传统的现成解决方案的限制下运行。
## 目标是正确性
对于只读取数据的无状态服务,出现问题时不会造成什么大问题:您可以修复该错误并重新启动服务,并且一切都恢复正常。像数据库这样的有状态的系统并不是那么简单:它们被设计成永远记住事物(或多或少),所以如果出现问题,效果也可能永远持续下去,这意味着它们需要更仔细的思考[50]。
我们希望构建可靠和正确的应用程序即使面对各种故障其语义也能很好地定义和理解的程序。大约四十年来原子性隔离性和耐久性第7章的交易特性一直是构建正确应用的首选工具。但是这些基础比看起来更弱例如见证弱隔离级别的混合请参见“弱隔离级别”第233页
在某些领域事务被完全抛弃并被提供更好性能和可伸缩性的模型取代但是更复杂的语义例如请参阅第167页上的“无Leaderless复制”。一致性经常被讨论但定义不明确参见第224页的“一致性”和第9章。有些人主张我们应该“为了更好的可用性而拥抱弱一致性”而对实际上的实际意义缺乏清晰的认识。
对于如此重要的话题,我们的理解和我们的工程方法是惊人的片状。例如,确定在特定事务隔离级别或复制配置下运行特定应用程序是否安全是非常困难的[51,52]。通常简单的解决方案似乎在并发性低的情况下正常工作,并且没有错误,但是在要求更高的情况下会出现许多细微的错误。
例如凯尔金斯伯里Kyle Kingsbury的杰普森Jepsen实验[53]强调了一些产品声称的安全保证与存在网络问题和崩溃时的实际行为之间的明显差异。即使像数据库这样的基础设施产品没有问题,应用程序代码仍然需要正确使用它们提供的功能,如果配置很难理解,这是很容易出错的(这是弱隔离级别,法定配置, 等等)。
如果您的应用程序可以容忍偶尔以不可预测的方式破坏或丢失数据,那么生活就会简单得多,您可能只需横过手指就能逃脱,希望获得最好的效果。另一方面,如果您需要更强的正确性保证,那么可序列化和原子提交就是建立的方法,但是它们是有代价的:它们通常只在单个数据中心(排除地理分布式体系结构)中工作,您可以实现的规模和容错性能。
虽然传统的交易方式并没有消失,但我也相信,在使应用程序正确和灵活地处理错误方面,并不是最后一句话。在本节中,我将提出一些关于数据流架构中正确性的思考方法。
### 数据库端到端的争论
仅仅因为应用程序使用提供比较强的安全属性的数据系统(例如可序列化的事务),并不意味着应用程序可以保证没有数据丢失或损坏。例如,如果一个应用程序有一个错误导致它写入不正确的数据,或者从数据库中删除数据,那么可序列化的事务不会为你节省。
这个例子可能看起来很无聊但值得认真对待应用程序错误发生人们犯错误。我在第459页的“状态流和不可变性”中使用了这个例子来支持不可变和只能追加的数据因为如果删除错误代码的能力来破坏好的数据更容易从这些错误中恢复数据。
虽然不变性是有用的,但它本身并非万能的。让我们看看可能发生的数据损坏的一个更为简单的例子。
#### 正好执行一次操作
在第476页的“容错”中我们遇到了一种精确调用一次或有效一次语义的想法。如果在处理消息时出现问题您可以放弃丢弃消息 - 即导致数据丢失)或再次尝试。如果再试一次,第一次就有成功的风险,但是你没有发现成功,所以这个消息最终被处理了两次。
处理两次是数据损坏的一种形式:对于相同的服务向客户收费两次(计费太多)或增加计数器两次(夸大一些度量)是不可取的。在这种情况下,正好一次就意味着安排计算,使得最终效果与没有发生错误的情况相同,即使操作实际上由于某种错误而被重试。我们以前讨论过实现这一目标的几种方法。
最有效的方法之一是使幂等操作参见第478页的“幂等性”;即确保它具有相同的效果无论是执行一次还是多次。但是采取一种不自然是幂等的操作并使其具有幂等性需要付出一定的努力和关注您可能需要维护一些额外的元数据例如更新了值的操作ID集合并在从一个节点到另一个节点请参阅第295页上的“领导和锁定”
#### 重复抑制
除了流处理之外还需要抑制重复的相同模式出现在许多其他位置。例如TCP使用数据包上的序列号将它们按正确的顺序排列在收件人处并确定网络上是否有数据包丢失或重复。任何丢失的数据包都会被重新传输并且在将数据交给应用程序之前TCP堆栈会删除任何重复数据包。
但是此重复抑制仅适用于单个TCP连接的上下文中。假设TCP连接是一个客户端与数据库的连接并且它正在执行示例12-1中的事务。在许多数据库中事务与客户端连接有关如果客户端发送了多个查询数据库就知道它们属于同一个事务因为它们是在同一个TCP连接上发送的。如果客户端在发送COMMIT之后但在从数据库服务器回听之前遇到网络中断和连接超时则不知道事务是否已被提交或中止图8-1
**例12-1 将资金从一个账户转移到另一个账户的非赦免**
```sql
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance + 11.00 WHERE account_id = 1234;
UPDATE accounts SET balance = balance - 11.00 WHERE account_id = 4321;
COMMIT;
```
客户端可以重新连接到数据库并重试事务但现在它在TCP重复抑制的范围之外。由于例12-1中的交易不是幂等的可能会发生\$22而不是所需的\$11。因此尽管例12-1是一个交易原子性的标准例子但它实际上并不正确而真正的银行并不像这样工作[3]。
两阶段提交请参阅第354页上的“原子提交和两阶段提交2PC协议会破坏TCP连接和事务之间的11映射因为它们必须允许事务协调器在数据库之后重新连接到数据库一个网络故障并告诉它是否提交或中止有疑问的交易。这足以确保交易只能执行一次吗不幸的是即使我们可以抑制数据库客户端和服务器之间的重复事务我们仍然需要担心最终用户设备和应用程序服务器之间的网络。例如如果最终用户客户端是Web浏览器则可能使用HTTP POST请求向服务器提交指令。也许用户处于一个微弱的蜂窝数据连接他们成功地发送POST但是信号在他们能够从服务器接收响应之前变得太弱。
在这种情况下,用户可能会显示错误消息,并且可能会手动重试。 Web浏览器警告说“你确定要再次提交这个表单吗” - 用户说是,因为他们希望操作发生。 Post / Redirect / Get模式[54]可以避免在正常操作中出现此警告消息但如果POST请求超时它将无济于事。从Web服务器的角度来看重试是一个单独的请求并且从数据库的角度来看这是一个单独的事务。通常的重复数据删除机制无济于事。
#### 操作标识符
要通过几次网络通信使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的 - 您需要考虑请求的端到端流。
例如您可以为操作例如UUID生成唯一的标识符并将其作为隐藏的表单字段包含在客户机应用程序中或计算所有相关表单字段的散列以派生操作ID [3]。如果Web浏览器提交两次POST请求这两个请求将具有相同的操作ID。然后您可以将该操作ID传递到数据库并检查您是否只使用给定的ID执行一个操作如示例12-2中所示。
**例12-2 使用唯一的ID来抑制重复的请求**
```sql
ALTER TABLE requests ADD UNIQUE (request_id);
BEGIN TRANSACTION;
INSERT INTO requests(request_id, from_account, to_account, amount)
VALUES('0286FDB8-D7E1-423F-B40B-792B3608036C', 4321, 1234, 11.00);
UPDATE accounts SET balance = balance + 11.00 WHERE account_id = 1234;
UPDATE accounts SET balance = balance - 11.00 WHERE account_id = 4321;
COMMIT;
```
例12-2依赖于request_id列上的唯一性约束。如果一个事务尝试插入一个已经存在的ID那么INSERT失败事务被中止使其无法生效两次。即使在较弱的隔离级别下关系数据库也能正确地维护唯一性约束而在第248页上的“编写偏斜和幻影”中讨论过应用程序级别的check-then-insert可能会在不可序列化的隔离下失败
除了抑制重复的请求之外示例12-2中的请求表充当事件日志的一种暗示着事件源的方向请参阅第457页的“事件源”。账户余额的更新事实上不必与插入事件相同的事务发生因为它们是多余的并且可以从下游消费者中的请求事件派生出来 - 只要该事件只处理一次这可以再次使用请求ID来执行。
**端到端的论点**
抑制重复交易的这种情况只是一个更普遍的原则的一个例子这个原则被称为端对端的论点它在1984年由SaltzerReed和Clark阐述[55]
只有在通信系统端点的应用程序的知识和帮助下,所讨论的功能才能够完全正确地实现。因此,将这种被质疑的功能作为通信系统本身的功能是不可能的。 (有时,通信系统提供的功能的不完整版本可能有助于提高性能。)
在我们的例子中有问题的函数是重复抑制。我们看到TCP在TCP连接级别抑制重复的数据包一些流处理器在消息处理级别提供了所谓的唯一的语义但是这不足以防止用户提交重复请求一次。 TCP数据库事务和流处理器本身并不能完全排除这些重复。解决这个问题需要一个端到端的解决方案从最终用户客户端一直传递到数据库的事务标识符。
端到端参数也适用于检查数据的完整性以太网TCP和TLS中内置的校验和可以检测网络中数据包的损坏情况但是它们无法检测到发送和接收软件中的错误网络连接的末端或数据存储在磁盘上的损坏。如果您想要捕获所有可能的数据损坏源则还需要端到端的校验和。
类似的说法适用于加密[55]家庭WiFi网络上的密码可以防止人们窃听您的WiFi流量但不会对互联网上其他地方的攻击者进行窥探;您的客户端和服务器之间的TLS / SSL可以得到保护对网络攻击者而不是对服务器的妥协。只有端到端的加密和认证可以防止所有这些事情。
尽管低级功能TCP复制抑制以太网校验和WiFi加密无法单独提供所需的端到端功能但它们仍然很有用因为它们可以降低较高级别出现问题的可能性。例如如果我们没有TCP将数据包放回正确的顺序那么HTTP请求通常会被破坏。我们只需要记住低级别的可靠性功能本身并不足以确保端到端的正确性。
#### 在数据系统中应用端到端思考
这使我回到我的原始论文:仅仅因为应用程序使用提供比较强的安全属性的数据系统(如可序列化事务),并不意味着应用程序保证不会丢失数据或损坏。应用程序本身也需要采取端到端的措施,例如重复压制。
这是一个耻辱因为容错机制很难得到正确的。低级可靠性机制如TCP中的可靠性机制运行良好因此其余较高级别的故障发生得相当少。将抽象中的高级容错机制封装起来非常好以便应用程序代码不必担心它 - 但是我担心我们还没有找到合适的抽象。
长期以来交易被认为是一个很好的抽象我相信它们是有用的。正如第7章介绍中所讨论的那样它们会涉及各种可能的问题并发写入约束违规崩溃网络中断磁盘故障并将其折叠为两种可能的结果提交或中止。这是编程模型的一个巨大的简化但我担心这是不够的。
事务处理非常昂贵尤其是涉及异构存储技术时请参阅第364页的“实践中的分布式事务”。当我们拒绝使用分布式事务是因为它们太昂贵时我们最终不得不在应用程序代码中重新实现容错机制。正如本书中大量的例子所显示的那样关于并发性和部分失败的推理是困难且违反直觉的所以我怀疑大多数应用程序级别的机制不能正常工作。结果是丢失或损坏的数据。
出于这些原因,我认为值得探索的容错抽象方法能够容易地提供特定于应用程序的端到端正确性属性,而且还可以在大型分布式环境中保持良好的性能和良好的操作特性。
### 强制实施约束
### 时间线与完整性
让我们考虑非捆绑数据库的想法背景下的正确性“剥离数据库”第499页。我们看到使用从客户端一直传递到记录写入的数据库的请求标识可以实现端到端的重复压缩。其他类型的限制呢
特别是让我们关注唯一性约束 - 例如我们在例12-2中所依赖的约束。在第330页的“约束和唯一性保证”中我们看到了几个其他需要强制实施唯一性的应用程序功能示例用户名或电子邮件地址必须唯一标识用户文件存储服务不能包含多个文件同名两个人不能在航班或剧院预订同一个座位。
其他类型的约束非常相似:例如,确保帐户余额永远不会变为负数,您不会出售比仓库中的库存更多的物料,或者会议室没有重复的预订。执行唯一性的技术通常也可以用于这些约束。
#### 唯一性约束需要达成共识
在第9章中我们看到在分布式环境中强制执行唯一性约束需要达成共识如果存在多个具有相同值的并发请求则系统需要决定哪个冲突操作被接受并拒绝其他违规操作的约束。
达成这一共识的最常见方式是将单个节点作为领导者并将其负责制定所有决策。只要您不介意通过单个节点发送所有请求即使客户端位于世界的另一端并且只要该节点没有失败就可以正常工作。如果您需要容忍领导者失败那么您又回到了共识问题请参阅第367页上的“单领导表示和共识”
唯一性检查可以根据需要唯一的值进行划分。例如如果需要通过请求标识确保唯一性如例12-2所示则可以确保具有相同请求标识的所有请求都路由到同一分区请参阅第6章。如果您需要用户名是唯一的您可以通过用户名的哈希分区。
但是排除了异步多主复制因为它可能会导致不同的主设备同时接受冲突的写操作因此这些值不再是唯一的请参阅第295页的“实现可线性化系统”。如果你想立即拒绝任何违反约束的写入同步协调是不可避免的[56]。
#### 基于日志的消息传递的唯一性
该日志确保所有消费者以相同的顺序查看消息 - 这种保证在形式上被称为全部命令广播并且等同于共识参见第346页上的“全部命令广播”。在使用基于日志的消息传递的非捆绑数据库方法中我们可以使用非常类似的方法来执行唯一性约束。
流处理器在单个线程上依次占用日志分区中的所有消息请参见第448页的“与传统消息传递相比的日志”。因此如果日志根据需要唯一的值进行分区则流处理器可以明确并确定性地确定几个冲突操作中的哪一个先到达。例如在多个用户尝试声明相同用户名的情况下[57]
1. 对用户名的每个请求都被编码为一条消息,并附加到由用户名散列确定的分区。
2. 流处理器使用本地数据库连续读取日志中的请求,以跟踪使用哪些用户名。对于每个可用的用户名请求,它都会记录该名称并将成功消息发送到输出流。对于每个已经被使用的用户名请求,它都会向输出流发送拒绝消息。
3. 请求用户名的客户端观察输出流并等待与其请求相对应的成功或拒绝消息。
该算法基本上与第363页上的“使用全阶广播实现线性化存储”中的算法相同。它可以通过增加分区数容易地扩展为较大的请求吞吐量因为可以独立处理每个分区。
该方法不仅适用于唯一性约束而且适用于许多其他类型的约束。其基本原理是任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如第174页上的“什么是冲突”和第246页上的“写入偏移和幻影”中所述冲突的定义可能取决于应用程序但流处理器可以使用任意逻辑来验证请求。这个想法与Bayou在20世纪90年代开创的方法类似[58]。
#### 多分区请求处理
当涉及多个分区时确保操作以原子方式执行同时满足约束条件变得更有趣。在示例12-2中可能有三个分区一个包含请求ID一个包含收款人账户另一个包含付款人账户。没有理由把这三件事放在同一个分区因为它们都是相互独立的。
在数据库的传统方法中,执行此事务需要跨所有三个分区进行原子提交,这实质上会将它强制为与任何这些分区上的所有其他事务的总顺序。由于现在存在跨分区协调,不能再独立处理不同的分区,因此吞吐量可能会受到影响。
但是,事实证明,使用分区日志可以实现同等的正确性,并且不需要原子提交:
1. 从账户A向账户B转账的请求由客户端提供唯一的请求ID并根据请求ID附加到日志分区。
2. 流处理器读取请求的日志。对于每个请求消息它发出两条消息以输出流付款人账户A由A分配的借方指令和收款人账户B由B分区的信贷指令。原始的请求ID包含在那些发出的消息中。
3. 其他处理器使用信用卡和借记指令流通过请求ID进行扣除并将更改应用于账户余额。
步骤1和步骤2是必要的因为如果客户直接发送信用和借记指令则需要在这两个分区之间进行原子提交以确保两者都不发生。为了避免分布式事务的需要我们首先将请求永久记录为单条消息然后从第一条消息中获取信用和借记指令。单对象写入在几乎所有数据系统中都是原子性的请参阅“单对象写入”第213页因此请求既可以出现在日志中也可以不出现而不需要多分区原子com-麻省理工学院。
如果步骤2中的流处理器崩溃则从上一个检查点恢复处理。这样做时它不会跳过任何请求消息但可能会多次处理请求并产生重复的信用和借记指令。但是由于它是确定性的因此它只会再次生成相同的指令并且步骤3中的处理器可以使用端到端请求ID轻松地对它们进行重复数据删除。
如果您想确保付款人帐户不会因此次转账而透支您可以额外使用流处理器分区使用多个不同分区的阶段的想法与我们所讨论的类似“多分区数据处理”一节第514页另请参阅“并发控制”一节第462页
### 及时性与完整性
事务的一个方便属性是它们通常是可线性化的(请参阅“可用性”),也就是说,一个作者等待事务提交,之后其所有读者立即可以看到它的写入。
在跨流处理器的多个阶段拆分操作时情况并非如此:日志的使用者在设计上是异步的,因此发送者不会等到其消息已经被消费者处理。但是,客户端可能会等待消息出现在输出流上。这是我们在检查是否满足唯一性约束时在“基于日志的消息传递中的唯一性”一节中所做的操作。
在这个例子中,唯一性检查的正确性不取决于消息的发送者是否等待结果。等待仅具有同步通知发送者唯一性检查是否成功的目的,但是该通知可以与处理消息的效果分离。
更一般地说,我认为术语一致性这个术语将两个不同的需求分开考虑:
***及时性***
及时性意味着确保用户观察系统处于最新状态。我们之前看到如果用户从数据的陈旧副本中读取数据他们可能会以不一致的状态观察数据请参阅第161页上的“复制滞后的问题”。但是这种不一致是暂时的最终只能通过等待和再次尝试来解决。
CAP定理参见第359页的“线性化成本”使用线性化的意义上的一致性这是实现及时性的强有力的方法。像写后读一致性这样的时效性较弱的属性请参阅第162页的“读取自己写的内容”也很有用。
***完整性***
诚信意味着没有腐败;即没有数据丢失并且没有矛盾或错误的数据。尤其是如果将某些派生数据集作为某些基础数据的视图进行维护请参阅“从事件日志导出当前状态”第458页派生必须正确。例如数据库索引必须正确地反映数据库的内容 - 缺少某些记录的索引不是很有用。如果完整性受到侵犯这种不一致是永久性的在大多数情况下等待并再次尝试不会修复数据库损坏。相反需要明确的检查和修理。在ACID事务的上下文中参见第223页上的“ACID的含义”一致性通常被理解为某种特定于应用程序的完整性概念。原子性和耐久性是保持完整性的重要工具。
口号形式:违反及时性是“最终一致性”,而违反诚信则是“永久不一致”。
我要断言,在大多数应用中,完整性比时间要重要得多。违反时效可能令人讨厌和混淆,但是对正直的侵犯可能是灾难性的。
例如在您的信用卡对账单上如果您在过去24小时内完成的交易尚未出现这并不奇怪 - 这些系统有一定的滞后是正常的。我们知道银行协调和异步结算交易,并且这里的及时性并不重要[3]。但是,如果报表余额不等于交易总和加上先前的报表余额(数额错误),或者交易是向您收取但未支付给商家的话,那将是非常糟糕的(消失的金钱)。这样的问题会违反系统的完整性。
#### 数据流系统的正确性
ACID事务通常既提供时间性例如线性化又提供完整性例如原子提交保证。因此如果从ACID交易的角度来看应用程序的正确性那么时间性与完整性的区别就相当不重要了。
另一方面,我们在本章中讨论的基于事件的数据流系统的一个有趣特性是它们将时间性和完整性分开。在异步处理事件流时,不能保证及时性,除非在返回之前明确地构建等待消息到达的消费者。但是,完整性实际上是流式传输系统的核心。
一次或一次有效的语义请参阅“故障容错”一节第437页是一种保持完整性的机制。如果事件丢失或者事件发生两次数据系统的完整性可能被侵犯。因此容错消息传递和重复抑制例如幂等操作对于在面对故障时保持数据系统的完整性是重要的。
正如我们在上一节看到的那样,可靠的流处理系统可以在不需要分布式事务和原子提交协议的情况下保持整体性,这意味着它们可以实现更好的可比较的正确性
性能和运行稳健性。我们通过以下机制的结合实现了这一完整性:
* 将写入操作的内容表示为单个消息,可以轻松地以原子方式编写 - 这种方法非常适合事件采购请参阅第457页的“事件采购”
* 使用确定性描述函数从该单个消息中获取所有其他状态更新这与存储过程类似请参见第252页的“实际的串行执行”和第479页的“作为派生函数的应用程序代码”
* 通过所有这些级别的处理传递客户端生成的请求ID启用端到端重复抑制和幂等性
* 使消息不可变并允许派生数据不时重新处理这使得从错误中恢复变得更加容易请参阅“不可变事件的优点”第367页
这种机制的组合在我看来是未来构建容错应用程序的一个非常有前途的方向。
#### 松散的解释约束
如前所述,执行唯一性约束需要达成共识,通常通过在单个节点中汇集特定分区中的所有事件来实现。如果我们想要传统的唯一性约束形式,并且流处理无法避免,这种限制是不可避免的。
然而,另一个要认识到的是,许多真正的应用程序实际上可以摆脱唯一性较弱的概念:
* 如果两个人同时注册相同的用户名或预订相同的座位,则可以发送其中一个消息来道歉,并要求他们选择不同的用户名。这种纠正错误的变化被称为补偿性交易[59,60]。
* 如果客户订购的物品多于仓库中的物品,则可以订购更多库存,为延误向客户道歉,并向他们提供折扣。实际上,如果叉车在仓库中的某些物品上方跑过来,而库存的物品数量比您想象的要少,那么您就必须这样做[61]。因此,无论如何,道歉工作流程已经需要成为业务流程的一部分,因此可能不需要对库存中的项目数量进行线性化约束。
* 同样地,许多航空公司预计飞行员会错过飞机,许多旅馆超额预订客房,预计部分客人将取消预订。在这些情况下,出于商业原因故意违反了“每个座位一人”的约束,并且处理补偿过程(退款,升级,在邻近酒店提供免费房间)以处理需求超过供应的情况。即使没有超额预订,为了应对由于恶劣天气而被取消的航班或者罢工的员工,这些问题的恢复仅仅是商业活动的正常组成部分,就需要道歉和赔偿流程。
* 如果有人收取比他们账户中更多的钱,银行可以向他们收取透支费用,并要求他们偿还欠款。通过限制每天的提款总额,银行的风险是有限的。
在许多商业环境中,临时违反约束并稍后通过道歉修复它实际上是可以接受的。道歉的成本(金钱或报酬)各不相同,但通常很低:您无法取消发送电子邮件,但可以发送后续电子邮件并进行更正。如果您不小心向信用卡收取了两次费用,您可以退还其中一项费用,而您的费用仅仅是处理费用,并且可以处理客户投诉。一旦自动提款机支付了钱,就不能直接将其退回,尽管原则上如果账户透支并且客户不支付,原则上可以派遣收债员收回款项。
道歉的成本是否可以接受是一个商业决策。如果可以接受的话,在写入数据之前检查所有约束的传统模型是不必要的限制,并且不需要线性化约束。乐观地继续写作,并在事实之后检查约束,这可能是一个合理的选择。您仍然可以确保验证发生在做恢复成本高昂的事情之前,但这并不意味着在您编写数据之前您必须先进行验证。
这些应用程序确实需要完整性您不会希望失去预订或者由于信用和借方不匹配而导致资金消失。但是他们并不需要及时执行约束如果您销售的物品多于仓库中的物品则可以在事后道歉后修补问题。这样做与我们在第171页上的“处理写入冲突”中讨论的冲突解决方法类似。
#### 协调避免数据系统
我们现在做了两个有趣的观察:
1. 数据流系统可以保持对派生数据的完整性保证,而无需原子提交,线性化或同步跨分区协调。
2. 虽然严格的唯一性约束要求及时性和协调性,但许多应用程序实际上可以很好地处理宽松的约束,只要整个过程保持完整性,它们可能会被暂时违反并予以修复。
总之,这些观察意味着数据流系统可以为许多应用程序提供数据管理服务,而不需要协调,同时仍然提供强大的完整性保证。这种避免协调的数据系统具有很大的吸引力:它们可以比需要执行同步协调的系统获得更好的性能和容错能力[56]。
例如,这种系统可以在多引导器配置中跨多个数据中心进行分布式操作,在区域之间异步复制。任何一个数据中心都可以继续独立运行,因为不需要同步跨区域协调。这样一个系统的时效性保证会很弱 - 如果不引入协调,它就不可能线性化,但它仍然可以提供强有力的完整性保证。
在这种情况下,序列化事务作为维护派生状态的一部分仍然是有用的,但它们可以在小范围内运行,在那里它们工作得很好[8]。异构分布式事务如XA事务请参阅“实践中的分布式事务”第367页不是必需的。同步协调仍然可以在需要的地方引入例如在无法恢复的操作之前执行严格的限制但是如果只有一个小的协议则不需要任何东西来支付协调费用应用程序的一部分需要它[43]。
查看协调和约束的另一种方法是:它们减少了由于不一致而必须做出的道歉数量,但也可能会降低系统的性能和可用性,从而可能会增加必须制定的道歉数量中断。您不能将道歉数量减少到零,但您可以根据自己的需求寻找最佳平衡点 - 这是既不存在太多不一致性又不存在太多可用性问题的最佳选择。
### 信任,但需要验证
我们所有关于正确性完整性和容错性的讨论都假设某些事情可能会出错但其他事情则不会。我们将这些假设称为我们的系统模型请参阅“将系统模型映射到现实世界”一节第309页例如我们应该假设进程可能会崩溃机器可能突然断电网络可能会任意延迟或丢弃消息。但是我们也可以假设写入磁盘的数据在fsync之后不会丢失内存中的数据没有损坏并且CPU的乘法指令总是返回正确的结果。
这些假设是非常合理的,因为大多数时候它们都是真实的,如果我们不得不经常担心计算机出错,那么很难完成任何事情。传统上,系统模型采用二元方法处理故障:我们假设有些事情会发生,而其他事情永远不会发生。实际上,这更像是一个概率问题:有些事情更可能,其他事情不太可能。问题是违反我们的假设是否经常发生,以至于我们可能在实践中遇到它们。
我们已经看到数据可能会在磁盘未触及时破损请参阅第227页的“复制和耐久性”并且网络上的数据损坏有时可能会妨碍TCP校验和请参阅第293页的“虚弱形式的说谎” )。也许这是我们应该更加关注的事情?
我过去曾经使用过的一个应用程序收集了来自客户端的崩溃报告,我们收到的一些报告只能通过在这些设备的内存中随机位翻转来解释。这看起来不太可能,但是如果你有足够的设备来运行你的软件,那么即使发生的事情也不会发生。除了由于硬件故障或辐射导致的随机存储器损坏之外,某些病态存储器访问模式甚至可以在没有故障的存储器中翻转位[62] - 可用于破坏操作系统中安全机制的效应[63]技术被称为rowhammer。一旦你仔细观察硬件并不是完美的抽象。
要清楚的是,随机位翻转在现代硬件上仍然非常罕见[64]。我只想指出,他们并没有超越可能性领域,所以他们值得关注。
#### 在软件缺陷面前保持完整性
除了这些硬件问题之外总是存在软件错误的风险这些错误不会被较低级别的网络内存或文件系统校验和所捕获。即使广泛使用的数据库软件也存在一些问题即使MySQL和PostgreSQL是健壮且充分的但我个人发现MySQL的例子未能正确维护唯一性约束[65]和PostgreSQL的序列化隔离级别表现出写入偏斜异常[66]他们认为这些数据库已经被许多人进行了多年的战斗测试。在不太成熟的软件中,情况可能会更糟糕。
尽管在仔细设计,测试和审查方面做出了相当大的努力,但错误仍在蔓延。虽然它们很少,并且最终被发现并修复,但仍然存在一段时期,这些错误可能会破坏数据。
当涉及到应用程序代码时,我们不得不承担更多的错误,因为大多数应用程序在数据库代码所做的评审和测试的数量上没有接近的地方。许多应用程序甚至没有正确使用数据库提供的用于保存完整性的功能,例如外键或唯一性约束[36]。
ACID意义上的一致性请参阅第224页的“一致性”基于数据库以一致状态启动并且事务将其从一个一致状态转换为另一个一致状态的想法。因此我们期望数据库始终处于一致状态。但是如果您认为该交易没有错误则这个概念才有意义。如果应用程序错误地使用数据库以某种方式例如不安全地使用弱隔离级别数据库的完整性无法得到保证。
#### 不要盲目信任他们的承诺
由于硬件和软件并不总是符合我们希望的理想,所以数据腐败似乎迟早是不可避免的。因此,我们至少应该有办法查明数据是否已经损坏,以便我们能够修复它并尝试追查错误的来源。检查数据的完整性称为审计。
如“不可变事件的优点”一节中所述,审计不仅适用于财务应用程序。然而,可审计性在财务中非常重要,因为每个人都知道错误发生了,而且我们都认识到需要能够检测和解决问题。
成熟的系统同样倾向于考虑不太可能的事情出错的可能性并管理这种风险。例如HDFS和Amazon S3等大规模存储系统不完全信任磁盘它们运行后台进程这些后台进程持续读回文件将其与其他副本进行比较并将文件从一个磁盘移动到另一个磁盘以便减轻沉默腐败的风险[67]。
如果你想确保你的数据仍然存在,你必须真正阅读并检查。大多数时候它仍然会在那里,但如果不是这样,你真的很想早日找到答案。通过同样的说法,尝试从备份中不时恢复是非常重要的,否则只有当它太迟而您已经丢失数据时才会发现备份被破坏。不要盲目地相信它是全部工作。
#### 验证的文化
像HDFS和S3这样的系统仍然必须假定磁盘大部分时间都能正常工作 - 这是一个合理的假设,但假设它们始终正常工作并不相同。然而,目前还没有多少系统有这种“信任但是验证”的方式来持续审计自己。许多人认为正确性保证是绝对的,并且没有为罕见数据损坏的可能性做出规定。我希望将来我们会看到更多的自我验证或自我审计系统,不断检查自己的诚信,而不是依赖盲目的信任[68]。
我担心ACID数据库的文化导致我们在盲目信任技术如交易机制的基础上开发应用程序而忽视了这种过程中的任何可审计性。由于我们所信任的技术在大多数情况下工作得很好审计机制并不值得投资。
但随之而来的是数据库的格局发生了变化在NoSQL的旗帜下一致性的保证变得越来越少成熟的存储技术越来越被广泛的使用。但是由于审计机制还没有形成我们继续在盲目信任的基础上建立应用程序即使这种方法现在变得更加危险。让我们思考一下关于可审计性的设计。
#### 设计可审计性
如果一个事务在一个数据库中改变了多个对象事实上很难说事实是什么意思。即使您捕获事务日志请参阅第454页上的“更改数据捕获”各种表中的插入更新和删除操作并不一定清楚表明为何执行这些突变。决定这些突变的应用逻辑的调用是暂时的不能被复制。
相比之下,基于事件的系统可以提供更好的可审计性。在事件源方法中,系统的用户输入被表示为一个单一的不可变事件,并且任何结果状态更新都来自该事件。派生可以做出确定性和可重复性,以便通过相同版本的派生代码运行相同的事件日志将导致相同的状态更新。
清楚地看到数据流请参阅第419页上的“批处理输出的原理”可以使数据的来源更加清晰从而使完整性检查更加可行。对于事件日志我们可以使用散列来检查事件存储没有被破坏。对于任何派生状态我们可以重新运行从事件日志中派生它的批处理和流处理器以检查是否获得相同的结果或者甚至并行运行冗余派生。
确定性和定义良好的数据流也使调试和跟踪系统的执行变得容易,以确定它为什么做了某些事情[4,69]。如果出现意想不到的事情,那么具有诊断能力来重现导致意外事件的确切环境 - 一种时间行程调试功能是非常有价值的。
#### 再次是端到端的论点
如果我们不能完全相信系统的每个组件都不会发生腐败 - 每一个硬件都是无错的,并且每一个软件都没有缺陷 - 那么我们至少必须定期检查数据的完整性。如果我们不检查,我们就不会发现腐败,直到它太晚了,并且已经造成了一些下游损害,在这一点上追踪这个问题将变得更加困难和昂贵。
检查数据系统的完整性最好是以端到端的方式完成请参阅“数据库的端到端争论”页码51我们可以在完整性检查中包含的系统越多那里的机会就越少在这个过程的某个阶段腐败是被忽视的。如果我们可以检查整个派生数据管道是端到端正确的那么沿着路径的任何磁盘网络服务和算法都隐含在检查中。
持续的端到端完整性检查可以提高您对系统正确性的信心,从而使您的移动速度更快[70]。与自动化测试一样,审计增加了发现错误的可能性,从而降低了系统更改或新存储技术造成损害的风险。如果您不害怕进行更改,您可以更好地开发应用程序以满足不断变化的需求。
#### 审计数据系统的工具
目前,数据系统并不多,这使可审计性成为高层关注的重点。一些应用程序实现自己的审计机制,例如将所有更改记录到单独的审计表中,但是保证审计日志的完整性,并且数据库状态仍然很困难。事务日志可以通过定期使用硬件安全模块对事务日志进行签名来进行防篡改,但这并不能保证正确的事务首先进入日志。
使用加密工具来证明系统的完整性是十分有趣的,这种方式对于广泛的硬件和软件问题以及甚至是潜在的恶意行为都很有效。加密货币,区块链和分布式账本技术,如比特币,以太坊,波纹,恒星等等[71,72,73]已经出现在这个领域。
我没有资格评论这些技术作为商定合同的货币或机制的优点。但是,从数据系统的角度来看,它们包含了一些有趣的想法。实质上,它们是分布式数据库,具有数据模型和事务机制,不同副本可以由互不信任的组织托管。复制品不断检查彼此的完整性,并使用共识协议来约定应执行的交易。
我对这些技术的拜占庭容错方面有些怀疑参见第304页的“拜占庭故障”而且我发现工作证明比如比特币挖掘技术非常浪费。比特币的交易吞吐量相当低尽管出于政治和经济原因而不是技术交易。但是完整性检查方面很有趣。
我可以想象完整性检查和审计算法(如证书透明度和分布式分类账算法)在一般数据系统中得到越来越广泛的应用。 一些工作将需要使它们具有同样的可扩展性,因为没有加密审计的系统,并且尽可能降低性能损失。 但我认为这是一个值得关注的领域。
## 做正确的事情
### 预测性的分析
在本书的最后部分,我想退后一步。在本书中,我们考察了各种不同的数据系统体系结构,评估了它们的优缺点,并探讨了构建可靠,可扩展和可维护应用程序的技术。但是,我们忽略了现在我想填补的讨论中一个重要的基本部分。
### 隐私与跟踪
每个系统都是为了一个目的而建造的我们采取的每一项行动都有既定的意义,也有无意义的后果。其目的可能与赚钱一样简单,但世界的后果可能会远远超出最初的目的。我们这些建立这些系统的工程师有责任仔细考虑这些后果并有意识地决定我们希望生活在哪一种世界。
我们将数据作为一个抽象的东西来讨论,但请记住,许多数据集都是关于人的:他们的行为,他们的兴趣,他们的身份。我们必须以人性和尊重来对待这些数据。用户也是人类,人的尊严是至高无上的。
软件开发日益涉及重要的道德选择。有一些指导原则可以帮助软件工程师解决这些问题例如ACM的软件工程道德规范和专业实践[77],但很少在实践中讨论,应用和实施。因此,工程师和产品经理有时会对隐私和产品潜在的负面后果采取非常傲慢的态度[78,79,80]。
技术本身并不好或不好,重要的是如何使用它以及它如何影响人。对于像搜索引擎这样的软件系统来说,就像使用像武器这样的武器非常相似。我认为,软件工程师仅仅专注于技术并忽视其后果是不够的:伦理责任也是我们的责任。有关道德的推理是困难的,但忽视这一点太重要了。
### 预测分析
例如,预测分析是“大数据”炒作的主要部分。使用数据分析预测天气或疾病传播是一回事[81];另一个问题是要预测一个罪犯是否可能再犯罪,一个贷款申请人是否有可能违约,或者一个保险客户是否有可能做出昂贵的索赔。后者直接影响到个人的生活。当然,支付网络希望防止欺诈交易,银行希望避免不良贷款,航空公司希望避免劫持,公司希望避免激怒无效或不可靠的人。从他们的角度来看,错过商业机会的成本很低,但不良贷款或有问题的员工的成本要高得多,因此组织希望保持谨慎是很自然的。如果有疑问,他们最好不要说。
然而,随着算法决策变得越来越普遍,某人(准确地或错误地)被某种算法标记为有风险,可能会遭受大量“不”决定。系统地排除工作,航空旅行,保险,物业租赁,金融服务和社会其他关键方面是个人自由的巨大制约因素,因此被称为“算法监狱”[82]。在尊重人权的国家,刑事司法系统在被证明有罪之前就认定无罪。另一方面,自动化系统可以有系统地,任意地排除一个人参与社会活动,而没有任何内疚的证据,而且几乎没有上诉的机会。
#### 偏见和歧视
算法做出的决定不一定比人类做出的更好或更差。每个人都可能有偏见,即使他们积极地试图抵消它们,歧视性做法也可能在文化上被制度化。人们希望根据数据做出决定,而不是通过人们的主观和现场评估来更加公平,给那些在传统系统中经常被忽视的人更好的机会[83]。
当我们开发预测分析系统时,我们不仅仅是通过使用软件来指定什么时候说是或否的规则来自动化人的决定;我们甚至将规则本身从数据中推断出来。但是,这些系统学到的模式是不透明的:即使数据中存在一些相关性,我们也可能不知道为什么。如果在算法输入中存在系统偏差,系统很可能会在输出中学习和放大偏差[84]。
在许多国家反歧视法律禁止根据种族年龄性别性别残疾或信仰等受保护特征对待不同的人。一个人的数据的其他特征可能会被分析但是如果他们与受保护的特征相关会发生什么例如在种族隔离的邻居中一个人的邮政编码甚至是他们的IP地址都是一个强烈的种族预测。这样说相信一种算法可以以某种方式将偏倚的数据作为输入并产生公平和公正的输出[85],这似乎是荒谬的。然而,这种观点似乎常常被数据驱动型决策制定的支持者所暗示,这种态度被讽刺为“机器学习就像洗钱对于偏见”[86]。
预测分析系统只是从过去推断出来的;如果过去是歧视性的,他们就会把这种歧视归为法律。如果我们希望未来比过去更好,那么就需要道德想象力,而这只有人类才能提供[87]。数据和模型应该是我们的工具,而不是我们的主人。
#### 责任和问责
自动决策开启了责任和问责的问题[87]。如果一个人犯了错误,他们可以被追究责任,受决定影响的人可以上诉。算法也会犯错误,但是如果他们出错,谁会犯错误[88]?当一辆自驾车引发事故时,谁负责?如果自动信用评分算法系统地区分特定种族或宗教的人,是否有任何追索权?如果机器学习系统的决定受到司法审查,您能向法官解释算法是如何做出决定的吗?
信用评级机构是收集数据来做出人们决策的一个老例子。不良信用评分会使生活变得困难,但至少一个信用评分通常是基于一个人的实际借款历史的相关事实,并且记录中的任何错误都可以得到纠正(尽管机构通常不会这么容易)。然而,基于机器学习的评分算法通常使用更广泛的输入范围,并且更加不透明,使得难以理解特定决策是如何发生的以及是否有人正在以不公平或歧视的方式被对待[89]。
信用评分总结了“你过去的表现如何?”,而预测分析通常是基于“谁与你类似,以及过去人们喜欢你的行为如何?”。与他人行为相似的绘图意味着刻板印象人们,例如根据他们居住的地方(一个关于种族和社会经济阶层的密切代理人)。那些放错人的人呢?而且,如果由于错误的数据而做出的决定是不正确的,则追索权几乎是不可能的[87]。
很多数据本质上是统计的这意味着即使概率分布总体上是正确的个别情况也可能是错的。例如如果贵国的平均寿命是80岁那么这并不意味着在80岁生日时就会死亡。从平均分布和概率分布来看你不能说很多关于某个特定人的生活年龄。同样预测系统的输出是概率性的在个别情况下可能是错误的。
盲目相信数据至高无上的决策不仅是妄想,它是非常危险的。随着数据驱动的决策变得越来越普遍,我们需要弄清楚如何使算法负责任和透明,如何避免加强现有的偏见,以及如何在不可避免的错误时加以修复。
我们还需要弄清楚如何防止数据被用来伤害人,并且实现其正向潜力。例如,分析可以揭示人们生活的财务和社会特征。一方面,这种权力可以用来把援助和支持集中在帮助那些最需要援助的人身上。另一方面,它有时被掠夺性企业用于识别弱势群体并向高风险产品销售,如高成本贷款和无价值的大学学位[87,90]。
#### 反馈回路
即使预测性应用程序对人们的影响较小,比如推荐系统,也存在难以解决的问题。当服务善于预测用户想要看到什么内容时,他们最终可能只会向人们展示他们已经同意的观点,导致产生陈腔滥调,错误信息和极化可能滋生的回声室。我们已经看到社交媒体呼应室对竞选活动的影响[91]。
当预测分析影响人们的生活时,特别是由于自我强化反馈循环而出现有害问题。例如,考虑雇主使用信用评分来评估潜在员工的情况。你可能是一个信誉好的好员工,但是由于你不能控制的不幸,你会突然发现自己陷入财务困境。由于您错过了账单付款,您的信用评分会受到影响,您将不太可能找到工作。失业使你陷入贫困,这进一步恶化了你的分数,使其更难找到工作[87]。由于有毒的假设,这是一个下降的螺旋,隐藏在数学严谨和数据伪装的背后。
我们不能总是预测这种反馈循环何时发生。然而,通过考虑整个系统(不仅仅是计算机化的部分,而且还有与之互动的人),可以预测许多后果 - 一种称为系统思维的方法[92]。我们可以尝试理解数据分析系统如何响应不同的行为,结构或特性。该系统是否加强和扩大了人们之间现有的差异(例如,使富人更穷或更穷),还是试图打击不公正?即使有最好的意图,我们也必须小心意想不到的后果。
#### 隐私和跟踪
除了预测分析的问题 - 即使用数据来做出关于人的自动决策 - 数据收集本身也存在道德问题。收集数据的组织与正在收集数据的人之间有什么关系?
当系统仅存储用户明确输入的数据时,系统会以特定方式存储和处理数据,系统正在为用户执行服务:用户就是客户。但是,当用户的活动被跟踪并记录为他们正在做的其他事情的副作用时,这种关系就不那么清晰了。该服务不再仅仅是用户告诉它做的事情,而是服务于它自己的利益,这可能与用户的兴趣相冲突。
跟踪行为数据对于许多在线服务的面向用户的功能变得越来越重要:跟踪点击哪些搜索结果有助于提高搜索结果的排名;推荐“喜欢X的人也喜欢Y”可以帮助用户发现有趣而有用的东西; A / B测试和用户流量分析可以帮助指出如何改进用户界面。这些功能需要一定量的用户行为跟踪用户可以从中受益。
但是,根据公司的商业模式,追踪往往不止于此。如果服务是通过广告获得资金支持的,那么广告客户就是真正的客户,而用户的利益则位居第二。跟踪数据变得更加详细,分析变得更深入,数据保留很长时间,以便为营销目的建立每个人的详细资料。
现在,公司和收集数据的用户之间的关系开始变得非常不同。用户可获得免费服务,并尽可能使用户接受该服务。对用户的追踪主要不是服务于该个人,而是服务于该服务的广告商的需求。我认为这种关系可以用一个更具罪犯内涵的词来恰当地描述:监视。
#### 监控
作为一个思想实验,尝试用监视来替换单词数据,并观察常用短语是否听起来如此好[93]。这样如何:“在我们的监控驱动的组织中,我们收集实时监控流并将它们存储在我们的监控仓库中。我们的监控科学家使用高级分析和监测处理来获得新的见解。“
这个思想实验对于本书“设计监控 - 密集型应用程序”来说是异乎寻常的争论,但我认为需要强调的话来强调这一点。在我们制作软件“吃世界”的尝试中[94],我们已经建立了世界上迄今为止所见过的最伟大的大众监视基础设施。我们正朝着物联网迈进,我们正在迅速接近这样一个世界:每个有人居住的空间至少包含一个互联网连接的麦克风,以智能手机,智能电视,语音控制助理设备,婴儿监视器甚至儿童玩具使用基于云的语音识别。这些设备中的很多都有可怕的安全记录[95]。
即使是极权主义和专制政权也只能梦想在每个房间放置一个麦克风,并强迫每个人不断地携带能够追踪其位置和动作的设备。然而,我们显然自愿地,甚至热心地投身于这个全面监视的世界。不同之处在于数据是由公司而不是由政府机构收集的[96]。
并不是所有的数据收集都必须符合监督的要求,但检查它们可以帮助我们理解我们与数据收集者的关系。为什么我们似乎很乐意接受企业的监督?也许你觉得你没有隐瞒 - 换句话说,你完全符合现有的权力结构,你不是被边缘化的少数派,你不必害怕迫害[97]。不是每个人都如此幸运。也许这是因为目的似乎是良性的 - 这不是强制性的,也不是强制性的,而只是更好的建议和更个性化的营销。但是,结合上一节中对预测分析的讨论,这种区分似乎不太清晰。
我们已经看到与汽车追踪设备挂钩的汽车保险费以及取决于佩戴健身追踪设备的人的健康保险范围。当监视被用来确定在生活的重要方面如保险或就业等方面的东西时,它开始变得不那么温和。此外,数据分析可以揭示出令人惊讶的侵入性事物:例如,智能手表或健身追踪器中的运动传感器可用于以相当好的准确度计算出您正在输入的内容(例如密码)[98]。而分析算法只会变得更好。
#### 同意和选择的自由
我们可能会断言用户自愿选择使用跟踪其活动的服务,并且他们已同意服务条款和隐私政策,因此他们同意收集数据。我们甚至可以声称,用户正在接受有价值的服务,以换取所提供的数据,并且为了提供服务,跟踪是必要的。毫无疑问,社交网络,搜索引擎以及其他各种免费的在线服务对于用户来说都是有价值的,但是这个说法存在问题。
用户几乎不知道他们提供给我们的数据库的数据,或者它们如何保留和处理 - 而大多数隐私政策的作用更多的是模糊而不是照亮。如果不了解他们的数据会发生什么,用户无法给出任何有意义的同意。通常,来自一个用户的数据还说明关于不是该服务的用户并且没有同意任何条款的其他人的事情。我们在本书的这一部分讨论的派生数据集(来自整个用户群的数据可能与行为跟踪和外部数据源相结合)恰恰是用户无法获得任何有意义理解的数据种类。
而且,数据是通过单向过程从用户中提取出来的,而不是真正的互惠关系,而不是公平的价值交换。没有对话,用户无法选择他们提供的数据量以及他们收到的服务回报:服务和用户之间的关系是非常不对称和片面的。这些条款是由服务设置,而不是由用户[99]。
对于不同意监视的用户,唯一真正的选择就是不使用服务。但是这个选择也不是免费的:如果一项服务如此受欢迎以至于“被大多数人认为是基本社会参与的必要条件”[99],那么指望人们选择退出这项服务是不合理的 - 使用它事实上是强制性的。例如在大多数西方社会群体中携带智能手机使用Facebook进行社交以及使用Google查找信息已成为常态。特别是当一项服务具有网络效应时人们选择不使用它会产生社会成本。
由于跟踪用户而拒绝使用服务,这只是少数拥有足够的时间和知识来了解其隐私政策的人员的一种选择,并且有可能错过社会参与或专业人士如果他们参与了服务,可能会出现机会。对于处境不太好的人来说,没有任何意义上的自由选择:监督变得不可避免。
#### 数据的隐私和使用
有时候,人们声称“隐私已经死了”,理由是有些用户愿意把各种有关他们生活的事情发布到社交媒体上,有时是通常的,有时是个人的。但是,这种说法是错误的,并且依赖于对隐私一词的错误理解。
拥有隐私并不意味着保密一切;它意味着要有自由选择要向谁揭示哪些事情,要公开什么,以及要保密什么。隐私权是一项决定权:它可以让每个人决定他们希望在每个场合保密和透明度之间的区域[99]。这是一个人的自由和自主的重要方面。
当通过监控基础设施从人身上提取数据时,隐私权不一定被侵蚀,而是转移到数据收集器。获取数据的公司本质上是说“相信我们用你的数据做正确的事情”,这意味着决定要透露什么和保密的权利是从个人转移到公司的。
这些公司反过来选择保留这个监视秘密的大部分结果,因为揭示它会被认为是令人毛骨悚然的,并且会损害他们的商业模式(这比其他公司更依赖于对人的了解)。关于用户的亲密信息只是间接显示的,例如以广告针对特定人群(如那些患有特定疾病的人群)的工具的形式。
即使特定用户无法从特定广告定位的人群中个人身份识别,他们已经失去了一些关于披露一些隐私信息的机构,例如他们是否患有某种疾病。决定根据个人喜好向谁透露什么的不是用户,而是公司以最大化利润为目标行使隐私权。
许多公司都有一个不被视为令人毛骨悚然的目标 - 避免了他们的数据收集的实际侵入性问题,而是专注于管理用户感知。即使这些看法经常被糟糕的管理:例如,事实可能是事实上的正确,但如果它触发痛苦的回忆,用户可能不希望被提醒它[100]。对于任何类型的数据,我们都应该期望在某种程度上出现错误,不可取或不适当的可能性,我们需要建立处理这些失败的机制。无论是“不可取的”还是“不适当的”,当然都是由人的判断决定的;除非我们明确地规划它们尊重人类的需求,否则算法会忽略这些概念。作为这些系统的工程师,我们必须谦虚,接受和规划这些失败。
允许在线服务的用户控制其他用户可以看到的数据的哪些方面的隐私设置是将一些控制交还给用户的起点。但是,无论设置如何,服务本身仍然可以不受限制地访问数据,并且可以以隐私策略允许的任何方式自由使用它。即使服务承诺不会将数据出售给第三方,它通常会授予自己不受限制的权利,以在内部处理和分析数据,而且往往比用户明显看到的要多得多。
这种从个人到公司的大规模隐私权转移历史上是史无前例的[99]。监控一直存在,但它过去是昂贵和手动的,不可扩展和自动化。信托关系一直存在,例如患者与其医生之间,或被告与其律师之间 - 但在这些情况下,数据的使用严格受到道德,法律和监管限制的约束。互联网服务使得在没有有意义的同意的情况下收集大量敏感信息变得容易得多,并且在没有用户理解他们的私人数据正在发生的情况下大规模使用它。
#### 数据作为资产和权力
由于行为数据是用户与服务交互的副产品,因此有时称为“数据耗尽” - 表明数据是毫无价值的浪费材料。从这个角度来看,行为和预测分析可以被看作是一种从数据中提取价值的回收形式,否则这些数据会被抛弃。
更准确的说是反过来看:从经济的角度来看,如果有针对性的广告是为服务付费的话,那么关于人的行为数据就是服务的核心资产。在这种情况下,用户与之交互的应用程序仅仅是一种诱骗用户将更多的个人信息提供给监控基础设施的手段[99]。数据提取机器人讥讽地发现,在线服务中常常表现出令人愉快的人类创造力和社交关系。
个人数据是宝贵资产的说法得到了数据中介的支持,这个数据中介是一个隐蔽的行业,它主要是为了市场目的而采购,收集,分析,推断和转售侵入性个人数据[ 90。初创公司通过他们的用户数量通过“眼球”即通过他们的监视能力来估价。
因为数据很有价值,所以很多人都想要它。当然,公司需要它 - 这就是为什么他们收集它的原因。但政府也想获得它:通过秘密交易,胁迫,法律强制或者只是偷窃[101]。当公司破产时,收集到的个人资料就是被出售的资产之一。而且,数据难以确保,因此违规事件经常令人不安地发生[102]。
这些观察已经导致批评者说数据不仅仅是一种资产,而是一种“有毒资产”[101],或者至少是“有害物质”[103]。即使我们认为我们有能力防止滥用数据,但是每当我们收集数据时,我们都需要平衡这些好处和落入他们手中的风险:计算机系统可能会被犯罪分子或敌对的外国情报服务,数据可能会被内部人士泄露,公司可能会落入不合法的管理层之中,而这些管理层不会分享我们的价值观,或者这个国家可能会被毫无疑问迫使我们交出数据的政权所接管。
正如古老的格言所言,“知识就是力量”。此外,“在避免审查自己的同时审视他人是最重要的权力形式之一”[105]。这就是为什么极权政府希望监督:这让他们有能力控制人口。尽管今天的科技公司并没有公开地寻求政治权力,但是他们积累的数据和知识却给他们带来了很多权力,其中大部分是在公共监督之外偷偷摸摸的[106]。
#### 记住工业革命
数据是信息时代的决定性特征。互联网,数据存储,处理和软件驱动的自动化正在对全球经济和人类社会产生重大影响。随着我们的日常生活和社会组织在过去的十年中发生了变化,并且在未来的十年中可能会继续发生根本性的变化,所以我们就会想到与工业革命的比较[87,96]。
工业革命是通过重大的技术和农业进步来实现的,它带来了持续的经济增长,长期的生活水平显着提高。然而,它也带来了一些重大问题:空气污染(由于烟雾和化学过程)以及水(工业和人类的废物)造成的污染是可怕的。工厂老板生活在辉煌中,而城市工人经常住在非常贫困的住房里,并且在恶劣的条件下长时间工作。童工是常见的,包括在矿场工作的危险和低薪。
花了很长时间才制定了保护措施,例如环境保护条例,工作场所安全协议,禁止使用童工和食品卫生检查。毫无疑问,生产成本增加了,因为事实再也不能把废物倒入河流,销售污染的食物或者剥削工人。但是,整个社会都从中受益匪浅,我们中很少人会想要回到这些规定之前的时间[87]。
就像工业革命有一个黑暗的一面需要管理一样我们对信息时代的转变也有我们需要面对和解决的重大问题。我相信收集和使用数据是其中的一个问题。用布鲁斯·施奈尔Bruce Schneier[96]的话来说:
> 数据是信息时代的污染问题,保护隐私是环境挑战。几乎所有的电脑都能生成信息。它停留在周围,溃烂。我们如何处理它 - 我们如何控制它以及如何处理它 - 对我们信息经济的健康至关重要。正如我们今天回顾工业时代的早期十年,并想知道我们的祖先在匆忙建立一个工业世界的过程中如何忽略污染,我们的孙辈在信息时代的前几十年将回顾我们,就我们如何应对数据收集和滥用的挑战来判断我们。
>
> 我们应该设法让他们感到骄傲。
#### 立法和自律
数据保护法可能有助于维护个人的权利。例如1995年的“欧洲数据保护指令”规定个人数据必须“为特定的明确的和合法的目的收集而不是以与这些目的不相符的方式进一步处理”并且数据必须“充分与收集目的相关并不过分“[107]。
但是,这个立法在今天的互联网环境下是否有效还是有疑问的[108]。这些规则直接违背了大数据的理念,即最大限度地收集数据,将其与其他数据集结合起来进行试验和探索,以便产生新的见解。探索意味着将数据用于未预期的目的,这与用户同意的“特定和明确”目的相反(如果我们可以有意义地谈论同意的话)[109]。目前正在制定更新的规定[89]。
收集大量有关人员数据的公司反对监管,认为这是创新的负担和阻碍。在某种程度上,这种反对是有道理的。例如,分享医疗数据时,隐私存在明显风险,但也有潜在机会:如果数据分析能够帮助我们实现更好的诊断或找到更好的治疗方法,可以预防多少人死亡[110]?过度监管可能会阻止这种突破。这种潜在机会与风险之间难以平衡[105]。
从根本上说,我认为我们需要在个人数据方面进行科技行业的文化转变。我们应该停止将用户作为度量标准进行优化,并记住他们是值得尊重,尊严和代理的人。我们应该自我调节我们的数据收集和处理实践,以建立和维持依赖我们软件的人们的信任[111]。我们应该自己去教育最终用户如何使用他们的数据,而不是让他们处于黑暗中。
我们应该允许每个人保持他们的隐私 - 即他们控制自己的数据 - 而不是通过监视来窃取他们的控制权。我们控制数据的个人权利就像是一个国家公园的自然环境:如果我们没有明确的保护和关心,它将被破坏。这将是公地的悲剧,我们都会因此而变得更糟。无所不在的监视不是不可避免的,我们仍然能够阻止它。
我们究竟能做到这一点是一个悬而未决的问题。首先,我们不应该永久保留数据,但一旦不再需要就立即清除数据[111,112]。清除数据与不变性的想法背道而驰请参阅第463页的“不变性的限制”但可以解决该问题。我所看到的一种很有前途的方法是通过加密协议来实施访问控制而不仅仅是通过策略[113,114]。总的来说,文化和态度的变化是必要的。
## 本章小结
在本章中,我们讨论了设计数据系统的新方法,并且包括了我对未来的个人意见和猜测。我们从观察开始,即没有一种工具可以有效地服务于所有可能的用例,因此应用程序必须编写几个不同的软件才能实现其目标。我们讨论了如何使用批处理和事件流来解决这个数据集成问题,以便让数据变化在不同系统之间流动。
在这种方法中,某些系统被指定为记录系统,而其他数据则通过转换从中得出。通过这种方式,我们可以维护索引,物化视图,机器学习模型,统计摘要等等。通过使这些派生和转换异步和松散耦合,防止一个区域中的问题扩散到系统的不相关部分,从而增加整个系统的稳健性和容错性。
将数据流表示为从一个数据集到另一个数据集的转换也有助于演化应用程序:如果您想更改其中一个处理步骤,例如更改索引或缓存的结构,则可以在整个输入数据集上重新运行新的转换代码以便重新输出。同样,如果出现问题,您可以修复代码并重新处理数据以便恢复。
这些过程与内部数据库已经完成的过程非常相似,因此我们重新构思了数据流应用程序的概念,将数据库的组件分开,并通过组合这些松散耦合的组件来构建应用程序。
派生状态可以通过观察底层数据的变化来更新。此外,下游消费者可以进一步观察派生状态本身。我们甚至可以将此数据流一直传送到显示数据的最终用户设备,从而构建可动态更新以反映数据更改并继续脱机工作的用户界面。
接下来,我们讨论了如何确保所有这些处理在出现故障时保持正确。我们看到强大的完整性保证可以通过异步事件处理可视化地实现,通过使用端到端操作标识符使操作幂等并异步检查约束。客户可以等到检查通过,或者不用等待就行,但是可能会有违反约束的道歉风险。这种方法比使用分布式事务的传统方法更具可扩展性和可靠性,并且适合于实践中有多少业务流程工作。
通过构建围绕数据流的应用程序并同步检查约束条件,我们可以避免大多数协调,并创建维护完整性但仍能很好地运行的系统,即使在地理分布的情况下和出现故障时也是如此。然后,我们谈了一些关于使用审计来验证数据的完整性并检测腐败的问题。
最后,我们退后一步,审查了构建数据密集型应用程序的一些道德问题。我们看到,虽然数据可以用来做好事,但它也可能造成重大损害:作出严重影响人们生活并难以申诉的决定的正当理由,导致歧视和剥削,规范监督以及揭露私密信息。我们也冒着数据泄露的风险,并且我们可能会发现善意使用数据会产生意想不到的后果。
由于软件和数据对世界产生如此巨大的影响,我们的工程师们必须记住,我们有责任为我们想要生活的这个世界努力工作:一个对待人性与尊重的世界。我希望我们能够一起为实现这一目标而努力。
## 参考文献
@ -485,3 +1316,12 @@
1. Phillip Rogaway:
“[The Moral Character of Cryptographic Work](http://web.cs.ucdavis.edu/~rogaway/papers/moral-fn.pdf),” Cryptology ePrint 2015/1162, December 2015.
------
| 上一章 | 目录 | 下一章 |
| --------------------------- | ------------------------------- | ------------------- |
| [第十一章:流处理](ch11.md) | [设计数据密集型应用](README.md) | [后记](colophon.md) |