Published

El mapa del cómputo — Spark, el cluster y la paridad

Connect any source, model it as an ontology, transform it, and operationalize it, analytics, automation and machine learning, under one governed, self-hostable roof. --- Most teams stitch the...

El mapa del cómputo — Spark, el cluster y la paridad

Fecha: 2026-08-10. Documento de PERSPECTIVA, no de fase: contesta «¿cómo llega
una query desde el editor hasta el Parquet?», «¿dónde somos débiles frente a los
líderes?» y «¿es Rubix la referencia correcta para expandir Spark?».

Manda sustrato-04.md sobre los hechos; esto ordena y mira lejos.

Regla de siempre: lo medido lleva su comando; lo demás va marcado como opinión.


1 · El pipeline completo — de la tecla al Parquet

  ① NAVEGADOR · CarbonSqlEditor.tsx
     el usuario escribe SQL · Monaco (pgsql) · Idempotency-Key por intento
                    │  POST /api/sql-editor/execute
  ② SUPERFICIE · app/api/sql-editor/execute  (Vercel · Next.js)
     Clerk da el principal · gramática de catálogo (¿es DDL curado?)
     clasifica lectura/escritura · DECLARA el destino (no lo parsea)
                    │  runQuery(sql, principal, { writeTarget })
  ③ ⭐ LA PUERTA · lib/compute/junction.ts   ← la política vive aquí
     catálogo lógico → plan físico → TENENCIA TABLA POR TABLA
     elige motor · abre la txn del LEDGER (durable, ANTES de ejecutar)
                    │  HTTPS + JWT (HS256, TTL 120 s)
  ④ EL PUENTE · services/spark-bridge  (GKE, 1 réplica)
     HTTP entra, gRPC sale · POOL DE SESIONES CALIENTES por inquilino
     (LRU 8 · 30 min · AuthManager que refresca el token en 2º plano)
                    │  gRPC Spark Connect
  ⑤ EL MOTOR · carbon-connect-server-0 + 2 executors  (Spark 4.1.2)
     parsea, planifica, ejecuta · para leer/escribir pide la tabla al catálogo
                    │  Iceberg REST  ──►  CATALOG_URI=app.paladio.io/api/iceberg
  ⑥ ⭐ LA CARA · app/api/iceberg/v1/[...path]  ← el catálogo GOBERNADO
     autoriza (Index) · comprueba el prefijo del inquilino · resuelve
     `diets` ↔ `ds_<uuid>` · FIRMA el commit · acuña credencial acotada
  ⑦ LAKEKEEPER (Railway) → STS 900 s → ⑧ CLOUDFLARE R2 (Parquet + metadata)
                    └──► de vuelta: ⑥ anota `access_events` · ③ cierra el ledger

Lo que hace especial a este camino, en una frase por pieza:

la puertaes la única que sabe quién pregunta y qué puede tocar. El destino se declara, no se parsea
el puenteexiste porque Spark Connect no tiene cliente Node, y su trabajo real no es traducir: es sostener sesiones calientes (primera query 5.832 ms · p50 caliente 125 ms · 47×)
la carahace dos cosas que el motor no puede: resolver nombres lógicos y firmar el commit. Hechas aquí, valen para todos los motores a la vez

Dos puertas y una cintura. La puerta del producto (③) y la puerta de los motores (⑥); entre ellas, todo lo que es dato pasa por Iceberg + Lakekeeper.

El viaje del UPDATE main.default.diets que ejecutaste

txn 7f55f64c  UPDATE committed  row_count=5  ventana 10.868 ms
   engine=spark · attributable=true · attribution=exclusive
   write_target=main.default.`diets`
   landed=true · snapshot=8478955347166888000     ⇒ RECONCILIABLE
   snapshots 1 → 2 · filas 5 → 5

