Skip to main content

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égionalrégional, SpikeeLabs


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
1 Dataset ✅ Corrigé 500 20 000 bornes prêtes
2 Nginx WebSocket ✅ Corrigé 2 200 Capacité 458K+
3 MariaDB ✅ Tuné N/A 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