Architecture résiliente en production : Circuit breakers, découverte de services et reprise d'activité

Les systèmes distribués modernes exigent des mécanismes sophistiqués pour garantir leur stabilité face aux défaillances. Cette analyse technique présente les stratégies essentielles de protection des services, de localisation dynamique des composants et de continuité d'activité, avec des implémentations concrètes adaptées aux environnements à forte charge.

Mécanismes de protection et régulation du trafic

Principe du disjoncteur logiciel

Le pattern Circuit Breaker constitue une barrière défensive contre les défaillances en cascade. Son fonctionnement s'inspire des disjoncteurs électriques : lorsqu'un seuil d'anomalies est atteint, le circuit s'ouvre automatiquement pour isoler le composant défaillant.

Automate à états du disjoncteur

Trois états distincts régissent son comportement :

  • FERMÉ : le trafic circule normalement, les métriques d'erreur sont collectées
  • OUVERT : les appels échouent immédiatemant sans solliciter le service sous-jacent
  • ENTR'OUVERT : un flux restreint de requêtes de test traverse pour évaluer la récupération

Implémentation avec Resilience4j

Cette bibliothèque légère offre une gestion complète des disjoncteurs. Configuration applicative pour Spring Boot :

resilience4j:
  circuitbreaker:
    instances:
      catalogueService:
        registerHealthIndicator: true
        failureRateThreshold: 45
        slowCallRateThreshold: 40
        slowCallDurationThreshold: 1500ms
        minimumNumberOfCalls: 15
        waitDurationInOpenState: 20s
        permittedNumberOfCallsInHalfOpenState: 5
        automaticTransitionFromOpenToHalfOpenEnabled: true
        slidingWindowType: TIME_BASED
        slidingWindowSize: 30

Code applicatif illustrant la protection d'un service externe :

@Component
public class CatalogueGateway {
    
    private final RestTemplate clientRest;
    private final CircuitBreakerRegistry registreDisjoncteurs;
    
    public CatalogueGateway(RestTemplate clientRest, 
                          CircuitBreakerRegistry registreDisjoncteurs) {
        this.clientRest = clientRest;
        this.registreDisjoncteurs = registreDisjoncteurs;
    }
    
    @CircuitBreaker(name = "catalogueService", fallbackMethod = "recupererArticleSecours")
    public Article recupererArticle(String reference) {
        return clientRest.getForObject(
            "https://catalogue.api/articles/{ref}", 
            Article.class, 
            reference
        );
    }
    
    private Article recupererArticleSecours(String reference, Exception exception) {
        log.warn("Service catalogue indisponible, article {} en mode dégradé", reference);
        return Article.builder()
                .reference(reference)
                .designation("Article temporairement indisponible")
                .prixUnitaire(BigDecimal.ZERO)
                .statut(StatutArticle.NON_REPERTORIE)
                .cacheLocal(true)
                .build();
    }
}

Régulation du débit par limiteurs

Le rate limiting préserve les ressources backend en imposant des plafonds d'utilisation. Cette régulation assure l'équité entre consommateurs et prévient l'asphyxie des services.

Algorithme du seau à jetons distribué

Pour les architectures multi-nœuds, une implémentation Redis garentit la cohérence :

@Service
public class LimiteurDebitRedis {
    
    private final StringRedisTemplate redis;
    private final Duration periodeRemplissage = Duration.ofMillis(500);
    private final long capaciteMaximale = 200L;
    
    public boolean autoriserRequete(String identifiantClient) {
        String cleRedis = "limiteur:" + identifiantClient;
        Instant maintenant = Instant.now();
        
        return redis.execute((connexion) -> {
            connexion.multi();
            
            Map<byte[], byte[]> etatCourant = connexion.hGetAll(cleRedis.getBytes());
            
            long dernierRemplissage = extraireValeur(etatCourant, "dernierRemplissage", 
                maintenant.toEpochMilli());
            long jetonsDisponibles = extraireValeur(etatCourant, "jetons", capaciteMaximale);
            
            long ecoule = maintenant.toEpochMilli() - dernierRemplissage;
            long jetonsAjoutes = ecoule / periodeRemplissage.toMillis();
            long nouveauStock = Math.min(jetonsDisponibles + jetonsAjoutes, capaciteMaximale);
            
            if (nouveauStock < 1) {
                connexion.discard();
                return false;
            }
            
            connexion.hSet(cleRedis.getBytes(), "jetons".getBytes(), 
                String.valueOf(nouveauStock - 1).getBytes());
            connexion.hSet(cleRedis.getBytes(), "dernierRemplissage".getBytes(), 
                String.valueOf(maintenant.toEpochMilli()).getBytes());
            connexion.expire(cleRedis.getBytes(), 120);
            
            connexion.exec();
            return true;
        });
    }
    
    private long extraireValeur(Map<byte[], byte[]> donnees, String champ, long defaut) {
        byte[] valeur = donnees.get(champ.getBytes());
        return valeur != null ? Long.parseLong(new String(valeur)) : defaut;
    }
}

Stratégies de limitation hiérarchiques :

regulation:
  niveaux:
    - identifiant: "client:${idUtilisateur}"
      quota: 50
      fenetre: 60
      unite: SECONDES
    - identifiant: "segment:${adresseIp}"
      quota: 500
      fenetre: 300
      unite: SECONDES
    - identifiant: "global:api"
      quota: 5000
      fenetre: 1
      unite: SECONDES

