Rapport de campagne — Tests de montée en charge OCPP WeGo
Date : 28 mai 2026
Environnement : Préprod (ocpp.preprod.we-go.pro)
Réalisé par : Brian KAZMIEROWSKI
Destinataires : Direction Technique, Direction régional
1. Contexte et objectif
Contexte
La plateforme WeGo OCPP est le socle de communication temps réel entre les bornes de recharge et le back-office. Chaque borne maintient une connexion WebSocket permanente vers le CSMS (Charging Station Management System) pour échanger les messages OCPP 1.6 : démarrage/arrêt de charge, remontée de consommation, heartbeat, etc.
Avec la croissance du parc, il est essentiel de valider la capacité de la plateforme à absorber un nombre élevé de bornes simultanées sans dégradation de service.
Objectif
Déterminer la capacité maximale de la stack actuelle en nombre de bornes connectées simultanément, identifier les goulots d'étranglement, et les corriger progressivement jusqu'à atteindre 20 000 bornes.
Méthodologie
- Outil d'injection : k6 avec extension OCPP custom (protocole WebSocket natif)
- Scénario : montée progressive par paliers de 5% toutes les 6 secondes
- Cycle de vie simulé : connexion → BootNotification → StatusNotification → Authorize → StartTransaction → MeterValues → StopTransaction
- Critère d'arrêt automatique : taux de succès global < 95%
- Métrique principale : nombre de bornes connectées simultanément avec latence OCPP < 30s au p95
2. Synthèse des résultats
Progression de la capacité
| Étape | Capacité maximale | Latence OCPP p95 | Facteur limitant |
|---|---|---|---|
| État initial | ~500 bornes | Non mesurable | Erreurs de données |
| Après correction du dataset | ~2 200 bornes | 137 ms ✅ | Nginx — connexions WebSocket |
| Après correction Nginx | 5 000 bornes ✅ | 2 s | Backend applicatif |
| Test à 10 000 bornes | 4 200 bornes | 5,9 s | Saturation CPU backend |
Résultat clé
La capacité de la plateforme a été multipliée par 10 (de ~500 à 5 000 bornes simultanées) grâce aux corrections infrastructure et données, sans aucune modification du code applicatif.
Bornes connectées simultanément
│
5000 ┤ ████████████████████████████████████████████████ 4 999 ✅
│
4000 ┤ ███████████████████████████████████████████ 4 172 (test 10K)
│
3000 ┤
│
2000 ┤ ████████████████████ 2 200 (plafond Nginx)
│
1000 ┤
│
500 ┤ █████ ~500 (état initial)
│
0 ┼──────────────────────────────────────────────────
Initial Dataset Nginx fix Test 10K
3. Problèmes identifiés et corrections apportées
3.1 — Dataset de test incohérent
Problème : les bornes de test existantes avaient des structures hétérogènes (connecteurs avec des identifiants variés : 1, 2, 10…), ce qui provoquait des échecs systématiques dans les scénarios k6.
Correction : génération d'un jeu de données dédié de 20 000 bornes avec une structure homogène (préfixe LOADTEST.CBID.*, 2 connecteurs par borne, autorisations RFID configurées). Un pool de 5 875 badges RFID a été constitué à partir des autorisations existantes.
Impact : élimination de 100% des erreurs liées aux données de test.
3.2 — Épuisement des badges RFID sous charge
Problème : le mécanisme de distribution des badges RFID dans l'injecteur était linéaire — une fois le stock épuisé, les VUs restaient bloquées indéfiniment, faussant les métriques.
Correction : mise en place d'un recyclage circulaire des badges, permettant à chaque VU de réutiliser ses RFID quand le stock est épuisé.
Impact : suppression totale des erreurs rfid_missing et des VUs bloquées.
3.3 — MariaDB sous-dimensionnée
Problème : la base de données fonctionnait avec une configuration par défaut inadaptée à la charge visée.
Correction :
| Paramètre | Avant | Après |
|---|---|---|
| Buffer pool InnoDB | 128 Mo | 4 Go |
| Connexions max | 151 | 1 000 |
| Slow query log | Désactivé | Activé (seuil : 1s) |
Impact : réduction de la latence DB et capacité de diagnostic des requêtes lentes.
Un backup complet de la configuration a été réalisé avant modification. La procédure de rollback est documentée.
3.4 — Nginx — Plafond de connexions WebSocket (cause racine principale)
Problème : les connexions WebSocket étaient plafonnées à environ 2 200 bornes. Au-delà, Nginx refusait toute nouvelle connexion avec l'erreur :
4096 worker_connections are not enough while connecting to upstream
Explication : chaque WebSocket proxifiée par Nginx consomme 2 descripteurs de fichier (un côté client, un côté backend). Avec worker_connections = 4096, la capacité réelle était de 4096 / 2 = 2 048 connexions WebSocket par worker. De plus, toutes les connexions de l'injecteur (IP unique) étaient routées vers un seul worker Nginx, concentrant la saturation.
Correction :
| Paramètre | Avant | Après |
|---|---|---|
worker_connections |
4 096 | 65 535 |
| Distribution inter-workers | Désactivée | Activée (reuseport) |
| Capacité théorique | ~2 048 WS | ~458 000 WS |
Impact : +127% de capacité immédiate — de 2 200 à 5 000 bornes connectées, avec 0% d'erreur de connexion.
Configuration rollbackable. Backups des fichiers originaux conservés sur le serveur.
3.5 — Limites de fichiers ouverts sur l'injecteur
Problème : le conteneur d'injection était limité à 10 000 descripteurs de fichier, insuffisant pour simuler plus de 10 000 bornes.
Correction : augmentation à 65 535 descripteurs.
Impact : permet la simulation jusqu'à 20 000+ bornes depuis un seul injecteur.
3.6 — Scripts d'injection k6
Problème : les scripts ne disposaient pas de gardes contre les débordements de dataset ni de compteurs d'erreurs exploitables.
Correction :
- Garde anti-débordement (au-delà de 20 000 bornes)
- Compteurs dédiés :
rfid_missing,stucked_vu,dataset_out_of_range - Alternance automatique des connecteurs 1 et 2
Impact : résultats de test fiables et exploitables.
4. Analyse du test à 5 000 bornes (résultat de référence)
Résultats
| Métrique | Valeur |
|---|---|
| Bornes connectées | 4 999 / 5 000 (99,98%) |
| Taux de connexion | 100% — 0 échec |
| Taux de succès global | 99,04% |
| Latence OCPP p95 | 1,99 s |
| Latence OCPP médiane | 139 ms |
| Transactions démarrées | 436 |
| Transactions terminées | 33 |
Bilan par composant
| Composant | État sous charge | Verdict |
|---|---|---|
| Nginx (14 workers) | Aucune erreur, connexions distribuées | ✅ Sain |
| OCPP Gateway | 11% CPU, 440 Mo RAM | ✅ Confortable |
| Worker NestJS | 104% CPU | ⚠️ Limite atteinte |
| Backend API | 89% CPU | ⚠️ Chargé |
| MariaDB | Latence stable, slow log actif | ✅ Sain |
| Redis | Stable | ✅ Sain |
5. Analyse du test à 10 000 bornes
Résultats
| Métrique | 5 000 bornes | 10 000 bornes |
|---|---|---|
| Bornes connectées | 4 999 | 4 172 |
| Taux de connexion | 100% | 95% |
| Authorize success | 89% | 15% ❌ |
| Latence p95 | 2 s | 5,9 s |
| Cause d'arrêt | Fin normale | Seuil qualité franchi |
Diagnostic
Le backend applicatif (NestJS) sature au-delà de ~4 000 bornes :
- Le worker NestJS consomme 100% d'un cœur CPU dès 5 000 bornes
- Les requêtes d'autorisation (
Authorize) expirent en timeout - La latence OCPP triple entre 5 000 et 10 000 bornes
Conclusion : l'infrastructure réseau et base de données tiennent la charge. Le facteur limitant est désormais exclusivement le backend applicatif.
6. Cartographie des goulots d'étranglement
flowchart LR
A["Dataset\n(corrigé ✅)"] --> B["Nginx\n(corrigé ✅)"]
B --> C["Backend NestJS\n⚠️ PROCHAIN"]
C --> D["MariaDB\n(tuné ✅)"]
C --> E["Redis\n(OK ✅)"]
style A fill:#4ade80,stroke:#166534,color:#000
style B fill:#4ade80,stroke:#166534,color:#000
style C fill:#fbbf24,stroke:#92400e,color:#000
style D fill:#4ade80,stroke:#166534,color:#000
style E fill:#4ade80,stroke:#166534,color:#000
| Rang | Composant | Statut | Seuil | Action |
|---|---|---|---|---|
| ✅ Corrigé | 20 000 bornes prêtes | |||
| ✅ Corrigé | Capacité 458K+ | |||
| ✅ Tuné | 4 Go buffer, 1 000 conn. | |||
| 4 | Backend NestJS | ⚠️ À traiter | ~4 200 | Voir section 7 |
| 5 | Scalabilité horizontale | Non démarré | ~10 000 | Voir section 7 |
7. Recommandations pour atteindre 20 000 bornes
Priorité 1 — Optimisation applicative (impact estimé : +50 à 100%)
Des points d'optimisation ont été identifiés dans le code NestJS lors de l'audit :
| Optimisation | Description | Impact attendu |
|---|---|---|
| Suppression d'un double appel d'autorisation | La vérification checkInternalGroupAuthorization est appelée deux fois lors de chaque Authorize |
Réduction de 50% de la charge DB sur les autorisations |
Remplacement save() par insert() |
Les MeterValues utilisent un save() TypeORM qui effectue un SELECT avant chaque INSERT |
Réduction significative des requêtes DB |
| Correction d'un crash silencieux | checkInternalGroupAuthorization peut crasher sur undefined |
Stabilité accrue sous forte charge |
Ces corrections ne nécessitent pas de changement d'architecture et peuvent être déployées rapidement.
Priorité 2 — Scalabilité horizontale du backend
Si les optimisations applicatives ne suffisent pas pour atteindre 10 000+ bornes :
- Ajout d'instances OCPP derrière un load balancer WebSocket (sticky sessions)
- Augmentation des ressources CPU du serveur backend (actuellement 4 cœurs)
- Séparation des workers : dédier des conteneurs aux traitements asynchrones (MeterValues, facturation)
Priorité 3 — Infrastructure d'injection
Pour les tests au-delà de 5 000 bornes :
- L'injecteur actuel (8 Go RAM) est insuffisant pour 10 000+ bornes
- Solution : utiliser une machine avec 16 Go+ ou distribuer sur plusieurs injecteurs (mécanisme déjà prévu dans l'outil)
8. Synthèse des risques
| Risque | Probabilité | Impact | Mitigation |
|---|---|---|---|
| Panne WebSocket en prod avec croissance du parc | Élevée si non corrigé | Bornes déconnectées | Appliquer le fix Nginx en prod |
| Saturation backend au-delà de 4 000 bornes actives | Moyenne | Dégradation du service (timeouts) | Optimisations NestJS + scaling |
| Requêtes lentes non détectées | Faible (slow log activé) | Latence progressive | Monitoring continu |
9. Conclusion
Cette campagne a permis de multiplier par 10 la capacité de la plateforme (de ~500 à 5 000 bornes) sans modification du code applicatif, uniquement par corrections infrastructure et données.
Le prochain palier (10 000 à 20 000 bornes) nécessitera des optimisations du backend NestJS, déjà identifiées et prêtes à être implémentées.
[!IMPORTANT] Action immédiate recommandée : vérifier et appliquer le correctif Nginx (
worker_connections) en production. Sans cette correction, la prod est exposée au même plafond de ~2 200 connexions WebSocket.
Rapport généré le 28 mai 2026 — Mis à jour le 1er juin 2026