Ir al contenido
  1. Posts/

Zarpando con Sail. Entorno limpio con Docker, Jupyter y RustRover

·8 mins
Rafael Fernandez
Autor
Rafael Fernandez
Matemáticas, programación y cosas de la vida

¡Muy buenas! Ahora que llega el veranito y con él el calor, apetece playa, mar, y cómo no… ¡un poquito de coding!

Si eres de los que prefieren la montaña en lugar de la costa, lo siento, pero este no es tu post. Hoy seguimos hablando de barcos, mar, y de Sail. En una entrada anterior ya lo presenté y conté por qué, desde mi punto de vista, tiene tanto futuro. En esta ocasión, voy a explicarte cómo montar un entorno con Jupyter para interactuar con Sail, tanto si quieres usarlo como usuario, como si te interesa colaborar como desarrollador.

zarpando-con-sail-entorno-limpio-con-docker-jupyter-y-rustrover-img-86.png

Los que me conocen saben que me gusta instalar lo mínimo posible. De hecho, evito a toda costa añadir cosas que, para mí, no forman parte real de mi workflow, o que simplemente considero demasiado ruido para tenerlas instaladas en mi máquina.

Por ejemplo: sí, trabajo con TypeScript, uso npm, pnpm y todo eso… pero jamás vas a verme instalando nada en local. Ni siquiera dejo que los packages se queden ahí ocupando espacio. Tengo Python instalado, sí, pero probablemente porque venía por defecto en Fedora. Aun así, no instalo paquetes globalmente. Todo lo hago usando Docker, devcontainers, o entornos virtuales.

Preparando el barco: setup mínimo con Docker
#

Voy a ir muy al grano, porque la documentación oficial ya explica bastante bien cómo tener un setup mínimo. Para empezar a usar Sail con Jupyter, yo utilizo Docker. Y lo que vamos a necesitar son, efectivamente, dos imágenes: una de Jupyter y otra de Sail.

Como Sail levanta un servicio al que debemos conectarnos usando Spark Connect (para versiones anteriores a Spark 4.0) o Spark Client (para versiones posteriores), necesitaremos una imagen de Jupyter que tenga instalado alguno de estos paquetes:

  • pyspark[connect]==3.5.0
  • pyspark[connect]==4.0.0
  • pyspark-client==4.0.0

Aunque podríamos partir de la imagen más ligera de Jupyter, jupyter/minimal-notebook, en este post vamos a usar jupyter/pyspark-notebook:latest, ya que ya viene con PySpark instalado. Y esto nos vendrá bien si queremos comparar Sail con PySpark tal cual, sin añadidos, cuando entremos en la parte de debuggear Sail.

Dockerfile para Jupyter con Spark Connect (jupyter-dockerfile)
#

FROM jupyter/pyspark-notebook:latest

USER root
RUN apt-get update && apt-get install -y netcat iputils-ping

# Instalar pyspark + Spark Connect
RUN pip install --upgrade pip && \
    pip install "grpcio>=1.48.1" grpcio-status "pyspark[connect]==3.5.5"

USER jovyan

Mientras que para Sail, solo nos hace falta un Dockerfile con Python:

Dockerfile con Sail (sail-dockerfile)
#

Para esta imagen me habría gustado usar algo más ligero, como Alpine. Sin embargo, por alguna razón, al instalar pysail con pip no termina de funcionar bien. Probablemente se deba a que ciertos binarios que vienen preinstalados en Debian no están disponibles en Alpine. Lo dejaré como deuda técnica para un próximo post.

Para arrancar Sail basta con ejecutar el comando sail spark server. Según la documentación, la configuración por defecto utiliza --ip 127.0.0.1 y --port 50051.

Pero hay un pequeño detalle: como estamos levantando todo el entorno con Docker, la IP 127.0.0.1 hace referencia al localhost dentro del contenedor. Si queremos que Sail esté accesible desde otros contenedores (o incluso desde el host), necesitamos cambiar esa IP a 0.0.0.0. Esa es la dirección que expone el servicio a cualquier interfaz de red, y es la que vamos a usar, ya que vamos a tener dos servicios corriendo dentro de un mismo docker-compose.

FROM python:3.11-bullseye
RUN pip install "pysail==0.3.1"
ENV RUST_LOG=info
CMD ["sail", "spark", "server", "--ip", "0.0.0.0", "--port", "50051"]

Y por último, vamos a unir todo en un único archivo docker-compose:

