HCatalog Streaming Mutation API
背景
Hive新版本支持插入,删除,更新操作,ORC格式的文件支持同一个事务中的大批量数据修改,Hive的执行引擎,现在在每一个单独的事务中提交每一个独立的修改操作,然后,让hive表扫描(这些扫描其实是MR任务)来执行修改操作。
这种方式不能满足在一个原子的管理方式下,处理大量的deltas文件。
Streaming API只能写新的数据,mutation API可以修改已有的数据。
这两个API是基于完全不同的事务模型。
Streaming API通过持续把许多小集合的写操作,分批到短小的事务中。
相反,mutation API被设计成,通过少次应用大量修改操作到一个数据集上,以原子的形式,要么所有的修改生效,要么所有修改都不生效。
Structure
API主要由两部分组成,事务管理和把修改操作写到数据集上。
这两个部分有一个小耦合,事务被一个单独的job launcher启动处理,然而,数据修改的写入则会被分配到很多工作节点上。
与hadoop的概念比较,这类似于工具和MR任务组件。
在内部,事务是通过Hive Metastore管理的。修改操作是通过ORC API执行到HDFS上面的,绕开了Metastore。
除此之外,你希望配置你的Metastore实例来执行阶段性的compaction。
Data Requirements
一般来说,对一条记录进行修改操作,必须有一个唯一的key来识别这条记录。
然而hive没有主键的概念。
在内部,hive在一个虚拟的ROW_ID列上,存储了RecordIdentifiers ,用它来唯一识别ACID表中的一条记录。
对虚拟的ROW_ID列的理解:
只有ORC格式存储的hive表此列才有值,其它情况下是NULL
ROW_ID与rowId不同
ROW_ID由transactionId,bucketId,rowId组成,
rowId只是ROW_ID组成的一部分。
举例:
第一个事务插入如下记录
MutableRecord asiaIndiaRecord1 = (MutableRecord) bucketIdResolver.attachBucketIdToRecord(
new MutableRecord(1,“Namaste streaming 1”));
MutableRecord asiaIndiaRecord2 = (MutableRecord) bucketIdResolver.attachBucketIdToRecord(
new MutableRecord(2,“Namaste streaming 2”));
在第二个事务中插入记录3
MutableRecord asiaIndiaRecord3 = (MutableRecord) bucketIdResolver.attachBucketIdToRecord(
new MutableRecord(20,“Namaste streaming 3”));
这样它们的RecordIdentifiers 如下
assertThat(indiaRecords.get(0).getRecordIdentifier(), is(new RecordIdentifier(1L, 0, 0L)));
assertThat(indiaRecords.get(1).getRecordIdentifier(), is(new RecordIdentifier(1L, 0, 1L)));
assertThat(indiaRecords.get(2).getRecordIdentifier(), is(new RecordIdentifier(2L, 0, 0L)));
(1L,0,0L)代表事务id 1,bucketid 0, 按照rowid排序后的顺序是0
(1L,0,1L)代表事务id 1,bucketid 0, 按照rowid排序后的顺序是1
(2L,0,0L)代表事务id 2,bucketid 0, 按照rowid排序后的顺序是0
因此,任何程序希望使用这个API来修改表,必须先获取目标记录的ROW__ID值。
这意味着,在实践中,进行修改操作,必须先读入数据的当前snapshot,然后和修改操作在一些字段上进行join,这样来获取对应的ROW_ID。
这个操作就是当update,insert,delete操作的时候,hive表进行的的scan操作。
AcidInputFormat提供了访问ROW_ID的方法,通过AcidRecordReader.getRecordIdentifier()。
ACID格式的实现,增加了一些约束,记录被写入的顺序被约束了,并且这种排序是强制的。
并且,数据必须被适当的分组来达到OrcRecordUpdater强加的约束。
分组同样使修改操作的并行执行成为了可能。
最后,正确的给新的记录分桶,也需要使用一个小技巧。
简而言之,API客户端应该为mutate API提供如下的数据:
(1) MUST: 记录按照 ROW__ID.originalTxn排序, 然后按照ROW__ID.rowId排序.
(2)MUST: 指定一个 ROW__ID, 包含一个计算好的 bucketId 给每条待insert的记录.
(3)SHOULD: 按照表分区值来分组, 然后按照 ROW__ID.bucketId分组.
Record Layout
记录的结构,设计,编码是client端修改专门关心的地方,可能和目标Hive ACID表非常不一样。
mutation API需要实现MutatorFactory 和Mutator类,来从记录中抽取相关的数据,并且序列化成ACID文件。
AbstractMutator, RecordInspectorImpl提供了基础类,简单的实现了上面两个类。
RecordInspectorImpl通过extractRecordIdentifier方法可以提供RecordIdentifier
通常,我们需要做的是,指定一个合适的ObjectInspector ,提供ROW__ID索引,记录的桶列。这两个类里面的所有索引列,都和你的记录结构有关,而不是Hive表结构。
你可能想要使用BucketIdResolver 来追加bucket id到一个新的记录上。
核心实现在BucketIdResolverImpl 中,但是,注意,bucket列索引必须和它们在hive表定义中的顺序一致,以此来保证一致性分桶。
注意,你不能在桶之间移动记录,如果这样做会抛出异常。
在实际场景中,这意味着,你不能使用UPDATE修改桶列。
Connection and Transaction Management
MutatorClient 类用来创建和管理事务。事务的范围可以跨越多个ACID表。
当一个客户端连接之后,它与metastore通信,获取目标表的metadata信息。
对newTransaction 方法的调用,打开和metastore的事务,完成对AcidTable的收集工作,并返回一个新的Transaction 实例。ACID 表是轻量的,序列化的对象。
通常情况下,你的MutatorClient会运行在一些master节点上面,你的协调者会运行在工作节点上。
一个事务,必须通过begin方法来初始化。这个方法的调用,需要使用metastore对目标表进行lock,并且启动heartbeat来阻止事务超时。
强烈推荐,你在客户端注册一个LockFailureListener,这样你的程序能够处理任何lock活事务的失败。当事务开始之后,你可以使用一个或多个MutatorCoordinator实例来执行流式的改变操作,然后当操作被完成之后,可以commit或者abort这个事务。
最后,你需要close mutation client来释放资源。
MutatorClientBuilder 用来简化客户端的构造。
Writing Data
MutatorCoordinator 类用来启动对一个ACID表的修改操作。
在事务中,对于每一个表,你至少需要一个实例。
一个给定实例的目标是由相关的构建协调者的ACID表决定的。
推荐使用MutatorClientBuilder 来简化构建过程。
修改操作,通过调用协调者上面的insert,update,delete方法来实现。
这些方法的参数是记录的分区和被修改的记录。
当是一个未分区表的时候,只需要传一个空的list参数作为分区值。
对于insert,只有bucket id会从RecordIdentifier中取到,事务id和row id会被忽略,
被RecordUpdater中相应的值来替换掉。
Delete操作的时候,所有的东西都会被忽略,除了记录的RecordIdentifier ,因此,简单的提交原始数据更容易一些。
Dynamic Partition Creation
新分区自动创建时很有必要的。在这种情况下,需要hive预先创建好需要的分区可能是不合理的。API允许协调者在需要的时候才创建分区。参考这个方法MutatorClientBuilder.addSinkTable(String, String, boolean)
多个协调者,可以竞赛创建分区,但是只有一个会创建成功,因此协调者客户端不需要异步创建分区。
当使用这个操作的时候,需要仔细一些,因为它需要协调者维护一个和metastore库的连接,
当协调者在一个分布式环境下运行的时候,它可能会给metastore增加压力。
在这种情况下,最好,关闭分区创建,在你的ETL merge进程中收集受影响的分区。
你可以在你的client端代码中,当集群端merge完成之后,单独和metastore连接,创建分区。
最终,注意一下,当分区创建关闭的时候,协调者一定自己合成分区URI,因为它们不能从metastore中检索到分区。此时,如果你HDFS中的分区的结构不遵循Hive标准,就会引起问题。(hive标准 在下面的类中实现org.apache.hadoop.hive.metastore.Warehouse.getPartitionPath(Path, LinkedHashMap
Reading Data
尽管这个API主要关心修改数据,我们必须先读取已经存在的数据,来获取相关的ROW__IDs。
因此,用一种强壮和连续的方式读取ACID数据需要如下:
使用API的步骤如下: