数据系统的未来

205 阅读58分钟

本文为《设计数据密集型应用》最后一章笔记,本章中,作者放眼未来,提出了一些想法和方法。

1 数据集成

本书一直强调,对于任何给定的问题都会有好几种解决方案,所有这些解决方案都有不同的优缺点与利弊权衡。对于不同的具体环境,总会有不同的合适方法,软件实现通常必须选择一种特定的方法。

软件工具的最佳选择也取决于情况,每一种软件都是针对特定使用模式设计的,供应商不会告诉我们他软件的工作负载。我们需要弄清软件与其适用环境的映射关系。并且,复杂的应用中数据的用法花样百出,不太可能存在适用于所有不同数据应用场景的软件,因此需要拼凑不同软件来提供应用所需功能。

1.1 组合使用衍生数据的工具

为处理任意关键词的搜索查询,经常将OLTP数据库与全文搜索索引集成在一起,以获得更复杂的搜索能力。而搜索索引通常不适合作为持久的记录系统,因此许多应用需要组合这两种工具。

随着数据不同表示形式的增加,集成问题越来越困难(参阅”使系统保持同步“)。我们可能需要:在分析系统中维护数据副本、维护从原始数据中衍生的缓存、将数据灌入机器学习或推荐系统中、基于数据变更发送通知。

对于数据执行的操作来说,范围极其宽广,我认为鸡肋而毫无意义的功能可能是别人的核心需求。当我们拉高视角,考虑跨越整个组织范围的数据流时,数据集成的需求往往变得明显起来。

理解数据流

需要在多个存储系统中维护相同数据的副本以满足不同访问模式时,要对输入和输出了如指掌:数据来源、格式、去向。可能要先将数据写入记录数据库系统,捕获对该数据库所做的变更。

如果可以通过单个系统提供所有用户输入,从而决定所有写入顺序,则可以更容易衍生出其他数据表示。这是状态机复制方法的一个应用。我们可以看到,在全序广播中,无论使用变更数据捕获还是事件源日志,都不如全局顺序共识重要。

基于事件日志来更新衍生数据系统,通常可以做到确定性与幂等性。

衍生数据与分布式事务

保持不同数据系统彼此一致的经典方法涉及分布式事务,与之相比,使用衍生数据系统的方法如何?

分布式事务通过进行互斥来决定写入顺序(参阅”两阶段锁定“),而CDC和事件溯源使用日志进行排序。分布式事务使用原子提交来确保变更只生效一次,而基于日志的系统通常基于确定性重试幂等性

最大不同在于:事务系统通常提供线性一致性(保证了读己之写等)。而衍生数据系统通常异步更新,因此默认不会提供相同的时序保证。

XA的容错能力差到限制了实用性(参阅”实践中的分布式事务“)。未来需要有更好的分布式协议。在没有好协议的情况下,基于日志的衍生数据是集成不同数据系统的最有前途的方法。

全局有序的限制

对于足够小的系统,可以构建一个完全有序的事件日志。随着体量增大会开始出现问题:

  • 大多数情况下,构建完全有序日志,需要所有事件汇集到单个领导节点。为了提高吞吐量而分割到多台计算机上,会使分区中事件顺序不明确。
  • 若服务器分布在多个地理位置分散的数据中心上,网络延迟会导致同步的跨数据中心协调效率低下。源自两个数据中心的事件顺序未定义。
  • 应用部署为微服务时,一般将每个服务器及其持久状态作为独立单元进行部署,服务之间不共享持久状态。两个事件来自不同服务时,事件顺序未定义。
  • 某些应用在客户端保存状态,该状态在用户输入时立即更新,甚至可以继续脱机工作。有了这样的应用程序,客户端和服务器可能以不同顺序看到事件。

决定事件的全局顺序称为全序广播,相当于共识。大多数共识算法是针对单个节点吞吐量足以处理整个事件流的情况而设计的,无法给多个节点共享事件排序。未来需要能在分布式环境、大于单个节点的吞吐量的环境中,仍工作良好的共识算法。

排序事件以捕捉因果关系

事件间不存在因果关系的情况下,缺乏全局顺序是个小问题。但因果关系可能以更微妙方式出现,例如:好友关系与消息存储在不同地方,删除好友事件与发送消息事件之间因果依赖丢失。服务可能在删除好友事件之前处理发送消息事件,导致向被删除的好友发送信息。

  • 时间戳可以提供无需协调的全局顺序。但仍要求收件人处理不按顺序发送的事件,且需要传输其他元数据。
  • 若能记录一个事件来记录用户在做出决定前看到的系统状态,并给该事件一个唯一标识符,那么后面任何事件都可以引用该事件标识符来记录因果关系。(参阅”读也是事件“)
  • 冲突解决算法有助于处理以意外顺序传递的事件。(参阅”自动冲突解决“)

1.2 批处理与流处理

数据集成的目标是,确保数据最终能在所有正确地方表现出正确形式。这样做需要消费输入,转换,连接,过滤,聚合,训练模型,评估,以及最终写出适当的输出。批处理和流处理是实现这一目标的工具。批处理和流处理的输出是衍生数据集,例如搜索索引,物化视图,向用户显示的建议,聚合指标等。

批处理和流处理有许多共同原则,根本区别在于:流处理的数据集无限,而批处理的输入有限大小。处理引擎的实现方式上的区别已经开始模糊。

维护衍生状态

批处理有很强的函数式风格:鼓励确定性的纯函数,输出仅依赖输入且没有副作用,输入不可变输出仅追加。流处理与之类似,但扩展了算子以允许受管理的、容错的状态。

管道的思维是有帮助的:将函数视为一个东西衍生出另一个的数据管道,将一个系统的状态变更推送至函数式应用代码中,并将其效果应用至衍生系统中。利于容错且简化数据流推理。

虽然衍生数据系统可以同步地维护,但是异步是基于事件日志的系统文件稳健的原因:它允许系统一部分故障被抑制在本地,而如果任何一个参与者失败,分布式事务将中止。即通过将故障传播到系统其余部分来放大故障。

