Aller au contenu principal

Méthodologie des Benchmarks

Ce document décrit la méthodologie complète du Gateway Arena, le benchmark comparatif continu de STOA pour les API gateways. Pour les résultats, consultez Performance Benchmarks. Pour la décision architecturale, voir ADR-049.

Principes de Conception

Le Gateway Arena repose sur quatre principes :

  1. Comparaison équitable — Tous les gateways interrogent le même backend echo local (nginx, JSON statique, <1ms de réponse). Les benchmarks mesurent uniquement la surcharge du gateway, pas la latence du backend ou du réseau.
  2. Rigueur statistique — Score médian sur N exécutions avec intervalles de confiance CI95 basés sur la loi de Student. Aucune sélection des meilleures exécutions.
  3. Reproductibilité — Tous les scripts sont open source. N'importe qui peut exécuter le même benchmark sur sa propre infrastructure.
  4. Participation ouverte — N'importe quel gateway peut être ajouté à l'Arena. Mêmes scénarios, même scoring, même méthodologie.

Cadre en Deux Couches

L'Arena comporte deux couches indépendantes, chacune avec son propre CronJob, ses métriques et son tableau de bord Grafana.

PropriétéCouche 0 : Proxy BaselineCouche 1 : Enterprise AI Readiness
MesureDébit HTTP brutCapacités AI-native du gateway
Scénarios7 (warmup, health, sequential, burst, sustained, ramp)8 dimensions enterprise
Exécutions5 (1 rejeté) → 4 valides3 (1 rejeté) → 2 valides
PlanificationToutes les 30 minutesToutes les heures
Plage de score0–1000–100 (Enterprise Readiness Index)
CronJobgateway-arenagateway-arena-enterprise
Job Prometheusgateway_arenagateway_arena_enterprise

Couche 0 : Proxy Baseline

Scénarios

Chaque scénario est une invocation k6 distincte avec des profils de charge différents.

#ScénarioExécuteurVUsItérations/DuréeObjectif
0warmupshared-iterations1050 itérations, 15s maxPréchauffage JVM/runtime. Exclu du scoring.
1healthshared-iterations11 itération, 10s maxBaseline health probe
2sequentialshared-iterations120 itérations, 30s maxLatence P95 client unique
3burst_10shared-iterations1010 itérations, 15s maxRafale légère
4burst_50ramping-vus0→505s montée, 10s stable, 3s descenteCapacité rafale moyenne
5burst_100ramping-vus0→1005s montée, 10s stable, 3s descenteCapacité rafale élevée
6sustainedshared-iterations1100 itérations, 60s maxCohérence sous charge constante
7ramp_upramping-arrival-rate10→100 req/s60s (6 étapes)Montée en charge du débit

Protocole d'Exécution

Pour chaque gateway, l'orchestrateur (run-arena.sh) exécute :

Pour run = 1 à 5 :
Pour scénario dans [warmup, health, sequential, burst_10, burst_50, burst_100, sustained, ramp_up] :
k6 run benchmark.js --env SCENARIO={scenario} → résumé JSON

L'exécution 1 est le run de préchauffage et est exclue du scoring. Les exécutions 2 à 5 sont scorées.

Formule de Scoring

Le score composite est une somme pondérée de 7 scores de dimensions :

Score = 0.10 × Base
+ 0.20 × Burst50
+ 0.20 × Burst100
+ 0.15 × Disponibilité
+ 0.10 × Erreur
+ 0.10 × Cohérence
+ 0.15 × RampUp

Dimension : Base (Latence Séquentielle)

Mesure la latence P95 d'un client unique avec un plafond de 400ms.

Base = max(0, min(100, 100 × (1 − P95_sequential / 0.4)))

Un P95 de 0ms donne un score de 100. Un P95 de 400ms donne 0.

Dimension : Burst50 / Burst100

Même formule avec des plafonds différents :

Burst50  = max(0, min(100, 100 × (1 − P95_burst50  / 2.5)))
Burst100 = max(0, min(100, 100 × (1 − P95_burst100 / 4.0)))
DimensionPlafond de LatenceP95 = 0P95 = PlafondP95 > Plafond
Base400ms10000
Burst502.5s10000
Burst1004.0s10000

Dimension : Disponibilité et Erreur

Les deux utilisent la même formule, basée sur le total des vérifications réussies pour toutes les exécutions :

Disponibilité = Erreur = 100 × (total_ok / total_requests)

total_ok et total_requests sont cumulés sur tous les scénarios et toutes les exécutions valides. En l'absence de requêtes, la valeur par défaut est 50.

Dimension : Cohérence

Utilise le coefficient de variation basé sur l'IQR du scénario sustained. Cette mesure est robuste aux latences réseau bimodales (contrairement à l'écart-type standard).

