Administra las dependencias de Java y Scala para Apache Spark

Las aplicaciones de Spark a menudo dependen de bibliotecas de terceros de Java o Scala. Estos son los enfoques recomendados para incluir estas dependencias cuando envías un trabajo 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 en 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'
    

Evita conflictos de dependencias

Los enfoques anteriores pueden fallar si las dependencias de la aplicación de Spark entran en conflicto con las dependencias de Hadoop. Este conflicto puede surgir porque Hadoop inyecta sus dependencias en la ruta de clase de la aplicación, por lo que esas dependencias tienen prioridad sobre las de la aplicación. Cuando ocurre un conflicto, se pueden generar errores como NoSuchMethodError.

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

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

  1. Crea un solo JAR que contenga el paquete de la aplicación y todas sus dependencias
  2. Reubicar los paquetes de dependencia en conflicto dentro del JAR uber para evitar que sus nombres de ruta de acceso entren en conflicto con los de los paquetes de dependencia de Hadoop. En lugar de modificar tu código, usa un complemento (se detalla a continuación) para realizar de forma automática esta reubicación (también conocida como “shading”) como parte del proceso de empaquetado

Crea un JAR uber con shading mediante Maven

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

El siguiente es un ejemplo de archivo de configuración pom.xml que aplica shading en la biblioteca de Guava, que se encuentra en el paquete com.google.common: Esta configuración le 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>

Usa el siguiente comando para ejecutar la compilación:

mvn package

Notas sobre pom.xml:

  • ManifestResourceTransformer procesa los atributos en el archivo de manifiesto del JAR uber (MANIFEST.MF). El manifiesto también puede especificar el punto de entrada para tu aplicación.
  • El permiso de Spark es provided, ya que Spark está instalado en Dataproc.
  • Especifica la versión de Spark que se instala 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 construir 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 META-INF de tus dependencias. Sin esta entrada, puede ocurrir una excepción de tiempo de ejecución 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 JAR uber.
  • Es posible que debas aplicar shading en varias bibliotecas. Para ello, debes incluir múltiples rutas de acceso. En el siguiente ejemplo, se aplica shading en las bibliotecas de Guava y Protobuf.
    <relocation>
      <pattern>com</pattern>
      <shadedPattern>repackaged.com</shadedPattern>
      <includes>
        <include>com.google.protobuf.**</include>
        <include>com.google.common.**</include>
      </includes>
    </relocation>
    

Crea un JAR uber con shading mediante SBT

SBT es una herramienta para compilar aplicaciones de Scala. Para crear un JAR con shading con SBT, agrega el complemento sbt-assembly a tu definición de compilación antes de crear un archivo llamado assembly.sbt en el directorio project/:

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

... y, luego, agrega la siguiente línea en assembly.sbt:

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

El siguiente es un ejemplo de archivo de configuración build.sbt que aplica shading en la biblioteca de Guava, que se encuentra en el 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
)

Usa el siguiente comando para ejecutar la compilación:

sbt assembly

Notas sobre build.sbt:

  • Es posible que la regla de shading del ejemplo anterior no resuelva todos los conflictos de dependencias porque SBT usa estrategias de resolución de conflictos estrictas. Por lo tanto, es posible que debas proporcionar reglas más detalladas que combinen de manera explícita tipos específicos de archivos en conflicto mediante MergeStrategy.first, last, concat, filterDistinctLines, rename, o discard estrategias. Consulta la estrategia de combinación de sbt-assembly para obtener más detalles.
  • Es posible que debas aplicar shading en varias bibliotecas. Para ello, debes incluir múltiples rutas de acceso. En el siguiente ejemplo, se aplica shading en las bibliotecas de 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
    )
    

Envía el JAR uber a Dataproc

Después de crear un JAR uber con shading que contenga tus aplicaciones de Spark y sus dependencias, puedes enviar un trabajo a Dataproc.

¿Qué sigue?

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