Quando usamos o Spark em ferramentas gerenciadas como o Databricks, muito que precisamos é fornecido com facilidade pela plataforma, e mesmo quando o cenário não nos favorece, podemos instalar bibliotecas com poucos passos, seguindo a documentação da plataforma.

Já em casos que precisamos ter maior controle sobre todo o ambiente de execução, seja por questões de custo, ou por limitações - quem sabe a falta de um contrato com o Databricks ou ferramentas semelhantes - instalar bibliotecas, sejam elas Jars adicionais para o funcionamento do Spark, ou libs Python, para organizar o funcionamento do código, simplicidade é uma palavra que não se aplica.

Neste artigo, vou detalhar um problema muito específico que passei ao utilizar o PySpark num ambiente Kubernetes. Meu código me levou a utilizar do pandas para fazer operações com map, o que me trouxe alguns problemas de dependências, e espero documentar e ajudar quem passar por algo semelhante.

Como é criado o ambiente de execução do Pyspark

O meio mais recomendado que temos para utilizar o Spark a partir de um ambiente virtual é compilando os dockerfiles presentes, ficando como alternativa, e mais omisso na documentação da ferramenta o uso das imagens específicas para cada versão e linguagem-alvo (sendo nosso caso o Python).

Depender do repositório do Spark exige amplo conhecimento sobre a organização do código, além da carga de manter o clone/fork do repo, se preocupar com as atualizações de versões, manter imagens com multi-layer, dentre outros problemas.

A decisão mais assertiva é tentar utilizar as imagens existentes, porém voltando para uma questão: como podemos instalar nossas bibliotecas? A documentação nos trás um pouco sobre dependency management, mas novamente falha, por não entrar em detalhes.

Uma precaução

Casos de uso de funções de integração do Spark com Pandas, que não são incomuns, exigem a instalação do Numpy, Pandas e PyArrow. Estes que ao serem instaladas com o pip, acabam se integrando com algumas outras bibliotecas em C! Em seguida vamos ver como evitar problemas dessa categoria.

Maneiras de instalar as bibliotecas

Vamos analisar os trade-offs dos cenários possíveis.

Spark submit

Instalar via spark submit pode parecer simples, mas tem algumas desvantagens, vamos a um exemplo:

spark-submit.sh --py-files libs.zip

Se torna necessário criar um arquivo comprimido com as libs que serão interpretadas durante execução. Dessa maneira ganhamos com a flexibilidade de manter as dependências em tempo de execução do cluster, mas não há muitas garantias quando se necessita da instalação nativa de libs C.

Dockerfile + Multilayer

ARG SITE_PACKAGES=/opt/spark/work-dir
FROM python:3.7-slim AS compile-image
ARG SITE_PACKAGES
ARG INDEX_URL
RUN mkdir -p ${SITE_PACKAGES}
RUN pip3 install --upgrade pip
RUN pip3 install --prefix=${SITE_PACKAGES} numpy

FROM apache/spark-py:v3.4.1
ARG SITE_PACKAGES
COPY --from=compile-image ${SITE_PACKAGES}/lib/python3.7/site-packages /usr/lib/python3/dist-packages
WORKDIR /opt/spark/work-dir
ENV PYTHONPATH "${PYTHONPATH}:/opt/spark/work-dir"

Nesse segundo meio conseguimos instalar nossas dependências previamente à execução do cluster, como também denominar nossas dependências linha a linha, ou ainda realizando um RUN pip3 install --prefix=${SITE_PACKAGES} -r requirements.txt com a referência de um arquivo requirements.txt.

Essa solução configura as dependências usando pip, mas como as obtém via um docker layer distinto, o problema antecedente persiste - vale lembrar que a imagem apache/spark-py não disponibiliza o pip com o usuário padrão, seguimos então à solução ideal…

Dockerfile

FROM apache/spark-py:v3.4.1
COPY src /opt/spark/work-dir/src

USER 0
RUN pip3 install --upgrade pip
COPY requirements.txt .
RUN pip3 install -r requirements.txt

USER 185
WORKDIR /opt/spark/work-dir
ENV PYTHONPATH "${PYTHONPATH}:/opt/spark/work-dir"

Aqui fazemos a troca do usuário para o 0 (sudo) que nos permite o uso do pip em uma única layer, tomando cuidado para retornar ao usuário padrão de execução ao fim da operação. Assim instalamos o que for necessário da maneira mais nativa possível, sem depender diretamente do repositório oficial do spark, evitando os problemas aqui listados, além de que podemos reutilizar a primeira proposta para instalar dependências no contexto de execução de um Spark App.

Referências

https://github.com/apache/spark/blob/v3.4.0/resource-managers/kubernetes/docker/src/main/dockerfiles/spark/bindings/python/Dockerfile

https://spark.apache.org/docs/latest/running-on-kubernetes.html#docker-images

https://spark.apache.org/docs/latest/api/python/user_guide/python_packaging.html