70 votes

Comment gérer les requêtes HTTP dans une architecture microservice/événementielle ?

Le contexte :

Je suis en train de construire une application et l'architecture proposée est pilotée par les événements et les messages sur une architecture de microservices.

La façon monolithique de faire les choses est que j'ai une User/HTTP request et que les actions certaines commandes qui ont un direct synchronous response . Ainsi, répondre à la même demande utilisateur/HTTP est "sans souci".

enter image description here

Le problème :

L'utilisateur envoie un HTTP request au Service UI (il existe plusieurs services d'interface utilisateur) qui envoie certains événements à une file d'attente (Kafka/RabbitMQ/toutes sortes). Un certain nombre de services récupèrent cet événement/message, font un peu de magie en cours de route et puis, à un moment donné, ce même service d'interface utilisateur devrait récupérer cette réponse et la renvoyer à l'utilisateur à l'origine de la demande HTTP. Le traitement des demandes est ASYNC mais le User/HTTP REQUEST->RESPONSE es SYNC comme pour une interaction HTTP typique.

Question : Comment puis-je envoyer une réponse au même service d'interface utilisateur que celui qui est à l'origine de l'action (le service qui interagit avec l'utilisateur via HTTP) dans ce monde agnostique/conduit par les événements ?

Mes recherches jusqu'à présent J'ai fait des recherches et il semble que certaines personnes résolvent ce problème en utilisant les WebSockets.

Mais la couche de complexité est qu'il doit y avoir une table qui met en correspondance (RequestId->Websocket(Client-Server)) qui est utilisé pour "découvrir" quel nœud de la passerelle possède la connexion websocket pour une réponse particulière. Mais même si je comprends le problème et sa complexité, je suis bloqué par le fait que 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 de mise en œuvre. ET ce n'est toujours pas une option viable en raison des intégrations tierces telles que les fournisseurs de paiements (WorldPay) qui attendent REQUEST->RESPONSE - surtout sur la validation de la 3DS.

Je suis donc quelque peu réticent à l'idée que les WebSockets soient une option. Mais même si les WebSockets sont acceptables pour les applications Webfacing, pour les API qui se connectent à des systèmes externes, ce n'est pas une bonne architecture.

** ** ** Mise à jour : ** ** **

Même si le polling long est une solution possible pour une API de service Web avec une 202 Accepted a Location header et un retry-after header il ne serait pas performant pour un site web à haute concurrence et à haute capacité. Imaginez qu'un grand nombre de personnes essaient d'obtenir la mise à jour de l'état de la transaction à CHAQUE demande qu'elles font et que vous devez invalider le cache du CDN (allez jouer avec ce problème maintenant ! ha).

Mais le plus important et le plus pertinent dans mon cas, c'est que j'ai des API tierces, comme les systèmes de paiement, où les systèmes 3DS ont des redirections automatiques qui sont gérées par le système du fournisseur de paiement et qui s'attendent à une réponse typique. REQUEST/RESPONSE flow Ce modèle ne me conviendrait donc pas, pas plus que le modèle des prises de courant.

En raison de ce cas d'utilisation, l HTTP REQUEST/RESPONSE devrait être traité de la manière typique où j'ai un client muet qui s'attend à ce que la complexité du prétraitement soit gérée en back-end.

Je cherche donc une solution où, à l'extérieur, je dispose d'un système typique de contrôle de la qualité. Request->Response (SYNC) et la complexité de l'état (ASYNCronie du système) est gérée en interne.

Un exemple d'interrogation longue, mais ce modèle ne fonctionnerait pas pour les API tierces comme 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

enter image description here

8 votes

Si vous ne souhaitez pas mettre en place une communication bidirectionnelle avec le client, renvoyez l'adresse suivante 202 Accepté avec un En-tête de localisation qui indique au client où il peut s'adresser pour savoir quand le traitement est terminé. Il s'agit d'un modèle courant pour gérer les requêtes HTTP de longue durée auxquelles vous ne pouvez pas répondre immédiatement.

2 votes

Moi aussi, je me suis posé des questions et j'ai cherché une telle solution après avoir lu l'article du blog de Confluent sur Kafka ici. confluent.io/blog/build-services-backbone-events

0 votes

Jonathan : Qu'avez-vous découvert ?

19voto

Tengiz Points 2800

Comme je m'y attendais, les gens essaient de tout faire entrer dans un concept, même si cela n'y correspond pas. Il ne s'agit pas d'une critique, mais d'une observation tirée de mon expérience et de la lecture de votre question et des autres réponses.

