HDFS廉颇老矣?基于对象存储的数据湖构建新思路

2021-06-08 20:25:50 浏览数 (1)

作者 | 王夏、滕昱、孙伟

编辑 | 蔡芳芳

1什么是数据湖?为什么是数据湖?

我们经常会被问到一个企业大数据架构的问题:随着企业收集 / 产生的数据越来越多,如何设计一套高效廉价的大数据架构,在尽可能多保留所有原始数据内容的同时还可以支持“无缝接入”的新的分析算法。本文所要介绍的数据湖解决方案可能是解决这个难题的一种新思路。

数据湖,实质上是一种数字资产的组织形式。这种新的组织形式希望向数据工程师提供一个灵活的平台,用以对更加广泛类型的数据,实施更加灵活的解决方案。

如上图所示,这是一个典型的数据湖架构,自底至上分为三层:存储层、数据定义层和计算层。相较于传统的数据仓库解决方案,数据湖最重要的特点是使用了更加灵活的数据定义,为计算层提供了一个统一的数据访问抽象。

但是,对多种数据格式的支持,使数据湖失去了访问结构化数据的便利性,下面我们可以对比一下传统数据仓库的实现。

数据仓库是非常传统的面向结构化数据的解决方案。在数据仓库中,数据被拆分并存储在不同的关系型数据库、非关系型数据库中,绝大多数的查询优化被内建在了数据库解决方案中,用户需要熟悉数据库解决方案,或者依赖数据库管理员,对相应的查询进行优化。对于一些规模较大的查询操作,数据仓库则需要依赖特定的计算平台,通过读取统一管理的元信息,跨越不同的数据库进行检索。

而在本文将会介绍的数据湖方案中,结构化数据并不再依赖(或减少依赖)特定的数据仓库方案。并且伴随着 Apache Iceberg、Apache Hudi、Delta 这些面向结构化数据的数据定义框架的诞生和发展,计算层和存储层的解决方案也变得灵活多样。通过对计算层和 / 或存储层进行一定的抽象,这些框架不再依赖特定的一种方案,并允许用户使用统一的业务逻辑在不同的解决方案间切换,以获得最佳的开发运行效率。

作为长期企业级对象存储解决方案的提供者,我们在对这些开源方案进行对比研究后,选择了 Apache Iceberg 作为我们的数据湖方案组成部分。其绝佳的灵活性使得我们的对象存储产品可以对其进行完美的适配。下面,我们会详细介绍 Apache Iceberg 的工作和存储细节,并详细阐述对象存储和 HDFS 在这个场景下的优劣。

2Apache Iceberg 如何管理其存储?

在研究了多个在数据湖内管理结构化数据的解决方案后,我们发现,无论是 Iceberg,还是其他方案,在存储上都有着类似的架构,如下图所示:

在存储上,这些解决方案往往包括三个部分。最左侧是管理数据,用来提供一个类似数据库或命名空间的管理能力。在管理数据右边的是表元数据,用来管理表格的元数据,例如表名、版本等。最右侧的则是表数据,用来存储结构化数据本身。

在 Iceberg 中,表数据由这几部分构成:

  1. 版本信息,指的是当前表格的版本,包括版本所对应的:
  • Schema,指表格有哪些字段;
  • Partition,指表格通过哪些信息进行分区;
  • 对 Manifest List 文件的引用,包括当前最新的版本,和若干历史的版本。

2. Manifest List 文件,引用多个 Manifest 文件。每一个文件都是表格的全量数据快照。

3. Manifest 文件,引用多个 Data 文件,是一个比较简单的集合概念。

4. Data 文件,存储表格的原始数据文件,数据支持使用 Apache Parquet、Apache ORC 和 Apache Avro 格式进行存储。

对于特定版本的数据快照,即 Manifest List 文件,Iceberg 使用了 2 层一对多的结构来引用所有的 Data 文件。这种 2 层的设计便于 Iceberg 进行批量数据文件的操作,例如提交大量 Data 文件时,合并其成为单独的 Manifest,这样设计的 Manifest List 只需要添加 1 行即可完成表数据的修改。由于 Iceberg 中的表数据文件不会进行修改,对文件的变更越小,越能降低对存储的额外消耗。

同时由于表数据文件不会进行修改,在数据写入完成后,读取数据将不会出现读取到部分数据的情况。即当用户读取到一个特定的快照时,其 Manifest List 引用的数据已经被确定,在读取过程中,就会正确的处理全量数据,或者因为其他意外导致操作失败。这有效的避免了静默的成功产生读取了部分数据或者错误数据的情况,为用户获取正确的数据提供了有效的保障。

Iceberg 的表元数据则非常简单,仅仅包括当前表的名称和版本信息。所有的 Schema 和 Properties 都由 Iceberg 本身管理。

为了管理这些数据,Iceberg 使用了 Catalog 作为其抽象,具有如下图所示的能力:

Catalog 的能力主要包括三部分:

1. 文件 IO,用来读、写、删除所有表数据文件。

2. 表操作,用于完成:

  • 读取最新版本的表。
  • 提交新版本的表。

3. 其他 Catalog 功能,用于完成:

  • 命名空间的管理。
  • 命名空间的树型关系。
  • 命名空间下的表格。

在这个结构上,Iceberg 提供了 Catalog 抽象管理这些数据,允许开发者定制其中的一些功能,这使得我们能够非常方便的将对象存储与 Iceberg 进行组合。

3对象存储的优势和挑战

在 Iceberg 的代码仓库中,包含了以下几种 Catalog 的实现:

绝大多数 Catalog 使用 Aapche HDFS 作为数据文件的存储,因为 HDFS 是当前被广泛使用的开源存储组件。它拥有一个文件系统抽象,从而让开发者更加便利的使用其作为存储引擎。但是在近 1-2 年以来,随着混合云 / 多云和边缘端的高速发展,对象存储在很多方面呈现出取代 HDFS 的趋势。

下文将会详细叙述相较于 HDFS,对象存储在数据湖场景下所体现的优势和所面临的挑战,以及解决方案。

对象存储易于集群扩展

HDFS 使用了 Name Node 作为元数据管理服务,所有文件的元数据交由 Name Node 进行管理。因此,Name Node 实际上成为了 HDFS 系统中的单点服务,无法进行横向扩展。在逐步增长的数据规模下,这样的架构会慢慢的体现出劣势,往往体现在需要使用越来越高的配置运行 Name Node,需要对 Name Node 设计高可用方案等。

在对象存储中,集群的横向扩展往往比较容易,因为对象存储通常使用了一致性哈希对元数据进行分区处理。对于已经被分区处理的元数据,可以在多个服务间移动,在数据量不断增长的情况下,通过扩展集群可以有效地提高服务容量,在部分服务出现异常时,对应分区的数据可以快速切换到可用服务,用以重新提供服务。当数据规模增长到分区的限制时,对象存储可以重新设定分区,以支持更高规模的元数据。

总之,被拆分的元数据为集群扩展性提供了基础支持,使得对象存储能够根据其配置,进行合理的规划,消化新的软硬件资源,从而提供高容量的服务。

对象存储在支持海量小文件上的架构优势

如前所述,HDFS 的元数据受限于 Name Node 的架构。当出现海量小文件时 (例如单节点 100 亿 -150 亿量级 4KB 文件),因为其元数据的占比较高,HDFS 的 Name Node 空间消耗极大,导致 Data Node 在尚空的情况下,Name Node 已经无法有效的提供服务。社区使用了多种手段优化小文件存储,但其基本原理都是将小文件合并成大文件,这或多或少的对性能和交互便利性产生了一定的影响。

如前节所示,对象存储由于使用了分布式的元数据管理设计,本质上元数据和数据都是用一致的底层存储方式,可以无缝的 scale out 到整个底层硬件层上,解决了元数据容量扩充的挑战。加之近年来,对象存储在快速发展中通常利用多种介质对元数据进行加速或缓存,使得其可以利用存储领域的新技术,例如全面引入 NVMe-oF,对元数据查询进行优化。

所以在对象存储中,元数据不再受限于单个节点的物理资源,对于小文件这种元数据与数据接近的数据湖场景,对象存储更能够平衡元数据和数据的资源配比,有效的利用整个系统的物理资源对小文件进行索引,使得单一的节点也能够容纳海量小文件,而不受限于某些特定节点的资源限制。

对象存储天然支持多站点部署

对于存储的数据,如果需要异地备份,或者多机房备份,就需要进行多站点部署。而在很多企业应用中 (例如金融客户),这又是个必选项。

HDFS 本身并不支持多站点部署。有一些商业软件试图提供多站点支持,但基本都是基于一个额外的消息系统进行异步数据复制。那么使用这样“混合”架构的代价是需要一定量额外工作的去支持两个系统,更不谈设计一个“仅一次性”消息系统本身又是一个挑战。

而绝大部分对象存储则天然支持多站点部署。通常用户只要配置数据的复制规则,对象存储就会建立起互联的通道,将增量和 / 或存量数据进行同步。对于配置了规则的数据,你可以在其中任何一个站点进行访问,由于跨站点的数据具备最终一致性,在有限可预期的时间内,用户会获取到最新的数据。

综上所述,一个支持多站点数据访问的单一存储抽象,是现阶段 HDFS 所不能提供的。

对象存储低存储开销(Lower TCO)

任何分布式存储的在设计上都需要一些额外的副本数据来抵御硬件故障产生的数据丢失风险。

在 HDFS 中,默认使用 3 副本存储数据,数据存储了 3 份,对于其中任意 2 份数据,如果因为软硬件故障发生了损坏,可以使用剩余的 1 份,保障了数据的准确性。但是,因此对于一次写入 - 多次读取的场景,其存储开销较大。对于存储有限的用户,往往希望在保障数据准确性的前提下,降低多副本带来的存储空间浪费。

因此,EC(纠删码)成为了存储领域的一种解决方案。在对象存储中,数据天然支持 EC,如图所示,数据使用了 10 2 的 EC 编码,数据将会被均匀的拆分成 10 份,并根据这 10 份数据计算出 2 份纠删数据。这 12 份数据中,任意 10 份就可以恢复出完整数据,存储开销仅仅是 1.2 倍。在这种模式下,数据密度被提升,有限的存储可以发挥出更高的价值。本质上,使用 EC 是在计算能力(计算 EC 编码)和存储空间中做的 trade-off。但是,考虑到当代 x86 架构上很多微码上的优化,使用 EC 的方案在经济性上的比拼已经远远胜出。

近年来,HDFS 版本也支持了 EC,但是受限于其存储模型,经过 EC 的数据不再支持 append 等存储操作。并且由于是对文件整体进行 EC 编码,当文件较小时,EC 算法可能无法拆分出足够的数据块,导致 EC 过后反而出现空间变大的情况,而在对象存储中,小文件的数据可以合并至块(Chunk)进行 EC,进一步加大对象存储在小文件上优势。

对象存储如何解决追加上传(append)的场景

在 S3 的标准 API 中,上传数据需要预先知道对象的大小,因此在追加上传的场景下,其调用方法无法像 HDFS 那样简洁。所以在具体实现中,追加写的操作需要在本地预先处理,并以整体上传。

而具体对于对象存储而言,上传有 2 种模式:小的对象,使用 Put Object,大的对象,使用 Multipart upload。

在对象写入过程中,如果当前对象的大小始终没有达到 Multipart upload 的要求时,直接使用 Put Object 上传对象。当对象在写入过程中,大小达到了 Multipart upload 的要求时,会立刻创建一个 Multipart upload 流程,将当前已有的数据提交为第 1 个 part,并将后续的数据写入新的缓存,逐一上传。在对象写入完成后,统一将这次 Multipart upload 的所有 part 信息提交,服务器将会将其拼装成一个完整的对象。这意味着,追加上传需要依赖一定的本地物理资源(内存 / 本地存储)进行数据的缓存。

尽管如此有着这样的限制,但是它也提供了额外的优势:

  1. 对象存储额外提供了不可变性。对于一个已经上传完成的数据,没有任何操作可以改变其部分内容,只能写入一个新的对象覆盖原有对象,这意味着不会出现部分上传的对象,导致服务读取到部分数据。
  2. 对于非常大的对象,使用 Multipart upload 可以异步进行上传,在上传分段的过程中,写入的流程不需要被阻塞,只需要在最终完成上传时,确保所有的分段都上传成功即可。这样可以提高数据的吞吐,有效提高写入文件的效率。

对象存储应对原子 Rename 的挑战

为了阐明这个问题,我们需要先看看 Iceberg 在修改表格时发生了什么:

这是一个使用 Flink 向 Iceberg 表格中插入数据的基本流程,主要分为两个部分:数据文件的写入和表格元信息的提交。

在写入数据文件时, Flink Data workers 将从数据源中逐行读取数据,根据当前定义的 Schema,解析行中的数据,计算分区信息,将该行写入对应分区的数据文件中。当到达检查点时,Data Workers 将会切换写入新的数据文件,同时完成正在写入的数据文件,所有完成的文件会被打包成文件清单。文件清单包含了数据文件的路径和其统计信息,这些信息将被移交至 Commit Worker 对表格的元数据进行变更。在变更时,需要读取当前的表格版本,如图所示,当前读取到的版本号为 006。紧接着将所有的数据文件信息与版本号为 006 的表格进行合并,生成新的表格元数据,并将版本号更新为 007。注意,这一过程是线性一致性的,因此,对于多个提交者:

