Una guía paso a paso de una aplicación de Databricks que combina la optimización de rutas de Model Serving, Lakebase y escalado automático para evaluar transacciones en decenas de milisegundos, con código y benchmarks en cada capa.
por Harsha Pasala y Subhadip Chanda
Estás frente a la caja. Acercas tu tarjeta para pagar. Aparece un pequeño indicador de carga durante medio segundo, o quizás menos, y luego dice Aprobado. O no.
Durante ese tiempo, algo tuvo que decidir si este cargo se parece a ti o a alguien que robó el número de tu tarjeta en una filtración de datos hace seis meses. Tenía que saber cosas sobre ti: tus patrones de gasto, tu límite diario, si incluso permites compras en otros países. Y tenía que hacer todo eso lo suficientemente rápido como para que no te dieras cuenta de que sucedió.
Esta publicación trata sobre cómo se ve ese "algo" cuando lo construyes en Databricks. Analizaremos retail-app, una aplicación de muestra (backend de FastAPI, frontend de React, implementada como una Databricks App) que combina dos capacidades de la plataforma:
El repositorio completo está en GitHub; puedes hacer un fork, implementarlo en tu espacio de trabajo y pulsar "pagar" tú mismo.
Antes de ver el código, aquí está la historia simple de un solo pago. Se realizan dos comprobaciones en secuencia: un modelo evalúa el cargo y luego la aplicación verifica las reglas de tu perfil. Cualquiera de las dos puede rechazar la transacción.