Armando la flota con Docker Compose (docker-compose.yml)
#

services:
  sail:
    build:
      context: .
      dockerfile: sail-dockerfile
    container_name: sail-server
    ports:
      - "50051:50051"
    environment:
      - RUST_LOG=info
    networks:
      - shared-net

  jupyter:
    build:
      context: .
      dockerfile: jupyter-dockerfile
    container_name: jupyter-notebook
    ports:
      - "8888:8888"
    depends_on:
      - sail
    environment:
      - JUPYTER_ENABLE_LAB=yes
    command: start-notebook.sh --NotebookApp.token='' --NotebookApp.password=''
    networks:
      - shared-net

networks:
  shared-net:
    driver: bridge

Ahora sí, con todo listo, solo queda levantar los contenedores con:

docker compose up --build

Y en cuestión de segundos tendrás corriendo ambos servicios:

  • Jupyter, accesible en el puerto 8888
  • Sail, escuchando en el puerto 50051

zarpando-con-sail-entorno-limpio-con-docker-jupyter-y-rustrover-img-87.png

Levando anclas: conectando Jupyter con Sail
#

Ahora que ya tenemos Jupyter levantado, vamos a conectarnos al servidor que expone Sail. Para ello, usaremos Spark Connect y crearemos una SparkSession apuntando al servidor remoto de Sail.

Cuando queremos conectar con un servidor remoto compatible con Spark Connect, debemos utilizar la API de SparkSession de la siguiente manera:

SparkSession.builder.remote("sc://<host>:<port>").getOrCreate()

En nuestro caso, como el servidor se encuentra dentro del contenedor llamado sail-server y escucha en el puerto 50051, la conexión quedaría así:

from pyspark.sql import SparkSession
from pyspark.sql.functions import col

spark = SparkSession.builder.remote("sc://sail-server:50051").getOrCreate()

spark.sql("SELECT 1 + 1").show(truncate=False)

listo! Con eso ya estarías ejecutando consultas sobre Sail directamente desde Jupyter. Bastante sencillo, ¿no?

De usuario a contributor: setup para colaborar
#

Hasta ahora hemos visto cómo interactuar directamente con Sail desde un Jupyter Notebook. Ahora vamos a ver cómo este mismo setup puede servirnos para agilizar nuestro workflow de desarrollo, especialmente cuando estamos en pleno proceso de implementación o debugging. A este enfoque lo voy a llamar “modo desarrollo”, o simplemente develop mode, para abreviar.

zarpando-con-sail-entorno-limpio-con-docker-jupyter-y-rustrover-img-88.png

Entrar en develop mode implica seguir una serie de pasos cada vez que quieras trabajar en este contexto virtualizado (ya que no tenemos nada instalado directamente en el sistema). Sin embargo, hay otros pasos que solo deben hacerse una vez, al crear el entorno por primera vez. En este post, te indicaré claramente cuáles son de un solo uso y cuáles se repiten.

Mi entorno para trabajar con Rust actualmente es RustRover, del equipo de JetBrains. Me gusta esta opción porque ofrece un depurador bastante sólido (similar al de IntelliJ), lo que me permite analizar el código línea por línea sin complicaciones.

Aunque la documentación oficial ya explica cómo montar un entorno de desarrollo para colaborar con Sail, aquí voy a aportar una alternativa que me ha funcionado muy bien para debuggear usando RustRover, Jupyter y, como siempre, sin instalar ni una sola dependencia de Python directamente en mi sistema.

Requisitos
#

  • Java 17. Yo uso sdkman, que me permite gestionar versiones fácilmente:
sdk install java 17.0.15-amzn
  • Python 3.11 con:
    • hatch
    • maturin

En mi caso, como trabajo también con Scala y Rust, tengo estas herramientas instaladas globalmente. Y por supuesto, necesitas tener el proyecto forkeado y clonado localmente:

git clone git@github.com:<my-user>/sail.git # Forked Sail project

Entornos virtuales con venv
#

No vamos a instalar maturin ni hatch globalmente. En su lugar, crearemos un entorno virtual con Python y trabajaremos desde ahí.

Recuerda: algunas tareas solo se hacen una vez, al crear el entorno, y otras se repiten cada vez que entras en modo desarrollo.

Dentro del directorio del proyecto Sail:

1. Crear el entorno virtual

Recuerda que es muy importante usar la version de Python 3.11

python3.11 -m venv .venvs/test-spark.3.5

2. Activar el entorno virtual

