Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Service de Rate Limiting

1. Vue d'ensemble

Ce service est une API HTTP de limitation de débit (rate limiting) conçue pour protéger des routes backend contre les abus de trafic. Il permet de définir des règles par client et par route, d'identifier les appelants par session, adresse IP ou de manière globale, et d'évaluer en temps réel si une requête doit être autorisée ou rejetée. Le service expose deux algorithmes atomiques — fenêtre glissante et fenêtre fixe — implémentés via des scripts Lua exécutés directement dans Redis afin de garantir la cohérence sous forte concurrence. L'architecture hexagonale sépare strictement la logique métier des détails d'infrastructure, rendant chaque couche testable et substituable indépendamment.


2. Architecture

Vue d'ensemble des flux

Requête HTTP
     │
     ▼
┌─────────────────────────────────────────────────────────────┐
│                     INFRASTRUCTURE                           │
│                                                             │
│  ┌──────────────┐    ┌──────────────┐    ┌───────────────┐  │
│  │ FastAPI      │    │  Pydantic    │    │  Middleware   │  │
│  │ Router       │───▶│  Schemas     │    │  X-Request-ID │  │
│  └──────┬───────┘    └──────────────┘    └───────────────┘  │
└─────────┼───────────────────────────────────────────────────┘
          │ DTO
          ▼
┌─────────────────────────────────────────────────────────────┐
│                     APPLICATION                              │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  AllowRequestUseCase / ManageClientUseCase / ...     │    │
│  └──────────────────────┬──────────────────────────────┘    │
└─────────────────────────┼───────────────────────────────────┘
                          │ Entités du domaine
                          ▼
┌─────────────────────────────────────────────────────────────┐
│                       DOMAINE                                │
│                                                             │
│  ┌──────────────────┐      ┌──────────────────────────┐     │
│  │ RateLimitRule    │      │  RateLimitEvaluator      │     │
│  │ Client           │      │  (service pur, sans I/O) │     │
│  │ Decision         │      └────────────┬─────────────┘     │
│  └──────────────────┘                   │                   │
│                              ┌──────────┴──────────┐        │
│                              │  CounterStore (port) │        │
│                              │  ClientRepo  (port)  │        │
│                              └──────────┬───────────┘        │
└─────────────────────────────────────────┼────────────────────┘
                                          │ Implémentation
                                          ▼
┌─────────────────────────────────────────────────────────────┐
│                     INFRASTRUCTURE (Redis)                   │
│                                                             │
│  ┌────────────────────────┐   ┌──────────────────────────┐  │
│  │ RedisCounterStore      │   │ RedisClientRepository    │  │
│  │ (Lua sliding/fixed)    │   │ (Hash + JSON rules)      │  │
│  └────────────┬───────────┘   └────────────┬─────────────┘  │
└───────────────┼────────────────────────────┼────────────────┘
                │                            │
                ▼                            ▼
                        ┌──────────┐
                        │  Redis   │
                        └──────────┘

Séparation des couches

Couche Responsabilité Dépendances
Domaine Entités, value objects, logique métier pure Aucune (stdlib uniquement)
Application Orchestration des use cases, DTOs Domaine uniquement
Infrastructure HTTP (FastAPI), Redis, configuration Application + Domaine

La règle fondamentale : les couches internes n'importent jamais les couches externes. Le domaine ne connaît ni Redis ni FastAPI ; il définit des ports (interfaces abstraites) que l'infrastructure implémente.


3. Algorithmes de Rate Limiting

Deux algorithmes sont disponibles, choisis par règle via le champ algorithm.

Algorithme Comportement À utiliser quand
sliding_window Fenêtre temporelle glissante — précis, pas d'effet de bord Précision importante
fixed_window Compteur réinitialisé à intervalle fixe — léger, très rapide Performance prioritaire

Fenêtre glissante : chaque requête est horodatée dans Redis. La fenêtre avance en continu — les entrées expirées sont supprimées avant chaque vérification.

Fenêtre fixe : un seul compteur par intervalle (ex. une clé par minute). Plus simple et moins coûteux, mais un pic est possible en chevauchement de deux intervalles.

