BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles Orchestration Saga Pour Les Microservices Utilisant Le Pattern Outbox

Orchestration Saga Pour Les Microservices Utilisant Le Pattern Outbox

Favoris

Points Clés

  • Les Sagas permettent la mise en œuvre de transactions métiers distribuées de longue durée, exécutant un ensemble d'opérations sur plusieurs microservices, appliquant une sémantique cohérente du tout ou rien.
  • Dans un souci de découplage, la communication entre les microservices doit de préférence se faire de manière asynchrone, par exemple en utilisant des journaux de validation distribués comme Apache Kafka.
  • Le pattern Outbox permet aux auteurs de services d'effectuer des écritures dans leur base de données locale et d'envoyer des messages via Apache Kafka, sans recourir à des «doubles écritures» dangereuses.
  • Debezium, une plate-forme de capture de données de changement open source distribuée, fournit une base solide et flexible pour orchestrer les flux Saga à l'aide du pattern Outbox.

Lors du passage aux microservices, l'une des premières choses à réaliser est que les services individuels n'existent pas de manière isolée. Bien que l'objectif soit de créer des services indépendants faiblement couplés avec le moins d'interaction possible, il y a de fortes chances qu'un service ait besoin d'un ensemble de données particulier appartenant à un autre service, ou que plusieurs services doivent agir de concert pour obtenir un résultat cohérent d'un opération dans le domaine de notre métier.

Le pattern Outbox, implémenté via la capture de données modifiées, est une approche éprouvée pour répondre au problème de l'échange de données entre microservices; évitant toute «double écriture» non sécurisée sur plusieurs ressources, par exemple, une base de données et un broker de messages, le pattern Outbox réalise finalement un échange de données cohérent, sans dépendre de la disponibilité synchrone de tous les participants, et ne nécessitant pas de protocoles complexes tels que XA (un standard utilisé pour le traitement des transactions distribuées défini par The Open Group) non plus.

Dans cet article, j'aimerais explorer comment faire passer le pattern Outbox à un niveau supérieur et l'utiliser pour mettre en œuvre des Sagas, des transactions métiers potentiellement de longue durée qui s'étendent sur plusieurs microservices. Un exemple courant est celui de la réservation d'un voyage comprenant plusieurs parties : soit toutes les étapes de vol et l'hébergement doivent être réservés ensemble, soit aucun d'entre eux. Sagas a divisé une de ces transactions métiers globales en une série de multiples transactions de base de données locales, qui sont exécutées par les services participants.

Une introduction sur les Sagas

Pour «roll backer» la transaction métier globale en cas d'échec, Sagas s'appuie sur la notion de transactions compensatoires : chaque transaction locale précédemment appliquée doit pouvoir être «annulée» en exécutant une autre transaction qui applique l'inversion des modifications précédemment effectuées.

Les Sagas ne sont en aucun cas un nouveau concept - elles ont d'abord été discutées par Hector Garcia-Molina et Kenneth Salem dans leur article SIGMOD '87 intitulé Sagas. Mais dans le contexte de l'évolution continue vers des architectures de microservices, les Sagas soutenues par des transactions locales au sein des services participants connaissent une popularité croissante actuellement, comme indiqué par la Spécification MicroProfile pour les actions de longue durée, actuellement en cours de développement. Les Sagas se prêtent en particulier à la mise en œuvre de flux transactionnels sur une période plus longue, ce qui typiquement ne peut pas être traité avec la sémantique ACID.

Pour rendre les choses tangibles, prenons l'exemple d'une entreprise de commerce électronique avec trois services : commande, client et paiement. Lorsqu'un nouveau bon de commande est soumis au service de commande, le flux suivant doit être exécuté, incluant les deux autres services :

 

Figure 1. Transitions d'état de commande

Tout d'abord, nous devons vérifier avec le service client si la commande entrante correspond à la limite de crédit du client (car nous ne voulons pas que les commandes en attente d'un client dépassent un certain seuil). Si le client a une limite de crédit de 500$ et qu'une nouvelle commande d'une valeur de 300$ arrive, cette commande s'inscrit dans la limite actuelle avec une limite restante de 200$. Une commande ultérieure d'une valeur de 250$ serait rejetée en conséquence, car elle dépasserait la limite de crédit ouverte alors en vigueur pour le client.

