场景描述
hive 数据表的导入导出功能部分代码如下所示,使用 assemble 将 Java 程序和 spark 相关依赖一起打成 jar 包,最后 spark-submit 提交 jar 到集群执行。
public class SparkHiveApplication {public static void main(String[] args){long start = System.currentTimeMillis();String writeSql = "";SparkConf sparkConf = new SparkConf();for (String arg : args) {if (arg.startsWith("WriteSql=")) {writeSql = arg.replaceFirst("WriteSql=", "");}}SparkSession spark = SparkSession.builder().appName("write data to hive table").config(sparkConf).enableHiveSupport().getOrCreate();// LOAD DATA LOCAL INPATH '/path/to/file.csv' INTO TABLE target_table PARTITION (field='x')spark.sql(writeSql);long end = System.currentTimeMillis();System.out.println("cost time:" + (end - start));}
}
<dependency><groupId>org.apache.spark</groupId><artifactId>spark-hive_2.11</artifactId><version>2.4.8</version></dependency>
在CDH6.3.2 集群(后面称CDH),当程序执行 spark.sql 导入本地磁盘 csv 数据到 hive 表时出现异常(如下),但导出表数据到本地磁盘、从 HDFS 导入导出功能却都是正常的。
Caused by: java.lang.IllegalArgumentException: Wrong FS: file:/input/data/training/csv_test1_1301125633652294217_1690451941587.csv, expected: hdfs://nameservice1at org.apache.hadoop.fs.FileSystem.checkPath(FileSystem.java:649)
查资料判定是 spark-hive_2.11 版本不兼容导致的,在调试的过程中陆续又出现异常(如下)
Exception in thread "main" org.apache.spark.sql.AnalysisException: org.apache.hadoop.hive.ql.metadata.HiveException: Unable to fetch table csv_test2. Invalid method name: 'get_table_req';
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/hadoop/hive/ql/metadata/HiveExceptionat java.lang.Class.getDeclaredConstructors0(Native Method)
最终使用 spark-hive_2.1.1: 2.4.0-cdh6.3.3 解决了最初的本地磁盘导入异常。
接着用包含 spark-hive_2.1.1: 2.4.0-cdh6.3.3 依赖的 jar 包在 CDP 集群(另一个大数据集群)执行导入导出时又抛了异常,修改依赖版本为 spark-hive_2.11: 2.4.8 ,异常解决。
java.lang.NoSuchMethodException: org.apache.hadoop.hive.ql.metadata.Hive.alterTable(java.lang.String, org.apache.hadoop.hive.ql.metadata.Table, org.apache.hadoop.hive.metastore.api.EnvironmentContext)
此时两个集群中参与导入导出的部分组件版本如下:
集群 | spark | hive | Java 中的 spark-hive_2.1 |
---|---|---|---|
CDH | 3.0.x | 2.1.1 | 2.4.0-cdh6.3.3 |
CDP | 3.0.x | 3.1.3 | 2.4.8 |
备注:导入导出操作采用 spark on k8s 方式执行,所以使用是镜像中的 spark 3.0 而非 CDH 、CDP 集群上安装的 spark。
异常原因分析
spark.sql 执行时要做三件事情:
- spark 首先创建 hiveMetaStoreClient 对象;
- 再调用 hiveMetaStoreClient 的方法去跟 CDH(CDP) 中的 hiveMetastoreServer 通信获取表相关元信息。
- 根据获取到的信息生成 sql 的执行计划,真正处理数据。
生成对象 jvm 首先需要通过全限定类名找到对应 Class 文件,通过反射的方式构造出对象再执行对象方法。问题也在这个地方:包名+类名相同,不同版本可能方法名、方法参数、方法内容不同,对应的出现 Invalid method name: 'get_table_req'
、 java.lang.NoSuchMethodException
以及方法执行时抛出异常。
场景描述中更换依赖版本实际上是在找适配的 hiveMetastore 版本,并且让 jvm 率先加载到。2.4.0-cdh6.3.3 内部包含 hive-metastore:2.1.1-cdh6.3.3,2.4.8内部包含 hive-metastore:1.2.1spark2。
另一种解决方式
spark1.4.0 以后的版本支持和不同版本的 Hive Metastore 交互。列表贴的是 spark 3.4.1 兼容的 hive meatstore 版本 0.12.0 到 2.3.9 和 3.0.0 到 3.1.3。不同版本兼容可在官方文档查看。
怎么配置和不同版本 hive metastore 交互?
(1)内置。spark 内置了 hive,如果应用程序 jar 包中也没有带,也没有外部指定时,默认使用内置的。不同版本 spark 内置的 hive 版本也有差异,spark3.4.1 内置 hive2.3.9,spark3.0.3 内置 hive2.3.7。在 spark-shell 中使用 spark.sql 时应该用的是内置的,因为那会没有 Java jar 包,启动也仅仅是在命令行敲了“spark-shell”。
(2)当场下载。配置spark.sql.hive.metastore.version=2.1.1
和 spark.sql.hive.metastore.jars=maven
,当执行spark.sql 时会先从 maven 仓库下载 2.1.1 相关的依赖到本地 /root/.livy/jars 路径下,大概 188 个 jar 包,总大小 200M 左右。但这种方式当网速很慢或者 maven 仓库没有某些依赖时会下载失败,而且当场下载也不适合生产环境。
(3)指定版本以及依赖的路径。
- spark 3.1.0 之前配置
spark.sql.hive.metastore.version=2.1.1
且spark.sql.hive.metastore.jars=/path-to-hive-jars/*
。执行 spark.sql 时就会率先从 path-to-hive-jars 路径下寻找依赖。 - spark 3.1.0 之后需要配置
spark.sql.hive.metastore.version=2.1.1
、spark.sql.hive.metastore.jars=path
、spark.sql.hive.metastore.jars.path=path-to-hive-jars
。“path-to-hive-jars” 可以是 HDFS 上的路径,具体细节看表格介绍。
这种方式可以用在生产环境中。
如果采用方式(3)怎么提前获取到正确的依赖,既能跟 spark 兼容又能和集群 hive 通信没问题?
要操作哪个集群如果该集群 hive 在 spark 版本兼容的范围内。直接将集群 hive/lib 下的全部 jar 包(200M左右)“怼” 给 spark 就可以了。(可能用不了那么多,但筛选需要做实验测试)。
下面是在 CDH 集群执行导入操作时的 spark-submit 命令。提前将 CDH 的 hive/lib 下的 jar 包拿出来挂载到容器的 /opt/ml/input/data/training/sparkjar/hive-jars 路径下。
# 在 k8s 容器中执行
/usr/local/spark/bin/spark-submit \
--conf spark.driver.bindAddress=172.16.0.44 \
--deploy-mode client \
--conf spark.sql.hive.metastore.jars=/data/training/sparkjar/hive-jars/* \
--conf spark.sql.hive.metastore.version=2.1.1 \
--properties-file /opt/spark/conf/spark.properties \
--class com.spark.SparkHiveApplication \
local:///data/training/sparkjar/hive-metastore-spark-app-jar-with-dependencies.jar \
WriteSql=TE9BRCBEQVRBIExPQ0FMIElOUEFUSCAnL29wdC9tbC9vdXRwdXQvMTc1NjQ2NDY2MDY3Mzk4NjU3LzE3NTY0NjQ2NjA2NzM5ODY1Ny9wYXJ0LTAwMDAwLWVhYjA2ZWZiLTcwNTktNGI4MS04YmRhLWE3NGE5Yzg3OTY2MS1jMDAwLmNzdicgSU5UTyBUQUJMRSBkdF90aW9uZV90ZXN0XzIwMjIwNzIyIHBhcnRpdGlvbiAocGFydF9udW09JzEnKQ==
与工程结合时肯定能获取到全部 jar 包以及找到合适的“怼”方式。这里列举的只是一种向 spark 任务添加依赖的方式。
尝试打“瘦”包
在创建 assembly jar 的时候,将 spark-hive_2.1 的生命周期设置为 provided,即不将该依赖打入最后的 jar 包。因为在运行 jar 任务时集群管理器可以自己提供依赖的 jar。而且 spark-hive 在 maven 官网的生命周期就被给定是 provided。
没有 spark-hive 依赖的 jar 包大小 9M (之前是 144M),分别在 CDP 和 CDH 上执行导入导出操作。结果:
-
CDP 集群测试通过。
-
CDH 集群异常。猜测是原生 spark3 和 hive-metastore:2.1.1-cdh6.3.3 不兼容(发行版有时会在原生基础上做改动),改用方式(3)中的配置后导入导出功能正常。
如果集群采用发行版部署,大版本下各组件兼容的可能性更大些。而且当频繁调试 Java jar 功能时 9M 大小缩短了上传时间,效率也变高了。
小结
通过配置的方式可以指定 spark 使用的 hiveMetastore。优先使用集群自带的依赖可以在一定程度上减少组件不兼容异常。Java jar 包中只管应用程序怎么写,依赖让集群提供,可以解除 jar 包与某个大数据集群的强绑定关系。但外部配置只是一种解决方案,如果要与工程结合还需要根据场景需求进一步设计实现方案并做实验。