Atomicité : les deux algorithmes s'exécutent via des scripts Lua dans Redis. La vérification et l'incrémentation sont indivisibles — aucune race condition possible.


4. Décisions techniques & compromis

Fail-open : autoriser quand Redis est indisponible

Lorsque le CounterStore lève une exception (Redis injoignable, timeout), le RateLimitEvaluator retourne une Decision avec reason=STORAGE_UNAVAILABLE et allowed=True.

Pourquoi fail-open et non fail-closed ? Ce service est un composant transversal, non un gardien de sécurité critique. Bloquer 100 % du trafic lors d'une panne Redis causerait une indisponibilité totale de l'application hôte — un impact plus grave que laisser passer temporairement des requêtes en excès. Cette décision est délibérée et doit être documentée dans les SLO du service aval.

Scripts Lua plutôt que WATCH/MULTI/EXEC

L'alternative classique consiste à utiliser une transaction optimiste : WATCH key → GET → calcul → MULTI/EXEC, avec retry en cas de conflit. Cette approche présente deux problèmes sous charge :

  1. Contention élevée : les retries se multiplient si de nombreux clients accèdent à la même clé simultanément
  2. Complexité client : la logique de retry alourdit le code applicatif

Les scripts Lua s'exécutent de manière sérialisée côté serveur — Redis étant single-threadé pour l'exécution des scripts — ce qui garantit l'atomicité sans aucun retry côté client, avec une latence prévisible.

Redis Sorted Sets pour la Sliding Window

Les Sorted Sets permettent trois opérations en une seule structure :

  1. ZADD — insérer un timestamp avec score (le timestamp lui-même)
  2. ZREMRANGEBYSCORE — supprimer les entrées expirées en O(log n)
  3. ZCARD — compter les entrées actives en O(1)

Un simple LIST ou SET ne permettrait pas de filtrer efficacement par plage temporelle. Un HASH ne supporterait pas le tri naturel par timestamp.

Modèle de cohérence : nœud Redis unique

Le service garantit la cohérence sur un nœud Redis unique. En configuration Redis Cluster (sharding), les scripts Lua multi-clés et les WATCH inter-slots ne sont pas supportés. Une migration vers Redis Cluster nécessiterait soit de router toutes les clés d'un même client vers le même slot (hash tags {client_id}), soit d'utiliser un algorithme distribué tel que Redlock pour les verrous multi-nœuds.


5. Démarrage

Prérequis

  • Docker et Docker Compose installés
  • make disponible

Lancer l'application

# Construire et démarrer l'API + Redis
docker-compose up --build

L'API est disponible sur http://localhost:8000. La documentation interactive Swagger est accessible sur http://localhost:8000/docs.

Lancer les tests

# Tous les tests (unitaires + intégration)
make test

# Tests unitaires uniquement (sans Redis)
make test-unit

# Tests d'intégration (requiert Redis de test)
make test-integration

Variables d'environnement

Variable Défaut Description
REDIS_URL redis://localhost:6379 URL de connexion Redis
APP_ENV development Environnement d'exécution
LOG_LEVEL INFO Niveau de log

Copier .env.example vers .env pour surcharger localement :

cp .env.example .env

6. Référence API

Vérification de santé

curl http://localhost:8000/health
# {"status": "ok"}

Créer un client

curl -X POST http://localhost:8000/v1/clients \
  -H "Content-Type: application/json" \
  -d '{"id": "mon-service", "name": "Mon Service Backend"}'

Réponse 201 :

{
  "id": "mon-service",
  "name": "Mon Service Backend",
  "created_at": "2024-01-15T10:00:00Z",
  "rules": []
}

Obtenir un client

curl http://localhost:8000/v1/clients/mon-service

Attacher une règle de rate limiting

curl -X POST http://localhost:8000/v1/clients/mon-service/policies \
  -H "Content-Type: application/json" \
  -d '{
    "route": "/api/videos",
    "method": "GET",
    "identifier_type": "session_id",
    "algorithm": "sliding_window",
    "limit": 100,
    "window_seconds": 60
  }'

Paramètres :