二级索引经常跨越分区边界(参阅”分区与次级索引“)。索引按照关键词分区时,需要将写入发送到多个分区或将读取发送到所有分区(索引按文档分区时)。若索引是异步维护的,这种交叉分区通信也是最可靠和最可扩展的。

应用演化后重新处理数据

维护衍生数据时,批处理和流处理都是有用的。流处理允许将输入中的变化低延迟反映在衍生视图中,批处理允许重新处理大量积累的历史数据以便将新视图导出到现有视图上。

重新处理现有数据,不仅使系统更好的演化并支持新功能和需求变更(参阅第四章),还可以将数据集重组为一个完全不同的模型,以便更好地满足新要求。

衍生视图允许渐进演化(gradual evolution) 。如果你想重新构建数据集,不需要执行迁移(例如突然切换),可以将旧架构和新架构并排维护为相同基础数据上的两个独立衍生视图,从而进行金丝雀发布

Lambda架构

若批处理用于重新处理历史数据,并且流处理用于处理最近更新,那么如何将这两者结合起来?Lambda架构是这方面的一个建议。

Lambda架构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件溯源。为了从这些事件中衍生出读取优化的视图,架构并行运行了批处理和流处理系统。

流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成衍生视图的更正版本。这么设计的原因是,批处理简单且不易出错,流处理器不靠谱且难以容错。而且,流处理可以使用快速近似算法,批处理可以用较慢的精确算法。

Lambda架构将数据系统设计变得更好,它的原则是:不可变事件流上建立衍生视图,并在需要时重新处理事件。

但它也有一些实际问题:

  • 在批处理和流处理框架中维护相同的逻辑是显著的额外工作。
  • 流管道和批处理各自产生独立的输出,需要合并它们以响应用户请求。
  • 在大型数据集上重新处理整个历史数据集开销巨大。批处理流水线通常要设置为处理增量批处理而不是重新处理所有内容。而增加批量计算会增加复杂性,使其更像流式传输,这与批处理保持尽可能简单的目标背道而驰。
统一批处理和流处理

Lambda架构的升级使得批处理计算和流计算在同一个系统中实现。这需要以下功能:

  • 通过处理最近事件流的相同处理引擎来重放历史事件的能力。
  • 对于流处理来说,恰好一次语义与批处理一样,这需要丢弃任何失败任务的部分输出。
  • 按事件时间进行窗口化的工具,而不是按处理时间进行窗口化。因为处理历史事件时,处理时间毫无意义。

2 分拆数据库

在最抽象的层面上,数据库、Hadoop和操作系统发挥相同的功能:存储一些数据,允许处理和查询这些数据。它们之间的相似和差异值得探讨。

Unix和关系数据库以非常不同的哲学来处理信息管理问题。Unix目的是提供一种相当低层次的硬件的逻辑抽象,而关系数据库则希望提供一种高层次的抽象,以隐藏磁盘上数据结构的复杂性、并发性、崩溃恢复等等。Unix发展出的管道和文件只是字节序列,而数据库发展出了SQL和事务。

Unix是”简单的“,它是硬件资源相当薄的包装;关系数据库是”更简单“的,因为一个简短的声明性查询可以利用很多强大的基础设施而查询者不需要理解实现细节。

2.1 组合使用数据存储技术

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

  • 次级索引(参阅”其他索引结构“)
  • 物化视图(参阅”聚合:数据立方体和物化视图“)
  • 复制日志(参阅”复制日志的实现“)
  • 全文搜索引擎(参阅”全文搜索和模糊索引“)

在10、11章中讨论了类似主题:如何构建全文搜索索引(参阅”批处理工作流的输出“)、了解有关实例化视图维护(参阅”维护实例化视图“)、有关将变更从数据库复制到衍生数据系统(参阅”变更数据捕获“)。数据库中内置的功能与人们用批处理和流处理器构建的衍生数据系统似乎有相似之处。

创建索引

数据库创建索引的过程类似于设置新的从库副本(参阅"设置新的数据副本"),也非常类似于流处理系统中的引导(bootstrap) 变更数据捕获(参阅”初始快照“)。

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

一切的元数据

整个组织的数据流像一个巨大的数据库,每当批处理、流或ETL过程将数据从一个地方传输到另一个地方并组装时,它表现地就像数据库子系统一样,使索引或物化视图保持最新。

从这个角度讲,批处理和流处理器就像触发器、存储过程和物化视图维护例程的精细实现,它们维护的衍生数据系统就像不同的索引类型。现在的衍生数据系统让它们由不同软件提供,运行在不同机器上,由不同团队管理。

有两种途径或许能将不同的存储和处理工具合成一个有凝聚力的系统:

  • 联合数据库:统一读取

    • 这是一种多态存储方法,能解决跨多个不同系统的只读查询问题。
    • 为各种各样的底层存储引擎和处理方法提供一个统一的查询接口。
    • 应用程序仍能访问底层存储引擎,组合来自不同位置的数据的用户可以通过联合接口轻松完成操作。
    • 联合接口遵循单一集成和关系模型的传统,带有高级查询语言。
    • 实现起来很复杂,且无法解决跨系统同步写入问题。
  • 分拆数据库:统一写入

    • 通过统一的低级API(管道)进行通信,并且可以用更高级的语言进行组合(shell)
    • 能确保所有数据变更都在正确的位置结束,即使发生故障。
    • 更容易实现,只是将存储系统插接在一起。
开展分拆工作

同步写入到几个存储系统是更困难的工程问题。传统同步写入需要跨异构存储系统的分布式事务,作者认为这是错误的解决方案(参阅”导出的数据与分布式事务“),具有幂等写入的异步事件日志才是更健壮和实用的方法。

分布式事务在流处理组件内使用以匹配恰好一次语义(”重新访问原子提交“),但当事务设计由不同人群编写的系统时,缺乏标准化的事务协议会使集成困难。有幂等消费者的事件的有序事件日志是一种更简单的抽象,因此在异构系统中更容易实现。

