Una guida pratica a un'app Databricks che combina l'ottimizzazione dei percorsi di Model Serving, Lakebase e l'autoscaling per valutare le transazioni in decine di millisecondi, con codice e benchmark per ogni livello.
Sei alla cassa. Avvicini la carta per pagare. Appare una piccola rotella di caricamento per forse mezzo secondo, forse meno, e poi compare la scritta Approvato. Oppure no.
In quel breve istante, qualcosa ha dovuto decidere se questo addebito ti somiglia o se sembra opera di qualcuno che ha rubato il numero della tua carta in una violazione dei dati sei mesi fa. Ha dovuto raccogliere informazioni su di te: le tue abitudini di spesa, il tuo limite giornaliero, persino se consenti acquisti da altri paesi. E ha dovuto fare tutto questo così rapidamente da non farti accorgere di nulla.
Questo post spiega come si presenta quel "qualcosa" quando lo crei su Databricks. Esamineremo retail-app, un'applicazione di esempio (backend FastAPI, frontend React, distribuita come Databricks App) che unisce due funzionalità della piattaforma:
Il repository completo è su GitHub; puoi farne un fork, distribuirlo nel tuo workspace e fare clic su "paga" tu stesso.
Prima di guardare il codice, ecco la storia semplice di un singolo pagamento. Vengono eseguiti due controlli in sequenza: un modello valuta l'addebito, poi l'app verifica le regole del tuo profilo. Ognuno dei due può rifiutare la transazione.

