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.mdsobre 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 puerta | es la única que sabe quién pregunta y qué puede tocar. El destino se declara, no se parsea |
| ④ el puente | existe 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 cara | hace 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 |
| Flink | streaming |
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»:
- 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.
- La cintura (catálogo + formato): ya la tenemos, y es la pieza más madura.
- La elección de motor como POLÍTICA declarada, no como accidente. Hoy
elegirMotorDeLecturadevuelvesparksiempre. 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.
| Furnace | Junction | |
|---|---|---|
| Una puerta, motores plurales detrás | ✅ | ✅ |
| Formato común (Iceberg) | ✅ | ✅ |
| Gobierno en un punto | ✅ | ✅ y más fuerte (la cara firma y resuelve nombres) |
| Parseo y planificación | ⭐ unificados y agnósticos — un AST validado y optimizado | ⛔ no hay: se manipula TEXTO con regex y se despacha SQL |
| Dialecto | ⭐ uno solo; el motor recibe un plan | ⛔ el del motor ⇒ «common syntax» no existe |
| Interfaz | Arrow 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 suoutput»
⇒ 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-… ─┘
| Rubix | Nuestro cluster | |
|---|---|---|
| Base | K8s endurecido, HA | GKE, 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 cargas | por requisitos · auth+authz+log en cada interacción | ⛔ driver, executors y puente comparten nodo y namespace |
| Qué aloja | todos los servicios | ⛔ sólo Spark — duck-server, ml-runner y warehouse-writer viven en Railway |
| Resiliencia | arquitectura «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
| Estado | Riesgo | |
|---|---|---|
| Corpus de LECTURA | 🏁 medido: 16/25 · 7 ruidosas · 2 silenciosas (cortadas antes del motor) | acotado |
| ⚠️ Corpus de ESCRITURA | ⛔ nunca medido. El DML pasó de DuckDB SQL a Spark SQL sin ejercer un corpus | el mayor — una divergencia silenciosa aquí corrompe datos, no resultados |
| ⚠️ ANSI de Spark 4 | confirmado activo (spark.sql.ansi.enabled=true), el corpus no lo cubre | casts y overflow lanzan donde DuckDB devolvía null |
rowsAffected | siempre 0 con Spark | visible 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
| Ellos | Nosotros | |
|---|---|---|
| Vistas materializadas | Databricks y Snowflake, con refresco incremental | ⛔ no existen |
| Time travel en SQL | SELECT … VERSION AS OF / TIMESTAMP AS OF | ⛔ los snapshots existen, el SQL no los expone |
| Streaming / tablas continuas | Structured Streaming · Flink · streaming tables | ⛔ todo es batch |
| Mantenimiento automático | compaction y expiración gestionadas | ⛔ 144 tablas sin compactar ni expirar (B7) |
| Resultados grandes | EXTERNAL_LINKS a object storage | ⛔ todo inline (B3) |
| Geoespacial / vectores | tipos nativos; Iceberg v3 trae geo, variant y row lineage | ⛔ nada |
| Concurrencia de serving | warehouses 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_SECRETes 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·2 | rowsAffected honesto (null → «—») | Horas de trabajo. Se ve cada vez que alguien escribe, y dice algo falso |
| P0·3 | Mantenimiento 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·2 | Elecció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 nodo | DuckDB 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 v3 | geo, variant y row lineage cambian el formato. Depende de que Lakekeeper y Spark lo soporten |
| Streaming | Foundry usa Flink. Aquí no hay caso de uso declarado todavía |
| La cuota | mientras sean 12 vCPU denegadas, la escala es teórica |
6 · Documentos
| Doc | Para qué |
|---|---|
⭐ sustrato-04.md | la foto completa y vigente. Manda sobre esto |
sustrato-03.md | congelado (08-10, mañana) |
w-write-path-approach.md | el camino de escritura, W0-W5 |
b-spark-bridge-approach.md | el puente y su latencia |
platform-thesis.md | el norte: el cliente compra contexto |