Skip to content

Migrer vers le Event Sourcing, partie 9: outils et patrons additionnels

by Julien on novembre 14th, 2011

An english version of this post is available here.

Je vais essayer de passer brièvement sur quelques possibilités que les systèmes centrés sur les événements peuvent offrir. Chaque sujet mériterait probablement un billet complet, ou même un livre. Si vous souhaitez plus d’informations, il y a beaucoup de matériel disponible dans le groupe d’usager DDD / CQRS. Greg Young va également prochainement sortir un ouvrage sur ces thèmes.

Clichés

Évidemment, le event sourcing pur va rencontrer rapidement des limitations quand les agrégats vont avoir plusieurs milliers d’événements. Vous allez donc probablement devoir implémenter un système de clichés. L’idée est de demander à l’agrégat de nous fournir un cliché de son état, et de stocker ce cliché. Lorsque l’on charge un agrégat dans l’application, nous demandons alors simplement le dernier cliché ainsi que les événements publiés depuis que ce cliché ai été stocké. Les clichés peuvent simplement être stocker dans une table comprenant l’identifiant de l’agrégat, sa version actuelle, le cliché courant et la version du cliché.

Vous devriez implémenter la génération des clichés dans un système dédié. En effet, c’est une optimisation qui n’est en rien la responsabilité de la couche d’affaire. Le processus créant les clichés a simplement besoin de faire une requête sur la table des clichés pour tous les agrégats qui n’ont pas eu de cliché depuis un certain nombre de commits. Il suffit alors de charger les dits agrégats, puis de leur demander un cliché qui servira en retour à mettre à jour la même table.

Versionnement

Qu’arrive-t-il lorsque vous avez besoin de modifier les informations portées par un type d’événement suite à l’évolution de votre modèle d’affaire?

Vous pouvez tout simplement créer une nouvelle version de cet événement (par exemple: CustomerAddressCorrected_v2) et modifier l’agrégat pour ne consommer que ce dernier événement. Il n’est pas nécessaire de mettre à jour tous les événements dans votre dépôt d’événements. Par contre, vous pouvez insérer un convertisseur aprés la requête au dépôt de façon à ce que les agrégats ne recoivent que les dernière versions des types d’événements dans leur constructeur.
Comment convertir un événement d’un ancien type vers sa nouvelle version? Prenons l’exemple d’un champ qui aurait été ajouté. Vous allez devoir prendre la même décision que lorsque vous rajoutez une colonne dans une table de base de données. Il va vous falloir trouver une valeur par défaut.
Bien sûr, il va y avoir des situations ou la conversion sera impossible. Selon Greg Young, ce genre de cas indique souvent que le nouveau type d’événement n’est pas une nouvelle version de cet événement mais bien un nouveau type d’événement. Dans ce cas, la conversion n’a pas de sens.

Rinat Abdulin couvre (en anglais) quelques exemples et donne plus de détails dans sa page sur le versionnement.

Modèle d’affichage / vues

Puisque nous ne sommes plus contraints par le modèle transactionnel pour nos vues, nous avons la liberté d’utiliser des systèmes plus efficaces. En particulier, pourquoi ne pas stocker directement une version sérialisée de la vue dans la base de données? Bien sûr, on pourrait utiliser une colonne de type blob pour ce faire, ainsi que quelque colonnes servant à indexer chaque ligne pour les recherches. Mais on peut aussi utiliser une base de donnée documentaire qui existe précisément pour ça. En .NET, je vous recommanderais RavenDb. Sur les autres plate-formes, il y a beaucoup de choix, comme par exemple MongoDb. Quelque unes de ces solution produisent même directement du JSON, que vous pouvez donc transférer, sans traitement de votre part, à votre application AJAX.

Pour ce sujet encore, Rinat Abdulin a publié une lecture intéressante.

Fusion intelligente de commits

Jusqu’à présent, notre gestion de concurrence fut assez faible: nous réessayons une commande lorsque nous détectons un conflit. C’est suffisant pour la majorité des applications. Mais si nécessaire, nous pouvons tirer partie du fait que les événements renferment énormément d’informations contextuelles sur ce qui s’est passé. Lorsque nous rencontrons un conflit, nous pouvons charger les événements commités dans intervalle du traitement de la commande en court, et les examiner. Nous pouvons avoir des règles qui pour chaque paire d’événements commité / à commiter indique s’il y a un réel conflit. Le plus souvent, il n’y a pas de conflit si les événements sont de type différent. Ce peut être la règle par défaut. Mais des règles plus fines peuvent être appliquées au cas par cas. Par exemple, si 2 changements de nom changent pour le même nom, alors il n’y a pas de conflit.

