Technical
Performance
Performance
Products Manager v5.7 est conçu pour absorber des volumes de données industriels — des imports de 100k lignes aux catalogues multi-fournisseurs de plusieurs millions de produits — tout en maintenant des temps de réponse sous les 200ms au p99 sur les routes critiques.
Objectifs de Performance
SLOs de Production
p99 < 200ms sur les routes produit | 50k lignes importées en ~45s | Recherche full-text < 50ms | 0% pool exhaustion depuis l'architecture multi-DB
| Opération | Cible | P95 | P99 |
|---|---|---|---|
| Lecture produit (cache L1) | <10ms | <20ms | <50ms |
| Lecture produit (cache L2 Redis) | <20ms | <40ms | <80ms |
| Lecture produit (base de données) | <50ms | <100ms | <200ms |
| Écriture produit | <100ms | <200ms | <500ms |
| Recherche full-text (Meilisearch) | <50ms | <80ms | <150ms |
| Recherche sémantique (Qdrant) | <100ms | <150ms | <300ms |
| Import CSV 50k lignes | ~45s | ~60s | ~90s |
| Import CSV 100k lignes | ~90s | ~120s | ~180s |
| Enrichissement IA batch 100 produits | ~8min | ~10min | ~15min |
| Export catalogue complet | <30s | <60s | <120s |
Base de Données
Architecture 8 Instances PostgreSQL
La séparation du trafic sur 8 bases PostgreSQL spécialisées est le levier principal de performance. Avant cette migration, les imports batch bloquaient les lectures catalogue par contention de verrous. Désormais chaque domaine fonctionnel dispose de sa propre instance avec un pool dimensionné à sa charge réelle.
| Base | Pool Base | Max Overflow | Temps Moyen | Charge Dominante |
|---|---|---|---|---|
| db_core | 5 | 10 | <10ms | Auth, JWT, RBAC (haute lecture) |
| db_catalog | 20 | 30 | <50ms | Produits, prix, stocks (trafic maximal) |
| db_imports | 15 | 25 | <40ms | Jobs d'import, logs (haute écriture burst) |
| db_exports | 10 | 20 | <20ms | Export jobs (batch séquentiel) |
| db_media | 10 | 20 | <30ms | Métadonnées fichiers, thumbnails |
| db_code2asin | 10 | 20 | <30ms | Mapping EAN/ASIN, cache Amazon |
| db_analytics | 10 | 20 | <60ms | Métriques, rapports BI |
| db_suppliers | 10 | 20 | <30ms | Données fournisseurs, configurations |
Connection Pooling
Chaque base utilise asyncpg avec un pool dimensionné indépendamment :
# Paramètres globaux de pool
DATABASE_POOL_TIMEOUT = 30 # 30s d'attente max avant erreur
DATABASE_POOL_RECYCLE = 3600 # Recyclage des connexions après 1h
DATABASE_POOL_PRE_PING = True # Vérification avant réutilisation
DATABASE_MAX_OVERFLOW = 30 # Connexions supplémentaires autorisées
# Dimensionnement par base (pool_size=20 pour db_catalog, max total=50)
CATALOG_POOL_SIZE = 20
IMPORTS_POOL_SIZE = 15
CORE_POOL_SIZE = 5
Le max_overflow=30 sur db_catalog permet d'absorber les pics de trafic (ex : un import massif déclenchant des lectures parallèles de validation) sans rejeter de requêtes.
Index Composites (100+)
Plus de 100 index composites couvrent les patterns d'accès fréquents. Exemples représentatifs :
-- db_catalog : filtre catalogue par catégorie et marque
CREATE INDEX idx_products_category_brand
ON products(category_id, brand_id)
WHERE deleted_at IS NULL;
-- db_catalog : lookup EAN avec filtre soft-delete
CREATE INDEX idx_products_ean_active
ON products(ean, deleted_at)
WHERE deleted_at IS NULL;
-- db_catalog : filtre fournisseur + statut (exports, rapports)
CREATE INDEX idx_products_supplier_status
ON products(supplier_id, status)
WHERE deleted_at IS NULL;
-- db_imports : monitoring des jobs actifs
CREATE INDEX idx_import_jobs_status_created
ON import_jobs(status, created_at DESC)
WHERE status IN ('pending', 'running');
-- db_analytics : agrégations métriques par période
CREATE INDEX idx_performance_metrics_date_type
ON performance_metrics(metric_date, metric_type);
Soft Delete avec Index Partiels
Le pattern soft-delete (deleted_at IS NULL) est systématiquement indexé via des index partiels, ce qui réduit la taille des index de 20 à 40% et accélère les scans sur les enregistrements actifs :
-- Index partiel : n'inclut que les enregistrements non supprimés
CREATE INDEX idx_products_active_supplier
ON products(supplier_id, updated_at DESC)
WHERE deleted_at IS NULL;
-- Requête optimisée par cet index
SELECT * FROM products
WHERE supplier_id = $1
AND deleted_at IS NULL
ORDER BY updated_at DESC
LIMIT 50;
Partitionnement par Mois
Les tables à forte croissance sont partitionnées par mois avec une rétention de 9 mois. Les partitions expirées sont droppées automatiquement via un job Celery planifié.
Tables partitionnées :
import_errors(db_imports) — erreurs de traitement ligne par ligne, volume proportionnel aux importsperformance_metrics(db_analytics) — métriques système collectées toutes les minutes
-- Partition actuelle (exemple mars 2026)
CREATE TABLE import_errors_2026_03
PARTITION OF import_errors
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- Job de nettoyage automatique (Celery Beat)
-- Retention : 9 mois glissants
-- Planification : 1er de chaque mois à 02:00
Monitoring APM (Sentry)
Les requêtes lentes sont capturées via l'intégration Sentry SQLAlchemy. Le seuil est configurable par environnement :
# Sentry slow query threshold
SENTRY_TRACES_SAMPLE_RATE = 0.1 # 10% des transactions
SLOW_QUERY_THRESHOLD_MS = 500 # Alerte si >500ms
DB_QUERY_APM_ENABLED = True
Les traces incluent le plan d'exécution, les paramètres (masqués en production) et le contexte de la requête HTTP parente.
Celery Workers
14 Queues Dédiées
L'isolation par queue garantit qu'une saturation d'un type de traitement (ex : imports massifs) ne bloque pas les autres opérations (ex : enrichissement IA, synchronisation Odoo).
| Queue | Workers | Priorité | Usage |
|---|---|---|---|
imports | 4 | High | Traitement fichiers CSV/Excel/XML |
icecat | 2 | Normal | Enrichissement catalogue Icecat |
enrichment | 2 | Normal | Enrichissement IA (OpenAI) |
odoo_sync | 1 | Normal | Synchronisation Odoo bidirectionnelle |
media | 2 | Low | Génération thumbnails, traitement images |
exports | 2 | Normal | Génération exports plateformes |
code2asin | 3 | Normal | Conversion EAN/ASIN Amazon |
catalog | 1 | Low | Tâches maintenance catalogue |
analytics | 1 | Low | Agrégation métriques |
notifications | 1 | High | Envoi notifications temps réel |
compliance | 1 | Normal | Vérifications conformité réglementaire |
pricing | 1 | Normal | Calculs prix, surveillance concurrents |
webhooks | 1 | High | Envoi webhooks outbound |
beat | — | — | Scheduler Celery Beat |
Total : 12 workers prefork (processus séparés, pas de GIL).
Configuration Prefork et Rate Limiting
# celery_config.py
CELERY_WORKER_CONCURRENCY = 4 # 4 processus par worker
CELERY_WORKER_PREFETCH_MULTIPLIER = 1 # Évite la surcharge mémoire
CELERY_TASK_ACKS_LATE = True # ACK après succès seulement
# Rate limiting par queue (évite la saturation des APIs externes)
CELERY_TASK_ANNOTATIONS = {
"tasks.icecat.*": {"rate_limit": "100/m"},
"tasks.code2asin.*": {"rate_limit": "10/s"},
"tasks.enrichment.*": {"rate_limit": "50/m"},
"tasks.odoo_sync.*": {"rate_limit": "30/m"},
}
Caching
L'architecture de cache à 2 niveaux réduit la charge base de données de ~70% sur les lectures catalogue.
- L1 (FastAPI in-memory) : données statiques (modules actifs, permissions RBAC, settings), TTL 60-300s, invalidé au restart worker
- L2 (Redis 7, 512MB) : données produits, listes paginées, stats dashboard, stratégie allkeys-lru
Un cache warming au démarrage de FastAPI précalcule les données les plus demandées pour éliminer les cold starts.
Pour les détails complets (clés Redis, TTL par ressource, invalidation, monitoring hit/miss) : Cache → /docs/technical/caching
Full-Text Search (Meilisearch)
Meilisearch sert d'index de recherche dédié, découplé de PostgreSQL.
- Latence : < 50ms au p99 sur les recherches produits
- Indexation : asynchrone via Celery après chaque mutation produit (queue
catalog) - Typo-tolérance : activée, distance de Levenshtein configurable par champ
- Champs indexés :
title,ean,sku,brand_name,category_name,description(poids différenciés) - Filtres :
supplier_id,category_id,status,is_active - Tri : par pertinence, prix, date de création
# Réindexation après import
@celery_app.task(queue="catalog")
async def reindex_products_meilisearch(product_ids: list[str]):
documents = await build_search_documents(product_ids)
await meili_client.index("products").add_documents_in_batches(
documents, batch_size=1000
)
Recherche Sémantique (Qdrant)
Qdrant stocke les embeddings vectoriels des produits pour la recherche par similarité sémantique.
- Modèle : OpenAI
text-embedding-3-small(1536 dimensions) - Latence : < 100ms au p95 pour une recherche ANN (Approximate Nearest Neighbors)
- Champs embarqués : titre + description + attributs clés (concaténés, tronqués à 8192 tokens)
- Indexation : asynchrone après enrichissement IA (queue
enrichment) - Use cases : "Produits similaires", recherche par description naturelle, détection de doublons sémantiques
Frontend Next.js 15
- Server Components : réduction du JavaScript envoyé au client, rendu HTML côté serveur pour les pages catalogue
- Lazy loading : composants lourds (éditeurs, charts) chargés à la demande
- Image optimization : composant
<Image>Next.js avec WebP automatique et srcset responsive - Code splitting : chunking automatique par route,
dynamic()pour les modules optionnels - Bundle size : First Load JS < 500 kB (hors chunks lazy)
Monitoring
Prometheus + Grafana
Les métriques applicatives sont exposées sur /metrics au format Prometheus :
# Latence des requêtes HTTP (histogramme)
http_request_duration_seconds{method, endpoint, status}
# Taille des pools de connexions DB
db_pool_connections_active{database}
db_pool_connections_idle{database}
# Queues Celery
celery_queue_length{queue_name}
celery_task_duration_seconds{task_name, queue}
# Cache Redis
redis_cache_hits_total{resource}
redis_cache_misses_total{resource}
redis_memory_used_bytes
Les dashboards Grafana couvrent : latence par endpoint, throughput imports, occupation des pools DB, longueur des queues Celery, hit ratio cache.
Sentry APM
- Tracing distribué sur 10% des transactions (configurable)
- Alertes automatiques sur les requêtes SQL > 500ms
- Profiling Python activable en production pour investigation ponctuelle
Benchmarks Observés (Production v5.7)
| Scénario | Résultat | Conditions |
|---|---|---|
| Import CSV 50k lignes | ~45s | Fichier fournisseur standard, mapping simple |
| Import CSV 100k lignes | ~90s | Avec validation EAN et déduplication |
| Enrichissement IA batch 100 produits | ~8min | OpenAI GPT-4o, 5 champs enrichis |
| Recherche full-text | <50ms | Index Meilisearch, catalogue 500k produits |
| Recherche sémantique | <100ms | Qdrant ANN, 500k vecteurs |
| Lecture produit (cache chaud) | <10ms | Redis L2, TTL actif |
| Dashboard analytics | <400ms | p95, agrégations pré-calculées |
| Export CSV 10k produits | <15s | Sans images, export direct S3 |
Variables d'Environnement Liées à la Performance
| Variable | Défaut | Description |
|---|---|---|
WORKER_COUNT | 8 | Nombre de workers Uvicorn |
DB_POOL_SIZE | 20 | Pool de connexions db_catalog |
DB_MAX_OVERFLOW | 30 | Overflow pool db_catalog |
DB_POOL_TIMEOUT | 30 | Timeout attente connexion (s) |
DB_POOL_RECYCLE | 3600 | Recyclage connexion (s) |
CELERY_WORKER_CONCURRENCY | 4 | Processus par worker Celery |
CELERY_PREFETCH_MULTIPLIER | 1 | Préchargement tâches |
SLOW_QUERY_THRESHOLD_MS | 500 | Seuil alerte requête lente (ms) |
SENTRY_TRACES_SAMPLE_RATE | 0.1 | Taux échantillonnage APM |
MEILISEARCH_BATCH_SIZE | 1000 | Taille batch indexation |
IMPORT_BATCH_SIZE | 500 | Lignes par batch d'import |
Ressources
Évolution Continue
Les benchmarks sont mesurés sur l'infrastructure de production (8 workers Uvicorn, 8 instances PostgreSQL, Redis 512MB). Ils varient selon la complexité des mappings et la qualité des données sources.