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 Dataproc :

  1. Lorsque vous envoyez une tâche à partir de votre machine locale à l'aide de la commande gcloud dataproc jobs submit, 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, exécutez 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'
    

Éviter les conflits de dépendance

Les approches ci-dessus 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. En cas de conflit, NoSuchMethodError ou d'autres erreurs peuvent s'afficher.

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 du 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 la version, procédez comme suit :

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 marqué comme provided, puisque Spark est installé sur Dataproc.
  • Spécifiez la version de Spark installée sur votre cluster Dataproc (consultez la section Liste des versions Dataproc). Si votre application nécessite une version de Spark différente de celle installée sur votre cluster Dataproc, 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

... 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 la version, procédez comme suit :

sbt assembly

Remarques sur build.sbt :

  • La règle d'ombrage de l'exemple ci-dessus est susceptible de ne pas résoudre tous les conflits de dépendance, car SBT utilise des stratégies de résolution de conflit strictes. Par conséquent, vous devrez peut-être fournir des règles plus détaillées permettant de fusionner explicitement des types de fichiers en conflit spécifiques à l'aide des stratégies MergeStrategy.first, last, concat, filterDistinctLines, rename ou discard. Pour en savoir plus, consultez la stratégie de fusion de sbt-assembly.
  • 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 :
    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
    )
    

Envoyer un fichier uber JAR à Dataproc

Une fois que vous avez créé un fichier uber JAR ombré contenant vos applications Spark et ses dépendances, vous êtes prêt à envoyer une tâche à Dataproc.

Étapes suivantes