IQR_CV = (P75 − P25) / P50
Cohérence = max(0, min(100, 100 × (1 − IQR_CV)))

Un gateway parfaitement cohérent (P75 = P25) obtient un score de 100. Une variance élevée donne un score plus bas.

Dimension : Ramp-Up

Mesure la montée en charge du débit sous une charge croissante. Le scénario ramp_up utilise un exécuteur ramping-arrival-rate qui passe de 10 à 100 requêtes/seconde sur 60 secondes.

effective_rate = observed_rate × success_rate

Si P99 dépasse 2 secondes, une pénalité est appliquée :

si P99 > 2s :
effective_rate × max(0.5, 1.0 − (P99 − 2.0) / 8.0)

Le score est le taux effectif plafonné à 100 :

RampUp = min(100, effective_rate)

Récapitulatif des Poids

PoidsDimensionCe qu'il récompense
0.20Burst50Gestion des rafales à moyenne échelle
0.20Burst100Gestion des rafales à grande échelle
0.15DisponibilitéTaux de succès des requêtes
0.15RampUpMontée en charge du débit
0.10BaseFaible latence client unique
0.10ErreurFonctionnement sans erreur
0.10CohérenceTemps de réponse prévisibles

La gestion des rafales est pondérée le plus fortement (40% combinés) car les API gateways en production font face à des patterns de trafic en rafale plus souvent qu'à une charge soutenue.


Couche 1 : Enterprise AI Readiness

8 Dimensions

#DimensionPoidsScénarioPlafond de Latence
1MCP Discovery0.15ent_mcp_discovery500ms
2MCP Tool Execution0.20ent_mcp_toolcall500ms
3Auth Chain0.15ent_auth_chain1s
4Policy Engine0.15ent_policy_eval200ms
5AI Guardrails0.10ent_guardrails1s
6Rate Limiting0.10ent_quota_burst1s
7Resilience0.10ent_resilience1s
8Agent Governance0.05ent_governance2s

Scoring par Dimension

Le score de chaque dimension est une combinaison pondérée de la disponibilité et de la latence :

availability_score = passes / (passes + fails) × 100
latency_score = max(0, min(100, 100 × (1 − P95 / cap)))
dimension_score = 0.6 × availability_score + 0.4 × latency_score

La disponibilité est pondérée plus fortement (60%) car un AI gateway qui ne prend pas en charge une fonctionnalité obtient un score de 0 indépendamment de la latence.

Enterprise Readiness Index (ERI)

Le score composite est la somme pondérée de toutes les dimensions :

ERI = Σ (poids_i × score_dimension_i)

Les gateways sans support MCP obtiennent un score de 0 — et non N/A. C'est intentionnel : l'absence d'une capacité est une mesure concrète et honnête. N'importe quel gateway peut implémenter MCP et relancer le benchmark.

Support du Protocole MCP

Le benchmark prend en charge deux variantes du protocole MCP :

ProtocoleValeur ConfigPattern d'EndpointUtilisé Par
STOA REST"stoa"GET /mcp/capabilities, POST /mcp/tools/list, POST /mcp/tools/callSTOA Gateway
Streamable HTTP"streamable-http"POST /mcp avec enveloppe JSON-RPC 2.0Gravitee 4.8+

Le script k6 lit MCP_PROTOCOL pour basculer entre les formats de requêtes. Les deux protocoles sont testés avec la même formule de scoring.


Méthodes Statistiques

Sélection de la Médiane

Toutes les métriques par scénario (P50, P95, P99, comptages pass/fail) sont calculées comme la médiane des exécutions valides. La médiane est préférée à la moyenne car elle est robuste à une seule exécution aberrante causée par des pauses GC, des voisins bruyants ou des problèmes réseau transitoires.

