Gestion de l’état dans les systèmes distribués

architecture cloud distributed-systems mémoire-m2 security

Extrait du mémoire de Master 2 Cybersécurité et Cloud de Holali David GAVI — Partie 2/3.

2.1 Stateful vs Stateless

La distinction entre services stateful (avec état) et stateless (sans état) est fondamentale dans la conception de systèmes distribués. Kleppmann (2017), dans Designing Data-Intensive Applications, définit un service stateless comme un composant qui « ne conserve aucune information entre deux requêtes successives — chaque requête contient toutes les informations nécessaires à son traitement ». À l’inverse, un service stateful maintient un état interne qui influence le traitement des requêtes futures.

Les services stateless sont intrinsèquement plus simples à scaler horizontalement : il suffit d’ajouter des instances derrière un load balancer, puisque n’importe quelle instance peut traiter n’importe quelle requête. Dans Kubernetes, un Deployment avec un HorizontalPodAutoscaler (HPA) suffit à gérer l’élasticité. Chaque Pod est interchangeable et remplaçable — la perte d’un Pod n’entraîne aucune perte de données.

Les services stateful — bases de données, caches, files de messages, registres — nécessitent des mécanismes spécifiques : identité réseau stable, stockage persistant, ordre de démarrage/arrêt. Kubernetes introduit les StatefulSets pour répondre à ces besoins : chaque Pod reçoit un nom DNS prédictible (mysql-0, mysql-1), un PersistentVolumeClaim dédié, et un déploiement/suppression ordonnés (Burns, 2018).

La twelve-factor methodology (Wiggins, 2012) préconise des processus stateless avec externalisation de l’état vers des services backing (bases de données, caches, files de messages). Ce principe de « shared-nothing architecture » (Stonebraker, 1986) maximise la scalabilité en éliminant les points de contention entre les instances d’un même service.

2.2 Sticky sessions

Les sticky sessions (ou session affinity) constituent une première approche pour gérer l’état dans les applications web. Le principe : le load balancer (NGINX, HAProxy, AWS ALB) route systématiquement les requêtes d’un même utilisateur vers la même instance backend, typiquement via un cookie ou l’adresse IP source.

Tanenbaum et Van Steen (2017) décrivent trois mécanismes courants :

  • Cookie-based — Le load balancer injecte un cookie (ex: SERVERID=backend-2) dans la réponse HTTP. Les requêtes suivantes sont routées vers le même backend tant que le cookie est présent.
  • IP hash — Un hash de l’adresse IP source détermine le backend cible. Simple mais problématique avec les NAT, les proxies d’entreprise et IPv6.
  • Header-based — Le routage se base sur un header HTTP spécifique (ex: X-Session-ID), offrant plus de flexibilité.

En pratique, les sticky sessions présentent des limitations sérieuses pour les déploiements cloud-native :

  • Répartition de charge inégale — Certaines instances accumulent plus de sessions longues, créant des hotspots. Burns et al. (2016) montrent que ce déséquilibre peut atteindre un ratio de 5:1 dans des scénarios réels.
  • Perte de session au redémarrage — Si l’instance cible crashe ou est redéployée (rolling update), toutes les sessions attachées sont perdues.
  • Incompatibilité avec l’autoscaling — Le scale-down est risqué car la suppression d’un Pod entraîne la perte des sessions qu’il héberge.
  • Complication des déploiements — Les stratégies blue/green et canary sont complexifiées car les utilisateurs doivent être « migrés » d’une version à l’autre.

Kubernetes supporte les sticky sessions au niveau du Service (sessionAffinity: ClientIP) avec un timeout configurable (sessionAffinityConfig.clientIP.timeoutSeconds), mais la documentation officielle recommande de les utiliser uniquement comme solution transitoire (Kubernetes Documentation, 2024).

2.3 Externalisation de session

L’externalisation de session consiste à déplacer l’état de session hors du processus applicatif vers un datastore partagé. Cette approche, préconisée par Richardson (2018) et par le pattern Externalized Session State de Fowler (2002), permet aux instances applicatives de rester stateless tout en maintenant la continuité de session pour l’utilisateur.

Les solutions de stockage de session les plus répandues sont :

  • Redis — Store clé-valeur en mémoire, avec persistance optionnelle (RDB snapshots, AOF). Latence sub-milliseconde, support natif du TTL (Time To Live) pour l’expiration automatique des sessions. Redis Cluster permet le sharding horizontal pour les déploiements à grande échelle (Carlson, 2013).
  • Memcached — Store distribué en mémoire pure, sans persistance. Plus simple que Redis mais sans réplication native. Adapté aux caches de session éphémères où la perte est acceptable.
  • Base de données relationnelle — PostgreSQL, MySQL avec table de sessions indexée. Offre la persistance et la cohérence ACID mais avec une latence supérieure. Spring Session (Pivotal, 2023) fournit une abstraction transparente pour stocker les sessions HTTP dans JDBC, Redis ou Hazelcast.
  • Hazelcast / Apache Ignite — In-Memory Data Grids (IMDG) offrant la distribution automatique des données entre nœuds, avec réplication synchrone ou asynchrone. Idéal pour les architectures nécessitant un cache distribué auto-découvrant dans Kubernetes.

Dans un contexte Kubernetes, Redis déployé via Helm (chart Bitnami) avec un mode Sentinel ou Cluster constitue la solution la plus courante. Le pattern consiste à configurer le framework applicatif (Spring Session, Express avec connect-redis, Django avec django-redis) pour utiliser Redis comme backend de session. Les Pods applicatifs deviennent alors entièrement interchangeables — un rolling update ne provoque aucune perte de session.

2.4 JWT / CAS / SSO

Les architectures modernes tendent vers des mécanismes d’authentification qui minimisent ou éliminent le besoin de sessions côté serveur. Trois approches dominent :

JSON Web Tokens (JWT) — Défini par la RFC 7519 (Jones et al., 2015), un JWT est un jeton autoportant (self-contained) contenant les claims d’identité de l’utilisateur, signé cryptographiquement (HMAC-SHA256 ou RSA/ECDSA). Le serveur n’a pas besoin de stocker de session : la vérification du token se fait localement par validation de la signature. Un JWT se compose de trois parties encodées en Base64URL : Header (algorithme, type), Payload (claims : sub, iat, exp, rôles) et Signature.

Les avantages des JWT sont considérables pour les architectures distribuées :

  • Scalabilité native — Aucun état serveur, chaque microservice valide le token indépendamment
  • Cross-domain — Fonctionne naturellement entre domaines et services hétérogènes
  • Performance — Pas de lookup en base de données à chaque requête

Cependant, les JWT présentent des défis de sécurité documentés par McLean (2023) : la révocation est impossible sans mécanisme complémentaire (blacklist, token introspection) puisque le token est valide jusqu’à son expiration. La combinaison access token (courte durée, 15 min) + refresh token (longue durée, stocké en httpOnly cookie) atténue ce risque.

CAS (Central Authentication Service) — Protocole développé par l’Université de Yale, largement adopté dans le monde académique et les grandes organisations. CAS centralise l’authentification : l’utilisateur s’authentifie une seule fois auprès du serveur CAS, qui délivre un Service Ticket validé côté serveur. Contrairement au JWT, le ticket CAS nécessite une validation serveur-to-serveur, impliquant un état centralisé (Aubry et al., 2004).

SSO (Single Sign-On) — Concept englobant qui permet à un utilisateur de s’authentifier une seule fois pour accéder à plusieurs applications. Les implémentations modernes s’appuient sur des protocoles standardisés :

  • OpenID Connect (OIDC) — Couche d’identité au-dessus d’OAuth 2.0, devenu le standard pour le SSO web. Keycloak (Red Hat), Auth0, Azure AD et Okta implémentent OIDC.
  • SAML 2.0 — Standard XML plus ancien, encore prédominant dans les environnements entreprise (Hughes et al., 2005). Utilisé pour la fédération d’identités inter-organisations.

Dans un cluster Kubernetes, l’authentification est typiquement déléguée à un Identity Provider (IdP) externe via OIDC. L’API server Kubernetes supporte nativement OIDC pour l’authentification des utilisateurs humains, tandis que les Service Accounts gèrent l’identité des workloads (Kubernetes Documentation, 2024).

2.5 Approches hybrides

En pratique, les systèmes distribués complexes combinent plusieurs stratégies de gestion d’état. Richardson (2018) décrit le pattern API Gateway comme un point de convergence naturel pour ces approches hybrides : le gateway termine les sessions utilisateur (JWT validation, rate limiting), tandis que les microservices backend communiquent de manière stateless via des tokens de service.

Un pattern courant dans les architectures à grande échelle est la session hiérarchique :

  • Niveau 1 — Cache local (in-process) : Caffeine, Guava Cache. Latence ~nanoseconde, mais limité à une seule instance.
  • Niveau 2 — Cache distribué (Redis) : Latence ~milliseconde, partagé entre toutes les instances.
  • Niveau 3 — Base de données (PostgreSQL) : Persistance durable, latence ~10ms, utilisé pour la reprise après sinistre.

Le pattern CQRS (Command Query Responsibility Segregation), formalisé par Young (2010) et approfondi par Betts et al. (2013), sépare les modèles de lecture et d’écriture. Combiné avec l’Event Sourcing, il permet de reconstruire l’état complet d’un système à partir de la séquence d’événements, offrant un audit trail naturel et la possibilité de « rejouer » l’historique.

