Présentation d'une application Databricks combinant l'optimisation des routes de Model Serving, Lakebase et l'autoscaling pour évaluer les transactions en quelques dizaines de millisecondes, avec code et benchmarks à chaque niveau.
par Harsha Pasala et Subhadip Chanda
Vous êtes devant la caisse. Vous approchez votre carte. Un petit indicateur de chargement apparaît pendant une demi-seconde, peut-être moins, puis le message « Approuvé » s'affiche. Ou pas.
Pendant ce temps, un système a dû déterminer si cette transaction vous correspond ou s'il s'agit de quelqu'un qui a volé votre numéro de carte lors d'une fuite de données il y a six mois. Il a dû analyser des informations vous concernant : vos habitudes de dépenses, votre plafond journalier, ou encore si vous autorisez les achats à l'étranger. Et il a dû faire tout cela si rapidement que vous ne vous en êtes même pas rendu compte.
Cet article explique à quoi ressemble ce « système » lorsque vous le concevez sur Databricks. Nous allons explorer retail-app, une application exemple (backend FastAPI, frontend React, déployée en tant que Databricks App) qui associe deux fonctionnalités de la plateforme :
Le dépôt complet est disponible sur GitHub. Vous pouvez le dupliquer (fork), le déployer sur votre espace de travail et cliquer vous-même sur « payer ».
Avant de nous pencher sur le code, voici le déroulement simple d'un paiement unique. Deux vérifications s'exécutent successivement : un modèle évalue la transaction, puis l'application vérifie les règles de votre profil. L'une ou l'autre de ces étapes peut refuser la transaction.