Oui, vous avez raison de dire que l'architecture des microservices est basée sur des modèles de messagerie asynchrone. Cependant, lorsque nous parlons d'interface utilisateur, il y a deux cas possibles à mon avis :

  1. L'interface utilisateur a besoin d'une réponse immédiate (par exemple, les opérations de lecture ou les commandes pour lesquelles l'utilisateur attend une réponse immédiate). Ils ne doivent pas nécessairement être asynchrones. . Pourquoi ajouter des frais généraux de messagerie et d'asynchronie si la réponse doit être affichée immédiatement à l'écran ? Cela n'a pas de sens. L'architecture microservice est censée résoudre les problèmes plutôt que d'en créer de nouveaux en ajoutant une surcharge.

  2. L'interface utilisateur peut être restructurée pour tolérer une réponse différée (par exemple, au lieu d'attendre le résultat, l'interface utilisateur peut simplement soumettre 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 site passerelle (avec lequel l'interface utilisateur interagit directement) peut orchestrer le traitement asynchrone (il attend des événements complets, etc.) et, une fois 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 par socket. Si le navigateur ne prend pas en charge les sockets, il devrait idéalement se rabattre sur l'interrogation. Quoi qu'il en soit, le point important est que cela ne peut fonctionner qu'en cas d'imprévu : L'IU peut tolérer des réponses tardives .

Si les microservices sont effectivement pertinents dans votre situation (cas 2), alors structurez le flux de l'interface utilisateur en conséquence, et il ne devrait pas y avoir de problème de microservices en back-end. Dans ce cas, votre question revient à appliquer l'architecture événementielle à l'ensemble des services (edge étant le microservice passerelle qui relie les interactions événementielles et l'interface utilisateur). Ce problème (services pilotés par les événements) est soluble et vous le savez. Vous devez simplement décider si vous pouvez repenser le fonctionnement de votre interface utilisateur.

4voto

Shasak Points 355

D'un point de vue plus général - à la réception de la demande, vous pouvez enregistrer un abonné sur la file d'attente dans le contexte de la demande actuelle (c'est-à-dire lorsque l'objet de la demande est dans la portée) qui reçoit un accusé de réception des services responsables lorsqu'ils terminent leur travail (comme une machine à état qui maintient la progression du nombre total d'opérations). Lorsque l'état de fin est atteint, il renvoie la réponse et supprime l'écouteur. Je pense que cela fonctionnera dans n'importe quelle file de messages de style pub/sub. Voici une démonstration très simplifiée de ce que je suggère.

// 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 le constater, il s'agit d'un modèle général qui peut être abstrait dans un cadre permettant de réduire la duplication du code et de gérer les problèmes transversaux. C'est ce que le modèle de saga est essentiellement un sujet. 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 la latence supplémentaire due à la communication entre les services. Veillez à ne pas bloquer le thread si vous utilisez un système basé sur une boucle d'événements comme NodeJS ou Python Tornado.

La simple utilisation d'un mécanisme de poussée basé sur les sockets web n'améliore pas nécessairement l'efficacité ou les performances de votre système. Cependant, il est recommandé de pousser les messages vers le client en utilisant une connexion socket car cela rend votre architecture plus générale (même vos clients se comportent comme vos services), cohérente et permet une meilleure séparation des préoccupations. Cela vous permettra également de faire évoluer de manière indépendante le service de poussée sans vous soucier de la logique commerciale. Le modèle saga peut être étendu pour permettre des retours en arrière en cas d'échecs partiels ou de délais d'attente et rendre votre système plus facile à gérer.

3voto

johlo Points 4638

Vous trouverez ci-dessous un exemple très simple de la façon dont vous pourriez mettre en œuvre la méthode d'évaluation de l'impact sur l'environnement. Service UI pour qu'il fonctionne avec un flux normal de demande/réponse HTTP. Il utilise le module node.js events.EventEmitter pour "acheminer" les réponses vers le bon gestionnaire HTTP.

