印度最大在线食品杂货公司Grofers的数据湖建设之路

1. 起源

作为印度最大的在线杂货公司的数据工程师,我们面临的主要挑战之一是让数据在整个组织中的更易用。但当评估这一目标时,我们意识到数据管道频繁出现错误已经导致业务团队对数据失去信心,结果导致他们永远无法确定哪个数据源是正确的并且可用于分析,因此每个步骤都会咨询数据平台团队,数据平台团队原本应该提供尽可能独立地做出基于数据的正确决策而又不减慢速度的工具。

现代数据平台会从许多不同的、不互连的,不同系统中收集数据,并且很容易出现数据收集问题,例如重复记录,错过更新等。为解决这些问题,我们对数据平台进行了深入调研并意识到技术架构上的债务会随着时间的推移导致大多数场景下数据的不正确,我们数据平台的主要功能(提取,转换和存储)都存在问题,从而导致数据平台存在上述质量问题。

列出的问题大致如下:

1.1 原始数据和处理后的数据缺乏分隔

5年前开始数据之旅时,我们由于缺乏远见未将源表与派生表分开,应用表与表模式一起被转储到同一仓库中。 当我们只有20张表时没有问题,但是当超过1000张表时就会成为一个棘手的问题。

源表不仅与基于这些表构建的数据集市放在一起,而且我们通常还会对源表进行修改,结果导致数据消费者经常不确定包含在不同表中的数据的含义,并且发现很难确定将哪个表或列用作事实来源。

从工程角度来看,在故障排除过程中跟踪数据血缘关系变得越来越困难,这使我们的MTTR很高,并经常导致终端用户使用的中断。由于我们将原始表与数据集市放在同一个存储中,缺乏分离也给我们的基础设施带来了开销。

1.2. 基于批处理SQL加载的局限性

最初,我们的源表是通过对生产数据库进行批量调度SQL生成的,这些批作业存在一些固有的问题:

  • 这些操作需要依赖一个固定的列,例如主键。"created_at"或"updated_at"字段可以用作跟踪已复制的行数d的标记,以便后续作业可以在上一个作业中断处开始。 但是这无法复制对跟踪列不可见的更改。例如假设使用"updated_at"列作为跟踪器,然后在源表中删除一行数据。
  • 在指定边界条件(例如必须从其查询数据的下一个标记时间)时必须非常精确。 而分析师在定义作业时通常不会考虑到这一点,并且会在边界条件看到重复或丢失的记录。
  • 我们的一些关键业务表更新频率很高,以至于表在复制同时仍会发生变更,这会导致作业由于行冲突而失败,但由于redshift没有原生的合并命令,因此可能导致产生的数据与源数据不同。为了解决这个问题,我们必须在平台中添加更多工具而使该平台变得支离破碎,增加复杂性的同时也增加了更多的故障点。

1.3. 组件无法扩展

为了使数据分析人员能够在没有数据工程团队任何帮助的情况下对复制操作进行编程,我们引入了一系列可视化拖拽工具。这使我们在开始时可以扩大数据操作规模,但很快又变得难以处理,几年实践下来发现该工作从困难变成了不可能。

曾经用来运行大部分作业的工具有一个另一个缺点是当出现问题时,很少或根本无法发出警报。这导致了我们经常不知道数据是否存在问题或不一致情况,而只有在某些数据分析师提出关注时才发现。 此外随着规模的扩展,这些工具开始花费的时间也越来越多。

1.4. 不支持实时数据管道

随着组织不断发展,开始出现越来越多的实时场景,而通过扩展现有数据平台无法满足这些情况。

由于困扰着我们仓库的问题太多了,我们意识到第一代数据仓库之路已经走到尽头,因此我们决定退后一步来考虑数据平台到底需要什么,如果有必要可从头开始构建一个系统。

以下是我们列出的数据基础架构希望具有的核心功能:

  • 克服上面列出的批处理作业的限制。
  • 尽可能分离存储和计算,以便它们可以独立扩展。
  • 减少故障点的数量。
  • 保持对变更的审核,在发生故障时也有可以轻松部署的数据管道。
  • 轻松跟踪系统中的更改并维护数据血缘关系以简化故障排除步骤。

