Spark 1.6升级2.x防踩坑指南
Spark 2.x自2.0.0发布到目前的2.2.0已经有一年多的时间了,2.x宣称有诸多的性能改进,相信不少使用Spark的同学还停留在1.6.x或者更低的版本上,没有升级到2.x或许是由于1.6相对而言很稳定,或许是升级后处处踩坑被迫放弃。
Spark SQL是Spark中最重要的模块之一,基本上Spark每个版本发布SQL模块都有不少的改动,而且官网还会附带一个Migration Guide帮忙大家升级。问题在于Migration Guide并没有详尽的列出所有变动,本文以SQL模块为主,扒一扒Spark升级2.x过程中可能会踩到的坑。
计算准确性
SELECT '0.1' = 0
返回的是true!Spark 2.2中,0.1会被转换为int,如果你的数据类型全部是文本类型,做数值计算时,结果极有可能不正确。之前的版本中0.1会被转换为double类型绝大多数场景下这样的处理是正确的。目前为止,社区还没有很好的处理这个问题,针对这个问题,我给社区提交过一个PR,想要自己解决这个问题的同学,可以手动合并下:https://github.com/apache/spark/pull/18986- 过于复杂的SQL语句执行可能会出现64KB字节码编译限制的问题,这算是个老问题了,Spark自从上了Tungsten基本上一直存在这个问题,也算是受到了JVM的限制,遇到此类问题,建议大家找找PR:https://github.com/apache/spark/search?utf8=%E2%9C%93&q=64KB&type=Issues
- 数据计算精度有问题,
SELECT 1 > 0.0001
会报错,这个问题已在2.1.2及2.2.0中修复:https://issues.apache.org/jira/browse/SPARK-20211 - 2.1.0版本中
INNER JOIN
涉及到常量计算结果不正确,后续版本已修复:https://issues.apache.org/jira/browse/SPARK-19766 - 2.1.0中,执行
GROUPING SET(col)
,如果col列数据为null,会报空指针异常,后续版本已修复:https://issues.apache.org/jira/browse/SPARK-19509 - 2.1.0中,嵌套的CASE WHEN语句执行有可能出错,后续版本已修复:https://issues.apache.org/jira/browse/SPARK-19472
行为变化
那些不算太致命,改改代码或配置就可以兼容的问题。
- Spark 2.2的UDAF实现有所变动,如果你的Hive UDAF没有严格按照标准实现,有可能会计算报错或数据不正确,建议将逻辑迁移到Spark AF,同时也能获得更好的性能
- Spark 2.x限制了Hive表中
spark.sql.*
相关属性的操作,明明存在的属性,使用SHOW TBLPROPERTIES tb("spark.sql.sources.schema.numParts")
无法获取到,同理也无法执行ALTER TABLE tb SET TBLPROPERTIES ('spark.sql.test' = 'test')
进行修改 - 无法修改外部表的属性
ALTER TABLE tb SET TBLPROPERTIES ('test' = 'test')
这里假设tb是EXTERNAL类型的表 - DROP VIEW IF EXISTS tb,如果这里的tb是个TABLE而非VIEW,执行会报错
AnalysisException: Cannot drop a table with DROP VIEW
,在2.x以下不会报错,由于我们指定了IF EXISTS关键字,这里的报错显然不合理,需要做异常处理。 - 如果你访问的表不存在,异常信息在Spark2.x里由之前的
Table not found
变成了Table or view not found
,如果你的代码里依赖这个异常信息,就需要注意调整了。 - EXPLAIN语句的返回格式变掉了,在1.6里是多行文本,2.x中是一行,而且内容格式也有稍微的变化,相比Spark1.6,少了Tungsten关键字;EXPLAIN中显示的HDFS路径过长的话,在Spark 2.x中会被省略为...
- 2.x中默认不支持笛卡尔积操作,需要通过参数
spark.sql.crossJoin.enabled
开启 - OLAP分析中常用的
GROUPING__ID
函数在2.x变成了GROUPING_ID()
- 如果你有一个基于Hive的UDF名为abc,有3个参数,然后又基于Spark的UDF实现了一个2个参数的abc,在2.x中,2个参数的abc会覆盖掉Hive中3个参数的abc函数,1.6则不会有这个问题
- 执行类似
SELECT 1 FROM tb GROUP BY 1
的语句会报错,需要单独设置spark.sql.groupByOrdinal false
类似的参数还有spark.sql.orderByOrdinal false
- CREATE DATABASE默认路径发生了变化,不在从hive-site.xml读取
hive.metastore.warehouse.dir
,需要通过Spark的spark.sql.warehouse.dir
配置指定数据库的默认存储路径。 - CAST一个不存在的日期返回null,如:year('2015-03-40'),在1.6中返回2015
- Spark 2.x不允许在VIEW中使用临时函数(temp function)https://issues.apache.org/jira/browse/SPARK-18209
- Spark 2.1以后,窗口函数ROW_NUMBER()必须要在OVER内添加ORDER BY,以前的
ROW_NUMBER() OVER()
执行会报错 - Spark 2.1以后,
SIZE(null)
返回-1,之前的版本返回null - Parquet文件的默认压缩算法由gzip变成了snappy,据官方说法是snappy有更好的查询性能,大家需要自己验证性能的变化
- DESC FORMATTED tb返回的内容有所变化,1.6的格式和Hive比较贴近,2.x中分两列显示
- 异常信息的变化,未定义的函数,Spark 2.x:
org.apache.spark.sql.AnalysisException: Undefined function: 'xxx’.
, Spark 1.6:AnalysisException: undefined function xxx
,参数格式错误:Spark 2.x:Invalid number of arguments
, Spark 1.6:No handler for Hive udf class org.apache.hadoop.hive.ql.udf.generic.GenericUDAFXXX because: Exactly one argument is expected..
- Spark Standalone的WebUI中已经没有这个API了:
/api/v1/applications
:https://issues.apache.org/jira/browse/SPARK-12299,https://issues.apache.org/jira/browse/SPARK-18683
版本回退
那些升级到2.x后,发现有问题回退后,让你欲哭无泪的问题。
- Spark 2.0开始,SQL创建的分区表兼容Hive了,Spark会将分区信息保存到HiveMetastore中,也就是我们可以通过SHOW PARTITIONS查询分区,Hive也能正常查询这些分区表了。如果将Spark切换到低版本,在更新分区表,HiveMetastore中的分区信息并不会更新,需要执行
MSCK REPAIR TABLE
进行修复,否则再次升级会出现缺数据的现象。 - Spark 2.0 ~ 2.1创建的VIEW并不会把创建VIEW的原始SQL更新到HiveMetastore,而是解析后的SQL,如果这个SQL包含复杂的子查询,那么切换到1.6后,就有可能无法使用这个VIEW表了(1.6对SQL的支持不如2.x)
其他
从2.2.0开始,Spark不在支持Hadoop 2.5及更早的版本,同时也不支持Java 7 了,所以,如果你用的版本比较老,还是尽快升级的比较好。
2.x中对于ThriftServer或JobServer这样的长时间运行的服务,稳定性不如1.6,如果您的计算业务复杂、SQL计算任务繁多、频繁的更新数据、处理数据量较大,稳定性的问题更加凸显。稳定性问题主要集中在内存方面,Executor经常出现堆外内存严重超出、OOM导致进程异常退出等问题。Executor进程OOM异常退出后相关的block-mgr目录(也就是SPARK_LOCAL_DIRS)并不会被清理,这就导致Spark Application长时间运行很容易出现磁盘被写满的情况。
总结
Spark 2.x中为了性能,SQL模块的改动相当大,这也导致Bug变多,稳定性变差。当然,随着Spark的不断改进迭代,这些问题也在逐步缓解。
对于一个计算服务,相比性能,数据计算的正确性及稳定性更加重要。建议尚未升级到2.x的同学,最好使用最新的Spark版本做升级;升级前,务必结合自己的业务场景做好充分的测试,避免踩坑。
自己动手为Spark 2.x添加ALTER TABLE ADD COLUMNS语法支持
SparkSQL从2.0开始已经不再支持ALTER TABLE table_name ADD COLUMNS (col_name data_type [COMMENT col_comment], ...)
这种语法了(下文简称add columns语法)。如果你的Spark项目中用到了SparkSQL+Hive这种模式,从Spark1.x升级到2.x很有可能遇到这个问题。
为了解决这个问题,我们一般有3种方案可以选择:
- 启动一个hiveserver2服务,通过jdbc直接调用hive,让hive执行add columns语句。这种应该是改起来最为方便的一种方式了,缺点就是,我们还需要在启动一个hiveserver服务,多一个服务依赖,会增加整个系统的维护成本。
- SparkSQL+Hive这种模式,要求我们启动一个HiveMetastore服务,给SparkSQL用,我们也可以在代码中直接直接连接HiveMetastore去执行add columns语句。这种方式的好处是不需要额外依赖其他服务,缺点就是我们要自己调用HiveMetastore相关接口,自己管理SessionState,用起来比较麻烦。
- 最后一种方式就是直接修改Spark,让他支持add columns语法。这种方式最大的好处就是我们原有的业务逻辑代码不用动,问题就在于,要求对Spark源码有一定的了解,否则改起来还是挺费劲的。这也是我写这篇文章的目的:让大家能够参考本文自行为Spark添加add columns语法支持。
OK,接下来,我们进入主题。
为Spark添加add columns语法支持
本文基于最新版的Spark 2.1.0,源码地址:https://github.com/apache/spark/tree/branch-2.1
1. 改进语法定义
Spark2.1开始使用ANTLR来解析SQL语法,它的语法定义文件借鉴的Presto项目,我们在Spark源码中找到这个文件sql/catalyst/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBase.g4
,做如下改动:
@@ -127,6 +127,8 @@ statement
('(' key=tablePropertyKey ')')? #showTblProperties
| SHOW COLUMNS (FROM | IN) tableIdentifier
((FROM | IN) db=identifier)? #showColumns
+ | ALTER TABLE tableIdentifier ADD COLUMNS
+ ('(' columns=colTypeList ')')? #addColumns
| SHOW PARTITIONS tableIdentifier partitionSpec? #showPartitions
| SHOW identifier? FUNCTIONS
(LIKE? (qualifiedName | pattern=STRING))? #showFunctions
@@ -191,7 +193,6 @@ unsupportedHiveNativeCommands
| kw1=ALTER kw2=TABLE tableIdentifier partitionSpec? kw3=COMPACT
| kw1=ALTER kw2=TABLE tableIdentifier partitionSpec? kw3=CONCATENATE
| kw1=ALTER kw2=TABLE tableIdentifier partitionSpec? kw3=SET kw4=FILEFORMAT
- | kw1=ALTER kw2=TABLE tableIdentifier partitionSpec? kw3=ADD kw4=COLUMNS
| kw1=ALTER kw2=TABLE tableIdentifier partitionSpec? kw3=CHANGE kw4=COLUMN?
| kw1=ALTER kw2=TABLE tableIdentifier partitionSpec? kw3=REPLACE kw4=COLUMNS
| kw1=START kw2=TRANSACTION
194行的kw1=ALTER kw2=TABLE tableIdentifier partitionSpec? kw3=ADD kw4=COLUMNS
是在unsupportedHiveNativeCommands
列表中,我们首先把它去掉。
为了让Spark能解析ALTER TABLE table_name ADD COLUMNS (col_name data_type [COMMENT col_comment], ...)
,我们还需要在129行处新增| ALTER TABLE tableIdentifier ADD COLUMNS ('(' columns=colTypeList ')')? #addColumns
最后的#addColumns
是为了让ANTLR插件(这个插件定义在sql/catalyst/pom.xml中)为我们自动生成addColumns相关方法,便于我们做语法解析处理。这个语法中有2个参数需要我们处理table_name和columns。
2. 改进SparkSqlAstBuilder,使其能处理addColumns
SparkSqlAstBuilder
的作用是将ANTLR的语法树翻译为LogicalPlan/Expression/TableIdentifier
要修改的文件为:sql/core/src/main/scala/org/apache/spark/sql/execution/SparkSqlParser.scala
,我们在178行处,新增如下方法:
override def visitAddColumns(ctx: AddColumnsContext): LogicalPlan = withOrigin(ctx) {
val tableName = visitTableIdentifier(ctx.tableIdentifier())
val dataCols = Option(ctx.columns).map(visitColTypeList).getOrElse(Nil)
AlterTableAddColumnsCommand(tableName, dataCols)
}
visitAddColumns方法是ANTLR插件自动为我们生成的方法,定义在SparkSqlAstBuilder的父类AstBuilder中(AST,Abstract Syntax Tree ,抽象语法树),这个方法用来处理我们在SqlBase.g4中定义的| ALTER TABLE tableIdentifier ADD COLUMNS ('(' columns=colTypeList ')')? #addColumns
,我们这里重载了visitAddColumns方法用来提取表名及新增的字段列表,并返回一个LogicalPlan:AlterTableAddColumnsCommand,这个类我们接下来会说明。
3. 新增一个为表添加字段的命令
修改sql/core/src/main/scala/org/apache/spark/sql/execution/command/tables.scala
,在120行处,新增AlterTableAddColumnsCommand类:
case class AlterTableAddColumnsCommand(
tableName: TableIdentifier,
newColumns: Seq[StructField]) extends RunnableCommand {
override def run(sparkSession: SparkSession): Seq[Row] = {
val catalog = sparkSession.sessionState.catalog
val table = catalog.getTableMetadata(tableName)
DDLUtils.verifyAlterTableType(catalog, table, isView = false)
val newSchema = StructType(table.schema.fields ++ newColumns)
val newTable = table.copy(schema = newSchema)
catalog.alterTable(newTable)
Seq.empty[Row]
}
}
RunnableCommand类继承自LogicalPlan,run方法用于执行addColumns语法对应的执行逻辑。这个类的处理逻辑比较简单,就不详细介绍了。
4. 修复HiveExternalCatalog无法修改表schema的问题
我们在第3步的AlterTableAddColumnsCommand中,虽然调用了catalog.alterTable(newTable)
来修改表信息,但实际上并不能将新的字段添加到表中,因为Spark代码写死了,不能改Hive表的schema,我们还需要修改HiveExternalCatalog类(sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveExternalCatalog.scala),改动如下:
@@ -588,7 +588,8 @@ private[spark] class HiveExternalCatalog(conf: SparkConf, hadoopConf: Configurat
val newTableProps = oldDataSourceProps ++ withStatsProps.properties + partitionProviderProp
val newDef = withStatsProps.copy(
storage = newStorage,
- schema = oldTableDef.schema,
+ // allow `alter table xxx add columns(xx)`
+ schema = tableDefinition.schema,
partitionColumnNames = oldTableDef.partitionColumnNames,
bucketSpec = oldTableDef.bucketSpec,
properties = newTableProps)
我们将591行的schema = oldTableDef.schema
替换为schema = tableDefinition.schema
即可。
至此,我们完成了整个代码的调整。
最后参考Spark的编译文档:http://spark.apache.org/docs/latest/building-spark.html#building-a-runnable-distribution,将Spark编译打包即可。
Spark 2.x会将编译后的assembly放到jars目录下,我们这次的改动会影响到以下几个jar包:
- spark-catalyst_2.11-2.1.0.jar
- spark-sql_2.11-2.1.0.jar
- spark-hive_2.11-2.1.0.jar
如果Spark已经部署过了,可以直接将以上3个jar替换掉。
更新Spark后,我们就可以使用alter table xxx add columns(xx)
了。