Cette stratégie de fusion est particulièrement utile dans les applications fortement collaboratives. Mais elle ouvre également d’autre possibilités dans le cas des applications déconnectées. Certains clients d’applications ne peuvent être connectés à leur serveur. C’est le cas par exemple dans les domaines de la prospection minière, ou encore pour la police. Dans ces cas, le client peut embarquer une copie du dépôt d’événement et fonctionner à partir de cette copie. Lorsque le client se reconnecte au serveur, tous les commits sont fusionnés. Seuls les événements rentrant en conflit devront être fusionnés à la main par l’utilisateur. C’est d’ailleurs la technique utilisée par la plupart des systèmes relationnels de base de données à travers leur journal des transactions.

Intégration

C’est un sujet énorme. L’intégration de systèmes intra- et inter- entreprise est habituellement un casse tête qui réclame beaucoup d’argent. Il y a beaucoup de facteurs en jeux. L’un d’eux est le fait que les applications ne sont pas toujours conçues pour être intégrées. Heureusement, cela change avec la popularisation de SOA. Un autre problème est le fait que la plupart des technologies d’intégration sur le marché sont synchrone: REST, Web Services, CORBA, RMI, WCF, etc… C’est un problème car les communications synchrones ne peuvent évoluer en charge. Imaginez un service qui a besoin d’appeler un service qui a besoin d’appeler un 3e service, qui… Le délai des appels réseau augmente rapidement. Pire, plus vous ajoutez de composants à la chaîne, plus les risques de rupture d’une transaction augmentent. Et je ne parles même pas du cauchemar que représente la maintenance d’une telle toile d’araignée.

Event sourcing offre une approche propre à de nombreux de ces problèmes. Combiné avec de la publication /abonnement, les applications deviennent déconnectées dans le temps, et découplées dans leur responsabilités. Si d’autres applications souhaitent s’intégrer à la votre, elles ont simplement besoin de s’abonner à votre flux d’événements. Vous pouvez pour ce faire publier votre flux dans un bus de service qui va router vos événements automatiquement à tous les abonnés. Votre application n’a donc même plus besoin de savoir qui est abonné. Et de nouvelles applications peuvent s’abonner sans même que vous ayez besoin de le savoir. Ces applications peuvent alors extraire toute l’information dont elle ont besoin à partir du flux de vos événements.

Cela résous le problème de la faillibilité d’un composant de la chaîne. En effet, ce genre d’intégration oblige les applications à supporter des workflows asynchrone. Si une application tombe, les messages vont simplement s’accumuler dans le bus de service jusqu’à ce qu’elle revienne en ligne et traite les messages. De même, si vous choisissez attentivement votre format de sérialisation, les problèmes de maintenabilité sont fortement réduit. En effet, votre application peut emmètre de nouveaux types de messages sans impacter les applications abonnées. Ces dernières seront responsable de mettre à jour leur logique d’intégration si elles souhaitent profiter des dernières versions des événements.

Cohérence retardée (eventual consistency)

On parle de cohérence retardée lorsqu’il y a un délai entre un commit d’un agrégat et la mise à jour des vues. Ou plus précisément, lorsque ces 2 processus utilisent des transactions différentes. Celà veut dire que vous ne commitez que les événements résultant d’une commande dans le dépôt d’événements. Un processus différent est responsable de propager ce nouveau commit aux gestionnaires de vues. Si vous avez déjà un bus de service, vous pouvez l’utiliser directement pour cette fonction. D’ailleurs, on pourrait quasiment considérer le modèle d’affichage comme un service distinct de la logique d’affaire, presque un “bounded context” différent.

Ceci dit, dans la plupart des cas, ce délai n’est pas nécessaire. Tant que vous n’en avez pas besoin, je vous conseillerais de l’éviter, et de continuer à mettre à jour vos vues en même temps que le dépôt d’événements.

Mais alors quand est ce qu’il est utile d’introduire ce délai? Nous verrons dans la section suivante qu’un cas d’utilisation est la montée en charge. Un autre cas est la performance. Une partie de vos vues peuvent être lourdes à mettre à jours, par exemple la mise à jours d’un cube OLAP pour vos rapports. Il peut alors être nécessaire d’isoler la mise à jours de cette partie dans un processus séparé, afin de préserver une bonne performance dans le traitement des commandes.

Monter en charge les lectures et rapports

Une fois que vous avez en place un système de cohérence retardée, il est aisé de faire évoluer l’aspect “lecture” de votre application. C’est une bonne nouvelle car la plupart des applications ont un volume de lectures beaucoup plus important que leur volume d’écritures. Tout ce que vous avez à faire est de créer plusieurs copies de votre modèle d’affichage. Vous pouvez même en avoir une copie sur chacune de vos machines web. Chaque copie serait bien évidemment abonnée au flux d’événements:

De cette manière, vous avez une évolutivité quasiment linéaire en ajoutant simplement des serveurs d’application. L’autre avantage de cette configuration est que maintenant votre modèle d’affichage est hébergé localement sur votre serveur d’application. Vous n’avez plus besoin d’un appel réseau pour retourner votre page.

Si vous voulez évoluer à de très large volumes, votre problème est réduit à faire évoluer votre service de publication / abonnement, ce qui est en général un problème plus simple.

Monter en charge les écritures

La première étape consiste à rendre le traitement des commandes asynchrones. Au lieu de traiter immédiatement la commande, le serveur d’application l’envoie simplement dans une queue. Bien entendu, cela signifie que l’expérience usager sera modifiée. Prenez Amazon par exemple: lorsque vous soumettez une commande, la page suivante ne vous indique pas si la commande fut un succès ou non. A la place, vous avez un message du style “Votre commande à été prise en compte. Vous recevrez un courriel de confirmation sous peu.”.

Ceci dit, si vous avez vraiment besoin, il existe des techniques pour recevoir le résultat de l’exécution d’une commande de façon asynchrone.

Plusieurs serveurs de logique d’affaire peuvent alors concourir pour vider la queue des commandes. Mais cela ne permet que de faire évoluer le traitement de la logique d’affaire. Le dépôt d’événements devient alors le goulot d’étranglement. Heureusement, de par leur nature, les événements et les commandes peuvent être facilement partitionnés (“sharding”) selon l’identifiant de l’agrégat. Vous pouvez alors router les commandes au serveur de logique d’affaire concerné (grâce au fait que les agrégats soient un “consistency boundary”):

Liste des billets “migrer vers le Event Sourcing”:

From → Event Sourcing

3 Comments
  1. Yan permalink

    Vraiment très intéressant cette série de billets!

  2. Tomas Jansson permalink

    Great series of post, but I think you sort of missing one part. Transitioning to event sourcing is not that hard if you don’t consider the existing data. What would have changed if the existing database was a data with lots of data? How would that data be considered when transitioning to event sourcing since that data is the base of many of the aggregates. Do you keep the old database as a baseline for the event store? Or do you have a migration phase where you migrate the existing database creating events from it and inject in the existing database, not that those events are probably events that will only be used in this scenario.

  3. Julien permalink

    Hi Tomas,

    Thank you for your comment. This is a good point. You will need some data migration at deployment time. And nothing magical here, this will require effort, but not more than other kinds of data migrations. It however do not need to happen all at once for the entire database. You can migrate one kind of aggregate at a time.You will need to generate events for this kind of aggregate. It is easier if you can have a down time so you can migrate offline, but there are other ways. For example, you can deploy a version of the application that will generate events on the fly from the legacy DB each time it is accessing an aggregate instance for the first time.
    As for the offline migration scenario, generating events is usually quite fast. On a single event store machine, I used to see 3000 event commits written per second with naive implementations, but some products are claiming to be faster, for example Greg Young’s Event Store. Since you will want to group events for an aggregate instance in a single commit, you can migrate approximately 10M aggregates per hour on a single machine with the naive implementation.
    There are a lot of situations where migrating to event sourcing might be an option worth considering, given its tremendous benefits. But of course, careful planing and cost analysis should always be performed upfront.

Leave a Reply

Note: XHTML is allowed. Your email address will never be published.

Subscribe to this comment feed via RSS