Évoluer du monolithe vers les microservices n’est pas un long fleuve tranquille… Les challenges ne manquent pas. L’un d’entre eux, et non des moindres, est la cohérence des données. Un des principes de base est que chaque service possède sa propre base de données. Quand une transaction métier invoque plusieurs services, on ne peut donc plus compter sur les bonnes vieilles transactions ACID des serveurs SQL.
Résumé de la conférence donnée par Jean-François James lors du devfest 2022
Dans un modèle classique de monolithe où plusieurs modules partagent la même base de données, le modèle ACID permet de garder une cohérence dans les données avec une simple transaction.
Mais comment maintenir ce niveau de cohérence dans une architecture microservice où chaque application possède sa BDD et son propre jeu de données ?
Plusieurs solutions existent :
- Transaction ACID locale et création de traitements de corrections à passages réguliers
- Two-phase Commit qui maintient à la fois une transaction locale par application et une transaction globale pour gérer la coordination de toutes les applications. Ce système génère des transactions très longues et n’est supporté que par les BDD relationnelles
- Utilisation de LRA/SAGA pour gérer des flux de compensation
LRA/SAGA est un pattern d’architecture qui permet de gérer des évènements de compensation sur plusieurs applications. Lorsque l’une des transactions locales rencontre une erreur (technique ou métier) sur l’un des différents microservices, un flux de compensation est lancé pour annuler les traitements dans les autres microservices. La définition de l’action de compensation est à la charge du développeur (recréditation de compte, annulation de réservation, …)
Les principes de fonctionnement de ce pattern d’architecture reposent sur :
- Des transactions locales
- Une coordination globale légère, faiblement couplée
- Des opérations de compensation explicites (définies par le développeur)
- Perte de l’isolation globale : de ACID à ACD (pas besoin d’attendre la fin de toutes les transactions)
Plusieurs solutions implémentent ce pattern, notamment :
- Microprofile LRA
- Eventuate SAGA
Microprofile LRA
Microprofile est un ensemble de spécifications permettant d’améliorer l’expérience de développement en JEE. Microprofile LRA est la spécification de la méthode permettant de maintenir une cohérence des données en se basant sur LRA.
Origine | Oasis WS-Composite Application Framework (2006) |
Nature | Spécification (64 p) |
Version | 1.0, avril 2021 |
Implémentations | Quarkus, OpenLiberty, Helidon, Wildfly, Camel EIP |
Modèle de programmation | Annotations |
Echange | REST/HTTP Synchrone |
Infrastructure | LRA Coordinator |
Architecture
LRA nécessite le déploiement d’un LRA coordinator (Narayana), qui coordonne les transactions des différentes applications et déclenche les traitements de compensation.
Un en-tête HTTP lraid est ajouté aux requêtes afin de relier les requêtes sur lesquelles doivent être lancées les flux de compensation.
Implémentation
Le paramétrage de LRA et des méthodes de compensation/complétion se fait grâce à des annotations. La configuration du service Holiday peut être par exemple :
@LRA(value = LRA.Type.REQUIRED, end = true, timeLimit = 2, timeUnit = ChronoUnit.SECONDS,
cancelOn = {Response.Status.INTERNAL_SERVER_ERROR},
cancelOnFamily = { Response.Status.Family.CLIENT_ERROR })
@POST
@Path("/book")
public Response book(@Parameter(description = "LRA id (automatically provided)", hidden = true) @HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) {
// ...
}
@Compensate
@Path("/compensate")
@PUT
public Response compensate(@Parameter(description = "LRA id (automatically provided)", hidden = true) @HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) {
// ...
}
@Compensate
@Path("/complete")
@PUT
public Response complete(@Parameter(description = "LRA id (automatically provided)", hidden = true) @HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) {
// ...
}
La méthode compensate peut être appelée de manière concurrente à l’un des services distants (attention à gérer les cas de collisions dans le code). Il est alors recommandé de persister en base les lraids de transaction qui sont en erreur, afin de lancer les traitements de compensation si l’un des services distants a continué de s’exécuter et d’enregistrer des données après le lancement de compensate.
Si nous reprenons l’exemple, la configuration du service Trip appelé par le service Holiday est la suivante :
@LRA(value = LRA.Type.SUPPORTS, end = false)
@POST
@Path("/book")
public Response book(@Parameter(description = "LRA id (automatically provided)", hidden = true) @HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) {
// ...
}
@Compensate
@Path("/compensate")
@PUT
public Response compensate(@Parameter(description = "LRA id (automatically provided)", hidden = true) @HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) {
// ...
}
@Compensate
@Path("/complete")
@PUT
public Response complete(@Parameter(description = "LRA id (automatically provided)", hidden = true) @HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId) {
// ...
}
SAGA Eventuate
SAGA Eventuate est un framework permettant la coordination de données entre des microservices JAVA utilisant JDBC/JPA.
Origine | SAGA: 1987 Eventuate: 2047 |
Nature | Plateforme Microservices Open SOurce |
Version | 0.30.0 (Quarkus), avril 2021 |
Implémentations | Spring, Micronaut, Quarkus |
Modèle de programmation | Orchestration (DSL) ou Chorégraphie (domain events) |
Echange | Messaging Asynchrone |
Infrastructure | CDC, Kafka, PostgreSQL |
Architecture
Eventuate nécessite un comportement asynchrone, d’où l’utilisation de Kafka dans l’exemple. Les CDC sont les éléments qui permettent de gérer le déclenchement d’une compensation. Les CDC étant développés à partir de Spring Boot, il est possible d’accéder aux metrics de ces briques avec actuator.
Implémentation
La configuration des services avec Eventuate se fait fonctionnellement via la définition d’étapes à suivre. Si on reprend l’exemple de Holiday, les étapes à suivre par le service seraient :
private SagaDefinition<HolidayBookSagaData> sagaDefinition = step()
.invokeLocal(this::create)
.withCompensation(this::reject)
.step()
.invokeLocal(this::checkCustomer)
.step()
.invokeParticipant(this::bookTrip)
.onReply(TripBooked.class, this::handleTripBooked)
.onReply(BookTripFailed.class, this::handleBookTripFailed)
.withCompensation(this::cancelTrip)
.step()
.invokeLocal(this::checkPricing)
.step()
.invokeParticipant(this::confirmTrip)
.step()
.invokeLocal(this::approve)
.build();
On peut lire de haut en bas le déroulement normal des actions à effectuer et de bas en haut l’ordre des actions pour annuler le traitement.
Dans le code métier des fonctions, l’appel aux méthodes withFailure et withSuccess permet de retourner le résultat et donc le déclenchement ou non d’une compensation.
Les différentes SAGA sont persistées dans des tables dédiées à Eventuate.
Le CDC scrute alors les logs de transactions locales du publisher (qui envoie le résultat de sa fonction) enregistrées en base et les envoie au consumer (qui peut lancer le traitement de compensation).
Conclusion
Pour résumer, Microprofile et Eventuate sont équivalents en terme de nombre de lignes de code. Cependant, on peut noter que Eventuate demande beaucoup de configurations à plusieurs endroits contrairement à Microprofile, qui nécessite principalement des annotations. Cependant, le LRA coordinator (Narayana) de Microprofile demande plus de surveillance en production, car s’il tombe, la coordination n’est plus maintenue.
Sources
La démonstration des deux systèmes sur Github :
Documentation LRA :
- Exemples sur quarkus https://quarkus.io/blog/using-lra/
- Blog Narayana https://jbossts.blogspot.com/
Documentation Eventuate :
- https://eventuate.io/
- Livre Microservices Pattern de Chris Richardson
Conférence de Jean-François James à la Devoxx :
Direction Technique
Stéphane YVON
Proxiad NORD