第十二章:数据系统的未来


作者:负雪明烛 时间:2022 年 1 月 19 日 image.png 之前的章节在讨论现状; 本章讨论未来。 我们的目标是,发现如何设计出比现有应用更好的应用 —— 健壮,正确,可演化,且最终对人类有益。

数据集成

  1. 每个工具都有适用场景
  2. 在复杂的应用中,数据的用法通常花样百出。不太可能存在适用于所有不同数据应用场景的软件,因此您不可避免地需要拼凑几个不同的软件来以提供应用所需的功能。

组合使用衍生数据的工具

随着数据不同表示形式的增加,集成问题变得越来越困难。

  • 除了数据库和搜索索引之外,也许你需要在分析系统(数据仓库,或批处理和流处理系统)中维护数据副本;
  • 维护从原始数据中衍生的缓存,或反规范化的数据版本;
  • 将数据灌入机器学习,分类,排名,或推荐系统中;
  • 或者基于数据变更发送通知。

某人认为鸡肋而毫无意义的功能可能是别人的核心需求。当你拉高视角,并考虑跨越整个组织范围的数据流时,数据集成的需求往往就会变得明显起来。

理解数据流

当需要在多个存储系统中维护相同数据的副本以满足不同的访问模式时,你要对输入和输出了如指掌:

  • 哪些数据先写入,哪些数据表示衍生自哪些来源?
  • 如何以正确的格式,将所有数据导入正确的地方?

几种写入方式:

  • 如果变更数据捕获(CDC)是更新索引的唯一方式,则可以确定该索引完全派生自记录系统,因此与其保持一致(除软件错误外)。
  • 允许应用程序直接写入搜索索引和数据库引入了如图11-4所示的问题。
  • 基于事件日志来更新衍生数据的系统,通常可以做到确定性幂等性(请参阅幂等性”),使得从故障中恢复相当容易。

衍生数据与分布式事务

与分布式事务相比,使用衍生数据系统的方法如何?

  • 在抽象层面,它们通过不同的方式达到类似的目标。
    • 分布式事务通过进行互斥来决定写入的顺序,而CDC和事件溯源使用日志进行排序。
    • 分布式事务使用原子提交来确保变更只生效一次,而基于日志的系统通常基于确定性重试幂等性
  • 最大的不同之处在于事务系统通常提供线性一致性,这包含着有用的保证,例如读己之写。另一方面,衍生数据系统通常是异步更新的,因此它们默认不会提供相同的时序保证。

全序的限制

对于足够小的系统,构建一个完全有序的事件日志是完全可行的(如单主数据库)。但是大系统会出现麻烦:

  • 在大多数情况下,构建完全有序的日志,需要所有事件汇集于决定顺序的单个领导者节点
    • 如果事件吞吐量大于单台计算机的处理能力,则需要将其分区到多台计算机上(请参阅“分区日志”)。然后两个不同分区中的事件顺序关系就不明确了。
  • 如果服务器分布在多个地理位置分散的数据中心上,例如为了容忍整个数据中心掉线,您通常在每个数据中心都有单独的主库,因为网络延迟会导致同步的跨数据中心协调效率低下。
    • 这意味着源自两个不同数据中心的事件顺序未定义。
  • 将应用程序部署为微服务时,常见的设计选择是将每个服务及其持久状态作为独立单元进行部署,服务之间不共享持久状态。
    • 当两个事件来自不同的服务时,这些事件间的顺序未定义。
  • 某些应用程序在客户端保存状态,该状态在用户输入时立即更新(无需等待服务器确认),甚至可以继续脱机工作。
    • 对于这样的应用程序,客户端和服务器很可能以不同的顺序看到事件。

共识算法存在的问题?

  • 在形式上,决定事件的全局顺序称为全序广播,相当于共识。
  • 大多数共识算法都是针对单个节点的吞吐量足以处理整个事件流的情况而设计的,并且这些算法不提供多个节点共享事件排序工作的机制。设计可以伸缩至单个节点的吞吐量之上,且在地理位置分散的环境中仍然工作良好的的共识算法仍然是一个开放的研究问题。

排序事件以捕获因果关系

  • 在事件之间不存在因果关系的情况下,全序的缺乏并不是一个大问题,因为并发事件可以任意排序。
  • 但是能够有效地捕获因果依赖关系,并且保持正确的衍生状态,仍是个难题。(文中举了先解除好友关系,然后发送信息的例子)

