概述

随着存储表格式 Apache Hudi、Apache Iceberg 和 Delta Lake 的发展,越来越多的公司正在这些格式的基础上构建其 Lakehouse,以用于许多用例,例如增量摄取。 但当数据量增加时,更新插入的速度有时仍然是一个问题。

在存储表中,使用Apache Parquet作为主要文件格式。 在本文中我们将讨论如何构建行级二级索引以及在 Apache Parquet 中引入的创新,以加快 Parquet 文件内数据的更新插入速度。 我们还将展示基准测试结果,显示速度比 Delta Lake 和 Hudi 中的传统的写入时复制快得多。

动机

高效的表 ACID 更新插入对于当今的 Lakehouse 至关重要。 数据保留和变更数据捕获 (CDC) 等重要用例严重依赖它。 虽然 Apache Hudi、Apache Iceberg 和 Delta Lake 在这些用例中被广泛采用,但当数据量扩大时,更新插入速度会变慢,特别是对于写入时复制模式。 有时缓慢的更新插入会成为耗时和资源消耗的任务,甚至会阻碍按时完成任务。

为了提高 upsert 的速度,我们在具有行级索引的 Apache Parquet 文件中引入了部分写时复制,可以跳过不必要的数据页(Apache Parquet 中的最小存储单元),从而实现高效读写。 这里的术语“部分”意味着仅对文件内的相关数据页执行更新插入,但跳过不相关的数据页。 一般情况下只需要更新一小部分文件,大部分数据页可以跳过。 与 Delta Lake 中的写入时复制相比,我们观察到速度有所提高。

LakeHouse 中的写时复制

在本文中我们使用 Apache Hudi 作为示例,但类似的想法也适用于 Delta Lake 和 Apache Iceberg。 Apache Hudi 支持两种类型的 upsert:写时复制和读时合并。 通过写时复制,在更新范围内具有记录的所有文件都将被重写为新文件,然后创建新的快照元数据以包含新文件。 相比之下读时合并只是添加用于更新的增量文件,然后将其留给读取器进行合并。 一些用例(例如“被遗忘权”)通常使用写时复制模式,因为它可以减轻读取压力。

下图显示了更新分区表的一个字段的示例。 从逻辑视图来看,用户 ID1 的电子邮件字段被替换为新电子邮件,并且其他字段没有更新。 从物理上讲,表数据作为单独的文件存储在磁盘上,并且在大多数情况下,这些文件根据时间或其他分区机制分组为分区。 Apache Hudi 使用索引系统来定位每个分区中受影响的文件,然后完全读取它们,更新内存中的电子邮件字段,最后写入磁盘并形成新文件。 图中的红色表示被重写的新文件。

图 1:表更新插入的逻辑和物理文件视图

正如博客“使用 Apache Hudi 在 Uber 构建大规模事务数据湖”中提到的,我们的数据湖中一些表收到的更新分布在 90% 的文件中,导致任何给定的大型数据重写约 100 TB。因此写时复制的速度对于许多用例来说至关重要,缓慢的写时复制不仅会导致作业运行时间更长,还会消耗更多的计算资源。 在某些用例中我们看到大量的 vCore 被使用,相当于花费了数百万美元。

引入行级二级索引

在讨论如何改进 Apache Parquet 中的写时复制之前,我们想先介绍一下 Parquet 行级二级索引,我们用它来定位 Parquet 中的数据页,以帮助加速写时复制。

Parquet行级二级索引是在第一次写入Parquet文件时或通过离线读取Parquet文件时构建的。 它将记录映射到 [file, row-id] 而不仅仅是 [file]。 例如,RECORD_ID可以用作索引键,FILE和Row_ID用于指向文件以及每个文件的偏移量。

图 2:Apache Parquet 的行级索引

在 Apache Parquet 内部,数据被划分为多个行组。 每个行组由一个或多个列块组成,这些列块对应于数据集中的一列。 然后每个列块的数据以页的形式写入。 块由页组成,页是访问单个记录必须完全读取的最小单位。 在页面内部,除了编码的词典页面之外,每个字段都附加有值、重复级别和定义级别。

如上图所示,每个索引都指向该记录所在页面内的行。 通过行级索引,当收到更新时,我们不仅可以快速定位到哪个文件,还可以定位到哪些数据页需要更新。 这将帮助我们跳过所有其他不需要更新的页面,并节省大量计算资源以加快写时复制过程。

Apache Parquet 中的写入时复制

我们引入了一种在 Apache Parquet 中执行写时复制的新方法,以实现 Lakehouse 的快速更新插入。 我们仅对 Parquet 文件内的相关数据页执行写时复制更新,但通过直接复制为字节缓冲区而不进行任何更改来跳过不相关的数据页。 这减少了更新插入操作期间需要更新的数据量并提高了性能。

图 3:Apache Hudi 中传统写时复制与新写时复制的比较

我们演示了新的写时复制过程,并将其与传统过程进行比较。 在传统的Apache Hudi upsert中,Hudi利用记录索引来定位需要更改的文件,然后将文件记录一条条读取到内存中,然后搜索要更改的记录。 应用更改后,它将数据作为一个全新文件写入磁盘。 在这个读取-更改-写入过程中,存在一些昂贵的任务(例如,解(重新)压缩、解(重新)编码、具有重复级别定义级别的记录解(重新)组装等),这些任务会消耗 大量的 CPU 周期和内存。

为了改善这个耗时和资源消耗的过程,我们使用行级索引和 Parquet 元数据来定位需要更新的页。 对于那些不在更新范围内的页,我们只是将数据作为字节缓冲区逐字复制到新文件,而无需解(重新)压缩、解(重新)编码或记录解(重新)组装。 我们称之为“复制和更新”过程。 下图对其进行了更详细的描述。

图 4:Parquet 文件中新的写时复制

基准测试结果

我们进行了基准测试,以使用TPC-DS数据比较我们的快速写时复制方法与传统方法(例如 Delta Lake)的性能。

我们使用具有相同 vCore 数量和 Spark 作业内存设置的 TPC-DS 销售数据,以开箱即用的配置进行测试。 我们选择了 5% 到 50% 之间的一定比例的数据进行更新,然后比较 Delta Lake 和新的写时复制所消耗的时间。 我们认为 50% 作为最大值足以满足实际用例。

测试结果表明新方法可以实现明显更快的速度。 当更新数据的百分比时,获得的性能是一致的。

免责声明:DeltaLake 上的基准测试使用默认的开箱即用配置。

图 5:新写时复制与传统 Delta Lake 的基准测试结果

结论

总之高效的 ACID 更新插入对于当今的LakeHouse至关重要。 虽然 Apache Hudi、Delta Lake 和 Apache Iceberg 被广泛采用,但更新插入的速度缓慢仍然是一个挑战,特别是当数据量扩大时。 为了解决这一挑战,我们在具有行级索引的 Apache Parquet 文件中引入了部分写时复制,这可以有效地跳过不必要的数据页读写。 我们已经证明这种方法可以显着提高更新插入的速度。 我们的方法使公司能够高效地执行数据删除和 CDC,以及依赖 LakeHouse 中高效表更新插入的其他重要用例。

未来工作

我们计划将行级索引和快速写时复制功能集成到 Apache Hudi,Uber 的 LakeHouse 就是在 Apache Hudi 上构建。 我们将看到这种集成将如何提高 Apache Hudi 的性能并帮助我们的客户解决增量摄取等问题。敬请关注!

加速LakeHouse ACID Upsert的新写时复制方案的更多相关文章

  1. php 垃圾回收机制----写时复制和引用计数

    PHP使用引用计数和写时复制来管理内存.写时复制保证了变量间复制值不浪费内存,引用计数保证了当变量不再需要时,将内存释放给操作系统. 要理解PHP内存管理,首先要理解一个概念----符号表. 符号表的 ...

  2. JAVA中写时复制(Copy-On-Write)Map实现

    1,什么是写时复制(Copy-On-Write)容器? 写时复制是指:在并发访问的情景下,当需要修改JAVA中Containers的元素时,不直接修改该容器,而是先复制一份副本,在副本上进行修改.修改 ...

  3. fork()和写时复制

    写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建:当发出fork(  )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程.这种行为是非常耗时的,因为它需要: · ...

  4. Linux的fork()写时复制原则(转)

    写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建:当发出fork(  )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程.这种行为是非常耗时的,因为它需要: · ...

  5. Linux进程管理——fork()和写时复制

    写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建:当发出fork(  )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程.这种行为是非常耗时的,因为它需要: · ...

  6. [转]QVector与QByteArray——Qt的写时复制(copy on write)技术

    我们在之前的博文QVector的内存分配策略与再谈QVector与std::vector——使用装饰者让std::vector支持连续赋值中简单聊了聊QVector内存分配和赋值方面的一点东西,今天接 ...

  7. 写时复制和fork,vfork,clone

    写时复制 原理: 用了“引用计数”,会有一个变量用于保存引用的数量.当第一个类构造时,string的构造函数会根据传入的参数从堆上分配内存,当有其它类需要这块内存时,这个计数为自动累加,当有类析构时, ...

  8. Java进阶知识点6:并发容器背后的设计理念 - 锁分段、写时复制和弱一致性

    一.背景 容器是Java编程中使用频率很高的组件,但Java默认提供的基本容器(ArrayList,HashMap等)均不是线程安全的.当容器和多线程并发编程相遇时,程序员又该何去何从呢? 通常有两种 ...

  9. php 内核变量 引用计数器写时复制

    写时复制,是一个解决内存复用的方法,就是你在php语言层,如$d=$c=$b=$a='value';把$a赋给另一个或多个变量,这时这个变量都只占用一个内存块,当其中一个变量值改变时,才会开辟另一个内 ...

  10. Rust写时复制Cow<T>

    写时复制(Copy on Write)技术是一种程序中的优化策略,多应用于读多写少的场景.主要思想是创建对象的时候不立即进行复制,而是先引用(借用)原有对象进行大量的读操作,只有进行到少量的写操作的时 ...

