R · Absorción de StarRocks — approach medido
Fecha de la medición: 2026-08-09 (sesión de tarde). Sucede al esqueleto R0-R5
dehandoff-2026-08-09.md§4, que era propuesta sin
medir. Este documento lo corrige con cuatro hallazgos y reordena las fases.Misma regla que
sustrato-02.md: cada hecho lleva el comando
que lo demuestra. Lo que no lo lleva va marcado como pendiente (§5) u opinión (§8).Alcance, y es el punto entero: StarRocks entra acotado al SQL Editor.
Nada más. Ingesta, ETL, notebooks, transformaciones y ML siguen siendo Spark.
1 · El estado, medido hoy
Las cuatro CLIs están vivas y apuntando donde deben:
| CLI | Estado |
|---|---|
gcloud | help@node-web.com · proyecto trino-k8s |
kubectl | contexto gke_trino-k8s_europe-west1_trino-compute-clone |
railway | Víctor Obregón · proyecto cozy-comfort · 7 servicios Online |
vercel | victorgvobregon-7575 |
kubectl get pods -n spark -o wide
carbon-connect-server-0 1/1 Running 27m
spark-connect-server-…-exec-1 1/1 Running 27m
spark-connect-server-…-exec-2 1/1 Running 27m
Spark está corriendo: el SparkConnectServer con sus 2 ejecutores, y los cuatro
operadores de Stackable 26.7.0 (commons, secret, listener, spark-k8s) en
Running. El namespace trino está vacío de cargas — la demolición se sostiene.
Railway: lakekeeper, openfga, keycloak, Duck, karma, warehouse-writer,
Jupyter, Carbon Jobs, todos Online.
2 · Los cuatro hallazgos que reordenan el plan
⛔ H1 · El Gateway no está «conservado»: está ROTO
sustrato-02 §4 lo lista como el activo estrella que sobrevive a la demolición.
Lleva roto desde las 09:32 UTC de hoy, y lo rompió la propia demolición:
kubectl describe gateway trino-gateway -n trino
Programmed: False · Attached Routes: 0
Message: Error GWCER102: Secret trino/trino-gw-placeholder not found.
Warning SYNC (x84 over 6h15m) failed to translate Gateway "trino/trino-gateway"
El barrido de kubectl delete cm,secret,pvc … -n trino (los «6 secrets» de
sustrato-02 §3) se llevó el secret placeholder que el listener HTTPS
referencia. El certificado de verdad viaja por la anotación
networking.gke.io/certmap; el Secret sólo existe porque la Gateway API exige
sintácticamente un certificateRefs.
Lo que sí sobrevivió, verificado:
gcloud compute addresses list --project trino-k8s
gcloud certificate-manager certificates list --project trino-k8s
trino-lb-global 136.69.107.171 IN_USE
trino-paladio-cert-global trino.paladio.io expira 2026-11-06
trino-paladio-certmap-global 136.69.107.171:443
⇒ IP y certificado intactos; el Gateway inservible por un secret de una línea. Arreglo trivial — pero ver H2 antes de gastar el arreglo.
⛔⛔ H2 · Y arreglado tampoco sirve: GKE Gateway no habla TCP
«HTTPRoute is the only Route type supported on GKE Gateway; TCPRoutes, UDPRoutes,
and TLSRoutes are not supported.» — documentación de GKE, capacidades por
GatewayClass
StarRocks se consulta por protocolo de cable MySQL, que es TCP en el 9030 —
no HTTP. El gke-l7-global-external-managed no puede transportarlo, ni con
TCPRoute (la CRD ni siquiera está instalada en GKE 1.35) ni de ninguna otra forma.
⇒ El activo que se conservó «para que Spark y lo que venga lo reusen» no aplica al
motor que viene. R5 necesita un camino L4 propio: Service type=LoadBalancer
TCP, IP propia, TLS servido por el propio StarRocks y su propio cortafuegos.
⚠️ Y eso abre una pregunta que no es de infraestructura sino de exposición: un puerto MySQL público. Vercel no da IPs de egreso estáticas fuera de los planes que lo venden, así que el allowlist por IP puede no estar disponible. Es una decisión del owner (§4·D3), no un detalle de despliegue.
⛔ H3 · No cabe en el cluster de hoy
kubectl describe node gke-trino-compute-clone-main-pool-4-d8c3cf38-h4mc
Allocatable cpu 3920m memory 13.591.684Ki
Allocated cpu 2976m (75%) memory 10.254.158.720 (73%)
Libre real: ~944m de CPU y ~3,3 GB de memoria, en un solo nodo
n2-standard-4. Un FE + un BE de StarRocks no entran ahí — y menos un BE, que es
justo el que quiere memoria y disco para la caché (§3·R4).
Lo que sí hay es sitio para crecer, medido:
gcloud compute regions describe europe-west1 --project trino-k8s
N2_CPUS límite 32 en uso 4
SSD_TOTAL_GB límite 250 en uso 50
PREEMPTIBLE_CPUS límite 0 ← Spot sigue bloqueado (cuenta trial)
⇒ R0 arranca añadiendo capacidad, no instalando el operador. Y sin Spot: la capa
de serving es siempre encendida (sustrato-02 §5·quater lo anticipó), así que
es coste fijo mensual, de forma opuesta a Spark, que es efímero.
⭐⭐ H4 · La razón nº2 para elegir StarRocks tiene un eslabón sin verificar — y está en NUESTRO lado
El handoff da el JWT identity passthrough como una de las tres razones de la elección. La parte de StarRocks existe y está documentada:
iceberg.catalog.security = JWT (valores válidos: NONE · OAUTH2 · JWT)
→ «the user is required to log in to the StarRocks cluster using the JWT method»
→ las credenciales se omiten: passthrough
iceberg.catalog.vended-credentials-enabled = true (v4.0+, default true)
Pero el login por JWT contra StarRocks exige un plugin de cliente concreto:
mysql -h … -P 9030 --authentication-openid-connect-client-id-token-file=<fichero> -u tom
«you need to enable theauthentication_openid-connect_clientplugin» · cliente MySQL 9.2 o superior
Y lo que tenemos, medido en el repo:
ls node_modules/mysql2/lib/auth_plugins/
caching_sha2_password mysql_clear_password mysql_native_password sha256_password
mysql2 3.18.2 —ya en package.json:188— no trae authentication_openid_connect_client.
⇒ «StarRocks 4.0 hace JWT» ≠ «nuestro editor puede autenticarse por JWT». Es
exactamente el patrón que sustrato-02 §5·bis convirtió en regla: un hecho del motor
dado por bueno en nuestro lado sin medirlo.
🟡 No es un muro, y conviene decirlo con precisión: mysql2 sí expone una API
para registrar plugins propios —authPlugins (lib/connection_config.js:175,
lib/commands/auth_switch.js:39)—, así que implementar el handshake OIDC en Node es
trabajo acotado, no imposible. Y queda una vía más barata por sondear:
mysql_clear_password sí está, y varios motores aceptan el token como contraseña
por ahí.
⇒ Esto sube a gate temprano (R2·bis), antes de comprometer el resto. Si el handshake no se cierra, StarRocks sigue entrando —el protocolo MySQL de siempre funciona— pero entra sin la razón nº2, y eso cambia el argumento con el que se eligió.
3 · El mapa de fases, revisado
| Fase | Gate — la salida que decide | Bloquea a | |
|---|---|---|---|
| R0·a | capacidad | un 2º nodo Ready, o el pool redimensionado, con Spark intacto | todo |
| R0·b | operador + StarRocksCluster | FE y BE Running, nada en Pending, y SELECT 1 por el 9030 dentro del cluster | R1 |
| R1 | catálogo Iceberg REST contra nuestra cara | SHOW DATABASES responde y lista sólo lo concedido | R2 |
| R2 | ⭐ lee con credencial vendida | count(*) real + SELECT * · y el negativo: sin grant → denegado | R3 |
| R2·bis | ⭐⭐ ¿puede Node autenticarse? | mysql2 completa el handshake JWT contra el 9030 — o el veredicto de que no | 🏁 VERDE — §3·bis |
| R3 | ⭐ identity passthrough | en access_events de Index aparece el usuario, no lakekeeper-starrocks | — |
| R4 | ⭐ la caché local | 2ª pasada de la misma query contra la 1ª, con el mismo arnés | la tesis |
| R5 | exposición L4 | Vercel abre conexión TCP+TLS al 9030 desde fuera de GCP | R6 |
| R6 | la costura | junction.ts:1534 enruta el read a StarRocks; el write sigue en DuckDB | — |
Por qué cada gate está donde está
- R0·a antes que el operador. Instalar un chart que se queda en
Pendingno distingue «el operador está mal» de «no hay sitio» (H3). Es el mismo corte quesustrato-02§5·bis hizo entre S1 y S2, y por el mismo motivo. - R2 lleva
SELECT *, no sólocount(*). Uncount(*)se resuelve desde el summary del snapshot sin abrir un fichero: pasaría con el storage inalcanzable. Es literalmente la lección del gate ④ de S2. - R2 lleva control negativo. Un catálogo que dijera que sí a todo aprobaría R1 y
R2 enteros. Y el negativo va directo a la tabla, no por
SHOW DATABASES: con el grant revocado la cara devuelve lista vacía, y vacío es ambiguo (S4). - R2·bis se adelanta a R3 a propósito. Es el eslabón de H4, es barato, y su respuesta decide si R3 existe o se retira. Medirlo tarde es descubrir a mitad de la absorción que la razón nº2 no era nuestra.
- R4 es el que de verdad importa. Los 3,4 s/query medidos con Trino eran
topología —GKE
europe-west1→ R2WEUR—, no motor. Si la caché local no los baja, cambiar de motor no arregla el problema y la absorción no tiene caso de rendimiento (sí de tenencia, ver §7). - R6 es una línea. Ver §6.
3·bis · 🏁 R2·bis — MEDIDO, y H4 queda cerrado
scripts/starrocks/r2bis-jwt-handshake.ts · StarRocks 4.1.4 en Docker local ·
JWT real del Keycloak lakehouse (cliente lakekeeper-operator) · 2026-08-09
[OK] P0 control positivo ACEPTA root conecta · current_user='root'@'%'
[OK] P1 usuario JWT ACEPTA CREATE USER ... authentication_jwt aceptado
[NO] P2 clear_password RECHAZA Server requests authentication using unknown
plugin authentication_openid_connect_client
[NO] P3 token+NUL RECHAZA Invalid serialized JWS: Missing second delimiter
[NO] P3 token-crudo RECHAZA Invalid serialized JWS: Missing second delimiter
[OK] P3 cap+lenenc ACEPTA handshake OK
current_user='service-account-lakekeeper-operator'@'%'
Tres cosas que sólo aparecen midiendo:
-
⛔ La vía barata no existe, y no por elección nuestra.
mysql_clear_passwordestá en mysql2, pero el servidor conmuta de plugin él: pideauthentication_openid_connect_clienty no negocia. No hay atajo que evite escribir protocolo. -
⭐ El formato se descubrió, no se adivinó. Las dos primeras codificaciones fallaron con
Missing second delimiter— el servidor estaba parseando nuestros bytes como un JWT y el marco estaba desplazado. Ese mensaje es lo que señaló dónde iba el error. La que entra:[0x01] [entero length-encoded] [token]⚠️ La sonda sólo ejercita la rama
0xfc+uint16LE(tokens de 251 a 65.535 bytes), que es donde cae un token de Keycloak. El adaptador de verdad necesita las cuatro ramas del entero length-encoded de MySQL (<251·0xfc+2 ·0xfd+3 ·0xfe+8), o se romperá con el primer token que se salga del rango. -
⭐⭐ La identidad ATERRIZA.
current_user()devuelve'service-account-lakekeeper-operator'@'%', elpreferred_usernamedel token. No es sólo «nos autenticamos»: StarRocks sabe quién eres a partir del JWT, que es la mitad de R3 ya de pie.
⇒ H4 cerrado. La razón nº2 de la elección se sostiene, y el coste es ~10 líneas
en authPlugins — código nuestro a mantener, pero acotado y ya medido.
⇒ La decisión D4 desaparece: no hay que elegir, funciona.
⛔ Y un hallazgo de sustrato que reordena DÓNDE se puede medir
CPU del host: Intel Xeon E5-2689 (Sandy Bridge, 2012) -> AVX, pero SIN AVX2
BE de StarRocks: FATAL "Exited too quickly", en bucle indefinido
El BE de StarRocks exige AVX2. No es configuración ni memoria: es la máquina. ⇒ En este equipo no correrá nunca un backend de StarRocks.
Lo que sí es medible en local, porque vive entero en el FE: la autenticación, la
identidad, SHOW DATABASES, SHOW GRANTS, CURRENT_USER() — es decir, R2·bis
completo y probablemente buena parte de R1 (montar el catálogo REST y listar es
metadata del FE). Lo que exige el cluster: R2 (leer bytes con credencial vendida)
y R4 (la caché), que son justo las dos que tocan datos.
⚠️ Y una trampa que mordió en el camino, del mismo linaje que las de sustrato-02
§5·bis: el bucle de espera original sondeaba con SELECT current_version(), que
StarRocks enruta al backend. Con el FE perfecto y escuchando, la espera giró
25 minutos dando «no está listo» — cuando lo que esta sonda mide (el handshake)
llevaba listo desde el primer minuto. Un gate de arranque debe comprobar la
capacidad que la sonda necesita, no una cualquiera. De propina: docker exec … | head devolvía EXIT=0 con un ERROR impreso — el código de salida era el de head.
⚠️ El veredicto se enumera, no se infiere
Regla heredada de sustrato-02 §5·bis, y aplica a los siete gates:
Un resultado que no es ni el éxito ni el fallo esperados no es un dato.
Cada arnés enumera sus salidas válidas y sale con código propio cuando falta
alguna. «No apareció la palabra mala» no es un aprobado.
Y los tres corolarios que ya costaron: el formato de salida es parte del gate
(una flecha unicode en consola cp1252 vació una medición entera);
spawnSync(…, {shell:true}) es cmd.exe en Windows, no bash; y un selector
-l app=… casa con los pods de la corrida anterior.
4 · Las decisiones que necesitan al owner
| Decisión | Por qué no la tomo yo | |
|---|---|---|
| D1 | ✅ RESUELTA (owner, 2026-08-09): un 2º nodo n2-standard-4. Reversible con Spark corriendo | — |
| D2 | Disco de la caché. PVC pd-ssd (cuota: 200 GB libres) o Local SSD (375 GB por incremento, hay que recrear el pool) | R4 depende de esto, y Local SSD obliga a tocar el node pool con Spark encima |
| D3 | ⭐ Exponer MySQL wire a internet (H2), o meter un proxy HTTP propio delante | Es superficie de ataque nueva. La alternativa —un sidecar HTTP→MySQL— es justo la pieza que descartamos para Spark Connect |
| ✅ DISUELTA por la medición (§3·bis). R2·bis salió verde: el plugin se implementa y funciona | — | |
| D5 | ⭐ Dos modelos de identidad conviviendo. S5·1 decidió «A · la cara es un proxy de confianza» y descartó passthrough para no duplicar política. R3 lo reintroduce sólo para StarRocks | O es una excepción consciente y acotada, o R3 contradice una decisión de esta misma mañana |
5 · Lo que NO entra — la acotación, explícita
Porque una absorción se mide por lo que el producto usa (sustrato-02 §5·bis:
Trino llegó a F7 y junction.ts:1534 siguió en DuckDB todo el tiempo):
- ⛔ StarRocks no escribe. El
writeTargetderunQueryywithDmlControlPlanese quedan enteros en DuckDB. Y hay razón de sustrato, no de gusto: el camino gobernado de escritura sigue bloqueado encan_commit(sustrato-02§5·ter) y ese bloqueante no es de Spark, es del sustrato — le pasaría igual a StarRocks. - ⛔ No toca ingesta, ETL, notebooks ni ML. Eso es Spark, y la decisión de S6 no se reabre.
- ⛔ No sustituye a
loadItemForConsumption. Entra detrás de la cintura, como unEngineAdaptermás — igual que DuckDB, Karma y PyIceberg. - ⬜ No hereda las 25 sondas. Están medidas contra Trino, que ya no existe.
Hay tres dialectos que re-medir contra el corpus —Spark SQL (S7), StarRocks (R6)
y el DuckDB de referencia— y
dialect-divergence.tssigue diciendo «Carbon SQL es Trino SQL». Sin eso,timestamp-arithmeticvuelve a morder: sintaxis correcta, resultado distinto, sin lanzar.
6 · La costura en el código — es una línea, y ya está preparada
F2 dejó el registry hecho. Añadir StarRocks es:
| Fichero | Cambio |
|---|---|
lib/compute/contract.ts:78 | Engine gana 'starrocks' |
lib/compute/engines/starrocks.ts | nuevo · EngineAdapter + runQuery por mysql2 |
lib/compute/engines/index.ts:28 | una entrada en REGISTRY |
lib/compute/junction.ts:1534 | getSqlEngine('duckdb') → StarRocks en el brazo de lectura |
REGISTRY es Record<Engine, EngineAdapter> — un mapa total: añadir el valor sin
registrar el adaptador rompe en tsc, no en producción.
⚠️ Y de paso: trino.ts sigue en el registry (index.ts:24,33) con sus 157
líneas de cabecera describiendo un motor que ya no existe. Es exactamente el tipo
de doc rancio que sustrato-01 §7 existe para cazar. Retirarlo va con R6.
⭐ available no se declara: se mide. El adaptador de StarRocks nace available: false mientras no exista su URL, igual que hicieron Karma y Trino. Un motor no
desplegado no puede mentir sobre sí mismo — y Trino enseñó el fallo contrario:
available: true sobre un coordinador que Vercel no alcanzaba.
7 · El caso que sigue en pie aunque R4 salga plano
Conviene tenerlo escrito, porque si la caché no baja los 3,4 s la tentación es abandonar. La razón estructural no era la velocidad:
ATTACHes de la instancia de DuckDB, no de la conexión. El aliaslakeno
puede significar dos warehouses a la vez ⇒ un duck-server no sirve a N inquilinos
con warehouse propio — que es exactamente a donde va el sustrato.
Ese techo lo documentó el propio adaptador de Trino, y no lo arregla ningún benchmark. StarRocks monta N catálogos externos en un proceso; es su modelo.
8 · Opinión (marcada como tal, no medida)
- El nombre. El cluster se llama
trino-compute-clone, la IPtrino-lb-global, el certificadotrino-paladio-cert-globaly el dominiotrino.paladio.io— de un motor demolido esta mañana. El certificado caduca el 2026-11-06: hay una ventana natural para renombrar sin re-emitir nada de urgencia. - El orden de R0·a. Yo tocaría el node pool antes de instalar nada. Añadir un
nodo con Spark corriendo es reversible; recrear el pool para meter Local SSD (D2)
no lo es con el
SparkConnectServerencima. - R2·bis primero, si hay que elegir. Es el gate más barato de los siete y el único que puede invalidar el argumento de la elección. Los demás sólo dicen «funciona o no»; ése dice «era verdad la premisa o no».
9 · Fuentes
- StarRocks · Iceberg catalog —
iceberg.catalog.security,vended-credentials-enabled - StarRocks · JWT authentication — el plugin de cliente y el MySQL 9.2+
- StarRocks · Data Cache — dos niveles memoria+disco en BE/CN
- StarRocks · Kubernetes Operator · releases — última 1.9.1
- GKE · gatewayclass capabilities — HTTPRoute y sólo HTTPRoute
- Gateway API v1.6 · TCPRoute a Standard — graduó en agosto 2026; GKE aún no
- MySQL 9.7 · OpenID Connect Pluggable Authentication