def median(values):
s = sorted(values)
return s[len(s) // 2]

Pour la Couche 0 : 5 exécutions, 1 rejetée → 4 valides → médiane de 4. Pour la Couche 1 : 3 exécutions, 1 rejetée → 2 valides → médiane de 2.

Intervalles de Confiance CI95

Les intervalles de confiance utilisent la loi de Student (et non les scores z) car les tailles d'échantillons sont petites (n=2–4). La loi de Student a des queues plus lourdes, produisant des intervalles plus larges (et plus honnêtes) pour les petits échantillons.

CI95 = mean ± t(α/2, df) × (stddev / √n)

où :
df = n − 1 (degrés de liberté)
t(α/2, df) (valeur critique t pour 95% de confiance)
stddev = √(Σ(xi − mean)² / (n − 1)) (écart-type de l'échantillon)

Valeurs Critiques t

dft-critiquen typique
112.7062 exécutions (Couche 1)
24.3033 exécutions
33.1824 exécutions (Couche 0)
42.7765 exécutions
≥1201.96Grands échantillons (≈ score z)

Les bornes CI95 sont contraintes à [0, 100] pour les scores composites.

Pourquoi Pas les Scores z ?

Avec n=4 exécutions, utiliser z=1.96 au lieu de t=3.182 produirait des intervalles de confiance 50% trop étroits, donnant une fausse impression de précision. La loi de Student tient correctement compte de l'incertitude inhérente aux petits échantillons.


Architecture

Infrastructure

K8s CronJob (OVH MKS, co-localisé) :
run-arena.sh (orchestrateur)
└── Pour chaque gateway (3 K8s + N VPS) :
└── Pour chaque exécution (5) :
├── k6 run (warmup) → rejeté
└── k6 run (7 scénarios) → résumés JSON
run-arena.py (scoring)
└── Lit JSON → médiane → score composite → CI95 → texte Prometheus
└── curl POST → Pushgateway

VPS Sidecar (host cron, co-localisé) :
Même image Docker + scripts
Benchmark 1 gateway local → push vers Pushgateway

Backend Echo

Tous les gateways proxifient vers un serveur echo partagé — un conteneur nginx retournant un payload JSON statique en <1ms :

{"status": "ok", "server": "echo-k8s"}

Cela garantit que les benchmarks mesurent uniquement la surcharge du gateway, et non les performances du backend ou la latence réseau. Le serveur echo s'exécute sur le même cluster/hôte que le gateway testé.

Participants Actuels

In-Cluster (K8s — OVH MKS)

GatewayPort ProxyEndpoint de SantéBackend
STOA8080/healthecho-backend:8888
Kong DB-less8000:8001/statusecho-backend:8888
Gravitee APIM8082:18082/_node/healthecho-backend:8888

VPS (Sidecars Co-localisés)

GatewayHôtePort ProxyBackend
STOA51.83.45.138080echo-local:8888
Kong51.83.45.138000echo-local:8888
Gravitee54.36.209.2378082echo-local:8888

Métriques Prometheus

Couche 0

MétriqueTypeLabelsDescription
gateway_arena_scoregaugegatewayScore composite (0–100)
gateway_arena_score_stddevgaugegatewayÉcart-type entre exécutions
gateway_arena_score_ci_lowergaugegatewayBorne inférieure CI95
gateway_arena_score_ci_uppergaugegatewayBorne supérieure CI95
gateway_arena_runsgaugegatewayNombre d'exécutions valides
gateway_arena_availabilitygaugegatewayTaux de succès health check (0–1)
gateway_arena_p50_secondsgaugegateway, scenarioLatence médiane par scénario
gateway_arena_p95_secondsgaugegateway, scenarioLatence P95 par scénario
gateway_arena_p99_secondsgaugegateway, scenarioLatence P99 par scénario
gateway_arena_ramp_rategaugegatewayDébit soutenu maximal (req/s)
gateway_arena_requests_totalgaugegateway, scenario, statusComptages de requêtes

Chaque métrique de latence dispose également de variantes _ci_lower_seconds et _ci_upper_seconds avec les mêmes labels.

Couche 1

MétriqueTypeLabelsDescription
gateway_arena_enterprise_scoregaugegatewayEnterprise Readiness Index (0–100)
gateway_arena_enterprise_dimensiongaugegateway, dimensionScore par dimension (0–100)
gateway_arena_enterprise_score_ci_lowergaugegatewayBorne inférieure CI95
gateway_arena_enterprise_score_ci_uppergaugegatewayBorne supérieure CI95
gateway_arena_enterprise_score_stddevgaugegatewayÉcart-type entre exécutions
gateway_arena_enterprise_runsgaugegatewayNombre d'exécutions enterprise valides
gateway_arena_enterprise_latency_p95gaugegateway, dimensionLatence P95 par dimension

Ajout d'un Nouveau Gateway

K8s (In-Cluster)

  1. Déployez le gateway dans le namespace stoa-system avec un Service
  2. Configurez une route vers le backend echo : http://echo-backend.stoa-system.svc:8888
  3. Ajoutez une entrée dans le JSON GATEWAYS de k8s/arena/cronjob-prod.yaml :
{
"name": "my-gateway",
"health": "http://my-gw-svc.stoa-system.svc:PORT/health",
"proxy": "http://my-gw-svc.stoa-system.svc:PORT/echo/get"
}
  1. Pour la Couche 1, ajoutez les champs MCP :
{
"name": "my-gateway",
"target": "http://my-gw-svc:PORT",
"health": "http://my-gw-svc:PORT/health",
"mcp_base": "http://my-gw-svc:PORT/mcp",
"mcp_protocol": "stoa"
}
  1. Mettez à jour le ConfigMap et lancez un benchmark manuel :
kubectl create configmap gateway-arena-scripts \
--from-file=scripts/traffic/arena/benchmark.js \
--from-file=scripts/traffic/arena/run-arena.sh \
--from-file=scripts/traffic/arena/run-arena.py \
-n stoa-system --dry-run=client -o yaml | kubectl apply -f -

kubectl create job --from=cronjob/gateway-arena arena-test-$(date +%s) -n stoa-system

VPS (Sidecar Co-localisé)

  1. Déployez le gateway sur le VPS
  2. Déployez le conteneur echo sur le même réseau Docker
  3. Ajoutez la configuration VPS dans deploy/vps/bench/deploy.sh
  4. Exécutez : ./deploy/vps/bench/deploy.sh

Vérification

Après l'ajout d'un gateway, vérifiez que les métriques apparaissent dans Pushgateway :

curl -s https://pushgateway.gostoa.dev/metrics | grep 'gateway="my-gateway"'

Interprétation des Scores

ScoreÉvaluationCause Typique
> 95ExcellentGateway co-localisé avec surcharge minimale (Rust, C, nginx optimisé)
80–95BienGateway bien configuré, normal pour les déploiements en production
60–80AcceptableVérifiez les contraintes de ressources, les sauts réseau ou le tuning JVM
< 60À investiguerÉchecs de connexion, taux d'erreur élevé ou mauvaise configuration

Lecture des Bornes CI95

  • Bornes étroites (ex : [82, 88]) : résultats stables et reproductibles
  • Bornes larges (ex : [60, 95]) : variance élevée entre exécutions — investiguez les voisins bruyants, les pauses GC ou les effets de cold-start
  • Bornes croisant un seuil (ex : [78, 92] croisant 80) : la vraie performance du gateway est ambiguë à ce seuil — plus d'exécutions resserreraient l'intervalle

Reproductibilité

Tous les scripts de l'Arena se trouvent dans le répertoire scripts/traffic/arena/ du dépôt stoa.

FichierObjectif
benchmark.jsDéfinitions des scénarios k6 (Couche 0)
benchmark-enterprise.jsScénarios enterprise k6 (Couche 1)
run-arena.shOrchestrateur shell (Couche 0)
run-arena-enterprise.shOrchestrateur shell (Couche 1)
run-arena.pyScoring Python avec CI95 (Couche 0)
run-arena-enterprise.pyScoring Python avec CI95 (Couche 1)
DockerfileImage Arena : k6 0.54.0 + jq + curl + bash + python3

Exécution en Local

# Construire l'image arena
docker build -t arena-bench scripts/traffic/arena/

# Lancer la Couche 0 contre un gateway local
docker run --rm \
-e GATEWAYS='[{"name":"local","health":"http://host.docker.internal:8080/health","proxy":"http://host.docker.internal:8080/echo/get"}]' \
-e RUNS=3 \
-e DISCARD_FIRST=1 \
arena-bench /scripts/run-arena.sh

Exécution sur Kubernetes

# Couche 0 — exécution ponctuelle
kubectl create job --from=cronjob/gateway-arena arena-manual -n stoa-system
kubectl logs -n stoa-system -l job-name=arena-manual --follow

# Couche 1 — exécution ponctuelle
kubectl create job --from=cronjob/gateway-arena-enterprise arena-ent-manual -n stoa-system
kubectl logs -n stoa-system -l job-name=arena-ent-manual --follow

# Nettoyage
kubectl delete job arena-manual arena-ent-manual -n stoa-system

Limites et Biais Connus

  • Proximité réseau — Les gateways K8s s'exécutent sur le même cluster que k6 (co-localisés). Les gateways VPS s'exécutent sur le même hôte. Les benchmarks inter-réseaux ne sont pas comparables en raison de la latence variable.
  • Préchauffage JVM — L'exécution de préchauffage atténue le biais de cold-start pour les gateways basés sur la JVM (Kong/OpenResty, Gravitee), mais 50 itérations peuvent ne pas réchauffer entièrement tous les chemins de code.
  • Petite taille d'échantillon — La Couche 1 utilise seulement 2 exécutions valides (3 au total, 1 rejetée). Les bornes CI95 avec df=1 utilisent t=12.706, produisant des intervalles très larges. C'est statistiquement correct mais limite la précision.
  • Biais de scoring MCP — La Couche 1 favorise intrinsèquement les gateways avec support MCP. C'est intentionnel : le benchmark mesure les capacités AI-native. La Couche 0 fournit la baseline proxy complémentaire.
  • Backend echo suppose GET — Tous les scénarios proxy utilisent HTTP GET contre le backend echo. Les patterns POST/PUT avec parsing du corps de requête ne sont pas benchmarkés.

Les comparaisons de fonctionnalités sont basées sur des tests exécutés dans des conditions identiques à la date indiquée ci-dessus. Les capacités des gateways évoluent fréquemment. Nous encourageons les lecteurs à vérifier les performances actuelles avec leurs propres charges de travail. Toutes les marques appartiennent à leurs propriétaires respectifs. Voir marques.