Delta Lake 0.5 增加了不少新特性,这篇文章主要讲解其 Presto Integration 和 Manifests 机制。
该功能与我们之前平台化 Delta Lake 实践(离线篇) 的很多工作都较为相似,比如与 metastore 的集成,直接通过 manifest 读取 delta 存活文件等。
在 0.5 之前的版本中只支持通过 Spark 读取数据,在新版本中增加了其他处理引擎通过 manifest 文件访问 Delta Lake 的能力。下文以Presto 为例说明如何通过 manifest 文件访问数据,manifest 文件的生成及其一些限制。
Presto 使用 manifest 文件从 hive 外部表中读取数据,manifest文件是一个文本文件,包含该表/分区所有存活数据的路径列表。
当使用 manifest 文件在 Hive metastore 中定义外部表时,Presto 将会先读取 manifest 中的文件路径列表再去访问想要的文件,而不是直接通过目录列表来查找文件。
支持 sql / scala / java / python 四种 api,以 sql 和 scala 为例。
sql
GENERATE symlink_format_manifest FOR TABLE delta.`pathToDeltaTable`
Scala
val deltaTable = DeltaTable.forPath(pathToDeltaTable)
deltaTable.generate("symlink_format_manifest")
使用 GENERATE 命令会在/path/to/deltaTable/_symlink_format_manifest/
目录下 生成一个 manifest 文件,其中包含了所有存活的文件路径。
cat /path/to/deltaTable/_symlink_format_manifest/manifest
hdfs://tdhdfs-cs-hz/user/hive/warehouse/bigdata.db/delta_lsw_test/part-00000-0a69ce8d-0d9e-47e2-95b2-001bd196441d-c000.snappy.parquet
hdfs://tdhdfs-cs-hz/user/hive/warehouse/bigdata.db/delta_lsw_test/part-00000-ba1767cb-ff0e-4e65-8e83-7a0cdce6a2f4-c000.snappy.parquet
如果是分区表,例如以 ds 作为分区字段,生成的结构如果下,每个分区下都有一个 manifest 文件包含了该分区的存活文件路径。
/path/to/table/_delta_log
/path/to/table/ds=20190101
/path/to/table/ds=20190102
/path/to/table/_symlink_format_manifest
---- /path/to/table/_symlink_format_manifest/ds=20190101/manifest
---- /path/to/table/_symlink_format_manifest/ds=20190102/manifest
存活文件定义:add file - remove file
CREATE EXTERNAL TABLE mytable ( ... ) -- 与 Delta table 一致的 schema 信息
PARTITIONED BY ( ... ) -- 分区参数可选,需要与 Delta table 一致
ROW FORMAT SERDE 'org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe'
STORED AS INPUTFORMAT 'org.apache.hadoop.hive.ql.io.SymlinkTextInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION '/_symlink_format_manifest/' -- 指定 manifest 地址
通过 SymlinkTextInputFormat
,Presto 可以直接从 manifest 中读取需要的文件而不需要直接定位到数据目录。
如果是分区表的话,在运行 generate 后,需要运行 MSCK REPAIR TABLE
使 Hive Metastore 能发现最新的分区。使用 repair 有两种场景:
ADD PARTITION
操作。important: 如果使用了 kerberos 认证,必须要在 etc/catalog/hive.properties
中配置 yarn-site.xml,否则在查询数据时会提示错误
com.facebook.presto.spi.PrestoException: Can't get Master Kerberos principal for use as renewer
at com.facebook.presto.hive.BackgroundHiveSplitLoader$HiveSplitLoaderTask.process(BackgroundHiveSplitLoader.java:191)
at com.facebook.presto.hive.util.ResumableTasks.safeProcessTask(ResumableTasks.java:47)
at com.facebook.presto.hive.util.ResumableTasks.access$000(ResumableTasks.java:20)
at com.facebook.presto.hive.util.ResumableTasks$1.run(ResumableTasks.java:35)
at io.airlift.concurrent.BoundedExecutor.drainQueue(BoundedExecutor.java:78)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.io.IOException: Can't get Master Kerberos principal for use as renewer
at org.apache.hadoop.mapreduce.security.TokenCache.obtainTokensForNamenodesInternal(TokenCache.java:116)
at org.apache.hadoop.mapreduce.security.TokenCache.obtainTokensForNamenodesInternal(TokenCache.java:100)
at org.apache.hadoop.mapreduce.security.TokenCache.obtainTokensForNamenodes(TokenCache.java:80)
at org.apache.hadoop.mapred.FileInputFormat.listStatus(FileInputFormat.java:206)
at org.apache.hadoop.mapred.FileInputFormat.getSplits(FileInputFormat.java:315)
at com.facebook.presto.hive.BackgroundHiveSplitLoader.loadPartition(BackgroundHiveSplitLoader.java:304)
at com.facebook.presto.hive.BackgroundHiveSplitLoader.loadSplits(BackgroundHiveSplitLoader.java:258)
at com.facebook.presto.hive.BackgroundHiveSplitLoader.access$300(BackgroundHiveSplitLoader.java:93)
at com.facebook.presto.hive.BackgroundHiveSplitLoader$HiveSplitLoaderTask.process(BackgroundHiveSplitLoader.java:187)
... 7 more
Generate 命令生成 manifest 的逻辑并不复杂,有兴趣的同学可以看下,方法入口:
DeltaGenerateCommand
-> GenerateSymlinkManifest.generateFullManifest(spark: SparkSession,deltaLog: DeltaLog)
def writeSingleManifestFile(
manifestDirAbsPath: String,
dataFileRelativePaths: Iterator[String]): Unit = {
val manifestFilePath = new Path(manifestDirAbsPath, "manifest")
val fs = manifestFilePath.getFileSystem(hadoopConf.value)
fs.mkdirs(manifestFilePath.getParent())
val manifestContent = dataFileRelativePaths.map { relativePath =>
DeltaFileOperations.absolutePath(tableAbsPathForManifest, relativePath).toString
}
val logStore = LogStore(SparkEnv.get.conf, hadoopConf.value)
logStore.write(manifestFilePath, manifestContent, overwrite = true)
}
// 这部分修复了Delta 0.5 删除非分区表失效的BUG,已将 PR 提交社区,未合入主分支
val newManifestPartitionRelativePaths =
if (fileNamesForManifest.isEmpty && partitionCols.isEmpty) {
writeSingleManifestFile(manifestRootDirPath, Iterator())
Set.empty[String]
} else {
withRelativePartitionDir(spark, partitionCols, fileNamesForManifest)
.select("relativePartitionDir", "path").as[(String, String)]
.groupByKey(_._1).mapGroups {
(relativePartitionDir: String, relativeDataFilePath: Iterator[(String, String)]) =>
val manifestPartitionDirAbsPath = {
if (relativePartitionDir == null || relativePartitionDir.isEmpty) manifestRootDirPath
else new Path(manifestRootDirPath, relativePartitionDir).toString
}
writeSingleManifestFile(manifestPartitionDirAbsPath, relativeDataFilePath.map(_._2))
relativePartitionDir
}.collect().toSet
}
val existingManifestPartitionRelativePaths = {
val manifestRootDirAbsPath = fs.makeQualified(new Path(manifestRootDirPath))
if (fs.exists(manifestRootDirAbsPath)) {
val index = new InMemoryFileIndex(spark, Seq(manifestRootDirAbsPath), Map.empty, None)
val prefixToStrip = manifestRootDirAbsPath.toUri.getPath
index.inputFiles.map { p =>
val relativeManifestFilePath =
new Path(p).toUri.getPath.stripPrefix(prefixToStrip).stripPrefix(Path.SEPARATOR)
new Path(relativeManifestFilePath).getParent.toString
}.filterNot(_.trim.isEmpty).toSet
} else Set.empty[String]
}
val manifestFilePartitionsToDelete =
existingManifestPartitionRelativePaths.diff(newManifestPartitionRelativePaths)
deleteManifestFiles(manifestRootDirPath, manifestFilePartitionsToDelete, hadoopConf)
在 Delta Lake 更新 manifest 时,它会原子的自动覆盖现有的 manifest 文件。因此,Presto 将始终看到一致的数据文件视图,然而,保证一致性的粒度取决于表是否分区。
简单的说,如果 Presto 在 Spark 更新清单文件时发起读请求,由于 manifest 所有分区并不是一次原子更新操作,所以有可能得到的结果并不是最新的数据。
大量的文件数量会造成 Presto 性能下降,官方的建议是在执行 generate
生成 manifest 前先对文件进行 compact 操作。分区表的单个分区或是非分区表的文件数量不超过1000。
原生的 Delta Lake 支持 schema evolution
,意味着无论 hive metastore 定义的 schema 如何,都会基于文件使用最新的 Schema。由于 Presto 直接使用了定义在 hive metastore 中的 schema ,所以如果要修改 schema 信息,必须要对表进行相应更新 。
一些BUG
测试过程中还发现了一个 BUG,如果将非分区表的数据全部删除,则 generate 后 manifest 不会更新。
提交了一个RP,目前已合入主分支,将在 0.6 版本 release。
Generate does not update manifest if delete data from unpartitioned table
实践经验
首先,由于需要额外的调用 generate 命令生成/更新 manifest 文件,使用体验肯定不如直接通过 Spark 读取数据。
其次,在 generate 过程中进行数据读取有可能会遇到跨分区查询版本不一致的情况,但是瑕不掩瑜,通过 manifest,与大数据生态其他处理引擎的道路被打开了。
_delta_log
中生成 manifest,再通过 manifest 获取到的文件路径直接从文件系统中读取 Parquet 实现,有了 generate 功能,就可以直接读取 manifest 文件,外部系统扩展工作量极大的简化。(delta generate 这种在写入时生成 manifest 的方式更适合那种读多写少的场景)