CAP定理指出数据库不能同时保证一致性,可用性和分区容错。但是你不能牺牲分区容忍度(见这里和这里),所以你必须在可用性和一致性之间进行权衡。管理这种权衡是NoSQL运动的核心焦点。
一致性意味着在您成功写入之后,将来的读取将始终考虑该写入。可用性意味着您始终可以读取和写入系统。在分区期间,您只能拥有其中一个属性。
选择一致性而非可用性的系统必须处理一些棘手的问题。数据库不可用时你会怎么做?您可以尝试稍后缓冲写入,但如果丢失了使用缓冲区的计算机,则可能会丢失这些写入。此外,缓冲写入可能是一种不一致的形式,因为客户端认为写入成功但写入不在数据库中。或者,您可以在数据库不可用时将错误返回给客户端。但是如果你曾经使用过一种告诉你“稍后再试”的产品,那么你知道这会让你感到恶心。
另一种选择是选择可用性而非一致性。这些系统可以提供的最佳一致性保证是“最终一致性”。如果您使用最终一致的数据库,那么有时您将阅读与您刚刚编写的结果不同的结果。有时多个读者同时阅读相同的密钥会得到不同的结果。更新可能不会传播到值的所有副本,因此您最终会获得一些副本以获得一些更新,而其他副本会获得不同的更新。一旦检测到值已经发散,就由您来修复该值。这需要使用矢量时钟追溯历史并将更新合并在一起(称为“读取修复”)。
我认为,在应用程序层中保持最终的一致性对于开发人员来说是一个沉重的负担。读取修复代码极易受开发人员错误的影响; 如果您犯了错误,错误的读取修复将在数据库中引入不可逆转的损坏。
因此牺牲可用性是有问题的,并且最终的一致性太复杂而不能合理地构建应用程序。然而这些是唯一的两个选择,所以我觉得如果你这样做你就该死,如果你不这样做就该死。CAP定理是自然界的事实,那么可能有什么替代方案呢?
还有另一种方式。您无法避免CAP定理,但您可以隔离其复杂性并防止它破坏您推理系统的能力。CAP定理引起的复杂性是我们如何处理建筑数据系统的基本问题的症状。特别突出的两个问题是:在数据库中使用可变状态以及使用增量算法来更新该状态。正是这些问题与CAP定理之间的相互作用导致了复杂性。
在这篇文章中,我将展示一个系统的设计,该系统通过防止它通常导致的复杂性来击败CAP定理。但我不会就此止步。CAP定理是关于数据系统对机器故障具有容错能力的结果。然而,有一种容错形式比机器容错更重要:人类的容错能力。如果在软件开发方面有任何确定性,那就是开发人员并不完美,而且bug将不可避免地达到生产。我们的数据系统必须对编写错误数据的错误程序具有弹性,而我将要展示的系统就像你可以获得的人类容错一样。
本文将挑战您对如何构建数据系统的基本假设。但是,通过打破我们当前的思维方式并重新构想如何构建数据系统,出现的结构是一种比您想象的更优雅,可扩展且更健壮的架构。
什么是数据系统?
在我们谈论系统设计之前,让我们首先定义我们试图解决的问题。数据系统的目的是什么?什么是数据?我们甚至无法开始接近CAP定理,除非我们能够用明确封装每个数据应用的定义来回答这些问题。
数据应用程序包括存储和检索对象,连接,聚合,流处理,连续计算,机器学习等等。目前尚不清楚数据系统有这么简单的定义 - 我们用数据做的事情似乎太多了,无法用一个定义来捕获。
但是,有这么简单的定义。就是这个:
Query = Function(All Data)
而已。该等式总结了数据库和数据系统的整个领域。该领域的所有内容 - 过去50年的RDBMS,索引,OLAP,OLTP,MapReduce,ETL,分布式文件系统,流处理器,NoSQL等 - 都以这种方式以某种方式进行了总结。
数据系统回答有关数据集的问题。这些问题称为“查询”。此等式表明查询只是您拥有的所有数据的函数。
这个等式似乎太笼统而无用。它似乎没有捕获任何数据系统设计的复杂性。但重要的是每个数据系统都属于这个等式。该等式是我们可以探索数据系统的起点,该等式最终将导致一种击败CAP定理的方法。
这个等式中有两个概念:“数据”和“查询”。这些是在数据库领域经常混淆的不同概念,所以让我们严格理解这些概念的含义。
数据
让我们从“数据”开始。一段数据是一个不可分割的单位,你认为它是真实的,除了存在之外别无他法。这就像数学中的公理。
关于数据有两个关键属性需要注意。首先,数据本质上是基于时间的。一段数据是你知道在某个时刻是真实的事实。例如,假设莎莉进入她居住在芝加哥的社交网络档案。您从该输入中获取的数据是她在芝加哥生活的特定时刻,她将该信息输入到她的个人资料中。假设Sally稍后将她的个人资料位置更新到亚特兰大。然后你知道她在特定时间住在亚特兰大。她现在住在亚特兰大的事实并没有改变她过去居住在芝加哥的事实。两条数据都是真的。
数据的第二个属性紧跟在第一个属性之后:数据本质上是不可变的。由于它与某个时间点的连接,一段数据的真实性永远不会改变。人们无法及时回过头来改变一段数据的真实性。这意味着您只能对数据执行两项主要操作:读取现有数据并添加更多数据。CRUD已成为CR。
我省略了“更新”操作。这是因为更新对于不可变数据没有意义。例如,“更新”Sally的位置确实意味着您正在添加一条新数据,表示她最近一次住在新位置。
我也省略了“删除”操作。同样,大多数删除情况更好地表示为创建新数据。例如,如果Bob在Twitter上停止关注Mary,那么这并不会改变他过去跟随她的事实。因此,不是删除他跟随她的数据,而是添加一条新的数据记录,表明他在某个时刻没有跟踪她。
在某些情况下,您确实希望永久删除数据,例如要求您在一定时间后清除数据的法规。我将要展示的数据系统设计很容易支持这些情况,因此为了简单起见,我们可以忽略这些情况。
这种数据定义几乎肯定与您习惯的不同,特别是如果您来自关系数据库世界,其中更新是常态。有两个原因。首先,这种数据定义非常通用:很难想到一种不符合这个定义的数据。其次,数据的不变性是我们在设计胜过CAP定理的人类容错数据系统时将要利用的关键属性。
询问
等式中的第二个概念是“查询”。查询是一组数据的派生。从这个意义上讲,查询就像是数学中的一个定理。例如,“Sally目前的位置是什么?” 是一个查询。您可以通过返回有关Sally位置的最新数据记录来计算此查询。查询是完整数据集的功能,因此它们可以执行任何操作:聚合,将不同类型的数据连接在一起,等等。因此,您可能会询问服务的女性用户数量,或者您可能会查询推文数据集,了解过去几个小时内哪些主题趋势。
我已经将查询定义为完整数据集上的函数。当然,许多查询不需要运行完整的数据集 - 它们只需要数据集的子集。但重要的是我的定义包含了所有可能的查询,如果我们要打败CAP定理,我们必须能够为任何查询做到这一点。
击败CAP定理
计算查询的最简单方法是在整个数据集上运行一个函数。如果您可以在延迟限制内完成此操作,那么您就完成了。没有别的东西要建造。
当然,期望完整数据集上的函数快速完成是不可行的。许多查询(例如服务于网站的查询)需要毫秒的响应时间。但是,让我们假装您可以快速计算这些函数,让我们看看这样的系统如何与CAP定理相互作用。正如您将要看到的,这样的系统不仅击败了CAP定理,而且还消灭了它。
CAP定理仍然适用,因此您需要在一致性和可用性之间做出选择。美丽的是,一旦你决定了你想做的权衡,你就完成了。通过使用不可变数据和从头开始计算查询,可以避免CAP定理通常导致的复杂性。
如果您选择一致性而非可用性,那么之前没有太大的变化。有时您将无法读取或写入数据,因为您可以获得可用性。但对于必须采用刚性一致性的情况,这是一种选择。
当您选择可用性超过一致性时,事情变得更有趣。在这种情况下,系统最终是一致的,没有任何最终一致性的复杂性。由于系统具有高可用性,因此您始终可以编写新数据和计算查询。在故障情形中,查询将返回不包含先前写入的数据的结果。最终,数据将保持一致,查询将把这些数据合并到他们的计算中。
关键是数据是不可变的。不可变数据意味着没有更新这样的东西,因此一条数据的不同副本不可能变得不一致。这意味着没有发散值,矢量时钟或读取修复。从查询的角度来看,一段数据存在或不存在。该数据只有数据和功能。您无需采取任何措施来强制执行最终的一致性,最终的一致性不会妨碍对系统的推理。
造成复杂性的原因是增量更新和CAP定理之间的相互作用。增量更新和CAP定理真的不能很好地结合在一起; 可变值需要在最终一致的系统中进行读取修复。通过拒绝增量更新,包含不可变数据以及每次从头开始计算查询,您可以避免这种复杂性。CAP定理被打败了。
当然,我们刚刚经历的是思想实验。虽然我们希望每次都能从头开始计算查询,但这是不可行的。但是,我们已经了解了真实解决方案的一些关键属性:
- 该系统可以轻松存储和扩展不可变的,不断增长的数据集
- 主要的写操作是添加新的不可变数据事实
- 该系统通过从原始数据重新计算查询来避免CAP定理的复杂性
- 系统使用增量算法将查询延迟降低到可接受的水平
让我们开始探索这样一个系统的样子。请注意,从现在开始的所有内容都是优化。数据库,索引,ETL,批量计算,流处理 - 这些都是优化查询功能并将延迟降低到可接受水平的技术。这是一个简单而深刻的实现。数据库通常被认为是数据管理的核心,但实际上它们只是大局的一部分。
批量计算
弄清楚如何在任意数据集上快速运行任意函数是一个令人生畏的问题。让我们放松一下这个问题。让我们假装查询过时几个小时就可以了。通过这种方式解决问题,可以为构建数据系统提供简单,优雅且通用的解决方案。之后,我们将扩展解决方案,以便不再放松问题。
由于查询是所有数据的函数,因此使查询快速运行的最简单方法是预先计算它们。每当有新数据时,您只需重新计算所有内容。这是可行的,因为我们放宽了问题,允许查询过时几个小时。以下是此工作流程的说明:
要构建它,您需要一个系统:
- 可以轻松存储大量且不断增长的数据集
- 可以以可伸缩的方式计算该数据集上的函数
存在这样的系统。它成熟,经过数百个组织的战斗测试,拥有庞大的工具生态系统。它被称为Hadoop。Hadoop 并不完美,但它是进行批处理的最佳工具。
很多人会告诉你,Hadoop只适用于“非结构化”数据。这完全是假的。Hadoop非常适合结构化数据。使用Thrift或Protocol Buffers等工具,您可以使用丰富,可演化的模式存储数据。
Hadoop由两部分组成:分布式文件系统(HDFS)和批处理框架(MapReduce)。HDFS擅长以可扩展的方式跨文件存储大量数据。MapReduce擅长以可扩展的方式对该数据运行计算。这些系统完全符合我们的需求。
我们将数据存储在HDFS上的平面文件中。文件将包含一系列数据记录。要添加新数据,只需将包含新数据记录的新文件附加到包含所有数据的文件夹即可。在HDFS上存储这样的数据解决了“存储大量且不断增长的数据集”的要求。
从该数据预先计算查询同样简单明了。MapReduce是一种富有表现力的范例,几乎任何函数都可以作为一系列MapReduce作业来实现。像工具Cascalog,层叠和猪使实现这些功能变得更加容易。
最后,您需要索引预计算的结果,以便应用程序可以快速访问结果。有一类非常擅长的数据库。ElephantDB和Voldemort只读专用于从Hadoop导出键/值数据以进行快速查询。这些数据库支持批量写入和随机读取,并且它们不支持随机写入。随机写入会导致数据库中的大多数复杂性,因此通过不支持随机写入,这些数据库非常简单。例如,ElephantDB只有几千行代码。这种简单性使得这些数据库非常强大。
让我们看一下批处理系统如何组合在一起的例子。假设您正在构建一个跟踪页面视图的Web分析应用程序,并且您希望能够在任何时间段内查询页面视图的数量,精确到一小时。
实现这一点很容易。每个数据记录包含单个页面视图。这些数据记录存储在HDFS上的文件中。按小时为每个URL卷起页面视图的函数实现为一系列MapReduce作业。该函数发出键/值对,其中每个键是一[URL, hour]
对,每个值是页面查看次数的计数。这些键/值对将导出到ElephantDB数据库中,以便应用程序可以快速获取任何[URL, hour]
对的值。当应用程序想知道某个时间范围的页面查看次数时,它会向ElephantDB查询该时间范围内每小时的页面查看次数,并将它们相加以获得最终结果。
批处理可以计算任意数据的任意函数,但缺点是查询已经过时了几个小时。这种系统的“任意性”意味着它可以应用于任何问题。更重要的是,它简单易懂,完全可扩展。您只需要考虑数据和功能,Hadoop负责并行化。
批处理系统,CAP和人为容错
到现在为止还挺好。那么我所描述的批处理系统如何与CAP排成一行,它是否符合我们人类容错的目标?
让我们从CAP开始吧。批处理系统最终以最极端的方式保持一致:写入总是需要几个小时才能合并到查询中。但它是一种易于推理的最终一致性形式,因为您只需考虑该数据的数据和功能。没有读取修复,并发或其他复杂问题需要考虑。
接下来,让我们来看看批处理系统的人为容错。批处理系统的人体容错能力就是您所能获得的。在这样的系统中,人类只能犯两个错误:部署错误的查询实现或编写错误数据。
如果您部署了一个错误的查询实现,那么您需要做的就是修复错误,部署固定版本,并重新计算主数据集中的所有内容。这是有效的,因为查询是纯函数。
同样,编写错误数据有明确的恢复途径:删除错误数据并再次预先计算查询。由于数据是不可变的并且主数据集是仅附加的,因此写入错误数据不会覆盖或以其他方式破坏良好数据。这与几乎所有传统数据库形成鲜明对比,如果您更新密钥,则会丢失旧值。
请注意,MVCC和类似HBase的行版本控制并未接近此级别的人为容错。MVCC和HBase行版本控制不会永久保存数据:一旦数据库压缩了行,旧值就会消失。只有不可变数据集才能保证在写入错误数据时有一条恢复路径。
实时图层
信不信由你,批处理解决方案几乎解决了实时计算任意数据上任意函数的完整问题。任何早于几个小时的数据都已合并到批处理视图中,因此剩下要做的就是补偿最后几个小时的数据。弄清楚如何针对几小时的数据实时查询,这比完整数据集更容易。这是一个重要的见解。
要补偿这几小时的数据,您需要一个与批处理系统并行运行的实时系统。实时系统预先计算最近几小时数据的每个查询函数。要解析查询功能,请查询批处理视图和实时视图,并将结果合并在一起以获得最终答案。
实时层是您使用Riak或Cassandra等读/写数据库的地方,实时层依赖于增量算法来更新这些数据库中的状态。
用于实时计算的Hadoop模拟是Storm。我编写了Storm,以便以可扩展和健壮的方式轻松地进行大量实时数据处理。Storm在数据流上运行无限计算,并为数据处理提供强有力的保证。
让我们回到查询一段时间内URL的页面查看次数的运行示例,看一下实时层的示例。
批处理系统与以前相同:基于Hadoop和ElephantDB的批处理工作流预先计算除最后几小时数据之外的所有内容的查询。剩下的就是构建实时系统来补偿最后几小时的数据。
我们将最近几个小时的统计信息汇总到Cassandra中,我们将使用Storm处理页面浏览量流并将更新并行化到数据库中。每个网页浏览都会导致一个计数器,[URL, hour]
以便在Cassandra中增加一个密钥。这就是它的全部 - 风暴让这些事情变得非常简单。
批处理层+实时层,CAP定理和人体容错
在某些方面,似乎我们回到了我们开始的地方。实现实时查询需要我们使用NoSQL数据库和增量算法。这意味着我们又回到了不同价值观,矢量时钟和读取修复的复杂世界。
但是有一个关键的区别。由于实时层仅补偿最后几小时的数据,因此实时层计算的所有内容最终都会被批处理层覆盖。因此,如果您在实时图层中出错或出现问题,批处理层将对其进行更正。所有复杂性都是短暂的。
这并不意味着您不应该关心实时层中的读取修复或最终一致性。您仍然希望实时图层尽可能一致。但是当你犯错误时,你不会永久性地破坏你的数据。这消除了肩膀上巨大的复杂负担。
在批处理层中,您只需要考虑该数据的数据和功能。批处理层很容易理解。另一方面,在实时层中,您必须使用增量算法和极其复杂的NoSQL数据库。将所有复杂性分离到实时层中,可以在制作强大,可靠的系统方面产生巨大的差异。
此外,实时层不会影响系统的人体容错能力。批处理层中仅附加的不可变数据集仍然是系统的核心,因此可以像以前一样恢复任何错误。
让我分享一个关于隔离实时层复杂性的巨大好处的个人故事。我的系统非常类似于我在这里描述的系统:批处理层的Hadoop和ElephantDB,以及实时层的Storm和Cassandra。由于我的监控不力,有一天我醒来发现Cassandra已经用完空间并且每次请求都超时。这导致我的Storm拓扑失败,并且数据流备份在队列中。相同的消息一遍又一遍地重放(并保持失败)。
如果我没有批处理层,我将被迫扩展并恢复Cassandra。这不重要。更糟糕的是,由于多次重播相同的消息,大部分数据库可能都不准确。
幸运的是,所有这些复杂性都在我的实时层中被隔离。我将备份的队列刷新到批处理层,并创建了一个新的Cassandra集群。批处理层像发条一样运行,几个小时内一切都恢复正常。没有数据丢失,我们的查询没有任何不准确之处。
垃圾收集
我在这篇文章中描述的所有内容都是建立在不断变化的,不断增长的数据集的基础之上的。那么,如果您的数据集太大而无法一直存储所有数据,即使使用水平可扩展存储,您又该怎么办?这个用例是否打破了我所描述的一切?你应该回到使用可变数据库吗?
不。使用“垃圾收集”扩展基本模型很容易处理这个用例。垃圾收集只是一个接收主数据集并返回主数据集的过滤版本的函数。垃圾收集摆脱了低价值的数据。您可以使用任何策略进行垃圾回收。您可以通过仅保留实体的最后一个值来模拟可变性,也可以保留每个实体的历史记录。例如,如果您要处理位置数据,您可能希望每个人每年保留一个位置以及当前位置。可变性实际上只是一种不灵活的垃圾收集形式(它与CAP定理的交互也很差)。
垃圾收集作为批处理任务实现。这是你偶尔跑步的东西,也许是每月一次。由于垃圾收集作为离线批处理任务运行,因此不会影响系统与CAP定理的交互方式。
结论
使可扩展数据系统困难的原因不是CAP定理。它依赖于增量算法和可变状态,导致我们系统的复杂性。直到最近,随着分布式数据库的兴起,这种复杂性已经失去控制。但这种复杂性一直存在。
我在本文开头说过,我将挑战你对如何构建数据系统的基本假设。我把CRUD变成了CR,将持久性分成了单独的批处理和实时系统,并且痴迷于人类容错的重要性。多年来,经历了许多来之不易的经历,打破了我的旧假设并得出了这些结论。
批处理/实时架构有很多有趣的功能,我还没有介绍。现在值得总结其中一些:
- 算法灵活性:某些算法难以逐步计算。例如,如果uniques集合变大,则计算唯一计数可能具有挑战性。批处理/实时拆分使您可以灵活地在批处理层上使用精确算法,并在实时层上使用近似算法。批处理层不断覆盖实时层,因此近似值得到纠正,系统显示出“最终准确性”的属性。
- 架构迁移很简单:架构迁移困难的日子已经一去不复返了。由于批处理计算是系统的核心,因此可以轻松地在完整数据集上运行函数。这样可以轻松更改数据或视图的架构。
- 轻松的临时分析:批处理层的任意性意味着您可以在数据上运行任何您喜欢的查询。由于可以在一个位置访问所有数据,因此这很简单方便。
- 自我审核:通过将数据视为不可变数据,您可以获得自我审核数据集。数据集记录自己的历史记录。我已经讨论过这对于人类容错有多重要,但它对于进行分析也非常有用。
我并没有声称已经“解决”了大数据领域,但我已经制定了思考大数据的框架。批处理/实时体系结构非常通用,可以应用于任何数据系统。我告诉你如何为任何种类的鱼和任何水制作钓竿,而不是给你一条鱼或钓鱼竿。
还有很多工作要做,以提高我们攻击大数据问题的集体能力。以下是一些重要的改进领域:
- 批量可写,随机读取数据库的扩展数据模型:并非每个应用程序都受键/值数据模型的支持。这就是为什么我的团队正在投资扩展ElephantDB以支持搜索,文档数据库,范围查询等。
- 更好的批处理原语:Hadoop并不是批处理计算的最终目的。对于某些类型的计算,它可能是低效的。Spark是一个重要的项目,在扩展MapReduce范例方面做了有趣的工作。
- 改进的读/写NoSQL数据库:有更多数据库具有不同数据模型的空间,这些项目通常将受益于更成熟。
- 高级抽象:未来工作中最有趣的领域之一是高级抽象,映射到批处理组件和实时处理组件。没有理由不应该将声明性语言的简洁性与批处理/实时体系结构的健壮性结合起来。
很多人都想要一个可扩展的关系数据库。我希望你在这篇文章中意识到的是你根本不需要它!大数据和NoSQL运动似乎使数据管理比RDBMS更复杂,但这仅仅是因为我们试图像处理RDBMS数据一样处理“大数据”:通过混合数据和视图以及依赖关于增量算法。大数据的规模使您可以以完全不同的方式构建系统。通过将数据存储为不断扩展的不可变事实集并将重新计算构建到核心中,大数据系统实际上比关系系统更容易推理。它会扩展。