随机推荐

  1. Redis(六)集群

    Redis集群 1.1 存在的问题 容量不够Redis如何扩容 并发写操作,Redis如何分摊 当主机或者从机宕机,薪火相传.反客为主等主从模式都会导致ip发生变化,应用程序中的配置需要对应修改主机地 ...

  2. YOLO1论文中文版

    文章目录 YOLO1中文版 摘要 1. 引言 2. 统一检测 2.1 网络设计 2.2 训练 2.3 推断 2.4 YOLO的限制 3. 与其它检测系统的比较 4. 实验 4. 1 与其它实时系统的比 ...

  3. Python 函数及参数的使用

    函数 带名字的代码块,用于完成具体的工作 关键字def定义一个函数,定义函数名,括号内是需要完成任务所需要的信息,最后定义冒号结尾 缩进构成函数体 函数调用,依次指定函数名以及冒号括起来的必要信息 d ...

  4. 记一次nginx配置不当引发的499与failover 机制失效

    背景 nginx 499在服务端推送流量高峰期长期以来都是存在的,间或还能达到告警阈值触发一小波告警,但主观上一直认为499是客户端主动断开,可能和推送高峰期的用户打开推送后很快杀死app有关,没有进 ...

  5. SRIO接口卡航电总线解决方案

    TES600是天津拓航科技的一款基于FPGA与DSP协同处理架构的通用高性能实时信号处理平台,该平台采用1片TI的KeyStone系列多核浮点/定点DSP TMS320C6678作为主处理单元,采用1 ...

  6. 2023-01-01:remix-ide是浏览器的ide,官方已经提供地址,但是需要连接外网。如果是内网,需要自己在服务器里搭建remix-ide;另一种方式是用remix-ide的桌面版。这里只讨论

    2023-01-01:remix-ide是浏览器的ide,官方已经提供地址,但是需要连接外网.如果是内网,需要自己在服务器里搭建remix-ide:另一种方式是用remix-ide的桌面版.这里只讨论 ...

  7. 2020-11-15:手写代码:行有序、列也有序的二维数组中,找num,找到返回true,否则false?

    福哥答案2020-11-15: 此题来源于leetcode240和剑指 Offer(第 2 版)面试题4.1.线性查找.从二维数组的坐下角开始查找.如果当前元素等于目标值,则返回 true.如果当前元 ...

  8. vue全家桶进阶之路24:Mock

    Mock 是一个 JavaScript 库,用于生成随机数据或模拟 HTTP 请求响应,用于前端开发中的单元测试.功能测试.集成测试等场景. Mock 可以生成各种类型的数据,包括字符串.数字.布尔值 ...

  9. ET介绍——组件式设计(优化版的ECS)

    组件式设计 在代码复用和组织数据方面,面向对象可能是大家第一反应.面向对象三大特性继承,封装,多态,在一定程度上能解决不少代码复用,数据复用的问题.不过面向对象不是万能的,它也有极大的缺陷: 1. 数 ...

  10. 天下苦 Spring 久矣,Solon v2.2.20 发布

    Solon 是什么框架? 一个,Java 新的生态型应用开发框架.它从零开始构建,有自己的标准规范与开放生态.与其他框架相比,它解决了两个重要的痛点:启动慢,费资源. 解决痛点? 由于Solon Be ...