Apache Spark に関する Java と Scala の依存関係の管理

Spark アプリケーションは、サードパーティ製の Java または Scala ライブラリに依存していることがよくあります。ここでは、Spark ジョブを Dataproc クラスタに送信する際に、このような依存関係を含めるうえで推奨される方法を紹介します。

  1. gcloud dataproc jobs submit コマンドを使用してローカルマシンからジョブを送信する場合は、--properties spark.jars.packages=[DEPENDENCIES] フラグを使用します。

    例:

    gcloud dataproc jobs submit spark \
        --cluster=my-cluster \
        --region=region \
        --properties=spark.jars.packages='com.google.cloud:google-cloud-translate:1.35.0,org.apache.bahir:spark-streaming-pubsub_2.11:2.2.0'
    

  2. クラスタでジョブを直接送信する場合、--packages=[DEPENDENCIES] パラメータを指定した spark-submit コマンドを使用します。

    例:

    spark-submit --packages='com.google.cloud:google-cloud-translate:1.35.0,org.apache.bahir:spark-streaming-pubsub_2.11:2.2.0'
    

依存関係による競合の回避

Spark アプリケーションの依存関係が Hadoop の依存関係と競合する場合、前述の手法は失敗する可能性があります。この競合は、Hadoop がその依存関係をアプリケーションの classpath に挿入すると、その依存関係がアプリケーションの依存関係よりも優先されるために発生します。競合が発生すると、NoSuchMethodError またはその他のエラーが生成される場合があります。

例:
Guava は、Hadoop を含む多数のライブラリやフレームワークによって使用される、Java 用の Google コアライブラリです。ジョブまたはその依存関係により、Hadoop によって使用されているバージョンよりも新しいバージョンの Guava が必要になると、依存関係の競合が発生する可能性があります。

Hadoop v3.0 では、この問題は解決されましたが、以前の Hadoop バージョンに依存しているアプリケーションでは、依存関係の競合が発生する可能性を回避するために、次の 2 つの手順を行う必要があります。

  1. アプリケーションのパッケージとそのすべての依存関係を含む単一の JAR を作成します。
  2. 競合する依存関係パッケージを、この uber JAR 内に再配置して、それらのパス名が Hadoop の依存関係パッケージのパス名と競合しないようにします。 コードを変更する代わりに、プラグイン(下記を参照)を使用すると、パッケージ化プロセスの一部としてこの再配置(つまり、シェーディング)が自動的に行われます。

Maven を使用してシェーディングされた uber JAR を作成する

Maven は、Java アプリケーションを構築するためのパッケージ管理ツールです。Maven scala プラグインを使用して、Spark アプリケーションによって使用される言語である Scala で作成されたアプリケーションを構築できます。Maven shade プラグインを使用して、シェーディングされた JAR を作成できます。

