Configurer un test de charge pour les points de terminaison de recherche vectorielle

Cette page fournit des conseils, des exemples de code et un exemple de notebook pour le test de charge des points de terminaison de recherche vectorielle. Le test de charge vous aide à comprendre les performances et la préparation de la production d’un point de terminaison de recherche vectorielle avant son déploiement en production. Les tests de charge peuvent vous indiquer les points suivants :

  • Latence à différents niveaux de mise à l’échelle
  • Limites de débit et goulots d’étranglement (demandes par seconde, répartition de la latence)
  • Taux d’erreurs sous charge soutenue
  • Utilisation des ressources et planification de la capacité

Pour plus d’informations sur le test de charge et les concepts connexes, consultez Tests de charge pour les points de terminaison de service.

Spécifications

Avant de commencer ces étapes, vous devez disposer d’un point de terminaison de recherche vectorielle déployé et d’un principal de service disposant d’autorisations Can Query sur le point de terminaison. Consultez l’étape 1 : Configurer l’authentification du principal de service.

Téléchargez et importez une copie des fichiers suivants et des exemples de notebooks dans votre espace de travail Azure Databricks :

  • input.json. Il s’agit d’un exemple de input.json fichier qui spécifie la charge utile envoyée par toutes les connexions simultanées à votre point de terminaison. Vous pouvez avoir plusieurs fichiers si nécessaire. Si vous utilisez l’exemple de notebook, ce fichier est généré automatiquement à partir de la table d’entrée fournie.
  • fast_vs_load_test_async_load.py. Chargez ce script dans votre espace de travail (par exemple /Workspace/Users/<your-username>/fast_vs_load_test_async_load.py) et définissez le paramètre notebook locust_script_path sur son chemin d’accès. Ce script gère l’authentification, la remise de charge utile et la collecte des métriques de débogage.
  • Le notebook d'exemple suivant, qui exécute les tests de charge. Pour des performances optimales, exécutez ce notebook sur un cluster à nœud unique avec un grand nombre de cœurs (échelles locust sur tous les processeurs disponibles). Il est recommandé d'avoir une grande quantité de mémoire pour les requêtes avec des embeddings prégénérés.

Exemple de notebook et démarrage rapide

Utilisez l’exemple de notebook suivant pour commencer. Il prend en charge deux modes d'exploration : un balayage progressif qui teste des niveaux de parallélisme spécifiques que vous définissez, et un mode de recherche binaire qui trouve automatiquement le point de rupture durable maximal en quelques étapes. Tous les paramètres sont configurés à l’aide de widgets, afin que le notebook puisse s’exécuter de manière interactive ou en tant que travail Databricks sans modification de code.

Bloc-notes de test de performance de charge Locust

Obtenir un ordinateur portable

Infrastructure de test de charge : Locust

Locust est une infrastructure de test de charge open source qui vous permet d’effectuer les opérations suivantes :

  • Varier le nombre de connexions clientes simultanées
  • Contrôler la vitesse de génération des connexions
  • Mesurer les performances des points de terminaison tout au long du test
  • Détecter et utiliser automatiquement tous les cœurs de processeur disponibles

L’exemple de notebook utilise l’indicateur --processes -1 pour détecter automatiquement les cœurs du processeur et les utiliser pleinement.

Si Locust est goulot d’étranglement par le processeur, un message apparaît dans la sortie.

Étape 1 : Configurer l’authentification du principal de service

Important

Pour les tests de performances de type production, utilisez toujours l’authentification du principal de service OAuth. Les principaux de service fournissent jusqu’à 100 ms de temps de réponse plus rapide et des limites de taux de requêtes plus élevées par rapport aux jetons d’accès personnels (PAT).

Créer et configurer un principal de service

  1. Créez un principal de service Databricks. Pour obtenir des instructions, consultez Ajouter des entités de service à votre compte.

  2. Accordez des autorisations :

    • Accédez à votre page de point de terminaison de recherche vectorielle.
    • Cliquez sur Autorisations.
    • Donnez au principal de service peut interroger les autorisations.
  3. Créez un secret OAuth.

    • Accédez à la page de détails du principal de service.
    • Cliquez sur l’onglet Secrets.
    • Cliquez sur Générer un secret.
    • Définissez la durée de vie (recommandez 365 jours pour les tests à long terme).
    • Copiez immédiatement l’ID client et le secret.
  4. Stockez les informations d’identification de manière sécurisée.

    • Créez un espace des secrets Databricks. Pour obtenir des instructions, consultez Tutoriel : Créer et utiliser un secret Databricks.
    • Comme indiqué dans l’exemple de code suivant, stockez l’ID client du principal de service en tant que service_principal_client_id et stockez le secret OAuth en tant que service_principal_client_secret.
    # In a Databricks notebook
    dbutils.secrets.put("load-test-auth", "service_principal_client_id", "<CLIENT_ID>")
    dbutils.secrets.put("load-test-auth", "service_principal_client_secret", "<SECRET>")
    

