第十二章
# 第十二章 数据系统的未来
我们的目标是探索如何设计比今天更好的应用程序:强大、正确、可演化并最终使所有人受益。
数据集成
在复杂的应用程序中,数据通常以多种不同的方式被使用。不太可能存在适用于所有不同环境的软件,因此你不可避免地要将几个不同的软件组合在一起,以提供应用程序的功能性。
采用派生数据来组合工具
随着不同类型的数据持续增加,集成问题会变得越来越困难。
为何需要数据流
如果可以通过单个系统来决定所有输入的写入顺序,那么以相同的顺序处理写操作就可以更容易地派生出数据的其他表示形式。
无论是使用变更数据捕获还是事件获取日志,都不如简化总体顺序的原则重要。
根据事件日志来更新一个派生数据系统通常会比较好实现,并且可以实现确定性和幂等性,也因此使系统很容易从故障中恢复。
派生数据与分布式事务
为了保持不同的数据系统彼此之间的一致性,经典的方法是通过分布式事务。
分布式事务通过使用锁机制进行互斥来决定写操作的顺序,而CDC和事件源使用日志进行排序。分布式事务使用原子提交来确保更改只生效一次,而基于日志的系统通常基于确定性重试和幕等性。
最大的不同在于事务系统通常提供线性化,这意味着它可以保证读自己的写等一致性。另一方面,派生的数据系统通常是异步更新的,所以默认情况下它们无怯提供类似级别保证。
全序的局限
对于非常小的系统,构建一个完全有序的事件日志是完全可行的。但是,随着系统越来越大,井且面对更为复杂的负载时,瓶
颈就开始出现了。
决定事件的全序关系称为全序关系广播,它等价于共识。大多数共识算怯是针对单节点吞吐量足以处理整个
事件流而设计的,并且这些算桂不提供支持多节点共享事件排序的机制。
排序事件以捕获因果关系
如果事件之间不存在因果关系, 则不支持全序排序并不是一个大问题,因为并发事件可以任意排序。然而,因果依赖有时候会有
一些更微妙的情况。
批处理和流处理集成
整合的目标是确保数据在所有正确的地方以正确的形式结束。而批处理和流处理则是实现这一目标的有效工具。
批处理和流处理有许多共同的原则,而根本区别在于流处理器运行在无界数据集上,而批处理的输入是已知的有限大小。
保持派生状态
批处理具有相当强的功能特性(即使代码不是用函数式编程语言编写的),包括倡导确定性、纯函数操作即输出仅依赖于输入,除了显式输出以外没有任何副作用,输入不可变,追加式输出结果等。流处理是类似的,但它扩展了操作来支持可管理的、 容错的状态
为应用程序演化而重新处理数据
在需要维护派生数据时,批处理和流处理者扫会用得上。流处理可以将输入的变化数据迅速反映在派生视图中,而批处理则可以反复处理大量的累积数据,以便将新视图导出到现有数据集上 。
Lambda架构
Lambda体系结构的核心思想是进来的数据以不可变事件形式追加写到不断增长的数据集,类似于事件源。基于这些总事件,可以派生出读优化的视图。
在lambda方法中,流处理器处理事件并快速产生对试图的近似更新;批处理系统则处理同一组事件并产生派生视图的校正版本。
统一批处理和流处理
在一个系统中统一批处理和流处理需要以下功能,而这些功能目前正越来越普及 :
- 支持以相同的处理引擎来处理最新事件和处理历史回放事件。
- 支持只处理一次语义。
- 支持依据事件发生时间而不是处理时间进行窗口化 。
分拆数据库
许多文件系统不能很好地处理包含一千万个小文件的目录,而包含一千万条记录的数据库是完全正常的 。
UNIX和关系型数据库采用了大不一样的哲学思想看待信息管理问题。 UNIX认为它的目的是为程序员提供一个逻辑的,但是相当低层次的硬件抽象,而关系型数据库则希望为应用程序员提供一个高层次的抽象,来隐藏磁盘上数据结构的复杂性、并发性、崩愤恢复等 。 UNIX开发的管道和文件只是字节序列,而数据库开发了 SQL和事务。
编排多种数据存储技术
数据库中内置的一些功能与采用批处理和流处理器构建的派生数据系统似乎很多对应关系 。
创建一个索引
在关系型数据库中运行CREATE INDEX来创建新索引 时。数据库必须扫描表的一致性快照,挑选出所有被索引的字段值,对它们进行排序,然后得到索引 ,必须处理从一致性快照创建以来所累计的写入操作。完成后,只要有事务写入表中,数据库就必须持续保持索引处于最新状态。这个过程和配置新的从节点副本非常相似,也非常类似于流处理系统中的初始变更数据捕获。
元数据库
批处理和流处理器就像触发器,存储过程和实体化视图维护相关实现。
没有一个统一的数据模型或存储格式适用于所有的访问模式,不同的存储和处理工具最终会组合成一个紧密结合的系统 :
- 联合数据库 : 统一读端
可以为各种各样的底层存储引擎和处理方告提供一个统一的查询接口 : 一种称为联合数据库或聚合存储的方法 - 分离式数据库 : 统一写端
对于一个数据库, 创建一致的索引是其内置的功能 。 而在构建跨多个存储系统的数据库时,我们同样需要确保所有数据更改都会体现在所有正确的位置上,即使中间发生了某些故障。
分离式如何工作
传统的同步写依赖于跨异构存储系统的分布式事务,在单个存储系统内或流处理系统内的事务是可行的 , 但是当数据跨越不同技术的边界时,具有幕等写入的异步事件日志是一种更加健壮和可行的方住。
基于日志的集成的一大优势是各个组件之间的松搞合,这体现在两个方面 :
- 在系统级别,异步事件流使整个系统在应对各个组件的中断或性能下降时表现更加稳健 。
- 在人员角度看 ,分离式数据系统使得不同的团队可以独立的开发、改进和维护不同的软件组件和服务。
分离式与集成式系统
即使分离式确实能成为未来的主流 ,它也不会取代目前形式的数据库,这些数据库仍然会像以前一样拥有巨大需求。
维护流处理器中的状态仍然需要数据库,为批处理和流处理器的输出提供合井的查询也同样需要数据库。
分离的 目标不是要与那些针对特定负载的单个数据库来竞争性能。目标是让你可以将多个不同的数据库组合起来, 以便在更广泛的工作负载范围内实现 比单一软件更好的性能。
遗漏了什么?
组合数据系统的工具正在变得越来越好,但是还缺少一个主要部分:以简单和声明式构建存储和处理系统的高级语言
围绕数据流设计应用系统
通过应用层代码来组合多个专门的存储与处理系统并实现分离式数据库的方陆也被称为“数据库由内向外”方法
应用程序代码作为派生函数
当某个数据集从另一个数据集派生而来时,它一定会经历某种转换函数。
应用程序代码与状态分离
理论上讲,数据库可以像操作系统那样成为任意应用程序代码的部署环境。然而,实际上它们并不太适合,主要是无法满足当今应用程序的很多开发要求 ,如依赖性和软件包管理,版本控制,攘动升级,可横化性,监控,指标,网络服务调用以及与外部系统的集成。
数据流 : 状态变化和应用程序代码之间的相互影晌
从数据流的角度思考应用意味着重新协调应用代码和状态管理之间的关系。我们不是将数据库简单地视为被应用程序所操纵的被动变量, 而是更多地考虑状态 、状态变化以及处理代码之间 的相互作用和协作关系。 应用程序代码在某个地方会触发状态、变化 ,而在另一个地方又要对状态变化做出响应。
维护派生数据与异步作业执行不同,后者主要是传统消息系统所针对的目标场景:
- 当维护报生数据时,状态更改的顺序通常很重要, 如果从事件日志中派生出了多个视图,每个试图都需要按 照相同的顺序来处理这些事件,以使它们互相保持一致。
- 容错性是派生数据的关键 :丢失哪怕单个消息都会导致派生数据集永远无法与数据源同步。
对稳定的消息排序和可容错的消息处理的要求都非常严格,但是它们比分布式事务代价小很多 ,井且在操作上也更加稳健 。 现代流式处理可以对大规模环境提供排序和可靠性保证,并允许应用程序代码作为 stream operator运行 。
流式处理与服务
当前流行的应用程序开发风格是将功能分解为一组通过同步网络请求(例如REST API) 进行通信的服务。 这种面向服务的结构优于单体应用程序之处在于松相合所带来的组织伸缩性 : 不同的团队可以在不同的服务上工作,这减少了团队之间的协调工作
观察派生状态
写路径可以看作是预计算的一部分,即一旦数据进入 ,即刻完成,无论是否有人要求访问它。而过程中的读路径贝 lj 只有当明确有人要求访问时才会发生。
写路径和i卖路径在派生数据集上交会,某种程度上,它是写入时需完成的工作量和读取时需完成的工作量之间的一种平衡。
实体化视图和缓存
缓存、索引和实体化视图的角色就比较清楚了 ,它们主要是调整读、写路径之间的边界。通过预先计算结果,写路径上承担了更多的工作,而读路径则可以简化加速。
有状态,可离线客户端
如果用户界面不需要等待同步网络请求,井且大多数应用程序可以离线工作,对用户来说是一个巨大优势。
我们可以将设备上的状态视为服务器上的状态缓存。屏幕上的呈现是一种客户端对象模型的实体化视图 ;而客户端的对象模型则是远程数据中心在本地的状态副本
状态更改推送至客户端
更新的协议已经超越了基本的HTTP的请求/响应模式:服务器端发送事件和JWebSocket提供了一个通信通道,通过该通道, Web浏览器可 以与服务器保持开放的TCP连接,只要处于连接状态,服务器可以主动推送消息至浏览器 。这为服务器提供了一种新的更新机制, 即主动通知井更新终端用户客户端本地存储状态,从而缩小与服务器状态的滞后程度。
这些设备有时会离线,在此期间无怯接收到服务器状态更改的任何通知。基于 日志的消息代理的消费者在失败或断开连接后可以重新连接,并确保它不会错过任何网络断开之后的新消息
端到端的事件流
目前的数据库、 函数库、 开发框架和交互协议等都有对无状态客户端和请求/响应交互根深蒂固的假设。许多数据存储支持的读写操作其实都是一个请求对应一个晌应,但很少支持订阅更改,即一段时间内会主动返回 一系列的响应。
为了将写路径扩展到最终用户,我们需要从根本上重新思考构建这些系统的方式:从请求/响应交互转向发布/订阅数据流
读也是事件
对存储的写入是通过事件日志进行的,而读取则是即时的网络请求方式,查询直接路由到那些存储数据节点。这样的设计也很合理,但它并不是唯一可能的设计方案。也可以将读请求表示为事件流,发送至流处理系统,流处理系统则将读结果发送至输出 流来响应读取事件
多分区数据处理
对于仅涉及单个分区的查询,通过流来发送查询并收集响应事件流可能显得有些大材小用 。然而 ,这种方怯却开启了一种分布式执行复杂查询的可能性,这需要合并来自多个分区的数据,并很好地借助底层流处理系统所提供的消息路由、分区和join功能。
端到端的正确性
在某些领域,事务处理被完全抛弃,被性能更好、扩展性更强的模型所取代,但是这些模型具有更复杂的语义。
在特定事务隔离级别或复制模式下,想要确定某个特定应用程序是否安全就很有挑战性。简单的解决方案似乎能够低并发且没有错误的情况下正常工作,但是在要求更高的情况下会出现许多细微的错误。
传统的事务方怯并不会消失,但它也未必是保证应用程序正确和灵活处理错误的终极手段。
数据库的端到端争论
仅仅因为应用程序使用了具有较强安全属性的数据系统,并不能意味着应用程序一定保证没有数据丢失或损坏。
Exactly-once执行操作
如果在处理消息过程中出现意外,可以选择放弃或者再次尝试。如果采用重试策略,最终这个消息可能处理了两次 。处理两次是某种形式的数据损坏,最有效的方法之一是使操作满足幕等性,即无论执行一次还是多次,确保具有相同的效果。
重复消除
从Web服务器的角度来看,重i式是一次独立的请求,而对于数据库来说重试请求变成一个另外全新的事务。通常的去重机制对此无能为力 。
操作标识符
可以为操作生成一个唯一的标识符(如UUID ),并将其作为一个隐藏的表单字段包含在客户机应用程序中:或者对所有相关表单字段计算一个哈希值依次来代表操作ID,将该操作ID一直传递到数据库,检查并确保对一个给定的ID只执行一个操作。
端到端的争论
TCP ,数据库事务和流处理器本身并不能完全排除这些重复。解决这个问题需要一个端到端的解决方案:从终端用户客户端一直传递到数据库的唯一事务标识符 。
端到端的参数也适用于检查数据的完整性,如果想要捕获所有可能的数据源损坏,则需要端到端的校验和。类似的讨论也适用于加密
在数据系统中采用端到端的恩路
即使应用程序所使用的数据系统提供了比较强的安全属性(如可串行化事务),也并不意味着应用程序就一定没有数据丢失或损坏,应用程序本身也需要采取端到端的措施,例如重复悄除。
长期以来,事务被认为是一个非常好的抽象,它把可能出现的诸多问题归结为只有两种可能的结果:提交或中止。这对编程模型来讲是一个巨大的简化。
事务处理的代价很高,特别是在楼及异构存储技术时,当我们因为代价昂贵而拒绝采用分布式事务,最终不得不在应用代码中重新实现容错机制 。
探索更好的容错抽象非常必要:它能够更容易地提供特定应用的端到端正确保证,井且在大规模的分布式环境中依然具有良好的性能和良好的操作性。
强制约束
唯一性约束需要达成共识
在分布式环境中,保证唯一性约束需要达成共识:如果有 多个具有相同值的井发请求,系统需要决定接受哪一个操作,井由于违陆约束因此拒绝其他的冲突操作。达成这一共识的最常见的方式是将单一节点作为主节点,井负责作出所有的决定 。
采用分区方案可以提高唯一性检查的扩展性,即基于要求唯一性的字段进行分区。
但是,它无桂支持异步的多主节点复制,因为可能会发生不同的主节点同时接受冲突写入,无法保证值的唯一。如果想达到立即拒绝任何违反约束的写请求,则同步式的协调不可避免。
基于日志的消息传递唯一性
日志机制可以确保所有悄费者以相同的顺序查看消息,这种保证在形式上被称为全序关系广播,它等价于共识问题。在基于日志的消息传递的分离式数据库系统中,我们可以采用非常类似的方怯来保证唯一性约束。
流处理系统在单线程上严格按照顺序来消费处理日志分区中的所有消息,因此 ,如果根据需要保证唯一性的字段进行日志分区,则流处理系统可以清晰、明确地确定多个冲突操作哪一个先到达。
这个方怯不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是,任何可能冲突的写人都被路由到特定的分区并按顺序处理。
多分区请求处理
通过将多分区事务划分为两个不同分区的处理阶段,并使用端到端的请求ID,我们实现了同样的正确性,即使可能会发生故障,而且避免使用原子提交协议。
时效性与完整性
一致性这个术语将两个值得分开考虑的不同的需求:时效性和完整性合二为一了。
时效性:时效性意味着确保用户观察到系统的最新状态。
完整性:完整性意味着避免数据损坏,即没有数据丢失, 也没有互相矛盾或错误的数据。
违反时效性导致“最终一致性”, 而违反完整性则是“永久性不一致”。
对于大多数应用,完整性比时效性重要得多。 违反时效性可能令人讨厌和混淆,但是对完整性的破坏则是灾难性的 。
数据流系统的正确性
在异步处理事件流时,除非在返回之前明确地创建了等待消息到达的消费者,否则不能保证及时性。但是,完整性仍上是流处理系统的核心 。
只执行一次是一种保证完整性的机制。如果事件丢失或者事件发生两次,数据系统的完整性可能被破坏。因此 ,容错的消息传递和重复消除对于面对故障时保持数据系统的完整性非常重要。
宽松的约束
保证唯一性约束需要达成共识,通常通过单个节点汇集特定分区的所有事件来实现。许多实际应用程序采用了较弱的唯一性因而可以摆脱
该限制。
无需协调的数据系统
数据流系统可以为许多应用程序提供数据管理服务,而不需要协调,同时仍然提供强大的完整性保证。这种避免协调的数据系统具有很大的吸引力:与需要执行同步协调的系统相比,可以实现更好的性能和容错能力
信任,但要确认
如果我们不得不每分每秒都在担心计算机会出错,最终将无也完成任何工作。
软件缺陷时的完整性
除了硬件问题,总是存在软件错误的风险( bug ),边些错误无也通过底层的网络、内存或文件系统校验和来捕获。
当谈及应用程序代码时,我们往往不得不假定里面存在更多的 bug ,主要是大多数应用程序所做的代码检查和测试的量级无怯和数据库相提并论。
不要盲目信任承诺
成熟的系统同样会考虑不太可能的事情出错的可能性, 并且主动管理这种风险。
如果想确保你的数据仍然在那,只能不断地去读取和检查。
验证的文化
数据审计机制尚未形成,我们仍然继续盲目信任底层技术而开发上层应用,这种做法已经变得更加危险。我们应该花些时间来思考一下关于可审计性的设计。
可审计性的设计
基于事件的系统可以提供更好的可审计性。在事件源方在去中,用户对系统中的输入都被表示为一个单一的不可变事件,并且任何结果状态的更新都是依据该事件派生而来。派生可以很确定性的执行并且是可重复的,所以通过相同版本的报生代码来处理相同的事件日志将产生相同的状态更新。
清楚地控制数据流可以使数据的来掘管理更加清晰,从而使完整性检查更加可行。
确定的和定义清晰数据流也有助于系统调试和跟踪系统的操作,从而确定为什么发生了某些事情
端到端论点的再讨论
检查数据系统的完整性最好以端到端的方式进行:在完整性检查 中所包含的系统部件越多 ,则过程中某些阶段发生无告警的数据破坏的概率就越少。如果我们可以检查整个派生系统流水线是端到端正确的,那么路径中的任何磁盘、网络、 服务和算桂等 已经全部囊括在内了 。
审计数据系统的工具
使用加密工具来证明系统的完整性将是很有趣的,这种方式对于广泛的硬件和软件问题甚至是潜在的恶意行为都是可靠的。
做正确的事情
技术不合适或者技术本身有缺陷井不重要,重要的是如何使用它以及如何影响人们。
预测性分析
偏见与歧视
算法所做出的决定不一定比人类做得更好或更糟。
如果在算法的输入中存在系统性偏见,那么系统很可能吸收塔并在最终输出 中放大这种偏见
数据和模型只应该是我们的工具,而不是我们的主人。
责任与问责
盲目地相信数据至高无上不仅是误解的,而且是非常危险的 。随着数据驱动的决策变得越来越普遍,我们需要弄清楚如何使算陆更负责任和透明,如何避免强化现有的偏见,以及如何在错误不可避免时加以修复。
我们还需要弄清楚如何防止数据被滥用,井努力发挥数据的正面作用。
反馈环路
当预测分析影响人们的生活时,特别是由于自我强化反馈环路而出现一些有害问题。
数据隐私与追踪
除了预测分析方面的问题之外,数据收集本身也存在道德问题 。
监控
赞成与选择的自由
由于担心服务跟踪用户而决定拒绝使用,这只对极少数拥有足够的时间和知识来充分了解隐私政策的人群可以称得上是一种选择,并且他们可以不需要担心 由此可能会失去某些机会而被迫参与这些服务。然而,对于处境较差的人来说,选择自由没有意义 : 对他们来说,被监视变得不可避免。
数据隐私和使用
所谓隐私设置,即允许在线服务的用户控制哪些数据对他人可见,只是将一些控制权还给用户的起点。
数据作为资产和权力
即使我们认为有能力防止数据滥用,但是每当我们收集数据时,都需要平衡带来的好处和落入坏人手中的风险:计算机系统可能会被犯罪分子篡改,数据可能会被内部人员泄露,公司可能会落入不当价值观的无良管理层手中等。
记住工业革命
正如工业革命存在需要被管理的黑暗面一样,向信息时代的过搜也有需要面对和解决的重大问题。
立法与自律
我们应该允许每个人维护自己的隐私,即控制自己的数据而不是通过监视来窃取他们的控制权。
无所不在的监视和数据滥用并非不可避免,我们仍然能够阻止它。