web-dev-qa-db-fra.com

Synchronisation des transactions entre la base de données et le producteur Kafka

Nous avons une architecture de micro-services, avec Kafka utilisé comme mécanisme de communication entre les services. Certains services ont leurs propres bases de données. Supposons que l'utilisateur appelle le service A, ce qui devrait résulter dans un enregistrement (ou un ensemble d'enregistrements) en cours de création dans la base de données de ce service. En outre, cet événement doit être signalé aux autres services, en tant qu'élément sur une rubrique Kafka. Quelle est la meilleure façon de s'assurer que les enregistrements de base de données ne sont écrits que si la rubrique Kafka est correctement mise à jour (créant essentiellement une transaction distribuée autour de la mise à jour de la base de données et de la mise à jour Kafka )?

Nous pensons utiliser spring-kafka (dans un service Spring Boot WebFlux), et je peux voir qu'il a un KafkaTransactionManager , mais d'après ce que je comprends, il s'agit plus de Kafka transactions elles-mêmes (assurant la cohérence entre les Kafka producteurs et consommateurs), plutôt que de synchroniser les transactions sur deux systèmes (voir ici : "Kafka ne prend pas en charge XA et vous devez faire face à la possibilité que le tx DB puisse se valider pendant que le Kafka tx revient en arrière."). De plus, je pense que cette classe s'appuie sur la transaction de Spring framework qui, au moins pour autant que je sache actuellement, est lié aux threads, et ne fonctionnera pas si vous utilisez une approche réactive (par exemple WebFlux) où différentes parties d'une opération peuvent s'exécuter sur différents threads (nous utilisons reactive-pg-client , tout comme la gestion manuelle des transactions, plutôt que l'utilisation du framework Spring.)

Quelques options auxquelles je peux penser:

  1. N'écrivez pas les données dans la base de données: écrivez-les uniquement dans Kafka. Utilisez ensuite un consommateur (dans le service A) pour mettre à jour la base de données. Cela semble ne pas être le plus efficace et aura des problèmes dans la mesure où le service que l'utilisateur a appelé ne peut pas voir immédiatement les modifications de la base de données qu'il aurait dû créer.
  2. N'écrivez pas directement à Kafka: écrivez uniquement dans la base de données et utilisez quelque chose comme Debezium pour signaler la modification à Kafka. Le problème ici est que les modifications sont basées sur des enregistrements de base de données individuels, alors que l'événement significatif métier à stocker dans Kafka peut impliquer une combinaison de données provenant de plusieurs tables.
  3. Écrivez d'abord dans la base de données (si cela échoue, ne faites rien et jetez simplement l'exception). Ensuite, lorsque vous écrivez à Kafka, supposez que l'écriture peut échouer. Utilisez la fonctionnalité de réessai automatique intégrée pour qu'elle continue d'essayer pendant un certain temps. Si cela échoue finalement, essayez d'écrire dans une file d'attente de lettres mortes et de créer une sorte de mécanisme manuel pour que les administrateurs le trient. Et si l'écriture dans le DLQ échoue (ie Kafka est complètement arrêté), connectez-le simplement d'une autre manière (par exemple à la base de données), et créez à nouveau une sorte de mécanisme manuel pour que les administrateurs le trient en dehors.

Quelqu'un a-t-il eu des réflexions ou des conseils sur ce qui précède, ou a-t-il pu corriger des erreurs dans mes hypothèses ci-dessus?

Merci d'avance!

12
Yoni Gibbs

Je suggère d'utiliser une variante légèrement modifiée de l'approche 2.

Écrivez dans votre base de données uniquement, mais en plus des écritures de table réelles, écrivez également des "événements" dans une table spéciale de cette même base de données; ces enregistrements d'événements contiendraient les agrégations dont vous avez besoin. De la manière la plus simple, vous insérez simplement une autre entité, par exemple mappé par JPA, qui contient une propriété JSON avec la charge utile globale. Bien sûr, cela pourrait être automatisé par certains moyens d'écoute de transaction/composant de structure.

Utilisez ensuite Debezium pour capturer les modifications uniquement à partir de cette table et les diffuser dans Kafka. De cette façon, vous avez les deux: éventuellement état cohérent dans Kafka (les événements dans Kafka peuvent suivre derrière ou vous pouvez voir quelques événements une deuxième fois après un redémarrage) , mais ils reflèteront éventuellement l'état de la base de données) sans avoir besoin de transactions distribuées et sans la sémantique des événements de niveau métier que vous recherchez.