Étape 2 : Configurer votre test de charge

Configuration du notebook

Configurez les paramètres du bloc-notes à l’aide des widgets en haut du bloc-notes. Lors de l'exécution du notebook en tant que Databricks Job, transmettez ces valeurs en tant que paramètres de Job. Aucune modification de code n’est nécessaire.

Paramètre Descriptif Valeur recommandée
endpoint_name Nom de votre point de terminaison de recherche vectorielle Nom de votre point de terminaison
index_name Nom complet de l’index (catalog.schema.index) Nom de votre index
test_table Table source pour exemples de requêtes à partir de (catalog.schema.table) Votre table d’entrée d’index
query_column Colonne de texte à utiliser pour les incorporations managées Laissez comme text ou définissez-le au nom de votre colonne
embedding_column Colonne contenant des vecteurs d’incorporation précomputés. Utilisé uniquement pour les incorporations auto-managées. Laissez vide pour les incorporations managées
sample_size Nombre de requêtes à échantillonner pour le test 1000
target_concurrencies Liste séparée par des virgules des nombres de clients simultanés à tester 5,10,20,50
step_duration_seconds Durée en secondes par niveau d’accès concurrentiel. Une valeur s’applique à tous les niveaux ou fournit une par niveau sous la forme d’une liste séparée par des virgules. 300 (5 minutes)
secret_scope_name Nom de votre étendue de secret Databricks Nom de votre étendue
locust_script_path Chemin d’accès de l’espace de travail au fast_vs_load_test_async_load.py script /Workspace/Users/<your-username>/fast_vs_load_test_async_load.py
output_table (Facultatif) Table Delta dans laquelle stocker les résultats (catalog.schema.table). Créé automatiquement lors de la première exécution. catalog.schema.load_test_results
run_name Indiquez un nom ou un commentaire pour marquer cette exécution pour une analyse ultérieure Une étiquette descriptive
exploration_mode gradual parcourt target_concurrencies dans l’ordre. binary_search recherche automatiquement le point de rupture (voir exploration du point de rupture). gradual
max_target_qps (binary_search uniquement) Limite supérieure pour la recherche QPS 500
exploration_steps (binary_search uniquement) Nombre maximal d’itérations de recherche binaire 8
error_rate_threshold (binary_search uniquement) Taux d’erreur acceptable maximal (%) pour qu’une étape soit comptabilisée comme un succès 1.0
num_results Nombre de résultats à retourner par requête 10
columns_to_return Liste séparée par des virgules de colonnes à retourner dans les résultats de requête (par exemple, id,text). Laisser vide pour afficher toutes les colonnes. Laissez vide pour la valeur par défaut

Incorporations managées et auto-managées

Le notebook prend en charge les incorporations managées (où Databricks génère des incorporations au moment de la requête) et les incorporations auto-managées (où vous passez directement des vecteurs précomputés). Configurez les paramètres appropriés en fonction de votre type d’index.

Type d’index Paramètre à définir Laissez non défini
Incorporations managées (index Delta Sync avec modèle d’incorporation géré par Databricks) query_column — nom de colonne de texte à utiliser comme requête embedding_column (laissez vide)
Incorporations autogétées (index Delta Sync ou Direct Vector Access avec des vecteurs précomputés) embedding_column — colonne contenant des vecteurs d’incorporation précomputés query_column

Note

Pour les index d'incorporation gérés, le test de charge mesure la latence de bout en bout, y compris le temps de génération de l'intégration. Si votre point de terminaison d’intégration est mis à l’échelle à zéro, une latence due au démarrage à froid apparaîtra lors du premier test. Consultez Identifier le goulot d’étranglement du modèle d’incorporation pour savoir comment isoler la latence d’incorporation de la latence de recherche.

Pourquoi 5 à 10 minutes ?

Une durée minimale de test de 5 minutes est critique.

  • Les requêtes initiales peuvent inclure une pénalité de démarrage à froid.
  • Les points de terminaison ont besoin de temps pour atteindre une performance à l'état stable.
  • La mise à l’échelle automatique des points de terminaison de service du modèle (si activé) prend du temps à s’activer.
  • Les tests courts ne détectent pas les comportements de limitation sous charge soutenue.

Le tableau suivant présente les durées de test recommandées en fonction de votre objectif de test.