El modelo se ejecuta primero porque queremos sus números de latencia independientemente del resultado. Luego, la búsqueda de perfil (límite de gasto diario, activación de transacciones internacionales, país de residencia) alimenta unas cuantas instrucciones if. La respuesta incluye el tiempo de cada paso para que puedas ver exactamente a dónde se fueron los milisegundos.
Actualizar tu perfil (cambiar tu límite diario, activar o desactivar transacciones internacionales) es una acción independiente. Guardas los cambios en la base de datos y el siguiente pago los aplica. Sin volver a implementar, sin invalidación de caché.
Cuando un modelo se implementa detrás de Databricks Model Serving, hay un salto de red entre tu aplicación y el contenedor de inferencia. Para cargas de trabajo por lotes (batch), unos pocos milisegundos adicionales por solicitud son irrelevantes. Para una experiencia de pago, lo son todo.
La optimización de rutas acorta esa ruta de red. Cuando habilitas la optimización de rutas en un endpoint, Databricks Model Serving mejora la ruta de red para las solicitudes de inferencia, lo que resulta en una comunicación m ás rápida y directa entre tu cliente y el modelo. Este enrutamiento optimizado permite obtener más consultas por segundo (QPS) en comparación con los endpoints no optimizados y proporciona latencias más bajas y estables para tus aplicaciones.
Lo habilitas al crear el endpoint y realizas las consultas a través del flujo del plano de datos (data-plane) usando OAuth, no tokens de acceso personal. Obtienes una menor latencia y un mayor rendimiento (throughput) para el mismo procesamiento, que es exactamente lo que necesita un caso de uso interactivo de detección de fraude.
En la aplicación de muestra, el endpoint se llama fraud-detection-lakebase. Aquí está la constante y la función que lo llama:
Algunas cosas a tener en cuenta:
**serving_endpoints_data_plane.query**: Esta es la ruta de consulta del plano de datos, que es la que utiliza la optimización de rutas. El SDK de Databricks gestiona el intercambio de tokens OAuth de forma interna.**asyncio.to_thread**: El método de consulta del SDK es síncrono. Envolverlo en to_thread mantiene libre el bucle de eventos de FastAPI mientras se ejecuta el modelo.**dataframe_records**: La carga útil (payload) es una lista de diccionarios (uno por fila). Para la evaluación de fraude, enviamos una transacción a la vez.El modelo en sí devuelve fraud_probability, fraud_flag y (lo que es crucial) su propio tiempo interno: lookup_ms (cuánto tiempo tomó la búsqueda de características dentro del contenedor del modelo), inference_ms (predicción de CatBoost) y total_ms. El backend los mapea para que el frontend pueda mostrar un gráfico de cascada de latencia:
Así que cuando ves el desglose de latencia en la interfaz de usuario (UI) ("Inferencia del modelo: 45 ms" con "Búsqueda de características: 8 ms" anidado debajo), son los números medidos en cada capa y combinados en una sola respuesta.
Para obtener más información sobre cómo configurar esto: Optimización de rutas · Consulta de endpoints optimizados para rutas.
El modelo de fraude no solo analiza la transacción de forma aislada. Busca las características históricas del cliente (monto promedio de transacción, proporción de transacciones transfronterizas, tasa de contracargos, velocidad en las últimas 24 horas) utilizando los primeros seis dígitos de la tarjeta de crédito (el BIN) como clave de búsqueda. Esas características residen en una tabla de Postgres de Lakebase llamada customer_features.
Esta es la misma tabla de la que lee el backend para obtener los datos del perfil (nombre, límite diario, activación de transacciones internacionales). Una tabla, dos lectores: el contenedor del modelo lee las características para la inferencia, la aplicación FastAPI lee los campos del perfil para las reglas de negocio.
En el lado de la lectura, el backend toma prestada una conexión del grupo (pool), ejecuta un SELECT parametrizado por user_id y devuelve la conexión en un bloque finally. Es psycopg2 directo, pero la disciplina de tomar/devolver es importante cuando esto se ejecuta en cada transacción. La consulta utiliza marcadores de posición parametrizados para la entrada del usuario, por lo que la clave de búsqueda nunca se concatena en el SQL.
En el lado de la escritura, cuando un usuario cambia su límite diario o activa/desactiva las transacciones internacionales en la interfaz de usuario (UI), el backend construye un UPDATE dinámico a partir de una lista de permitidos de columnas editables. Solo se escriben los campos de esa lista; el cliente no puede inyectar nombres de columnas arbitrarios. La escritura se ejecuta con autocommit = True, por lo que el cambio es visible de inmediato: la siguiente transacción detecta el límite actualizado sin esperar a un vaciado por lotes (batch flush) o a la invalidación de la caché.
Cada transacción en esta aplicación accede a Lakebase al menos dos veces: una dentro del contenedor del modelo para la búsqueda de características y otra en el backend para la verificación del perfil. Abrir una nueva conexión cada vez implica un saludo TCP (handshake) más una negociación TLS en cada solicitud, lo que agregaría fácilmente de 20 a 50 ms de sobrecarga por llamada. Un grupo de conexiones (connection pool) mantiene abiertas y listas unas cuantas conexiones, por lo que la mayoría de las solicitudes simplemente toman una y continúan.
Aquí está cómo se ve eso en la práctica. Cada operación de base de datos sigue el mismo patrón de tomar/consultar/devolver:
Toma una conexión prestada, ejecuta tu consulta y devuelve la conexión en un bloque finally para que siempre regrese, incluso si ocurre un error. Cada lectura y escritura en la aplicación sigue este mismo patrón.
El pool en sí es un psycopg2.pool.ThreadedConnectionPool con 3–10 conexiones. Pero su construcción es donde las cosas se ponen interesantes, porque Lakebase se autentica a través de OAuth. La aplicación intercambia las credenciales de la entidad de servicio por un token de acceso, luego usa el ID de cliente como nombre de usuario de Postgres y el token como contraseña. Sin contraseñas de base de datos de larga duración.
Esto significa que cuando el token expira, no puedes simplemente seguir usando la contraseña antigua en las conexiones del pool, porque fallarían en la siguiente consulta. Por lo tanto, _ensure_pool verifica si el token ha cambiado y, de ser así, crea un pool nuevo:
La mayoría de las veces, se activa la ruta rápida: el pool existe, el token no ha cambiado y retornamos de inmediato. Cuando el token se rota, el bloqueo de doble verificación evita que dos hilos reconstruyan el pool al mismo tiempo, y el temporizador de 30 segundos para cerrar el pool antiguo da tiempo a las consultas en curso para finalizar antes de que sus conexiones desaparezcan.
El propio modelo de fraude (desplegado como una pyfunc de MLflow) realiza su propia búsqueda en Lakebase en el momento de la predicción. Extrae el BIN de la tarjeta, consulta customer_features, ensambla un vector de características y ejecuta la inferencia de CatBoost. Se mide el tiempo de cada paso:
El contenedor del modelo mantiene su propio ThreadedConnectionPool hacia Lakebase (la clase LakebaseConnectionPool en fraud_model.py), con actualización de token en segundo plano para que el pool siga siendo válido en instancias de servicio de larga duración. Este es el mismo patrón que el backend (pool + rotación de OAuth), pero ejecutándose dentro del contenedor del modelo en lugar del proceso de FastAPI.
Esos valores lookup_ms y inference_ms fluyen de regreso a través de la respuesta del servicio, a través del backend y hacia el frontend. Así es como se obtiene visibilidad de extremo a extremo: el modelo informa su tiempo interno, el backend agrega su propia medición de tiempo real y el usuario lo ve todo.
Después de que el modelo evalúa la transacción, el backend lee el perfil del cliente desde Lakebase y ejecuta dos reglas simples. Primero, compara el monto de la transacción con el límite de gasto diario del usuario. Si el cargo supera el límite, la transacción se rechaza con un mensaje que le indica al usuario que puede aumentarlo en la configuración de su perfil. Segundo, si el usuario ha desactivado las transacciones internacionales, el backend verifica si el país de la transacción coincide con el país de residencia del usuario. Una discrepancia significa un rechazo.
Cualquiera de las dos reglas puede anular la aprobación de un modelo. Una transacción que el modelo considera correcta aún puede ser rechazada porque el usuario estableció un l ímite diario de $500. Esto es intencional. El modelo maneja el riesgo estadístico; el perfil maneja las preferencias del usuario. Ambos leen de la misma tabla de Lakebase, pero sirven para propósitos diferentes.
El tiempo dedicado a la búsqueda del perfil y a las verificaciones de reglas se registra como business_logic_ms y se devuelve junto con el tiempo del modelo, para que puedas ver exactamente cuánta sobrecarga agrega la lógica de negocio a cada transacción.
En producción, tu instancia de Postgres debe manejar tanto las búsquedas de características del contenedor del modelo como las lecturas de perfil del backend, potencialmente muchas de cada una por segundo durante las horas pico. Lakebase se escala automáticamente, ajustando el cómputo dentro de un rango mínimo/máximo configurado para que no pagues por la capacidad pico a las 3 a. m., pero tampoco pierdas consultas al mediodía.
En los resultados de la prueba de rendimiento a continuación, los tiempos de búsqueda constantes de un solo dígito de p50 a p75 reflejan lo que ofrece una instancia precalentada de Lakebase bajo una carga constante. El salto en p95 (13.9 ms) es típico de la rotación del pool de conexiones o de breves eventos de escalado, aún muy dentro del presupuesto de latencia para un flujo de pago, y exactamente el tipo de pico que el escalado automático absorbe antes de que sea visible para el usuario. Con el escalado a cero activado, también dejas de pagar cuando no hay transacciones fluyendo.
Aquí está el panorama completo de la latencia para una sola transacción:
| Medición | Qué captura | Dónde se mide |
|---|---|---|
| model_call_ms | Tiempo real para toda la llamada de servicio | Backend (router.py) |
| model_lookup_ms | Búsqueda de características dentro del contenedor del modelo | Model (fraud_model.py) |
| model_interfere_ms | Tiempo de predicción de CatBoost | Model (fraud_model.py) |
| model_total_ms | Tiempo total dentro del contenedor del modelo | Model (fraud_model.py) |
| business_logic_ms | Lectura de perfil + evaluación de reglas | Backend (router.py) |
| backend_total_ms | Tiempo real desde el inicio de la solicitud hasta la llamada al modelo de fraude | Backend (router.py) |
La diferencia entre round_trip_ms y model_total_ms es la sobrecarga de red, y ahí es exactamente donde ayuda la optimización de rutas.
La diferencia entre backend_total_ms y model_call_ms es la sobrecarga del framework (serialización, enrutamiento, etc.).
Cuando ejecutas la aplicación y envías una transacción, la UI muestra las clave: inferencia del modelo, búsqueda de características y lógica de negocio. Esto facilita ver la diferencia que hace la optimización de rutas, o demostrar que una búsqueda de características de Lakebase agrega milisegundos de un solo dígito en lugar de los cientos que se podrían esperar de una conexión de base de datos fría.
Enviamos 5,000 solicitudes secuenciales (concurrencia = 1, retraso de 50 ms entre llamadas) al endpoint optimizado para rutas fraud-detection-lakebase (CPU, tamaño de carga de trabajo "Small", una sola región de Azure) y recopilamos la latencia en cada capa, desde el interior del contenedor del modelo hasta el viaje de ida y vuelta del cliente. El objetivo era aislar la anatomía de la latencia por solicitud, es decir, a dónde van los milisegundos en cada capa (búsqueda de características, inferencia, sobrecarga de red), en lugar de realizar una prueba de carga del rendimiento bajo contención. Estos números provienen del script de prueba de rendimiento (scripts/benchmark.py), que llama directamente al endpoint del modelo. El desglose de latencia de la UI muestra una sección diferente (inferencia del modelo, búsqueda de características y lógica de negocio) medida a través de la ruta completa del backend.
| Métrica | Qué mide | p50 | p75 | p90 | p95 |
|---|---|---|---|---|---|
| Búsqueda de características (model_lookup_ms) | Lectura de Lakebase dentro del contenedor del modelo | 8.9 ms | 9.8 ms | 11.7 ms | 13.9 ms |
| Inferencia (model_inference_ms) | Predicción de CatBoost | 0.4 ms | 0.5 ms | 1.6 ms | 6.0 ms |
| Tiempo total del modelo (model_total_ms) | Búsqueda + inferencia + sobrecarga del contenedor | 9.5 ms | 10.9 ms | 14.9 ms | 17.6 ms |
| Ida y vuelta de extremo a extremo (round_trip_ms) | Llamada completa del plano de datos desde el emisor hasta la respuesta | 27.2 ms | 29.6 ms | 33.8 ms | 37.3 ms |
| Sobrecarga de red (round_trip_ms - model_total_ms) | Ida y vuelta menos tiempo del modelo | 17.4 ms | 18.5 ms | 19.8 ms | 21.1 ms |
Algunos aspectos a destacar:
La aplicación está construida como una Databricks App (backend de FastAPI, frontend de React) utilizando apx. Instálala usando:
También necesitarás configurar la autenticación de Databricks CLI para el espacio de trabajo donde se implementan el endpoint de servicio de modelos y la instancia de Lakebase.
Para ejecutar localmente:
Para implementar en tu espacio de trabajo:
Los archivos clave:
src/retail_app/backend/router.py: Endpoint de transacción, verificación de fraude, reglas de negociosrc/retail_app/backend/postgres.py: Pool de conexiones de Lakebase, CRUD de perfilesmodel_training/fraud_model.py: pyfunc de MLflow con búsqueda de características y CatBoostapp.yml: Configuración de implementación (punto de entrada de uvicorn, variables de entorno)Los casos de uso en tiempo real con requisitos de latencia inferiores a 50 ms, como la detección de fraudes, la personalización y la fijación de precios dinámica, se pueden crear de forma nativa en Databricks. La optimización de rutas de Model Serving y Lakebase colocan la ruta de inferencia y la ruta de datos en la misma plataforma gobernada. Si anteriormente habías llegado a la conclusión de que una arquitectura de lakehouse no podía cumplir con una latencia adecuada para un flujo de pago, vale la pena volver a mirar las cifras de referencia aquí (mediana de 27 ms de extremo a extremo con búsquedas de características de un solo dígito). Si una carga de trabajo en tiempo real ha estado en la lista de "demasiado difícil", este es el momento de volver a abordarla. Implementa la aplicación en tu propio espacio de trabajo para ver la cascada de latencia por ti mismo y explora Lakebase y Databricks Model Serving.
(Esta entrada del blog ha sido traducida utilizando herramientas basadas en inteligencia artificial) Publicación original
Suscríbete a nuestro blog y recibe las últimas publicaciones directamente en tu bandeja de entrada.