Aller au contenu

Le commit à deux phases (2PC) expliqué : prepare, commit et pourquoi il bloque

Comment le commit à deux phases rend une transaction atomique entre bases, ce qui arrive si le coordinateur tombe, et quand préférer une saga ou une outbox.

· Fondateur et développeur

Publié le

Diagramme de séquence : une application, un coordinateur, une base des commandes et une base des paiements. Un fragment par montre PREPARE envoyé aux deux bases et les votes YES. Un fragment alt montre COMMIT et les accusés si tous votent oui, et ROLLBACK si l'un vote non.

Une commande est écrite dans une base de données, le paiement dans une autre. Les deux doivent avoir lieu, ou aucun. Chaque base peut valider sa propre part de façon atomique, mais rien au niveau local n'empêche l'une de valider pendant que l'autre plante. Le commit à deux phases (2PC) est le protocole classique pour ce problème : il est intégré à la plupart des bases relationnelles via la norme XA et utilisé aujourd'hui au cœur des bases SQL distribuées.

Il a aussi une faiblesse célèbre : si le coordinateur tombe au mauvais moment, les participants peuvent rester bloqués avec leurs verrous, sans moyen de décider seuls. Cet article déroule le protocole avec un diagramme de séquence, chaque cas de panne, la façon dont les vrais systèmes l'implémentent, et les cas où une saga ou une outbox est le meilleur outil.

Les rôles

  • Coordinateur, ou gestionnaire de transactions : il pilote le protocole et décide de l'issue. Ce peut être une bibliothèque dans votre serveur d'applications, un service séparé ou un composant d'une base de données distribuée.
  • Participants, ou gestionnaires de ressources : les bases de données ou les files qui détiennent chacune une partie de la transaction.

Chaque partie tient un journal durable. Les écritures dans ce journal ne sont pas un détail d'implémentation : ce sont elles qui permettent au protocole de survivre aux plantages.

Le protocole

Phase 1 : prepare.

  1. L'application fait son travail dans chaque participant au sein d'une transaction globale, puis demande au coordinateur de valider.
  2. Le coordinateur envoie PREPARE à chaque participant.
  3. Chaque participant s'assure qu'il pourra valider quoi qu'il arrive ensuite : il écrit les modifications de la transaction et un enregistrement de préparation dans son journal durable, conserve ses verrous et vote YES. S'il ne peut pas valider, par exemple parce qu'une contrainte a échoué, il vote NO et annule.

Phase 2 : commit ou abort.

  1. Si tous les votes sont YES, le coordinateur écrit une décision COMMIT dans son propre journal. Cette écriture est le point de commit : dès lors, la transaction est validée, même si toutes les machines plantent une milliseconde plus tard.
  2. Le coordinateur envoie COMMIT à chaque participant. Chacun valide, libère ses verrous et accuse réception.
  3. Si un participant vote NO ou ne répond pas à temps, le coordinateur décide ABORT et envoie ROLLBACK à la place.

Un vote YES est une promesse. Après avoir voté YES, un participant a renoncé au droit de décider seul : il doit valider si on le lui demande, même après un redémarrage, et il ne peut pas annuler de lui-même, car le coordinateur a peut-être déjà décidé de valider.

Le coût

Pour N participants, un commit réussi demande deux allers-retours depuis le coordinateur et quatre messages par participant : PREPARE, vote, COMMIT et accusé de réception. Chaque participant force deux fois l'écriture de son journal sur disque, au prepare et au commit, et le coordinateur force sa décision une fois. Les verrous sont tenus pendant tout ce temps : c'est donc le participant le plus lent qui décide combien de temps chacun des autres bloque le travail concurrent.

Ce qui se passe en cas de panne

Panne Quand Issue
Un participant plante ou expire Avant de voter Le coordinateur annule la transaction
Un participant vote NO Phase 1 Le coordinateur annule la transaction
Un participant plante Après avoir voté YES Au redémarrage, il trouve l'enregistrement de préparation et demande l'issue au coordinateur, en gardant ses verrous entre-temps
Le coordinateur plante Avant d'écrire la décision Les participants préparés attendent. À la reprise, sans décision dans son journal, le coordinateur annule
Le coordinateur plante Après avoir écrit COMMIT À la reprise, il lit son journal et renvoie COMMIT
Partition réseau Après le vote YES d'un participant Ce participant reste bloqué jusqu'à pouvoir joindre le coordinateur