考虑到这些因素,我们最终决定构建一个使用CDC(Change Data Capture)来复制源表以构建域分离的数据湖。

2. 数据湖和CDC

首先让我们定义"数据湖"和"数据仓库"术语,许多组织都犯了交叉使用这些术语的错误,因为数据湖与数据仓库不是同一回事,数据仓库是存储的数据可以被数据分析师和业务消费者轻松使用,另一方面数据湖是一个大型存储系统,旨在存储原始数据,可以在需要时通过数据仓库进行处理和提供服务。

从工程角度来看,数据湖需要复制和存储生产数据库中的原始应用层数据,这意味着只要生产数据库发生更改,数据湖就需要确保也复制该变更,有多种技术可以捕获这些更改以便进行复制,通过重放对源所做的变更来复制数据的方法称为"更改数据捕获",简称CDC。

更改数据捕获(CDC)可以通过三种方式完成:

  • 基于查询
  • 基于触发器
  • 基于日志

前面描述的复制方法可以归类为基于查询的CDC,我们按计划调度查询来批量复制数据,此方法需要一个增量键来在作为拉取表中数据的标记。

基于触发器的CDC系统使用影子表和一组触发器来跟踪变更,由于非常低的性能和较高的维护开销,它是一种相对较少使用的技术。

基于日志的CDC系统使用数据库更改日志,例如 Postgres中的预先写入日志(WAL),MySQL中的Binlog,MongoDB中的Oplog等,以在复制的数据存储上重放变更,可以通过插件读取这些日志,如Wal2Json,Postgres中的Pgoutput和Mysql中的open-replicator,基于日志的CDC与其他方案相比有很多好处,例如捕获了所有数据更改,包括DELETES,应用完整的事务顺序使得建立实时流变得可能,因此我们在Grofers使用此技术来创建复制管道。

3. 采用增量处理的数据湖的原因

建立数据湖既昂贵又费时,在决定进行架构变更前需要确保进行彻底的评估,我们经历了类似的过程并总结了如下几个关注点:

3.1. 表分离有助于扩展性和查询效率

数据湖是不同于数据仓库的存储引擎,从本质上讲可以从逻辑上和物理上分离应用层的表和集市,也解决了表的特征和血缘问题,使分析人员在进行查询之前就已经意识到它们,万一数据集市有不一致,团队会进行检查。

数据湖提供的另一个主要好处是存储和计算的分离,其背后的原因是数据湖使用类似文件系统存储(例如AWS S3或传统的HDFS)来构建,与Amazon Redshift等数据仓库相比成本要低得多。如果查询数据的时间小于所存储数据的全量历史数据,则此优势将带来更多的成本价值,例如我们组织中的大多数报表仅查询过去一年的数据,但是我们在数据湖中存储了超过5年的数据,如果将所有这些数据存储在仓库中,支付存储成本会更高。

3.2.不再对源数据库进行批量轮询

由于我们使用数据库日志来为我们提供有关表上发生的所有事务的完整信息,因此不再需要基于批处理的SQL进行数据提取,而且由于不需要标记列(例如created_at,updated_at等),因此不再有丢失数据的风险,我们还可以在整个仓库中复制DELETE行,而通过批量查询会丢失删除的这些行。

3.3.支持多种应用程序数据库引擎和业务约定

每个数据库引擎都有其处理数据类型(例如时间,十进制,JSON和枚举列)的特定方式,此外我们还有许多不同的后端团队,他们在设计数据库架构时会考虑最终用户的需求,然而由于每个团队根据其用户、技术不同而遵循不同的约定,因此我们经常看到相似列的命名方式有很大的不同,数据湖可在将数据提供给分析人员之前消除精度、命名等差异。

3.4.数据损坏与Failover

任何拥有大量数据库和系统的组织都需要在最短的时间内解决问题,数据湖允许我们做同样的事情,在发生任何故障的情况下,我们都不必依赖源数据库副本,由于我们可以在不影响生产系统的情况下进行重新装载,因此对故障有了更强的防御能力。