批处理与流处理

数据集成的目标是,确保数据最终能在所有正确的地方表现出正确的形式。 这样做需要消费输入,转换,连接,过滤,聚合,训练模型,评估,以及最终写出适当的输出。 批处理和流处理是实现这一目标的工具。

维护衍生状态

  • 批处理有着很强的函数式风格(即使其代码不是用函数式语言编写的);
  • 为了提高速度,可以使用写入数据和更新索引可以使用异步;出现问题时,再报错。

应用演化后重新处理数据

如何迁移系统?

  • 衍生视图允许渐进演化(gradual evolution)
  • 如果你想重新构建数据集,不需要执行突然切换式的迁移。
  • 取而代之的是,你可以将旧架构和新架构并排维护为相同基础数据上的两个独立衍生视图。
  • 然后可以开始将少量用户转移到新视图,以测试其性能并发现任何错误,而大多数用户仍然会被路由到旧视图。
  • 你可以逐渐地增加访问新视图的用户比例,最终可以删除旧视图。

Lambda架构

什么是 Lambda 架构?

  • 融合了批处理和流处理。
  • Lambda架构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件溯源。
  • 为了从这些事件中衍生出读取优化的视图, Lambda架构建议并行运行两个不同的系统:批处理系统(如Hadoop MapReduce)和独立的流处理系统(如Storm)。
  • 在Lambda方法中,流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成衍生视图的更正版本。
  • 这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。

缺点?

  • 批处理和流处理框架中维护相同的逻辑是很显著的额外工作。
  • 批流两个独立的输出,合并可能会困难。
  • 批处理开销巨大。

统一批处理和流处理

最近的工作使得 Lambda 架构摒弃缺点。 在一个系统中统一批处理和流处理需要以下功能,这些功能也正在越来越广泛地被提供:

  • 通过处理最近事件流的相同处理引擎来重播历史事件的能力。例如,基于日志的消息代理可以重播消息,某些流处理器可以从HDFS等分布式文件系统读取输入。
  • 对于流处理器来说,恰好一次语义 —— 即确保输出与未发生故障的输出相同,即使事实上发生故障。与批处理一样,这需要丢弃任何失败任务的部分输出。
  • 按事件时间进行窗口化的工具,而不是按处理时间进行窗口化,因为处理历史事件时,处理时间毫无意义。例如,Apache Beam提供了用于表达这种计算的API,可以在Apache Flink或Google Cloud Dataflow使用。

分拆数据库

Unix发展出的管道和文件只是字节序列,而数据库则发展出了SQL和事务。 这两种哲学的矛盾,希望能各取其美。

组合使用数据存储技术

在本书的过程中,我们讨论了数据库提供的各种功能及其工作原理,其中包括:

  • 次级索引,使您可以根据字段的值有效地搜索记录
  • 物化视图,这是一种预计算的查询结果缓存
  • 复制日志,保持其他节点上数据的副本最新
  • 全文搜索索引,允许在文本中进行关键字搜索,也内置于某些关系数据库

创建索引

  • 无论何时运行CREATE INDEX,数据库都会重新处理现有数据集(如“应用演化后重新处理数据”中所述),并将该索引作为新视图导出到现有数据上。
  • 现有数据可能是状态的快照,而不是所有发生变化的日志,但两者密切相关(请参阅“状态、流和不变性”)。

一切的元数据库

各种数据库产品发展在未来将会把我们带到哪里?如果我们从没有适合所有访问模式的单一数据模型或存储格式的前提出发,我推测有两种途径可以将不同的存储和处理工具组合成一个有凝聚力的系统: 联合数据库:统一读取

  • 可以为各种各样的底层存储引擎和处理方法提供一个统一的查询接口 —— 一种称为联合数据库(federated database)多态存储(polystore) 的方法。
  • 例如,PostgreSQL的外部数据包装器(foreign data wrapper) 功能符合这种模式。
  • 需要专用数据模型或查询接口的应用程序仍然可以直接访问底层存储引擎,而想要组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。

分拆数据库:统一写入

  • 虽然联合能解决跨多个不同系统的只读查询问题,但它并没有很好的解决跨系统同步写入的问题。
  • 我们说过,在单个数据库中,创建一致的索引是一项内置功能。
  • 当我们构建多个存储系统时,我们同样需要确保所有数据变更都会在所有正确的位置结束,即使在出现故障时也是如此。
  • 想要更容易地将存储系统可靠地插接在一起(例如,通过变更数据捕获和事件日志),就像将数据库的索引维护功能以可以跨不同技术同步写入的方式分开。