Y lo único que NO es correcto: rowsAffected: 0. Afectó 5. Es un defecto conocido y acotado: sparkEngine.runQuery no devuelve commit, y la puerta hace r.commit?.rows ?? 0. Spark no devuelve filas afectadas en un DML por SQL, así que el arreglo honesto no es inventar el número: es devolver null y que la UI muestre «—». Un 0 dice «no afectó nada», que es falso; un dice «el motor no lo reporta», que es cierto. El total sí lo sabemos (el ledger apunta row_count=5, preguntado al catálogo).


2 · ⭐⭐⭐ ¿Es Rubix la referencia? Sí — y dice lo contrario de lo que parece

La intuición es buena y la fuente lo confirma… pero invierte la conclusión.

Rubix nació en 2017 como «un motor de scheduling y ejecución seguro, escalable e
inteligente para Spark y otros frameworks de cómputo distribuido»
, y Foundry
corre sobre «una malla de cómputo basada en Kubernetes, autoescalada y endurecida
(Palantir Rubix)»
.

Rubix es la MALLA, no el motor. Lo que Palantir centralizó fue dónde y cómo se ejecuta, no con qué. Y la prueba está en su propia guía de elección de motor:

Lo que hace Foundry
@lightweight con Polars«tu opción por defecto para transforms de producción»
Spark«la única opción cuando el dato excede la capacidad de un solo nodo» — el umbral que citan es ~10 M filas
DuckDB / Polars«a menudo la opción más simple y rápida» para cargas medianas
Flinkstreaming

Y lo dicen sin rodeos: «Spark tiene más overhead para operaciones pequeñas».

⇒ Lo que esto significa para nosotros

La arquitectura que ya tenemos es la de Rubix, y está escrita en el sustrato desde hace dos días: «el cómputo es PLURAL y desechable; los motores entran y salen por un registro; ninguno es la arquitectura» + «la cintura estrecha es el catálogo».

«Expandir Spark como motor único» sería alejarnos de la referencia, no acercarnos. Un SELECT * FROM diets LIMIT 10 sobre 5 filas tarda 6-15 s por Spark. Esa misma query por un motor de un nodo tarda milisegundos. Foundry no lo resolvió metiendo todo en Spark: lo resolvió eligiendo motor por tamaño, sobre una malla común.

Lo que sí hay que centralizar —y es lo que Rubix enseña— son tres cosas, ninguna de ellas «el motor»:

  1. El sustrato de ejecución (Kubernetes): un sitio donde todo cómputo corre, se autoescala y se apaga. Hoy tenemos el cluster, pero sólo Spark vive en él — el resto (duck-server, ml-runner, warehouse-writer) está repartido en Railway.
  2. La cintura (catálogo + formato): ya la tenemos, y es la pieza más madura.
  3. La elección de motor como POLÍTICA declarada, no como accidente. Hoy elegirMotorDeLectura devuelve spark siempre. Eso fue correcto para cerrar el fallback silencioso, y es insuficiente como destino.

2·bis · ⭐⭐⭐ Junction ≈ Furnace — correcto en la INTENCIÓN, falso en el MECANISMO

La memoria del repo dice «Junction = el Furnace de Carbon». Investigado: Furnace existe y es el motor de SQL warehousing de Foundry. La equivalencia es real… y la diferencia es exactamente donde nos duele.

«Furnace's architecture decouples query parsing and planning from query
execution
. All SQL queries are parsed and planned using a unified engine-agnostic
layer
that leverages Apache Calcite to parse SQL into an AST, validate it, and
optimize the query, regardless of the eventual compute engine

«engineered from first principles to support multiple compute engines behind a
common syntax
»
· expuesto por Arrow Flight SQL.

FurnaceJunction
Una puerta, motores plurales detrás
Formato común (Iceberg)
Gobierno en un puntoy más fuerte (la cara firma y resuelve nombres)
Parseo y planificaciónunificados y agnósticos — un AST validado y optimizadono hay: se manipula TEXTO con regex y se despacha SQL
Dialectouno solo; el motor recibe un planel del motor ⇒ «common syntax» no existe
InterfazArrow Flight SQL (estándar, lo consumen BI y terceros)HTTP propio + JWT

⇒ Y esto explica, de golpe, TODOS los fallos de dialecto de esta semana

No fueron descuidos independientes: son el mismo fallo estructural, seis veces.

· `WITH … CREATE TABLE …`        → ParseException  (el prólogo, mal colocado)
· destino cualificado DOS veces  → main.default.main.default.diets
· el CTE homónimo capturaba el destino de un DELETE  (silencioso)
· `qualifyWriteTarget` anclado a `^` no veía el prólogo → devolvía el SQL intacto
· 2 divergencias SILENCIOSAS del corpus de lectura (16/25)
· `INSERT … SELECT` fuera, porque «la fuente no se resuelve»

⭐⭐ Todos salen de tratar el SQL como texto. Y la consecuencia importante es que ya tenemos un parser —accidental, disperso y por regex— repartido en sql-parse.ts (locateDmlCommand, extractDmlTarget, splitCtas, extractCteNames, rewriteDottedFromRefs), duck-plan.ts, qualifyWriteTarget, dialect-divergence.ts y equivalence.ts. No es que falte un parser: es que hay uno malo, en diez sitios.

⭐ Y el repo YA SABE cuál es la respuesta — la aplicó a la mitad

catalog-sql/grammar.ts (M3) hizo exactamente este movimiento para los comandos de catálogo, y lo dice en su cabecera:

«LA GRAMÁTICA DE CATÁLOGO — declarada como DATOS, con su esquema de salida […]
tres propiedades que no tenía la cadena de 10 regex que sustituye: la gramática
es una tabla, no código […] cada comando declara su output»

La dirección no hay que inventarla: hay que terminarla. M3 llevó a datos la gramática de catálogo (SHOW, DESCRIBE, information_schema); el camino de datos (SELECT/DML) sigue en regex. Ése es el hueco, y es el mismo que Furnace cerró con Calcite.

⚠️ Lo que NO propone esto: adoptar Calcite mañana. Es Java, y el planificador vive en Node. Lo que sí propone es cambiar la frontera: que lo que cruce hacia el motor deje de ser una cadena manipulada y pase a ser una estructura validada — empezando por lo que ya duele (el destino de escritura y la resolución de fuentes), no por reescribirlo todo.


2·ter · El sustrato vs la malla de Rubix — lo medido

Rubix es «a hardened, autoscaling, highly available implementation of Kubernetes»,
aloja todos los servicios core de Foundry/AIP/Apollo, y sus nodos «cannot live
longer than 48 hours»
, de modo que «every service is architected for disruption».
Además: «every workload is securely isolated» y «every interaction between
workloads must be authenticated, authorized, and logged»
.

Lo nuestro, medido hoy:

kubectl -n spark get pods -o wide
  carbon-connect-server-0        ─┐
  spark-connect-…-exec-1          ├─ LOS CUATRO EN EL MISMO NODO
  spark-connect-…-exec-2          │   gke-…-tnj7 · 4 vCPU · 16 GB · europe-west1-b
  spark-bridge-…                 ─┘
RubixNuestro cluster
BaseK8s endurecido, HAGKE, 1 nodo, autoscaling 1-3 con techo de cuota 12 vCPU
Ciclo de nodo≤ 48 h, efímero por diseño⛔ sin política de ciclo
Aislamiento entre cargaspor requisitos · auth+authz+log en cada interaccióndriver, executors y puente comparten nodo y namespace
Qué alojatodos los serviciossólo Spark — duck-server, ml-runner y warehouse-writer viven en Railway
Resilienciaarquitectura «para la disrupción»⚠️ 1 réplica del puente con sesiones en proceso: si cae, se pierden

No tenemos una malla: tenemos un nodo con cuatro pods. Y eso está bien para donde estamos —sirve el editor entero— pero no es lo que la palabra «cluster» sugiere, y conviene no confundirse al planificar.

⚠️ Detalle que delata la historia: el cluster se llama trino-compute-clone, por un motor demolido hace dos días.


