概览
随着爱彼迎及其技术栈的演进,我们需要一个可靠并可扩展的基础能力来简化对复杂数据集的访问和处理。于是便有了 Riverbed,一个为快速读取性能和高可用性设计的数据框架。在本文中,我们将会介绍 Riverbed,包括其目标、设计以及特性。
为什么创建 Riverbed
随着爱彼迎的增长,我们管理的数据库数量加速增加,它们服务的数据类型也越来越多样化,同时,访问这些数据库的数据密集型服务也在增加,这使得数据基础设施变得复杂,而且管理起来也变得困难,因为它采用了难以管理的服务导向架构 (Service-Oriented Architecture, SOA)。
图 1. 爱彼迎的SOA依赖关系图
我们观察到了一种特定的查询模式,它涉及访问多个数据源、有复杂的业务逻辑,且涉及难以优化的复杂数据转换。爱彼迎在读取路径上大量利用这些查询,这使得性能问题更加严重。
让我们来看看爱彼迎的支付系统在从单体架构转变为 SOA 后面临的挑战。爱彼迎的支付系统非常复杂,需要访问多个数据源,并需要复杂的业务逻辑来计算费用、交易日期、货币、金额和总收入。然而,当支付系统迁移到 SOA 后,这些计算所需的数据散布在各个服务和表中。这使得以简单且高效的方式提供所有必要的信息变得困难,特别是对于读取密集型的请求。
对此,一种可能的解决方案是记录最常使用的查询,预先计算好所需要的非规范化的支付数据,并提供一个存储计算结果的表,从而对读取密集型请求进行优化。这就是所谓的物化视图,许多数据库都提供这样的内置功能。
在 SOA 环境中,数据分布在多个数据库中,我们创建的视图依赖于来自各种数据库的数据。这种技术在业界广为采用,通常使用数据变更捕捉 (Change-Data-Capture, CDC)、流处理和数据库存储最终结果的组合来实现。
Lambda 和 Kappa 是两种实时数据处理架构。Lambda 结合了批处理和实时处理,以便高效处理大规模数据,而 Kappa 则完全专注于流处理。尽管Kappa 的简单性提供了更好的可维护性,但它在实现回填机制和确保数据一致性方面,特别是处理乱序事件时,带来了挑战。
为了解决这些挑战,并简化分布式物化视图的构建和管理,我们开发了 Riverbed。Riverbed 是一个类似于 Lambda 的数据框架,可以抽象出维护物化视图的复杂性,从而加快产品迭代。接下来的部分,我们将讨论 Riverbed 的设计选择,以及为了实现高性能、可靠性和一致性目标而做出的取舍。
Riverbed 设计
概览
总的来说,Riverbed 采用了 Lambda 架构,它包括一个处理实时事件变化的在线组件和一个补充缺失数据的离线组件。Riverbed 为产品工程师提供了一种声明性接口,用于定义查询并使用 GraphQL 为在线和离线组件实现计算的业务逻辑。在底层,框架有效地执行查询,计算派生数据,最终将其写入一个或多个指定的接收方。Riverbed 处理了一些数据密集型系统的常见挑战,如并发写入、版本控制、与爱彼迎的各种基础设施组件的集成、数据正确性保证,最终使产品团队能够快速迭代产品特性。
流处理系统
图 2. 流处理系统
流处理系统的主要功能是解决对记录表进行更改时出现的增量视图构建问题。为了实现这一目标,系统通过消费变化数据捕获(CDC,Change-Data-Capture)事件,将这些事件转化为“通知”触发器,这些触发器与接收器中的特定文档 ID 相关联。一个“通知”触发器的作用是刷新特定的文档。这个过程以高度并行的方式进行,其中的通知触发器在批量消费者中进行去重后写入 Kafka。
接着,第二个处理流程开始消耗先前生成的“通知”触发器。通过一系列的表连接,数据拼接,以及执行用户指定的操作,"通知"会被转换为一个文档。然后,该文档会被传输至指定的接收端。每当一个记录系统的表格发生变动时,系统会将受影响的文档替换为更新的版本,确保了最终的一致性。
批处理系统
尽管如此,无论是在整个流程中还是由于 CDC 的 故障,仍然可能偶尔丢失事件。为了解决这些潜在的不一致性,我们实现了一个批处理系统,该系统可以处理由在线流更改产生的丢失事件。该系统可以仅识别与物化视图文档相关的更改数据,并提供了通过回填初始化物化视图的机制。然而,从在线源头读取和处理大量数据可能会引起性能瓶颈和潜在的异质性问题,这使得直接从这些源进行回填变得困难。
为了应对这些挑战,River 在其回填或调整流程中利用 Apache Spark,尽可能地利用存储在离线数据仓库中的日常快照。该框架根据客户创建的 GraphQL 查询来生成基于 Spark SQL 的查询。Riverbed 利用数据仓库中的数据,复用流式系统中的业务逻辑进行数据转换,并写入到接收端。
图 3:批处理系统
并发 / 版本控制
在任何分布式系统中,同时进行的更新可能会导致数据错误或产生不一致性,这就是竞态条件。Riverbed 通过使用 Kafka 序列化每个文档中的所有变化来避免竞态条件。即将到来的源变动首先被转换为仅包含接收端文档 ID 的中间事件,并被写入 Kafka,然后次级的(通知)流程消费这些中间事件,实现并写入接收端。因为中间的 Kafka 主题是根据事件的文档 ID 划分的,所有具有相同文档 ID 的文档都将被同一个消费者按序处理,彻底避免了并行实时流写入所导致的竞态条件问题。
为了解决实时流式与离线任务之间的并行写入问题,我们在接收端存储了一个基于时间戳的版本。每种接收端类型都只允许写入版本大于或等于当前版本的数据,这解决了流式与批处理系统之间的竞争条件问题。
从概念上来说,Riverbed 将每一次变动视为一次更改的提示。处理器始终使用来自真实源头的数据,因此会生成在处理时间点为最新一致状态的接收端文档。事件的处理是幂等的,可以以任意顺序执行多次。
结果
Riverbed 在爱彼迎中产生了广泛的影响。它目前每天处理 24 亿事件,写入 3.5 亿文档,并在爱彼迎中提供 50 多个物化视图。Riverbed 支持功能如支付、消息搜索、列表页面上的评论渲染以及其他关于房东搭档、行程和内部产品的许多其他功能。
总结与展望
总的来说,Riverbed 提供了一个可扩展且高性能的数据框架,提高了读取密集型工作负载的效率。Riverbed 的设计提供了一种声明式的接口,以高效地执行查询并保证数据的正确性。这简化了分布式物化视图的构建和管理,并使产品团队能够快速迭代特性。使用 Riverbed 预计算数据视图已经显著提高了延迟性能,并提高了流程的可靠性,确保了爱彼迎的房东和房客社区能获得更快、更可靠的体验。
在未来的文章中,我们将更深入地探讨 Riverbed 的不同方面,包括它的设计思路,性能优化,以及未来的发展方向。
作者:Amre Shakim
译者:Ming Shang
感谢所有项目参与者的努力,合作与支持!
如果你想了解关于爱彼迎技术的更多进展,欢迎关注我们的 Github 账号(https://github.com/airbnb/) 以及微信公众号(爱彼迎技术团队)