基于日志的集成达成了各个组件之间的松散耦合(loose coupling) ,体现在两方面:

  1. 在系统级别,异步事件流使整个系统对各个组件的中断或性能下降更加稳健。若使用者运行缓慢或失败,则事件日志可以缓冲消息(”磁盘空间使用情况“),让生产者和其他使用者继续不受影响地运行。而分布式事务的同步交互会将本地故障升级成大规模故障。
  2. 人力方面,分拆数据系统允许不同的团队独立开发、改进和维护不同的软件组件和服务。并使团队专业化做一件事。事件日志提供一个足够强大的接口,以捕获相当强的一致性属性,但也足够普适于几乎任何类型的数据。
分拆系统vs集成系统

数据库仍然是需要的:维护流处理组件中的状态,为批处理和流处理器的输出提供查询服务。专用的查询引擎对于特定工作负载仍然非常重要,例如MMP数据仓库中的查询引擎针对探索性分析查询进行优化,并能够很好地处理这种类型的工作负载。

运行好几种基础设施会带来复杂性问题,单一集成软件产品也可以在其设计应对的工作负载类型上实现更好且更可预测的性能。为了不需要的规模而构建系统是白费精力,还可能会将我们锁死在一个不灵活的设计中。

分拆的目的是允许结合多个不同的数据库以获得更好的性能,这是关于广度而非深度。当单一软件无法满足所有需求时,拆分和联合才有优势。若一项技术可以满足所有需求,直接用它就行。

少了什么?

目前还没有与Unix shell类似的分拆数据库,这是一种声明式的、简单的、用于组装存储和处理系统的高级语言。

能够预先计算和更新缓存是一件好事,物化视图的本质上就是一个预先计算的缓存,所以可以通过复杂查询声明指定物化视图来创建缓存,包括图的递归查询和应用逻辑。

2.2 围绕数据流设计应用

使用应用代码组合专用存储与处理系统来分拆数据库的方法,也被称为“数据库由内而外”方法。可以看作是一种设计模式,是很多人的思想的融合。

即使是电子表格也在数据流编程能力上比主流编程语言强。电子表格中,将公式放入一个单元格,只要公式的输入发生变更,公式的结果也会自动重新计算。这正是数据系统层次所需要的,我们希望:当记录发生变更时,自动更新该记录的任何索引,并且自动刷新依赖于该记录的任何缓存视图或聚合。

与电子表格不同的是,数据系统需要容错、可扩展、持久存储,还要能整合不同人群编写的不同技术并重用现有的库和服务。期望使用某种特定语言、框架或工具开发所有软件是不切实际的。

本节将探讨一些围绕分拆数据库和数据流的想法构建应用的方法。

应用代码作为衍生函数

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

  • 次级索引:由一种直白的转换函数生成的衍生数据集,挑选被索引的列或字段中的值直接排序。用于次级索引的衍函数作为核心功能被内建到许多数据库中,可以通过CREATE INDEX调用。
  • 全文搜索索引:通过应用各种自然语言处理函数创建用于搞笑查找的数据结构。常见的基本语言特征可能内置到数据库中,但更复杂的特征通常需要领域特定的调整。
  • 机器学习系统:从训练数据通过应用各种特征提取,统计分析函数衍生的数据。特征工程是众所周知的特定于应用的特征,通常包含很多关于用户交互与应用部署的详细知识。
  • 缓存:填充进前端UI的数据集合,填充缓存需要知道UI中引用的字段。
应用代码和状态的分离

理论上,数据库可以是任意应用代码的数据环境,就像操作系统一样。但实践中它们对此的适配性很差,它们不满足于现代应用开发的要求,例如依赖性和软件包管理,版本控制,滚动升级,可演化性,监控,指标,对网络服务的调用以及与外部系统的集成。

现在大多数Web应用是作为无状态服务部署的,任何用户请求可以路由到任何应用程序服务器,并在服务器发送响应后会忘记所有请求。这种部署的可扩展性很高,可以随意添加或删除服务器,但状态必须到某个地方:通常是数据库。现在的趋势是将无状态应用程序逻辑与状态管理(数据库)分开:不将应用程序逻辑放入数据库中,也不将持久状态置于应用程序中。

大多数编程语言和数据库中,你无法订阅而只能定期读取变量的变更。与电子表格不同,如果变量的值发生变化,变量的读者不会收到通知。 (可以在自己的代码中实现这样的通知,这称为观察者模式,但大多数语言没有将这种模式作为内置功能。)

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

从数据流的角度思考应用,意味着重新协调应用代码与状态管理之间的关系。更多的考虑数据状态、状态变更和处理它们的代码之间的相互作用与协同关系。应用代码通过在另一个地方触发状态变更来响应状态变更。

我们讨论过将数据库的变更日志视为一种我们可订阅的事件流(参阅“流与数据库”),诸如Actor的消息传递系统也具有响应事件的概念。早在20世纪80年代,元组空间(tuple space) 模型就已经探索了表达分布式计算的方式:观察状态变更并作出反应。

当触发器由于数据变更而被触发时,或次级索引更新以反映索引表中的变更时,数据库内部也发生着类似的情况。分拆数据库意味着将这个想法应用于在主数据库之外,用于创建衍生数据集:缓存,全文搜索索引,机器学习或分析系统。我们可以为此使用流处理和消息传递系统。

传统消息系统通常是为执行异步任务设计的(参阅“与传统消息传递相比的日志”):

  • 维护衍生数据时,状态变更的顺序通常很重要,许多消息代理在重传未确认消息时没有此属性,双写也被排除在外。
  • 容错是衍生数据的关键:仅丢失单个消息会导致衍生数据集永远与其数据源失去同步。消息传递和衍生状态更新都必须可靠。

稳定的消息排序和容错消息处理是相当严格的要求,但与分布式事务相比开销更小且更稳定。现代流处理组件可以提供这些排序和可靠性保证,并允许应用代码以流算子形式运行。

这些应用代码可以执行任意处理,包括数据库内置衍生函数通常不提供的功能。流算子可以围绕着数据流构建大型系统(就像通过管道链接的Unix工具),每个算子接受状态变更的流作为输入,并产生其他状态变化的流作为输出。

流处理器和服务