3 · Dónde somos débiles — medido, no supuesto

3·1 · Dialecto y semántica

EstadoRiesgo
Corpus de LECTURA🏁 medido: 16/25 · 7 ruidosas · 2 silenciosas (cortadas antes del motor)acotado
⚠️ Corpus de ESCRITURAnunca medido. El DML pasó de DuckDB SQL a Spark SQL sin ejercer un corpusel mayor — una divergencia silenciosa aquí corrompe datos, no resultados
⚠️ ANSI de Spark 4confirmado activo (spark.sql.ansi.enabled=true), el corpus no lo cubrecasts y overflow lanzan donde DuckDB devolvía null
rowsAffectedsiempre 0 con Sparkvisible al usuario, mina la confianza

La asimetría que ordena la prioridad: una divergencia de lectura da un número raro que alguien puede ver; una de escritura deja el dato mal y nadie mira.

3·2 · Profundidad analítica — lo que los líderes tienen y nosotros no

EllosNosotros
Vistas materializadasDatabricks y Snowflake, con refresco incremental⛔ no existen
Time travel en SQLSELECT … VERSION AS OF / TIMESTAMP AS OF⛔ los snapshots existen, el SQL no los expone
Streaming / tablas continuasStructured Streaming · Flink · streaming tables⛔ todo es batch
Mantenimiento automáticocompaction y expiración gestionadas144 tablas sin compactar ni expirar (B7)
Resultados grandesEXTERNAL_LINKS a object storagetodo inline (B3)
Geoespacial / vectorestipos nativos; Iceberg v3 trae geo, variant y row lineage⛔ nada
Concurrencia de servingwarehouses elásticos e independientes⚠️ 1 puente, 1 réplica, sesiones en proceso ⇒ sin afinidad no escala

3·3 · Sustrato

  • Cuota dura: CPUS_ALL_REGIONS = 12, subida denegada (cuenta de trial), y Spot también (PREEMPTIBLE_CPUS = 0). Ningún plan de escala es real hasta que eso cambie — es el techo de todo lo demás.
  • ⚠️ El cluster es regional con un node pool de una zona: paga tarifa regional sin beneficio (−$73/mes si se corrige).
  • ⚠️ BRIDGE_JWT_SECRET es simétrico y sin rotar — y desde W4 ese conducto también crea tablas.
  • ⚠️ 56 huérfanas · 20 fantasmas de deriva Index↔físico.

4 · El norte, en tres movimientos

⚠️ Esto es propuesta, no medición. Cada punto necesita su spec y sus gates.

M1 · Elegir motor por TAMAÑO, no por defecto — «el Rubix nuestro»

elegirMotorDeLectura pasa de devolver spark siempre a decidir con una política declarada y medible, que es exactamente lo que hace Foundry:

metadata de la tabla (filas · bytes · ficheros)  →  motor
    pequeño / interactivo   →  motor de un nodo   (ms)
    grande / distribuido    →  Spark              (segundos, pero escala)
    escritura gobernada     →  Spark              (hoy es el único que firma)

Y el dato para decidir ya lo tenemos: getTableStatus devuelve filas, bytes y número de ficheros sin escanear nada. La política puede leerse del catálogo.

Con una condición innegociable, y es la lección de esta semana: la elección debe ser ruidosa y observable ([junction] motor=…), y nunca un try/catch. El fallback silencioso ya nos costó días de servir DuckDB creyendo que era Spark.

⚠️ Y un requisito previo que no se puede saltar: el corpus diferencial de escritura. Volver a tener dos motores sin haber medido la divergencia de DML sería reabrir el agujero con conocimiento de causa.

M2 · Subir el resto del cómputo a la malla

Hoy sólo Spark vive en el cluster. duck-server, ml-runner y warehouse-writer están en Railway — es decir, hay dos sustratos de ejecución, y Rubix enseña que el valor está en tener uno. No es urgente; es la dirección.