开展分拆工作

怎么写入到几个存储系统中?

  • 传统的同步写入方法需要跨异构存储系统的分布式事务,我认为这是错误的解决方案
  • 单个存储或流处理系统内的事务是可行的,但是当数据跨越不同技术之间的边界时,我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法

基于日志的集成的一大优势是各个组件之间的松散耦合(loose coupling),这体现在两个方面:

  1. 在系统级别,异步事件流使整个系统在个别组件的中断或性能下降时更加稳健。
  2. 在人力方面,分拆数据系统允许不同的团队独立开发,改进和维护不同的软件组件和服务。

分拆系统vs集成系统

什么时候拆分系统,什么时候集成系统?

  • 分拆的目标不是要针对个别数据库与特定工作负载的性能进行竞争;我们的目标是允许您结合多个不同的数据库,以便在比单个软件可能实现的更广泛的工作负载范围内实现更好的性能。
  • 因此,如果有一项技术可以满足您的所有需求,那么最好使用该产品,而不是试图用更低层级的组件重新实现它。只有当没有单一软件满足您的所有需求时,才会出现拆分和联合的优势。

少了什么?

用于组成数据系统的工具正在变得越来越好,但我认为还缺少一个主要的东西:我们还没有与Unix shell类似的分拆数据库等价物(即,一种声明式的、简单的、用于组装存储和处理系统的高级语言)。

  • 如果我们可以简单地声明mysql | elasticsearch,类似于Unix管道,成为CREATE INDEX的分拆等价,那就爽了!

围绕数据流设计应用

即使是电子表格也在数据流编程能力上甩开大多数主流编程语言几条街:某一个单元格可以使用公式对其他列求和。

应用代码作为衍生函数

当一个数据集衍生自另一个数据集时,它会经历某种转换函数。如:

  • 次级索引是由一种直白的转换函数生成的衍生数据集:对某列或者字段的值排序建索引。
  • 全文搜索索引是通过应用各种自然语言处理函数而创建的
  • 在机器学习系统中,我们可以将模型视作从训练数据通过应用各种特征提取、统计分析函数衍生的数据
  • 缓存通常包含将以用户界面(UI)显示的形式的数据聚合。

但是,当创建衍生数据集的函数不是像创建二级索引那样的标准函数时,需要自定义代码来处理特定于应用的东西,数据库就做不好了!!

应用代码和状态的分离

为什么要让计算和存储分离?

  • 让系统的某些部分专门用于持久数据存储并让其他部分专门运行应用程序代码是有意义的。这两者可以在保持独立的同时互动。
  • 比如大多数Web应用程序都是作为无状态服务部署的,数据库存储数据。
  • 这样可以独立扩缩容,互不影响。

数据流:应用代码与状态变化的交互

应用代码和状态怎么联系?

  • 从数据流的角度思考应用程序,意味着重新协调应用代码和状态管理之间的关系。
  • 我们不再将数据库视作被应用操纵的被动变量,取而代之的是更多地考虑状态,状态变更和处理它们的代码之间的相互作用与协同关系
  • 应用代码通过在另一个地方触发状态变更来响应状态变更。

维护衍生数据不同于执行异步任务:

  • 传统的消息传递系统通常是为执行异步任务设计的
    • 状态变更的顺序
    • 容错
  • 稳定的消息排序和容错消息处理是相当严格的要求,但与分布式事务相比,它们开销更小,运行更稳定。
  • 现代流处理组件可以提供这些排序和可靠性保证,并允许应用代码以流算子的形式运行。

流处理器和服务

服务的优点?

  • 当今流行的应用开发风格涉及将功能分解为一组通过同步网络请求(如REST API)进行通信的服务(service)
  • 优点:
    • 通过松散耦合来提供组织上的可伸缩性:不同的团队可以专职于不同的服务上,从而减少团队之间的协调工作(因为服务可以独立部署和更新)。

数据流中组装流算子与微服务方法的区别:

  • 数据流采用单向异步消息流,而不是同步的请求/响应式交互。

数据流系统的优点:

  • 除了在“消息传递中的数据流”中列出的优点(如更好的容错性),数据流系统还能实现更好的性能。
    • 如:把远程服务器的变更同步到本地
  • 订阅变更流,而不是在需要时查询当前状态,是一个非常有希望的方向

