☰
Hive on Spark集成实战:选without-hive避免依赖冲突
2026/10/10 9:41:43 网站建设 项目流程

简介:面向需要在无内置Hive JAR包环境下运行Spark的用户,这份Spark 2.3.0 for Hadoop 2.x tgz压缩包提供了完整的Spark发行版,重点解决通过Spark访问Hive元数据、实现Hive on Spark的资源配套问题。包内共有867个文件、约127.77MB,涵盖Java/Scala/Python源码、jar依赖、Shell/CMD启动与提交脚本、配置模板,以及少量Parquet/Avro示例数据,目录结构清晰,便于直接部署或按需裁剪。已有659人浏览学习。使用者可据此检查Spark默认配置,手动设置spark.sql.hive.metastore.uris指向已有Hive metastore,在不依赖Hive执行引擎的前提下用Spark SQL或DataFrame接口查询Hive表,适合希望发挥Spark计算优势、又不想引入整套Hive组件的团队。

1. Hive on Spark 实战:为什么我最终选了 spark-2.3.0-bin-hadoop2-without-hive

不少人在搭 Hive on Spark 时,会直接下载带with-hive后缀的 Spark 包,觉得省事、开箱即用。我在某公司负责离线数仓组件升级时,最开始也这么干,结果一连踩了好几个依赖冲突的坑。后来换成了spark-2.3.0-bin-hadoop2-without-hive这个版本,反而把 Hive on Spark 的链路彻底盘顺了。这份资源解决的核心问题,就是帮你在 Spark 2.3.0 与 Hive 之间建立一个干净、可控的集成环境,同时绕开两套 Hive 依赖互相污染引发的 ClassNotFound、版本不兼容等典型故障。适合正在做 Hive 执行引擎切换或 Spark SQL 任务迁移的开发者,也适合想让数仓组件边界更清晰、故障更好排查的运维同学。

2. 选型思路:without-hive 到底在省什么,Hive on Spark 的架构本质

2.1 without-hive 与 with-hive 的差异:一个 jar 包的"干净"有多重要

Spark 官方发布的压缩包分两类:spark-2.3.0-bin-hadoop2-with-hive和spark-2.3.0-bin-hadoop2-without-hive。后者在编译时只带上了 Spark SQL 依赖的 Hive 接口代码,没有把 Hive 的完整运行时依赖、相关 JAR 和默认配置打包进去。这带来的最大便宜是:你自己搭的 Hive 环境是什么版本,就让 Spark 去适配那个版本,而不是被 Spark 内置的那一套旧 Hive 库绑架。

有人会问:既然 Spark SQL 本身能执行 HiveQL,那 with-hive 版本是不是更完整?我刚接手某跨平台系统时也是这么想的。但实际跑起来就会发现,with-hive包里的 Hive 版本往往和集群里已部署的 Hive 版本不一致。一旦两套 Hive 的 class 都出现在 Spark 的执行路径里,类加载顺序稍有偏差,就会抛NoSuchMethodError或者ClassNotFoundException,而且报错堆栈往往让人一头雾水,玄学得很。

用without-hive版本,相当于你自己决定 Spark 与 Hive 之间的边界:Spark 只负责计算,Hive Metastore 只负责元数据和表定义。两个进程之间的通信主要通过 Hive Metastore 的 Thrift 接口,不再依赖 Spark 进程中内置的完整 Hive 服务。这个边界一旦清了,后续排查问题会节省大量时间。

2.2 Hive on Spark 的执行链路:Metastore、Thrift 与执行引擎的角色分工

我们说的 Hive on Spark,严格来讲是指 Hive 把 SQL 翻译成 Spark 作业,用 Spark 来跑。这与 Spark SQL 直接连接 Hive 表(Spark on Hive)是两个方向,但共享同一套 Metastore 访问机制。

在 Hive on Spark 的经典架构里,Hive 客户端把 HiveQL 解析成执行计划,再通过 Spark 的接口提交任务。Spark 2.3.0 作为执行引擎时,Hive 服务端需要能动态加载 Spark 的 JAR 包,并把 Spark 作业分发到 YARN 集群上。这和 Spark SQL 的"原生"执行是两条不同的路,但最终都要访问同一个 Hive Metastore,用来获取表结构、分区信息以及数据的存储位置。

