Gestire le dipendenze Java e Scala per Apache Spark

Le applicazioni Spark spesso dipendono da librerie Java o Scala di terze parti. Di seguito sono riportati gli approcci consigliati per includere queste dipendenze quando invii un job Spark a un cluster Managed Service for Apache Spark:

  1. Quando invii un job dalla tua macchina locale con il comando gcloud dataproc jobs submit, utilizza il flag --properties spark.jars.packages=[DEPENDENCIES].

    Esempio:

    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. Quando invii un job direttamente sul cluster, utilizza il comando spark-submit con il parametro --packages=[DEPENDENCIES].

    Esempio:

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

  3. Quando includi file JAR o bundle personalizzati:

    • Dipendenze specifiche del job:specifica il percorso Cloud Storage del file JAR utilizzando il flag --jars, il campo File JAR della console Cloud de Confiance o il campo jarFileUris dell'API quando invii un job.
    • Dipendenze a livello di cluster: utilizza la proprietà cluster spark:spark.jars durante la creazione del cluster (--properties="spark:spark.jars=gs://<var>your-bucket</var>/<var>your-jarfile</var>.jar").

Evitare conflitti di dipendenza

Gli approcci precedenti potrebbero non riuscire se le dipendenze dell'applicazione Spark sono in conflitto con quelle di Hadoop. Questo conflitto può verificarsi perché Hadoop inserisce le proprie dipendenze nel classpath dell'applicazione, quindi le sue dipendenze hanno la precedenza su quelle dell'applicazione. Quando si verifica un conflitto, possono essere generati NoSuchMethodError o altri errori.

Esempio:
Guava è la libreria principale di Google per Java utilizzata da molte librerie e framework, tra cui Hadoop. Un conflitto di dipendenze può verificarsi se un job o le relative dipendenze richiedono una versione di Guava più recente di quella utilizzata da Hadoop.

Hadoop v3.0 risolto

questo problema, ma le applicazioni che si basano su versioni precedenti di Hadoop richiedono la seguente soluzione alternativa in due parti per evitare possibili conflitti di dipendenze.

  1. Crea un unico file JAR contenente il pacchetto dell'applicazione e tutte le relative dipendenze.
  2. Sposta i pacchetti di dipendenze in conflitto all'interno del file JAR uber per evitare che i nomi dei percorsi siano in conflitto con quelli dei pacchetti di dipendenze di Hadoop. Anziché modificare il codice, utilizza un plug-in (vedi sotto) per eseguire automaticamente questa ricollocazione (nota anche come "ombreggiatura") nell'ambito del processo di packaging.

Crea un uber JAR ombreggiato con Maven

Maven è uno strumento di gestione dei pacchetti per la creazione di applicazioni Java. Il plug-in Maven scala può essere utilizzato per creare applicazioni scritte in Scala, il linguaggio utilizzato dalle applicazioni Spark. La tonalità Maven

può essere utilizzato per creare un file JAR ombreggiato.

Di seguito è riportato un file di configurazione pom.xml di esempio che ombreggia la libreria Guava, che si trova nel pacchetto com.google.common. Questa configurazione indica a Maven di rinominare il pacchetto com.google.common in repackaged.com.google.common e di aggiornare tutti i riferimenti alle classi dal pacchetto originale.

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

Per eseguire la build:

mvn package

Note su pom.xml:

  • ManifestResourceTransformer elabora gli attributi nel file manifest dell'uber JAR (MANIFEST.MF). Il manifest può anche specificare il punto di ingresso per l'applicazione.
  • L'ambito di Spark è provided, poiché Spark è installato su Managed Service for Apache Spark.
  • Specifica la versione di Spark installata sul cluster Managed Service for Apache Spark (consulta l'elenco delle versioni di Managed Service for Apache Spark). Se la tua applicazione richiede una versione di Spark diversa da quella installata sul cluster Managed Service for Apache Spark, puoi scrivere un'azione di inizializzazione o creare un'immagine personalizzata che installa la versione di Spark utilizzata dalla tua applicazione.
  • La voce <filters> esclude i file di firma dalle directory delle dipendenze META-INF. Senza questa voce, può verificarsi un'eccezione di runtime java.lang.SecurityException: Invalid signature file digest for Manifest main attributes perché i file di firma non sono validi nel contesto dell'uber JAR.
  • Potrebbe essere necessario ombreggiare più librerie. Per farlo, includi più percorsi. L'esempio successivo ombreggia le librerie Guava e Protobuf.
    <relocation>
      <pattern>com</pattern>
      <shadedPattern>repackaged.com</shadedPattern>
      <includes>
        <include>com.google.protobuf.**</include>
        <include>com.google.common.**</include>
      </includes>
    </relocation>

Crea un uber JAR ombreggiato con SBT

SBT è uno strumento per la creazione di applicazioni Scala. Per creare un JAR ombreggiato con SBT, aggiungi sbt-assembly

al plug-in nella definizione della build, prima creando un file denominato assembly.sbt nella directory project/:

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

… quindi aggiungendo la seguente riga in assembly.sbt:

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

Di seguito è riportato un file di configurazione build.sbt di esempio che ombreggia la libreria Guava, che si trova in 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
)

Per eseguire la build:

sbt assembly

Note su build.sbt:

  • La regola di ombreggiatura nell'esempio precedente potrebbe non risolvere tutti i conflitti di dipendenza perché SBT utilizza strategie di risoluzione dei conflitti rigorose. Pertanto, potresti dover fornire regole più granulari che uniscano esplicitamente tipi specifici di file in conflitto utilizzando le strategie MergeStrategy.first, last, concat, filterDistinctLines, rename o discard. Per ulteriori dettagli, consulta la strategia di unione di sbt-assembly.
  • Potrebbe essere necessario ombreggiare più librerie. Per farlo, includi più percorsi. L'esempio successivo ombreggia le librerie Guava e 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
    )

Invia l'uber JAR a Managed Service for Apache Spark

Dopo aver creato un file JAR uber ombreggiato che contiene l'applicazione Spark e le relative dipendenze, carica il file JAR su Cloud Storage e invia un job a Managed Service for Apache Spark (utilizzando il flag --jars in Google Cloud CLI, il campo File JAR nella console Cloud de Confiance , il campo API jarFileUris o specificando il file JAR ombreggiato come JAR dell'applicazione principale).

Passaggi successivi

  • Consulta spark-translate, un'applicazione Spark di esempio che contiene file di configurazione per Maven e SBT.
  • Scrivi ed esegui job Spark Scala su Managed Service for Apache Spark. Guida rapida per scoprire come scrivere ed eseguire job Spark Scala su un cluster Managed Service for Apache Spark.