现在流行的开发风格是将功能分解为一组通过同步网络请求(如REST API)进行通信的服务(service) (参阅”通过服务实现数据流:REST和RPC“)。这种架构有点在于通过松耦合能提供组织上的可扩展性,让不同团队专职于不同服务,减少团队协调工作。

在底层通信机制上,微服务使用同步的请求/响应式交互,而数据流中组装流算子采用单向异步消息流。数据流系统还能实现更好的性能,例如:假设客户正在购买以一种货币定价但以另一种货币支付的商品,我们需要知道当前汇率,这个操作可以这么实现:

  1. 微服务方法中,代码可能查询汇率服务或数据库,以获取特定货币的当前汇率。
  2. 数据流方法中,处理订单的代码提取订阅汇率变更流,并在汇率发生变动时将当前汇率存储在本地数据库中。处理订单时查询本地数据库即可。

数据流方法更快且可用性更高,我们现在不再使用RPC,而是之间建立流联接(参阅”流表联接“)。微服务方法中,也可以将汇率缓存在本地,并且轮询以保证新鲜度,从而模拟数据流方法。

2.3 观察衍生数据状态

写路径(write path) :数据流系统提供的创建衍生数据集并使其保持更新的过程。某些信息写入系统后,可能经历批处理和流处理等多个阶段,而最终每个衍生数据集都会被更新,以适配写入的数据。如下图。

分拆数据库 - 图1

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

写路径和读路径涵盖了数据的整个旅程——从收集数据开始,到使用数据结束。写路径是预计算——数据进入即刻完成。而读路径需要有人请求才发生,类似于惰性求值。

衍生数据集是写路径和读路径相遇的地方,代表了在写入时需要完成的工作量在读取时需要完成的工作量之间的权衡。

物化视图和缓存

全文搜索索引是一个很好的例子:写路径更新索引,读路径在搜索中搜索关键字。写入需要更新文档中出现的所有关键词的索引条目。读取需要搜索查询中的每个单词,并应用语句来找到目标词。

如果没有索引,搜索查询将扫描所有文档。没有索引意味着写入路径上的工作量少,但是增加了读取路径的工作量。

可以为一组固定的最常见的查询预先计算搜索结果,从而快速搜索而不必去索引。这就是常见查询的缓存,也称物化视图(materialized view)

从这个例子中我们可以看到,索引不是写路径和读路径之间唯一可能的边界。由此来看,缓存,索引和物化视图的作用很简单:它们改变了读路径与写路径之间的边界。通过预先计算结果,从而允许我们在写路径上做更多的工作,以节省读取路径上的工作量。

有状态,可离线的客户端

我们可以尝试改变写路径和读路径之间的边界,并探讨这种改变的实际意义。

过去二十年来,客户端/服务器模型已经普遍到我们几乎忘记了还有其他模型的存在,但是技术不断发展,不时的质疑现状是很重要的。传统上,浏览器是无状态的客户端,连上互联网才能做事。而最近,JavaScript Web应用出现很多有状态的功能,包括客户端用户界面交互,以及Web浏览器中的持久化本地存储。移动应用可以类似地在设备上存储大量状态,且用户交互不需要与服务器交互。

这些变化引起了人们对离线优先(offline-first) 应用的兴趣。这些应用尽可能在设备上使用本地数据库,无需连接互联网,并在后台网络连接可用时与远程服务器同步。

摆脱无状态客户端与中央数据库交互的假设,转向在终端用户设备上维护状态。我们可以将设备上的状态视为服务器状态的缓存。屏幕上的像素是客户端应用中模型对象的物化视图;模型对象是远程数据中心的本地状态副本。

将状态变更推送给客户端

典型的网页中,浏览器只能在一个时间点读取数据,不会订阅来自服务器的更新。最近的协议已经超越了HTTP的基本请求/响应模式:EventSource API和WebSockets提供了通信信道,通过这些信道,Web浏览器与服务器保持打开的TCP连接。服务器从而能主动向浏览器推送信息,这会减少客户端状态的陈旧程度。

主动将状态变更推送至客户端设备,这意味着将写路径一直延伸到终端用户。客户端首次初始化时需要使用读路径读取状态,之后便可依赖于服务器发送的状态变更流。设备相当于一个事件流的订阅者,也能使用断线重连的技术(参阅”消费者偏移量“)。

端到端的事件流

最近,用于开发带状态客户端与用户界面的工具(React、Flux、Redux等),已经通过订阅表示用户输入和服务器响应的事件流,来管理客户端的内部状态,其结构与事件溯源相似(参阅”事件溯源“)。

可以将这种编程模型扩展为:允许服务器将状态变更事件,推送到客户端的事件管道中。因此,状态变化可以通过端到端(end-to-end) 的写路径流动:从一个设备上的交互触发状态变更开始,经由事件日志,并穿过几个衍生数据系统与流处理器,一直到另一台设备上的用户界面。

这种方式使得状态变化的传播延迟相当低。但也有缺点:关于无状态客户端和请求/响应交互的假设根深蒂固的植入于数据库、库、框架、协议之中。很少有数据存储提供订阅变更——即为请求返回一个随时间推移返回响应的流。

为了将写路径延伸到终端用户,需要从根本上重新思考我们构建这些系统的方式:从请求/响应转向发布/订阅数据流。更具响应性的用户界面与更好的离线支持。

读也是事件

我们说过,当流处理器将衍生数据写入存储时,以及当用户请求查询该存储时,存储将充当写路径和读路径之间的边界。该存储应当允许对数据进行随机访问的读取查询,否则这些查询将需要扫描整个事件日志。

很多情况下,数据存储与流处理系统是分开的。但回想一下,流处理器还是需要维护状态以执行聚合和连接的(参阅“流连接”)。这种状态通常隐藏在流处理器内部,但一些框架也允许这些状态被外部客户端查询,将流处理器本身变成一种简单的数据库。

通常,我们通过事件日志写入存储,用临时的网络请求读取。用"读也是事件"的思想,我们可以将读取请求表示为事件流,同时将读事件与写事件送往流处理器,流处理器通过将读取结果发送到输出流来响应读取事件。