观察衍生数据状态

什么是写路径?

  • 上一节讨论的数据流系统提供了创建衍生数据集(例如搜索索引、物化视图和预测模型)并使其保持更新的过程。
  • 我们将这个过程称为写路径(write path):只要某些信息被写入系统,它可能会经历批处理与流处理的多个阶段,而最终每个衍生数据集都会被更新,以适配写入的数据。

更新搜索索引的例子: image.png 什么是读路径?

  • 读路径(read path):当服务用户请求时,你需要从衍生数据集中读取,也许还要对结果进行一些额外处理,然后构建给用户的响应。

写路径和读路径的区别于联系:

  • 如果你熟悉函数式编程语言,则可能会注意到写路径类似于立即求值,读路径类似于惰性求值。

物化视图和缓存

如何加速查询?

  • 选择一:构建索引。读路径在索引中搜索关键字。
  • 选择二:缓存。只为一组固定的最常见的查询预先计算搜索结果,以便它们可以快速地服务而不必去走索引。
    • 不常见的查询仍然可以通过索引来提供服务。这通常被称为常见查询的缓存(cache),尽管我们也可以称之为物化视图(materialized view),因为当新文档出现,且需要被包含在这些常见查询的搜索结果之中时,这些索引就需要更新。

这些操作起到了什么作用?

  • 缓存,索引和物化视图的作用很简单:它们改变了读路径与写路径之间的边界。通过预先计算结果,从而允许我们在写路径上做更多的工作,以节省读路径上的工作量。

有状态、可离线的客户端

为什么客户端有状态?

  • 以前的 web 时代,采用客户端/服务器模型 —— 客户端大多是无状态的,而服务器拥有数据的权威 —— 已经普遍到我们几乎忘掉了还有其他任何模型的存在。
  • 但是技术在不断地发展,我认为不时地质疑现状非常重要。
  • 现在客户端、web 浏览器都可以持久化本地存储,大多数交互不需要和服务端往返交互。

怎么做到客户端有状态?

  • 离线优先(offline-first) 应用:尽可能地在同一设备上使用本地数据库,无需连接互联网,并在后台网络连接可用时与远程服务器同步。
    • 优点:不用网络,速度快
  • 我们可以将设备上的状态视为服务器状态的缓存,屏幕上的像素是客户端应用中模型对象的物化视图;模型对象是远程数据中心的本地状态副本。

将状态变更推送给客户端

怎么做到服务器推送给客户端?

  • 以前的网页中,如果页面不刷新重新请求,就不知道服务器的更新。
  • 最新的协议支持服务端发送的事件(EventSource API)和WebSockets提供了通信信道,通过这些信道,Web浏览器可以与服务器保持打开的TCP连接,只要链接不断,服务器就可以给浏览器推送信息。

什么含义?

  • 主动将状态变更推至到客户端设备,意味着将写路径一直延伸到终端用户。
  • 当客户端首次初始化时,它仍然需要使用读路径来获取其初始状态,但此后它就能够依赖服务器发送的状态变更流了。
  • 有些设备会离线,但是基于日志的消息代理的消费者能在失败或断开连接后重连,并确保它不会错过掉线期间任何到达的消息。
  • 每个设备都是一个小事件流的小小订阅者。

端到端的事件流

编程语言支持了端到端的写路径:

  • 最近用于开发有状态的客户端与用户界面的工具,例如如Elm语言和Facebook的React,Flux和Redux工具链,已经通过订阅表示用户输入或服务器响应的事件流来管理客户端的内部状态,其结构与事件溯源相似。
  • 因此可以实现**端到端(end-to-end):**从一个设备的交互触发,经由事件日志,并穿过几个衍生数据系统与流处理器,一直到另一台设备上的用户界面,而有人正在观察用户界面上的状态变化。

为何不使用端到端?

  • 为了将写路径延伸至终端用户,我们需要从根本上重新思考我们构建这些老系统的方式:从请求/响应交互转向发布/订阅数据流。
  • 老系统的改动成本大
  • 新系统可以考虑!

读也是事件

