Contexte:
Je construis une application et l’architecture proposée est basée sur les événements/messages sur une architecture de microservices.
La façon monolithique de faire est d’avoir un User/HTTP request
et d’agir sur certaines commandes qui ont un synchronous response
direct. Ainsi, répondre à la même requête utilisateur/HTTP est «sans tracas».
Le problème:
L'utilisateur envoie un HTTP request
au service UI (il existe plusieurs services d'interface utilisateur) qui déclenche certains événements dans une file d'attente (Kafka/RabbitMQ/any). un n de services capte cet événement/message, effectue un peu de magie en cours de route et puis, à un moment donné, le même service d'interface utilisateur devrait la récupérer et la restituer à l'utilisateur à l'origine de la requête HTTP. Le traitement de la demande est ASYNC
mais le User/HTTP REQUEST->RESPONSE
est SYNC
conformément à votre interaction HTTP typique.
Question: Comment puis-je envoyer une réponse au même service d'interface utilisateur à l'origine de l'action (le service qui interagit avec l'utilisateur via HTTP) dans ce monde régi par les agnostiques/événements?
Mes recherches jusqu'à présent J'ai regardé autour de moi et il semble que certaines personnes résolvent ce problème en utilisant WebSockets.
Mais la couche de complexité est qu’il faut un tableau qui mappe (RequestId->Websocket(Client-Server))
qui est utilisé pour "découvrir" quel nœud de la passerelle dispose de la connexion websocket pour une réponse particulière. Mais même si je comprends le problème et la complexité, je suis coincé et je ne trouve aucun article qui me donnerait des informations sur la façon de résoudre ce problème au niveau de la couche d’implémentation. ET ceci n'est toujours pas une option viable en raison des intégrations tierces telles que les fournisseurs de paiement (WorldPay) qui attendent REQUEST->RESPONSE
- spécialement lors de la validation 3DS.
Je suis donc quelque peu réticent à penser que WebSockets est une option. Mais même si les WebSockets sont acceptables pour les applications Webfacing, une API qui se connecte à des systèmes externes n’est pas une excellente architecture.
** ** ** Mettre à jour: ** ** **
Même si l'interrogation longue est une solution possible pour une API WebService avec un 202 Accepted
un Location header
et un retry-after header
, il ne serait pas performant pour un site Web à haute simultanéité et haute capacité. Imaginez qu’un très grand nombre de personnes essaient d’obtenir la mise à jour de l’état de la transaction à CHAQUE demande et qu’il faut invalider le cache CDN (allez jouer à ce problème maintenant! Ha).
Mais le plus important et pertinent en ce qui concerne mon cas est celui des API tierces, telles que les systèmes de paiement où les systèmes 3DS ont des redirections automatiques gérées par le système du fournisseur de paiement et s’attendent à un REQUEST/RESPONSE flow
typique; le modèle de sockets fonctionnerait.
En raison de ce cas d'utilisation, le HTTP REQUEST/RESPONSE
devrait être traité de la manière habituelle, où j'ai un client stupide qui s'attend à ce que la complexité de la précession soit gérée en back-end.
Je recherche donc une solution où, à l’extérieur, j’ai un Request->Response
(SYNC) typique et où la complexité de l’état (ASYNCrony du système) est gérée en interne
Un exemple de longue interrogation, mais ce modèle ne fonctionnerait pas pour les API tierces telles que le fournisseur de paiements sur 3DS Redirects
qui ne sont pas sous mon contrôle.
POST /user
Payload {userdata}
RETURNs:
HTTP/1.1 202 Accepted
Content-Type: application/json; charset=utf-8
Date: Mon, 27 Nov 2018 17:25:55 GMT
Location: https://mydomain/user/transaction/status/:transaction_id
Retry-After: 10
GET
https://mydomain/user/transaction/status/:transaction_id
D'un point de vue plus général, à la réception de la demande, vous pouvez inscrire un abonné dans la file d'attente dans le contexte de la demande en cours (c'est-à-dire lorsque l'objet de demande est dans l'étendue), lequel reçoit un accusé de réception des services responsables à la fin de leur travail qui maintient la progression du nombre total d’opérations). Lorsque l'état final est atteint, il renvoie la réponse et supprime le programme d'écoute. Je pense que cela fonctionnera dans n'importe quelle file de messages de style pub/sub. Voici une démo trop simplifiée de ce que je propose.
// a stub for any message queue using the pub sub pattern
let Q = {
pub: (event, data) => {},
sub: (event, handler) => {}
}
// typical express request handler
let controller = async (req, res) => {
// initiate saga
let sagaId = uuid()
Q.pub("saga:register-user", {
username: req.body.username,
password: req.body.password,
promoCode: req.body.promoCode,
sagaId: sagaId
})
// wait for user to be added
let p1 = new Promise((resolve, reject) => {
Q.sub("user-added", ack => {
resolve(ack)
})
})
// wait for promo code to be applied
let p2 = new Promise((resolve, reject) => {
Q.sub("promo-applied", ack => {
resolve(ack)
})
})
// wait for both promises to finish successfully
try {
var sagaComplete = await Promise.all([p1, p2])
// respond with some transformation of data
res.json({success: true, data: sagaComplete})
} catch (e) {
logger.error('saga failed due to reasons')
// rollback asynchronously
Q.pub('rollback:user-added', {sagaId: sagaId})
Q.pub('rollback:promo-applied', {sagaId: sagaId})
// respond with appropriate status
res.status(500).json({message: 'could not complete saga. Rolling back side effects'})
}
}
Comme vous pouvez probablement le constater, cela ressemble à un schéma général qui peut être résumé dans un cadre permettant de réduire la duplication de code et de gérer les problèmes transversaux. C’est l’essentiel du motif saga . Le client n'attendra que le temps nécessaire pour terminer les opérations requises (ce qui se produirait même si tout était synchrone), plus le temps de latence supplémentaire dû à la communication interservices. Assurez-vous de ne pas bloquer le thread si vous utilisez un système basé sur une boucle d'événement, tel que NodeJS ou Python Tornado.
Le simple fait d'utiliser un mécanisme Push basé sur un socket Web n'améliore pas nécessairement l'efficacité ou les performances de votre système. Cependant, il est recommandé d’envoyer des messages au client à l’aide d’une connexion socket, car cela rendra votre architecture plus générale (même vos clients se comporteront comme vos services), cohérente et permettra une meilleure séparation des problèmes. Cela vous permettra également d'adapter indépendamment le service Push sans vous soucier de la logique métier. Le modèle de saga peut être développé pour permettre les restaurations en cas de pannes partielles ou de délais, ce qui rend votre système plus facile à gérer.
Comme je m'y attendais, les gens essaient de tout intégrer dans un concept même s'il ne correspond pas à cela. Ce n’est pas une critique, c’est une observation de mon expérience et après avoir lu votre question et d’autres réponses.
Oui, vous avez raison de dire que l'architecture de microservices repose sur des modèles de messagerie asynchrones. Cependant, lorsque nous parlons d’assurance-chômage, il ya 2 cas possibles dans mon esprit:
L’UI a besoin d’une réponse immédiate (par exemple, les opérations de lecture ou les commandes sur lesquelles l’utilisateur attend une réponse immédiate). Celles-ci ne doivent pas nécessairement être asynchrones . Pourquoi voudriez-vous ajouter une surcharge de messagerie et d'asynchronisme si la réponse est requise immédiatement à l'écran? N'a pas de sens. L'architecture de microservice est censée résoudre les problèmes plutôt que d'en créer de nouveaux en ajoutant un temps système.
L'interface utilisateur peut être restructurée pour tolérer une réponse retardée (par exemple, au lieu d'attendre le résultat, elle peut simplement envoyer une commande, recevoir un accusé de réception et laisser l'utilisateur faire autre chose pendant la préparation de la réponse). Dans ce cas, vous pouvez introduire l'asynchronie. Le service gateway (avec lequel l'interface utilisateur interagit directement) peut orchestrer le traitement asynchrone (attend les événements complets, etc.) et, lorsqu'il est prêt, il peut communiquer avec l'interface utilisateur. J'ai vu l'interface utilisateur utiliser SignalR dans de tels cas, et le service de passerelle était une API qui acceptait les connexions socket. Si le navigateur ne supporte pas les sockets, il devrait idéalement revenir à la scrutation. Quoi qu'il en soit, le point important est que cela ne peut fonctionner qu'avec une contingence: l'interface utilisateur peut tolérer des réponses différées .
Si les microservices sont effectivement pertinents dans votre situation (cas 2), structurez le flux de l'interface utilisateur en conséquence, et les microservices sur le back-end ne devraient pas poser de problème. Dans ce cas, votre question se résume à l'application d'une architecture pilotée par les événements à l'ensemble des services (Edge étant le microservice de passerelle qui connecte les interactions pilotées par les événements et l'interface utilisateur). Ce problème (services événementiels) peut être résolu et vous le savez. Vous devez simplement décider si vous pouvez repenser le fonctionnement de votre interface utilisateur.
Vous trouverez ci-dessous un exemple très simple de la manière dont vous pouvez implémenter le UI Service afin qu’il fonctionne avec un flux de requête/réponse HTTP normal. Il utilise la classe node.js events.EventEmitter
pour "acheminer" les réponses au bon gestionnaire HTTP.
Aperçu de la mise en œuvre:
Connectez le producteur/consommateur à Kafka
Créer un répartiteur global d'événements à partir de la classe EventEmitter
Notez que j'ai essayé de garder le code aussi petit que possible, en laissant de côté les erreurs, le temps imparti, etc.
Notez également que kafkaProduceTopic
et kafkaConsumTopic
sont les mêmes sujets pour simplifier les tests. Aucun autre service/fonction n'est nécessaire pour produire dans le sujet UI Service consume.
Le code suppose que les packages kafka-node
et uuid
ont été installés à npm
et que Kafka est accessible sur localhost:9092
.
const http = require('http');
const EventEmitter = require('events');
const kafka = require('kafka-node');
const uuidv4 = require('uuid/v4');
const kafkaProduceTopic = "req-res-topic";
const kafkaConsumeTopic = "req-res-topic";
class ResponseEventEmitter extends EventEmitter {}
const responseEventEmitter = new ResponseEventEmitter();
var HighLevelProducer = kafka.HighLevelProducer,
client = new kafka.Client(),
producer = new HighLevelProducer(client);
var HighLevelConsumer = kafka.HighLevelConsumer,
client = new kafka.Client(),
consumer = new HighLevelConsumer(
client,
[
{ topic: kafkaConsumeTopic }
],
{
groupId: 'my-group'
}
);
var s = http.createServer(function (req, res) {
// Generate a random UUID to be used as the request id that
// that is used to correlated request/response requests.
// The internal micro-services need to include this id in
// the "final" message that is pushed to Kafka and consumed
// by the ui service
var id = uuidv4();
// Send the request data to the internal back-end through Kafka
// In real code the Kafka message would be a JSON/protobuf/...
// message, but it needs to include the UUID generated by this
// function
payloads = [
{ topic: kafkaProduceTopic, messages: id},
];
producer.send(payloads, function (err, data) {
if(err != null) {
console.log("Error: ", err);
return;
}
});
responseEventEmitter.once(id, () => {
console.log("Got the response event for ", id);
res.write("Order " + id + " has been processed\n");
res.end();
})
});
s.timeout = 10000;
s.listen(8080);
// Listen to the Kafka topic that streams messages
// indicating that the request has been processed and
// emit an event to the request handler so it can finish.
// In this example the consumed Kafka message is simply
// the UUID of the request that has been processed (which
// is also the event name that the response handler is
// listening to).
//
// In real code the Kafka message would be a JSON/protobuf/... message
// which needs to contain the UUID the request handler generated.
// This Kafka consumer would then have to deserialize the incoming
// message and get the UUID from it.
consumer.on('message', function (message) {
responseEventEmitter.emit(message.value);
});
Malheureusement, je pense que vous devrez probablement utiliser de longues sondages ou des prises Web pour accomplir quelque chose comme ça. Vous devez "envoyer" quelque chose à l'utilisateur ou laisser la demande http ouverte jusqu'à ce que quelque chose revienne.
Pour traiter la restitution des données à l'utilisateur réel, vous pouvez utiliser quelque chose comme socket.io . Quand un utilisateur se connecte, socket.io crée un identifiant. Chaque fois qu'un utilisateur se connecte, vous mappez l'ID utilisateur sur l'id socket.io vous donne la valeur ..___. Une fois que chaque demande est associée à un ID utilisateur, vous pouvez renvoyer le résultat au bon client. Le flux serait quelque chose comme ça:
commande de requêtes Web (POST avec données et userId)
l'interface utilisateur place l'ordre en file d'attente (cette commande devrait avoir userId)
x nombre de services travaillant sur commande (en passant l'ID utilisateur à chaque fois)
service ui consomme de sujet. À un moment donné, des données apparaissent sur le sujet. Les données qu’il consomme ont l’utilisateur userId, le service ui examine la carte pour déterminer le socket à émettre.
Quel que soit le code en cours d'exécution sur votre interface utilisateur, il doit également être piloté par les événements, de sorte qu'il traitera un transfert de données sans le contexte de la requête d'origine. Vous pouvez utiliser quelque chose comme redux pour cela. Essentiellement, le serveur créerait des actions redux sur le client, cela fonctionne plutôt bien!
J'espère que cela t'aides.