当写入和读取都被表示为事件,并且被路由到同一个流算子以便处理时,我们实际上是在读取查询流和数据库之间执行流表连接。读取事件需要被送往保存数据的数据库分区(参阅“请求路由”),就像批处理和流处理器在连接时需要在同一个键上对输入分区一样(参阅”Reduce端连接与分组“)。

服务请求与执行连接之间的这种相似之处非常关键。一次性读取请求只是将请求传过连接算子,然后请求马上就被忘掉了;而一个订阅请求,则是与连接另一侧过去与未来事件的持久化连接。

记录读取事件的日志可能对于追踪整个系统的因果关系与数据来源有好处:可以让你重现出当用户做出特定决策之前看见了什么。这可以用于大数据分析等,但也会带来额外的存储与I/O成本。优化这些系统以减少开销仍然是一个开放的研究问题。

多分区数据处理

单个分区的查询也使用流可能是杀鸡用牛刀。但是我们可以得到启发:合并来自多个分区的数据,利用流处理器已经提供的消息路由,分区和连接基础设施。

Storm的分布式RPC功能支持这种使用模式(参阅”消息传递和RPC“),有些计算需要合并来自多个分区的结果,例如查找转发URL的推特粉丝的并集。

MPP数据库的内部查询表执行图有着类似的特征,若需要执行这种多分区连接,那么不如直接使用提供此功能的数据库。

我们可以将这视为一种实现超出传统解决方案的关于流的大规模应用。

3 将事情做正确

对于只读数据的无状态服务,出问题可以随时重启服务,而对于数据库这样的有状态系统出问题,就没那么简单了。

我们希望构建可靠且正确的应用,四十年来,原子隔离持久等事务特性是构建正确应用的首选工具:

  • 然而这些地基没有那么牢固,例如弱隔离级别带来困惑。
  • 一致性定义并不明确(参见”第九章“)。
  • 事务在某些领域被完全抛弃,被更好的模型取代。
  • 如果数据库的配置很难理解那么容易出错,代码需要正确使用它们的功能才能不出问题。

传统事务无法走远,本节将提出数据流框架中考量正确性的方式。

3.1 为数据库使用端到端的参数

使用有相对强的安全属性的数据系统(例如可序列化事务),不意味着能保证没有数据丢失或损坏。应用会出Bug,而人会犯错误。不可变和仅追加错误也没法解决一些问题。

正好执行一次操作

我们介绍过恰好一次语义(参阅”容错“)。如果处理消息时出现问题而重试,可能会发生:第一次处理实际上成功了,而重试让这个消息被处理了两次。

处理两次是数据损坏的一种形式,最有效的方法是使操作幂等(idempotent) (参阅”,幂等性“)。可能需要维护额外的元数据,并在从一个节点故障切换至另一个节点时做好防护(参阅”领导与锁定“)。

抑制重复

流处理外的许多地方也需要抑制重复的模式。例如TCP使用数据包上的序列号,用于排序、检测丢包和重复。但是这种重复抑制只适用于单条TCP连接的场景中,若事务中,客户端发送COMMIT但是网络断开,那么客户端不知道事务是否被提交。客户端可以重联然后重试,但现在已经处于TCP重复抑制的范围之外了,因为事务可能不是幂等的,就可能造成数据库的数据损坏。

两阶段提交(参阅”原子提交与两阶段提交“)会破坏TCP连接与事务之间的1:1映射,因为它们必须在故障后允许事务协调器重连到数据库,告诉数据库将存疑事务提交还是中止。这不足以确保事务恰好执行一次。

即使我们能抑制重复事务,我们也得担心用户设备与服务器之间的网络,用户可能发出请求却收不到响应,这就可能会向用户显示错误信息。

操作标识符

要在通过几跳的网络通信上使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的 —— 你需要考虑端到端(end-to-end) 的请求流。例如,可以为操作生成一个唯一标识符,通过在数据库中检查这个标识符是否存在来判断是否进行过操作。

端到端原则

抑制重复事务的这种情况只是一个更普遍的原则的一个例子,这个原则被称为端到端的原则(end-to-end argument) :只有在通信系统两端应用的帮助下,所讨论的功能才能完全正确的实现。

重复抑制无法阻止用户亲自提交重复的请求,解决这个问题需要一个端到端的解决方案:从终端用户的客户端一路传递到数据库的事务标识符。

端到端参数也适用于检查数据的完整性:以太网,TCP和TLS中内置的校验和可以检测网络中数据包的损坏情况,但是它们无法检测到由连接两端发送/接收软件中Bug导致的损坏,以及数据存储所在磁盘上的损坏。如果要捕获数据所有可能的损坏来源,需要端到端的校验和。加密也差不多,TLS/SSL可以阻挡攻击者但无法阻止恶意服务器。

低级别的可靠性功能不足以确保端到端的正确性,但是它们仍然有用,可以降低较高层级出现问题的可能性。

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

仅仅因为应用使用了提供相对较强安全属性的数据系统,例如可序列化的事务,并不意味着应用的数据就不会丢失或损坏了。应用本身也需要采取端到端的措施,例如除重。

事务是一个很好的抽象,将各种问题合并为两种可能结果:提交或中止。这对编程模型而言是一种巨大的简化。事务代价高昂,特别是当涉及异构存储技术时。我们拒绝使用分布式事务是因为它开销巨大,结果不得不在代码中重新实现容错机制。出于这些原因,探索对容错的抽象是很有价值的。

3.2 强制约束

端到端的除重可以通过从客户端一路透传到数据库的请求ID实现,那么其他类型的约束呢?

唯一性约束(参阅”约束和唯一性保证“)保证了一些需要强制实施唯一性的功能正确运行,例如用户名和电子邮件地址必须唯一标识用户,文件存储服务不能包含多个重名文件,两个人不能再航班或剧院预定同一个座位。也能用于其他约束,例如余额不会变负数,会议室不能重复预定。

唯一性约束需要达成共识

分布式中强制执行唯一性约束需要共识:若存在多个有相同值的并发请求,系统需要决定冲突操作中哪一个被接受,并拒绝其他操作(参阅“单领导者复制与共识”)。

