Zeppelin SparkSQL Hive 查询不一致问题
1. 问题
Zeppelin Spark sql 查询出的数据量与 hive 不一致,重启 spark interpreter 就没问题,查询结果一致。
表存储格式为 orc,spark 版本:2.4.7
2. 结论
Zeppelin Spark 这种常驻应用查询 hive 表,无法感知 hive 外部的变化,那么由于缓存机制,spark 没有去主动感知表底层元数据的变化,查询跟实际数据会不一致。有两种方式:
- 每次查询之前 refresh table
- 禁用 hive metadata 缓存 :spark.sql.filesourceTableRelationCacheSize 配置为 0
方案 1 稍显麻烦,对于即时查询类应用不太适合,方案 2 有一丢丢的性能损失,但是对业务可用性更强。
3. 定位过程
查看 spark interpreter 进程是三天前启动的,对比执行计划,其中一张表查询 3 个月分区数据,扫描的行数就少了很多。重启 interpreter 就对了,怀疑是一种缓存机制导致,spark 在 hive 上的某种缓存。
检索关键词 sparksql
hive
cache
。查到为了提升查询性能 ,Spark 确实会缓存 hive 的 metadata,当表在外部被改变,需要刷新表, 更新缓存,取到最新的数据了。为了验证, 手动刷新这张表,再查询数据就对了。
刷新方式:
- spark.sql("""REFRESH TABLE """)
- spark.catalog.refreshTable("my_table")
- sqlContext.refreshTable("my_table")
4. 相关原理
查看官方文档,spark sql 读取 hive 表的时候会做一些性能优化:
例如参数 :spark.sql.hive.convertMetastoreParquet (默认值为 true),spark sql 会使用自己内置的 parquet reader and writer 读写通过 hive 创建的 parquet 格式的表。优化之前是用的 hive 默认的 Hive sede。为了解决数据不一致问题,网上的文章都是建议把这个参数设为 false,但是这样就用不到 spark sql 的优化,不太满意网上这个结果。
spark.sql.hive.convertMetastoreParquet:When set to true, the built-in Parquet reader and writer are used to process parquet tables created by using the HiveQL syntax, instead of Hive serde
进一步研究,开启这项优化后,为了进一步提升性能,spark sql 还会缓存 parquest metadata,如果表在 spark 外部(例如 hive 或者 hdfs)被更新了,需要手动刷新 metadata,不然读取的还是旧数据。
Spark SQL caches Parquet metadata for better performance. When Hive metastore Parquet table
conversion is enabled, metadata of those converted tables are also cached. If these tables are
updated by Hive or other external tools, you need to refresh them manually to ensure consistent
metadata.
这项有优化处理对 orc 同理(参数:spark.sql.hive.convertMetastoreOrc),orc 优化以及默认配置是在 spark 2.4 加入进来的。参考文档:
Since Spark 2.0, Spark converts Parquet Hive tables by default for better performance. Since Spark 2.4, Spark converts ORC Hive tables by default, too. It means Spark uses its own ORC support by default instead of Hive SerDe. As an example,
CREATE TABLE t(id int) STORED AS ORC
would be handled with Hive SerDe in Spark 2.3, and in Spark 2.4, it would be converted into Spark’s ORC data source table and ORC vectorization would be applied. To setfalse
tospark.sql.hive.convertMetastoreOrc
restores the previous behavior.
查看 Cache 在 SessionCatalog.scala 里除初始化创建,缓存的数据是表名对应的 LogicalPlan,也就是整个逻辑执行计划。缓存是使用的 guava cache,缓存大小由 spark.sql.filesourceTableRelationCacheSize
参数配置控制。
private val tableRelationCache: Cache[QualifiedTableName, LogicalPlan] = {
val cacheSize = conf.tableRelationCacheSize
CacheBuilder.newBuilder().maximumSize(cacheSize).build[QualifiedTableName, LogicalPlan]()
}
缓存的使用也是有限制的,截取部分代码,只有当查询的 path,schema,patition 一致才会取缓存,另外只有需要转换的才会走缓存,也就是配置了 spark.sql.hive.convertMetastoreParquet 、spark.sql.hive.convertMetastoreOrc 等的才会走缓存,但是这几个默认值都是 true,所以 parquet orc 类型的都会走缓存。
catalogProxy.getCachedTable(tableIdentifier) match {
case null => None // Cache miss
case logical @ LogicalRelation(relation: HadoopFsRelation, _, _, _) =>
val cachedRelationFileFormatClass = relation.fileFormat.getClass
expectedFileFormat match {
case `cachedRelationFileFormatClass` =>
// If we have the same paths, same schema, and same partition spec,
// we will use the cached relation.
val useCached =
relation.location.rootPaths.toSet == pathsInMetastore.toSet &&
logical.schema.sameType(schemaInMetastore) &&
// We don't support hive bucketed tables. This function `getCached` is only used for
// converting supported Hive tables to data source tables.
// 只有 converting 的 hive 表才会用缓存。
relation.bucketSpec.isEmpty &&
relation.partitionSchema == partitionSchema.getOrElse(StructType(Nil))
if (useCached) {
Some(logical)
Zeppelin Spark 这种常驻应用查询 hive 表,而无法感知 hive 外部的变化,那么由于缓存机制,spark 没有去主动感知表底层元数据的变化,查询跟实际数据会不一致。根据分析,有以下三种方式可以解决缓存导致查询不一致的问题:
- 禁用 spark built-in 优化,设置 spark.sql.hive.convertMetastoreParquet、spark.sql.hive.convertMetastoreOrc = false
- 每次查询之前 refresh table
- 禁用缓存 :spark.sql.filesourceTableRelationCacheSize 配置为 0
方案 1 过于粗暴,直接禁用优化,不推荐;
方案 2 稍微麻烦一点,对于即时查询类应用不太适合,业务上不太接受;
方案 3 有一丢丢的性能损失,但是更适合;
5. 测试验证
创建一张 orc 测试表:
create table mgwh_rd_dwm.testf1_orc(id string) PARTITIONED BY(day string) stored as orc;
创建一张 text file 数据表,并 load 本地测试数据到 hive:
create table mgwh_rd_dwm.testf1(id string) PARTITIONED BY(day string) ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' stored as textfile;
hive -v -e "LOAD DATA LOCAL INPATH '/tmp/htt.csv' INTO TABLE mgwh_rd_dwm.testf1 PARTITION (day='01')"
将 textfile 表数据 insert 到 orc 表:
INSERT INTO mgwh_rd_dwm.testf1_orc PARTITION(day) SELECT id, "01" as day from mgwh_rd_dwm.testf1
设置参数,orc 表读写优化打开,缓存 size 1000
spark.sql.hive.convertMetastoreOrc= true
spark.sql.filesourceTableRelationCacheSize = 1000
测试:
spark sql 查询初始行数为 3 (select count(1) from mgwh_rd_dwm.testf1_orc where day = '01')
使用 hive sql 重新 insert 3 行数据到 mgwh_rd_dwm.testf1_orc,此时实际行数为 6.
spark sql 再查询一次,行数依然为 3,问题复现。
验证解决方案:
设置 spark.sql.hive.convertMetastoreParquet = false 或者 spark.sql.filesourceTableRelationCacheSize = 0 都可以解决问题。
6. 参考
https://blog.csdn.net/zyzzxycj/article/details/85166571
https://spark.apache.org/docs/2.4.1/sql-migration-guide-upgrade.html
https://blog.csdn.net/ToBe_BetterMan/article/details/106231059?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.highlightwordscore&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.highlightwordscore