本节书摘来自华章出版社《大数据系统构建:可扩展实时数据系统构建原理与最佳实践》一书中的第1章,第1.1节,南森·马茨(Nathan Marz) [美] 詹姆斯·沃伦(JamesWarren) 著 马延辉 向 磊 魏东琦 译,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1.6 全增量架构的问题
在最高的层次上,传统的架构如图1-3所示。
这种架构的特征是读/写数据库的使用以及随着新数据的可用,增量地维护这些数据库中数据的状态。例如,一个计算页面浏览量的增量方法,将通过对URL计数器加1来处理新的页面浏览量。这种架构的特征比关系型与非关系型更基础—事实上,几十年来,绝大多数的关系型和非关系型数据库都是用全增量架构来部署的。
值得强调的是,全增量架构应用得如此广泛,以至于许多人没有意识到可以使用另外一种不同的架构来避免它们的问题。这是熟悉的复杂性的典型示例—复杂性如此根深蒂固,以至于人们认为无法避免它。
研究全增量架构的问题意义重大。通过查看任何全增量架构带来的一般复杂性,我们将开始这个主题的探索。然后,我们将查看针对相同问题的两种截然不同的解决方案:一种是使用最好的全增量解决方案;另一种是使用Lambda架构的解决方案。你会发现,全增量的版本在各方面明显更加糟糕。
1.6.1 操作复杂性
在全增量架构中有许多内在复杂性,给操作生产基础架构造成了困难。这里我们将关注其中一个方面—需要读/写数据库来执行在线合并,以及为保证这项工作平稳运行所必须做的操作。
在读/写数据库中,随着磁盘索引会逐步增加和修改,但部分索引从未使用过。这些未使用的部分索引占用空间,最终需要被回收以防磁盘被填满。如果索引一旦变成未使用的,回收空间就立刻回收,那样付出的代价太高,所以空间在一个被称为合并的进程中不定期地被批量予以回收。
合并是一类集中操作。在合并过程中,服务器对CPU和磁盘有更高的要求,这大大降低了该时期机器的性能。众所周知,数据库(如HBase和Cassandra)都需要仔细地配置和管理,以免在合并时出现问题或服务器锁定。合并时性能的损失是一类甚至会导致级联故障的复杂性—如果太多机器同时执行合并操作,那么它们所支撑的负载将必须由集群中的其他机器处理。这可能使剩余的集群过载,导致彻底失败。这种失败已经出现过很多次了。
为了正确地管理合并,你必须在每个节点上都安排合并,以保证同一时间不会有太多节点受到影响;你必须知道一个合并操作会花多少时间以及时间的方差,以免有比预期更多的节点在执行合并;你必须确保节点上有足够的磁盘容量,以保证在合并期间它们能够维持正常的操作;你还必须确保集群有足够的容量,以保证在合并时资源丢失就不会造成过载。
所有这些都可以由一个合格的操作人员来管理,但我们的观点是,处理任何一种复杂性的最好方式是完全摆脱这种复杂性。系统失败模式越少,就越不可能遇到意外的故障时间。处理在线合并是全增量架构中固有的复杂性,但在Lambda架构中,主数据库不需要任何在线合并。
1.6.2 实现最终一致性的极端复杂性
试图让系统高可用会导致增量架构的复杂性。高可用系统甚至允许在机器或部分网络发生故障时进行查询和更新。
事实证明,实现高可用性将与另一个称为一致性的重要属性直接竞争。具备一致性的系统返回结果时,会考虑以前所有的写操作。CAP(Consistency、Availability、Partition tolerance)原理表明,在网络分区情况下,不可能在同一系统中同时实现高可用性和一致性,所以在一个网络分区中,高可用系统有时会返回陈旧的结果。
本书将在第12章深入讨论CAP原理—现在我们总是希望专注于不能实现的完全一致性和高可用性上,这会影响构建系统的能力。事实证明,如果业务需求中对高可用性的需要超过完全一致性,那么你必须处理大量的复杂性。
一旦网络分区结束,为了高可用性系统能回到一致性状态(即最终一致性),应用程序需要许多帮助,例如,在数据库中维护一个计数的基本用例,最显而易见的方法是在数据库中存储一个数值,当接收到需要加和的事件时,就做增量。你也许会很惊讶地发现,如果采取这种方法,在网络分区时,你会遇到海量数据丢失的情况。
导致这种情况的原因是,分布式数据库通过保存所有被存储信息的多个副本来实现高可用性。当你保存了相同信息的多份副本时,即使机器出现故障或网络分区,这些信息仍然可用,如图1-4所示。在网络分区时,选择成为高可用性的系统,只要副本是可获得的,就会有客户端的更新。这将导致副本产生分歧并接收不同的更新。只有分区消失,副本才可以合并成一个共同的值。
当网络分区开始时,假设有两个副本的计数为10。假设第一个副本得到两个增量,第二个副本得到一个增量。当这些副本合并在一起时,值分别为12和11,合并后的值应该是多少?虽然正确的答案是13,但是没有办法通过查看数值12和11得到该值。这两个数可能在11的时候产生分歧(在这种情况下,答案会是12),或者它们可能会在0的时候产生分歧(在这种情况下,答案是23)。
图1-4 使用副本增加可用性
为了做高可用性的、正确的计算,只存储一个计数是不够的。当值出现分歧时,你需要一个负责合并的数据结构,并且需要实现一段用于分区结束后对值进行修复的代码。为了维护一个简单的计数,你必须处理令人难以置信的复杂性。
一般来说,在增量、高可用性系统中处理最终一致性,是不直观的且容易出错的。在高可用、全增量系统中,这种复杂性是固有的。稍后你将看到Lambda架构本身是如何以一种不同的方式,极大地减少实现高可用性、最终一致性系统的负担的。
1.6.3 缺乏容忍人为错误
我们希望指出的全增量架构的最后一个问题是,它们天生缺乏容忍人为错误的特性。增量系统不断修改保存在数据库中的状态,这意味着即使是一个错误也可以修改数据库中的状态。因为错误是不可避免的,所以全增量架构的数据库肯定会受到破坏。
重要的是要注意,全增量架构中,这是不用重新思考架构就可以解决的少数复杂性之一。下面考虑如图1-5所示的两种架构:同步架构,应用程序直接更新数据库;异步架构,在后台更新数据库之前事件先进到一个队列中。在这两种情况下,每个事件都被永久地记录到事件的数据存储中。通过保存每个事件,如果是人为错误导致数据库被破坏,那么你可以返回到事件存储,为数据库重建正确的状态。因为事件存储是不可变且不断增长的,冗余校验,如权限,可以放入事件存储,这使得不可能因出现某个错误而影响事件存储。这种技术也是Lambda架构的核心,我们将在第2章和第3章深入讨论。
尽管附带日志记录的全增量架构可以解决无日志记录的完全增量架构对人为错误缺乏容忍的缺陷,但是日志记录对于前面讨论的其他复杂性于事无补。在下一节中你将看到,纯粹基于完全增量计算的各种架构,包括那些附带日志记录的架构,需要努力解决很多问题。
1.6.4 全增量架构解决方案与 Lambda架构解决方案
贯穿整本书实现的示例查询之一,适合作为全增量架构和Lambda架构的一个很好的对比。这个查询没有任何矫揉造作的地方—事实上,它是基于我们职业生涯中多次面临的现实生活中的问题的。这个查询用于处理网页浏览分析,并且完成对传入的两类数据的查询:
页面访问数据,包括用户ID、URL和时间戳。
等价数据,其中包含两个用户ID。一份等价数据表明两个用户ID是指同一个人。例如,你可能在电子邮箱sally@gmail.com和用户名sally之间有一份等价数据。如果sally@gmail.com也注册了用户名sally2,那么你在sally@gmail.com和sally2之间有一份等价数据。通过传递性,你会知道用户名sally和sally2指的是同一个人。
查询的目的是计算一段时间内对一个URL来说独立访客的数量。查询应该将所有这段时间的数据加起来,并以最小的延迟(少于100ms)来响应。查询的接口如下:
使得这个查询实现起来有些棘手的就是那些等价数据。如果在一个时间范围内,一个人用两个用户ID访问了相同的URL,通过等价数据的连接(甚至传递),这应该只算一次访问。一个新的等价数据的传入,可以改变任何URL在任何时间范围内任何查询的结果。
在这一点上,我们将不去展示解决方案的细节,因为必须提及非常多的概念(如索引、分布式数据库、批处理、HyperLogLog等)才能理解它们。此时让读者淹没在这些概念中往往会适得其反。相反,我们将专注于解决方案的特征和它们之间的显著差异。最佳完全增量解决方案将在第10章详细讨论,Lambda架构解决方案将在第8、9、14和15章中进行讨论。
两种解决方案可以在准确性、延迟和吞吐量三个轴上进行对比。Lambda架构解决方案在各方面表现得更胜一筹。这两种方案都必须实现近似值,但全增量版本被迫使用具有3~5倍甚至更糟糕的错误率的劣质近似技术。在全增量版本中执行查询的代价更高,并且会影响延迟和吞吐量。但这两种方案之间最显著的区别是:全增量版本需要使用特殊的硬件来实现接近合理的吞吐量。因为全增量版本必须做许多随机访问查找来解决查询问题,它实际上需要使用固态硬盘,以防磁盘寻道时间成为瓶颈。
Lambda架构可以生成在每个方面都具有更高性能的解决方案,同时也避免了困扰全增量架构的复杂性,展示了正在发生的非常根本的事情—关键是挣脱全增量计算的束缚,采用不同的技术。现在让我们看看Lambda架构是如何做到这一点的。