Il modello viene eseguito per primo perché vogliamo i suoi dati di latenza indipendentemente dall'esito. Successivamente, la ricerca del profilo (limite di spesa giornaliero, opzione per transazioni internazionali, paese di residenza) alimenta una serie di istruzioni condizionali (if-statement). La risposta include i tempi di ogni passaggio, così puoi vedere esattamente dove sono stati spesi i millisecondi.
L'aggiornamento del profilo (modifica del limite giornaliero, attivazione/disattivazione delle transazioni internazionali) è un'azione separata. Salvi le modifiche nel database e il pagamento successivo le acquisisce. Nessuna ridistribuzione, nessuna invalidazione della cache.
Quando un modello viene distribuito dietro Databricks Model Serving, c'è un passaggio di rete (network hop) tra l'applicazione e il container di inferenza. Per i carichi di lavoro batch, pochi millisecondi in più per richiesta sono irrilevanti. Per un'esperienza di pagamento, è tutto.
La route optimization accorcia questo percorso di rete. Quando abiliti la route optimization su un endpoint, Databricks Model Serving migliora il percorso di rete per le richieste di inferenza, garantendo una comunicazione più rapida e diretta tra il client e il modello. Questo routing ottimizzato consente di ottenere un maggior numero di query al secondo (QPS) rispetto agli endpoint non ottimizzati, offrendo latenze più stabili e ridotte per le tue applicazioni.
La abiliti quando crei l'endpoint ed esegui le query attraverso il flusso del data-plane utilizzando OAuth, non i token di accesso personali. Ottieni una latenza inferiore e un throughput più elevato a parità di risorse di calcolo, che è esattamente ciò di cui ha bisogno un caso d'uso interattivo di valutazione delle frodi (fraud-scoring).
Nell'app di esempio, l'endpoint si chiama fraud-detection-lakebase. Ecco la costante e la funzione che lo chiama:
Alcuni aspetti da notare:
**serving_endpoints_data_plane.query**: questo è il percorso di query del data-plane, utilizzato dalla route optimization. L'SDK di Databricks gestisce lo scambio di token OAuth dietro le quinte.**asyncio.to_thread**: il metodo di query dell'SDK è sincrono. L'inserimento all'interno di to_thread mantiene libero l'event loop di FastAPI durante l'esecuzione del modello.**dataframe_records**: il payload è un elenco di dizionari (uno per riga). Per la valutazione delle frodi, inviamo una transazione alla volta.Il modello stesso restituisce fraud_probability, fraud_flag e (cosa fondamentale) le proprie tempistiche interne: lookup_ms (il tempo impiegato per la ricerca delle feature all'interno del container del modello), inference_ms (predizione CatBoost) e total_ms. Il backend mappa questi dati in modo che il frontend possa mostrare un grafico a cascata (waterfall) della latenza:
Quindi, quando vedi il dettaglio della latenza nella UI ("Model Inference: 45ms" con "Feature Lookup: 8ms" annidato sotto), si tratta dei numeri misurati a ogni livello e uniti in un'unica risposta.
Per ulteriori informazioni sulla configurazione: Route optimization · Esecuzione di query su endpoint ottimizzati per il percorso (route-optimized).
Il modello antifrode non analizza la transazione in modo isolato. Cerca le feature storiche del cliente (importo medio della transazione, rapporto transazioni transfrontaliere, tasso di chargeback, frequenza nelle ultime 24 ore) utilizzando le prime sei cifre della carta di credito (il BIN) como chiave di ricerca. Queste feature risiedono in una tabella Postgres di Lakebase chiamata customer_features.
Questa è la stessa tabella da cui il backend legge i dati del profilo (nome, limite giornaliero, opzione internazionale). Una tabella, due lettori: il container del modello legge le feature per l'inferenza, l'app FastAPI legge i campi del profilo per le regole aziendali (business rules).
In fase di lettura, il backend prende in prestito una connessione dal pool, esegue un comando SELECT parametrizzato per user_id e restituisce la connessione in un blocco finally. Semplice psycopg2, ma la prassi di prestito/restituzione è fondamentale quando viene eseguita per ogni transazione. La query utilizza segnaposto parametrizzati per l'input dell'utente, in modo che la chiave di ricerca non venga mai concatenata direttamente nel codice SQL.
In fase di scrittura, quando un utente modifica il proprio limite giornaliero o attiva/disattiva le transazioni internazionali nella UI, il backend crea un comando UPDATE dinamico a partire da una lista consentita (allowlist) di colonne modificabili. Vengono scritti solo i campi presenti in tale elenco; il client non può inserire nomi di colonna arbitrari. La scrittura viene eseguita con autocommit = True, quindi la modifica è immediatamente visibile: la transazione successiva rileva subito il limite aggiornato senza attendere uno svuotamento del batch (batch flush) o l'invalidazione della cache.
Ogni transazione in questa app interroga Lakebase almeno due volte: una all'interno del container del modello per la ricerca delle feature e una nel backend per il controllo del profilo. Aprire una nuova connessione ogni volta comporterebbe un handshake TCP e una negoziazione TLS per ogni singola richiesta, aggiungendo facilmente 20-50 ms di overhead per chiamata. Un pool di connessioni mantiene alcune connessioni aperte e pronte all'uso, in modo che la maggior parte delle richieste debba solo utilizzarne una al volo.
Ecco come funziona in pratica. Ogni operazione sul database segue lo stesso schema di prestito, query e restituzione:
Richiedi una connessione, esegui la query, restituisci la connessione in un blocco finally in modo che venga sempre rilasciata anche in caso di errore. Ogni operazione di lettura e scrittura nell'app segue questo stesso schema.
Il pool stesso è un psycopg2.pool.ThreadedConnectionPool con 3–10 connessioni. Ma è nella sua creazione che le cose si fanno interessanti, perché Lakebase si autentica tramite OAuth. L'app scambia le credenziali del service principal con un token di accesso, quindi utilizza il client ID come nome utente Postgres e il token come password. Nessuna password del database a lungo termine.
Ciò significa che alla scadenza del token non è possibile continuare a utilizzare la vecchia password sulle connessioni nel pool, perché fallirebbero alla query successiva. Quindi _ensure_pool verifica se il token è cambiato e, in tal caso, crea un nuovo pool:
La maggior parte delle volte si attiva il percorso rapido: il pool esiste, il token non è cambiato e la risposta è immediata. Quando il token ruota, il blocco con doppio controllo (double-check locking) impedisce a due thread di ricreare il pool contemporaneamente, e il timer di 30 secondi per la chiusura del vecchio pool consente alle query in corso di completarsi prima che le relative connessioni vengano interrotte.
Il modello di frode stesso (distribuito come pyfunc di MLflow) esegue la propria ricerca su Lakebase al momento della previsione. Estrae il BIN della carta, interroga customer_features, assembla un vettore di feature ed esegue l'inferenza CatBoost. Ogni passaggio viene cronometrato:
Il container del modello mantiene il proprio ThreadedConnectionPool verso Lakebase (la classe LakebaseConnectionPool in fraud_model.py), con aggiornamento del token in background in modo che il pool rimanga valido tra istanze di serving a lungo termine. Questo è lo stesso pattern del backend (pool + rotazione OAuth), ma viene eseguito all'interno del container del modello anziché nel processo FastAPI.
I valori lookup_ms e inference_ms ritornano attraverso la risposta di serving, passano per il backend e arrivano al frontend. In questo modo si ottiene una visibilità end-to-end: il modello riporta le sue tempistiche interne, il backend aggiunge la propria misurazione del tempo reale (wall-clock) e l'utente vede tutto.
Dopo che il modello ha valutato la transazione, il backend legge il profilo del cliente da Lakebase ed esegue due semplici regole. In primo luogo, confronta l'importo della transazione con il limite di spesa giornaliero dell'utente. Se l'addebito supera la soglia, la transazione viene rifiutata con un messaggio che informa l'utente che può aumentarla nelle impostazioni del profilo. In secondo luogo, se l'utente ha disabilitato le transazioni internazionali, il backend verifica se il paese della transazione corrisponde al paese di residenza dell'utente. Una mancata corrispondenza comporta un rifiuto.
Entrambe le regole possono sovrascrivere l'approvazione di un modello. Una transazione che il modello ritiene corretta può comunque essere rifiutata perché l'utente ha impostato un limite giornaliero di 500 $. Questo è intenzionale. Il modello gestisce il rischio statistico; il profilo gestisce le preferenze dell'utente. Entrambi leggono dalla stessa tabella Lakebase, ma hanno scopi diversi.
Il tempo dedicato alla ricerca del profilo e ai controlli delle regole viene tracciato come business_logic_ms e restituito insieme alle tempistiche del modello, in modo da poter vedere esattamente quanto overhead la logica di business aggiunge a ciascuna transazione.
In produzione, l'istanza Postgres deve gestire sia le ricerche di feature del container del modello sia le letture del profilo del backend, potenzialmente molte di ciascuna al secondo durante le ore di punta. Lakebase esegue l'autoscaling, regolando la capacità di calcolo entro un intervallo minimo/massimo configurato, in modo da non pagare per la capacità di picco alle 3 del mattino, ma senza perdere query a mezzogiorno.
Nei risultati del benchmark riportati di seguito, i tempi di ricerca costanti a una sola cifra da p50 a p75 riflettono ciò che un'istanza Lakebase già avviata (warmed) fornisce in condizioni di carico costante. Il salto a p95 (13,9 ms) è tipico del churn del pool di connessioni o di brevi eventi di scale-up, comunque ampiamente all'interno del budget di latenza per un flusso di checkout, ed è esattamente il tipo di picco che l'autoscaling assorbe prima che diventi visibile all'utente. Con la funzionalità scale-to-zero abilitata, smetti anche di pagare quando non ci sono transazioni in corso.
Ecco il quadro completo della latenza per una singola transazione:
| Misurazione | Cosa rileva | Dove viene misurato |
|---|---|---|
| model_call_ms | Tempo reale (wall-clock) per l'intera chiamata di serving | Backend (router.py) |
| model_lookup_ms | Ricerca delle feature all'interno del container del modello | Model (fraud_model.py) |
| model_interfere_ms | Tempo di previsione di CatBoost | Model (fraud_model.py) |
| model_total_ms | Tempo totale all'interno del container del modello | Model (fraud_model.py) |
| business_logic_ms | Lettura del profilo + valutazione delle regole | Backend (router.py) |
| backend_total_ms | Tempo reale (wall-clock) dall'inizio della richiesta fino alla chiamata del modello di frode | Backend (router.py) |
Il divario tra round_trip_ms e model_total_ms è l'overhead di rete, ed è esattamente qui che l'ottimizzazione del percorso è utile.
Il divario tra backend_total_ms e model_call_ms è l'overhead del framework (serializzazione, routing, ecc.).
Quando si esegue l'app e si invia una transazione, l'interfaccia utente mostra quelle principali: Inferenza del modello, Ricerca delle feature e Logica di business. In questo modo è facile vedere la differenza introdotta dall'ottimizzazione del percorso o dimostrare che una ricerca delle feature di Lakebase aggiunge millisecondi a una sola cifra anziché le centinaia che ci si potrebbe aspettare da una connessione al database a freddo.
Abbiamo inviato 5.000 richieste sequenziali (concorrenza = 1, ritardo di 50 ms tra le chiamate) all'endpoint fraud-detection-lakebase ottimizzato per il percorso (CPU, dimensioni del carico di lavoro "Small", singola area Azure) e abbiamo raccolto la latenza a ogni livello, dall'interno del container del modello fino al round-trip del chiamante. L'obiettivo era isolare l'anatomia della latenza per richiesta, analizzando dove si concentrano i millisecondi in ciascun livello (ricerca delle feature, inferenza, overhead di rete), anziché eseguire un test di carico del throughput in condizioni di contesa. Questi numeri provengono dallo script di benchmark (scripts/benchmark.py), che chiama direttamente l'endpoint del modello. La scomposizione della latenza dell'interfaccia utente mostra una sezione diversa (Inferenza del modello, Ricerca delle feature e Logica di business) misurata attraverso l'intero percorso di backend.
| Metrica | Cosa misura | p50 | p75 | p90 | p95 |
|---|---|---|---|---|---|
| Ricerca delle feature (model_lookup_ms) | Lettura di Lakebase all'interno del container del modello | 8,9 ms | 9,8 ms | 11,7 ms | 13,9 ms |
| Inferenza (model_inference_ms) | Predizione CatBoost | 0.4 ms | 0.5 ms | 1.6 ms | 6.0 ms |
| Tempo totale del modello (model_total_ms) | Ricerca + inferenza + overhead del container | 9.5 ms | 10.9 ms | 14.9 ms | 17.6 ms |
| Round-trip end-to-end (round_trip_ms) | Chiamata completa del data plane dal chiamante alla risposta | 27.2 ms | 29.6 ms | 33.8 ms | 37.3 ms |
| Overhead di rete (round_trip_ms - model_total_ms) | Round-trip meno tempo del modello | 17.4 ms | 18.5 ms | 19.8 ms | 21.1 ms |
Alcuni elementi degni di nota:
L'applicazione è creata come Databricks App (backend FastAPI, frontend React) utilizzando apx. Installala usando:
Sarà inoltre necessario configurare l'autenticazione della Databricks CLI per il workspace in cui sono distribuiti l'endpoint di model serving e l'istanza Lakebase.
Per eseguire localmente:
Per distribuire nel tuo workspace:
I file principali:
src/retail_app/backend/router.py: Endpoint delle transazioni, controllo delle frodi, regole di businesssrc/retail_app/backend/postgres.py: Pool di connessioni Lakebase, CRUD del profilomodel_training/fraud_model.py: pyfunc MLflow con ricerca delle feature e CatBoostapp.yml: Configurazione di deployment (entrypoint uvicorn, variabili d'ambiente)I casi d'uso in tempo reale con requisiti di latenza inferiori a 50 ms, come il fraud scoring, la personalizzazione e la determinazione dinamica dei prezzi, possono essere creati nativamente su Databricks. L'ottimizzazione del routing di Model Serving e Lakebase collocano il percorso di inferenza e il percorso dei dati sulla stessa piattaforma governata. Se in precedenza avevi concluso che un'architettura lakehouse non potesse soddisfare i requisiti di latenza tipici di un checkout, i numeri di benchmark qui riportati (27 ms di mediana end-to-end con ricerche di feature a una sola cifra) meritano un secondo sguardo. Se un carico di lavoro in tempo reale è rimasto finora nella lista delle cose "troppo difficili", questo è il momento di riconsiderarlo. Distribuisci l'applicazione nel tuo workspace per vedere di persona la cascata di latenza ed esplora Lakebase e Databricks Model Serving.
(Questo post sul blog è stato tradotto utilizzando strumenti basati sull'intelligenza artificiale) Post originale
Iscriviti al nostro blog e ricevi gli ultimi articoli direttamente nella tua casella di posta.