为什么说读也是事件?

  • 当写入和读取都被表示为事件,并且被路由到同一个流算子以便处理时,我们实际上是在读取查询流和数据库之间执行流表连接
  • 服务请求与执行连接之间的这种相似之处是非常关键的。一次性读取请求只是将请求传过连接算子,然后请求马上就被忘掉了;而一个订阅请求,则是与连接另一侧过去与未来事件的持久化连接。
  • 记录读取事件的日志可能对于追踪整个系统中的因果关系与数据来源也有好处:它可以让你重现出当用户做出特定决策之前看见了什么。
  • 将读取事件写入持久存储可以更好地跟踪因果关系(请参阅“排序事件以捕获因果关系”),但会产生额外的存储与I/O成本。如果需要的话,那么是值得的。

多分区数据处理

为什么要有多分区数据处理?

  • 对于只涉及单个分区的查询,通过流来发送查询与收集响应可能是杀鸡用牛刀了。
  • 然而,这个想法开启了分布式执行复杂查询的可能性,这需要合并来自多个分区的数据,利用了流处理器已经提供的消息路由、分区和连接的基础设施。

多分区数据处理举例?

  • Twitter 用 Storm 统计多个用户分区点击了某个 URL 的人数
  • 信用卡反欺诈,需要读取多个数据库的分区;
  • MPP 数据库内部也有多分区连接功能,但是比流处理器更简单。

将事情做正确

为什么需要正确?

  • 只读数据的无状态服务,出问题可以重启;
  • 数据库这样的有状态系统,出问题后只能永远的持续下去,因此需要仔细的思考。
    • 使用事务保证正确

数据库的端到端原则

虽然数据库有较强的可靠安全属性,但是并不能保证数据没有损失或损坏:还要考虑应用代码。

正好执行一次操作

举一个可能发生数据损坏的例子:

  • 我们见到了恰好一次(或等效一次)语义的概念。
  • 如果在处理消息时出现问题,你可以选择放弃(丢弃消息 —— 导致数据丢失)或重试。
  • 如果重试,就会有这种风险:第一次实际上成功了,只不过你没有发现。结果这个消息就被处理了两次。(导致收费两次)

如何解决?

  • 最有效的方法之一是使操作**幂等(idempotent),**即确保它无论是执行一次还是执行多次都具有相同的效果。
  • 实现幂等需要一些额外的努力。

抑制重复

怎么抑制重复?

  • 除了流处理,还有很多地方需要抑制重复:如 TCP 重传中的序列号。
  • 我们即使能抑制数据库的重复事务,但是仍要担心网络重传。

下面的例子,如果客户端没收到转账成功的响应,那么可能再次提交,导致转了 22$。

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;

操作标识符

不能依赖网络实现幂等,需要考虑端到端(end-to-end) 的请求流。

  • 比如,你可以为操作生成一个唯一的标识符(例如UUID),每次操作的时候带上。
  • 服务端如果发现两次同样的标识符,那么就不操作。

在数据库上可以对此标识符进行唯一的标记,防止重复插入:

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;

端到端原则

抑制重复事务的这种情况只是一个更普遍的原则的一个例子,这个原则被称为端到端原则(end-to-end argument):

  • 只有在通信系统两端应用的知识与帮助下,所讨论的功能才能完全地正确地实现。因而将这种被质疑的功能作为通信系统本身的功能是不可能的。 (有时,通信系统可以提供这种功能的不完备版本,可能有助于提高性能。)

只有端到端的安全,才能抑制重复事务。 我们只需要记住,低级别的可靠性功能本身并不足以确保端到端的正确性。

在数据系统中应用端到端思考

  • 事务是很好的抽象,也有用,但是还不够端到端的安全。
  • 事务代价高昂,当涉及异构存储技术时尤为甚。
  • 我们拒绝使用分布式事务是因为它开销太大,结果我们最后不得不在应用代码中重新实现容错机制。

强制约束

在端到端去重之外,我们来关注其他的约数。特别关注一下唯一性约束。

唯一性约束需要达成共识

怎么实现?

  • 在分布式环境中,强制执行唯一性约束需要共识:如果存在多个具有相同值的并发请求,则系统需要决定冲突操作中的哪一个被接受,并拒绝其他违背约束的操作。
  • 达成这一共识的最常见方式是使单个节点作为领导,并使其负责所有决策。
  • 唯一性检查可以通过对唯一性字段分区做横向伸缩,如通过哈希分区。
  • 但异步多主复制排除在外,因为可能会发生不同主库同时接受冲突写操作的情况,因而这些值不再是唯一的。此时同步协调是无法避免的。

基于日志消息传递中的唯一性

