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