(Avertissement: je suis le responsable de Debezium; curieusement, je suis en train d'écrire un article de blog discutant de cette approche plus en détail)

Voici les articles

https://debezium.io/blog/2018/09/20/materializing-aggregate-views-with-hibernate-and-debezium/

https://debezium.io/blog/2019/02/19/reliable-microservices-data-exchange-with-the-outbox-pattern/

11
Gunnar

tout d'abord, je dois dire que je ne suis ni Kafka, ni un expert Spring, mais je pense que c'est plus un défi conceptuel lors de l'écriture sur des ressources indépendantes et la solution devrait être adaptable à votre pile technologique. De plus, je dois dire que cette solution essaie de résoudre le problème sans composant externe comme Debezium, car à mon avis, chaque composant supplémentaire pose des problèmes de test, de maintenance et d'exécution d'une application qui est souvent sous-estimée lors du choix d'une telle option. De plus, toutes les bases de données ne peuvent pas être utilisées comme source Debezium.

Pour nous assurer que nous parlons des mêmes objectifs, clarifions la situation dans un exemple de compagnie aérienne simplifiée, où les clients peuvent acheter des billets. Après une commande réussie, le client recevra un message (mail, push-notification,…) qui sera envoyé par un système de messagerie externe (le système avec lequel nous devons parler).

Dans un monde JMS traditionnel avec une transaction XA entre notre base de données (où nous stockons les commandes) et le fournisseur JMS, cela ressemblerait à ceci: Le client définit la commande sur notre application où nous commençons une transaction. L'application stocke la commande dans sa base de données. Ensuite, le message est envoyé à JMS et vous pouvez valider la transaction. Les deux opérations participent à la transaction même lorsqu'elles parlent à leurs propres ressources. Comme la transaction XA garantit ACID, tout va bien.

Apportons Kafka (ou toute autre ressource qui ne peut pas participer à la transaction XA) dans le jeu. Comme il n'y a plus de coordinateur qui synchronise les deux transactions, l'idée principale de ce qui suit est de traitement divisé en deux parties avec un état persistant.

Lorsque vous stockez la commande dans votre base de données, vous pouvez également stocker le message (avec des données agrégées) dans la même base de données (par exemple, en tant que JSON dans une colonne CLOB) que vous souhaitez envoyer à Kafka après . Même ressource - ACID garanti, tout va bien jusqu'à présent. Vous avez maintenant besoin d'un mécanisme qui interroge votre "KafkaTasks" -Table pour de nouvelles tâches qui devraient être envoyées à un Kafka-Topic (par exemple avec un service de minuterie, peut-être que l'annotation @Scheduled peut être utilisé au printemps). Une fois le message envoyé avec succès à Kafka, vous pouvez supprimer l'entrée de tâche. Cela garantit que le message à Kafka n'est envoyé que lorsque la commande est également enregistrée avec succès dans la base de données d'application. Avons-nous obtenu les mêmes garanties que lors de l'utilisation d'une transaction XA? Malheureusement, non, car il est toujours possible d'écrire dans Kafka fonctionne mais la suppression de la tâche échoue. Dans ce cas, le mécanisme de nouvelle tentative (vous en aurez besoin comme mentionné dans votre question) retraiterait la tâche et enverrait le désordre vieillir deux fois. Si votre analyse de rentabilisation est satisfaite de cette garantie "au moins une fois", vous avez ici une solution semi-complexe à mon humble avis qui pourrait être facilement mise en œuvre en tant que fonctionnalité de structure afin que tout le monde n'ait pas à se soucier des détails.

Si vous avez besoin de "exactement une fois", vous ne pouvez pas stocker votre état dans la base de données d'application (dans ce cas, "la suppression d'une tâche" est l '"état") mais à la place, vous devez le stocker dans Kafka = (en supposant que vous avez des garanties ACID entre deux Kafka rubriques). Un exemple: supposons que vous ayez 100 tâches dans la table (ID 1 à 100) et que le travail de tâche traite les 10 premières. Vous écrivez vos messages Kafka dans leur sujet et un autre message avec l'ID 10 dans "votre sujet". Tous dans la même transaction Kafka. Au cycle suivant, vous consommez votre sujet (la valeur est 10 ) et prenez cette valeur pour obtenir les 10 prochaines tâches (et supprimez les tâches déjà traitées).

S'il existe des solutions plus faciles (en application) avec les mêmes garanties, j'ai hâte de vous entendre!

Désolé pour la longue réponse mais j'espère que ça aide.

2
Jonas