# Tests de montée en charge

# 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, 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 &lt; 95%
- **Métrique principale** : nombre de bornes connectées simultanément avec latence OCPP &lt; 30s au p95

---

## 2. Synthèse des résultats

### Progression de la capacité

<table id="bkmrk-%C3%89tape-capacit%C3%A9-maxim"><thead><tr><th>Étape</th><th>Capacité maximale</th><th>Latence OCPP p95</th><th>Facteur limitant</th></tr></thead><tbody><tr><td>État initial</td><td>**~500 bornes**</td><td>Non mesurable</td><td>Erreurs de données</td></tr><tr><td>Après correction du dataset</td><td>**~2 200 bornes**</td><td>137 ms ✅</td><td>Nginx — connexions WebSocket</td></tr><tr><td>**Après correction Nginx**</td><td>**5 000 bornes** ✅</td><td>2 s</td><td>Backend applicatif</td></tr><tr><td>Test à 10 000 bornes</td><td>**4 200 bornes**</td><td>5,9 s</td><td>Saturation CPU backend</td></tr></tbody></table>

### 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** :

<table id="bkmrk-param%C3%A8tre-avant-apr%C3%A8"><thead><tr><th>Paramètre</th><th>Avant</th><th>Après</th></tr></thead><tbody><tr><td>Buffer pool InnoDB</td><td>128 Mo</td><td>**4 Go**</td></tr><tr><td>Connexions max</td><td>151</td><td>**1 000**</td></tr><tr><td>Slow query log</td><td>Désactivé</td><td>**Activé** (seuil : 1s)</td></tr></tbody></table>

**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** :

<table id="bkmrk-param%C3%A8tre-avant-apr%C3%A8-1"><thead><tr><th>Paramètre</th><th>Avant</th><th>Après</th></tr></thead><tbody><tr><td>`worker_connections`</td><td>4 096</td><td>**65 535**</td></tr><tr><td>Distribution inter-workers</td><td>Désactivée</td><td>**Activée** (`reuseport`)</td></tr><tr><td>Capacité théorique</td><td>~2 048 WS</td><td>**~458 000 WS**</td></tr></tbody></table>

**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

<table id="bkmrk-m%C3%A9trique-valeur-born"><thead><tr><th>Métrique</th><th>Valeur</th></tr></thead><tbody><tr><td>Bornes connectées</td><td>**4 999 / 5 000** (99,98%)</td></tr><tr><td>Taux de connexion</td><td>**100%** — 0 échec</td></tr><tr><td>Taux de succès global</td><td>**99,04%**</td></tr><tr><td>Latence OCPP p95</td><td>**1,99 s**</td></tr><tr><td>Latence OCPP médiane</td><td>**139 ms**</td></tr><tr><td>Transactions démarrées</td><td>436</td></tr><tr><td>Transactions terminées</td><td>33</td></tr></tbody></table>

### Bilan par composant

<table id="bkmrk-composant-%C3%89tat-sous-"><thead><tr><th>Composant</th><th>État sous charge</th><th>Verdict</th></tr></thead><tbody><tr><td>**Nginx** (14 workers)</td><td>Aucune erreur, connexions distribuées</td><td>✅ Sain</td></tr><tr><td>**OCPP Gateway**</td><td>11% CPU, 440 Mo RAM</td><td>✅ Confortable</td></tr><tr><td>**Worker NestJS**</td><td>104% CPU</td><td>⚠️ Limite atteinte</td></tr><tr><td>**Backend API**</td><td>89% CPU</td><td>⚠️ Chargé</td></tr><tr><td>**MariaDB**</td><td>Latence stable, slow log actif</td><td>✅ Sain</td></tr><tr><td>**Redis**</td><td>Stable</td><td>✅ Sain</td></tr></tbody></table>

---

## 5. Analyse du test à 10 000 bornes

### Résultats

<table id="bkmrk-m%C3%A9trique-5-000-borne"><thead><tr><th>Métrique</th><th>5 000 bornes</th><th>10 000 bornes</th></tr></thead><tbody><tr><td>Bornes connectées</td><td>4 999</td><td>**4 172**</td></tr><tr><td>Taux de connexion</td><td>100%</td><td>95%</td></tr><tr><td>Authorize success</td><td>89%</td><td>**15%** ❌</td></tr><tr><td>Latence p95</td><td>2 s</td><td>**5,9 s**</td></tr><tr><td>Cause d'arrêt</td><td>Fin normale</td><td>Seuil qualité franchi</td></tr></tbody></table>

### 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

```mermaid
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

```

<table id="bkmrk-rang-composant-statu"><thead><tr><th>Rang</th><th>Composant</th><th>Statut</th><th>Seuil</th><th>Action</th></tr></thead><tbody><tr><td><s>1</s></td><td><s>Dataset</s></td><td>✅ Corrigé</td><td><s>500</s></td><td>20 000 bornes prêtes</td></tr><tr><td><s>2</s></td><td><s>Nginx WebSocket</s></td><td>✅ Corrigé</td><td><s>2 200</s></td><td>Capacité 458K+</td></tr><tr><td><s>3</s></td><td><s>MariaDB</s></td><td>✅ Tuné</td><td><s>N/A</s></td><td>4 Go buffer, 1 000 conn.</td></tr><tr><td>**4**</td><td>**Backend NestJS**</td><td>⚠️ À traiter</td><td>**~4 200**</td><td>Voir section 7</td></tr><tr><td>5</td><td>Scalabilité horizontale</td><td>Non démarré</td><td>~10 000</td><td>Voir section 7</td></tr></tbody></table>

---

## 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 :

<table id="bkmrk-optimisation-descrip"><thead><tr><th>Optimisation</th><th>Description</th><th>Impact attendu</th></tr></thead><tbody><tr><td>Suppression d'un double appel d'autorisation</td><td>La vérification `checkInternalGroupAuthorization` est appelée deux fois lors de chaque `Authorize`</td><td>Réduction de 50% de la charge DB sur les autorisations</td></tr><tr><td>Remplacement `save()` par `insert()`</td><td>Les MeterValues utilisent un `save()` TypeORM qui effectue un SELECT avant chaque INSERT</td><td>Réduction significative des requêtes DB</td></tr><tr><td>Correction d'un crash silencieux</td><td>`checkInternalGroupAuthorization` peut crasher sur `undefined`</td><td>Stabilité accrue sous forte charge</td></tr></tbody></table>

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

<table id="bkmrk-risque-probabilit%C3%A9-i"><thead><tr><th>Risque</th><th>Probabilité</th><th>Impact</th><th>Mitigation</th></tr></thead><tbody><tr><td>Panne WebSocket en prod avec croissance du parc</td><td>Élevée si non corrigé</td><td>Bornes déconnectées</td><td>Appliquer le fix Nginx en prod</td></tr><tr><td>Saturation backend au-delà de 4 000 bornes actives</td><td>Moyenne</td><td>Dégradation du service (timeouts)</td><td>Optimisations NestJS + scaling</td></tr><tr><td>Requêtes lentes non détectées</td><td>Faible (slow log activé)</td><td>Latence progressive</td><td>Monitoring continu</td></tr></tbody></table>

---

## 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*