Les lignes qui comptent sont celles où un participant a voté YES et ne peut pas entendre la décision. Il ne peut pas valider, car un autre participant a peut-être voté NO. Il ne peut pas annuler, car le coordinateur a peut-être validé. Il attend, verrous en main, tant que le coordinateur est injoignable. Toutes les autres transactions qui touchent ces lignes attendent aussi. C'est ce que l'on veut dire quand on qualifie 2PC de protocole bloquant.

Les transactions préparées bloquées, en pratique

PostgreSQL implémente le côté participant avec PREPARE TRANSACTION, COMMIT PREPARED et ROLLBACK PREPARED. La fonctionnalité est désactivée par défaut (max_prepared_transactions vaut 0), et la documentation prévient qu'elle est destinée aux gestionnaires de transactions externes, pas aux applications. Une transaction préparée oubliée garde ses verrous et empêche VACUUM de récupérer de l'espace.

En trouver une et la résoudre ressemble à ceci :

-- Prepared transactions waiting for a decision
SELECT gid, prepared, owner, database
FROM pg_prepared_xacts
ORDER BY prepared;

-- Only after checking the coordinator's log for the real outcome:
COMMIT PREPARED 'order-7f3a';
-- or
ROLLBACK PREPARED 'order-7f3a';

Ne résolvez jamais une transaction préparée au hasard. Si le coordinateur a validé et que vous annulez un participant, vous avez créé exactement l'incohérence que 2PC existe pour empêcher. MySQL offre le même mécanisme avec XA PREPARE, XA COMMIT et XA RECOVER.

Les variantes qui atténuent la douleur

  • Presumed abort. Si le coordinateur n'a aucune trace d'une transaction, la réponse est l'annulation. Cela économise des écritures de journal pour les transactions annulées et simplifie la reprise ; la plupart des implémentations l'utilisent.
  • Participants en lecture seule. Un participant qui n'a rien modifié peut voter « read-only » et sauter la phase 2.
  • Commit à trois phases. Ajoute un tour de pre-commit pour que les participants puissent terminer sans le coordinateur, mais seulement si les délais des messages sont bornés et que le réseau ne se partitionne jamais. Ces hypothèses tiennent rarement : le 3PC reste surtout un sujet de manuel.
  • Répliquer le coordinateur et les participants. Le vrai remède au blocage consiste à faire de chaque rôle un groupe répliqué qui survit à la perte d'une machine. Spanner, chez Google, exécute le commit à deux phases sur des groupes Paxos précisément pour cette raison, et d'autres bases SQL distribuées appliquent la même idée avec Raft. Le commit à deux phases décide toujours de l'issue ; le consensus garde chaque partie disponible.

Quand utiliser 2PC, et quand l'éviter

2PC convient quand tous les participants sont des bases de données ou des brokers qui prennent en charge une étape de préparation, que vous les contrôlez tous, que les transactions sont courtes et que vous avez besoin d'atomicité et d'isolation entre eux. Il fonctionne mal entre des services appartenant à d'autres équipes, entre des API HTTP sans étape de préparation (un prestataire de paiement ne gardera pas votre débit dans un état préparé) et pour des processus métier qui durent des minutes ou des jours.

Commit à deux phases Saga Transactional outbox
Cohérence Atomique ; les verrous assurent l'isolation À terme, avec des étapes de compensation À terme, entre une base de données et un broker
Blocage Oui, tant qu'une décision est en attente Non Non
Prérequis Des participants qui gèrent prepare Une logique de compensation par étape Une base de données plus un relais
Usage typique Les bases ou shards que vous exploitez Les workflows métier entre services Publier des événements de façon fiable après une écriture

Le diagramme du pattern saga montre l'alternative par compensation, et le pattern transactional outbox couvre le cas le plus courant où les équipes se tournent vers 2PC : écrire dans une base de données et publier un message ensemble. Souvent, la meilleure solution est structurelle : placez les données qui doivent changer ensemble dans une seule base, ou un seul shard, comme l'explique le diagramme de sharding de base de données.

Le diagramme du commit à deux phases

Diagramme de séquence : une application, un coordinateur, une base des commandes et une base des paiements. Un fragment par montre PREPARE envoyé aux deux bases et les votes YES. Un fragment alt montre COMMIT et les accusés si tous votent oui, et ROLLBACK si l'un vote non.

10 composants · 12 connexions