怎么使用日志消息传递中实现唯一性?

  • 日志确保所有消费者以相同的顺序看见消息 —— 这种保证在形式上被称为全序广播(total order boardcast) 并且等价于共识。
  • 类似于分拆数据库的方法。
  • 流处理器在单个线程上依次消费单个日志分区中的所有消息。因此,如果日志是按需要确保唯一的值做的分区,则流处理器可以无歧义地、确定性地决定几个冲突操作中的哪一个先到达。

多分区请求处理

当涉及多个分区时,确保操作以原子方式执行且同时满足约束就变得很有趣了。

如果一个操作需要三个分区:一个包含请求ID,一个包含收款人账户,另一个包含付款人账户,咋办?

  • 在数据库的传统方法中,执行此事务需要跨全部三个分区进行原子提交,就这些分区上的所有其他事务而言,这实质上是将该事务嵌入一个全序。
  • 缺点:影响性能。

有没有更好的方法? 但事实证明,使用分区日志可以达到等价的正确性而无需原子提交:

  1. 从账户A向账户B转账的请求由客户端提供一个唯一的请求ID,并按请求ID追加写入相应日志分区。
  2. 流处理器读取请求日志。对于每个请求消息,它向输出流发出两条消息:付款人账户A的借记指令(按A分区),收款人B的贷记指令(按B分区)。被发出的消息中会带有原始的请求ID。
  3. 后续处理器消费借记/贷记指令流,按照请求ID除重,并将变更应用至账户余额。

因为有确定的 ID 的存在,那么失败重启可以从断点恢复,如果重复提交可以用 ID 去重。

及时性与完整性

一致性要同步吗?

  • 事务的一个便利属性是,它们通常是线性一致的:写入者等待写入完成;
  • 而跨越多个阶段的流处理器时,却并非如此:日志的消费者在设计上就是异步的,因此发送者不会等其消息被消费者处理完。

更一般地来讲,我认为术语一致性(consistency) 这个术语混淆了两个值得分别考虑的需求: 及时性(Timeliness)

  • 及时性意味着确保用户观察到系统的最新状态。
  • 用户从旧数据中读取,那么可能观察到系统处于不一致的状态。但这是暂时的。
  • CAP定理使用线性一致性(linearizability) 意义上的一致性,这是实现及时性的强有力方法。像写后读这样及时性更弱的一致性也很有用也很有用。

完整性(Integrity) 完整性意味着没有损坏;即没有数据丢失,并且没有矛盾或错误的数据。衍生数据也必须都正确。

结论:

  • 口号形式:违反及时性,“最终一致性”;违反完整性,“永无一致性”。
  • 我断言在大多数应用中,完整性比及时性重要得多。违反及时性可能令人困惑与讨厌,但违反完整性的结果可能是灾难性的。

数据流系统的正确性

数据库、流系统的正确性?

  • ACID事务通常既提供及时性(例如线性一致性)也提供完整性保证(例如原子提交)。因此如果你从ACID事务的角度来看待应用的正确性,那么及时性与完整性的区别是无关紧要的。
  • 基于事件的数据流系统而言,它们的一个有趣特性就是将及时性与完整性分开。在异步处理事件流时不能保证及时性,除非你显式构建一个在返回之前明确等待特定消息到达的消费者。但完整性实际上才是流处理系统的核心。

怎么保证完整性?

  • 恰好一次等效一次语义(请参阅“容错”)是一种保持完整性的机制。
  • 如果事件丢失或者生效两次,就有可能违背数据系统的完整性。
  • 因此在出现故障时,容错消息传递与重复抑制(例如,幂等操作)对于维护数据系统的完整性是很重要的。

为了达成这种正确性,我们组合使用了多种机制:

  • 将写入操作的内容表示为单条消息,从而可以轻松地被原子写入 —— 与事件溯源搭配效果拔群(请参阅“事件溯源”)。
  • 使用与存储过程类似的确定性衍生函数,从这一消息中衍生出所有其他的状态变更(请参阅“真的串行执行”和“应用代码作为衍生函数”)
  • 将客户端生成的请求ID传递通过所有的处理层次,从而允许端到端的除重,带来幂等性。
  • 使消息不可变,并允许衍生数据能随时被重新处理,这使从错误中恢复更加容易(请参阅“不可变事件的优点”)

宽松地解释约束

在执行唯一性约束时,许多真实世界的应用实际上可以摆脱这种形式,接受弱得多的唯一性:

  • 给客户道歉,补偿,给折扣

