Gestisci le dipendenze Java e Scala per Apache Spark

Le applicazioni Spark spesso dipendono da librerie Java o Scala di terze parti. Ecco alcuni approcci consigliati per includere queste dipendenze quando invii un job Spark a un cluster Dataproc:

  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'
    

Evitare conflitti di dipendenze

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

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

Hadoop v3.0 ha 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 dipendenza.

  1. Creare un singolo JAR contenente il pacchetto dell'applicazione e tutte le sue dipendenze.
  2. Riposiziona i pacchetti delle dipendenze in conflitto all'interno dell'uber JAR 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 di seguito) per eseguire automaticamente questo trasferimento ("shading") nell'ambito del processo di pacchettizzazione.

Creazione di un 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. È possibile utilizzare il plug-in Maven shadow per creare un JAR ombreggiato.

Di seguito è riportato un file di configurazione pom.xml di esempio che oscura 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 di Uber JAR (MANIFEST.MF). Il file manifest può anche specificare il punto di ingresso per l'applicazione.
  • L'ambito di Spark è provided, poiché Spark è installato su Dataproc.
  • Specifica la versione di Spark installata sul tuo cluster Dataproc (vedi Elenco delle versioni di Dataproc). Se la tua applicazione richiede una versione Spark diversa da quella installata sul cluster Dataproc, puoi scrivere un'azione di inizializzazione o creare un'immagine personalizzata che installi la versione Spark utilizzata dalla tua applicazione.
  • La voce <filters> esclude i file di firma dalle directory META-INF delle dipendenze. Senza questa voce, potrebbe verificarsi un'eccezione java.lang.SecurityException: Invalid signature file digest for Manifest main attributes di runtime, perché i file di firma non sono validi nel contesto del tuo JAR Uber.
  • Potrebbe essere necessario sfumare più librerie. Per farlo, includi più percorsi. Il prossimo esempio mette in ombra 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>
    

Creazione di un JAR ombreggiato con SBT

SBT è uno strumento per la creazione di applicazioni Scala. Per creare un JAR ombreggiato con SBT, aggiungi il plug-in sbt-assembly alla definizione della build, creando innanzitutto un file denominato assembly.sbt nella directory project/:

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

... allora 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 oscura 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 ombreggiata nell'esempio precedente potrebbe non risolvere tutti i conflitti di dipendenza perché SBT utilizza rigide strategie di risoluzione dei conflitti. Pertanto, potresti dover fornire regole più granulari che uniscono 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 sfumare più librerie. Per farlo, includi più percorsi. Il prossimo esempio mette in ombra 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
    )
    

Invio di Uber JAR a Dataproc

Dopo aver creato un JAR ombreggiato che contiene le applicazioni Spark e le sue dipendenze, puoi inviare un job a Dataproc.

Passaggi successivi

  • Vedi spark-translate, un'applicazione Spark di esempio che contiene file di configurazione sia per Maven che per SBT.
  • Scrivi ed esegui job Spark Scala su Dataproc. Guida rapida per imparare a scrivere ed eseguire job Spark Scala su un cluster Dataproc.