Type de test Durée du test Objectifs du test
Test de fumée rapide 2 à 3 minutes Vérifier les fonctionnalités de base
Base de référence des performances 5-10 minutes Métriques à état stable fiables
Test de stress 15 à 30 minutes Identifier l’épuisement des ressources
Tests d’endurance 1 à 4 heures Dégradation, stabilité de la latence

Exploration du seuil de rupture (mode de recherche binaire)

Outre le balayage progressif (exploration_mode=gradual), le notebook prend en charge un mode de recherche binaire automatique qui localise le maximum des QPS supportables sans que vous deviez spécifier manuellement les niveaux de concurrence.

Fonctionnement

Définissez et spécifiez exploration_mode=binary_searchmax_target_qps (par exemple, 500). Le notebook utilise la loi de Little (concurrency = QPS × avg_latency_sec) pour convertir chaque cible QPS en un niveau de simultanéité estimé, puis exécute une recherche binaire comme suit :

  1. Commencez à max_target_qps / 2 (250 dans l’exemple).
  2. Si le taux d’erreur est inférieur error_rate_threshold (réussite), augmentez la limite inférieure et essayez un QPS plus élevé (375, puis 500, etc.).
  3. Si le taux d’erreur dépasse le seuil (échec), réduisez la limite supérieure et essayez à mi-chemin entre le dernier succès et l’échec.
  4. Répétez jusqu'à exploration_steps étapes (par défaut 8) ou, jusqu'à ce que la plage de recherche se réduise à moins de 5 % de max_target_qps.

Le tableau suivant montre comment la recherche converge pour un point de terminaison hypothétique avec un point de rupture autour de 430 QPS :

Étape QPS cible Taux d’erreur Résultat Nouvelle gamme
1 250 0.1% SUCCÈS [250, 500]
2 375 0.3% SUCCÈS [375, 500]
3 437 4.5% ÉCHEC [375, 437]
4 406 0,8 % SUCCÈS [406, 437]
5 421 2.1% ÉCHEC [406, 421]

Après 5 à 8 étapes, la recherche converge sur le point de rupture ( dans cet exemple, environ 406 à 421 QPS) avec beaucoup moins d’exécutions de test qu’un balayage exhaustif.

Quand utiliser chaque mode

Mode Quand utiliser
gradual Vous connaissez déjà la plage d’exploitation attendue et souhaitez caractériser les performances à des niveaux de simultanéité spécifiques.
binary_search Vous souhaitez rapidement déterminer le QPS maximal durable, sans connaître à l'avance les niveaux de concurrence.

Étape 3. Concevoir votre jeu de requêtes

Dans la mesure du possible, l’ensemble de requêtes doit refléter le trafic de production attendu aussi étroitement que possible. Plus précisément, vous devez essayer de faire correspondre la distribution attendue des requêtes en termes de contenu, de complexité et de diversité.

  • Utilisez des requêtes réalistes. N’utilisez pas de texte aléatoire tel que « requête de test 1234 ».

  • Mettre en correspondance la distribution du trafic de production attendue. Si vous attendez 80% requêtes courantes, 15% requêtes de fréquence moyenne et 5% requêtes peu fréquentes, votre jeu de requêtes doit refléter cette distribution.

  • Correspond au type de requête que vous prévoyez de voir en production. Par exemple, si vous prévoyez que les requêtes de production utilisent la recherche hybride ou les filtres, vous devez également les utiliser dans votre jeu de requêtes.

    Exemple de requête utilisant des filtres :

    {
      "query_text": "wireless headphones",
      "num_results": 10,
      "filters": { "brand": "Sony", "noise_canceling": true }
    }
    

    Exemple de requête à l’aide de la recherche hybride :

    {
      "query_text": "best noise canceling headphones for travel",
      "query_type": "hybrid",
      "num_results": 10
    }
    

Diversité et mise en cache des requêtes

Les points de terminaison de recherche vectorielle cachent plusieurs types de résultats de requête pour améliorer les performances. Cette mise en cache peut affecter les résultats des tests de charge. Pour cette raison, il est important de prêter attention à la diversité de l’ensemble de requêtes. Par exemple, si vous envoyez à plusieurs reprises le même ensemble de requêtes, vous testez le cache, et non les performances de recherche réelles.

Utilisez : Quand : Example
Requêtes identiques ou rares
  • Votre trafic de production a une répétition de requête élevée (par exemple, « produits populaires »)
  • Vous testez spécifiquement l’efficacité du cache
  • Votre application bénéficie de la mise en cache (par exemple, des tableaux de bord avec des requêtes fixes)
  • Vous souhaitez mesurer les performances en cache dans le meilleur cas
