【Hudi数据湖应用】手把手带你应用hudi的hive sync tool与避坑

应用hudi不可避免地要创建对应的hive表以方便查询hudi数据。一般我们使用flink、spark写入数据时,可以配置自动建表、同步元数据。有时也会选择使用hive sync tool工具离线进行操作。

一、Hive sync tool的介绍

Hudi提供Hive sync tool用于同步hudi最新的元数据(包含自动建表、增加字段、同步分区信息)到hive metastore。
Hive sync tool提供三种同步模式,Jdbc,Hms,hivesql。推荐使用jdbc、hms。

1.1 Jdbc模式同步

Jdbc:通过hive2 jdbc协议同步,提供的是hive server2的地址,如jdbc:hive2://hive-server:10000。默认为jdbc。

./run_sync_tool.sh --base-path hdfs:///tmp/hudi-flink/test/ --database test --table test --jdbc-url jdbc:hive2://hiveserver:10000 --user hdfs --pass hdfs --partitioned-by dt

1.2 Hms模式同步

hive meta store同步,提供hive metastore的地址,如thrift://hms:9083,通过hive metastore的接口完成同步。使用时需要设置 --sync-mode=hms --use-jdbc=false。

./run_sync_tool.sh  --base-path hdfs:///tmp/hudi-flink/test --database test --table test  --jdbc-url thrift://metastore:9083  --user hdfs --pass hdfs --partitioned-by dt --sync-mode hms

二、Hive sync tool的配置

HiveSyncConfig DataSourceWriteOption 描述
–database hoodie.datasource.hive_sync.database 同步到hive的目标库名
–table hoodie.datasource.hive_sync.table 同步到hive的目标表名
–user hoodie.datasource.hive_sync.username hive metastore 用户名
–pass hoodie.datasource.hive_sync.password hive metastore 密码
–use-jdbc hoodie.datasource.hive_sync.use_jdbc 使用JDBC连接到hive metastore
–jdbc-url hoodie.datasource.hive_sync.jdbcurl Hive metastore url
–sync-mode hoodie.datasource.hive_sync.mode 同步hive元数据的方式. 有效值为 hms, jdbc 和hiveql.
–partitioned-by hoodie.datasource.hive_sync.partition_fields hive分区字段名,多个字段使用逗号连接.
–partition-value-extractor hoodie.datasource.hive_sync.partition_extractor_class 解析分区值的类名,默认SlashEncodedDayPartitionValueExtractor

三、Hive sync tool的应用与避坑

理想情况下,可以按照官网文档Hive Metastore:syncing_metastore的步骤完成hudi表元数据同步。但现实一般都很骨感的,使用Hudi也不例外。

3.1 运行报NoClassDefFoundError

##hudi 0.11.0项目路径
cd hudi-sync/hudi-hive-sync
./run_sync_tool.sh  --jdbc-url jdbc:hive2://hiveserver:10000 --user hive --pass hive --partitioned-by dt --base-path hdfs:///tmp/hudi/test --database test --table test

第一次运行上面的代码,满怀希望认为可以成功建表。结果还没有起步,就邂逅了NoClassDefFoundError。一路上会遇到很多NoClassDefFoundError,我们遇到的第一个是NoClassDefFoundError: org/apache/log4j/LogManager。

Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/log4j/LogManager
        at org.apache.hudi.hive.HiveSyncTool.(HiveSyncTool.java:65)
Caused by: java.lang.ClassNotFoundException: org.apache.log4j.LogManager
        at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        ... 1 more

3.2 分析与处理NoClassDefFoundError

明显这是缺少了log4j的依赖。常规想法时,增加依赖,重新打包?这个办法行得通,但不够灵活,还有以下缺点:
1、重复编译,费时费力。
2、补充了这个依赖,不一定就完整了。万一还需要其他依赖呢?运行时报了新的NoClassDefFoundError,又重新编译吗?
显然,增加依赖重新编译是一个有效,不是很方便的一个解决方案。这里我们说另外一种解决方案。一般而言,大型项目打包时只会打包核心类,而常用的依赖则引用外部lib里jar包,这样可以减少编译时间和包体积,hive sync tool就是采用这种方式。我们可以看一下run_sync_tool.sh的脚本。脚本里引入HADOOP_HOME、HIVE_HOME,获取相应的依赖后,拼接到运行命令中,以使用外部依赖运行hive sync tool。

function error_exit {
    echo "$1" >&2   ## Send message to stderr. Exclude >&2 if you don't want it that way.
    exit "${2:-1}"  ## Return a code specified by $2 or 1 by default.
}

if [ -z "${HADOOP_HOME}" ]; then
  error_exit "Please make sure the environment variable HADOOP_HOME is setup"
fi

if [ -z "${HIVE_HOME}" ]; then
  error_exit "Please make sure the environment variable HIVE_HOME is setup"
fi

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
#Ensure we pick the right jar even for hive11 builds
HUDI_HIVE_UBER_JAR=`ls -c $DIR/../../packaging/hudi-hive-sync-bundle/target/hudi-hive-sync-*.jar | grep -v source | head -1`

