Le théorème CAP expliqué par des exemples concrets
Chaque éditeur de base de données distribuée affirme être rapide, cohérent et toujours disponible. Le théorème CAP est la raison pour laquelle vous devriez vous méfier de ce discours : en présence d'une partition réseau, on ne peut garder que deux propriétés parmi Consistency (cohérence), Availability (disponibilité) et Partition tolerance (tolérance au partitionnement) ; or la tolérance au partitionnement n'est pas vraiment facultative, donc le vrai choix se fait entre cohérence et disponibilité.
Ce que signifient réellement les trois lettres
- Cohérence (C) : chaque lecture reçoit l'écriture la plus récente, ou une erreur. Tous les nœuds voient les mêmes données au même moment.
- Disponibilité (A) : chaque requête reçoit une réponse (non erronée), sans garantie qu'elle contienne l'écriture la plus récente.
- Tolérance au partitionnement (P) : le système continue de fonctionner malgré un nombre arbitraire de messages perdus ou retardés entre les nœuds.
Les partitions réseau arrivent : un commutateur tombe en panne, une liaison entre centres de données sature, la carte réseau d'un nœud devient instable. Comme on ne peut pas choisir que le réseau soit fiable, P n'est pas un vrai choix de conception : c'est un fait des systèmes distribués. Il reste donc à choisir entre C et A pendant une partition. En dehors d'une partition, un système bien construit peut offrir les deux.
Pourquoi « en choisir deux » est trompeur
La formulation classique « choisissez-en deux sur trois » laisse entendre que CA est une option valable, comme si la tolérance au partitionnement pouvait être désactivée. Ce n'est pas le cas, sauf si votre système tourne sur une seule machine sans aucun appel réseau. En pratique, le théorème se réduit donc à une question : quand une partition survient, votre système reste-t-il disponible au prix de la cohérence, ou reste-t-il cohérent au prix de la disponibilité ?
CP en pratique : un magasin de configuration à leader
Prenons un service de configuration reposant sur un magasin de type Raft (etcd, Consul, ZooKeeper). Les écritures passent par un leader, qui doit les répliquer sur une majorité de nœuds avant d'accuser réception.
Client -> Leader -> [replicate to majority] -> ack -> Client
Si une partition réseau scinde le cluster en deux groupes, seul le groupe contenant une majorité de nœuds peut élire un leader et accepter des écritures. Le côté minoritaire cesse complètement d'accepter des écritures : il renvoie des erreurs plutôt que de risquer de servir des données périmées ou en conflit.
# Simplified: a node in the minority partition rejects writes
def handle_write(request):
if not self.has_quorum():
raise Unavailable("no quorum: refusing to risk inconsistent write")
return self.replicate_and_commit(request)
C'est du CP : la cohérence est préservée en sacrifiant la disponibilité du côté minoritaire. C'est exactement ce qu'il faut pour des données de configuration, l'élection d'un leader ou des verrous distribués : servir un propriétaire de verrou périmé est pire qu'une panne temporaire.
AP en pratique : un service de panier d'achat
Prenons maintenant un panier d'achat stocké dans une base de style Dynamo (Cassandra, DynamoDB) avec plusieurs réplicas répartis entre régions. Pendant une partition, chaque réplica continue d'accepter les écritures indépendamment.
# Simplified: every reachable replica accepts writes during a partition
def handle_write(request):
self.local_store.put(request.key, request.value, timestamp=now())
return Ack() # never blocks on other replicas
Deux régions peuvent maintenant avoir des versions divergentes du même panier. Quand la partition se résorbe, le système a besoin d'une stratégie de réconciliation : dernière écriture gagnante selon l'horodatage, horloges vectorielles, ou fusion au niveau applicatif (faire l'union des articles au lieu de choisir un camp).
# A simple merge strategy: union cart items instead of picking a winner
def merge_carts(cart_a, cart_b):
return {**cart_a, **cart_b} # naive union; real systems need care around removals
C'est du AP : le service de panier reste disponible des deux côtés de la partition, et la justesse est rétablie après coup plutôt qu'imposée à l'écriture. Pour un panier d'achat, un système disponible mais à cohérence éventuelle vaut mieux qu'un système cohérent mais indisponible : personne ne veut voir « service indisponible » entre la navigation et le paiement.
Ce n'est pas un choix unique pour tout le système
L'idée utile est que CAP s'applique par opération, et non à un produit de base de données dans son ensemble. De nombreux systèmes réels mélangent les deux : DynamoDB propose des lectures à cohérence éventuelle et des lectures à cohérence forte sur la même table. PostgreSQL avec réplication synchrone est CP pour les écritures du primaire, mais peut être configuré avec des secondaires asynchrones servant des lectures périmées (à tendance AP).
-- PostgreSQL: synchronous_commit controls where you sit between C and A
SET synchronous_commit = 'on'; -- wait for replica ack: favors consistency
SET synchronous_commit = 'off'; -- don't wait: favors availability/latency
Concevez au niveau des opérations individuelles : cette écriture exige-t-elle une cohérence linéarisable, ou cette lecture peut-elle tolérer quelques secondes de retard ? Répondre par opération, et non par base, est l'endroit où CAP rend vraiment service comme outil de conception.
PACELC : la suite sous-estimée du théorème
CAP ne décrit que le comportement pendant une partition. L'extension PACELC de Daniel Abadi ajoute le cas plus courant : même sans partition, on échange de la latence contre de la cohérence. S'il y a Partition (P), choisir entre Disponibilité (A) et Cohérence (C) ; Sinon (Else), en fonctionnement normal, choisir entre Latence et Cohérence.
Un système qui réplique de façon synchrone vers une région distante obtient une cohérence forte mais paie un aller-retour inter-régions à chaque écriture. Un système qui réplique de façon asynchrone obtient une faible latence, mais les lecteurs peuvent voir des données périmées même quand le réseau est parfaitement sain. Ce compromis est présent à chaque requête, et pas seulement lors de rares événements de partition, ce qui explique pourquoi il compte davantage pour la conception quotidienne d'un système que le cas de la partition.
L'appliquer quand vous concevez réellement quelque chose
- Identifiez quelles opérations relèvent du style verrous/élection de leader/décrémentation d'inventaire (besoin de CP) et lesquelles relèvent du style panier/profil/analytique (tolèrent AP).
- Ne choisissez pas une base à cause d'une promesse marketing de « toujours cohérente et toujours disponible » : cette combinaison ne survit pas à une vraie partition. Demandez précisément ce qu'elle fait quand les nœuds ne peuvent pas se joindre.
- Vérifiez si le réglage de cohérence est par cluster ou par requête. Des systèmes comme DynamoDB et Cassandra permettent de choisir par lecture, ce qui est généralement le bon niveau de granularité.
- Souvenez-vous de PACELC : votre choix de cohérence a un coût de latence sur le chemin nominal, pas seulement un coût de risque pendant les pannes.
Le théorème CAP n'est pas une raison de s'angoisser face aux systèmes distribués : c'est une question de liste de contrôle à poser à toute base de données avant de construire dessus : que se passe-t-il exactement pour mes requêtes quand une partition survient, et de quel côté de ce compromis mes données ont-elles réellement besoin ?