1. 为什么还要把 Spark SQL 和 Hive 绑在一起用
大数据圈子里有个挺有意思的现象:很多人刚接触 Spark 的时候,觉得这玩意儿比 Hive 快那么多,迟早要把 Hive 干掉。结果干了几年发现,生产环境里 Spark SQL 和 Hive 不仅没互相替代,反而越绑越紧。我待过的几个数据团队,几乎都是 Spark SQL 负责算,Hive Metastore 负责管元数据,HDFS 或者对象存储负责存数据,三者配合得相当默契。
这套组合解决的核心问题其实很朴素:让同一份数据既能被 Hive 的 SQL 引擎访问,又能被 Spark 的高速引擎访问,而且元数据统一、不用来回同步。你想想,如果数仓团队用 Hive 建了几万张表,突然要全部迁到 Spark 体系,光是元数据迁移就能把人逼疯。反过来,如果 Spark 作业算完的数据,Hive 那边看不到,那下游做报表的同事又得找你麻烦。所以 Spark SQL 与 Hive 集成这件事,本质上是在解决元数据共享和计算引擎互通这两个刚需。
这篇文章适合谁看?如果你正在搭建数据平台,或者手头有一堆 Hive 表想让 Spark 跑得更快,又或者你被“Spark 读不到 Hive 表”这类问题折磨过,那接下来的内容应该能帮到你。我会从整体设计思路讲到具体配置,再到实际踩过的坑,尽量把每个环节的“为什么”说清楚。毕竟光知道怎么配不够,出了问题能自己排查才是真本事。
2. 集成方案的整体设计与选型考量
2.1 两种集成模式:Metastore 直连与 JDBC 桥接
Spark SQL 访问 Hive 数据,主流有两种路子。第一种是直连 Hive Metastore,Spark 通过配置拿到 Metastore 的地址,直接读取元数据信息,然后自己去 HDFS 上读数据文件。第二种是通过 JDBC 连 HiveServer2,相当于 Spark 把 SQL 发给 HiveServer2,让 Hive 去执行,Spark 只拿结果。
这两种模式差别很大。直连 Metastore 的方式,计算完全由 Spark 负责,性能上限高,适合做 ETL 和复杂分析。JDBC 方式则更像是把 HiveServer2 当成一个数据库来查,适合做点轻量级的查询或者跟已有 Hive 权限体系对接。实际生产里,绝大多数场景用的是直连 Metastore,因为大家要的就是 Spark 的计算能力,而不是让 Hive 来算。
我一般会建议团队优先考虑直连模式,理由有三:一是性能可控,Spark 的 Catalyst 优化器能充分发挥作用;二是元数据实时同步,Hive 那边建了表,Spark 这边立刻能查到;三是部署简单,不需要额外维护 HiveServer2 的高可用。当然,如果你的安全策略要求所有访问必须经过 HiveServer2 的权限校验,那 JDBC 模式也有它的价值。
2.2 元数据统一:Hive Metastore 作为唯一真相源
集成里最关键的一个设计决策,就是把 Hive Metastore 当作元数据的唯一真相源。什么意思呢?就是不管你是用 Hive 还是用 Spark 建表,元数据都写到同一个 Metastore 里。这样两边看到的是同一套表结构、同一套分区信息,不会出现“Hive 里有这张表,Spark 里查不到”的尴尬。
要做到这一点,Spark 需要配置spark.sql.catalogImplementation为hive,并且把hive-site.xml放到 Spark 的配置目录下。这样 Spark 启动时会去读 Hive 的配置,连上同一个 Metastore。这里有个细节:Metastore 的版本要和 Spark 内置的 Hive 版本兼容。Spark 每个大版本都会绑定一个特定的 Hive 版本,比如 Spark 3.x 默认绑 Hive 2.3.9,如果你生产环境的 Hive 是 3.1.2,那就需要重新编译 Spark 或者用spark.sql.hive.metastore.version和spark.sql.hive.metastore.jars来指定。
我见过不少团队在这块翻车,要么是版本不匹配导致连不上,要么是连上了但某些字段读出来是乱码。所以选型阶段一定要把版本对齐这件事确认清楚,别等到上线了才发现。
2.3 存储层解耦:数据放在哪、怎么放
元数据统一之后,数据文件放哪也是个要考虑的问题。传统 Hive 表的数据放在 HDFS 上,Spark 读的时候直接去 HDFS 拉。现在很多团队用对象存储,比如 S3、OSS 之类的,Spark 也能读,但路径格式和权限配置跟 HDFS 不太一样。
这里有个经验:建表时尽量用外部表(External Table)。内部表在 drop 的时候会把数据一起删掉,万一哪天手抖删错了表,数据就没了。外部表只删元数据,数据文件还在,恢复起来容易得多。而且外部表在 Spark 和 Hive 之间切换时更灵活,不会因为一边的操作影响另一边。
另外,分区字段的选择也很关键。Spark SQL 对分区裁剪的优化做得很好,但如果分区字段设计得不合理,比如用高基数的字段做分区,会导致分区数量爆炸,Metastore 压力大,查询也慢。一般建议按时间维度分区,比如天、小时,再配合一些业务维度做二级分区。
3. 核心配置细节与实操要点拆解
3.1 把 hive-site.xml 放到正确的位置
这是最基础也最容易出错的一步。Spark 要连 Hive Metastore,必须能读到hive-site.xml。这个文件通常放在$SPARK_HOME/conf/目录下。如果你用的是 CDH 或者 HDP 这类发行版,配置文件可能在/etc/hive/conf/下面,需要拷贝或者做软链接。
拷贝的时候注意权限问题。Spark 作业是以提交用户的身份运行的,如果hive-site.xml的权限设置得太严格,普通用户读不到,就会报Permission denied。我一般会把它设成644,确保所有需要跑 Spark 的用户都能读。
还有一个坑:如果集群里同时存在多个 Hive 版本的配置文件,要确保 Spark 读到的是正确的那份。曾经有个环境,/etc/hive/conf/下有一份旧版本的配置,$SPARK_HOME/conf/下有一份新的,结果 Spark 优先读了旧的,连到了错误的 Metastore 地址,排查了半天才发现。
3.2 关键参数逐项说明与推荐值
下面这张表是我在实际项目中总结的常用配置,放在spark-defaults.conf或者提交作业时通过--conf指定都可以。
| 参数名 | 推荐值 | 作用说明 |
|---|---|---|
spark.sql.catalogImplementation | hive | 启用 Hive Catalog,让 Spark 能访问 Hive 元数据 |
spark.sql.hive.metastore.version | 与生产 Hive 版本一致 | 指定 Metastore 版本,避免兼容性问题 |
spark.sql.hive.metastore.jars | maven或自定义路径 | 指定 Metastore 客户端 jar 的来源 |
spark.sql.warehouse.dir | 与 Hive 的 warehouse 目录一致 | 确保建表时数据落到正确位置 |
spark.sql.hive.convertMetastoreParquet | true | 让 Spark 用自己的 Parquet 读取器,性能更好 |
spark.sql.hive.convertMetastoreOrc | true | 同上,针对 ORC 格式 |
spark.sql.hive.metastore.jars这个参数值得多说一句。默认值是builtin,意思是使用 Spark 内置的 Hive jar 包。但如果你的 Hive 版本和 Spark 内置的不一致,就得改成maven,让 Spark 自己去下载对应版本的 jar,或者指定一个包含所有依赖的路径。用maven方式需要机器能访问 Maven 仓库,内网环境可能得配代理或者提前把 jar 下好。
3.3 版本兼容性:别让小版本号坑了你
Spark 和 Hive 的版本兼容性是个老生常谈的问题。大版本对不上肯定不行,但有时候小版本差异也会出问题。比如 Spark 3.0 内置的是 Hive 2.3.7,而你生产环境是 Hive 2.3.9,大部分情况下没问题,但某些特定的 Metastore API 可能有细微差别。
我的建议是:尽量让 Spark 内置的 Hive 版本和生产 Hive 版本保持一致。如果做不到,至少保证大版本相同,小版本差异不要太大。实在不行,就用spark.sql.hive.metastore.jars指定生产环境的 Hive jar 包,让 Spark 用生产版本的客户端去连。
还有一点,Hive Metastore 的服务端版本和客户端版本也要兼容。Metastore 服务端通常是向后兼容的,但太老的客户端连太新的服务端可能会缺一些功能。这个在升级 Hive 的时候要特别注意,别只升级了服务端,客户端还是老版本。
4. 完整实操流程:从零搭建一套可用的集成环境
4.1 环境准备与前置检查
假设你手头有一个 Hadoop 集群,上面已经跑着 Hive,现在要加一个 Spark 客户端。第一步不是急着配 Spark,而是先确认 Hive 这边的状态。
先连上 Hive 的命令行,执行show databases;看看能不能正常列出数据库。然后找一张有分区的表,执行show partitions 表名;确认分区信息能读到。这一步是为了排除 Hive 本身的问题,免得后面 Spark 连不上时搞不清楚是谁的锅。
接着确认 HDFS 上 warehouse 目录的位置。在 Hive 里执行set hive.metastore.warehouse.dir;就能看到。记下这个路径,后面配 Spark 的时候要用到。同时确认当前用户对这个目录有读写权限,不然 Spark 建表时会失败。
最后确认 Metastore 的服务地址和端口。通常在hive-site.xml里的hive.metastore.uris配置项,格式类似thrift://metastore-host:9083。如果这个值是空的,说明 Hive 用的是内嵌模式,那 Spark 就连不上了,需要先把 Hive 配成远程 Metastore 模式。
4.2 Spark 端配置落地
环境确认没问题后,开始配 Spark。把 Hive 的hive-site.xml拷贝到$SPARK_HOME/conf/下,然后编辑spark-defaults.conf,加上前面表格里提到的那些参数。
一个典型的配置长这样:
spark.sql.catalogImplementation=hive spark.sql.hive.metastore.version=2.3.9 spark.sql.hive.metastore.jars=maven spark.sql.warehouse.dir=hdfs://namenode:8020/user/hive/warehouse spark.sql.hive.convertMetastoreParquet=true spark.sql.hive.convertMetastoreOrc=true配完之后,启动spark-sql或者pyspark,执行show databases;。如果能看到 Hive 里的数据库列表,说明元数据连接成功了。再执行show tables;看看能不能列出表。如果这两步都正常,基本集成就算通了。
这里有个小技巧:启动 Spark 时加上--verbose参数,可以看到它加载了哪些配置、连了哪个 Metastore。排查问题时特别有用。
4.3 跨引擎读写验证
配置通了不代表万事大吉,还得验证两边读写是否一致。我一般会做三个测试:
第一个测试,在 Hive 里建一张表,插入几条数据,然后到 Spark 里查。如果能查到,说明 Hive 写、Spark 读没问题。
第二个测试反过来,在 Spark 里建表插数据,然后到 Hive 里查。这一步能验证 Spark 写的元数据 Hive 能不能识别。
第三个测试针对分区表。在 Spark 里往分区表插入数据,然后到 Hive 里执行show partitions,看分区信息是否同步。再在 Hive 里加一个分区,到 Spark 里查能不能读到。
这三个测试都过了,基本可以放心用了。如果某个环节出问题,根据现象去排查对应的配置。
4.4 性能相关配置调优
集成通了之后,接下来要考虑性能。Spark SQL 读 Hive 表时,有几个参数对性能影响很大。
spark.sql.hive.convertMetastoreParquet和spark.sql.hive.convertMetastoreOrc这两个建议都设为true。开启后,Spark 会用自己的 Parquet/ORC 读取器,而不是走 Hive 的 SerDe。Spark 自己的读取器在向量化读取、谓词下推方面做得更好,实测能快不少。
spark.sql.orc.filterPushdown和spark.sql.parquet.filterPushdown也建议开启,这样过滤条件能下推到文件读取层,减少读入的数据量。
还有一个容易忽略的点:小文件问题。Hive 表如果有很多小文件,Spark 读取时会启动大量 task,调度开销大。可以在 Spark 里用spark.sql.files.maxPartitionBytes控制每个分区的最大字节数,或者定期做小文件合并。
5. 常见问题与排查技巧实录
5.1 连不上 Metastore 的几种典型情况
报错信息通常是Unable to instantiate org.apache.hadoop.hive.ql.metadata.SessionHiveMetaStoreClient或者Could not connect to meta store。遇到这种,按下面的顺序排查:
先看网络通不通。在 Spark 所在机器上telnet metastore-host 9083,如果连不上,那就是网络或者防火墙的问题。再看 Metastore 服务是不是活着,去 Metastore 所在节点jps看一下有没有HiveMetaStore进程。然后检查hive-site.xml里的hive.metastore.uris配置对不对,有没有写错端口。最后确认版本兼容性,如果 Metastore 是 3.x 而 Spark 内置的是 2.x,可能需要指定spark.sql.hive.metastore.jars。
还有一种情况是 Kerberos 认证问题。如果集群开了 Kerberos,Spark 需要能拿到有效的 ticket。报错通常是GSS initiate failed之类的。这种需要确保提交作业前kinit过,或者配置好 keytab。
5.2 表能查到但读不到数据的排查思路
有时候show tables能看到表,但select的时候报FileNotFoundException或者返回空结果。这种一般是元数据和实际数据文件对不上。
先确认表的 location 指向哪里。在 Spark 里执行desc formatted 表名,看 Location 字段。然后去 HDFS 上ls这个路径,看文件在不在。如果路径不对,可能是建表时 warehouse 目录配错了。如果路径对但文件不存在,可能是数据被删了或者还没写入。
还有一种情况是分区信息不对。比如 Hive 里加了分区,但 Spark 的 Metastore 缓存还没刷新。可以执行msck repair table 表名来修复分区元数据。在 Spark 里也可以用refresh table 表名来刷新缓存。
5.3 数据类型映射的坑
Hive 和 Spark 的数据类型不是完全一一对应的。大部分类型没问题,但有些边界情况要注意。
比如 Hive 的decimal类型,如果精度定义得比较高,Spark 读的时候可能会有精度损失。timestamp类型在不同版本间的行为也有差异,Hive 0.13 之前的 timestamp 和之后的语义不一样。还有char和varchar类型,Spark 在某些版本里会当成string处理。
我遇到过一次,Hive 表里有个decimal(38,18)的字段,Spark 读出来变成了decimal(38,18)但计算结果精度不对。后来发现是 Spark 的 decimal 实现和 Hive 有细微差别,需要显式转换。这种问题比较隐蔽,建议在集成测试阶段就把各种数据类型都覆盖到。
5.4 权限与安全相关的常见报错
如果集群开了 Ranger 或者 Sentry 这类权限管理组件,Spark 访问 Hive 表时也需要相应的权限。报错通常是Permission denied或者Access denied。
排查时先确认当前用户在 Hive 里有没有这张表的 select 权限。如果有,但 Spark 还是报错,可能是 Spark 的权限插件没配好。有些环境需要把 Hive 的权限插件 jar 包也放到 Spark 的 classpath 里。
另外,如果 Hive 表的数据文件权限设置得很严格,比如只有 Hive 用户能读,那 Spark 用其他用户跑的时候也会失败。这种需要调整 HDFS 文件权限,或者把 Spark 作业提交用户加到对应的用户组里。
6. 一些实战中攒下来的经验
6.1 关于 Metastore 的高可用
生产环境里 Metastore 一般是多实例部署,前面挂个负载均衡。Spark 配置hive.metastore.uris时可以写多个地址,用逗号分隔。这样某个实例挂了,Spark 会自动重试下一个。
但要注意,多个 Metastore 实例连的必须是同一个数据库。如果每个实例连的元数据库不一样,那就会出现“这个实例能看到表,那个实例看不到”的诡异现象。这个在部署时要确认清楚。
6.2 建表时的一些习惯
我习惯在 Spark 里建表时显式指定STORED AS ORC或者STORED AS PARQUET,并且加上TBLPROPERTIES设置一些压缩参数。比如:
CREATE EXTERNAL TABLE IF NOT EXISTS my_table ( id BIGINT, name STRING ) PARTITIONED BY (dt STRING) STORED AS ORC TBLPROPERTIES ('orc.compress'='SNAPPY');用外部表、指定列式存储格式、配好压缩,这三点能让后续的查询性能好很多。ORC 和 Parquet 选哪个看团队习惯,两者在 Spark 上的性能差异不大,ORC 在 Hive 上略好一些。
6.3 关于spark.sql.hive.metastore.jars的选择
如果生产 Hive 版本和 Spark 内置版本一致,用默认的builtin最省事。如果不一致,用maven让 Spark 自动下载对应版本。但内网环境可能下载不了,这时候可以提前把 jar 包下好放到一个目录,然后把这个参数设成目录路径。
用目录路径时要注意,这个目录下需要有 Hive Metastore 客户端的所有依赖 jar,包括 hadoop、thrift 相关的。少一个都可能报 ClassNotFoundException。我一般会从 Hive 的安装目录里把lib下的相关 jar 都拷过来。
6.4 监控与日常维护
集成上线后,建议对 Metastore 的连接数做监控。Spark 作业多的时候,Metastore 的连接数会飙升,如果超过上限就会拒绝新连接。可以在 Metastore 的配置里调大hive.metastore.server.max.threads,或者用连接池来管理。
另外,定期检查 Metastore 的数据库大小。元数据积累多了,查询会变慢。可以定期清理一些无用的临时表和过期分区。
7. 最后聊几句个人体会
这套集成方案我用了好几年,整体上是稳定的,但每次新环境部署还是会遇到这样那样的问题。最大的感受就是:版本对齐和配置检查这两件事,再怎么强调都不为过。很多问题追到根上,都是某个版本号差了一点,或者某个配置项写错了。
另外,别怕折腾。第一次配的时候可能会花几个小时,但把流程跑通之后,后面就是复制粘贴的事了。建议把配置过程整理成一个脚本或者文档,下次新环境直接照着做,能省不少时间。
还有一点,Spark 和 Hive 的社区都很活跃,遇到问题多去翻翻官方文档和邮件列表,大部分坑前人都踩过。实在搞不定的时候,把报错信息完整地贴出来搜一搜,往往能找到线索。