if [ -z "$HADOOP_CONF_DIR" ]; then
  echo "setting hadoop conf dir"
  HADOOP_CONF_DIR="${HADOOP_HOME}/etc/hadoop"
fi

## Include only specific packages from HIVE_HOME/lib to avoid version mismatches
HIVE_EXEC=`ls ${HIVE_HOME}/lib/hive-exec-*.jar | tr '\n' ':'`
HIVE_SERVICE=`ls ${HIVE_HOME}/lib/hive-service-*.jar | grep -v rpc | tr '\n' ':'`
HIVE_METASTORE=`ls ${HIVE_HOME}/lib/hive-metastore-*.jar | tr '\n' ':'`
HIVE_JDBC=`ls ${HIVE_HOME}/lib/hive-jdbc-*.jar | tr '\n' ':'`
if [ -z "${HIVE_JDBC}" ]; then
  HIVE_JDBC=`ls ${HIVE_HOME}/lib/hive-jdbc-*.jar | grep -v handler | tr '\n' ':'`
fi
HIVE_JACKSON=`ls ${HIVE_HOME}/lib/jackson-*.jar | tr '\n' ':'`
HIVE_JARS=$HIVE_METASTORE:$HIVE_SERVICE:$HIVE_EXEC:$HIVE_JDBC:$HIVE_JACKSON