无协调数据系统

两个现象:

  1. 数据流系统可以维持衍生数据的完整性保证,而无需原子提交、线性一致性或者同步的跨分区协调。
  2. 虽然严格的唯一性约束要求及时性和协调,但许多应用实际上可以接受宽松的约束:只要整个过程保持完整性,这些约束可能会被临时违反并在稍后被修复。

总之:

  • 数据流系统可以为许多应用提供无需协调的数据管理服务,且仍能给出很强的完整性保证。
  • 这种无协调(coordination-avoiding) 的数据系统有着很大的吸引力:比起需要执行同步协调的系统,它们能达到更好的性能与更强的容错能力。

所以:

  • 我们可以在一小部分中使用同步协调,没必要让所有操作都付出协调代价。
  • 异构分布式事务,不是必须的。

另一种协调与约束的角度是:

  • 它们减少了由于不一致而必须做出的道歉数量,但也可能会降低系统的性能和可用性,从而可能增加由于宕机中断而需要做出的道歉数量。
  • 自己平衡点。

信任但验证

我们所有关于正确性,完整性和容错的讨论都基于一些假设,假设某些事情可能会出错,但其他事情不会。我们将这些假设称为我们的系统模型(system model)。 这些假设相当合理,但是错误是个概率问题。

维护完整性,尽管软件有Bug

软件有存在Bug的风险,Mysql 也可能有 bug。 ACID 也可能有 bug。 错误使用数据库,也可能导致数据出问题。

不要盲目信任承诺

数据有概率损坏,检查数据完整性称为审计(auditing) 定期审计数据和备份。

验证的文化

  • 我希望未来能看到更多的自我验证(self-validating)自我审计(self-auditing) 系统,不断检查自己的完整性,而不是依赖盲目的信任。
  • 我担心ACID数据库的文化导致我们在盲目信任技术(如事务机制)的基础上开发应用,而忽视了这种过程中的任何可审计性。由于我们所信任的技术在大多数情况下工作得很好,通常会认为审计机制并不值得投资。
  • 在NoSQL的旗帜下,更弱的一致性保证成为常态,更不成熟的存储技术越来越被广泛使用。
  • 如果没有审计,越来越危险。

为可审计性而设计

  • 事务操作发生后,很难说清楚意味着什么。
  • 相比之下,基于事件的系统可以提供更好的可审计性。
  • 显式处理数据流可以使数据的来龙去脉(provenance) 更加清晰,从而使完整性检查更具可行性。
  • 具有确定性且定义良好的数据流,也使调试与跟踪系统的执行变得容易,以便确定它为什么做了某些事情

端到端原则重现

  • 检查数据系统的完整性,最好是以端到端的方式进行
  • 持续的端到端完整性检查可以不断提高你对系统正确性的信心,从而使你能更快地进步

用于可审计数据系统的工具

  • 目前,将可审计性作为顶层关注点的数据系统并不多。
  • 使用密码学工具来证明系统的完整性是十分有趣的,现在有些加密货币、区块链技术出现在这个领域。
  • 密码学审计与完整性检查通常依赖默克尔树(Merkle tree),这是一颗散列值的树,能够用于高效地证明一条记录出现在一个数据集中(以及其他一些特性)。
  • 除了炒作的沸沸扬扬的加密货币之外,证书透明性(certificate transparency) 也是一种依赖Merkle树的安全技术,用来检查TLS/SSL证书的有效性。
  • 这是值得关注的有趣领域。

做正确的事情

  • 每个系统都服务于一个目的;我们,建立这些系统的工程师,有责任去仔细考虑这些后果,并有意识地决定,我们希望生活在怎样的世界中。
  • 许多数据集都是关于人的:他们的行为,他们的兴趣,他们的身份。对待这些数据,我们必须怀着人性与尊重。用户也是人类,人类的尊严是至关重要的。
  • 技术本身并无好坏之分 —— 关键在于它被如何使用,以及它如何影响人们。
  • 软件工程师仅仅专注于技术而忽视其后果是不够的:道德责任也是我们的责任。对道德推理很困难,但它太重要了,我们无法忽视。

预测性分析

  • 预测性分析是“大数据”炒作的主要内容之一。
  • 如果算法决策把某个人判定为有风险的人,那么就会带来很大风险。
  • 无罪推定。

偏见与歧视