Localisation dynamique et résilience infrastructurelle

Mécanismes de découverte de services

Dans les écosystèmes éphémères, les instances apparaissent et dispariassent continuellement. La découverte de services automatise cette cartographie dynamique.

Registres de services comparés

Solution Langage Consensus Santé Protocoles
Consul Go Raft Actif + Passif DNS, HTTP, gRPC
etcd Go Raft Passif HTTP, gRPC
ZooKeeper Java Zab Passif Propriétaire
Eureka Java Pair-à-pair Heartbeat REST

Configuration d'un agent Consul :

datacenter = "paris"
data_dir = "/var/lib/consul"
server = true
bootstrap_expect = 5
ui = true

performance {
  raft_multiplier = 1
}

service {
  name = "service-paiement"
  port = 8443
  tags = ["v2.3", "critique"]
  
  check {
    id = "verification-paiement"
    name = "Sondage API paiement"
    http = "https://localhost:8443/sante"
    tls_skip_verify = false
    interval = "5s"
    timeout = "2s"
    failure_before_critical = 3
  }
}

Stratégies de continuité d'activité

La reprise après sinistre repose sur deux métriques fondamentales :

  • RTO (Recovery Time Objective) : durée maximale d'indisponibilité tolérable
  • RPO (Recovery Point Objective) : perte de données maximale acceptable
Criticité RTO cible RPO cible Architecture
Mission critique < 30 min < 1 min Active-active géo-répartie
Élevée 2-4 heures 15-30 min Active-passive avec réplication synchrone
Standard 8-24 heures 2-4 heures Réplication asynchrone avec snapshots

Orchestration Kubernetes de la résilience

Déploiement avec probes de santé et stratégie de mise à jour :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: service-critique
  annotations:
    backup.velero.io/backup-volumes: "donnees-persistantes"
spec:
  replicas: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 2
  template:
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - service-critique
                topologyKey: kubernetes.io/hostname
      containers:
        - name: application
          image: registre/service-critique:2.1.4
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /pret
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /vivant
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 15
            timeoutSeconds: 5
            failureThreshold: 5
          startupProbe:
            httpGet:
              path: /demarrage
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
            failureThreshold: 30
          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "2Gi"
              cpu: "1000m"
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: service-critique-pdb
spec:
  minAvailable: 3
  selector:
    matchLabels:
      app: service-critique

Surveillance des disjoncteurs avec Micrometer

Instrumentation pour observabilité :

@Configuration
public class MetriquesResilience {
    
    private final MeterRegistry registreMetriques;
    
    public MetriquesResilience(MeterRegistry registreMetriques) {
        this.registreMetriques = registreMetriques;
    }
    
    @Bean
    public Consumer<CircuitBreakerRegistry> enregistrerMetriquesDisjoncteurs() {
        return registre -> registre.getAllCircuitBreakers().forEach(disjoncteur -> {
            String nom = disjoncteur.getName();
            
            Gauge.builder("resilience.disjoncteur.etat", 
                         disjoncteur, 
                         d -> d.getState().ordinal())
                .tag("service", nom)
                .description("État du disjoncteur (0=Fermé, 1=Ouvert, 2=Entr'ouvert)")
                .register(registreMetriques);
            
            Gauge.builder("resilience.disjoncteur.taux_erreur", 
                         disjoncteur, 
                         d -> d.getMetrics().getFailureRate())
                .tag("service", nom)
                .description("Pourcentage d'appels en échec")
                .register(registreMetriques);
            
            Gauge.builder("resilience.disjoncteur.appels_bufférisés", 
                         disjoncteur, 
                         d -> d.getMetrics().getNumberOfBufferedCalls())
                .tag("service", nom)
                .description("Nombre d'appels dans la fenêtre glissante")
                .register(registreMetriques);
            
            Gauge.builder("resilience.disjoncteur.appels_lents", 
                         disjoncteur, 
                         d -> d.getMetrics().getNumberOfSlowCalls())
                .tag("service", nom)
                .description("Nombre d'appels dépassant le seuil de lenteur")
                .register(registreMetriques);
        });
    }
}

Configuration dynamique avec Spring Cloud Config

@RefreshScope
@Component
public class ParametresAdaptatifs {
    
    @Value("${resilience.disjoncteur.seuil_erreur:50}")
    private int seuilErreur;
    
    @Value("${resilience.disjoncteur.duree_ouverture:30000}")
    private long dureeOuvertureMs;
    
    @Value("${resilience.limite_debit.utilisateur:100}")
    private int quotaUtilisateur;
    
    public CircuitBreakerConfig genererConfiguration() {
        return CircuitBreakerConfig.custom()
            .failureRateThreshold(seuilErreur)
            .waitDurationInOpenState(Duration.ofMillis(dureeOuvertureMs))
            .build();
    }
    
    @EventListener
    public void surChangementConfiguration(RefreshScopeRefreshedEvent evenement) {
        log.info("Configuration résilience rechargée : seuil={}, attente={}ms", 
                 seuilErreur, dureeOuvertureMs);
    }
}

Ces mécanismes combinés établissent une défense en profondeur : la régulation du trafic prévient la surcharge, les disjoncteurs isolent les défaillances, la découverte de services assure l'adaptabilité, et les procédures de reprise garantissent la pérennité face aux incidents majeurs.

Étiquettes: Circuit Breaker Resilience4j Rate Limiting Service Discovery Consul

Publié le 7 octobre à 20h25