Commit Worker 1 读取到了 006,处理了数据变更,并提交了新的版本 007。在这个过程中,如果发起了另一个更新请求,使得另一个 Commit Worker 2 也读取到了 006,那么在其最终提交版本时,如果提交了 007,就会产生冲突。此时,必须有机制保障它不提交 007,并在感知到失败后,重试去提交 008,避免当前变更的丢失。这就是 Iceberg 所要求的线性一致性。

在官方内置的 Catalog 实现中,主要存在两个流派的设计。一个是基于 Apache Hadoop 的实现,使用了原子的重命名确保特定的版本被唯一提交:

在 HDFS 中,由于重命名操作是原子的,HDFS Catalog 使用了每个版本唯一的文件作为标记,如图则是 007。在提交的时候,会先将提交后的元信息写入一个随机文件名称的文件,并尝试将这个文件重命名成指定的 007,如果重命名失败,则会返回重新提交。这样,意味着在提交的时候,HDFS 能够完美的保证线性一致性。但是,在查询表格最新的版本时,则会出现一些性能问题:HDFS Catalog 会列出当前所有的版本文件,并选择最大的一个作为最新的版本。

在对象存储上,则是沿用了第三方锁的实现:

当最终表格提交的时候,使用一个锁用来确保其他人无法提交新的版本,此时检查自己希望提交的版本是否存在,如果版本不存在,则直接提交,如果版本不存在,则获取最新的版本再次提交。

商业对象存储的解决方法

当然,针对标准 S3 API 在前 2 小节提到的限制,现有商用对象存储(公有云 / 混合云)比较常见的做法是扩充 S3 的实现,提供 Append 和 CAS 语义来解决。Dell EMC ECS 对象存储,也使用了类似的思路实现更高效的 Iceberg catalog。

ECS 支持使用 Append 操作,如果用户本地缓存受限时,使用 Append 的操作可以完美应对顺序写入未知长度文件的场景。注意在这个场景下,如果在写入过程中出现了客户端程序崩溃等现象,部分数据可能会残留在 ECS 中留待后台清理流程去处理。但是 ECS 仍旧提供了用户可见对象数据的完整性。

在并发提交的场景下,ECS 支持使用 If-Match 和 If-None-Match 对对象进行 CAS 操作。对于对象的读写,ECS 是强一致性的,通过提交前一个版本的 ETag 信息(常见实现是文件的 MD5),ECS 会在最终更新对象信息时进行检查,如果出现 ETag 不匹配则会拒绝对象的更新,从而保障数据以合适的顺序被提交。

如图,ECS 使用了一个 table.object 存储对象的版本信息,每次提交新的版本时,携带原版本的 E-Tag 更新,因此 v007_b 会在最终提交时失败,并获取最新版本重新提交。相较于 HDFS 的原子性 rename,CAS 提供了更高的灵活度,只需要一个版本对象记录版本信息即可。由于 Listing 操作在对象存储上有着比较大的开销,使用单一的版本对象能够极大的提升获取最新版本的性能。

基于以上两个方面,使用 ECS,就可以满足 Iceberg 的所有需求,而不依赖其他第三方应用提供诸如元数据和锁的管理。使用 Apache Iceberg 和 ECS,可以构建出一套完整的、针对结构化数据的数据湖解决方案。

4总结

在对 Apache Iceberg 进入深入探索后,我们作为对象存储产品的提供方,对数据湖的解决方案有了一些思考。社区在推动数据湖的解决方案时,对存储层的良好定义使得更加多的存储产品可以在大数据解决方案中扮演全新的角色。

作为数据湖解决方案中的新星,Apache Iceberg 定义了良好的表格格式,用于帮助用户组织数据。其灵活的特性,使得无论是 Apache Flink、Apache Spark 等计算层解决方案,还是 HDFS、对象存储等存储层的解决方案,都能够与其完美适配。

在未来,我们会积极的参与到社区中,参与到数据湖、大数据解决方案的建设中,我们正在积极准备向社区贡献 ECS catalog 的代码,希望在未来,愈加高性能、高自由度的对象存储,能够为数据分析领域添砖加瓦,能够在当下的数据热潮中,成为更多人的选择,让存储变得简单,让数据发挥更大的价值。

0 人点赞