Widget de recommandation de produit qui affiche des « éléments tendances » : la même requête s’exécute des milliers de fois par heure.
Requêtes diverses
  • Votre trafic de production comporte des requêtes utilisateur uniques (par exemple, des moteurs de recherche ou des chatbots)
  • Vous souhaitez mesurer les performances non mises en cache du pire cas
  • Vous souhaitez tester les performances de l’analyse d’index, et non les performances du cache
  • Les requêtes ont une cardinalité élevée (millions de variantes possibles)
Une recherche de commerce électronique où chaque utilisateur tape des recherches de produits différentes.

Pour obtenir des recommandations supplémentaires, consultez Résumé des meilleures pratiques.

Options de création d’un jeu de requêtes

Les onglets de code affichent trois options pour créer un ensemble de requêtes diversifié. Il n’y a pas de solution universelle. Choisissez celui qui fonctionne le mieux pour vous.

  • (Recommandé) Échantillonnage aléatoire à partir de la table d’entrée d’index. C’est un bon point de départ général.
  • Échantillonnage à partir des journaux de production. C'est un bon début si vous avez des journaux de production. Gardez à l’esprit que les requêtes changent généralement au fil du temps, donc actualisez régulièrement le jeu de tests pour le maintenir à jour.
  • Génération de requêtes synthétiques. Cela est utile si vous n’avez pas de journaux de production ou si vous utilisez des filtres complexes.

Échantillonnage aléatoire à partir de la table d’entrée

Le code suivant échantillonne des requêtes aléatoires à partir de votre table d’entrée d’index.

import pandas as pd
import random

# Read the index input table
input_table = spark.table("catalog.schema.index_input_table").toPandas()

# Sample random rows
n_samples = 1000
if len(input_table) < n_samples:
    print(f"Warning: Only {len(input_table)} rows available, using all")
    sample_queries = input_table
else:
    sample_queries = input_table.sample(n=n_samples, random_state=42)

# Extract the text column (adjust column name as needed)
queries = sample_queries['text_column'].tolist()

# Create query payloads
query_payloads = [{"query_text": q, "num_results": 10} for q in queries]

# Save to input.json
pd.DataFrame(query_payloads).to_json("input.json", orient="records", lines=True)

print(f"Created {len(query_payloads)} diverse queries from index input table")

Exemple de journaux de production

Les exemples de code suivants proviennent proportionnellement des requêtes de production.

# Sample proportionally from production queries
production_queries = pd.read_csv("queries.csv")

# Take stratified sample maintaining frequency distribution
def create_test_set(df, n_queries=1000):
    # Group by frequency buckets
    df['frequency'] = df.groupby('query_text')['query_text'].transform('count')

    # Stratified sample
    high_freq = df[df['frequency'] > 100].sample(n=200)  # 20%
    med_freq = df[df['frequency'].between(10, 100)].sample(n=300)  # 30%
    low_freq = df[df['frequency'] < 10].sample(n=500)  # 50%

    return pd.concat([high_freq, med_freq, low_freq])

test_queries = create_test_set(production_queries)
test_queries.to_json("input.json", orient="records", lines=True)

Requêtes synthétiques

Si vous n’avez pas encore de logs de production, vous pouvez générer des requêtes synthétiques variées.

# Generate diverse queries programmatically
import random

# Define query templates and variations
templates = [
    "find {product} under ${price}",
    "best {product} for {use_case}",
    "{adjective} {product} recommendations",
    "compare {product1} and {product2}",
]

products = ["laptop", "headphones", "monitor", "keyboard", "mouse", "webcam", "speaker"]
prices = ["500", "1000", "1500", "2000"]
use_cases = ["gaming", "work", "travel", "home office", "students"]
adjectives = ["affordable", "premium", "budget", "professional", "portable"]

diverse_queries = []
for _ in range(1000):
    template = random.choice(templates)
    query = template.format(
        product=random.choice(products),
        product1=random.choice(products),
        product2=random.choice(products),
        price=random.choice(prices),
        use_case=random.choice(use_cases),
        adjective=random.choice(adjectives)
    )
    diverse_queries.append(query)

print(f"Generated {len(set(diverse_queries))} unique queries")

Étape 4. Tester votre charge utile

Avant d’exécuter le test de charge complète, validez votre charge utile :

  1. Dans l’espace de travail Databricks, accédez à votre point de terminaison de recherche vectorielle.
  2. Dans la barre latérale gauche, cliquez sur Service.
  3. Sélectionnez votre point de terminaison.
  4. Cliquez sur UtiliserRequête.
  5. Collez votre input.json contenu dans la zone de requête.
  6. Vérifiez que le point de terminaison retourne les résultats attendus.