Ouvrir dans Archboard
Contenu du diagramme
  • Application
  • Coordinator
  • Orders DB
  • Payments DB
  • phase 1: prepare
  • phase 2: all votes YES
  • commit()
  • PREPARE
  • YES (prepare record on disk)
  • COMMIT
  • ACK, locks released
  • ACK
  • ROLLBACK
  • committed or aborted
Commit à deux phases entre deux bases de données. En phase 1, le coordinateur demande aux deux participants de se préparer et recueille leurs votes ; en phase 2, il journalise la décision et dit à tous de valider, ou d'annuler si un vote était non.

Comment le lire :

  • Quatre lignes de vie. L'application, le coordinateur, la base des commandes et la base des paiements. Le temps s'écoule vers le bas.
  • Le fragment par correspond à la phase 1. PREPARE part en même temps vers les deux participants, et chacun renvoie un vote après avoir écrit son enregistrement de préparation.
  • Le fragment alt correspond à la phase 2. La branche du haut est le cas normal : tous les votes sont YES, le coordinateur journalise COMMIT et les deux participants valident et accusent réception. La branche du bas est le chemin d'annulation.
  • La note marque la fenêtre d'incertitude entre un vote YES et la décision, pendant laquelle les participants tiennent des verrous et ne peuvent pas décider seuls.
  • Le bloc Markdown liste les cas de panne du tableau ci-dessus.

Dessiner honnêtement les transactions distribuées

  • Montrez les écritures de journal. Un diagramme 2PC sans « écrire l'enregistrement de préparation » ni « écrire la décision » omet ce qui le rend correct.
  • Marquez le point de commit. Un seul message ou une seule écriture de journal décide de l'issue ; montrez-le.
  • Dessinez la fenêtre où tout bloque. C'est la raison pour laquelle les équipes évitent 2PC ; un diagramme qui la cache induit en erreur.
  • Placez les chemins de panne dans leurs propres diagrammes. Un plantage du coordinateur après le prepare mérite sa propre séquence avec les étapes de reprise.

Pour la notation elle-même, l'exemple de diagramme de séquence couvre les lignes de vie, les fragments et le nommage.

Le construire dans Archboard

  1. Connectez-vous avec Google ou GitHub et créez un diagramme.
  2. Ajoutez quatre nœuds Ligne de vie du pack Séquence : Application, Coordinator, Orders DB et Payments DB.
  3. Dessinez les messages sous forme d'arêtes, de haut en bas : la demande de commit, PREPARE vers chaque base, les votes YES, COMMIT vers chaque base et les accusés de réception.
  4. Ajoutez un Fragment combiné du pack Séquence avec l'opérateur par autour des messages de préparation, et un second Fragment combiné avec l'opérateur alt autour des branches de commit et de rollback.
  5. Ajoutez une Note pour la fenêtre d'incertitude et un bloc Markdown du pack Base avec le tableau des pannes.

L'exemple utilise 7 nœuds de diagramme et reste dans les limites de l'offre Free.

FAQ

Pourquoi dit-on que le commit à deux phases est un protocole bloquant ? Un participant qui a voté YES ne peut ni valider ni annuler de lui-même. Si le coordinateur devient injoignable avant d'envoyer la décision, le participant attend en gardant ses verrous, tout comme chaque transaction qui a besoin de ces lignes.

Le commit à deux phases est-il la même chose qu'une transaction distribuée ? C'est la façon la plus courante d'en valider une. Une transaction distribuée s'étend sur plusieurs ressources ; le commit à deux phases est le protocole qui rend sa validation atomique.

Le commit à trois phases résout-il le problème du blocage ? Seulement sous des hypothèses qui tiennent rarement : des délais de message bornés et aucune partition réseau. Le remède pratique consiste à répliquer le coordinateur et les participants par consensus, comme le fait Spanner.

Puis-je utiliser le commit à deux phases entre microservices ? Rarement avec succès. Les services devraient exposer une étape de préparation et tenir des verrous les uns pour les autres, ce qui couple leur disponibilité. Entre services, les sagas et le pattern outbox sont les choix habituels.

Le commit à deux phases garantit-il l'isolation ? Il garantit l'atomicité. L'isolation vient des verrous des participants, que 2PC conserve jusqu'à l'arrivée de la décision : les transactions concurrentes ne voient donc pas de résultats partiels.

Commencez gratuitement — un siège, sans carte bancaire.

Articles associés

← Retour au blog

Ouvrez un canevas en une minute

Connectez-vous avec Google ou GitHub. Gratuit pour un siège. Passez à un forfait supérieur quand l’équipe vous rejoint.