source .venvs/test-spark.3.5/bin/activate

3. Instalar las dependencias

pip install --upgrade pip
pip install maturin hatch patchelf

4. Build del proyecto

cargo +nightly fmt && \
  cargo clippy --all-targets --all-features && \
  cargo build && \
  env SAIL_UPDATE_GOLD_DATA=1 cargo test

5. Formatear y preparar el entorno

hatch fmt
hatch run maturin develop

6. Clonar repositorios de Spark e Ibis (solo la primera vez)

git clone git@github.com:apache/spark.git opt/spark
git clone git@github.com:ibis-project/testing-data.git opt/ibis-testing-data

7. Compilar Spark (solo la primera vez)

Este paso puede tardar unos 20 minutos, dependiendo de tu máquina. Consume bastantes recursos, así que aprovecha para tomarte un break:

env SPARK_VERSION=3.5.5 scripts/spark-tests/build-pyspark.sh

Navegando a fondo: debug en local con RustRover #

Para empezar a debuggear Sail, primero necesitamos configurar un proceso de ejecución de cargo, tal como se explica en la documentación oficial: Using the Rust Debugger in RustRover .

Una vez configurado, ejecutamos en modo debug y esperamos a que se levante el servidor de Sail:

zarpando-con-sail-entorno-limpio-con-docker-jupyter-y-rustrover-img-89.png

zarpando-con-sail-entorno-limpio-con-docker-jupyter-y-rustrover-img-90.png

Opción 1: Usar CLI de PySpark
#

La forma más directa de interactuar con Sail mientras debuggeamos es, como sugiere la documentación, instalando PySpark en el entorno virtual y lanzando el CLI:

hatch run test-spark.spark-3.5.5:install-pyspark
hatch run pyspark

Desde ahí, podemos escribir comandos directamente en la terminal de PySpark mientras observamos lo que sucede en RustRover.

Opción 2: Usar Jupyter
#

La alternativa más cómoda (y visual) es usar Jupyter, como ya vimos en las primeras secciones. La única diferencia ahora es que, como Sail ya está corriendo en modo debug desde RustRover, no necesitamos levantar el contenedor de Sail.

Entonces, reutilizamos el docker-compose.yml inicial, pero eliminamos el servicio de sail y cambiamos el modo de red a host, de esta forma:

services:
  jupyter:
    build:
      context: .
      dockerfile: jupyter-dockerfile
    container_name: jupyter-notebook
    ports:
      - "8888:8888"
    environment:
      - JUPYTER_ENABLE_LAB=yes
    network_mode: host
    command: start-notebook.sh --NotebookApp.token='' --NotebookApp.password=''

Levantamos el contenedor:

docker compose up --build

Y ahora sí: tenemos nuestro Jupyter Notebook disponible en localhost:8888.

Como el servidor de Sail ya está corriendo localmente (desde RustRover), simplemente creamos la SparkSession apuntando a localhost:50051 y ejecutamos nuestros comandos directamente desde el notebook:

from pyspark.sql import SparkSession
from pyspark.sql.functions import col

spark = SparkSession.builder.remote("sc://localhost:50051").getOrCreate()

spark.sql("SELECT 1 + 1").show(truncate=False)

Cuaderno de Bitácoras. Conclusión
#

Con este post hemos visto cómo montar un entorno ligero, reproducible y flexible para trabajar con Sail, ya sea como usuario o como colaborador del proyecto. Desde levantar servicios con Docker, conectar con Jupyter usando Spark Connect, hasta debuggear en local con RustRover, todo sin necesidad de instalar dependencias directamente en tu máquina.

Este tipo de setup no solo ayuda a mantener tu entorno limpio, sino que además facilita colaborar en proyectos complejos sin pelearte con configuraciones locales, versiones o conflictos de dependencias.

Como comenté al principio, a mí me gusta tener un workflow lo más limpio y aislado posible, y este enfoque me ha permitido mantener esa filosofía sin renunciar a productividad ni capacidad de análisis.

¿Y ahora qué?
#

Si llegaste hasta aquí, ¡gracias por leer! Espero que este post te haya sido útil, tanto si querías echarle un vistazo a Sail como si estás pensando en contribuir al proyecto.

Si tienes preguntas, ideas o simplemente quieres compartir tu propio setup, los comentarios están abiertos. Y si quieres más posts técnicos como este, ya sabes: comparte o sígueme por donde más te guste.

¡Nos leemos en el siguiente post!