来源https://motherduck.com/DuckLake权威指南构建基于 SQL 原生表格式的下一代数据湖仓Matt Martin 和 Alex Monahan 著第 1 章 重新思考数据湖仓当今数据湖仓的痛点想象一下在不到一分钟内搭建一个挂载到云对象存储的数据湖仓。无需 Apache Spark无需 Java无需目录连接无需分布式架构管理。只需几行 SQL一切就绪。这听起来可能乐观得离谱但事实并非如此。DuckLake 让这成为现实以下是证明。只需两行 SQL您就能拥有一个连接到 GCS、准备创建和管理表的 DuckLake 数据湖仓CREATEORREPLACESECRET gcs_creds(TYPEGCS,KEY_ID getenv(GCS_KEY),SECRET getenv(GCS_SECRET));ATTACHORREPLACEducklake:gcs_wh.ducklakeASgcs_wh(DATA_PATH getenv(GCS_WHS_PATH));如果您使用过其他格式构建数据湖仓那么这里展示的简洁性可能令人难以置信。为什么 DuckLake 只需要几行代码和配置而 Apache Iceberg 和 Delta Lake 却需要数页的配置答案归结为一个核心架构决策——元数据存储在哪里。在 DuckLake 中元数据存储在一个针对索引、低延迟查找而优化的关系数据库中。对象存储仅保存数据文件。这种方法并非异想天开而是一个有条理且有意的决策。它使用了经过验证的技术如对象存储、PostgreSQL 和 DuckDB。DuckLake 尽可能简单但又不失简洁——它仍然可以扩展到 PB 级的工作负载。而对于其他数据湖仓元数据与数据一起存储在对象存储中。您可能会说“那又怎样对象存储具有极高的持久性和可扩展性。” 您说得对。Amazon S3 以其 11 个 9 的持久性而闻名¹。如果您真的能向 AWS 证明他们在 S3 中“丢失”了一个文件他们可能会给您一个奖杯。但是这些对象存储并非为低延迟访问数千个小文件而设计它们擅长读取大文件而不擅长高扇出的小型元数据文件扫描。要理解为什么这是个问题我们需要看一个实际的例子并观察会发生什么。以下是一段简单的 Spark 代码片段它构建了一个 Iceberg 表并插入了一行数据sqlf CREATE TABLE IF NOT EXISTS{catalog_name}.{namespace}.orders ( order_id BIGINT, order_date DATE, customer_id BIGINT, total_amount DOUBLE ) USING ICEBERG spark.sql(sql)spark.sql(finsert into{catalog_name}.{namespace}.orders values (1, current_date(), 1001, 250.75))执行此操作后让我们在终端中运行 tree 命令查看 Iceberg 为表创建了哪些文件(ducklake-definitive-guide) orders % tree . ├── data │ └── 00000-0-069cd534-d44f-4b2b-a9f6-3fdd16dcbed9-0-00001.parquet └── metadata ├── 02611d41-d398-48f4-8a24-9ecb9e7524d6-m0.avro ├── snap-6774179103726272682-1-02611d41-d398-48f4-8a24-9ecb9e7524d6.avro ├── v1.metadata.json ├── v2.metadata.json └── version-hint.text创建表和插入一行这两个简单操作产生了五个元数据文件。平心而论最后一个元数据文件 version-hint.text 是一个快速查找文件指向最新的快照在我们的例子中是 v2.metadata.json。但是 Iceberg 为什么要创建其他四个元数据文件呢在最简单的形式中每个提交快照的 Iceberg 事务至少会产生三个元数据工件• 一个 vN.metadata.json 文件捕获表的逻辑定义和当前状态包括模式、分区规范、表属性和快照列表。每次提交都会产生一个新的元数据文件最新的文件代表了表的当前视图。• 一个 snap-*.avro 文件清单列表属于特定快照列举了该快照中包含的清单文件以及高级摘要统计信息如添加/删除的文件和添加/删除的记录。• 一个或多个-m.avro 文件清单文件描述了快照引用的实际数据文件。每个清单条目包括数据文件路径、记录数、分区值如果适用以及用于查询计划和文件裁剪的列级统计信息如最小值/最大值。Delta Lake 遵循不同的实现但模式类似元数据条目作为文件存储在对象存储中形成一个不断增长的日志。在小规模下这种元数据设计运行良好。但在大规模下它会在元数据和查询计划上造成瓶颈。为了更好地理解 Iceberg 和 Delta Lake 元数据文件在对象存储中针对一个表的增长情况我们可以应用这个简单的扩展方程其中 N 代表一个事务Iceberg: 1 3NDelta Lake: 1 N现在考虑一个真实场景。您最近使用 Iceberg 构建了一个新的应用程序日志分析数据湖仓。您同时实现了用于近实时更新信息的流处理和用于处理夜间批处理的批处理。这就是所谓的现代 Lambda 架构。您的团队对您的专业知识赞叹不已。数据湖仓运转良好。六个月后流入了 50,000,000 条日志您在周末被叫进了作战室。原本只需几秒钟的简单读写操作现在需要超过 30 秒。但这怎么可能呢您遵循了最佳实践。是什么导致了如此严重的性能下降在那运行的六个月里您的 Iceberg 仓库产生了大约 150,000,000 个元数据文件。没有任何东西“损坏”。仓库按照设计的方式运行。只是查询规划现在涉及到更多的远程文件读取。这里需要理解的关键点是对象存储虽然可以具有非常高的吞吐量但代价是高延迟。它们没有针对这种访问模式读取大量元数据文件进行优化。而 DuckLake 正是针对这种访问模式进行了优化因为它使用数据库来查找和管理元数据。对象存储的设计目的是在列式数据文件如 Parquet上实现极快的速度而不是高扇出的元数据遍历。相比之下事务数据库在过去 40 多年里专门针对小型、频繁、低延迟的读写进行了优化——它们在这方面表现出色现在回顾我们最初设置 DuckLake 并将其连接到 GCS 的例子以下是 Iceberg 和 Spark 的等效设置# Iceberg 运行时相关配置spark_versionos.getenv(SPARK_VERSION,3.5)scala_versionos.getenv(SCALA_VERSION,2.12)iceberg_versionos.getenv(ICEBERG_VERSION,1.7.0)iceberg_packageforg.apache.iceberg:iceberg-spark-runtime-{spark_version}_{scala_version}:{iceberg_version}# 定义 Iceberg 仓库路径warehouse_pathfgs://{gcs_bucket}/icehouselocal_jar_path./jars/gcs-connector-3.0.4-shaded.jarreturnSparkSession.builder \.appName(local_spark_gcs)\.config(spark.sql.extensions,org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions)\.config(fspark.sql.catalog.{catalog_name},org.apache.iceberg.spark.SparkCatalog)\.config(fspark.sql.catalog.{catalog_name}.type,hadoop)\.config(fspark.sql.catalog.{catalog_name}.warehouse,warehouse_path)\.config(spark.jars.packages,iceberg_package)\.config(spark.jars,local_jar_path)\.config(spark.hadoop.google.cloud.auth.service.account.enable,false)\.config(spark.hadoop.fs.gs.impl,com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem)\.config(spark.hadoop.fs.AbstractFileSystem.gs.impl,com.google.cloud.hadoop.fs.gcs.GoogleHadoopFS)\.config(spark.driver.host,localhost)\.config(spark.driver.bindAddress,127.0.0.1)\.config(spark.hadoop.google.cloud.auth.type,APPLICATION_DEFAULT)\.getOrCreate()再次强调您的眼睛没有花。Iceberg 和 Spark 需要调节和配置更多的旋钮。这反过来给数据工程师增加了显著的认知负担。您看到的是启动分布式架构如 Spark并将其引导到云对象存储所需的具体细节。DuckLake 几乎将这种情况完全逆转使设置变得极其简单用户需要担心的旋钮也少得多。马特注。我个人可以说第一次在 GCS 上运行 DuckLake 数据湖仓时我震惊了下巴几乎掉了下来。我简直不敢相信它竟然如此简单。我意识到 DuckLake 将注意力重新放回了解决真正的业务问题上让我们摆脱了管理和排查复杂分布式查询引擎的事务。现在暂且不谈这些让我们深入探讨。元数据性能与并发挑战到目前为止我们只是触及了 DuckLake 的表面通常一个好的过渡方式是将现有技术栈与新技术栈进行并排比较。图 1-1 是 Apache Iceberg 的元数据和数据架构与 DuckLake 的并排比较。[图 1-1Apache Iceberg 和 DuckLake 的目录及文件架构并排比较]正如您所见Iceberg 生成大量元数据文件以维持其灵活性和时间旅行能力。而 DuckLake 则在数据库而非云对象存储中管理元数据消除了 Iceberg 所承担的元数据开销。但这种开销在实际的 DML 操作如读/写中意味着什么呢让我们来分析一下。读操作对于 Iceberg一个读操作仅为了元数据就需要从查询引擎进行 4 次独立的往返调用查询目录以获取最新快照 ~ 1ms查询所有元数据文件1 到 n 次对象存储查询~ 100ms查询所有清单列表文件1 到 n 次对象存储查询~ 100ms查询所有清单文件1 到 n 次对象存储查询~ 100ms查询 Parquet 数据文件本身1 到 n 次对象存储查询~ 可变因此Iceberg 上最快的查询至少也需要大约半秒钟。写操作写操作也存在类似的开销问题。对于 Iceberg写入单条记录或一批记录将需要几次往返写入新的数据文件写入新的清单文件集写入新的清单列表文件集写入新的元数据文件更新 Iceberg 目录以指向最新的元数据文件这意味着即使是一次写入也可能需要大约半秒钟。现在考虑一下当应用程序尝试并发写入数百条记录时会发生什么。您可能认为解决方案是弹性扩展更多工作节点并并行处理写入但 Iceberg 的乐观并发模型使这变得无效。要理解原因了解乐观并发控制OCC会有所帮助。OCC 旨在避免传统关系数据库管理系统RDBMS中的一个典型瓶颈即增加读取或写入范围会导致锁升级——先是行锁然后是页锁有时甚至是整个表锁——迫使其他事务等待锁释放。OCC 采取了不同的方法。查询读取的是查询开始时存在的数据的一致快照。正在进行的未提交的更改是不可见的并且在读取期间不持有任何锁。这提高了读并发性和可扩展性这就是为什么 OCC 已成为许多现代数据库和云数据仓库包括 Google BigQuery 等平台的默认模型。但对于数据湖仓中的写操作OCC 实际上很痛苦因为它需要保证 ACID。如果当前查询正在写入其元数据文件而另一个并发事务完成了那么当前查询将不得不取消、回滚并重试其事务以维护 Iceberg 的 OCC 姿态。因此更多的工作节点并不能解决问题——这是架构的根本限制。DuckLake 也使用 OCC 模型。所以您可能会想“嗯我猜它也有同样的问题” 实际上并非如此。请记住DuckLake 的优势在于其在数据库内部管理元数据因此它的事务吞吐量将比 Iceberg 快几个数量级。您问快多少DuckLake 的元数据事务在大约 30ms 内完成而 Iceberg 至少需要 300ms延迟显著降低了 90%。既然我们已经讨论了现代数据湖仓的读写复杂性我们需要关注另一个基本问题小文件问题。小文件问题小文件问题就像复利但以一种非常糟糕的方式随着时间的推移随着数据湖仓表处理许多行级更改每次更改都会生成一个或多个新的元数据文件Iceberg 的情况下是三个以上以及更多的数据文件如前所述。最终这个问题表现为查询性能差因为 Iceberg 列出查询需要考虑的所有元数据文件需要花费大量时间。在对象存储中读取许多小文件比读取几个大文件要慢得多您会遇到这些读取操作的延迟下限但是一旦文件被打开您会获得大约 8MB/秒的良好读取速度考虑到大多数元数据文件很小只有几 KB读取较少数量的元数据文件在许多情况下将极大地提高查询规划性能。有几种方法可以解决小文件问题但每种方法都有自身的问题减少写入频率将写入操作排队并批量处理。这将降低 Iceberg 表随时间需要管理的文件数量但是您需要部署基础设施来支持写入的缓冲和批处理例如 Apache Kafka 或 Flink。除非是硬性要求否则永远不要尝试使用应用程序逻辑通过 SQL insert 语句向 Iceberg 写入单条记录这将导致为那一行生成一组新的元数据文件。Kafka 和 Flink 在将缓冲区刷新并提交到表之前缓冲成批的行方面要好得多。使用压缩压缩小文件有效地将许多较小的元数据文件合并成较大的、整合后的文件。这意味着您需要多次写入元数据。此外当这些压缩作业运行时它们将与工作负载中并发进行的其他写入竞争。最后每次压缩数据时由于元数据文件被合并您会失去查询时间旅行的灵活性。在我们继续深入之前将小文件压缩成大文件听起来是否有些熟悉如果您是在数据库领域成长起来的这听起来很像重建/重组/整理索引。我们尚未真正解决这个问题我们只是转移了问题发生的地点。数据湖仓的创新与现存差距数据湖仓有一个非常合乎逻辑的历史演变过程。简而言之我们可以认为数据湖仓是从数据世界的三个主要里程碑演变而来的• Teradata 推出正式的数据仓库1984年。• 随着 Hadoop 和 schema-on-read读时模式成为计算策略数据湖和云对象存储被引入2010年。• 数据湖仓诞生2021年。数据仓库坚持了很长时间然而其紧密耦合的存储和计算造成了明显的瓶颈。一旦数据增长开始超出这些数据仓库的物理硬件存储容量我们就遇到了快速弹性扩展的问题。数据湖旨在通过解耦存储和计算将存储放置在对象存储上来解决数据扩展问题。对象存储可以几乎无限地扩展并提供了传统数据仓库没有考虑的另一个价值主张处理半结构化和非结构化数据。数据湖也坚持了相当长一段时间但有一些重大缺陷并最终演变成人们所说的数据沼泽即相同数据的无休止的副本治理控制薄弱。然后在 2021 年数据湖仓问世旨在提供数据仓库和数据湖的最佳优点提供仓库的速度和治理能力同时提供云对象存储的灵活性和无限扩展能力。但它们将如何做到这一点计算和存储会是什么样子数据湖仓范式有一个雄心勃勃的任务处理行星规模的数据同时保持强大的治理控制和您对数据仓库期望的性能。为了满足这些要求创建了两个新的表规范Delta Lake第一个流行的数据湖仓表规范发布。由 Databricks 开发Delta Lake 于 2019 年 10 月成为 Linux 基金会的一部分。Iceberg2017 年由 Netflix 内部开发用于应对他们在整个组织范围内为了分析必须处理的所有数据以及他们在大型数据湖中遇到的 Apache Hive 的限制。它在 2020 年被采纳为 Apache 顶级项目。一旦这些表规范获得了广泛采用并证明可靠、符合 ACID 的表可以直接存在于对象存储上我们在随后几年中看到了现在称为数据湖仓的架构模式的出现。Delta Lake 和 Iceberg 都为接下来的五年铺平了道路然而正如历史所展示的新技术最终会遇到一些有趣的边缘情况这些情况会演变成更大的问题比如我们之前讨论的 Iceberg 的那些问题。DuckLake数据库优先的架构DuckLake 建立在数据湖仓的核心承诺之上但增加了两个主要好处• 易用性• 低延迟如果您回想我们之前的代码片段实现连接到云对象存储的 DuckLake 只需要两行 SQL。它不需要分布式架构、特殊软件或复杂的配置。正如史蒂夫·乔布斯常说的“它就能用”。DuckLake 是一个开源规范其主要实现在 DuckDB 中次要实现在 Apache Spark 中。它既是一个数据湖仓格式也是一个数据湖仓目录。DuckLake 架构有三个主要组件存储、元数据目录和计算图 1-2。每一层都有多个简单的选项可供选择。[图 1-2三个组件]例如您总是可以从本地开发开始使用笔记本电脑的 SSD 进行存储使用 DuckDB 数据库管理元数据使用笔记本电脑的 CPU 进行计算。然后当部署到生产环境时您可以轻松切换到使用云对象存储和 Postgres 数据库以实现公司级别的并发性。如果您更喜欢托管服务MotherDuck 提供了一个 DuckLake 云服务使用对象存储、自动管理的目录数据库和无服务器的 DuckDB 驱动的计算。DuckLake 的架构北极星是元数据值得拥有一个真正的数据库元数据在 DuckDB、SQLite 或 Postgres 数据库中进行管理。这为读写提供了显著更低的延迟。然而您的数据文件仍然可以像其他数据湖仓一样以标准的 Parquet 格式存储在对象存储上——保留其低成本、高可扩展性和开放性。与上一代数据湖仓相比读写查询的所有元数据操作都可以在比单次对象存储往返更短的时间内完成。我们也完全避免了小文件问题这极大地降低了读取延迟并将每秒写入事务数提高了一个数量级以上。简单即快速如果您正在将 DuckLake 基于 SQL 优先的方法与传统的基于 Spark 的数据湖仓进行对比优势就变得清晰了对于大多数组织而言SQL 是一种更自然的选择。数据团队已经在使用数据库工作他们中的很大一部分用户能够流利地读写 SQL。这意味着采用 DuckLake 在很大程度上是应用现有技能的过程而不是重新培训人们去思考 Spark DataFrame API 和分布式执行模型。在实践中这降低了进入门槛缩短了达到生产力的时间并使数据湖仓对更广泛的用户群体开放。Spark 也是一个大型引擎带有显著的开销并非所有数据湖仓用例都能从这种复杂性中受益。除非您的数据规模接近世界最大级别否则由 DuckDB 或 MotherDuck 驱动的计算通常可以在不管理分布式查询引擎的情况下提供更好的性能。DuckLake 的另一个好处是它被整合到 DuckDB 生态系统中。这意味着 DuckLake 受益于所有 DuckDB 的代码库、扩展和易用性我们怎么强调易用性都不为过DuckDB 可以说是最容易安装和启动运行的 SQL 处理引擎。您只需要这样做pip install duckdbimportduckdb duckdb.sql(SELECT 42 as answer).show()除了上手容易之外DuckDB 的 SQL 方言非常丰富它直观提供了很大的灵活性以及我们称之为极致的“语法糖”。例如您是否记得在传统 RDBMS 上编写带有数百列的长篇 merge 语句DuckDB 决定通过启用仅根据列名匹配BY NAME来更新和插入记录从而极大地简化了 merge 语句。以下是这种语法糖的一个简单示例MERGEINTOgcs_wh.ordersastgtUSINGgcs_wh.orders_tmpassrcONtgt.order_idsrc.order_idWHENMATCHEDTHENUPDATEWHENNOTMATCHEDTHENINSERTBYNAME至此我们希望我们已经帮助阐明了为什么 DuckLake 对于数据湖仓来说是伟大的以及它带来的好处。这引出了我们第一章的最后一部分。为什么 DuckLake 现在很重要让我们总结一下我们讨论过的内容以及为什么我们认为您应该继续与我们同行。我们讨论了当前的数据湖仓及其架构带来的挑战包括高延迟和小文件问题。我们还展示了启动运行它们所需的复杂性相比之下DuckLake 只需要两行 SQL。DuckLake 不需要计算机集群也不需要 JVM——只需 pip install duckdb您就可以开始了。您甚至可以通过更改 DuckDB 连接字符串来使用托管的 MotherDuck DuckLake。但展望未来我们认为行业趋势将走向何方DuckLake 如何让我们保持领先我们知道房间里有一些大象。其中一个当然是人工智能它正在对工程团队处理工作流程和积压工作的方式以及创建的数据量产生巨大影响。理想情况下AI 应该使团队能够更快地交付产品减少错误但是随着这种更快交付和构建更好产品的能力需要管理的数据量将继续以惊人的速度增长。这就是为什么我们认为 DuckLake 处于有利位置可以随着未来一起扩展鉴于其元数据在数据库中进行管理它已准备好应对需要高吞吐量和低延迟的 AI 工作负载。房间里的另一头大象是“大数据已死”的想法。许多组织声称他们拥有大数据这是理所当然的如果您管理着 TB 级或更高的数据那么您就有大数据。但是您的大部分工作负载会一次性查询所有数据吗很可能不会只有一小部分工作负载可能会考虑需要一次性处理 TB 级的数据。DuckLake 和 DuckDB 的 SQL 引擎处于有利位置可以处理您组织中的大多数工作负载。马特曾多次对数据团队说过一句很棒的话“您并不总是需要 Spark 大锤来敲开一颗花生。”这个前提对许多工作负载都适用。通过简化为单节点架构DuckDB 的性能可以比分布式系统高效得多。听到“单节点”可能会让您心生恐惧但不要害怕如今的单节点非常强大。您可以在多个云提供商处租用实例或者使用无服务器的 MotherDuck 计算提供 TB 级别的 RAM 和数十 TB 的存储。没有多少工作负载不适合 TB 级的 RAM更不用说 DuckDB 在计算期间也可以充分利用所有这些存储空间。此外每个工作负载都可以使用自己独立的 DuckDB 计算实例与中央元数据库协调。因此您可以拥有任意多的大型单节点甚至每个查询一个大型节点如果您不需要那种级别的能力您的笔记本电脑可以处理所有计算。默认简单但需要时可扩展。DuckLake 与 DuckDB 的可移植性相结合开启了全新的数据平台架构。突然间“边缘”的任何 CPU 都可以成为 DuckLake 的计算节点。您的手机应用程序可以在手机的 CPU 上运行 DuckLake 查询。您连接到工厂设备的 Raspberry Pi 设备可以直接将数据流式传输到 DuckLake。从边缘摄取数据时您甚至可以直接在源头使用 SQL 过滤数据。这些手机和树莓派在过去几年中也变得非常强大。MotherDuck 的双重执行功能可以将单个查询拆分部分在云端运行部分在本地计算上运行——甚至无需安装即可在您的浏览器中运行。DuckLake 以一种全新的方式开启了数据平台的设计。DuckLake 还有多个内置的逃生舱口。DuckLake 有一个 Spark 连接器。因此没有什么能阻止您启动一个 Spark 集群来处理您的数据。您可以为工作选择最佳工具。但是如果您只同时分析几百 GB 的数据那么当您唤醒集群并启动作业时连接到您 DuckLake 的 DuckDB 客户端可能已经完成了同时只使用了一小部分计算资源。另一个逃生舱口是DuckLake 可以通过一条命令将整个目录导出到 Iceberg。因此您和您的团队可以考虑将 DuckLake 作为无风险试用的起点。如果将来需要迁移到 IcebergDuckLake 可以为您提供支持。感谢您一直以来的陪伴本书的其余部分将深入探讨激动人心的深度代码会话并讨论您需要了解的所有细节以使 DuckLake 为您工作。