此外数据经历了多个处理阶段,将使得我们能够在每个阶段对应用完整性和质量进行检查,提早告警以及发现问题。

3.5.实时分析和用例

我们采用的数据捕获机制生成实时数据流,这些数据流可用于服务许多用例,例如监视日常操作,异常检测,实时建议以及更多的机器学习用例。

我们不同后端系统中大约有800个表(Postgres和MySQL)在不断更新,一天中插入与更新的比率接近60:40,如此庞大的更新量需要对大量数据进行重新处理和更新(重复数据的几率更高)。在生产环境中,很难控制查询的质量,而由于我们无法独立扩展计算能力,一些写得不好的临时查询会影响到SLA,由于数据湖使我们能够处理分区并基于键来合并行,因此它可以解决重复行的问题,然后再加载到仓库中。此外,CDC流使我们能够捕获表中的每个更改,从而解决了更新遗漏的问题,因此将基于日志的CDC与数据湖一起用作我们复制管道的体系结构似乎是我们数据平台正确发展的下一步。

4. 与Data Lake合作的CDC工具

如前所述,我们管道中的问题不仅在于转换和存储层,在捕获诸如行冲突、锁、更新遗漏、架构变更之类的更改方面我们面临也许多问题,因此我们必须改变从应用程序数据库获取数据的方式,而我们发现的解决方案是使用基于日志的CDC,有许多基于云和付费的工具围绕基于日志的数据捕获的工具,但是由于它是体系结构中最关键的部分,我们决定对其进行更详尽的调研,我们试用了AWS Data Migration Service,但由于它不在kappa架构的长期愿景中(成为无法进一步复用的一对一管道),因此我们不得不寻找其他解决方案。

我们希望对该工具进行细粒度的控制以便能够轻松地对监视和警报进行修改,因此不能使用黑盒服务。

我们围绕CDC尝试了几个开源项目,例如Debezium,Maxwell,SpinalTap和Brooklin,其中Debezium在数据库引擎(MySQL和Postgres)的支持、快照、列掩码、过滤、转换和文档支持方面表现突出,此外Debezium还拥有一个活跃的Redhat开发团队,如果遇到无法解决问题,他们可以为我们提供及时的支持,另外我们还尝试了Confluent源连接器,但是其是基于查询的CDC,所以无法继续使用。

5. 创建一个有用的数据湖

数据湖是大量原始数据和半处理数据的存储库,它们的用例通常仅限于处理数据的中间层,而对业务没有任何直接价值,此外围绕数据湖效率低下的批评也很多,事实证明这是因为没有挖掘数据湖价值造成的,因此我们决定不将它们作为一般的Parquet/ORC文件进行存储,而是添加一些直接的业务价值,例如消费者对数据湖进行主动查询。

数据湖是一种相对较新的架构模式,有许多开源项目将元数据添加到这些湖文件中,这使其与仓库非常相似,可以使用Hive,Presto和许多其他SQL进行进一步查询。 这些项目大多数是基于以下原则:使用bloom过滤器,分区,索引等元数据来进行增量更新。

  • Delta Lake:这个项目是社区中最知名的项目,由Databricks开发。它有两种实现,一种是Databricks公司实现的商业版,另一种是开源版本,两者虽然具有相同的体系结构,但是目前在压缩,索引,修剪和许多其他功能方面,开源版本与Databricks商业版本差距很大,但就基于ACID的数据存储而言,它在终端用户工具箱中拥有最大的发展势头和被最广泛的采用。

  • Apache Iceberg:最初由Netflix开发,用于存储缓慢移动的表格数据,它具有优雅的设计,通过清单进行模式管理(模块化OLAP),但与其他两个框架相比知名度相对较低,并且缺乏与Apache Spark或Flink等处理引擎或云供应商的紧密集成,这使其难以采用。

  • Apache Hudi:这是Uber最初开发的开源项目,用于在DFS(HDFS或云存储)上摄取和管理大文件,它非常注重性能(如延迟和吞吐量),对不同实现如""写时复制""和"读时合并"进行深度优化,通常也可以将其定义为批数据的增量处理。目前,AWS生态系统已支持它(通过Redshift Spectrum)。经过POC后我们目前正在采用此方法。