M3 · Cerrar la paridad por orden de daño, no de vistosidad

⇒ Ver §7, que ordena esto junto con todo lo demás.


7 · ⭐ LAS PRIORIDADES, con la vista completa

El criterio es qué pasa si no se hace, no la dificultad ni lo vistoso. Y con la comparación contra Furnace y Rubix delante, el orden cambia respecto a la intuición.

P0 · Lo que ya está cobrando intereses

QuéPor qué primero
P0·1⭐⭐ El corpus diferencial de ESCRITURA (+ ANSI de Spark 4)Es el único punto donde un fallo corrompe el dato y nadie mira. Una divergencia de lectura da un número raro que alguien puede ver; una de escritura, no. Y bloquea M1: volver a tener dos motores sin haberlo medido sería reabrir el agujero a sabiendas
P0·2rowsAffected honesto (null → «—»)Horas de trabajo. Se ve cada vez que alguien escribe, y dice algo falso
P0·3Mantenimiento Iceberg (B7)144 tablas sin compactar ni expirar. ⚠️ No llega con la escala: llega con el TIEMPO. Cada día que pasa lo empeora sin que nadie haga nada

P1 · El cambio de punto de vista — la frontera deja de ser texto

QuéPor qué
P1·1⭐⭐⭐ Consolidar el parseo del camino de datos — terminar lo que M3 empezóSeis fallos de esta semana salen de aquí, y no eran independientes: eran el mismo. Furnace resolvió esto con Calcite; nosotros no necesitamos Calcite, necesitamos dejar de manipular cadenas. Empezar por lo que ya duele: destino de escritura y resolución de fuentes
P1·2Elección de motor por tamaño (el «Rubix nuestro»)Un SELECT sobre 5 filas tarda 6-15 s. El dato para decidir ya lo da getTableStatus sin escanear. ⛔ Depende de P0·1

P2 · Producto que aún no existe

EXTERNAL_LINKS para resultados grandes (B3) · time travel en SQL (los snapshots ya están, sólo falta exponerlos — es lo más barato de esta lista) · vistas materializadas · Iceberg v3 (geo, variant, row lineage).

P3 · Sustrato — importante, y no urgente hasta que se levante la cuota

Malla real (HA, ciclo de nodos, aislamiento entre cargas) · subir el resto del cómputo al cluster · renombrar trino-compute-clone · rotar BRIDGE_JWT_SECRET (⚠️ simétrico, y desde W4 ese conducto también crea tablas) · node pool zonal (−$73/mes).

CPUS_ALL_REGIONS = 12 con la subida denegada es el techo de todo P3. Mientras no cambie, «malla» es una palabra: no caben más nodos. Conviene atacarlo por el lado administrativo (salir del trial) antes que por el técnico.

⚠️ Y lo que NO está en ninguna prioridad, a propósito

«Expandir Spark a toda la plataforma». La investigación dice lo contrario: Foundry pone Polars/DuckDB por defecto y Spark «sólo cuando el dato excede un nodo». Lo que sí se expande es la malla (P3) y la puerta (P1) — que es exactamente lo que Rubix y Furnace son, cada uno en su capa.


5 · Lo que este documento NO decide

Qué motor de un nodoDuckDB está desplegado y probado, pero salió de la puerta a propósito (no pasa por la cara ⇒ no firma). Reincorporarlo exige resolver eso, no sólo enchufarlo
Iceberg v3geo, variant y row lineage cambian el formato. Depende de que Lakekeeper y Spark lo soporten
StreamingFoundry usa Flink. Aquí no hay caso de uso declarado todavía
La cuotamientras sean 12 vCPU denegadas, la escala es teórica

6 · Documentos

DocPara qué
sustrato-04.mdla foto completa y vigente. Manda sobre esto
sustrato-03.mdcongelado (08-10, mañana)
w-write-path-approach.mdel camino de escritura, W0-W5
b-spark-bridge-approach.mdel puente y su latencia
platform-thesis.mdel norte: el cliente compra contexto