唯一性检查可以通过对唯一性字段分区做横向扩展。例如,如果需要通过请求ID确保唯一性,我们可以确保所有具有相同请求ID的请求都被路由到同一分区。若需要让用户名是唯一的,则可以按用户名的散列值做分区。

异步多主复制可能会发生不同主库同时接受冲突写的情况,因而这些值不唯一。如果想立即拒绝任何违背约束的写入,同步协调是不可避免的。

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

全序广播(total order boardcast) 并且等价于共识:日志确保所有消费者以相同顺序看见消息(参阅“全序广播”)。基于日志的消息传递的分拆数据库中,可以使用类似的方法达成唯一性约束。

流处理器在单线程上按顺序消费单个日志分区中的所有消息。因此,当日志按唯一的值做的分区时,流处理器可以无歧义地决定哪个操作先到达。例如,多个用户尝试宣告相同用户名的情况下:

  1. 每个对用户名的请求都被编码为一条信息,追加到按用户名散列值确定的分区。
  2. 流处理器用本地数据库来追踪哪些用户名被占用了。对于所有申请可用用户名的操作,成功则记录用户名并发送成功消息,若用户名已被占用则发送拒绝消息。
  3. 请求用户名的客户端监视输出流,等待对应的成功或拒绝消息。

该算法基本与“使用全序广播实现线性一致性的存储”中的算法相同。可以简单地通过增加分区数扩展至较大的请求吞吐量,因为每个分区可以被独立处理。

该方法也适用于其他类型的约束,原理是让任何可能的冲突写入路由到相同分区按顺序处理。

多分区请求处理

涉及多个分区时,很难确保操作以原子方式执行并同时满足约束。传统方法中,执行事务需要跨多个分区进行原子提交,这实际上是将该事务嵌入一个全序,需要跨分区协调,不同的分区将无法独立处理从而影响吞吐量。

分区日志可以达到等价的正确性而无需原子提交:例,收付款中,有三个分区分别包含请求ID、收款人账户、付款人账户。使用分区日志时

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

如果直接扣款和加钱,则需要在这两个分区之间原子提交。这一通操作是为了将持久化记录为单条消息,从而避免对分布式事务的需要。单条消息请求要么出现在日志中,要么不出现,无需多分区原子提交。

如果流处理器在步骤2崩溃,那么会从上一个存档点恢复处理。这样做时不会跳过任何请求消息,但可能生成重复借贷指令,而步骤3中的处理器可以通过请求ID去重。

若想确保付款人余额够,可以用另一个流处理器来维护账户余额并校验事务,只有有效的事务会被记录在步骤1的请求日志中。

3.3 及时性与完整性