Dans les architectures événementielles, Apache Kafka sert de système nerveux central : les services produisent et consomment des événements de manière asynchrone. Le log Kafka, immuable et ordonné, constitue une source de vérité partagée. Narkhede et al. (2017) argumentent que cette approche résout élégamment le problème de la cohérence entre services : plutôt que de synchroniser des états, on synchronise des événements.

2.6 Impacts sur scalabilité et résilience

Le choix de la stratégie de gestion d’état a des conséquences directes sur la capacité d’un système à scaler et à résister aux pannes.

Scalabilité horizontale — Le théorème CAP (Brewer, 2000), formalisé par Gilbert et Lynch (2002), établit qu’un système distribué ne peut garantir simultanément la Cohérence (Consistency), la Disponibilité (Availability) et la tolérance au Partitionnement réseau (Partition tolerance). Les systèmes stateless maximisent la disponibilité en éliminant la contrainte de cohérence d’état entre nœuds. Les systèmes stateful doivent choisir : CP (cohérence forte, ex: etcd, ZooKeeper) ou AP (disponibilité maximale, ex: Cassandra, DynamoDB).

Le concept de cohérence éventuelle (eventual consistency), théorisé par Vogels (2009) chez Amazon, représente un compromis pragmatique : le système garantit que toutes les répliques convergeront vers le même état, mais sans garantie de délai. Ce modèle, adapté à de nombreux cas d’usage (profil utilisateur, préférences, compteurs), permet une latence et une disponibilité optimales.

Résilience — La résilience d’un système distribué se mesure à sa capacité à maintenir un niveau de service acceptable en présence de pannes. Nygard (2018), dans Release It!, identifie plusieurs patterns de résilience :

  • Circuit Breaker — Coupe les appels vers un service défaillant pour éviter la propagation de la panne (cascade failure). Implémenté par Resilience4j ou Istio.
  • Bulkhead — Isole les composants pour limiter le blast radius d’une panne. En Kubernetes, les Resource Quotas et LimitRanges implémentent ce pattern au niveau infrastructure.
  • Retry avec backoff exponentiel — Réessaye les requêtes échouées avec un délai croissant, évitant la surcharge d’un service en cours de récupération (thundering herd).
  • Timeout — Libère les ressources (threads, connexions) après un délai maximum, prévenant l’épuisement des ressources.

Pour les services stateful, la résilience passe par la réplication (synchrone ou asynchrone) et le failover automatique. PostgreSQL avec Patroni, Redis Sentinel, et les opérateurs Kubernetes spécialisés automatisent la promotion d’une réplique en primaire en cas de défaillance. Le Recovery Point Objective (RPO) et le Recovery Time Objective (RTO) guident le choix entre réplication synchrone (RPO=0, latence plus élevée) et asynchrone (RPO>0, performance optimale).

Références

  • Aubry, S., Mazurier, J. & Mishra, P. (2004). « CAS: a Central Authentication Service ». Yale University ITS.
  • Betts, D., Dominguez, J., Melnik, G. & Simonazzi, F. (2013). Exploring CQRS and Event Sourcing. Microsoft Patterns & Practices.
  • Brewer, E. (2000). « Towards Robust Distributed Systems ». ACM PODC Keynote.
  • Burns, B. (2018). Designing Distributed Systems. O’Reilly Media.
  • Carlson, J. (2013). Redis in Action. Manning Publications.
  • Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley.
  • Gilbert, S. & Lynch, N. (2002). « Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services ». ACM SIGACT News, 33(2).
  • Hughes, J. et al. (2005). « SAML 2.0 Technical Overview ». OASIS.
  • Jones, M., Bradley, J. & Sakimura, N. (2015). « JSON Web Token (JWT) ». RFC 7519, IETF.
  • Kleppmann, M. (2017). Designing Data-Intensive Applications. O’Reilly Media.
  • Kubernetes Documentation (2024). « Service — Session Affinity ». kubernetes.io.
  • McLean, T. (2023). « Critical vulnerabilities in JSON Web Token libraries ». auth0.com/blog.
  • Narkhede, N., Shapira, G. & Palino, T. (2017). Kafka: The Definitive Guide. O’Reilly Media.
  • Nygard, M. (2018). Release It!, 2nd Edition. Pragmatic Bookshelf.
  • Richardson, C. (2018). Microservices Patterns. Manning Publications.
  • Stonebraker, M. (1986). « The Case for Shared Nothing ». IEEE Database Engineering Bulletin, 9(1).
  • Tanenbaum, A. & Van Steen, M. (2017). Distributed Systems, 3rd Edition. Pearson.
  • Vogels, W. (2009). « Eventually Consistent ». Communications of the ACM, 52(1), 40-44.
  • Wiggins, A. (2012). « The Twelve-Factor App ». 12factor.net.
  • Young, G. (2010). « CQRS Documents ». cqrs.files.wordpress.com.
Share: Partager :