Revenir au contenu principal
Ingénierie

Que se passe-t-il dans les millisecondes qui suivent votre appui sur « Payer »

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

  • Une application Databricks de démonstration (FastAPI + React) qui évalue le risque de fraude des transactions par carte de crédit en temps réel, en utilisant l'optimisation des routes de Model Serving pour une inférence à faible latence et Lakebase Postgres pour les recherches en ligne de features et de profils.
  • Une inférence rapide ne suffit pas. L'application associe un Model Serving optimisé pour les routes à Lakebase, ainsi que le pooling de connexions, la rotation des jetons OAuth et des modèles d'autoscaling qui maintiennent la latence stable sous la charge.
  • Sur 5 000 requêtes, l'endpoint optimisé pour les routes a répondu en 27 ms au p50 et 37 ms au p95 de bout en bout, avec un temps médian de 8,9 ms pour les recherches de features Lakebase et un taux de réussite de 100 %, ce qui respecte largement les budgets de latence de paiement.

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 :

  • Model Serving avec optimisation des routes (route optimization), un chemin réseau plus rapide vers votre modèle déployé.
  • Lakebase, un service Postgres géré pour les données de profil et de caractéristiques (features) dont le modèle a besoin lors de la prédiction, avec une mise à l'échelle automatique (autoscaling) pour que la base de données s'adapte à la demande au lieu de devenir le nouveau goulot d'étranglement.

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 ».

Le flux : que se passe-t-il réellement lors d'une transaction ?

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.

image2.png

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.

Optimisation des routes : pourquoi le chemin réseau est important

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.

Lakebase : Postgres pour les données dont le modèle a besoin

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.

Pool de connexions et rotation des jetons OAuth

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.

Côté modèle : recherche de caractéristiques dans le conteneur

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.

Règles métier : la vérification du profil

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.

Mise à l'échelle automatique de Lakebase avec réduction à zéro : gestion de la demande

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.

En résumé : répartition de la latence de bout en bout

Voici l'aperçu complet de la latence pour une seule transaction :

MesureCe qu'elle captureOù elle est mesurée
model_call_msTemps réel (wall-clock) pour l'intégralité de l'appel de serviceBackend (router.py)
model_lookup_msRecherche de caractéristiques dans le conteneur du modèleModèle (fraud_model.py)
model_interfere_msTemps de prédiction CatBoostModèle (fraud_model.py)
model_total_msTemps total dans le conteneur du modèleModèle (fraud_model.py)
business_logic_msLecture du profil + évaluation des règlesBackend (router.py)
backend_total_msTemps réel (wall-clock) du début de la requête jusqu'à l'appel du modèle de fraudeBackend (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.

Résultats : quelle est la vitesse réelle ?

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étriqueCe qu'elle mesurep50p75p90p95
Recherche de caractéristiques (model_lookup_ms)Lecture Lakebase à l'intérieur du conteneur du modèle8,9 ms9,8 ms11,7 ms13,9 ms
Inférence (model_inference_ms)Prédiction CatBoost0.4 ms0.5 ms1.6 ms6.0 ms
Temps total du modèle (model_total_ms)Recherche + inférence + surcharge du conteneur9.5 ms10.9 ms14.9 ms17.6 ms
Aller-retour de bout en bout (round_trip_ms)Appel complet du plan de données de l'appelant à la réponse27.2 ms29.6 ms33.8 ms37.3 ms
Surcharge réseau (round_trip_ms - model_total_ms)Aller-retour moins le temps du modèle17.4 ms18.5 ms19.8 ms21.1 ms

Quelques points clés se dégagent :

  • L'aller-retour de bout en bout est de 27 ms à la médiane, et de 37 ms au p95. C'est le parcours complet : appelant → plan de données optimisé pour le routage → conteneur de modèle → recherche Lakebase → inférence CatBoost → réponse. Ce qui respecte largement le budget de latence pour un processus de paiement.
  • La recherche de caractéristiques (features) ne prend que quelques millisecondes (un seul chiffre) au p50 (8,9 ms). Le pool de connexions du modèle vers Lakebase maintient les connexions actives, de sorte que la plupart des lectures évitent complètement la phase de handshake TLS. Même au p95, la recherche reste inférieure à 14 ms.
  • L'inférence est quasiment gratuite. La prédiction CatBoost sur un vecteur de 12 caractéristiques (features) prend 0,4 ms à la médiane. Le temps du modèle est dominé par la recherche de caractéristiques, et non par la prédiction elle-même.
  • La surcharge réseau est d'environ 17 ms. L'écart entre ce que le conteneur de modèle indique et ce que l'appelant voit correspond à l'infrastructure de service : routage des requêtes, sérialisation et saut dans le plan de données. L'optimisation du routage maintient cette cohérence : l'écart entre le p50 et le p95 n'est que de 4 ms.

Essayez par vous-même : déployez l'application dans votre espace de travail

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étier
  • src/retail_app/backend/postgres.py: Pool de connexions Lakebase, CRUD de profil
  • model_training/fraud_model.py: MLflow pyfunc avec recherche de caractéristiques (features) et CatBoost
  • app.yml: Configuration de déploiement (point d'entrée uvicorn, variables d'environnement)

Documentation associée

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

Recevez les derniers articles dans votre boîte mail

Abonnez-vous à notre blog et recevez les derniers articles directement dans votre boîte mail.