Gérer les dépendances Java et Scala pour Apache Spark

Les applications Spark dépendent souvent de bibliothèques tierces Java ou Scala. Voici les méthodes recommandées pour inclure ces dépendances lorsque vous envoyez une tâche Spark à un cluster Managed Service pour Apache Spark :

  1. Lorsque vous envoyez une tâche à partir de votre machine locale à l'aide de la gcloud dataproc jobs submit commande, utilisez l'option --properties spark.jars.packages=[DEPENDENCIES].

    Exemple :

    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. Lorsque vous envoyez une tâche directement sur votre cluster utilisez la commande spark-submit avec le paramètre --packages=[DEPENDENCIES].

    Exemple :

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

  3. Lorsque vous incluez des fichiers JAR ou des bundles personnalisés :

    • Dépendances spécifiques à la tâche : spécifiez le chemin d'accès Cloud Storage à votre fichier JAR à l'aide de l'option --jars, du Google Cloud champ Fichiers JAR de la console ou du champ jarFileUris de l'API lorsque vous envoyez une tâche.
    • Dépendances à l'échelle du cluster : utilisez la spark:spark.jars propriété de cluster lors de la création du cluster (--properties="spark:spark.jars=gs://<var>your-bucket</var>/<var>your-jarfile</var>.jar").

Éviter les conflits de dépendance

Les approches précédentes peuvent échouer si les dépendances de l'application Spark entrent en conflit avec les dépendances de Hadoop. Ce conflit peut survenir du fait que Hadoop injecte ses dépendances dans le paramètre classpath de l'application. Par conséquent, ses dépendances sont prioritaires par rapport aux dépendances de l'application. Lorsqu'un conflit se produit, NoSuchMethodError ou d'autres erreurs peuvent être générées.

Exemple :
Guava est la bibliothèque principale de Google pour Java. Elle est utilisée par de nombreuses bibliothèques et de nombreux frameworks, y compris Hadoop. Un conflit de dépendance peut survenir si une tâche ou ses dépendances nécessitent une version de Guava plus récente que celle utilisée par Hadoop.

Hadoop v3.0 a résolu

ce problème, mais les applications qui reposent sur des versions antérieures de Hadoop requièrent la solution suivante en deux étapes pour éviter d'éventuels conflits de dépendance.

  1. Créez un fichier JAR unique contenant le package de l'application et toutes ses dépendances.
  2. Transférez les packages de dépendance en conflit dans le fichier uber JAR pour éviter que les noms de leurs chemins d'accès n'entrent en conflit avec ceux des packages de dépendance Hadoop. Plutôt que de modifier votre code, utilisez un plug-in (voir ci-dessous) pour effectuer automatiquement ce transfert (ou "ombrage") dans le cadre du processus d'empaquetage.

Créer un uber JAR ombré avec Maven

Maven est un outil de gestion de package permettant de créer des applications Java. Le plug-in Maven Scala permet de créer des applications écrites en Scala, le langage utilisé par les applications Spark. Le plug-in Maven Shade

permet de créer un fichier JAR ombré.

Vous trouverez ci-dessous un exemple de fichier de configuration pom.xml qui effectue un ombrage sur la bibliothèque Guava, située dans le package com.google.common. Cette configuration demande à Maven de renommer le package com.google.common avec repackaged.com.google.common et de mettre à jour toutes les références aux classes issues de le package d'origine.

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

Pour exécuter le build :

mvn package

Remarques sur pom.xml :

  • ManifestResourceTransformer traite les attributs du fichier manifeste de l'uber JAR (MANIFEST.MF). Le fichier manifeste peut également spécifier le point d'entrée de votre application.
  • Le champ d'application de Spark est provided, car Spark est installé sur Managed Service pour Apache Spark.
  • Spécifiez la version de Spark installée sur votre cluster Managed Service pour Apache Spark (consultez la liste des versions de Managed Service pour Apache Spark ). Si votre application nécessite une version de Spark différente de celle installée sur votre cluster Managed Service pour Apache Spark, vous pouvez écrire une action d'initialisation ou créer une image personnalisée qui installera la version de Spark utilisée par votre application.
  • L'entrée <filters> exclut les fichiers de signature des répertoires META-INF de vos dépendances. Sans cette entrée, une exception d'exécution java.lang.SecurityException: Invalid signature file digest for Manifest main attributes peut se produire, car les fichiers de signature ne sont pas valides dans le contexte de votre fichier uber JAR.
  • Vous devrez peut-être ombrer plusieurs bibliothèques. Pour ce faire, incluez plusieurs chemins d'accès. L'exemple suivant illustre l'ombrage des bibliothèques Guava et Protobuf :
    <relocation>
      <pattern>com</pattern>
      <shadedPattern>repackaged.com</shadedPattern>
      <includes>
        <include>com.google.protobuf.**</include>
        <include>com.google.common.**</include>
      </includes>
    </relocation>

Créer un uber JAR ombré avec SBT

SBT est un outil permettant de créer des applications Scala. Pour créer un fichier JAR ombré avec SBT, ajoutez le plug-in sbt-assembly

à la description de votre build, en commençant par créer un fichier nommé assembly.sbt dans le répertoire project/ :

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

... puis en ajoutant la ligne suivante dans assembly.sbt :

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

Vous trouverez ci-dessous un exemple de fichier de configuration build.sbt qui effectue un ombrage sur la bibliothèque Guava, située dans le package com.google.common package.

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
)

Pour exécuter le build :

sbt assembly

Remarques sur build.sbt :

Envoyer l'uber JAR à Managed Service pour Apache Spark

Une fois que vous avez créé un fichier uber JAR ombré contenant votre application Spark et ses dépendances, importez le fichier JAR dans Cloud Storage et envoyez une tâche à Managed Service pour Apache Spark (à l'aide de l'option --jars dans l' interface de ligne de commande Google Cloud CLI, du champ Fichiers JAR de la Google Cloud console, du champ jarFileUris de l' API ou en spécifiant votre fichier JAR ombré comme fichier JAR d'application principal).

Étape suivante