问题描述
我们通过Flink插入一张演示用Hudi表,SQL语句如下:
CREATE TABLE t1(
uuid VARCHAR(20),
name VARCHAR(10),
age INT,
ts TIMESTAMP(3),
`partition` VARCHAR(20)
)
PARTITIONED BY (`partition`)
WITH (
'connector' = 'hudi',
'path' = 'hdfs:///path/to/table/',
'table.type' = 'COPY_ON_WRITE'
);
-- insert data using values
INSERT INTO t1 VALUES
('id1','Danny',23,TIMESTAMP '1970-01-01 00:00:01','par1'),
('id2','Stephen',33,TIMESTAMP '1970-01-01 00:00:02','par1'),
('id3','Julian',53,TIMESTAMP '1970-01-01 00:00:03','par2'),
('id4','Fabian',31,TIMESTAMP '1970-01-01 00:00:04','par2'),
('id5','Sophia',18,TIMESTAMP '1970-01-01 00:00:05','par3'),
('id6','Emma',20,TIMESTAMP '1970-01-01 00:00:06','par3'),
('id7','Bob',44,TIMESTAMP '1970-01-01 00:00:07','par4'),
('id8','Han',56,TIMESTAMP '1970-01-01 00:00:08','par4');
然后我们进入Hudi cli,执行show fsview all
命令,查看HUDI数据目录的文件存储结构。我们发现无法查询到任何内容。但是通过hdfs dfs -ls /path/to/table
命令查看,数据文件确实是存在的。
问题分析和临时解决方案
第一部分
我们首先怀疑的地方是HUDI。找到HUDI处理这条命令的源代码。如下所示:
@CliCommand(value = "show fsview all", help = "Show entire file-system view")
public String showAllFileSlices(
@CliOption(key = {"pathRegex"}, help = "regex to select files, eg: 2016/08/02",
unspecifiedDefaultValue = "*/*/*") String globRegex,
@CliOption(key = {"baseFileOnly"}, help = "Only display base files view",
unspecifiedDefaultValue = "false") boolean baseFileOnly,
@CliOption(key = {"maxInstant"}, help = "File-Slices upto this instant are displayed",
unspecifiedDefaultValue = "") String maxInstant,
@CliOption(key = {"includeMax"}, help = "Include Max Instant",
unspecifiedDefaultValue = "false") boolean includeMaxInstant,
@CliOption(key = {"includeInflight"}, help = "Include Inflight Instants",
unspecifiedDefaultValue = "false") boolean includeInflight,
@CliOption(key = {"excludeCompaction"}, help = "Exclude compaction Instants",
unspecifiedDefaultValue = "false") boolean excludeCompaction,
@CliOption(key = {"limit"}, help = "Limit rows to be displayed", unspecifiedDefaultValue = "-1") Integer limit,
@CliOption(key = {"sortBy"}, help = "Sorting Field", unspecifiedDefaultValue = "") final String sortByField,
@CliOption(key = {"desc"}, help = "Ordering", unspecifiedDefaultValue = "false") final boolean descending,
@CliOption(key = {"headeronly"}, help = "Print Header Only",
unspecifiedDefaultValue = "false") final boolean headerOnly)
throws IOException {
HoodieTableFileSystemView fsView = buildFileSystemView(globRegex, maxInstant, baseFileOnly, includeMaxInstant,
includeInflight, excludeCompaction);
// ...
// 后面的代码省略
}
我们注意到pathRegex
这个参数(在方法中的参数名为globRegex
)。这个参数的默认值提供的是有3个分区字段的情况。和我们上面的例子不同。上面的例子中只有1个分区字段。为了验证方便,我们修改源代码,将globRegex
变量通过日志打印出来(中间步骤省略)。编译后再次执行show fsview all
命令。我们惊奇的发现,globRegex
变量和我们输入的pathRegex
参数不一致。我们传入*/*/*
,但globRegex
得到的却是*/**
。到这里,我们开始怀疑Shell框架也有问题。
结合@CliOption
注解这种写法和pom文件中的依赖,不难发现Hudi cli使用的Shell框架为spring-shell 1.2.0.RELEASE。我们尝试着找spring-shell源代码中是否有处理符号相关的内容。发现有这么一段代码。它位于org.springframework.shell.core.AbstractShell::executeCommand
,spring shell本意用这段代码处理shell命令中的块注释和单行注释。碰巧我们的参数*/*/*
就包含了块注释开始和结束的符号。spring-shell的这个功能在此场景下明显是“画蛇添足”了。
我们分析下spring-shell是如何处理块注释的。org.springframework.shell.core.AbstractShell::executeCommand
方法代码如下所示:
// We support simple block comments; ie a single pair per line
// spring shell只支持行内的块注释
// inBlockComment标记了目前解析到了块注释之内
// 如果不在块注释之内,并且命令行字符串同时包含/*和*/
if (!inBlockComment && line.contains("/*") && line.contains("*/")) {
blockCommentBegin();
// 我们将最后一个/*,连同它后面的字符串都裁剪掉
String lhs = line.substring(0, line.lastIndexOf("/*"));
// 如果含有块注释结束*/符号
if (line.contains("*/")) {
// 裁剪掉命令行最有一个*/及其之前的所有内容,和lhs拼接在一起,这样就得到了/*和*/之外的字符串内容
line = lhs + line.substring(line.lastIndexOf("*/") + 2);
blockCommentFinish();
} else {
line = lhs;
}
}
if (inBlockComment) {
if (!line.contains("*/")) {
return new CommandResult(true);
}
blockCommentFinish();
line = line.substring(line.lastIndexOf("*/") + 2);
}
// We also support inline comments (but only at start of line, otherwise valid
// command options like http://www.helloworld.com will fail as per ROO-517)
if (!inBlockComment && (line.trim().startsWith("//") || line.trim().startsWith("#"))) { // # support in ROO-1116
line = "";
}
spring-shell针对块注释的处理逻辑在上面代码注释中我们已分析,但是它还有bug。出现bug的地方正是我们的场景。对于*/*/*
。首先裁剪掉最后一个/*
及其后面的字符串,此时lhs
为*/*
。然后裁剪掉最后一个*/
及其前面的所有内容,得到*
,然后将二者拼接在一起,最终结果为*/**
。这明显是有问题的。spring-shell没考虑到*/
会在/*
之前这种情况。上面是分析这段代码时候顺便发现的问题。不过不用去纠结这一点,回到本篇开篇的问题,我们只需要将spring-shell的块注释解析功能屏蔽掉就可以了。
阅读了spring-shell 1.2.0.RELEASE的官方文档和查看注释解析的源代码,并没有发现官方为我们预留了什么配置,可以用来关闭注释解析这个特性。我们只能自己动手。将上面代码片段的内容注释掉,重新编译spring-shell。将编译好的jar包覆盖Hudi cli的同名文件后,注释解析问题解决。获取到的globRegex
终于和pathRegex
传入的参数值相同。
第二部分
解决了上面的问题,我们需要分析pathRegex
默认参数和我们例子不匹配的问题。pathRegex
的默认值为*/*/*
。对于一个有三个分区字段的表(比如按照时间的年、月、日分区)。这种表在存储的时候,表数据文件以年/月/日
这种三级目录的结构组织。目录结构正好和*/*/*
正则表达式匹配。但是对于我们的例子呢?只有一个分区字段,明显不是*/*/*
这种目录结构。所以说我们需要手工指定pathRegex
参数。参数中的目录层级需要和分区表的分区字段数匹配。这一点非常重要。
最终解决
本人提交了Hudi社区issue和patch:[HUDI-4485] Hudi cli got empty result for command show fsview all - ASF JIRA (apache.org)。由于spring-shell 1.2.0.RELEASE版本过于老旧,和Spring社区联系后表示这个版本不再维护,建议升级到spring-shell 2.x最新版本。一直使用老版本也不利于以后新功能的开发。经过版本特性调研和试用发现spring-shell 2.1.1版本默认不解析块注释。因此,最终决定通过升级spring-shell版本的方式,解决了这个问题。目前PR已合并,新版本中此问题已解决。
声明
本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。
网友评论