数据湖漫谈|Hive ACID vs. Delta Lake

Qubole现在支持对存储在Cloud数据湖中的数据进行高效的Update和Delete。用户可以对开启了事务的Hive表进行insert,update和delete,并通过Apache Spark或Presto进行查询。使用Apache Spark或Presto操作Hive的事务表功能,我们已将其开源,我们对于更多引擎支持update和delete的工作也在进行中,这块同样也会开源。

在这篇文章中,我会介绍该功能,设计实现以及未来的路线图。

动机和背景

我们看到越来越多的用户对存储在数据湖中的数据渴望有高效可靠的update和delete解决方案,尤其是保存在云对象存储中的数据。对于这种update和delete,传统的做法是在分区(partition)级别全部重写并覆盖(overwrite)旧的数据。如果使用这种方法,即使只是数据更新了几条,你都需要全部重写大量数据,因此该方法无法有效扩展。由于GDPR和CCPA之类的安全合规要求,对高性能和高性价比解决方案的需求也变得迫在眉睫。

在比较了不同的技术方法之后,我们选择了Apache Hive的ACID事务作为Qubole中update/delete支持的基础。Qubole现在使用的Hive3.1支持事务,用户可以使用Hive的DML语句对以ORC格式保存的数据进行追加(append),更新(update)和删除(delete),如果是Parquet格式的数据则只能进行追加(append)。我们还增强了Qubole中的Presto和Apache Spark,使其能够读取此类事务表,并将这些更改回馈给了开源社区。我们的解决方案建立在Hive的Metastore Server上,当数据发生变化时,可以提供自动或者手动的合并/清除操作。

2.1 开源用户设置指南

1.用户必须使用Hive 3.0及更高版本。如果你使用的是旧版本,建议你将Hive Metastore database和server升级到3.1.2。旧一点的Hive比如v2.3可以继续与Hive3.1.2兼容。

2.为了让Spark能够读取Hive事务表,用户必须使用可用的Spark ACID数据源,参考:

https://github.com/qubole/spark-acid

或者相对应的Spark package与Spark2.4+版本一起使用,Spark package参考:

https://spark-packages.org/package/qubole/spark-acid

3.要让Presto读取Hive事务表,用户必须从master分支构建PrestoSQL并且应用补丁PR-1257:

https://github.com/prestosql/presto/pull/1257

对应open issue PrestoSQL-576

https://github.com/prestosql/presto/issues/576

2.2 Qubole用户设置指南

1.要让Hive能够支持读取/写入ACID事务表,参考:

https://docs.qubole.com/en/latest/user-guide/hive/hive-acid/prerequisites-acid.html

2.要让Presto和Spark读取Hive ACID事务表,可以联系Qubole的技术支持customersupport@qubole.com

2.3 用法示例

以下是具有完整ACID表(当前仅支持ORC格式)的典型流程示例:

1.在Hive中创建一个事务表并插入一些数据

create table acidtbl (key int, value string)
                     stored as orc 
                     TBLPROPERTIES ("transactional"="true");
insert into acidtbl values(1,'a');
insert into acidtbl values(2,'b');

或者

2.通过简单的操作元数据也可以实现将现有的ORC非事务表转换为事务表

alter table nonacidtbl set TBLPROPERTIES ('transactional'='true');

3.使用Hive进行delete,update和merge数据

delete from acidtbl where key=1;
update acidtbl set value='updated' where key=2;
merge into acidtbl using src
           on acidtbl.key = src.key
           when matched and src.value is NULL then delete
           when matched and (acidtbl.value != src.value) then 
                update set value = src.value
           when not matched then insert values (src.key, src.value);

4.使用Hive或Presto读取结果

select * from acidtbl;

5.使用Scala通过Spark读取结果

scala> val df = spark.read.format("HiveAcid").options(Map("table" -> "default.acidtbl")).load()
scala> df.collect()

对于已有的ORC格式数据文件,你也可以直接使用Hive的create table语法直接创建事务表,而无需进行任何数据格式转换。如果已有的数据文件格式为Parquet,同样的方法你只能创建仅支持插入(insert-only)的表。

深度分析

3.1 Why Hive ACID?

许多开源项目都在解决多版本并发控制(MVCC, multi-version concurrency)以及对数据湖中的数据进行事务更新和删除。比较突出的几个产品包括:

这里我们首先排除Apache Kudu,因为它不是为云存储中的数据而构建的。所有其他项目都支持快照隔离。我们按照以下不同的维度对他们进行对比,但没有特定的顺序:

1.Support for updates and deletes

2.Support for compaction and cleanup

3.Support for Parquet and ORC formats

4.Support for Hive, Spark, and Presto

5.Support for SQL DML statements

6.Write amplification

7.Open source governance

下图总结了截至到2019年9月的一些对比,红色部分代表它们有一些问题,绿色部分则代表它们比较有优势。

通过上表,你可以发现如果要支持所有的特性,对Hive的改动会最小,具体来说只需要:

  • 增加Presto和Spark对Hive ACID的读/写支持;

  • 增加Hive ACID支持Parquet文件格式的更新/删除。