Cela garantit que votre test de charge mesure les requêtes réalistes, et non les réponses d’erreur.

Étape 5. Exécuter le test de charge

Vérification de la connectivité et préchauffement

Avant le début du test de charge, le notebook effectue deux étapes de configuration :

  1. Vérification de la connectivité : Envoie une requête de sonde unique à l'aide des informations d'identification du service principal. Si l'interface retourne une erreur 401 ou 403, le notebook échoue immédiatement avec une indication claire PermissionError au lieu d’exécuter un test de charge complet qui ne produit que des données d'erreur. Cela permet de gagner du temps lorsque les informations d’identification ou les autorisations sont mal configurées.

  2. Test de préchauffage (1 minute) : exécute un test court à faible concurrence qui réchauffe les caches de point de terminaison et valide le flux de requête de bout en bout. Les résultats de la préchauffe ne sont pas utilisés pour les métriques de performances. En mode de recherche binaire, la latence de préchauffage est également utilisée comme référence pour l'estimation de la concurrence selon la loi de Little.

Série de tests de charge principale

Le notebook exécute une série de tests avec une concurrence des clients qui augmente progressivement.

  • Début : faible concurrence (par exemple, 5 clients simultanés)
  • Milieu : concurrence moyenne (par exemple, 10, 20 ou 50 clients)
  • Fin : concurrence élevée (par exemple, plus de 100 clients)

Chaque test s’exécute pendant la durée configurée en step_duration_seconds (5 à 10 minutes recommandées).

Ce que le bloc-notes mesure

Le notebook mesure et signale les éléments suivants :

Métriques de latence :

  • P50 (médiane) : La moitié des requêtes sont plus rapides que cela.
  • P95 : 95 % des requêtes sont plus rapides que ce temps. Il s’agit d’une métrique clé des SLA.
  • P99: 99% des requêtes sont exécutées plus rapidement.
  • Max: Latence du pire des cas.

Métriques de débit :

  • RPS (demandes par seconde) : Requêtes réussies par seconde.
  • Nombre total de requêtes : Nombre de requêtes terminées.
  • Taux de réussite : Pourcentage de requêtes réussies.

Erreurs:

  • Échecs de requête par type
  • Messages d’exception
  • Comptes des temps d'attente

Stockage des résultats

Si le paramètre output_table est défini, le bloc-notes stocke une ligne par niveau de concurrence (ou par étape de recherche binaire) dans une table Delta du Unity Catalog. La table est créée automatiquement lors de la première exécution et ajoutée lors des exécutions suivantes. Chaque ligne inclut run_name, concurrence exploration_mode, taux de réussite/échec, centiles de latence, RPS et champs spécifiques à la recherche binaire (bs_step, bs_target_qps, bs_outcome). Cela vous permet de comparer les exécutions au fil du temps à l’aide d’outils SQL ou BI.

Exécution en tant que travail Databricks

Tous les paramètres de notebook sont définis en tant que dbutils.widgets, qui correspondent directement aux paramètres Databricks Job. Pour planifier ou automatiser des tests de charge :

  1. Créez un travail avec le bloc-notes comme tâche.
  2. Définissez les valeurs du widget en tant que paramètres de travail. Aucune modification de code n’est nécessaire.
  3. Attachez le travail à un cluster à nœud unique avec de nombreux cœurs de processeur (Locust bénéficie des travailleurs parallèles).
  4. Exécutez à la demande ou selon une planification pour les tests de base périodiques.

Étape 6. Interpréter les résultats

Le tableau suivant présente des cibles pour de bonnes performances :

Unité de mesure Cible Commentaire
Latence P95 < 500 ms La plupart des requêtes sont rapides
Latence P99 < 1 seconde Performances raisonnables sur les requêtes de longue durée
Taux de réussite > 99,5% Faible taux d’échec
Latence au fil du temps Stable Aucune dégradation observée pendant le test
Requêtes par seconde Atteint l'objectif Le point de terminaison peut gérer le trafic attendu

Les résultats suivants indiquent des performances médiocres :

  • P95 > 1s. Indique que les requêtes sont trop lentes pour une utilisation en temps réel.
  • P99 > 3s. La latence sur les requêtes de longue durée nuit à l’expérience utilisateur.
  • < Taux de réussite 99%. Trop de défaillances.
  • Latence croissante. Indique l’épuisement des ressources ou la fuite de mémoire.
  • Erreurs de limitation de débit (429). Indique que la capacité de point de terminaison supérieure est requise.

