终于来到这本书最后的一章了《Designing Data-Intensive Applications》大部头,这本书应该是我近两年读过最棒的技术书籍。作者Martin Kleppmann帮助我们梳理了数据系统的纷繁复杂的技术逻辑,在这本书的最后,他将带领我们瞭望数据系统的未来,虽然预测未来是一件很主观的事情,但是,立足于现在去展望未来是可能的事情。所以现在,扬帆起航,让咱们来一起看看作者 Martin Kleppmann 眼中数据系统的未来、
1.因地制宜的工具
对于任何给定的数据问题,总会有多种解决方案。所有这些解决方案都会有不同的优缺点和权衡。因此,最合适的软件工具选择也要视情况而定。每一个软件,甚至一个所谓的“通用”数据库,都是为特定的使用模式而设计的。所以,在复杂的应用程序中,数据工具通常会串联起来共同工作。不存在有一个软件适合于使用数据的所有不同环境,因此不可避免地要将几个不同的软件串联在一起,以便更好帮助应用程序工作。
数据流
当需要在多个数据系统中维护相同的数据副本时,为了满足不同的存储需求时,需要非常明确的界定系统的输入和输出:首先写入的数据在何处,哪些表示来自哪个来源?
举个栗子:数据通常会首先写入数据库系统,之后捕获对数据库的更改,然后按相同顺序将更改应用到搜索索引之中。如果捕获数据更改是更新索引的唯一方法,那么就可以确信索引的数据流完全来自数据库,因为数据库的写入操作是向系统提供输入的唯一方法。但是假若允许应用程序直接写入搜索索引,由两个不同数据源同时发送写请求,就很容易出现写冲突,则很容易导致数据的出现不一致,后续需要花大量的功夫来避免这些不一致性。所以通过单个系统来输入所有的输入,那么通过以相同的顺序处理写操作,就可以更容易地派生出数据的其他表示形式。维护程序的输入流很大程度上会简化数据系统的模型。
从另一个角度来说,使用派生数据流的方式同样能够实现分布式事务。在抽象的层面上,通过不同的方法达到了同样的目标。分布式事务通过一个互斥锁,与原子提交来确保数据一致性,而数据流通过日志进行操作排序与操作幂等性来确保数据一致性。 两者最大区别在于:事务能够提供线性化,也就是最强的一致性等级。而数据流的派生系统通常是异步执行的,很难提供最终的时间担保。。所以在愿意承担分布式事务成本的环境中,事务是最优的选择,但是也真是因为分布式事务的成本,严重限缩了它的实用性。
作者认为:在缺乏对良好的分布式事务协议的广泛支持的情况下,基于数据流的派生数据系统是来集成不同数据系统的或许是将来能够大放异彩的方法。(数据流派生系统本质上是最终一致性的模型,所以作者显然看好最终一致性的模型是未来数据系统的发展趋势,最终一致性的模型在对一致性要求不严格的场景下确实是大大提高开发效率与简化系统模型的好方式。)
有序日志的限制
在一个较小的数据系统之中,构建一个有序的事件日志是完全可行的。然而,随着系统向着更大和更复杂的系统扩展时,会出现一些问题。在绝大多数的情况下,构造一个完全有序的日志需要所有事件都通过一个决定顺序的Leader 节点,但如果事件产生的吞吐量大于一台机器所能处理的吞吐量,则需要在多台机器上进行日志分区。此时问题就出现了,两个不同分区日志之中的事件顺序是不明确的。
而如果服务器分布在多个地理上分散的数据中心,为了降低数据中心通信延迟的情况下,可以在每个数据中心的设一个独立的Leader,而因为网络延迟等问题。两个不同的数据中心的事件日志也很难进行排序,一个未定义的排序。同样的,如前文的例子一样,当两个事件源于不同的数据系统,事件是很难定义顺序的。
全序事件的广播,本质上相当于协商一致性。而绝大多数共识算法都是针对单个节点的吞吐量足以处理整个事件流的情况而设计的,而这些算法并没有提供多个节点共享事件排序工作的机制。所以设计一致性算法的问题仍然是一个开放的研究问题,它可以超越单个节点的吞吐量,并且在地理分布的环境中工作得很好。
2.数据的计算
数据系统本质的目标是确保数据以正确的形式出现在所有正确的地方。这样做需要工程师做出很大的努力,处理输入、转换、连接、过滤、聚合、训练模型、评估,并最终产生恰当的输出。
批处理和流处理
批处理和流处理的输出的都是派生的数据集,如搜索索引、物化视图、向用户显示的建议等。批处理和流处理有许多共同的原则,主要的根本区别是流处理器在无界数据集上操作,而批处理输入是已知的、有限的大小数据。
函数式状态
批处理具有相当强的函数式功能性:它鼓励确定性的纯函数,其输出只依赖于输入,而不会产生额外的副作用。流处理,保持了函数性,并且扩展了操作符,所以可以通过重新计算实现容错。 具有明确的输入和输出确定性函数的原理不仅是容错性好,还简化了数据流的推断过程,所以很适合作为派生数据系统的输入。
派生的数据系统可以同步维护,就像关系数据库在同一事务中同步更新次要索引一样,将其写入索引表中。但是,异步模型同样可以用日志实现容错,而不会使错误扩张, 而分布式事务在任何一个参与者失败时中止,因此分布式事务会扩展到系统的其余部分导致故障的放大。
渐变式过渡
在维护派生数据系统时,批处理和流处理可以结合使用。 流处理可以输入的变化时低延迟的反馈在新视图之上,而批处理允许积累的大量历史数据进行再进行处理以获得新的数据集。
通过批处理与流处理作为工具组合,可以对现有数据进行再处理,并将其演进为支持新特性和新需求的数据系统。并将数据集重构为完全不同的模型,以便更好地满足新的需求。 派生视图允许渐进演化: 如果要重构数据集,则不需要突然切换来执行迁移。相反的是,可以将旧模式和新模式并排为建立在同一个数据集上的两个独立的派生视图。一开始,可以开始将少量用户转移到新的数据视图中,以便测试其性能并发现任何bug,而大多数用户继续被使用旧的数据视图。逐步地进行渐变式的过渡,增加访问新数据视图的用户比例,最终删除旧的数据视图。
这种渐进迁移的美妙之处在于:如果出现了问题,这个过程的每个阶段都是可逆的。
Lambda架构
Lambda体系结构是目前分布式计算领域流行的一个解决思路,它的核心思想是:通过将不可变事件附加到不断增长的数据集之上,并从这些事件中派生出读取优化的视图。Lambda体系结构建议并行运行两个不同的系统:一个批处理系统,如MapReduce和一个单独的流处理系统,如Storm。在Lambda体系之中,流处理器消耗事件并快速生成对视图的近似更新;而批处理处理器随后消耗相同的事件集,并生成派生视图的修正版本。 这种设计背后的原因是:批处理更简单,因此不容易出现bug,而流处理器被认为不可靠,难以实现容错。流处理可以使用快速近似算法,而批处理过程使用较慢的精确算法。
但是Lambda架构也存在一些实际的问题:
必须保持流处理与批处理具有相同的逻辑。
由于流处理和批处理都产生单独的输出,进行数据合并的逻辑可能会相对复杂。
3.再探计算与存储
存储的最终目的是为了服务计算,计算最终的结果也可以写入存储。所以我们再在一个更高的角度,重新看存储与计算。
元数据库
整个系统之中的所有数据流开始看起来像一个巨大的数据库系统。每当批处理、流处理或ETL将数据从一个地方传输到另一个地方时,类似于数据库子系统,保持索引或物化视图的最新状态。批处理和流处理类似于触发器或存储过程,通过它们来维护的派生数据系统。简单来说,数据系统本质就是由一个数据源衍生出的多个各司其职的异构数据子系统,将多个异构的数据子系统如何协调,同步写入,是工程实践之中最为复杂的部分。
同步写入
分布式事务是在异构存储系统之中同步写入的传统方法,单个存储事件使用事务可行的,但是当数据需要跨越不同技术之间的边界时,幂等写的异步事件日志是一种更加健壮和实用的方法。 因为在缺乏标准化的事务协议会使集成各个数据系统成为一个困难的问题,而一个有序的记录日志与幂等的写操作,是一个简单与松散耦合的抽象化方式。
异步事件流可以使作为子系统的单个组件的更为健壮。如果用户运行缓慢或失败,事件日志可以缓冲消息,允许生产者和其他任何用户继续不受影响地运行。相比之下,分布式事务往往会将本地故障升级为大规模故障。
通过松耦合的方式,独立的不同团队允许数据系统允许不同的软件组件和服务的彼此独立的开发、改进、维护。松耦合可以使每个团队都能专注于做一件事情,与其他团队的系统定义良好的接口。
事件日志提供了一个强大的接口,由于事件的持久性和顺序的特点,可以确保多个数据系统之间的一致性。
写路径与读路径
在派生数据的过程之中(如搜索索引、物化视图和预测模型),会通过一系列的写的操作,并使它们保持最新。这个过程称为写路径:每当将一段信息写入系统时,它可能经过批处理和流处理的多个阶段,并最终对每个派生数据集进行更新,以合并所写的数据。而读路径:为用户请求服务时,从派生数据集中读取数据时,需要对结果执行更多的处理,并构造对用户的响应。
写路径与读路径共同串联包含了数据由产生到消费的全程。写路径是数据系统预先计算的部分,类似于渴望求值。读路径是读取时实时处理的部分,只有当用户使用时才会发生,类似于惰性求值。
读路径与写路径的权衡
全文检索的索引就是一个很好的例子:写路径更新索引,读路径搜索索引关键字。 读写都需要做一些工作。写入需要更新文档中出现的所有项的索引项。读取需要搜索查询中的单词。如果没有索引,搜索查询需要扫描所有文件,当读取大量的文件代价十分昂贵。所以当写路径上的工作更少时,但读路径上的工作要多得多。(无论如何都没有办法偷懒)
所以,缓存、索引和物化视图的作用很简单:通过改变读路径与写路径之间的边界。通过在写路径做更多的工作,通过预先计算的结果,为了节省在读路径的代价。
端到端的订阅模式
传统的编程模型都是建立在请求/响应的读写路径逻辑之上,这种方式相对来说读路径相对较长。 而通过数据生产端到数据消费端的发布订阅的模型允许服务器将状态更改事件推入客户端,这是一种更新的思路。状态变化可以通过一个端到端的写路径:从一个触发状态变化的设备上的交互,通过事件日志和几个派生的数据系统,一直推送到另一个设备上观察状态的用户界面。这些状态变化可以用相当低的延迟传播,有些应用程序,如即时消息和网络游戏,已经有了这样一个“实时”架构。所以未来为什么不用这种方式构建所有应用程序呢?
通过将写入路径扩展到最终用户,需要从根本上重新考虑我们构建这些系统的方式,更具响应性的用户界面和更好的离线支持的优势将使端到端的订阅模型值得尝试。
4.数据系统的正确性
ACID事务能够提供时效性(例如,线性化)和完整性(例如,原子提交)的担保。
而异步事件日志同样也需要有效地保持完整性的机制。如果某个事件丢失,或者某个事件发生两次,则可能违反数据系统的完整性。因此,容错的消息传递和幂等操作对于维护数据系统在故障面前的完整性是非常重要的。
通过一个客户端生产唯一的ID,让数据处理的全程都有一个唯一标识,使端到端的保持事件不会重复处理,并且由了幂等性的保障。这种机制,在未来建立容错应用的一个非常有前途的方向。(目前实际工业界许多系统都采用类似的机制,笔者之前参加美图技术团队的技术分享会,美图团队的美拍交易系统的构建就是建立在Kafka构建的异步事件流的基础之上。)
验证正确性
数据系统总是会出错的,无论是任何的系统模型。例如,进程可能会崩溃,机器会突然失去功率,网络会出现延迟。但我们也认为,写入磁盘的数据不会丢失后,内存中的数据不会被破坏,CPU的乘法指令总是返回正确的结果。
不要盲目地相信,硬件和软件会符合我们所期望的理想,数据可能会出现问题。所以,我们需要找到一种方法,来审视数据是否已被损坏,以便能够修复它,并设法找出错误的来源。成熟的系统同样倾向于考虑出错的可能性,并管理风险。例如,大规模存储系统如HDFS和Amazon S3并不完全相信磁盘:他们运行的后台进程的不断对比检查文件,和其他副本进行比较,并进行修复。
在未来我们会看到更多的自我验证或自我审核的数据系统,不断检查自己的完整性,而不是依赖盲目信任数据系统。数据库的事务导致了我们对盲目的相信ACID,而忽略了在这个过程中。
而随着弱一致性的保证了与NoSQL数据库的发展,和不太成熟的存储技术也得到了广泛的应用,这些技术会更加依赖数据的正确性的审计。
小结:
完结撒花~~~谢谢大家一路的支持和陪伴。在这本书的最后一章,作者带领我们展望自己对于未来数据系统发展的看法,并且对前文进行了总结与回顾。并且花了很大的篇幅告诫了读者作为数据系统开发人员的一些伦理选择,必须以人道和尊重对待这些数据。用户也是人,人的尊严是至高无上的。这本书的结尾让我很感动,技术并不是冷冰冰的,而是让人感觉温暖而有意义的。这本书是我在硕士生阶段阅读的最棒的一本书,(另一本应该是来自松本行弘的《代码的未来》,同样让人感受到作者的高山仰止,Ruby也是一门让人感觉有幸福感的语言)在这里感谢未来狼厂的赵纯前辈的推荐。