Si la vérification du plafond de crédit réussit, le paiement de la commande doit être demandé via le service de paiement. Si la vérification de la limite de crédit et la demande de paiement aboutissent, la commande passe à l'état Acceptée, de sorte que son traitement peut commencer (ce qui ne fait pas partie du processus décrit ici).

Cependant, si la vérification de la limite de crédit échoue, la commande passera immédiatement à l'état Rejetée. Si cette étape réussit mais que la demande de paiement suivante échoue, la limite de crédit précédemment allouée doit être libérée à nouveau, avant de passer à l'état Rejeté.

Choix de l'implémentation

Il existe deux manières générales d'implémenter des Sagas distribuées : la chorégraphie et l'orchestration. Dans l'approche chorégraphique, un service participant envoie un message au suivant après avoir exécuté sa transaction locale. Avec l'orchestration, par contre, il existe un service de coordination qui invoque un participant après l'autre.

Les deux approches ont leurs avantages et leurs inconvénients (par exemple, voir cet article par Chris Richardson etcelui-ci par Yves do Régo pour une discussion plus détaillée). Personnellement, je préfère l'approche d'orchestration, car elle définit un lieu central qui peut être interrogé pour obtenir le statut actuel d'une Saga particulière (l'orchestrateur, ou «coordinateur d'exécution de Saga» ("Saga execution coordinator"), SEC en abrégé). Comme il évite la communication point à point entre les participants (autres que l'orchestrateur), il permet également l'ajout d'étapes intermédiaires supplémentaires dans le flux, sans qu'il soit nécessaire de modifier chaque participant.

Avant de plonger dans la mise en œuvre d'un tel flux Saga, il vaut la peine de passer un peu de temps à réfléchir à la sémantique transactionnelle fournie par Sagas. Examinons comment Sagas satisfait aux quatre propriétés ACID classiques des transactions, telles que définies par Theo Härder et Andreas Reuter (sur la base de travaux antérieurs par Jim Gray) dans leur article fondamentalPrinciples of Transaction-Oriented Database Recovery :

  • Atomicité : ✅ —Le pattern garantit que soit tous les services appliquent les transactions locales, soit, en cas d'échec, toutes les transactions locales déjà exécutées sont compensées afin qu'aucune modification des données n'est appliquée de manière effective.
  • Consistance : ✅ —Toutes les contraintes locales sont garanties d'être satisfaites après l'exécution réussie de toutes les transactions constituant la saga, faisant passer le système global d'un état cohérent à un autre.
  • Isolation : ❌ - Comme les transactions locales sont validées pendant que la saga est en cours d'exécution, leurs modifications sont déjà visibles pour d'autres transactions simultanées, malgré la possibilité que la saga échouent finalement, entraînant la compensation de toutes les transactions précédemment appliquées. C'est-à-dire que du point de vue de la saga dans son ensemble, le niveau d'isolement est comparable à «read uncommitted».
  • Durabilité : ✅ —Une fois que les transactions locales de la saga ont été validées, leurs modifications sont persistantes et durables, par exemple après une panne et un redémarrage du service.

Du point de vue du consommateur de services - par exemple, un utilisateur passant un bon de commande avec le service de commande - le système est finalement cohérent; c'est-à-dire qu'il faudra un certain temps avant que le bon de commande soit dans son état correct, selon la logique des différents services participants.

En ce qui concerne la communication entre les services participants, cela peut se produire de manière synchrone, par exemple via HTTP ou gRPC, ou de manière asynchrone, par exemple via des brokers de messages ou des journaux distribués tels que Apache Kafka. Dans la mesure du possible, la communication asynchrone entre les services doit être préférée, car elle délie le service d'envoi de la disponibilité des services consommateurs. Et comme nous le verrons dans la section suivante, même la disponibilité de Kafka en soi n'est pas un problème, grâce à la capture des données modifiées.

Récapitulatif : le pattern Outobx