我们之所以选择Apache Hudi作为数据湖存储引擎,主要是因为它采用了性能驱动的方法,我们的大多数表都是"写时复制"的,因为我们不希望通过redshift提供实时更新。在使用Debezium和Kafka从源数据库捕获CDC之后,我们将它们放在S3中,以便它们可以通过Spark处理引擎进行增量消费,另外由于CDC原始格式无法以直接放入Hudi表中,因此我们为Nessie(源自Lake Monster)开发了一个内部工具。

6. Nessie: 湖中的怪物

Nessie是用于在处理引擎与数据湖耦合的同时提供抽象化的工具,因为CDC日志需要被转化为适当的格式,以便可以将其存储为Hudi表。

在Nessie上开发的功能的如下:

  • 降低Apache Hudi与Debezium CDC集成的复杂性,例如使用来自CDC记录的事务信息和模式演变来生成增量键。
  • 支持转储不同间隔(分钟/小时/天)、不同类型的原始文件转储(Avro / JSON / Parquet),在MySQL和Postgres中有不同的数据类型。
  • 支持通过DeltaStreamer,Spark Streaming轮询Kafka主题进行消费。
  • 支持生成用于进一步处理的标准银表。
  • 支持使用标记、压缩和其他表指标监视SLA。

7. 要考虑的点

如果打算开始使用Debezium和Hudi构建数据湖,需要考虑一些问题和局限性,为了描述这些问题,让我们首先了解Hudi的一些基本参数,Hudi表有如下3个重要参数,即:

  • 记录键(每行的唯一ID)
  • 增量键(行版本的最大唯一值状态ts)
  • 分区键(行的分区值)

在使用CDC数据构建数据湖时,我们面临着围绕增量键的挑战,在增量键中我们必须根据LSN、Binlog Position等属性,为Postgres和MySQL DB引擎中的每个变更/事务生成增量变更值,我们考虑过在每一行中都使用相同的修改时间,但是由于时间值的精确度和并行事务的原因,它无法解决问题。

我们还围绕Hudi中的时间戳格式进行了自定义开发,因为其Hive集成当前不支持时间戳数据类型,另外还必须更紧密的集成仓库(redshift)和数据湖以处理架构演变。

在Debezium中,由于缺少数据更改的情况,我们面临着一致性方面的问题。 其中一些关键如下:

DBZ-2288:Postgres连接器可能在快照流转换期间跳过事件

DBZ-2338:复制插槽中的LSN并非单调增加

同时为了规避该问题,我们开发一个补丁脚本,该脚本可以跟踪和修复丢失的更改事件,开发能够处理具有不同版本的不同数据库引擎的验证脚本也很困难,因为它们需要运行在数据湖和数据仓库。

在整个管道的性能方面,由于Debezium遵循消息的严格排序,因此我们面临与Kafka有关将主题限制为单个分区的一些问题,我们通过使用增加的生产者请求大小来解决相同的问题,从而提高了记录的整体传输率。

8. 总结

我们目前几乎将所有关键表以及消费者都迁移到了Hudi数据湖方案上,将来我们计划整合数据血缘和健康监控工具,同时我们也正在慢慢转向基于流的Kappa体系结构,并且将在此基础上构建实时系统,此外我们计划对仓库中基于日志的表尝试使用读时合并(MOR)的Hudi表,尽管对Hudi计划的取决于RedShift对Hudi的支持

总体而言,迁移到此架构减少了数据管道的大量波动,同时大大降低了成本。

最后感谢Grofers整个数据团队的辛勤工作,尤其是Apoorva Aggarwal,Ashish Gambhir,Deepu T Philip,Ishank Yadav,Pragun Bhutani,Sangarshanan,Shubham Gupta,Satyam Upadhyay,Satyam Krishna,Sourav Sikka和整个团队帮助数据分析师团队实现了这一过渡。 另外还要感谢Debezium和Apache Hudi的开源维护者开发了这些出色的项目。

posted @ 2020-10-27 09:34  leesf  阅读(791)  评论(5编辑  收藏  举报