Java- und Scala-Abhängigkeiten für Apache Spark verwalten

Spark-Anwendungen sind häufig auf Java- oder Scala-Bibliotheken von Drittanbietern angewiesen. In diesem Artikel finden Sie empfohlene Vorgehensweisen dazu, wie Sie diese Abhängigkeiten einbeziehn, wenn Sie Spark-Jobs an Dataproc-Cluster senden:

  1. Wenn Sie einen Auftrag von Ihrem lokalen Rechner mit dem gcloud dataproc jobs submit-Befehl senden, verwenden Sie das Flag --properties spark.jars.packages=[DEPENDENCIES].

    Beispiel:

    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. Wenn Sie einen Job direkt im Cluster senden, verwenden Sie den Befehl spark-submit mit dem Parameter --packages=[DEPENDENCIES].

    Beispiel:

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

Abhängigkeitskonflikte vermeiden

Die obigen Vorgehensweisen können fehlschlagen, wenn Abhängigkeiten von Spark-Anwendungen mit Abhängigkeiten von Hadoop in Konflikt stehen. Ein solcher Konflikt kann entstehen, da Hadoop seine Abhängigkeiten in den Klassenpfad der Anwendung so einfügt, dass sie gegenüber den Abhängigkeiten der Anwendung Vorrang haben. Wenn ein Konflikt auftritt, können Fehler wie NoSuchMethodError ausgegeben werden.

Beispiel:
Guava ist die zentrale Google-Mediathek für Java, die von vielen Mediatheken und Frameworks, einschließlich Hadoop, verwendet wird. Ein Abhängigkeitskonflikt kann auftreten, wenn für einen Job oder dessen Abhängigkeiten eine Version von Guava erforderlich ist, die neuer als die von Hadoop verwendete ist.

Mit Hadoop Version 3.0 wurde dieses Problem behoben. Bei Anwendungen, die auf früheren Hadoop-Versionen basieren, ist allerdings der folgende zweiteilige Workaround erforderlich, um mögliche Abhängigkeitskonflikte zu vermeiden.

  1. Erstellen Sie eine einzelne JAR-Datei, die das Paket der Anwendung und alle zugehörigen Abhängigkeiten enthält.
  2. Verschieben Sie die in Konflikt stehenden Abhängigkeitspakete innerhalb der Uber-JAR-Datei. So wird verhindert, dass ihre Pfadnamen mit den Pfadnamen der Hadoop-Abhängigkeitspakete in Konflikt geraten. Statt hierfür Ihren Code zu ändern, verwenden Sie ein Plug-in (siehe unten), um diese Verschiebung (auch als "Shading" bezeichnet) automatisch im Rahmen der Paketerstellung durchzuführen.

Shading-Uber-JAR-Datei mit Maven erstellen

Maven ist ein Paketverwaltungstool zum Erstellen von Java-Anwendungen. Mit dem Maven-Scala-Plug-in können Anwendungen erstellt werden, die in Scala geschrieben sind, der von Spark-Anwendungen verwendeten Sprache. Das Maven-Shade-Plug-in kann dazu verwendet werden, eine Shading-JAR-Datei zu erstellen.

Das folgende Beispiel zeigt eine pom.xml-Konfigurationsdatei, die die Guava-Mediathek, die sich im Paket com.google.common befindet, schattiert. Diese Konfiguration weist Maven an, das Paket com.google.common in repackaged.com.google.common umzubenennen und alle Verweise auf die Klassen aus dem ursprünglichen Paket zu aktualisieren.

<?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>

So führen Sie den Build aus:

mvn package

Hinweise zu pom.xml:

  • ManifestResourceTransformer verarbeitet Attribute in der Manifestdatei der Uber-JAR-Datei (MANIFEST.MF). Das Manifest kann auch den Einstiegspunkt für Ihre Anwendung angeben.
  • Der Bereich von Spark ist provided, da Spark in Dataproc installiert ist.
  • Geben Sie die Version von Spark an, die in Ihrem Cloud Dataproc-Cluster installiert ist. Informationen hierzu finden Sie in der Liste der Cloud Dataproc-Versionen. Wenn die in Ihrem Dataproc-Cluster installierte Spark-Version für Ihre Anwendung nicht geeignet ist, schreiben Sie entweder eine Initialisierungsaktion oder erstellen Sie ein benutzerdefiniertes Image, mit dem die von der Anwendung verwendete Spark-Version installiert wird.
  • Der Eintrag <filters> schließt Signaturdateien aus den META-INF-Verzeichnissen Ihrer Abhängigkeiten aus. Ohne diesen Eintrag kann eine java.lang.SecurityException: Invalid signature file digest for Manifest main attributes-Laufzeitausnahme auftreten, da die Signaturdateien im Kontext Ihrer Uber-JAR-Datei ungültig sind.
  • Es kann sein, dass mehrere Bibliotheken umbenannt werden müssen. Fügen Sie dazu mehrere Pfade ein. Im nächsten Beispiel werden die Bibliotheken Guava und Protobuf umbenannt.
    <relocation>
      <pattern>com</pattern>
      <shadedPattern>repackaged.com</shadedPattern>
      <includes>
        <include>com.google.protobuf.**</include>
        <include>com.google.common.**</include>
      </includes>
    </relocation>

Shading-Uber-JAR-Datei mit SBT erstellen

SBT ist ein Tool zum Erstellen von Scala-Anwendungen. Wenn Sie eine Shading-JAR-Datei mit SBT erstellen möchten, fügen Sie der Build-Definition das Plug-in sbt-assembly hinzu. Erstellen Sie dazu zuerst eine Datei mit dem Namen assembly.sbt im Verzeichnis project/:

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

... und fügen Sie dann die folgende Zeile in assembly.sbt hinzu:

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

Das folgende Beispiel zeigt die Konfigurationsdatei build.sbt, mit der die Guava-Mediathek, die sich im com.google.common package befindet, umbenannt wird (Shading).

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
)

So führen Sie den Build aus:

sbt assembly

Hinweise zu build.sbt:

  • Möglicherweise werden mit der Shading-Regel oben nicht alle Abhängigkeitskonflikte gelöst, da SBT ganz bestimmte Strategien zur Konfliktlösung verwendet. Daher kann es sein, dass Sie detailliertere Regeln angeben müssen, durch die bestimmte Arten von in Konflikt stehenden Dateien ausdrücklich zusammengeführt werden. Verwenden Sie hierbei die Strategie MergeStrategy.first, last, concat, filterDistinctLines, rename oder discard. Weitere Informationen finden Sie in der Zusammenführungsstrategie von sbt-assembly.
  • Es kann sein, dass mehrere Bibliotheken umbenannt werden müssen. Fügen Sie dazu mehrere Pfade ein. Im nächsten Beispiel werden die Bibliotheken Guava und Protobuf umbenannt.
    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
    )

Uber-JAR-Datei an Dataproc senden

Nachdem Sie eine Shading-Uber-JAR-Datei erstellt haben, die Ihre Spark-Anwendungen und deren Abhängigkeiten enthält, können Sie einen Job an Dataproc senden.

Nächste Schritte