所以,without-hive版本真正需要你动手做的事情是:把 Spark 的 JAR 包让 Hive 能引用到,同时把 Hive 的hive-site.xml挂到 Spark 的配置目录里,让 Spark 知道往哪个 Metastore 去连。这两件事是 Hive on Spark 集成中最核心的动作。

注意一点,Spark 2.3.0 中,spark.sql.hive.metastore.version参数默认识别的 Hive 版本范围有限,如果集群里装的是 Hive 2.1.0 以上,我一般会手动把这个参数和hive-site.xml里的配置对齐,否则会有元数据连接层面的兼容问题。

3. 环境搭建:从解压到打通 Hive Metastore 的六个关键步骤

3.1 解压与目录规划:把两种环境的配置物理隔离

先用一段常规命令把资源包解压并放到指定目录。这一步没有难度,但目录规划却影响后续维护。我喜欢采用独立的部署目录,而不是直接放在用户目录下。

tar -xf spark-2.3.0-bin-hadoop2-without-hive.tar.gz mkdir -p /data/lib/spark-2.3.0 mv spark-2.3.0-bin-hadoop2-without-hive/* /data/lib/spark-2.3.0/

解压完成后,检查一下jars目录下是否有hive-exec、hive-metastore这类 JAR。without-hive版本里大概率是看不到完整 Hive 系列 JAR 的,这反而是好事。后续你再放 Hive 相关 JAR 时,不会和包里现成的冲突。

接下来,把集群 Hive 安装目录下的hive-site.xml软链到 Spark 的 conf 目录:

ln -s /data/lib/hive/conf/hive-site.xml /data/lib/spark-2.3.0/conf/hive-site.xml

软链是一个很好的做法,以后 Hive 端配置有调整,Spark 这边不需要重新同步。但要注意hive-site.xml里的数据库连接串,必须确保从 Spark 的机器上也能访问到这个 MySQL 库。很多人在测试环境连得上,上线后从一个新节点跑任务突然报连接失败,基本都是因为没检查网络连通性。

3.2 配置 spark-defaults.conf:Metastore 连通参数与资源参数

修改conf/spark-defaults.conf,重点配这三类参数:Metastore 连接、Spark 与 Hive 版本声明、执行资源。

spark.sql.warehouse.dir=hdfs://namenode:8020/user/hive/warehouse spark.sql.hive.metastore.version=1.2.1 spark.sql.hive.metastore.jars=/data/lib/hive/lib/* spark.hadoop.hive.metastore.uris=thrift://metastore-host:9083

这里说明一下几个容易出错的地方。spark.sql.warehouse.dir和hive-site.xml里的hive.metastore.warehouse.dir最好保持同一个 HDFS 路径,否则用 Spark SQL 创建表时,表数据目录可能落在你预期之外的位置。之前某一次升级中,因为这两个目录不一致,所有通过 Spark 新建的表都跑到了/user/spark/warehouse目录下,Hive 里查得到元数据但数据文件读不到,血泪教训。

spark.sql.hive.metastore.jars这个参数会直接影响 Spark 加载哪一套 Hive 类库。without-hive版本本来就缺这些 JAR,所以需要指向 Hive 的 lib 目录。这里填的路径要确保 Spark 进程有读取权限。另一个常见做法是把需要的 Hive JAR 直接拷到 Spark 的jars目录下,但我不太推荐,因为拷过来之后,以后再想切到别的 Hive 小版本,就得重新清理替换,维护成本高。

3.3 Hive 端配置:让 Hive 把 Spark 当作执行后端

在真正跑 Hive on Spark 之前,还需要在 Hive 配置里把执行引擎切换为 Spark。修改 Hive 的hive-site.xml:

<property> <name>hive.execution.engine</name> <value>spark</value> </property> <property> <name>spark.home</name> <value>/data/lib/spark-2.3.0</value> </property> <property> <name>spark.master</name> <value>yarn</value> </property> <property> <name>hive.spark.jars.dir</name> <value>hdfs://namenode:8020/user/spark/jars</value> </property>

spark.home指向刚才解压的 Spark 目录。hive.spark.jars.dir是 Hive 会把 Spark 相关的 JAR 上传到 HDFS 的位置,用于在 YARN 上为 Spark 任务提供资源。这个目录要求 HDFS 上提前创建好,并且用户要有写入权限,否则任务提交时会报权限错误。

这里还要多说一句,要确认hive-exec的版本和 Spark 2.3.0 能兼容。我遇到过一次 Hive 1.2.1 配 Spark 2.3.0 时,在解析 Spark 任务分发逻辑时总报NullPointerException,后来查了一圈,发现是 Hive 里的 Spark 适配模块版本过旧,更换到 Hive 2.3.x 后才稳定下来。如果你的 Hive 还是老的 1.x,建议提前做一次小版本升级评估。

4. 代码实战:用 Spark Session 连接 Hive 表并跑通 on Spark 任务

4.1 建一个干净的 SparkSession,显式挂载 Hive 支持

用代码接入 Hive on Spark 环境前,要确认 Spark 程序是带着 Hive 支持编译的。without-hive这个包里 Spark SQL 的HiveSession相关类是在的,只是运行时依赖外部 JAR,所以代码里不需要特殊处理,正常构建SparkSession即可。

下面是一段标准的初始化代码,可以用在数据同步或报表任务里。

from pyspark.sql import SparkSession spark = SparkSession \ .builder \ .appName("HiveOnSparkDemo") \ .config("spark.sql.warehouse.dir", "hdfs://namenode:8020/user/hive/warehouse") \ .config("hive.metastore.uris", "thrift://metastore-host:9083") \ .enableHiveSupport() \ .getOrCreate()

这样创建的SparkSession内部携带了 Hive 的 Metastore 客户端,可以直接执行spark.sql("show tables")查看 Hive 里的表。代码中enableHiveSupport()是必须的一步,不加这个,Spark SQL 里的表读写默认走的是内嵌的 Derby 元数据库,根本看不到 Hive 里的表,这可是新手最容易踩的一个点。

4.2 从 Hive 表读取数据,再写回分区表

实际业务里,最常见的是从一张明细大表读取数据,加工后再写回另一张目标表。下面这段代码展示了完整链路,并包含了分区写入的常规操作。

# 读取 Hive 中的源表,只取最近一天的数据 source_df = spark.sql(""" SELECT user_id, item_id, action_time, action_type FROM ods.user_behavior_log WHERE dt = '2024-01-15' """) # 简单过滤:只保留点击与下单两类行为 filtered_df = source_df.filter(source_df.action_type.isin("click", "order")) # 直接写入 Hive 目标表,使用动态分区 filtered_df.write \ .mode("overwrite") \ .format("hive") \ .partitionBy("dt") \ .saveAsTable("dws.user_behavior_daily")

这里有几个参数值得展开。.mode("overwrite")会先删掉目标表分区下的数据再写入,但注意如果不指定分区列,overwrite 操作会覆盖整张表的数据,这个很危险。partitionBy("dt")配合动态分区使用,意味着写入时自动按dt字段的值建分区,前提是目标表已经定义过dt分区列。

在 Spark 2.3.0 中,写入 Hive 表默认走的是 Hive 的HiveOutputFormat,它会在执行阶段产生较多小文件。如果上游数据量偏大,建议开启分区内文件合并机制,在spark-defaults.conf里补上:

spark.sql.shuffle.partitions=200 spark.sql.hive.convertMetastoreParquet=true

spark.sql.hive.convertMetastoreParquet这个参数打开后,Spark 会把 Hive 的 Parquet 表转成 Spark 原生数据源来读,而不是走 Hive 的 SerDe 逻辑,读取性能和谓词下推能力都会明显更好。但如果你 Hive 表里的数据是其他非 Parquet 格式,这个参数就别打开了,转换过程反而增加额外开销。

4.3 从 RDD 到 DataFrame:没有 Hive 表时的临时视图方案

有时候数据源不在 Hive 里,而在消息队列或日志文件里,需要通过 RDD 兜底转成 DataFrame。without-hive版本下,这个过程和普通 Spark 程序完全一样,关键是最后怎么把数据挂到 Hive 表上。

from pyspark.sql import Row raw_rdd = sc.textFile("hdfs://namenode:8020/data/logs/2024-01-15/*.log") parsed_rdd = raw_rdd.map(lambda line: line.split(",")) \ .map(lambda arr: Row(user_id=arr[0], item_id=arr[1], action_time=arr[2], action_type=arr[3])) log_df = spark.createDataFrame(parsed_rdd) log_df.createOrReplaceTempView("tmp_log_view") # 在 Spark SQL 中使用 Hive 表与临时视图做关联 result_df = spark.sql(""" SELECT l.user_id, l.item_id, u.user_level, l.action_type FROM tmp_log_view l JOIN dwd.user_dim u ON l.user_id = u.user_id """)

createOrReplaceTempView创建的只是 Spark Session 内的临时表,不会注册进 Hive Metastore,任务结束或者 Session 关闭后自动消失。用它关联正式 Hive 表完全没问题,跑批任务时比较灵活。但要注意,如果多个 Spark 任务都要共享这份数据,直接把结果写回 Hive 表会更好。

这种场景还有一个细节:日志文件里如果存在脏字符,split(",")返回的数组长度可能不够,取值时容易ArrayIndexOutOfBoundsException。我一般会在 map 函数里加点防御逻辑,比如先判断长度再构造 Row,不然跑大数据量时某个坏行就够你把整个任务重跑一遍。

5. 避坑指南:Hive on Spark 集成中最常见的五个翻车现场

5.1 启动 Session 就报 ClassNotFoundException:Hive 的类没进 Spark 的类加载路径

现象:执行spark.sql("show tables")时,抛java.lang.ClassNotFoundException: org.apache.hadoop.hive.conf.HiveConf。

原因:without-hive版本的 Spark 运行环境中没有 Hive 的依赖 JAR。要么spark.sql.hive.metastore.jars没有正确配置,要么配置指向的目录不对,Spark 进程拿不到 Hive 的类。

解决:检查spark-defaults.conf里spark.sql.hive.metastore.jars的路径是否真实存在,且指向 Hive 安装目录的lib文件夹。最好直接到 Spark 进程中确认这个路径有读取权限。我一般是先手动跑一段代码,打印System.getProperty("java.class.path"),看看有没有 Hive JAR,有就是路径问题,没有就是参数没生效。

5.2 Hive 能查到表但 Spark 查不到:Metastore 连接配置不一致

现象:Hive CLI 里show tables正常,但 Spark SQL 里执行同样语句返回空。

原因:Spark 进程连到了另一个 Metastore 上,或者连到了内嵌的 Derby 元数据库上。大多数情况是hive.metastore.uris没有配置,Spark 默认使用本地metastore_db目录。

解决:确认hive-site.xml中确实配了hive.metastore.uris,并且这个 Thrift 地址从 Spark 的部署节点上能 telnet 通。常见做法是先手动执行hive --service metastore确认端口,再把地址写进配置。还有一个隐蔽地方:hive-site.xml被 Spark 读取时,是以 resource 文件方式加载的,放在 conf 目录下的文件如果权限为 600,也会导致 Spark 读不到。

5.3 写入 Hive 表报 Execution Error:动态分区数超出限制

现象:写入任务跑到接近尾声时报Too many dynamic partitions,任务失败。

原因:Hive 默认限制动态分区的数量,默认值是 1000 或更少,取决于 Hive 版本。数据日志表往往按天或按小时分区,某些调度任务会一次性写入几百上千个分区,容易超限。

解决:在 Hive 的配置里调大动态分区上限:

<property> <name>hive.exec.max.dynamic.partitions</name> <value>5000</value> </property>

注意调大之后,并不是就没有坏处了。分区数越多,一次写入的 HDFS 文件操作就越频繁,NameNode 的压力和 Spark 的 shuffle 负载都会变大。如果你发现自己经常要突破这个限制,建议先复核分区粒度是否合理,比如是不是把数据量很小的维度也设成了分区列。

5.4 任务反复提交到 YARN 但立即失败:JAR 上传目录不存在或没权限

现象:Hive on Spark 作业在 Hive 端能看到执行日志,但 YARN 上 Spark ApplicationMaster 启动即退,查看日志发现是 Spark JAR 包路径异常。

原因:hive.spark.jars.dir指定的 HDFS 目录没有提前创建,或者当前执行用户的写权限不足,Hive 无法把 Spark 的 JAR 包上传到集群。

解决:手动在 HDFS 上创建目录并授权,建议由集群管理员统一处理:

hdfs dfs -mkdir -p /user/spark/jars hdfs dfs -chown -R spark:hadoop /user/spark/jars hdfs dfs -chmod -R 750 /user/spark/jars

这类问题最阴的地方在于,测试环境可能用的是 root 执行,怎么跑怎么过,一到正式环境换普通用户,权限就崩了。所以上线前最好用正式账号把这条链路完整过一遍。

5.5 查询性能比 Hive MR 还慢:没开谓词下推和文件转换

现象:换到 Spark 引擎后,某些查询反而比原 MR 任务更慢,尤其大表过滤场景。

原因:Hive 表里数据文件是 Parquet 格式,但 Spark 默认没开启spark.sql.hive.convertMetastoreParquet,导致读取时还是走 Hive 的 SerDe,无法利用 Parquet 的列剪枝和谓词下推能力。

解决:在spark-defaults.conf里把这个参数置为 true。如果表是 ORC 格式,同样也存在spark.sql.hive.convertMetastoreOrc参数。开启后对比一下执行计划,能明显看到PushedFilters和PrunedAttributes里的内容变多。

还有一个容易忽视的地方是表统计信息。Spark 2.3.0 的 CBO 还比较初级,如果hive-site.xml里关掉了自动统计,或者表从未执行过ANALYZE TABLE,Spark 做 Join 时可能选错 Broadcast 策略。建议定期对新写入的大表执行ANALYZE TABLE table_name COMPUTE STATISTICS。

6. 进阶技巧:用静态分区与动态分区混写,把 Hive on Spark 的写入稳定性再提升一个台阶

定期跑批时,如果任务每天都写当天分区,推荐用"静态分区指定 + 动态分区兜底"的混合写法。静态分区可以避免 Spark 在执行阶段去枚举所有分区,减少 Driver 的元数据调用;动态分区则保留了对未来新分区的适应性。比如下方 SQL,将日期作为静态分区,小时作为动态分区,这样当天数据保证了全量覆盖,小时分区无需手动指定。

INSERT OVERWRITE TABLE dws.app_daily_hour_agg PARTITION (dt='2024-01-15', hour) SELECT user_id, item_id, hour, COUNT(*) AS cnt FROM ods.user_behavior_log WHERE dt='2024-01-15' GROUP BY user_id, item_id, hour;

任务结束后建议再看一眼 Spark UI 的 Shuffle 记录:如果最后几个 Stage 的Shuffle Write Size远大于输入数据量,多半是聚合算子使用不当或分区键写错。比如上面 SQL 的 GROUP BY 中,把hour放进了分区列,那么聚合结果天然按小时划分,Shuffle 量只取决于分区栏目数基数,不要误用user_id做分区列,效率会完全不同。

另外从 Spark 2.3.0 起,INSERT OVERWRITE会先删除对应静态分区目录,再写新文件,因此做任务重跑时逻辑上比较安全,不会残留脏数据。但对动态分区,overwrite 只清理本次产生匹配的分区。如果同一天内任务跑了两遍且上游数据有变化,前一版本的旧分区文件还在,读表时可能会有重复数据。所以关键任务我习惯先ALTER TABLE DROP PARTITION,再执行写入,避免"看似覆盖实际残留"的隐患。

说回资源本身,spark-2.3.0-bin-hadoop2-without-hive在 Hive on Spark 场景下的定位就是一块干净的基座。配好了,它就是高效执行引擎;配坏了,报错几乎都发生在依赖和配置边界上。从那以后,我每次部署 Spark 与 Hive 的集成环境,都会强制走一遍hive-site.xml -> metastore JAR -> 动态分区参数 -> 写权限确认这条链路,步骤不多,但每步都能省下后面排查的半天时间。希望这些踩坑记录对你有帮助。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询