Gerenciar dependências Java e Scala para o Apache Spark

Os aplicativos Spark geralmente dependem de bibliotecas Java ou Scala de terceiros. Veja a seguir as abordagens recomendadas para incluir essas dependências quando você envia um job do Spark para um cluster do Dataproc:

  1. Ao enviar um job da máquina local com o comando gcloud dataproc jobs submit, use a sinalização --properties spark.jars.packages=[DEPENDENCIES].

    Examplo:

    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. Ao enviar um job diretamente no cluster, use o comando spark-submit com o parâmetro --packages=[DEPENDENCIES].

    Examplo:

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

Como evitar conflitos de dependência

As abordagens acima podem falhar se as dependências do aplicativo Spark entrarem em conflito com as dependências do Hadoop. Esse conflito pode surgir porque o Hadoop injeta suas dependências no classpath do aplicativo de forma que as dependências dele têm precedência sobre as dependências do aplicativo. Quando ocorre um conflito, NoSuchMethodError ou outros erros podem ser gerados.

Exemplo:
Guava é a biblioteca principal do Google para Java que é usada por muitas bibliotecas e frameworks, incluindo o Hadoop. Um conflito de dependência pode ocorrer se um job ou as dependências desse job exigirem uma versão do Guava que seja mais recente do que a utilizada pelo Hadoop.

O Hadoop v3.0 resolveu esse problema, mas os aplicativos que dependem de versões anteriores do Hadoop exigem a seguinte solução alternativa com duas partes para evitar possíveis conflitos de dependência.

  1. Crie um único JAR que contenha o pacote do aplicativo e todas as suas dependências.
  2. Realoque os pacotes de dependência conflitantes dentro do JAR uber para impedir que seus nomes de caminho conflitem com os dos pacotes de dependência do Hadoop. Em vez de modificar seu código, use um plug-in (veja abaixo) para executar automaticamente essa realocação (também conhecida como "sombreamento") como parte do processo de empacotamento.

Como criar um JAR uber sombreado com o Maven

O Maven é uma ferramenta de gerenciamento de pacotes para a criação de aplicativos Java. O plug-in Maven scala pode ser usado para criar aplicativos escritos em Scala, a linguagem usada pelos aplicativos Spark. O plug-in de sombreamento do Maven pode ser usado para criar um JAR sombreado.

Este é um exemplo de arquivo de configuração pom.xml que sombreia a biblioteca Guava, localizada no pacote com.google.common. Essa configuração instrui o Maven a renomear o pacote com.google.common como repackaged.com.google.common e a atualizar todas as referências às classes do pacote 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 executar a versão:

mvn package

Observações sobre pom.xml:

  • ManifestResourceTransformer processa atributos no arquivo de manifesto do JAR uber (MANIFEST.MF). O manifesto também pode especificar o ponto de entrada para seu aplicativo.
  • O escopo do Spark é provided, já que o Spark está instalado no Dataproc.
  • Especifique a versão do Spark instalada no cluster do Dataproc (consulte a Lista de versões do Dataproc). Se o aplicativo exigir uma versão do Spark diferente da versão instalada no cluster do Dataproc, escreva uma ação de inicialização ou crie uma imagem personalizada que instale a versão do Spark usada pelo aplicativo.
  • A entrada <filters> exclui arquivos de assinatura dos diretórios META-INF das dependências. Sem essa entrada, uma exceção de tempo de execução java.lang.SecurityException: Invalid signature file digest for Manifest main attributes pode ocorrer pelo fato dos arquivos de assinatura serem inválidos no contexto do JAR uber.
  • Pode ser necessário sombrear várias bibliotecas. Para fazer isso, inclua vários caminhos. O exemplo a seguir sombreia as bibliotecas Guava e Protobuf.
    <relocation>
      <pattern>com</pattern>
      <shadedPattern>repackaged.com</shadedPattern>
      <includes>
        <include>com.google.protobuf.**</include>
        <include>com.google.common.**</include>
      </includes>
    </relocation>
    

Como criar um JAR uber sombreado com o SBT

O SBT é uma ferramenta para criar aplicativos Scala. Para criar um JAR sombreado com o SBT, adicione o plug-in sbt-assembly à sua definição de compilação, criando primeiro um arquivo chamado assembly.sbt no diretório project/:

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

... e adicione esta linha em assembly.sbt:

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

Este é um exemplo de arquivo de configuração build.sbt que sombreia a biblioteca Guava, localizada no 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 executar a versão:

sbt assembly

Observações sobre build.sbt:

  • A regra de sombreamento no exemplo acima pode não resolver todos os conflitos de dependência, uma vez que o SBT usa estratégias restritas de resolução de conflitos. Portanto, talvez seja necessário fornecer regras mais granulares que mesclem explicitamente tipos específicos de arquivos conflitantes usando estratégias MergeStrategy.first, last, concat, filterDistinctLines, rename ou discard. Consulte a estratégia de mesclagem de sbt-assembly para mais detalhes.
  • Pode ser necessário sombrear várias bibliotecas. Para fazer isso, inclua vários caminhos. O exemplo a seguir sombreia as bibliotecas 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
    )
    

Como enviar o JAR uber para o Dataproc

Depois de criar um JAR uber sombreado que contenha os aplicativos do Spark e as respectivas dependências, estará tudo pronto para enviar um job ao Dataproc.

A seguir

  • Consulte spark-translate, uma amostra do aplicativo Spark que contém arquivos de configuração para o Maven e para o SBT.
  • Grave e execute jobs do Spark Scala no Dataproc. Guia de início rápido para aprender a escrever e executar jobs do Spark Scala em um cluster do Dataproc.