Gestionar las dependencias de Java y Scala para Apache Spark

Las aplicaciones de Spark suelen depender de bibliotecas de Java o Scala de terceros. A continuación, se indican los enfoques recomendados para incluir estas dependencias al enviar una tarea de Spark a un clúster de Dataproc:

  1. Cuando envíes un trabajo desde tu máquina local con el comando gcloud dataproc jobs submit, usa la marca --properties spark.jars.packages=[DEPENDENCIES].

    Ejemplo:

    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. Cuando envíes un trabajo directamente a tu clúster, usa el comando spark-submit con el parámetro --packages=[DEPENDENCIES].

    Ejemplo:

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

Evitar conflictos de dependencias

Los enfoques anteriores pueden fallar si las dependencias de la aplicación Spark entran en conflicto con las de Hadoop. Este conflicto puede surgir porque Hadoop inserta sus dependencias en la ruta de clases de la aplicación, por lo que sus dependencias tienen prioridad sobre las de la aplicación. Cuando se produce un conflicto, se pueden generar NoSuchMethodError u otros errores.

Ejemplo:
Guava es la biblioteca principal de Google para Java que usan muchas bibliotecas y frameworks, incluido Hadoop. Puede producirse un conflicto de dependencias si un trabajo o sus dependencias requieren una versión de Guava más reciente que la que usa Hadoop.

Hadoop v3.0 resolvió este problema , pero las aplicaciones que dependen de versiones anteriores de Hadoop requieren la siguiente solución provisional de dos partes para evitar posibles conflictos de dependencias.

  1. Crea un único archivo JAR que contenga el paquete de la aplicación y todas sus dependencias.
  2. Reubica los paquetes de dependencias conflictivos en el uber JAR para evitar que sus nombres de ruta entren en conflicto con los de los paquetes de dependencias de Hadoop. En lugar de modificar el código, usa un complemento (consulta la información que se indica más abajo) para realizar automáticamente esta reubicación (también conocida como "shading") como parte del proceso de empaquetado.

Crear un archivo JAR uber sombreado con Maven

Maven es una herramienta de gestión de paquetes para crear aplicaciones Java. El complemento Maven scala se puede usar para compilar aplicaciones escritas en Scala, el lenguaje que usan las aplicaciones de Spark. El complemento Maven shade se puede usar para crear un archivo JAR sombreado.

A continuación, se muestra un ejemplo de archivo de configuración de pom.xml que sombrea la biblioteca Guava, que se encuentra en el paquete com.google.common. Esta configuración indica a Maven que cambie el nombre del paquete com.google.common a repackaged.com.google.common y que actualice todas las referencias a las clases del paquete original.

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

Para ejecutar la compilación:

mvn package

Notas sobre pom.xml:

  • ManifestResourceTransformer procesa los atributos del archivo de manifiesto del uber JAR (MANIFEST.MF). El manifiesto también puede especificar el punto de entrada de tu aplicación.
  • El ámbito de Spark es provided, ya que Spark está instalado en Dataproc.
  • Especifica la versión de Spark que está instalada en tu clúster de Dataproc (consulta la lista de versiones de Dataproc). Si tu aplicación requiere una versión de Spark diferente de la que está instalada en tu clúster de Dataproc, puedes escribir una acción de inicialización o crear una imagen personalizada que instale la versión de Spark que usa tu aplicación.
  • La entrada <filters> excluye los archivos de firma de los directorios de tus dependencias.META-INF Sin esta entrada, se puede producir una excepción de tiempo de ejecución de java.lang.SecurityException: Invalid signature file digest for Manifest main attributes porque los archivos de firma no son válidos en el contexto de tu uber JAR.
  • Es posible que tengas que sombrear varias bibliotecas. Para ello, incluya varias rutas. En el siguiente ejemplo se sombrean las bibliotecas Guava y Protobuf.
    <relocation>
      <pattern>com</pattern>
      <shadedPattern>repackaged.com</shadedPattern>
      <includes>
        <include>com.google.protobuf.**</include>
        <include>com.google.common.**</include>
      </includes>
    </relocation>

Crear un archivo JAR uber sombreado con SBT

SBT es una herramienta para crear aplicaciones Scala. Para crear un archivo JAR sombreado con SBT, añade el complemento sbt-assembly a tu definición de compilación. Para ello, primero crea un archivo llamado assembly.sbt en el directorio project/:

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

... y, a continuación, añade la siguiente línea en assembly.sbt:

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

A continuación, se muestra un ejemplo de archivo de configuración de build.sbt que sombrea la biblioteca Guava, que se encuentra en 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
)

Para ejecutar la compilación:

sbt assembly

Notas sobre build.sbt:

  • Es posible que la regla de sombreado del ejemplo anterior no resuelva todos los conflictos de dependencias, ya que SBT usa estrategias estrictas de resolución de conflictos. Por lo tanto, es posible que tengas que proporcionar reglas más específicas que combinen explícitamente determinados tipos de archivos en conflicto mediante las estrategias MergeStrategy.first, last, concat, filterDistinctLines, rename o discard. Consulta la estrategia de combinación de sbt-assembly para obtener más información.
  • Es posible que tengas que sombrear varias bibliotecas. Para ello, incluya varias rutas. En el siguiente ejemplo se sombrean las bibliotecas Guava y 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
    )

Enviar el archivo JAR uber a Dataproc

Una vez que hayas creado un uber JAR sombreado que contenga tus aplicaciones de Spark y sus dependencias, podrás enviar una tarea a Dataproc.

Siguientes pasos

  • Consulta spark-translate, una aplicación de Spark de ejemplo que contiene archivos de configuración para Maven y SBT.
  • Escribe y ejecuta tareas de Scala en Spark en Dataproc. guía de inicio rápido para aprender a escribir y ejecutar tareas de Scala en Spark en un clúster de Dataproc.