Champ Type Valeurs Description
route string /api/videos, /api/* Route exacte ou préfixe (avec *)
method string GET, POST, * Méthode HTTP ou wildcard
identifier_type string none, session_id, ip_user_agent Stratégie d'identification
algorithm string sliding_window, fixed_window Algorithme de comptage
limit integer > 0 Nombre max de requêtes autorisées
window_seconds integer > 0 Durée de la fenêtre en secondes

Réponse 201 :

{
  "id": "3f7a1b2c-...",
  "route": "/api/videos",
  "method": "GET",
  "identifier_type": "session_id",
  "algorithm": "sliding_window",
  "limit": 100,
  "window_seconds": 60,
  "burst": null
}

Supprimer une règle

curl -X DELETE http://localhost:8000/v1/clients/mon-service/policies/3f7a1b2c-...
# 204 No Content

Supprimer un client

curl -X DELETE http://localhost:8000/v1/clients/mon-service
# 204 No Content

Inspecter l'état des compteurs

curl http://localhost:8000/v1/clients/mon-service/state

Réponse 200 :

{
  "client_id": "mon-service",
  "rules": [
    {
      "id": "3f7a1b2c-...",
      "route": "/api/videos",
      "method": "GET",
      "identifier_type": "none",
      "algorithm": "sliding_window",
      "limit": 100,
      "window_seconds": 60,
      "burst": null,
      "current_count": 42
    }
  ]
}

Évaluer une requête

curl -X POST http://localhost:8000/v1/evaluate \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "mon-service",
    "route": "/api/videos",
    "method": "GET",
    "identifier": {
      "type": "session_id",
      "session_id": "user-abc-123"
    }
  }'

Réponse 200 — Requête autorisée :

{
  "allowed": true,
  "limit": 100,
  "remaining": 57,
  "reset_at": "2024-01-15T10:01:00Z",
  "retry_after": null,
  "rule_id": "3f7a1b2c-...",
  "reason": "allowed"
}

Réponse 429 — Quota dépassé :

{
  "allowed": false,
  "limit": 100,
  "remaining": 0,
  "reset_at": "2024-01-15T10:01:00Z",
  "retry_after": 23,
  "rule_id": "3f7a1b2c-...",
  "reason": "quota_exceeded"
}

Valeurs possibles pour reason :

Valeur Signification
allowed Requête dans les limites
quota_exceeded Limite atteinte
no_policy Aucune règle ne correspond — fail-open
storage_unavailable Redis injoignable — fail-open

Types d'identifiants :

Type Champ(s) requis Clé de comptage
none — Compteur global (toutes requêtes confondues)
session_id session_id Valeur brute du session ID
ip_user_agent ip, user_agent sha256(ip | user_agent)[:16]

7. Limitations connues & Feuille de route production

Limitations actuelles

Limitation Impact
Nœud Redis unique Pas de haute disponibilité native ; une panne Redis est détectée en fail-open
Pas de persistance configurée Les compteurs sont perdus au redémarrage de Redis si AOF/RDB n'est pas activé
Inspection par identifiant global uniquement GET /state retourne le compteur pour l'identifiant none ; les compteurs par session_id ou ip_user_agent ne sont pas agrégés
Pas d'authentification API Les endpoints d'administration ne sont pas protégés

Feuille de route

  • Support Redis Cluster — utiliser des hash tags {client_id} pour co-localiser les clés d'un même client sur le même slot, permettant l'exécution de scripts Lua multi-clés
  • Persistance Redis — activer la configuration AOF (Append-Only File) pour éviter la perte de compteurs au redémarrage
  • Observabilité — intégration OpenTelemetry (traces distribuées sur le chemin evaluate) et exposition de métriques Prometheus : rate_limit_requests_total{result="allowed|rejected"}, rate_limit_redis_latency_seconds
  • Token Bucket — implémenter un troisième algorithme permettant des bursts contrôlés (le champ burst est déjà présent dans RateLimitRule)
  • Interface d'administration — dashboard pour visualiser les compteurs actifs, modifier les règles à chaud et consulter les logs de décisions
  • Authentification API — HMAC ou JWT sur les routes d'administration, séparation du plan de contrôle (gestion des règles) et du plan de données (évaluation)
  • Latence cible — mesurer et alerter sur la latence du chemin POST /v1/evaluate (objectif p99 < 5 ms sur réseau local avec Redis)

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages