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)
了。