Compromis entre RPS et latence

Le nombre maximal de RPS n’est pas le point optimal pour le débit de production. La latence augmente de façon non linéaire à mesure que vous approchez du débit maximal. L’exploitation à un nombre maximal de RPS entraîne souvent une latence de 2 à 5 fois supérieure à celle de 60 à 70% de capacité maximale.

L’exemple suivant montre comment analyser les résultats pour trouver le point d’exploitation optimal.

  • Le nombre maximal de RPS est de 480 à 150 clients simultanés.
  • Le point d’exploitation optimal est de 310 RPS à 50 clients simultanés (65% capacité).
  • Pénalité de latence au maximum : P95 est 4,3x plus élevé (1,5 s par rapport à 350 ms)
  • Dans cet exemple, la recommandation consiste à dimensionner le point de terminaison pour la capacité 480 RPS et à fonctionner à ~310 RPS.
Concurrency P50 P95 P99 RPS Success Capacity
5 80 ms 120 ms 150 ms 45 100 % 10 %
10 85 ms 140 ms 180 ms 88 100 % 20 %
20 95 ms 180 ms 250 ms 165 99,8% 35 %%
50 150 ms 350 ms 500 ms 310 99,2% 65% ← Point idéal
100 250 ms 800 ms 1.2s 420 97,5 % 90% ⚠️ Proche du maximum
150 450 ms 1,5s 2.5s 480 95,0% 100% RPS ❌ maximum

Le fonctionnement au maximum de RPS peut entraîner les problèmes suivants :

  • Dégradation de la latence. Dans l’exemple, P95 est de 350 ms à 65% de capacité, mais est de 1,5 s à 100% de capacité.
  • Pas de capacité pour gérer les pics ou surcharges de trafic. À 100% capacité, tout pic provoque un délai d’expiration. À 65% capacité, un pic de trafic de 50% peut être géré sans problème.
  • Augmentation des taux d’erreur. Dans l’exemple, le taux de réussite est de 99,2% à 65% capacité, mais 95,0% — un taux d’échec de 5% — à 100% capacité.
  • Risque d’épuisement des ressources. À charge maximale, les files d’attente augmentent, la pression de la mémoire augmente, les pools de connexions commencent à saturer et le temps de récupération après les incidents augmente.

Le tableau suivant présente les points d’exploitation recommandés pour différents cas d’usage.

Cas d’utilisation Capacité cible Justification
Respect de la latence (recherche, conversation) 50 à 60% du maximum Prioriser une faible latence P95/P99
Équilibré (recommandations) 60-70% du maximum Bon équilibre des coûts et de la latence
Optimisation des coûts (traitement par lots) 70-80 % du maximum Latence plus élevée acceptable
Non recommandé > 85% du maximum Pics de latence, aucune capacité de rafale

Fonctions d’assistance pour le calcul de la taille du point d’exploitation et du point de terminaison

Trouver le point optimal

Le code suivant trace la latence QPS et P95. Dans le tracé, recherchez le point où la courbe commence à se plier fortement vers le haut. Il s’agit du point d’exploitation optimal.

import matplotlib.pyplot as plt

# Plot QPS vs. P95 latency
qps_values = [45, 88, 165, 310, 420, 480]
p95_latency = [120, 140, 180, 350, 800, 1500]

plt.plot(qps_values, p95_latency, marker='o')
plt.axvline(x=310, color='green', linestyle='--', label='Optimal (65% capacity)')
plt.axvline(x=480, color='red', linestyle='--', label='Maximum (100% capacity)')
plt.xlabel('Queries Per Second (QPS)')
plt.ylabel('P95 Latency (ms)')
plt.title('QPS vs. Latency: Finding the Sweet Spot')
plt.legend()
plt.grid(True)
plt.show()

Formule de recommandation de taille

def calculate_endpoint_size(target_qps, optimal_capacity_percent=0.65):
    """
    Calculate required endpoint capacity

    Args:
        target_qps: Your expected peak production QPS
        optimal_capacity_percent: Target utilization (default 65%)

    Returns:
        Required maximum endpoint QPS
    """
    required_max_qps = target_qps / optimal_capacity_percent

    # Add 20% safety margin for unexpected bursts
    recommended_max_qps = required_max_qps * 1.2

    return {
        "target_production_qps": target_qps,
        "operate_at_capacity": f"{optimal_capacity_percent*100:.0f}%",
        "required_max_qps": required_max_qps,
        "recommended_max_qps": recommended_max_qps,
        "burst_capacity": f"{(1 - optimal_capacity_percent)*100:.0f}% headroom"
    }