HADOOP_HIVE_JARS=${HIVE_JARS}:${HADOOP_HOME}/share/hadoop/common/*:${HADOOP_HOME}/share/hadoop/mapreduce/*:${HADOOP_HOME}/share/hadoop/hdfs/*:${HADOOP_HOME}/share/hadoop/common/lib/*:${HADOOP_HOME}/share/hadoop/hdfs/lib/*

echo "Running Command : java -cp ${HADOOP_HIVE_JARS}:${HADOOP_CONF_DIR}:$HUDI_HIVE_UBER_JAR org.apache.hudi.hive.HiveSyncTool $@"
java -cp $HUDI_HIVE_UBER_JAR:${HADOOP_HIVE_JARS}:${HADOOP_CONF_DIR} org.apache.hudi.hive.HiveSyncTool "$@"

那么问题来了,运行时拼少了依赖,导致运行时报NoClassDefFoundError。我们在HADOOP或HIVE的依赖中,找一个log4j的依赖拼进去就好了。脚本运行时会打印启动命令,我们把Running Command的内容拷贝一下,加log4j的包。当然,我们也可以直接改脚本,在脚本里拼。在试验的时候,我们选择直接改执行命令。

java -cp /usr/hdp/3.0.0.0-1634/hive/lib/hive-metastore-3.1.0.3.0.0.0-1634.jar::/usr/hdp/3.0.0.0-1634/hive/lib/hive-service-3.1.0.3.0.0.0-1634.jar::/usr/hdp/3.0.0.0-1634/hive/lib/hive-exec-3.1.0.3.0.0.0-1634.jar::/usr/hdp/3.0.0.0-1634/hive/lib/hive-jdbc-3.1.0.3.0.0.0-1634.jar:/usr/hdp/3.0.0.0-1634/hive/lib/hive-jdbc-handler-3.1.0.3.0.0.0-1634.jar:/usr/hdp/3.0.0.0-1634/hive/lib/hive-jdbc-handler.jar::/usr/hdp/3.0.0.0-1634/hive/lib/jackson-annotations-2.9.5.jar:/usr/hdp/3.0.0.0-1634/hive/lib/jackson-core-2.9.5.jar:/usr/hdp/3.0.0.0-1634/hive/lib/jackson-core-asl-1.9.13.jar:/usr/hdp/3.0.0.0-1634/hive/lib/jackson-databind-2.9.5.jar:/usr/hdp/3.0.0.0-1634/hive/lib/jackson-dataformat-smile-2.9.5.jar:/usr/hdp/3.0.0.0-1634/hive/lib/jackson-mapper-asl-1.9.13.jar::/usr/hdp/3.0.0.0-1634/hadoop/share/hadoop/common/*:/usr/hdp/3.0.0.0-1634/hadoop/share/hadoop/mapreduce/*:/usr/hdp/3.0.0.0-1634/hadoop/share/hadoop/hdfs/*:/usr/hdp/3.0.0.0-1634/hadoop/share/hadoop/common/lib/*:/usr/hdp/3.0.0.0-1634/hadoop/share/hadoop/hdfs/lib/*:/usr/hdp/3.0.0.0-1634/hadoop/etc/hadoop:./hudi-hive-sync-bundle-0.10.1.jar:/usr/hdp/3.0.0.0-1634/hadoop/lib/log4j-1.2.17.jar org.apache.hudi.hive.HiveSyncTool --base-path hdfs:///tmp/hudi-flink/test --database test --table test --jdbc-url thrift://hms:9083 --user hdfs --pass hdfs --partition-value-extractor org.apache.hudi.hive.HiveStylePartitionValueExtractor --partitioned-by dt --sync-mode hms

再次运行报NoClassDefFoundError:org.apache.hadoop.conf.Configuration。

Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/hadoop/conf/Configuration
        at org.apache.hudi.hive.HiveSyncTool.main(HiveSyncTool.java:381)
Caused by: java.lang.ClassNotFoundException: org.apache.hadoop.conf.Configuration
        at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        ... 1 more

这个是hadoop-common的包。难道我们再加一次?不断地加上依赖后,必然是可以的,对吧?本人试过这样逐步加,这样至少要加6次以上,加到内心崩溃。这种以最少依赖启动的方法真的很坑爹。
最简单的方法是,一次性把HADOOP/HIVE的运行依赖都加到hive sync tool的启动依赖中。运行时需要什么类就加载什么类。代价是运行时多解析几个包。由于hive sync tool是本地运行,运行不过十几秒,这个代价真的不算什么。
由于将hadoop/hive的依赖一次性入加运行命令中,命令就简化为:

java -cp hudi-hive-sync-bundle-0.10.1.jar:`hadoop classpath`:$HIVE_HOME/lib/*   org.apache.hudi.hive.HiveSyncTool --base-path hdfs:///tmp/hudi-flink/test --database test --table test  --jdbc-url thrift://hms:9083  --user hdfs --pass hdfs --partition-value-extractor org.apache.hudi.hive.HiveStylePartitionValueExtractor --partitioned-by dt --sync-mode hms

这样执行后,就能看到成功的模样:

3.3 修改固化脚本

简单改的话,就是把run_hive_sync.sh里的HADOOP_HIVE_JARS改为以下内容就好了。这样的话,前面的依赖查找其实就不需要了,不喜欢的话可以删掉。

HADOOP_HIVE_JARS=`hadoop classpath`:$HIVE_HOME/lib/*

四、分区信息解析类的配置

Hive sync tool默认使用分区信息解析类是SlashEncodedDayPartitionValueExtractor。它的作用是解析路径yyyy/mm/dd为分区yyyy-mm-dd。其他看源码的话,它还可以解析三层路径,a=yyyy/b=mm/c=dd,这样的格式也是支持的(a/b/c可以任意名称),看源码就知道,splits[0].contains(“=”) ? splits[0].split(“=”)[1] : splits[0,包含等号时支持取后面的值。

class SlashEncodedDayPartitionValueExtractor
  @Override
  public List<String> extractPartitionValuesInPath(String partitionPath) {
    // partition path is expected to be in this format yyyy/mm/dd
    String[] splits = partitionPath.split("/");
    if (splits.length != 3) {
      throw new IllegalArgumentException("Partition path " + partitionPath + " is not in the form yyyy/mm/dd ");
    }
    // Get the partition part and remove the / as well at the end
    int year = Integer.parseInt(splits[0].contains("=") ? splits[0].split("=")[1] : splits[0]);
    int mm = Integer.parseInt(splits[1].contains("=") ? splits[1].split("=")[1] : splits[1]);
    int dd = Integer.parseInt(splits[2].contains("=") ? splits[2].split("=")[1] : splits[2]);
    ZonedDateTime dateTime = ZonedDateTime.of(LocalDateTime.of(year, mm, dd, 0, 0), ZoneId.systemDefault());

    return Collections.singletonList(dateTime.format(getDtfOut()));
  }

当我们分区路径是其他格式时,解析就会报错。所以根据需要选择合适的解析类很重要!

Caused by: java.lang.IllegalArgumentException: Partition path dt=20220507 is not in the form yyyy/mm/dd
        at org.apache.hudi.hive.SlashEncodedDayPartitionValueExtractor.extractPartitionValuesInPath(SlashEncodedDayPartitionValueExtractor.java:55)
        at org.apache.hudi.hive.HoodieHiveClient.getPartitionEvents(HoodieHiveClient.java:180)
        at org.apache.hudi.hive.HiveSyncTool.syncPartitions(HiveSyncTool.java:341)
        ... 4 more

目前,hudi提供了几种分区解析类实现,通过–partition-value-extractor参数指定。功能如下:

类名 解析格式
org.apache.hudi.hive.SlashEncodedDayPartitionValueExtractor 解析路径/yyyy/mm/dd为分区yyyy-mm-dd,也支持YYYY=[yyyy]/MM=[mm]/DD=[dd]
org.apache.hudi.hive.SlashEncodedHourPartitionValueExtractor 解析路径/yyyy/mm/dd/HH为分区yyyy-mm-dd-HH,也支持YYYY=[yyyy]/MM=[mm]/DD=[dd]/HH=[hh]
org.apache.hudi.hive.HiveStylePartitionValueExtractor hive分区风格解析,k=v格式,仅支持单分区
org.apache.hudi.hive.MultiPartKeysValueExtractor hive分区风格解析,k=v格式,支持多分区
org.apache.hudi.hive.NonPartitionedExtractor 无分区解析

这几个类基本能满足日常需求,如果不能的话,自己实现PartitionValueExtractor就可以了。

你可能感兴趣的:(Hudi,Flink,hive,hadoop,big,data,大数据,flink)