com.google.common パッケージに置かれている Guava ライブラリをシェーディングする pom.xml 構成ファイルの例を次に示します。この構成により、Maven に com.google.common パッケージの名前を repackaged.com.google.common に変更する指示が行われ、元のパッケージからクラスへのすべての参照が更新されます。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <groupId><!-- YOUR_GROUP_ID --></groupId>
  <artifactId><!-- YOUR_ARTIFACT_ID --></artifactId>
  <version><!-- YOUR_PACKAGE_VERSION --></version>

  <dependencies>

    <dependency>
      <groupId>org.apache.spark</groupId>
      <artifactId>spark-sql_2.11</artifactId>
      <version><!-- YOUR_SPARK_VERSION --></version>
      <scope>provided</scope>
    </dependency>

    <!-- YOUR_DEPENDENCIES -->

  </dependencies>

  <build>
    <plugins>

      <plugin>
        <groupId>net.alchim31.maven</groupId>
        <artifactId>scala-maven-plugin</artifactId>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <scalaVersion><!-- YOUR_SCALA_VERSION --></scalaVersion>
        </configuration>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass><!-- YOUR_APPLICATION_MAIN_CLASS --></mainClass>
                </transformer>
                <!-- This is needed if you have dependencies that use Service Loader. Most Google Cloud client libraries do. -->
                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
              </transformers>
              <filters>
                <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                    <exclude>META-INF/maven/**</exclude>
                    <exclude>META-INF/*.SF</exclude>
                    <exclude>META-INF/*.DSA</exclude>
                    <exclude>META-INF/*.RSA</exclude>
                  </excludes>
                </filter>
              </filters>
              <relocations>
                <relocation>
                  <pattern>com</pattern>
                  <shadedPattern>repackaged.com.google.common</shadedPattern>
                  <includes>
                    <include>com.google.common.**</include>
                  </includes>
                </relocation>
              </relocations>
            </configuration>
          </execution>
        </executions>
      </plugin>

    </plugins>
  </build>

</project>

ビルドするには、次のようにします。

mvn package

pom.xml についての注意事項:

  • ManifestResourceTransformer は、uber JAR のマニフェスト ファイル(MANIFEST.MF)にある属性を処理します。マニフェストで、アプリケーションのエントリ ポイントが指定されていることもあります。
  • Spark は Dataproc にインストールされているため、Spark のスコープprovided です。
  • Dataproc クラスタにインストールされている Spark のバージョンを指定します(Dataproc バージョン リストを参照)。アプリケーションで、Dataproc クラスタにインストールされているバージョンとは異なるバージョンの Spark が必要な場合、初期化アクションを作成するか、アプリケーションで使用している Spark バージョンをインストールするカスタム イメージを作成します。
  • <filters> エントリによって、依存関係の META-INF ディレクトリから署名ファイルが除外されます。このエントリがない場合、java.lang.SecurityException: Invalid signature file digest for Manifest main attributes ランタイム例外が発生する可能性があります。これは、uber JAR のコンテキストでは署名ファイルが無効であるためです。
  • 複数のライブラリのシェーディングが必要になることがあります。これを行うには、複数のパスを含めます。 次の例では、Guava と Protobuf ライブラリがシェーディングされます。
    <relocation>
      <pattern>com</pattern>
      <shadedPattern>repackaged.com</shadedPattern>
      <includes>
        <include>com.google.protobuf.**</include>
        <include>com.google.common.**</include>
      </includes>
    </relocation>

SBT を使用してシェーディングされた uber JAR を作成する

SBT は、Scala アプリケーションを構築するためのツールです。SBT を使用してシェーディングされた JAR を作成するには、まず、project/ ディレクトリ内に assembly.sbt という名前のファイルを作成し、sbt-assembly プラグインをビルド定義に追加します。

├── src/
└── build.sbt
└── project/
    └── assembly.sbt

続いて、次の行を assembly.sbt に追加します。

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")

com.google.common package に置かれている Guava ライブラリをシェーディングする build.sbt 構成ファイルの例を次に示します。

lazy val commonSettings = Seq(
 organization := "YOUR_GROUP_ID",
 name := "YOUR_ARTIFACT_ID",
 version := "YOUR_PACKAGE_VERSION",
 scalaVersion := "YOUR_SCALA_VERSION",
)

lazy val shaded = (project in file("."))
 .settings(commonSettings)

mainClass in (Compile, packageBin) := Some("YOUR_APPLICATION_MAIN_CLASS")

libraryDependencies ++= Seq(
 "org.apache.spark" % "spark-sql_2.11" % "YOUR_SPARK_VERSION" % "provided",
 // YOUR_DEPENDENCIES
)

assemblyShadeRules in assembly := Seq(
  ShadeRule.rename("com.google.common.**" -> "repackaged.com.google.common.@1").inAll
)

ビルドするには、次のようにします。

sbt assembly

build.sbt についての注意事項:

  • SBT では厳密な競合解決戦略が使用されているため、上記の例のシェーディング ルールで一部の依存関係競合が解決されない場合があります。このため、MergeStrategy.firstlastconcatfilterDistinctLinesrenamediscard 方式を使用して、競合している特定のタイプのファイルを明示的にマージする、より詳細なルールの提供が必要になることがあります。詳細については、sbt-assemblyマージ方式をご覧ください。
  • 複数のライブラリのシェーディングが必要になることがあります。これを行うには、複数のパスを含めます。 次の例では、Guava と Protobuf ライブラリがシェーディングされます。
    assemblyShadeRules in assembly := Seq(
      ShadeRule.rename("com.google.common.**" -> "repackaged.com.google.common.@1").inAll,
      ShadeRule.rename("com.google.protobuf.**" -> "repackaged.com.google.protobuf.@1").inAll
    )

Dataproc に uber JAR を送信する

Spark アプリケーションとその依存関係を含む、シェーディングされた uber JAR を作成したら、Dataproc にジョブを送信できるようになります。

次のステップ

  • Maven と SBT の両方の構成ファイルを含むサンプルの Spark アプリケーションである spark-translate を確認します。
  • Dataproc クイックスタートで Spark Scala ジョブを作成して実行し、Dataproc クラスタで Spark Scala ジョブを作成して実行する方法を学習します。