# Example
result = calculate_endpoint_size(target_qps=200)
print(f"Target production QPS: {result['target_production_qps']}")
print(f"Size endpoint for: {result['recommended_max_qps']:.0f} QPS")
print(f"Operate at: {result['operate_at_capacity']}")
print(f"Available burst capacity: {result['burst_capacity']}")

# Output:
# Target production QPS: 200
# Size endpoint for: 369 QPS
# Operate at: 65%
# Available burst capacity: 35% headroom

Identifier le goulot d’étranglement du modèle incorporé

Si votre index utilise des embeddings gérés, le notebook de test de charge capture le temps de traitement par composant via le paramètre debug_level=1 pour chaque requête. Le tableau des résultats comprend les éléments suivants :

  • ann_time — temps consacré à la recherche approximative du voisin le plus proche
  • embedding_gen_time — temps passé à générer l’incorporation de requêtes sur le point de terminaison de service du modèle
  • reranker_time — temps consacré à la reclassement (si activé)
  • response_time — temps de réponse total de bout en bout

Si embedding_gen_time est systématiquement grand par rapport à ann_time, le point de terminaison d'incorporation est le facteur limitant, et non le point de terminaison de recherche vectorielle. Causes courantes :

  • L'endpoint de service du modèle d'intégration est avec mise à l'échelle à zéro activée. Désactivez-le pour les tests de charge de production. Voir Éviter le mode zéro pour la production.
  • Le point d'intégration n’a pas suffisamment de concurrence provisionnée pour le taux de requêtes que vous testez.
  • Le point de terminaison du modèle incorporé est partagé avec d’autres charges de travail. Utilisez un point de terminaison dédié pour les tests de charge.

Conseil / Astuce

Pour isoler les performances de recherche vectorielle des performances du modèle d’incorporation, basculez vers des incorporations autogéées pour les tests de charge. Passez des vecteurs précomputés dans le EMBEDDING_COLUMN paramètre au lieu de requêtes de texte. Cela supprime entièrement la latence d’intégration de la mesure.

Étape 7 : Dimensionner votre point de terminaison

Utiliser la recommandation du bloc-notes

Après avoir analysé les résultats, le notebook vous demande de :

  1. Sélectionnez la ligne qui répond le mieux à vos besoins en matière de latence.
  2. Entrez le RPS souhaité de votre application.

Le notebook affiche ensuite une taille de point de terminaison recommandée. Elle calcule la capacité requise en fonction des éléments suivants :

  • Votre RPS cible
  • Latence observée à différents niveaux d’accès concurrentiel
  • Seuils de taux de réussite
  • Marge de sécurité (généralement 2x charge maximale attendue)

Mise à l'échelle - Éléments à prendre en compte

Points de terminaison standard :

  • Monter automatiquement en puissance pour prendre en charge la taille d’index
  • Monter en puissance manuellement pour prendre en charge le débit
  • Effectuer un scale-down automatiquement lorsque les index sont supprimés
  • Effectuer un scale-down manuellement pour réduire la capacité

Points de terminaison optimisés pour le stockage :

  • Monter automatiquement en puissance pour prendre en charge la taille d’index
  • Effectuer un scale-down automatiquement lorsque les index sont supprimés

Étape 8 : Valider la configuration finale

Après avoir mis à jour votre configuration de point de terminaison :

  1. Attendez que le point de terminaison soit prêt. Cela peut prendre plusieurs minutes.
  2. Exécutez le test de validation final dans le notebook.
  3. Vérifiez que les performances répondent à vos besoins :
    • RPS ≥ débit cible
    • La latence P95 respecte l'ANS
    • > Taux de réussite 99,5%
    • Aucune erreur soutenue

En cas d’échec de la validation, essayez ce qui suit :

  • Augmenter la capacité du point de terminaison
  • Optimiser la complexité des requêtes
  • Passer en revue les performances du filtre
  • Vérifier la configuration du point de terminaison d’incorporation

Quand re-tester

Pour maintenir la visibilité des performances, il est judicieux d’exécuter des tests de charge planifiés tous les trimestres. Vous devez également effectuer un nouveau test lorsque vous apportez l’une des modifications suivantes :

  • Modifier les modèles de requête ou la complexité
  • Mettre à jour l’index de recherche vectorielle
  • Modifier les configurations de filtre
  • Attendez-vous à des augmentations significatives du trafic
  • Déployer de nouvelles fonctionnalités ou optimisations
  • Passer des types de points de terminaison standard aux types de points de terminaison optimisés pour le stockage

Résolution des problèmes

Toutes les requêtes échouent avec une latence d’environ 10 ms et des réponses de 240 octets