Maintenant, comment le pattern Outbox et la capture des données modifiées (comme fourni par Debezium) s'intègrent-ils dans tout cela ? Comme indiqué ci-dessus, un coordinateur Saga doit de préférence communiquer de manière asynchrone avec les services participants, via des canaux de messages de requête et de réponse. Apache Kafka est un choix très populaire pour la mise en œuvre de ces canaux. Mais l'orchestrateur (et chaque service participant) doit également appliquer des transactions à leurs bases de données spécifiques pour exécuter leurs parties du flux Saga global.

S'il peut être tentant d'exécuter simplement une transaction de base de données et d'envoyer un message correspondant à Kafka peu de temps après, ce n'est pas une bonne idée. Ces deux actions ne se produiraient pas en une seule transaction couvrant la base de données et Kafka. Ce ne sera qu'une question de temps jusqu'à ce que nous nous retrouvions avec un état incohérent lorsque, par exemple, la transaction de base de données est validée mais l'écriture dans Kafka échoue. Mais les amis ne laissent pas leurs amis faire des écritures doubles, et le pattern Outbox offre un moyen très élégant de résoudre ce problème :

Figure 2. Mise à jour en toute sécurité de la base de données et envoi d'un message à Kafka via le pattern Outbox

Au lieu d'envoyer directement un message à Kafka lors de la mise à jour de la base de données, le service utilise une seule transaction pour à la fois effectuer la mise à jour normale et insérer le message dans une table d'envoi spécifique de sa base de données. Étant donné que cela est effectué dans une seule transaction de base de données, les modifications apportées au modèle du service sont conservées et le message est stocké en toute sécurité dans la table outbox, ou aucune de ces modifications n'est appliquée. Une fois la transaction écrite dans le journal des transactions de la base de données, le processus de capture des données de modification Debezium peut récupérer le message outbox et l'envoyer à Apache Kafka.

Ceci est fait en utilisant au moins une sémantique : dans des circonstances spécifiques, le même message outbox peut être envoyé à Kafka plusieurs fois. Pour permettre aux consommateurs de détecter et d'ignorer les messages en double, chaque message doit avoir un identifiant unique. Il peut s'agir par exemple d'un UUID ou d'une séquence monotone incrémentée spécifique à chaque producteur de message, propagé comme un en-tête de message Kafka.

Implémentation de Sagas à l'aide du pattern Outbox

Avec le pattern Outbox dans notre boîte à outils, les choses deviennent un peu plus claires. Le service de commande, agissant en tant que coordinateur de Saga, déclenche l'ensemble du flux après un appel de commande entrant (généralement via une API REST), en mettant à jour son état local - comprenant le modèle de commande persistant et le journal d'exécution de Saga - et émet des messages vers le deux autres services participants, l'un après l'autre.

Ces deux services réagissent aux messages qu'ils reçoivent via Kafka, effectuent une transaction locale qui met à jour leur état de données et émettent un message de réponse pour le coordinateur via leur table outbox. La conception globale de la solution ressemble à ceci :

Figure 3. Orchestration de la saga à l'aide du pattern Outbox

Vous pouvez trouver une implémentation complète d'un proof-of-concept de cette architecture dans le référentiel d'exemples Debezium sur GitHub. Les éléments clés de l'architecture sont les suivants :

  • Les trois services, ordre (pour gérer les bons de commande et agir comme l'orchestrateur Saga), client (pour gérer la limite de crédit du client), et paiement (pour gérer les paiements par carte de crédit), chacun avec sa propre base de données locale ( Postgres)
  • Apache Kafka comme épine dorsale de la messagerie
  • Debezium, fonctionnant au-dessus de Kafka Connect, s'abonnant aux modifications des trois bases de données différentes et les envoyant aux rubriques Kafka correspondantes, à l'aide de composant de routage d'événements outbox

Les trois services sont implémentés à l'aide de Quarkus, une stack permettant de créer des microservices cloud natifs s'exécutant sur la JVM ou compilés en binaires natifs (via GraalVM). Bien sûr, le modèle pourrait également être implémenté en utilisant d'autres stacks ou langages, à condition qu'ils fournissent un moyen de consommer les messages de Kafka et d'écrire dans une base de données. En outre, une combinaison de différentes technologies de mise en œuvre est possible.

Il y a quatre topics Kafka impliqués : un topic de requête et de réponse pour les messages d'approbation de crédit, et un topic de demande et de réponse pour les messages de paiement. En cas d'exécution réussie de Saga, exactement quatre messages seraient échangés. Si l'une des étapes échoue et qu'une transaction de compensation est nécessaire, il y aura des paires supplémentaires de messages de requête et de réponse pour chaque étape à compenser.

 

Garanties de l'ordre
À des fins de mise à l'échelle, les topics Kafka peuvent être organisés en plusieurs partitions.
Uniquement au sein d'une partition, il est garanti qu'un consommateur recevra les messages exactement dans le même ordre qu'ils ont été envoyés par le producteur. Comme par défaut, tous les messages avec la même clé iront dans la même partition, l'identifiant unique d'une Saga est un choix naturel pour la clé de message Kafka. De cette façon, le bon ordre de traitement des messages d'une seule instance de Saga est assuré.
Plusieurs instances de Saga peuvent être traitées en parallèle si elles se retrouvent dans différentes partitions des topics utilisées pour l'échange de messages Saga.

Figure 4. La séquence d'exécution d'un flux Saga réussi

Chaque service émet des messages sortants via la table outbox dans sa propre base de données. De là, les messages sont capturés via Debezium et envoyés à Kafka, et finalement consommés par le service récepteur. Lors de l'envoi et de la réception de messages, le service de commande, agissant en tant qu'orchestrateur, persiste également la progression de la saga dans une table d'état locale. (Plus à ce sujet ci-dessous.) De plus, tous les participants consignent les identifiants des messages qu'ils ont consommés dans un tableau de journal, pour identifier les doublons potentiels plus tard.

Maintenant, que se passe-t-il si une étape du flux échoue ? Supposons que l'étape de paiement échoue parce que la carte de crédit du client a expiré. Dans ce cas, le montant du crédit précédemment réservé dans le service client doit être à nouveau débloqué. Pour ce faire, le service de commande envoie une demande de compensation au service client. En effectuant un petit zoom arrière (car les détails autour de Debezium et Kafka sont les mêmes qu'avant), voici à quoi ressemblerait l'échange de messages dans ce cas :

Figure 5. La séquence d'exécution d'un flux Saga avec compensation

Après avoir discuté du flux de messages entre les services, passons maintenant à quelques détails d'implémentation du service de commande. L'implémentation du proof-of-concept fournit un orchestrateur Saga générique sous la forme d'une simple machine à états et l'implémentation Saga spécifique à la commande, qui sera discutée plus en détail ci-dessous. La partie «framework» de l'implémentation du service de commande garde une trace de l'état actuel de l'exécution de Saga dans la table sagastate, dont le schéma ressemble à ceci:

Figure 6. Schéma de la table d'état Saga

Cette table remplit le rôle de Saga Log. Ses colonnes sont les suivantes:

  • id : identifiant unique d'une instance Saga donnée, représentant la création d'un bon de commande particulier
  • currentStep : étape à laquelle la saga en est actuellement, par exemple, "credit-approval" ou "payment"
  • payload : une structure de données arbitraire associée à une instance Saga particulière, contenant par exemple l'identifiant du bon de commande correspondant et d'autres informations utiles pendant le cycle de vie de Saga; alors que l'exemple d'implémentation utilise JSON comme format de charge utile, on pourrait également penser à utiliser d'autres formats, par exemple Apache Avro , avec des schémas de charge utile stockés dans un registre de schémas
  • status : le statut actuel de la saga; l'un des STARTED, SUCCEEDED, ABORTING, ou ABORTED
  • stepState : une structure JSON stringifiée décrivant le statut des étapes individuelles, par exemple, "{\"credit-approval\":\"SUCCEEDED\",\"payment\":\"STARTED\"}"
  • type : un type nominal de Saga, par exemple, "order-placement"; utile pour distinguer différents types de Sagas supportés par un système
  • version : une version de verrouillage optimiste, utilisée pour détecter et rejeter les mises à jour simultanées d'une instance de Saga (auquel cas le message déclenchant la mise à jour défaillante doit être retenté, rechargeant l'état actuel à partir du journal de Saga)

Au fur et à mesure que le service de commande envoie des demandes au client et aux services de paiement et reçoit leurs réponses de Kafka, l'état de Saga est mis à jour dans cette table. En mettant en place un connecteur Debezium pour suivre la table sagastate, nous pouvons bien examiner la progression de l'exécution d'une Saga dans Kafka.

Voici les transitions d'état pour un bon de commande dont le paiement échoue. Tout d'abord, la commande errive et l'étape "credit-approval;" démarre:

{
  "id": "73707ad2-0732-4592-b7e2-79b07c745e45",
  "currentstep": null,
  "payload": "\"order-id\": 2, \"customer-id\": 456, \"payment-due\": 4999, \"credit-card-no\": \"xxxx-yyyy-dddd-9999\"}",
  "sagastatus": "STARTED",
  "stepstatus": "{}",
  "type": "order-placement",
  "version": 0
}
{
  "id": "73707ad2-0732-4592-b7e2-79b07c745e45",
  "currentstep": "credit-approval",
  "payload": "{ \"order-id\": 2, \"customer-id\": 456, ... }",
  "sagastatus": "STARTED",
  "stepstatus": "{\"credit-approval\": \"STARTED\"}",
  "type": "order-placement",
  "version": 1
}

À ce stade, un message de demande "credit-approval" a également été conservé dans la table outbox. Une fois celui-ci envoyé à Kafka, le service client le traitera et enverra un message de réponse. Le service de commande traite cela en mettant à jour l'état de Saga et en commençant l'étape de paiement :

{
  "id": "73707ad2-0732-4592-b7e2-79b07c745e45",
  "currentstep": "payment",
  "payload": "{ \"order-id\": 2, \"customer-id\": 456, ... }",
  "sagastatus": "STARTED",
  "stepstatus": "{\"payment\": \"STARTED\", \"credit-approval\": \"SUCCEEDED\"}",
  "type": "order-placement",
  "version": 2
}

Encore une fois, un message est envoyé via la table outbox, maintenant la requête "payment". Cela échoue et le système de paiement répond par un message de réponse indiquant ce fait. Cela signifie que l'étape "credit-approval" doit être compensée via le système client:

{
  "id": "73707ad2-0732-4592-b7e2-79b07c745e45",
  "currentstep": "credit-approval",
  "payload": "{ \"order-id\": 2, \"customer-id\": 456, ... }",
  "sagastatus": "ABORTING",
  "stepstatus": "{\"payment\": \"FAILED\", \"credit-approval\": \"COMPENSATING\"}",
  "type": "order-placement",
  "version": 3
}

Une fois que cela a réussi, la Saga est dans son état final ABORTED :

{
  "id": "73707ad2-0732-4592-b7e2-79b07c745e45",
  "currentstep": null,
  "payload": "{ \"order-id\": 2, \"customer-id\": 456, ... }",
  "sagastatus": "ABORTED",
  "stepstatus": "{\"payment\": \"FAILED\", \"credit-approval\": \"COMPENSATED\"}",
  "type": "order-placement",
  "version": 4
}

Vous pouvez essayer cela vous-même en suivant les instructions du fichier README de l'exemple, où vous trouverez les requêtes de création de commandes réussies et échouées . Il contient également des instructions pour examiner les messages échangés dans les topics Kafka provenant des tables outbox des différents services.

Examinons maintenant certaines parties de l'implémentation spécifique du cas d'utilisation. Le flux Saga démarre dans l'implémentation du endpoint REST du service de commande comme suit :

@POST
@Transactional
public PlaceOrderResponse placeOrder(PlaceOrderRequest req) {
    PurchaseOrder order = req.toPurchaseOrder();
    order.persist(); 

    sagaManager.begin(OrderPlacementSaga.class, OrderPlacementSaga.payloadFor(order)); 

    return PlaceOrderResponse.fromPurchaseOrder(order);
}
 

Conserver la commande d'achat entrante

 

Commencer le flux Saga de passation de commande pour la commande entrante

SagaManager.begin() créera un nouvel enregistrement dans la table sagastate, obtiendra le premier événement outbos de l'implémentation OrderPlacementSaga et le persistera dans la table outbox. La classe OrderPlacementSaga implémente toutes les parties spécifiques au cas d'utilisation du flux Saga :

  • les événements outbox à envoyer pour exécuter une partie du flux Saga
  • les événements outox pour compenser une partie du flux Saga
  • gestionnaires d'événements pour traiter les messages de réponse des autres participants de Saga

L'implémentation OrderPlacementSaga est un peu trop longue pour la montrer ici dans son intégralité (vous pouvez trouver sa source complète sur GitHub ), mais voici quelques éléments clés:

@Saga(type="order-placement", stepIds = {CREDIT_APPROVAL, PAYMENT}) 1️⃣
public class OrderPlacementSaga extends SagaBase {

  private static final String REQUEST = "REQUEST";
  private static final String CANCEL = "CANCEL";
  protected static final String PAYMENT = "payment";
  protected static final String CREDIT_APPROVAL = "credit-approval";

  // ...
  @Override
  public SagaStepMessage getStepMessage(String id) { 2️⃣
    if (id.equals(PAYMENT)) {
      return new SagaStepMessage(PAYMENT, REQUEST, getPayload());
    }
    else {
      return new SagaStepMessage(CREDIT_APPROVAL, REQUEST, getPayload());
    }
  }

  @Override
  public SagaStepMessage getCompensatingStepMessage(String id) { 3️⃣
    // ...
  }

  public void onPaymentEvent(PaymentEvent event) { 4️⃣
    if (alreadyProcessed(event.messageId)) {
      return;
    }

    onStepEvent(PAYMENT, event.status.toStepStatus());
    updateOrderStatus();

    processed(event.messageId);
  }

  public void onCreditApprovalEvent(CreditApprovalEvent event) { 5️⃣
     // ...
  }

  private void updateOrderStatus() { 6️⃣
    if (getStatus() == SagaStatus.COMPLETED) {
      PurchaseOrder order = PurchaseOrder.findById(getOrderId());
      order.status = PurchaseOrderStatus.ACCEPTED;
    }
    else if (getStatus() == SagaStatus.ABORTED) {
      PurchaseOrder order = PurchaseOrder.findById(getOrderId());
      order.status = PurchaseOrderStatus.CANCELLED;
    }
  }

  // ...
}
 

1️⃣ Les identifiants des étapes de la Saga par ordre d'exécution

 

2️⃣ Renvoie le message outbox à émettre pour l'étape donnée

 

3️⃣ Renvoie le message outbox à émettre pour compenser l'étape donnée

 

4️⃣ Gestionnaire d'événements pour les messages de réponse "payment"; il mettra à jour le statut du bon de commande ainsi que le statut de la Saga (via la callback onStepEvent()), qui selon le statut peut soit terminer la Saga, soit lancer sa restauration en appliquant tous les messages de compensation

 

5️⃣ Gestionnaire d'événements pour les messages de réponse "credit approval"

 

6️⃣ Met à jour le statut du bon de commande, en fonction des états actuels de la Saga

...

this.outboxEvent.fire(CreditEvent.of(sagaId, CreditStatus.CANCELLED));
...

La mise en œuvre de services clients et de paiement n'est pas fondamentalement nouvelle, ils sont donc omis ici par souci de concision. Vous pouvez trouver leur code source complet ici et ici.

Quand les choses vont mal

Un élément clé de la mise en œuvre de modèles d'interaction distribuée tels que Sagas est de comprendre comment ils se comportent dans les scénarios de défaillance et de s'assurer que la cohérence (éventuelle) est également obtenue dans de telles circonstances imprévues.

Notez qu'un résultat négatif de l'une des étapes de la Saga (par exemple, si le service de paiement rejette le paiement en raison d'une carte de crédit invalide) n'est pas un scénario d'échec ici; on s'attend explicitement à ce que les participants ne puissent pas exécuter avec succès leur part du flux global, ce qui entraîne l'exécution de transactions locales de compensation appropriées. Cela signifie également qu'un tel échec d'exécution généralement anticipé ne doit pas entraîner une annulation de la transaction de la base de données locale, sinon aucun message de réponse ne serait renvoyé à l'orchestrateur via l'outbox.

Avec ceci en tête, discutons de quelques scénarios d'échec possibles :

Le gestionnaire d'événements d'un message Kafka lève une exception

La transaction de base de données locale est annulée et le consommateur de message ne reconnaît pas au courtier Kafka qu'il a pu traiter le message. Étant donné que le courtier ne reçoit aucune confirmation que le message a été traité, après un certain temps, il renverra le message à plusieurs reprises jusqu'à ce qu'il soit reconnu. Vous devriez avoir une surveillance en place pour détecter une telle situation car le flux Saga ne pourra pas continuer tant que le message n'aura pas été traité.

Le connecteur Debezium se bloque après l'envoi d'un message outbox à Kafka, mais avant de valider le décalage dans le journal des transactions de la base de données source

Après avoir redémarré le connecteur, il continuera à lire les messages de la table outbox en commençant dans le journal par ce qui a été validé en dernier, ce qui peut entraîner l'envoi de certains événements outbox une deuxième fois; c'est pourquoi tous les participants doivent être idempotents, comme implémenté dans l'exemple en utilisant des identifiants de message uniques et des consommateurs qui suivent les messages traités avec succès via les tables de journal.

Le broker Kafka n'est pas en cours d'exécution ou ne peut pas être atteint, par exemple en raison d'un problème réseau

Les connecteurs Debezium peuvent reprendre leur travail une fois que Kafka est à nouveau disponible et accessible; jusque-là, les flux de Saga ne peuvent naturellement pas continuer.

Un message est traité, mais l'acquittement avec Kafka échoue

Le message sera à nouveau transmis au service consommateur, qui trouvera l'identifiant du message dans sa table de journal et ignorera ainsi le message dupliqué.

Mises à jour simultanées de la table d'état de Saga lors du traitement de plusieurs étapes de Saga en parallèle

Bien que nous ayons discuté d'un flux séquentiel avec l'orchestrateur déclenchant les services participants les uns après les autres, vous pouvez également envisager une implémentation Saga qui traite plusieurs étapes en parallèle. Dans ce cas, les messages de réponse arrivant simultanément peuvent entrer en compétition pour mettre à jour la table d'état Saga. Cette situation serait détectée via le verrouillage optimiste implémenté sur cette table, provoquant l'échec, la restauration et la nouvelle tentative d'un gestionnaire d'événements tentant de valider une mise à jour basée sur une version remplacée de l'état de Saga.

Nous pourrions discuter d'autres cas, mais la sémantique générale de la conception globale est celle d'un système finalement cohérent avec des garanties de type au moins une fois.

Bonus: traçage distribué

Lors de la conception d'un flux d'événements entre des systèmes distribués, des informations opérationnelles sont essentielles pour s'assurer que tout fonctionne correctement et efficacement. Le traçage distribué fournit de telles informations : il collecte les informations de trace des systèmes individuels qui contribuent à cette interaction et permet d'examiner les flux d'appels, par exemple dans une interface utilisateur Web, ce qui en fait un outil inestimable pour l'analyse des pannes et le débogage.

La prise en charge d'outbox de Debezium répond à ce problème grâce à une intégration étroite avec la spécification OpenTracing (la prise en charge d'OpenTelemetry est dans la roadmap). En mettant en place un outil tel que Jaeger , ce n'est qu'une question de configuration pour collecter les informations de trace de la commande, du client et des services de paiement et afficher les traces de bout en bout.

Figure 7. Flux Saga dans l'interface utilisateur de Jaeger

La visualisation dans Jaeger montre bien comment le flux Saga est déclenché par la demande REST entrante dans le service de commande (1), un message d'envoi est envoyé au client (2) et de retour à la commande (3), suivi d'un autre envoyé au paiement (4) et enfin retour à l'ordre (5).

La fonctionnalité de traçage permet d'identifier assez facilement les flux inachevés (par exemple, parce qu'un gestionnaire d'événements dans l'un des services participants ne parvient pas à traiter un message) ainsi que les goulots d'étranglement concernant les performances, comme lorsqu'un gestionnaire d'événements met trop de temps à remplir sa tâche du flux Saga.

Récapitulatif et perspectives

Le modèle Saga offre une solution puissante et flexible pour la mise en œuvre de «transactions métiers» de longue durée, qui nécessitent plusieurs services distincts pour s'entendre sur l'application ou l'annulation d'un ensemble de modifications de données.

Grâce au pattern outbox, implémenté avec CDC, Debezium et Apache Kafka, le coordinateur Saga est découplé de la disponibilité de l'un des autres services participants. Les pannes temporaires de participants uniques n'ont pas d'impact sur le flux global de la saga: une fois que les composants sont de nouveau en place, la Saga continuera à partir du point où elle a été interrompue auparavant.

Bien entendu, nous devrions aspirer à une réduction des services qui réduise autant que possible le besoin d'interaction avec les services distants. Par exemple, il peut être possible de déplacer la logique de limite de crédit de l'exemple vers le service de commande lui-même, évitant ainsi la coordination avec le service client. Mais en fonction des besoins de l'entreprise, la nécessité d'une telle interaction couvrant plusieurs services peut être impossible à éviter, en particulier lorsqu'il s'agit d'intégrer des systèmes hérités ou des systèmes qui ne sont pas sous notre contrôle.

Lors de la mise en œuvre de modèles complexes tels que Sagas, il est essentiel de comprendre exactement leurs contraintes et leur sémantique. Deux choses à prendre en compte dans le contexte de la solution proposée sont la cohérence éventuelle inhérente et le niveau d'isolement limité de la transaction métier globale. Par exemple, l'allocation d'une partie de la limite de crédit du client peut entraîner le rejet d'une autre commande de ce client, qui a été soumise au même moment, même si la première commande ne passe pas par la suite.

L'exemple de projet abordé dans cet article fournit une implémentation au niveau PoC pour l'orchestration Saga basée sur CDC et le pattern outbox. Il est organisé en deux parties:

  • Un composant «framework» générique qui fournit la logique d'orchestration de Saga sous la forme d'une simple machine à états avec le journal d'exécution de Saga
  • L'implémentation spécifique du cas d'utilisation de placement de commande discuté (la classe OrderPlacementSaga indiquée dans les parties ci-dessus, les points de endpoints REST accompagnant, etc.)

À l'avenir, nous pourrions extraire l'ancienne partie dans un composant réutilisable, par exemple via l'extension Debezium Quarkus existante. Si cela vous intéresse, faites-le nous savoir en accédant à la liste de diffusion Debezium. Une fonctionnalité potentielle à ajouter serait le moyen d'exécuter simultanément plusieurs étapes de Saga. Que ce soit raisonnable ou non est une décision métier, mais ce ne serait pas difficile d'un point de vue technique. Les conflits lors de la mise à jour de l'état de la saga peuvent devenir un problème critique dans ce cas; le post Optimizations to scatter-gather sagas discute des solutions potentielles pour cela. Il serait également intéressant de disposer d'une fonctionnalité pour surveiller et identifier les Sagas qui n'ont pas été achevées après un certain temps.

L'implémentation proposée offre un moyen d'exécuter de manière fiable des transactions métiers avec une sémantique «tout ou rien» sur une gamme de services multiples. Pour les cas d'utilisation avec des exigences plus complexes, telles que les flux avec une logique conditionnelle, vous pouvez jeter un coup d'œil aux moteurs de workflow et aux outils d'automatisation des processus métier existants, tels que Kogito. Une autre technologie intéressante à surveiller est la spécification MicroProfile for long-running activities (LRA), qui est actuellement en cours de développement. La communauté MicroProfile discute également de l'intégration avec des implémentations transactionnelles d'outbox comme celle de Debezium.

Un grand merci à Hans-Peter Grahsl , Bob Roldan , Mark Little et Thomas Betts pour leurs nombreux commentaires lors de la rédaction de cet article !

A propos de l'auteur

Gunnar Morling est un ingénieur logiciel et un passionné de l'open source. Il dirige le projet Debezium , un outil de capture de données de changement (change data capture : CDC). Il est Java Champion, responsable des spécifications pour Bean Validation 2.0 (JSR 380) et a fondé plusieurs projets open source tels que Layrry , Deptective et MapStruct . Avant de rejoindre Red Hat, Gunnar a travaillé sur un large éventail de projets Java EE dans les secteurs de la logistique et de la vente au détail. Il est basé à Hambourg, en Allemagne. Twitter: @gunnarmorling

 

 

Evaluer cet article

Pertinence
Style

Contenu Éducatif

Bonjour étranger!

Vous devez créer un compte InfoQ ou cliquez sur pour déposer des commentaires. Mais il y a bien d'autres avantages à s'enregistrer.

Tirez le meilleur d'InfoQ

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

Commentaires de la Communauté

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

BT