Spark 程序依賴問題兩則
上傳依賴
用 maven 打包出來的 jar 由於沒有包括依賴,提交到 Spark 會因為找不到依賴而無法執行。簡單的解決方法是用 maven-assembly-plugin 打包出一個包含了所有依賴的、可以獨立使用的 jar(俗稱 fat jar)。我並不喜歡這種方式,原因有二:
- 生成的 jar 包羅萬象,所以非常大。比如我的程序用這種方式打包的話,通常會生成 100M+ 的包。
- 不容易解決不同 jar 中 resource 的衝突。比如 Spring 可以用
classpath*:application-context.xml
解析所有 jar 中的application-context.xml
;而 maven-assembly-plugin 打包出來卻只能保留一個 application-context.xml。
利用 spark-submit
的 --jars
參數,我們不必把所有東西都打包到一起。它可以將本地的 jars 上傳到 Spark driver 和 executors 並添加到 classpath 里。但是,maven 將所有的 jar 都保存在 $HOME/.m2
目錄中,我們要如何收集到項目所需的依賴呢?maven-dependency-plugin 的 copy-dependencies 功能可以幫我們做到。這個功能會將項目依賴的 jar 都複製到指定目錄。比如,如下配置會將所有的 runtime 和 compile 依賴複製到 project_path/target/lib
:
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
在提交任務時,需要在 --jars
後指明每一個 jar 的完整路径。我使用一個 ruby 腳本輔助:
exec "spark-submit",
"--jars", Dir[File.join(project_path, "target/lib/*.jar")].join(","),
...
依賴衝突
我的程序使用 gson 解析從 kafka 中接收的數據。在單獨測試時,解析模塊正常工作,而提交到 Spark 後卻抛出解析異常。一番調試後,發現在 Spark 上運行時 @TypeAdapter
並不起作用,導致 gson 不知如何解析某個類型。然而,我在 --jars
參數後指定的 gson 確實是程序依賴的版本,不應該不識別。
我猜測原因是 Spark 運行環境中,在加載 --jars
指定的 jar 之前,已加載過老版本的 gson。雖然可以使用如下兩個參數使 Spark 優先加載 user classpath,但這樣會有風險。
--conf spark.executor.userClassPathFirst=true
--conf spark.driver.userClassPathFirst=true
比如,user classpath 中某個類新於 Spark classpath 中的,然而 Spark 用到了新版本中已移除的方法……幸好解決依賴衝突常用的 maven-shade-plugin 有個 relocate 功能可以幫助我繞過這個問題。
relocate 可以將一個 package 改名成另一個,並修改程序中所有使用這個 package 的地方使用改名後的 package。比如,如下配置將 gson 包含在生成的 jar 中,並將 com.google.gson
/ com.google.gson.*
改名為 shaded.com.google.gson
/ shaded.com.google.gson.*
:
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<includes>
<include>com.google.code.gson:gson</include>
</includes>
</artifactSet>
<relocations>
<relocation>
<pattern>com.google.gson</pattern>
<shadedPattern>shaded.com.google.gson</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
這個方法有一個副作用,當 gson 抛出異常時,stacktrace 會顯示新的 package 名稱,而非原来的。
shaded.com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 105 path $.timestamp
at shaded.com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220)
at shaded.com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:116)
at shaded.com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:216)
at shaded.com.google.gson.Gson.fromJson(Gson.java:879)
at shaded.com.google.gson.Gson.fromJson(Gson.java:844)
at shaded.com.google.gson.Gson.fromJson(Gson.java:793)
at shaded.com.google.gson.Gson.fromJson(Gson.java:765)
...