Cela indique que le principal de service reçoit une réponse 401/403. Vérifier:

  1. Le principal de service dispose des autorisations Peut interroger sur le point de terminaison de recherche vectorielle (pas seulement l’index).
  2. L’étendue du secret contient service_principal_client_id et service_principal_client_secret des clés valides.
  3. Le secret OAuth n’a pas expiré.

Le notebook comprend une vérification de la connectivité qui détecte ce problème avant d'exécuter le test de charge complète.

Exécution de plusieurs travaux de test de charge sur le même cluster

Si vous exécutez deux travaux de test de charge simultanément sur le même cluster, un travail peut recevoir des jetons OAuth obsolètes ou rencontrer des conflits de processeur avec les workers Locust de l'autre tâche. Pour obtenir des résultats fiables, exécutez des travaux de test de charge un à la fois sur un cluster dédié.

Les graphiques de minutage des composants sont vides

Les graphiques de minutage des composants (ann_time, embedding_gen_time, reranker_time) nécessitent que le point de terminaison renvoie debug_info dans les réponses aux requêtes. Si ces graphiques sont vides :

  • Vérifiez que vous utilisez le script fast_vs_load_test_async_load.py (qui analyse les debug_info des réponses) en tant que locust_script_path.
  • Certaines configurations de point de terminaison peuvent ne pas retourner debug_info. Les index incorporés auto-gérés retournent généralement ann_time et response_time, mais pas embedding_gen_time ou reranker_time.

Table de résultats non interrogeable à partir d’un entrepôt SQL

Le notebook écrit les résultats de la session Spark du cluster. Si un entrepôt SQL affiche 0 lignes pour une table que le notebook signale comme remplie, le problème peut être un délai de synchronisation des métadonnées du Unity Catalog. Patientez quelques minutes et réessayez, ou interrogez la table directement à partir d’un bloc-notes attaché au même cluster.

Résumé des meilleures pratiques

Configuration de test

  • Exécutez des tests pendant au moins 5 minutes au pic de charge.

  • Utilisez des principaux de service OAuth pour l’authentification.

  • Créez des charges utiles de requête réalistes qui correspondent aux requêtes de production attendues.

  • Testez avec des filtres et des paramètres de type production.

  • Incluez une période de préchauffage avant de mesurer.

  • Testez à plusieurs niveaux de concurrence.

  • Suivez les latences P95/P99, pas seulement les moyennes.

  • Testez les performances mises en cache et non mises en cache.

    # Conservative approach: Size endpoint for UNCACHED performance
    uncached_results = run_load_test(diverse_queries, duration=600)
    endpoint_size = calculate_capacity(uncached_results, target_rps=500)
    
    # Then verify cached performance is even better
    cached_results = run_load_test(repetitive_queries, duration=300)
    print(f"Cached P95: {cached_results['p95']}ms (bonus performance)")
    

Conception du jeu de requêtes

  • Faire correspondre la diversité de vos requêtes de test à la distribution du trafic réel (requêtes fréquentes et rares).
  • Utilisez des requêtes réelles à partir de journaux (anonymisées).
  • Incluez différentes complexités de requête.
  • Testez les scénarios mis en cache et non mis en cache et suivez les résultats séparément.
  • Testez avec des combinaisons de filtres attendues.
  • Utilisez les mêmes paramètres que ceux que vous utiliserez en production. Par exemple, si vous utilisez la recherche hybride en production, incluez des requêtes de recherche hybride. Utilisez un paramètre similaire num_results comme en production.
  • N’utilisez pas de requêtes qui ne se produisent jamais en production.

Optimisation des performances

Si les latences sont trop élevées, essayez ce qui suit :

  1. Utilisez des Principaux de Service OAuth (et non des PAT) - amélioration de 100 ms
  2. Réduire num_results : l’extraction de 100 résultats est plus lente que 10
  3. Optimiser les filtres - Filtres complexes ou trop restrictifs ralentissant les requêtes
  4. Vérifier le point de terminaison d’incorporation : vérifiez qu’il n’est pas mis à l’échelle vers zéro ou qu’il dispose d’une bande passante suffisante

Si vous atteignez des limites de taux, essayez ce qui suit :

  1. Augmenter la capacité du point de terminaison - Monter en puissance votre point de terminaison
  2. Implémenter la limitation du débit des requêtes ou leur répartition dans le temps côté client.
  3. Utiliser le regroupement de connexions - Réutiliser les connexions
  4. Ajouter une logique de nouvelle tentative - Utiliser un backoff exponentiel (déjà une partie du Kit de développement logiciel (SDK) Python)

Ressources supplémentaires