事务的一个便利是,通常都是线性一致的(参阅“线性一致性")。然而,当我们将一个操作拆分成跨越多个阶段的流处理器时却并非如此:日志的消费者在设计上是异步的,发送者不会等其消息被消费者处理完。但是,客户端等待输出流中特定消息是可能的。

在这个例子中,唯一性检查的正确性不取决于消息发送者是否等待结果。等待是为了同步通知发送者唯一性检查是否成功。但该通知可以与消息处理的结果相解耦。

我(作者)认为一致性(consistency) 这个术语混淆了两个需求:

  • 及时性(Timeliness):确保用户观察到系统的最新状态。如果存在复制延迟(参阅”复制延迟问题“),这种不一致是暂时的。
  • 完整性(Integrity):没有数据丢失,没有矛盾或错误的数据。如果完整性被违背,这种不一致是永久的,需要显示检查和修复。完整性通常比及时性重要得多,后果是灾难性的。
数据流系统的正确性

ACID事务通常同时提供及时性(例如线性一致性)和完整性(例如原子提交),因此对于ACID事务来说,它们之间的区别无关紧要。

恰好一次等效一次语义是一种保持完整性的机制。若事件丢失或生效两次,就有可能违背数据系统的完整性。因此面对故障时,容错消息传递与重复抑制(例如幂等)对于维护数据系统的完整性至关重要。

异步事件流不能保证及时性,除非显示构建一个返回之前就明确等待特定消息到达的消费者。但是流处理系统核心是完整性,正如上一节所说,可靠的流处理系统可以在无需分布式事务与原子提交的情况下保持完整性,即达成正确性时性能更好。为了达成正确性,我们组合了多种机制:

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

这些机制的组合在我(作者)看来是很有前景的方向。

宽松地解释约束

执行传统的唯一性约束需要共识,通常通过在单个节点中汇集特定分区中所有事件来实现,流处理也不例外。但现实中许多应用能摆脱这种形式,接受弱得多的唯一性——补偿性事务(compensating transaction)

  • 若两人同时注册相同用户名,可以向其中一个发送消息并道歉。
  • 若客户下单而库存不够了,可以下单补仓,并为延误向客户道歉或提供折扣。将道歉工作变成商业过程的一部分,那么就没必要对库存物品数量添加线性一致约束。
  • 旅馆抱着部分客人取消预订的期望超卖客房,当需求超过供给时再进入补偿流程。
  • 从银行账户中超额取款,银行再收取欠款+透支费用。银行可以限制每天的提款总额以降低风险。

类似以上的许多商业场景确实需要完整性,但执行约束时并不需要及时性,因此可以临时违背约束然后通过道歉来修复,这种做法与“处理写入冲突”中的冲突解决方法类似。而道歉的成本能否被接受是一个商业决策,传统模型在写入数据之前检查所有约束,反而会带来不必要的限制。线性一致也不是必须的,可以先乐观写入再事后检查。

无协调数据系统

以上两小节提到:

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

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

例如,这种系统可以使用多领导者配置运维,跨越多个数据中心,在区域间异步复制。不需要同步的跨区域协调,使得数据中心可以独立运行。这种系统提供的时效性很弱但完整性很强。

这种情况下:

  • 可序列化事务作为维护衍生状态的一部分仍然有用,它们可以在小范围内运行。
  • 异构分布式事务(如XA)事务不是必需的。
  • 同步协调仍然可以在需要的地方引入。

另一种审视协调与约束的角度是:减少了由于不一致而必须做出的道歉数量,但也可能降低系统的性能和可用性,从而可能增加由于宕机中断而需要做出的道歉数量。这就得根据需求寻找能让道歉数量最少的平衡点。

3.4 信任但验证

我们所有关于正确性、完整性和容错的讨论,都基于系统模型(system model) (参阅“将系统模型映射到现实事件"):某些事情会出错,但其他事情永远不会。问题在于违反我们假设的情况经常发生,系统会在硬件方面出错和在软件方面出Bug。

我们不能盲目信任硬件和软件管用,如果想确保数据仍然存在,必须读取并检查。同时,不时尝试从备份中恢复是很重要的,防止备份损坏。

检查数据完整性称为审计(auditing) :查明数据是否已经损坏,以便修复它,并尝试追查错误的来源。可审计性在财务中至关重要。

验证的文化

目前没有多少系统采用:”信任但是验证“的方式来持续审计自己。未来需要更多能自我验证(self-validating)自我审计(self-auditing) 系统,不断检查自己的完整性,而不是依赖盲目的信任。

ACID数据库上开发的应用经常忽视可审计性,事务在大多数情况下工作良好所以认为审计机制不值得投资。但是在NoSQL的发展下,更弱的一致性和更不成熟的存储技术使用越来越广泛,我们就需要针对可审计性设计。

为可审计性而设计
  • 基于事件的系统可以提供更好的可审计性。
  • 显式处理数据流可以使数据的来龙去脉(provenance) 更加清晰,从而使完整性检查更具可行性。
  • 可以使用散列来检查事件存储有没有被损坏。
  • 对于任何衍生状态,可以直接运行一遍,从事件日志中衍生它的批处理器和流处理器,以检查是否获得相同结果,或者可以并行运行冗余的衍生流程。
  • 具有确定性且定义良好的数据流,使得调试与跟踪系统的执行变得容易,可以更好的确定它为什么做了某些事情。重现导致意外事件的事故现场是很有价值的。
端到端原则重现

检查数据系统的完整性,最好是以端到端方式进行(参阅”数据库的端到端争论“):我们能在完整性检查中涵盖的系统越多,某些处理阶段中出现不被察觉损坏的几率就越小。

自动化测试一样,审计提高了快速发现错误的可能性,从而降低了系统变更或新存储技术可能导致损失的风险。如果你不害怕进行变更,就可以更好地充分演化一个应用,使其满足不断变化的需求。

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

使用密码学工具来证明系统的完整性是十分有趣的,这种方式对于宽泛的硬件与软件问题、潜在的恶意行为都很有效。加密货币和区块链等技术已经迅速出现在这一领域。

实质上它们是分布式数据库,具有数据模型与事务机制,不同副本可以由互不信任的组织托管。副本不断检查其他副本的完整性,并用共识协议对应当执行的事务达成一致。缺点是工作证明(proof of work)技术非常浪费。

密码学审计与完整性检查通常依赖默克尔树(Merkle tree) ,这是一颗散列值的树,能够用于高效地证明一条记录出现在一个数据集中(以及其他一些特性)。除了加密货币外,证书透明性(certificate transparency) 也是一种依赖Merkle树的安全技术,用来检查TLS/SSL证书的有效性。

未来,完整性检查和审计算法将会在通用数据系统中得到越来越广泛的应用。

4 做正确的事情

本书考察了各种不同的数据系统架构,评价了它们的优点与缺点,并探讨了构建可靠,可扩展,可维护应用的技术。但是,我们忽略了讨论中一个重要而基础的部分,现在补充一下。

每个系统都服务于一个目的;我们采取的每个举措都会同时产生期望的后果与意外的后果。而后果可能会远远超出最初的目的。我们这些建立系统的工程师,有责任去仔细考虑这些后果。

我们将数据当成一种抽象的东西来讨论,但请记住,许多数据集都是关于人的:对待这些数据,我们必须怀着人性与尊重。用户也是人类,人类的尊严是至关重要的。软件开发越来越多的涉及到道德抉择,但实践中很少讨论,更不用说强制执行。技术本身并无好坏之分 —— 关键在于它被如何使用,以及它如何影响人们。

4.1 预测性分析

预测性分析是”大数据“主要内容之一,随着算法决策变得越来越普遍,被某种算法(准确地或错误地)标记为某个标签的某人可能会受到大量相同的决定。这是一种对个体自由的极大约束,因此被称为“算法监狱”。例如短视频平台的信息茧房。

偏见与歧视

算法做出的决定不一定比人类更好或更差。每个人都或多或少存在偏见,人们希望根据数据做出决定,而不是通过人的主观评价与直觉,希望这样能更加公平。

当我们开发预测性分析系统时,那些决策规则是从数据中推断出来的,而这些系统学到的模式是个黑盒——如果算法的输入中存在系统性的偏见,则系统很有可能会在输出中学习并放大这种偏见。

预测性分析系统只是基于过去进行推断;如果过去是歧视性的,它们就会将这种歧视归纳为规律。如果我们希望未来比过去更好,那么就需要道德想象力,而这是只有人类才能提供的东西。

责任与问责

如果算法出错,谁来负责?基于机器学习的评分算法通常会使用更宽泛的输入,并且更不透明;因而很难理解特定决策是怎样作出的,以及是否有人被不公正地,歧视性地对待。很多数据本质上是统计性的,这意味着即使概率分布在总体上是正确的,对于个例也可能是错误的。

收集关于人的数据并进行决策,信用评级机构是一个很经典的例子。信用分总结了“你过去的表现如何?”,而预测性分析通常是基于“谁与你类似,以及与你类似的人过去表现的如何?”。

盲目相信数据决策至高无上,是一种有切实危险的妄想。随着数据驱动的决策变得普遍,我们需要弄清楚,如何使算法更负责任且更加透明,如何避免加强现有的偏见,以及如何在它们不可避免地出错时加以修复。此外,我们还得弄清楚如何避免数据被用于害人,如何认识数据的积极潜力。

反馈循环

当服务变得善于预测用户想要看到什么内容时,它最终可能只会向人们展示他们已经同意的观点,将人们带入滋生刻板印象,误导信息,与极端思想的回音室(信息茧房)。

当预测性分析影响人们的生活时,自我强化的反馈循环会导致非常有害的问题。例如,A突然下岗而没法还款,那么信用分就会下降,就会导致更难找工作,然后更加没钱。在数据与数学严谨性的伪装背后,隐藏的是由恶毒假设导致的恶性循环。

使用系统思维(systems thinkin) 对整个系统进行整体思考,许多后果是可以预测的。我们可以尝试理解数据分析系统如何响应不同行为、结构或特性——该系统是否增大了现实的矛盾?

4.2 隐私和追踪

数据收集本身也可能有道德问题。系统是在为用户提供服务,但是当用户的活动被跟踪并记录,作为他们正在做的事情的副作用时,该服务是服务于它自己,而这可能与用户的利益相冲突。

追踪用户行为数据对于许多面向用户的在线服务而言,变得越来越重要——可以帮助用户找到自己喜欢的东西,这些功能需要一定量的用户行为追踪,而用户也可能从中受益。但是,如果服务通过广告盈利,那么广告才是真正的客户,而用户的利益排第二,数据就会用于建立画像以用于营销 —— 与用户的关系就变得像是监视(surveilance)

监视

如果放任不管,智能设备会无时不刻的收集我们的设备,但是数据由资本家而不是政府机构保存。

当监视被用于推荐算法,似乎企业和用户之间没有剧烈的矛盾。但当被用于生活的重要方面,例如保险和就业时,数据分析可以揭示出令人惊讶的私密事务:例如运动手表能精确的计算你在输入的内容(比如密码)。而分析算法只会越来越精确。

同意与选择的自由

虽然用户同意了服务条款与隐私政策,因此他们同意数据收集,但是用户几乎不知道他们提供给我们的是什么数据,哪些数据被放进了数据库,数据又是怎样被保留与处理的 —— 大多数隐私政策都是模棱两可的,忽悠用户而不敢打开天窗说亮话。如果用户不了解他们的数据会发生什么,就无法给出任何有意义的同意。

而且从用户身上挖掘数据是一个单向过程,而不是真正的互惠关系,也不是公平的价值交换。用户对能用多少数据换来什么样的服务,既没有没有发言权也没有选择权:服务与用户之间的关系是非常不对称与单边的。这些条款是由服务提出的,而不是由用户提出的。

同时,不使用服务几乎是不可能的。例如,一个人拒绝使用手机聊天软件进行社交,会产生很多的社会成本。对于普通人而言,没有真正意义上的选择,监控是不可避免的。

隐私与数据使用

人们通常对隐私(privacy) 一词存在误解。拥有隐私并不意味着保密一切东西,而是意味着拥有选择向谁展示哪些东西的自由,要公开什么,以及要保密什么——隐私权是一项决定权,这是一个人自由与自主的重要方面。

互联网服务使得在未经有意义的同意下收集大量敏感信息变得容易得多,而且无需用户理解他们的私人数据到底发生了什么。当通过监控基础设施从人身上提取数据时,隐私权不一定受到损害,而是转移到了数据收集者手中。这意味着我们已经失去了披露一些私密信息的能动性,决定要透露什么和保密什么的权利从个体手中转移到了公司手中,而公司以利润最大化为目标行使隐私权。

许多公司都有一个目标,不要让人感觉到毛骨悚然,因此公司会保密的监视。用户的私密信息只会间接地披露,例如针对特定人群定向投放广告的工具。

允许用户控制其隐私设置,例如选择其他用户能看到哪些东西。这是将一些控制交还给用户的第一步,但服务本身仍然可以不受限制地访问和使用这些数据。

数据资产与权力

行为数据是用户与服务交互的副产品,因此有时被称为“数据废气” —— 暗示数据是毫无价值的废料。从这个角度来看,行为和预测性分析是从数据中提取价值的回收形式。

更准确的看法恰恰相反:从经济的角度来看,如果定向广告是服务的金主,那么关于人的行为数据就是服务的核心资产。在这种情况下,应用是一种诱骗用户将更多的个人信息提供给监控设施的手段。

个人数据是珍贵资产的说法因为数据中介的存在得到支持,这是阴影中的秘密行业——交易个人数据用于市场营销。当公司破产时,个人数据就是被出售的资产之一。如果数据落入恶人手中,不仅危害个人安全也可能危害国家安全。

回顾工业革命

两次时代进步或许有共同规律。工业革命一开始有许多问题,例如污染、工人权益等,这些问题经过了很长时间的发展才得到改善。这些改善使得生产成本增加,但社会从中受益,例如禁止排放污水、雇佣童工等。

就像工业革命的早期一样,转向信息时代的我们也需要完善许多问题,而数据的收集与使用就是重大问题之一。

用布鲁斯·施奈尔的话来说:数据是信息时代的污染问题,保护隐私是环境挑战。几乎所有的电脑都能生产信息。它堆积在周围,开始溃烂。我们如何处理它 —— 我们如何控制它,以及如何摆脱它 —— 是信息经济健康发展的核心议题。正如我们今天回顾工业时代的早期年代,并想知道我们的祖先在忙于建设工业世界的过程时怎么能忽略污染问题;我们的孙辈在回望信息时代的早期年代时,将会就我们如何应对数据收集和滥用的挑战来评断我们。

立法和自律

数据保护法有助于维护个人权利,但是在互联网环境下它们很多并不有效。这些规则会直接否定了大数据的哲学——最大限度的收集数据,将其与其他数据集结合起来进行试验和探索,以便产生新的发现。

那些收集了大量有关人的数据的公司反对监管,认为这是创新的负担与阻碍。在某种程度上,这种反对是有道理的。例如疾病数据的过度监管会导致研发变缓,从而导致更多人死亡。

我们究竟能做到哪一步,是一个开放的问题。首先,我们不应该永久保留数据,而是一旦不再需要就立即清除数据,但这与不变性的想法背道而驰。这是可以解决的问题,目前较有前景的办法是通过加密协议来实施访问控制。

从根本上来说,我们应该停止将用户视作待优化的指标数据,并记住他们是值得尊重,有尊严和能动性的人。我们应该允许每个人保留自己的隐私 —— 即,对自己数据的控制,而不是通过监视来窃取这种控制权。