上述两点其实对于Hive来说非常简单,因为Hive的社区相当的活跃,尽管这是一个主观的呼吁,但是相较Hive,其他的产品向功能全面的解决方案过渡的难度要更大,比如:

1.Apache Iceberg非常有前途,但有关更新/删除支持的设计尚未最终确定

2.Apache Hudi似乎也很有前途,但是在数据摄取(data ingestion)这一块与Spark结合的太紧密,我们认为需要花费较大的成本才能扩展到其他引擎。

3.Delta.io是为Spark和Parquet量身定制的,但是它的写入放大(high write amplification),缺少SQL DML支持和缺乏压缩支持方面都存在明显的缺陷。上表中其他的项目都是Apache项目,Delta Lake最近才成为Linux基金会的子项目。

3.2 Hive ACID是如何工作的

Hive ACID大致上通过维护子目录来存储不同的版本,并对表的变化进行update/delete。Hive Metastore用于跟踪不同的版本,下图是一张动画示意:

3.3 Hive ACID的挑战

Hive ACID主要用于使用Hadoop的HDFS文件系统中。由于云存储与HDFS语义上的差异,在云中使用此类工具不可避免会碰到一些问题,这里强调两点:

  • 云存储中重命名(renames)开销特别大   – Hive在写入数据的时候,首先会将其写入临时位置,然后在最后的提交步骤中将其重命名为最终位置。在AWS的S3等云存储系统中,重命名的开销比较大。

为了减少Hive因为这个特性带来的印象,我们更改了Qubole中Hive的行为,使其直接写入最终位置,并避免了昂贵的重命名操作。Qubole对于普通的Hive表(regular table)一直采用的是这种优化手段 – 这个办法也特别适用于事务表,因为正在进行的事务数据不会被任何查询读取。

  • 在云存储中重命名目录不具备原子性(atomic)- 由于目录重命名不是原子操作,因此在目标目录中可以看到部分数据。这不是Hive中的事务更新的问题。但是,Hive 3.1中的Hive ACID compaction不是作为事务运行的。导致的结果就是,compaction(执行rename操作)与读取操作同时运行是不安全的。此问题在Hive的更高版本中通过HIVE-20823已修复。Qubole使用的Hive3.1中已包含该补丁。

3.4 Spark实现

如之前提到的,我们正在开发使用Spark读取Hive ACID事务表功能,并将这块功能开源,我们想选择一种易于开源的设计方法。考虑到这一点,我们倾向于基于Spark DataSource的实现,该实现可以作为第三方库开源,并可以由用户通过Spark包的方式引入。参考:

https://spark-packages.org/

我们选择DataSource v1,因为它是目前较为稳定的DataSource API。这种实现方式的关键点如下:

1.它使用Hive readers(InputFormat)读取Hive事务表,然后它会使用Hive writers(OutputFormat)来进行insert,update和delete;

2.与Hive Metastore通信以获取可以读取的事务表的当前快照,并在RDD的整个生命周期中使用相同的快照;

3.不获取Hive表上的读取锁(read locks),因此依赖管理员不删除可能正在读取的数据。管理员需要禁用自动清除(automatic cleanup),以便Hive Metastore不会删除数据;

4.在数据摄取和更新期间将采取写锁定(write locks)。

参考:

https://github.com/qubole/spark-acid

3.5 Presto实现

在添加对读取Hive事务表的支持时,Presto面临两个主要挑战:

  • 协调Hive事务和Presto事务 – Presto拥有自己的事务管理,我们扩展了该事务管理,以便为Presto事务中的每个查询设置Hive事务。多个Hive事务(一次仅一个活动的)可以成为Presto事务的一部分。它们在查询开始时打开,并在查询结束时关闭;Hive事务中的任何失败都会使整个Presto事务失败。

  • Hive事务表的高性能reader – 我们为此评估了多种设计选择,并决定扩展Presto原生的ORC reader。与其它方法相比,此方法涉及的改动会较大,但从性能角度来看,这是最佳选择。在此实现中,们确保事务表继续使用流拆分生成(streaming split generation),利用读数据的延迟物化(lazy materialization),并且不会受到Presto原生的ORC reader对STRUCT数据类型的性能影响。这在我们的基准测试中带来了不错的效果,与读取普通表相比,在读取Hive事务表方面几乎没有表现出任何损失。

我们目前正在努力增强Spark的功能,以提供从Spark到Hive ACID表的插入,更新和删除事务的功能。我们希望它能够很快开源并可用,大家可以关注Spark-ACID github存储仓库以获取更新:

https://github.com/qubole/spark-acid

Presto的更改正在被合并到开源中,您可以按照Presto Pull Request#1257的要求获取最新的详细信息和补丁。

https://github.com/prestosql/presto/pull/1257

最后我们还在评估Hive ACID支持Parquet文件格式的update/delete。

原文参考:

https://www.qubole.com/blog/qubole-open-sources-multi-engine-support-for-updates-and-deletes-in-data-lakes/