预测性分析系统只是基于过去进行推断;

  • 如果过去是歧视性的,它们就会将这种歧视归纳为规律。
  • 如果我们希望未来比过去更好,那么就需要道德想象力,而这是只有人类才能提供的东西。
  • 数据与模型应该是我们的工具,而不是我们的主人。

责任与问责

  • 自动决策引发了关于责任与问责的问题:算法犯错误,谁来负责?比如自动驾驶。
  • 很多数据本质上是统计性的,这意味着即使概率分布在总体上是正确的,对于个例也可能是错误的。
  • 盲目相信数据决策至高无上,这不仅仅是一种妄想,而是有切实危险的。
  • 我们还需要想清楚,如何避免数据被用于害人,如何认识数据的积极潜力。

反馈循环

推荐算法,使人进入信息茧房、回音室; 反馈算法,导致恶性循环,增大贫富差距。

隐私和追踪

  • 当系统只存储用户明确输入的数据时,是因为用户希望系统以特定方式存储和处理这些数据,系统是在为用户提供服务:用户就是客户。
  • 但是如广告这样的商业模式,通过隐私和追踪获利。
  • 相当于监视(surveilance)

监视

并不是所有的数据收集都称得上监视,但检视这一点有助于理解我们与数据收集者之间的关系。

同意与选择的自由

  • 而且从用户身上挖掘数据是一个单向过程,而不是真正的互惠关系,也不是公平的价值交换。
  • 因为一个服务会跟踪用户而拒绝使用它,这只是少数人才拥有的权力,他们有足够的时间与知识来了解隐私政策,并承受得起代价:错过社会参与,以及使用服务可能带来的专业机会。
  • 对于那些处境不太好的人而言,并没有真正意义上的选择:监控是不可避免的。

隐私与数据使用

  • 隐私权是一项决定权:在从保密到透明的光谱上,隐私使得每个人都能决定自己想要在什么地方位于光谱上的哪个位置。这是一个人自由与自主的重要方面。
  • 决定向谁透露什么并不是由个体按照自己的喜好决定的,而是由公司,以利润最大化为目标来行使隐私权的。
  • 这种从个体到公司的大规模隐私权转移在历史上是史无前例的。

数据资产与权力

  • 因为数据很有价值,所以很多人都想要它。
  • 当然,公司也想要它 —— 这就是为什么它们一开始就收集数据的原因。
  • 但政府也想获得它:通过秘密交易、胁迫、法律强制或者只是窃取。
  • 俗话说,“知识就是力量”。更进一步,“在避免自己被审视的同时审视他人,是权力最重要的形式之一”

回顾工业革命

  • 就像工业革命有着黑暗面需要应对一样,我们转向信息时代的过程中,也有需要应对与解决的重大问题。
  • 我相信数据的收集与使用就是其中一个问题。

立法与自律

  • 那些收集了大量有关人的数据的公司反对监管,认为这是创新的负担与阻碍。
  • 我们应该允许每个人保留自己的隐私 —— 即,对自己数据的控制 —— 而不是通过监视来窃取这种控制权。
  • 我们究竟能做到哪一步,是一个开放的问题。
  • 总的来说,文化与态度的改变是必要的。

本章小结

没有单种工具能高效服务所有可能的用例,因此应用必须组合使用几种不同的软件才能实现其目标。

  • 我们讨论了如何使用批处理与事件流来解决这一数据集成(data integration) 问题,以便让数据变更在不同系统之间流动。

通过使这些衍生和转换操作异步且松散耦合,能够防止一个区域中的问题扩散到系统中不相关部分,从而增加整个系统的稳健性与容错性。 将数据流表示为从一个数据集到另一个数据集的转换也有助于演化应用程序。 这些过程与数据库内部已经完成的过程非常类似,因此我们将数据流应用的概念重新改写为,分拆(unbundling) 数据库组件,并通过组合这些松散耦合的组件来构建应用程序。 衍生状态可以通过观察底层数据的变更来更新。 接下来,我们讨论了如何确保所有这些处理在出现故障时保持正确。 通过围绕数据流构建应用,并异步检查约束,我们可以避免绝大多数的协调工作,创建保证完整性且性能仍然表现良好的系统, 最后,我们退后一步,审视了构建数据密集型应用的一些道德问题。

由于软件和数据对世界产生了如此巨大的影响,我们工程师们必须牢记,我们有责任为我们想要的那种世界而努力:一个尊重人们,尊重人性的世界。我希望我们能够一起为实现这一目标而努力。