spark2.x 的坑 轉

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過程中可能會踩到的坑。

計算準確性

行爲變化

那些不算太致命,改改代碼或配置就可以兼容的問題。

  • 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/applicationshttps://issues.apache.org/jira/browse/SPARK-12299https://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種方案可以選擇:

  1. 啓動一個hiveserver2服務,通過jdbc直接調用hive,讓hive執行add columns語句。這種應該是改起來最爲方便的一種方式了,缺點就是,我們還需要在啓動一個hiveserver服務,多一個服務依賴,會增加整個系統的維護成本。
  2. SparkSQL+Hive這種模式,要求我們啓動一個HiveMetastore服務,給SparkSQL用,我們也可以在代碼中直接直接連接HiveMetastore去執行add columns語句。這種方式的好處是不需要額外依賴其他服務,缺點就是我們要自己調用HiveMetastore相關接口,自己管理SessionState,用起來比較麻煩。
  3. 最後一種方式就是直接修改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)了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章