Le modèle s'exécute en premier car nous voulons obtenir ses chiffres de latence quel que soit le résultat. Ensuite, la recherche de profil (plafond de dépenses quotidien, activation des transactions internationales, pays de résidence) alimente quelques instructions conditionnelles (if-statements). La réponse inclut le temps d'exécution de chaque étape afin que vous puissiez voir exactement où passent les millisecondes.
La mise à jour de votre profil (modification de votre plafond journalier, activation/désactivation des transactions internationales) est une action distincte. Vous enregistrez les modifications dans la base de données, et le paiement suivant les prend en compte. Pas de redéploiement, pas d'invalidation de cache.
Lorsqu'un modèle est déployé derrière Databricks Model Serving, il y a un saut réseau entre votre application et le conteneur d'inférence. Pour les traitements par lots (batch), quelques millisecondes supplémentaires par requête n'ont pas d'importance. Pour une expérience de paiement, cela change tout.
L'optimisation des routes raccourcit ce chemin réseau. Lorsque vous activez l'optimisation des routes sur un point de terminaison (endpoint), Databricks Model Serving améliore le chemin réseau pour les requêtes d'inférence, ce qui permet une communication plus rapide et plus directe entre votre client et le modèle. Ce routage optimisé permet d'obtenir un nombre de requêtes par seconde (QPS) plus élevé par rapport aux points de terminaison non optimisés, ainsi que des latences plus stables et plus faibles pour vos applications.
Vous l'activez lors de la création du point de terminaison, et vous effectuez vos requêtes via le flux du data-plane en utilisant OAuth, et non des jetons d'accès personnels. Vous bénéficiez d'une latence plus faible et d'un débit plus élevé pour la même puissance de calcul, ce qui est exactement ce dont a besoin un cas d'usage interactif d'évaluation de la fraude.
Dans l'application exemple, le point de terminaison s'appelle fraud-detection-lakebase. Voici la constante et la fonction qui l'appelle :
Quelques points à noter :
**serving_endpoints_data_plane.query** : Il s'agit du chemin de requête du data-plane, utilisé par l'optimisation des routes. Le SDK Databricks gère l'échange de jetons OAuth en arrière-plan.**asyncio.to_thread** : La méthode de requête du SDK est synchrone. L'envelopper dans to_thread permet de libérer la boucle d'événements (event loop) de FastAPI pendant l'exécution du modèle.**dataframe_records** : La charge utile (payload) est une liste de dictionnaires (un par ligne). Pour l'évaluation de la fraude, nous envoyons une transaction à la fois.Le modèle lui-même renvoie fraud_probability, fraud_flag et (point crucial) son propre timing interne : lookup_ms (le temps qu'a pris la recherche de caractéristiques au sein du conteneur du modèle), inference_ms (prédiction CatBoost) et total_ms. Le backend mappe ces données afin que le frontend puisse afficher une cascade de latence :
Ainsi, lorsque vous voyez la répartition de la latence dans l'UI (« Inférence du modèle : 45 ms » avec « Recherche de caractéristiques : 8 ms » imbriquée en dessous), il s'agit des chiffres mesurés à chaque couche et regroupés dans une seule réponse.
Pour en savoir plus sur cette configuration : Optimisation des routes · Interroger des points de terminaison optimisés pour les routes.
Le modèle de fraude ne se contente pas d'analyser la transaction de manière isolée. Il recherche les caractéristiques historiques du client (montant moyen des transactions, ratio transfrontalier, taux de rejet de débit, fréquence au cours des dernières 24 heures) en utilisant les six premiers chiffres de la carte bancaire (le BIN) comme clé de recherche. Ces caractéristiques résident dans une table Postgres Lakebase appelée customer_features.
Il s'agit de la même table dans laquelle le backend lit les données de profil (nom, plafond journalier, activation des transactions internationales). Une seule table, deux lecteurs : le conteneur du modèle lit les caractéristiques pour l'inférence, tandis que l'application FastAPI lit les champs de profil pour les règles métier.
Côté lecture, le backend emprunte une connexion au pool, exécute un SELECT paramétré par user_id, puis renvoie la connexion dans un bloc finally. Du psycopg2 classique, mais la discipline d'emprunt/restitution est essentielle lorsque cela s'exécute à chaque transaction. La requête utilise des espaces réservés (placeholders) paramétrés pour les entrées utilisateur, de sorte que la clé de recherche n'est jamais concaténée dans le SQL.
Côté écriture, lorsqu'un utilisateur modifie son plafond journalier ou active/désactive les transactions internationales dans l'UI, le backend génère un UPDATE dynamique à partir d'une liste d'autorisation (allowlist) de colonnes modifiables. Seuls les champs de cette liste sont écrits ; le client ne peut pas injecter de noms de colonnes arbitraires. L'écriture s'exécute avec autocommit = True, de sorte que la modification est immédiatement visible : la transaction suivante prend en compte le plafond mis à jour sans attendre un vidage de lot (batch flush) ou une invalidation de cache.
Chaque transaction dans cette application interroge Lakebase au moins deux fois : une fois dans le conteneur du modèle pour la recherche de caractéristiques, et une fois dans le backend pour la vérification du profil. Ouvrir une nouvelle connexion à chaque fois impliquerait un établissement de liaison (handshake) TCP ainsi qu'une négociation TLS pour chaque requête, ce qui ajouterait facilement 20 à 50 ms de surcharge par appel. Un pool de connexions maintient quelques connexions ouvertes et prêtes, de sorte que la plupart des requêtes en récupèrent simplement une et s'exécutent.
Voici à quoi cela ressemble en pratique. Chaque opération de base de données suit le même schéma emprunt/requête/restitution :
Empruntez une connexion, exécutez votre requête, puis renvoyez la connexion dans un bloc finally afin qu'elle soit toujours restituée, même en cas d'erreur. Chaque lecture et écriture dans l'application suit ce même modèle.
Le pool lui-même est un psycopg2.pool.ThreadedConnectionPool contenant 3 à 10 connexions. Mais c'est lors de sa création que les choses deviennent intéressantes, car Lakebase s'authentifie via OAuth. L'application échange les identifiants du principal de service contre un jeton d'accès, puis utilise l'ID client comme nom d'utilisateur Postgres et le jeton comme mot de passe. Pas de mots de passe de base de données à longue durée de vie.
Cela signifie que lorsque le jeton expire, vous ne pouvez pas simplement continuer à utiliser l'ancien mot de passe sur les connexions du pool, car elles échoueraient lors de la requête suivante. Ainsi, _ensure_pool vérifie si le jeton a changé et, si c'est le cas, crée un nouveau pool :
La plupart du temps, le chemin rapide est emprunté : le pool existe, le jeton n'a pas changé et nous renvoyons le résultat immédiatement. Lorsque le jeton est renouvelé, le verrouillage par double vérification empêche deux threads de recréer le pool en même temps, et le minuteur de 30 secondes pour la fermeture de l'ancien pool donne aux requêtes en cours le temps de se terminer avant que leurs connexions ne disparaissent.
Le modèle de fraude lui-même (déployé en tant que pyfunc MLflow) effectue sa propre recherche Lakebase au moment de la prédiction. Il extrait le BIN de la carte, interroge customer_features, assemble un vecteur de caractéristiques et exécute l'inférence CatBoost. Chaque étape est chronométrée :
Le conteneur du modèle maintient sa propre ThreadedConnectionPool vers Lakebase (la classe LakebaseConnectionPool dans fraud_model.py), avec un rafraîchissement du jeton en arrière-plan afin que le pool reste valide sur les instances de service à longue durée d'exécution. Il s'agit du même modèle que le backend (pool + rotation OAuth), mais s'exécutant à l'intérieur du conteneur du modèle plutôt que dans le processus FastAPI.
Ces valeurs lookup_ms et inference_ms sont renvoyées via la réponse du service, transitent par le backend et arrivent dans le frontend. C'est ainsi que vous obtenez une visibilité de bout en bout : le modèle signale son temps d'exécution interne, le backend ajoute sa propre mesure de temps réel (wall-clock) et l'utilisateur voit l'ensemble.
Une fois que le modèle a évalué la transaction, le backend lit le profil du client depuis Lakebase et applique deux règles simples. Tout d'abord, il compare le montant de la transaction à la limite de dépenses quotidienne de l'utilisateur. Si le montant dépasse le plafond, la transaction est refusée avec un message indiquant à l'utilisateur qu'il peut l'augmenter dans les paramètres de son profil. Deuxièmement, si l'utilisateur a désactivé les transactions internationales, le backend vérifie si le pays de la transaction correspond au pays de résidence de l'utilisateur. Une non-correspondance entraîne un refus.
L'une ou l'autre de ces règles peut outrepasser une approbation du modèle. Une transaction que le modèle juge correcte peut tout de même être refusée parce que l'utilisateur a fixé un plafond quotidien de 500 $. C'est intentionnel. Le modèle gère le risque statistique ; le profil gère les préférences de l'utilisateur. Tous deux lisent à partir de la même table Lakebase, mais ils répondent à des objectifs différents.
Le temps passé sur la recherche de profil et les vérifications des règles est suivi sous le nom de business_logic_ms et renvoyé aux côtés du temps d'exécution du modèle, afin que vous puissiez voir exactement quelle surcharge la logique métier ajoute à chaque transaction.
En production, votre instance Postgres doit gérer à la fois les recherches de caractéristiques du conteneur de modèle et les lectures de profil du backend, potentiellement plusieurs de chaque par seconde pendant les heures de pointe. Lakebase s'adapte automatiquement, ajustant les ressources de calcul dans une plage min/max configurée afin que vous ne payiez pas pour la capacité de pointe à 3 heures du matin, tout en évitant de perdre des requêtes à midi.
Dans les résultats du benchmark ci-dessous, les temps de recherche constants inférieurs à dix millisecondes de p50 à p75 reflètent ce qu'une instance Lakebase chaude fournit sous une charge constante. Le pic à p95 (13,9 ms) est typique de l'agitation du pool de connexions ou de brefs événements de mise à l'échelle, restant largement dans les limites du budget de latence pour un flux de paiement, et correspond exactement au type de pic que la mise à l'échelle automatique absorbe avant qu'il ne devienne visible pour l'utilisateur. Lorsque la réduction à zéro est activée, vous arrêtez également de payer lorsqu'aucune transaction n'est en cours.
Voici l'aperçu complet de la latence pour une seule transaction :
| Mesure | Ce qu'elle capture | Où elle est mesurée |
|---|---|---|
| model_call_ms | Temps réel (wall-clock) pour l'intégralité de l'appel de service | Backend (router.py) |
| model_lookup_ms | Recherche de caractéristiques dans le conteneur du modèle | Modèle (fraud_model.py) |
| model_interfere_ms | Temps de prédiction CatBoost | Modèle (fraud_model.py) |
| model_total_ms | Temps total dans le conteneur du modèle | Modèle (fraud_model.py) |
| business_logic_ms | Lecture du profil + évaluation des règles | Backend (router.py) |
| backend_total_ms | Temps réel (wall-clock) du début de la requête jusqu'à l'appel du modèle de fraude | Backend (router.py) |
L'écart entre round_trip_ms et model_total_ms correspond à la surcharge réseau, et c'est précisément là que l'optimisation des routes intervient. L'écart entre backend_total_ms et model_call_ms correspond à la surcharge du framework (sérialisation, routage, etc.).
Lorsque vous lancez l'application et soumettez une transaction, l'UI affiche les éléments clés : l'inférence du modèle, la recherche de caractéristiques et la logique métier. Cela permet de voir facilement la différence apportée par l'optimisation des routes, ou de montrer qu'une recherche de caractéristiques Lakebase n'ajoute que quelques millisecondes plutôt que les centaines auxquelles on pourrait s'attendre avec une connexion de base de données froide.
Nous avons envoyé 5 000 requêtes séquentielles (concurrency = 1, délai de 50 ms entre les appels) au point de terminaison fraud-detection-lakebase optimisé pour les routes (CPU, taille de charge de travail « Small », région Azure unique) et avons collecté la latence à chaque couche, depuis l'intérieur du conteneur de modèle jusqu'au trajet aller-retour de l'appelant. L'objectif était d'isoler l'anatomie de la latence par requête, de voir où vont les millisecondes à chaque couche (recherche de caractéristiques, inférence, surcharge réseau), plutôt que de tester la charge du débit sous contention. Ces chiffres proviennent du script de benchmark (scripts/benchmark.py), qui appelle directement le point de terminaison du modèle. La répartition de la latence de l'UI montre une autre perspective (inférence du modèle, recherche de caractéristiques et logique métier) mesurée tout au long de la route backend complète.
| Métrique | Ce qu'elle mesure | p50 | p75 | p90 | p95 |
|---|---|---|---|---|---|
| Recherche de caractéristiques (model_lookup_ms) | Lecture Lakebase à l'intérieur du conteneur du modèle | 8,9 ms | 9,8 ms | 11,7 ms | 13,9 ms |
| Inférence (model_inference_ms) | Prédiction CatBoost | 0.4 ms | 0.5 ms | 1.6 ms | 6.0 ms |
| Temps total du modèle (model_total_ms) | Recherche + inférence + surcharge du conteneur | 9.5 ms | 10.9 ms | 14.9 ms | 17.6 ms |
| Aller-retour de bout en bout (round_trip_ms) | Appel complet du plan de données de l'appelant à la réponse | 27.2 ms | 29.6 ms | 33.8 ms | 37.3 ms |
| Surcharge réseau (round_trip_ms - model_total_ms) | Aller-retour moins le temps du modèle | 17.4 ms | 18.5 ms | 19.8 ms | 21.1 ms |
Quelques points clés se dégagent :
L'application est conçue comme une Databricks App (back-end FastAPI, front-end React) à l'aide d' apx. Installez-la en utilisant :
Vous devrez également configurer l'authentification Databricks CLI pour l'espace de travail où le point de terminaison de service de modèle (model serving) et l'instance Lakebase sont déployés.
Pour l'exécuter localement :
Pour la déployer dans votre espace de travail :
Les fichiers clés :
src/retail_app/backend/router.py: Point de terminaison des transactions, détection de la fraude, règles métiersrc/retail_app/backend/postgres.py: Pool de connexions Lakebase, CRUD de profilmodel_training/fraud_model.py: MLflow pyfunc avec recherche de caractéristiques (features) et CatBoostapp.yml: Configuration de déploiement (point d'entrée uvicorn, variables d'environnement)Les cas d'usage en temps réel avec des exigences de latence inférieures à 50 ms, tels que l'évaluation de la fraude (fraud scoring), la personnalisation ou la tarification dynamique, peuvent être créés de manière native sur Databricks. L'optimisation du routage de Model Serving et Lakebase placent le chemin d'inférence et le chemin des données sur la même plateforme gouvernée. Si vous aviez conclu auparavant qu'une architecture lakehouse ne pouvait pas répondre aux exigences de latence d'un processus de paiement, les chiffres de référence présentés ici (médiane de 27 ms de bout en bout avec des recherches de caractéristiques inférieures à 10 ms) méritent un second coup d'œil. Si une charge de travail en temps réel figurait jusqu'ici sur votre liste des tâches « trop difficiles », c'est le moment de la réexaminer. Déployez l'application dans votre propre espace de travail pour visualiser vous-même la cascade de latence, et explorez Lakebase ainsi que Databricks Model Serving.
(Cet article de blog a été traduit à l'aide d'outils basés sur l'intelligence artificielle) Article original
Abonnez-vous à notre blog et recevez les derniers articles directement dans votre boîte mail.