Schéma de la mise en œuvre :

  1. Connecter le client producteur/consommateur à Kafka

    1. Le producteur est utilisé pour envoyer les données de la demande aux micro-services internes.
    2. Le consommateur est utilisé pour écouter les données des microservices, ce qui signifie que la demande a été traitée et je suppose que ces éléments Kafka contiennent également les données qui doivent être renvoyées au client HTTP.
  2. Créer une distributeur d'événements de la EventEmitter classe

  3. Enregistrez un gestionnaire de requête HTTP qui

    1. Crée un UUID pour la demande et l'inclut dans les données utiles envoyées à Kafka.
    2. Enregistre un écouteur d'événements avec notre distributeur d'événements où l'UUID est utilisé comme nom d'événement qu'il écoute.
  4. Commencez à consommer le sujet Kafka et récupérez l'UUID que le gestionnaire de requête HTTP attend et émettez un événement pour cela. Dans le code de l'exemple, je n'inclus aucune charge utile dans l'événement émis, mais vous voudriez généralement inclure des données provenant des données Kafka en tant qu'argument afin que le gestionnaire HTTP puisse les renvoyer au client HTTP.

Notez que j'ai essayé de garder le code aussi petit que possible, en laissant de côté la gestion des erreurs et des délais d'attente, etc !

Notez également que kafkaProduceTopic y kafkaConsumTopic sont les mêmes sujets afin de simplifier les tests, pas besoin d'un autre service/fonction pour produire à la Service UI consommer le sujet.

Le code suppose que le kafka-node y uuid les paquets ont été npm installé 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);
});

0 votes

Je vous en remercie. Il y a plusieurs choses omises ici, l'une est que responseEventEmitter n'est pas à l'écoute de kafka . L'idée est que le HTTP REQ Le point d'entrée envoie et consomme à partir d'un gestionnaire d'événements par given ID y on event-chain-finish (cela pourrait être done o error ). Cet exemple pousse un événement vers kafka et quelques données mais il n'écoute pas (ou ne filtre pas) les mêmes données d'événement par ID et par changement de statut (un statut donné qui indique qu'il doit répondre à l'utilisateur). La question clé dans cette architecture est de retourner à l'utilisateur des données traitées par ID et EVENT_TYPE à partir d'un système distribué.

0 votes

@JonathanThurft Les dernières lignes du code relient le consommateur Kafka à l'émetteur responseEventEmitter. Pour garder le code simple, je suppose que les données consommées sont juste l'UUID que le gestionnaire de requête a généré et envoyé au back-end interne comme jeton de coordination. En réalité, il s'agirait bien sûr de données plus complexes dont il faudrait extraire le jeton. Les services internes doivent s'assurer d'inclure cet UUID/token dans toutes les requêtes internes afin qu'il puisse être inclus dans le message final envoyé au sujet Kafka à partir duquel le service utilisateur consomme.

1voto

jakhicks Points 336

Malheureusement, je pense que vous devrez probablement utiliser soit une interrogation longue soit des web-sockets pour accomplir quelque chose comme ça. Vous devez "pousser" quelque chose à l'utilisateur, ou garder la requête http ouverte jusqu'à ce que quelque chose revienne.

Pour gérer le retour des données à l'utilisateur réel, vous pouvez utiliser quelque chose comme socket.io . Lorsqu'un utilisateur se connecte, socket.io crée un identifiant. Chaque fois qu'un utilisateur se connecte, vous faites correspondre l'identifiant de l'utilisateur à l'identifiant que socket.io vous donne. Une fois qu'un identifiant est associé à chaque requête, vous pouvez émettre le résultat vers le bon client. Le flux serait quelque chose comme ça :

commande de demandes web (POST avec données et userId)

le service ui place une commande dans la file d'attente (cette commande doit avoir un userId)

x services travaillent sur la commande (en transmettant à chaque fois l'userId)

Le service ui consomme à partir du sujet. À un moment donné, des données apparaissent sur le sujet. Les données qu'il consomme ont l'userId, le service ui consulte la carte pour savoir vers quel socket émettre.

Quel que soit le code exécuté sur l'interface utilisateur, il devra également être événementiel, de sorte qu'il devra traiter une poussée de données sans le contexte de la demande initiale. Vous pourriez utiliser quelque chose comme redux pour cela. Essentiellement, vous avez le serveur qui crée des actions redux sur le client, cela fonctionne plutôt bien !

J'espère que cela vous aidera.

0voto

cherrysoft Points 762

Et si on utilisait Promesses ? Socket.io pourrait aussi être une solution si vous voulez du temps réel.

Jetez un coup d'œil à CQRS également. Ce modèle architectural s'adapte au modèle piloté par les événements et à l'architecture des microservices.

Encore mieux. Jetez un coup d'oeil à este .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X