1. 起因
前段时间(嗯,几个月前。。),我们的智能归因系统监测到单个hive测试表磁盘空间占用超过3TB,并发出预警。进一步查看发现该表的存储格式为text。
众所周知,当前存储空间效果最优的数据格式就是orc和parquet(而且还可以进行压缩),因此除非特殊情况,text格式不应该作为默认表格式。
text格式hive表示例:
这就引发了一个新的问题,不论是trino(presto)的ctas,还是spark的saveAsTable,默认建表格式都不是text(修改过默认参数)。而其他建表入口,如hive cli,则不对普通用户开放命令行执行权限。
那么,这个text格式的表是怎么来的呢?
2. 问题确定
既然大概确定了问题的原因,那么就要确认能否通过spark sql复现。(当然,也很简单)
spark.sql("""create table test.test_spark_sql_default_table_format_tmp
as
select xxx""")
通过hive查看刚刚创建的表的格式,发现确实是text格式,可以稳定复现。
所以可以确定问题就在spark sql这里。
2.1 进一步排查
第一个问题,就是要确认为什么spark dsl的默认参数为什么不对spark sql起作用,通过查阅spark官方文档以及spark源码(v2.4.8),发现了更“有趣”的现象。
spark sql 和 spark dsl配置并不是统一的。😥
以建表格式配置参数为例:
spark sql : hive.default.fileformat(至少表面看起来是这样)
源码见:org/apache/spark/sql/internal/HiveSerDe.scala getDefaultStorage
spark dsl : spark.sql.sources.default
配置页面见:sql-data-sources-load-save-functions
源码见:org/apache/spark/sql/internal/SQLConf.scala DEFAULT_DATA_SOURCE_NAME
3. 尝试修复
既然确定了问题(spark sql 默认建表格式没有指定为orc),那么就需要针对这个问题进行修复了。
通过查看官方文档和源代码,可以发现spark sql支持通过hint的方式,来指定创建表时的存储格式等参数:
文档链接:sql-data-sources-hive-tables
源码:
org.apache.spark.sql.execution.command.DDLParserSuite
test("create hive table - table file format") {
val allSources = Seq("parquet", "parquetfile", "orc", "orcfile", "avro", "avrofile",
"sequencefile", "rcfile", "textfile")
allSources.foreach { s =>
val query = s"CREATE TABLE my_tab STORED AS $s"
val ct = parseAs[CreateTable](query)
val hiveSerde = HiveSerDe.sourceToSerDe(s)
assert(hiveSerde.isDefined)
assert(ct.tableDesc.storage.serde ==
hiveSerde.get.serde.orElse(Some("org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe")))
assert(ct.tableDesc.storage.inputFormat == hiveSerde.get.inputFormat)
assert(ct.tableDesc.storage.outputFormat == hiveSerde.get.outputFormat)
}
}
这个确实也不失为一种解决方案,但是对于sql编写人员无疑提出了更多的要求。
所以从易用性的角度而言,我们更需要能够指定spark sql 默认建表格式。
因此可以说问题并未解决。还记得上面那个HiveSerDe吗,里面还有一个方法叫做getDefaultStorage,用于从配置信息中获取hive的默认建表格式(生成逻辑计划的时候):
def getDefaultStorage(conf: SQLConf): CatalogStorageFormat = {
val defaultStorageType = conf.getConfString("hive.default.fileformat", "textfile")
val defaultHiveSerde = sourceToSerDe(defaultStorageType)
CatalogStorageFormat.empty.copy(
inputFormat = defaultHiveSerde.flatMap(_.inputFormat)
.orElse(Some("org.apache.hadoop.mapred.TextInputFormat")),
outputFormat = defaultHiveSerde.flatMap(_.outputFormat)
.orElse(Some("org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat")),
serde = defaultHiveSerde.flatMap(_.serde)
.orElse(Some("org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe")))
}
val defaultStorageType = conf.getConfString("hive.default.fileformat", "textfile")
3.1 新的转机?
好像发现了罪魁祸首?😀
我们测试获取下这个参数:
结果是undefined,也就是没有在spark conf中没有指定过默认建表格式,那么我们试试在已启动的spark会话中设置下这个参数:
spark.sql("set hive.default.fileformat = 'orc'")
再执行下spark sql ctas语句。果然,这次的hive测试表的格式成功变为orc了。
看起来,似乎只是因为我们没有在spark conf配置过hive.default.fileformat这个参数?😏
因为spark支持通过--conf的方式动态配置参数,所以可以先通过这个方式测试下:
spark --conf hive.default.fileformat=orc
在新的spark 会话中,我们再次执行ctas语句。可是结果却很让人惊讶,这次创建的测试表仍然是text格式?!😅
很奇怪呀,不是已经传递了这个参数了吗?为什么没有生效呢?
3.2 真正的问题
我们再看看这个参数真的生效了吗?
果然,并不是期待中的orc格式,还是undefined。
这个就很让人费解了,明明在源码中这个参数的key就是hive.default.fileformat呀?
这一步让我开始怀疑,--conf设置的参数,真的传递进去了吗?
基于日常排查问题的经验,我猜测spark启动脚本也支持verbose参数: 执行下spark --help。果然,一个期待中的参数出现在屏幕中。
那么开启verbose模式试试,看下到底是什么原因。
spark --conf hive.default.fileformat=orc --verbose
Ignoring non-Spark config property: hive.default.fileformat
spark日志显示,我们传递了一个非spark的配置参数,然后被过滤掉了。😥
那么就只能根据日志信息,看下对应的spark源码,确定到底是什么逻辑了:
org.apache.spark.deploy.SparkSubmitArguments.ignoreNonSparkProperties
private def ignoreNonSparkProperties(): Unit = {
sparkProperties.keys.foreach { k =>
if (!k.startsWith("spark.")) {
sparkProperties -= k
logWarning(s"Ignoring non-Spark config property: $k")
}
}
}
emmm,显而易见。spark要求传递的参数必须以spark.为前缀,不符合这个要求的配置参数,就会被过滤掉。
啊哈,这一切的问题,居然是因为配置参数命名风格不统一导致的,这么简单?
既然找到了真正的问题,接下来就so easy了。修复下获取参数的key这部分的源码,然后重新编译、打包就可以了。
def getDefaultStorage(conf: SQLConf): CatalogStorageFormat = {
val HiveStorageType = conf.getConfString("spark.hive.default.fileformat", "textfile")
var defaultStorageType = conf.getConfString("hive.default.fileformat", "textfile")
if (HiveStorageType != defaultStorageType) {
defaultStorageType = HiveStorageType
}
val defaultHiveSerde = sourceToSerDe(defaultStorageType)
CatalogStorageFormat.empty.copy(
inputFormat = defaultHiveSerde.flatMap(_.inputFormat)
.orElse(Some("org.apache.hadoop.mapred.TextInputFormat")),
outputFormat = defaultHiveSerde.flatMap(_.outputFormat)
.orElse(Some("org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat")),
serde = defaultHiveSerde.flatMap(_.serde)
.orElse(Some("org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe")))
}
这里为了不破坏原有参数的配置效果,只新增了一个符合spark命名规范的参数,仅用来满足配置默认值的需求。
4. 总结
这次帮助平台的小伙伴解决了一个不大不小的“坑”,一定程度上提升了集群的稳定性和写数据的效率。
so,遇到奇怪的问题不要轻易放弃,再研究下,也许问题并不复杂,而你能够解决掉它呢?
5. 后记
后来也看了下spark3.x,官方已经解决了这个问题,建表格式默认为orc了。(因为hadoop.xml的配置参数优先级会更高)