web-dev-qa-db-ja.com

マイクロサービス/イベント駆動型アーキテクチャでHTTPリクエストを処理する方法は?

背景:

私はアプリケーションを構築しており、提案されているアーキテクチャはイベント/メッセージ駆動型のマイクロサービスアーキテクチャです。

モノリシックな方法は、私がUser/HTTP requestを持ち、直接synchronous responseを持つコマンドを実行することです。したがって、同じUser/HTTPリクエストに応答するのは「面倒」です。

enter image description here

問題:

ユーザーはHTTP requestUIサービス(複数のUIサービスがあります)に送信し、キュー(Kafka/RabbitMQ /どれか)。サービスのNは、イベント/メッセージが途中で何らかの魔法を行うことをピックアップし、その後、ある時点で同じUIサービスが応答をピックアップし、HTTPを発信したユーザーにそれを返す必要がありますrequest。要求処理はASYNCですが、User/HTTP REQUEST->RESPONSESYNCであり、通常のHTTPインタラクションに従っています。

質問:このAgnostic /でアクション(HTTPを介してユーザーと対話するサービス)を開始したのと同じUIサービスに応答を送信する方法イベント駆動型の世界?

これまでの私の研究私は周りを見回しており、WebSocketsを使用してその問題を解決している人がいるようです。

ただし、複雑なのは、特定の応答に対してゲートウェイのどのノードがWebSocket接続を持っているかを「検出」するために使用される(RequestId->Websocket(Client-Server))をマッピングするテーブルが必要なことです。しかし、問題と複雑さを理解していても、実装層でこの問題を解決する方法に関する情報を提供する記事を見つけることができないということで立ち往生しています。 [〜#〜] and [〜#〜]これは、支払いプロバイダー(WorldPay)などのサードパーティの統合が期待されるため、まだ実行可能なオプションではありませんREQUEST->RESPONSE-特に3DS検証について。

そのため、WebSocketsがオプションであると考えるのはどうやら嫌がります。しかし、WebSocketsがWebfacingアプリにとっては問題ないとしても、外部システムに接続するAPIは優れたアーキテクチャではありません。

** ** **更新:** ** **

長いポーリングが202 Accepted a Location headerおよびretry-after headerを使用するWebService APIの可能なソリューションである場合でも、高い同時実行性と高能力のWebサイトではパフォーマンスが向上しません。膨大な数の人々が、リクエストのたびにトランザクションステータスの更新を取得しようとし、CDNキャッシュを無効にしなければならないことを想像してみてください(今すぐその問題を解決してください!ha)。

しかし、私の場合に最も重要で関連性のある3DSシステムには、支払いプロバイダーシステムによって処理される自動リダイレクトがあり、典型的なREQUEST/RESPONSE flowを期待する支払いシステムなどのサードパーティAPIがあり、このモデルは機能しません私にとってもソケットモデルも機能しません。

このユースケースのため、HTTP REQUEST/RESPONSEは、歳差運動の複雑さがバックエンドで処理されることを期待するダムクライアントがある典型的な方法で処理する必要があります。

だから、私は外部で典型的なRequest->Response(SYNC)を持ち、ステータスの複雑さ(システムの非同期)が内部的に処理されるソリューションを探しています

長いポーリングの例ですが、このモデルは、私の制御の範囲外にある3DS Redirectsの支払いプロバイダーなどのサードパーティAPIでは機能しません。

 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

27
Jonathan Thurft

私が期待していたように-人々は、たとえそこに収まらなくても、すべてを概念に適合させようとします。これは批判ではありません。これは私の経験からの観察であり、あなたの質問やその他の回答を読んだ後の観察です。

はい、マイクロサービスアーキテクチャは非同期メッセージングパターンに基づいていることは正しいです。しかし、UIについて話すとき、私の考えている2つのケースがあります。

  1. UIはすぐに応答する必要があります(たとえば、読み取り操作や、ユーザーがすぐに答えを期待するコマンド)。 これらは非同期である必要はありません。すぐに画面上で応答が必要な場合、メッセージングと非同期のオーバーヘッドを追加するのはなぜですか?意味を成さない。マイクロサービスアーキテクチャは、オーバーヘッドを追加して新しい問題を作成するのではなく、問題を解決することになっています。

  2. 遅延応答を許容するためにUIを再構築できます(たとえば、結果を待つ代わりに、UIはコマンドを送信し、確認を受け取り、応答の準備中にユーザーに何かをさせることができます)。この場合、非同期性を導入できます。 gatewayサービス(UIが直接対話する)は、非同期処理(完全なイベントの待機など)を調整でき、準備ができたらUIと通信できます。そのような場合にSignalRを使用するUIを見てきましたが、ゲートウェイサービスはソケット接続を受け入れるAPIでした。ブラウザがソケットをサポートしていない場合、理想的にはポーリングにフォールバックする必要があります。とにかく、重要な点は、これは偶発性でのみ機能することです:Iは遅延応答に耐えることができます

マイクロサービスが実際に状況に関連している場合(ケース2)、それに応じてUIフローを構成します。バックエンドのマイクロサービスに問題はありません。その場合、あなたの質問は、イベント駆動型アーキテクチャを一連のサービスに適用することです(エッジは、イベント駆動型とUIの相互作用を接続するゲートウェイマイクロサービスです)。この問題(イベント駆動型サービス)は解決可能であり、ご存知のとおりです。 UIの動作を再考できるかどうかを判断する必要があります。

3
Tengiz

より一般的な観点から-リクエストを受信すると、現在のリクエストのコンテキスト(リクエストオブジェクトがスコープ内にあることを意味します)でキューにサブスクライバーを登録できます。操作の総数の進捗を維持します)。終了状態に達すると、応答を返し、リスナーを削除します。これは、任意のpub/subスタイルのメッセージキューで機能すると思います。これは、私が提案しているものの非常に単純化されたデモです。

// 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'})
  }

}

おそらくおわかりのように、これはフレームワークに抽象化してコードの重複を減らし、横断的な懸念を管理できる一般的なパターンのように見えます。これが saga pattern の本質です。クライアントは、必要な操作(これがすべて同期的であっても何が起こるか)に加えて、サービス間通信に起因する遅延を追加するまで待機します。 NodeJSやPython Tornadoなどのイベントループベースのシステムを使用している場合は、スレッドをブロックしないでください。

Webソケットベースのプッシュメカニズムを使用するだけでは、必ずしもシステムの効率やパフォーマンスが向上するとは限りません。ただし、ソケット接続を使用してクライアントにメッセージをプッシュすることをお勧めします。これにより、アーキテクチャがより一般的になり(クライアントがサービスのように動作する場合でも)、一貫性があり、懸念事項をより適切に分離できます。また、ビジネスロジックを気にすることなく、プッシュサービスを個別にスケーリングできます。サガパターンを拡張して、部分的な障害やタイムアウトの場合のロールバックを有効にし、システムをより管理しやすくすることができます。

2
Shasak

以下は、I Serviceを実装して、通常のHTTP要求/応答フローで動作する方法の非常に基本的な例です。 node.jsを使用しますevents.EventEmitterクラスは、適切なHTTPハンドラーに応答を「ルーティング」します。

実装の概要:

  1. プロデューサー/コンシューマークライアントをKafkaに接続する

    1. プロデューサーは、リクエストデータを内部マイクロサービスに送信するために使用されます
    2. コンシューマーは、リクエストが処理されたことを意味するマイクロサービスからのデータをリッスンするために使用され、これらのKafka=アイテムにはHTTPクライアントに返されるデータも含まれていると思います。
  2. EventEmitterクラスからグローバルイベントディスパッチャーを作成します

  3. というHTTP要求ハンドラーを登録します
    1. 要求のUUIDを作成し、Kafkaにプッシュされたペイロードに含めます
    2. UUIDがリッスンするイベント名として使用されるイベントディスパッチャにイベントリスナを登録します
  4. Kafka=トピックの消費を開始し、HTTPリクエストハンドラが待機しているUUIDを取得し、そのイベントを発行します。サンプルコードでは、発行されたイベントにペイロードを含めませんが、通常、Kafkaデータの一部のデータを引数として含めることで、HTTPハンドラーがHTTPクライアントにデータを返すことができます。

エラーやタイムアウト処理などを省き、コードをできるだけ小さくしようとしたことに注意してください!

また、kafkaProduceTopickafkaConsumTopicはテストを簡素化するための同じトピックであり、Iサービス消費トピックに対して生成する別のサービス/関数は不要であることに注意してください。

コードはkafka-nodeおよびuuidパッケージはnpmがインストールされており、そのKafkaは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);
});
2
johlo

Promises の使用はどうですか? Socket.ioは、リアルタイムが必要な場合のソリューションにもなります。

[〜#〜] cqrs [〜#〜] もご覧ください。このアーキテクチャパターンは、イベント駆動型モデルとマイクロサービスアーキテクチャに適合します。

さらに良い。 this を読んでください。

0
cherrysoft

残念ながら、このようなことを達成するには、長いポーリングまたはWebソケットのいずれかを使用する必要があると思われます。ユーザーに何かを「プッシュ」するか、何かが戻ってくるまでhttpリクエストを開いたままにする必要があります。

実際のユーザーにデータを戻す処理には、 socket.io のようなものを使用できます。ユーザーが接続すると、socket.ioはIDを作成します。ユーザーが接続するたびに、ユーザーIDをsocket.ioが提供するIDにマップします。各リクエストにユーザーIDがアタッチされると、正しいクライアントに結果を返すことができます。フローは次のようになります。

web要求の順序(データとuserIdを含むPOST)

uIサービスはキューに注文を出します(この注文にはuserIdが必要です)

x個のサービスが注文どおりに機能する(毎回userIdを渡す)

uIサービスはトピックから消費します。ある時点で、トピックにデータが表示されます。消費するデータにはuserIdがあり、UIサービスはマップを検索して、どのソケットに送信するかを判断します。

UIで実行されているコードはすべてイベント駆動型である必要があるため、元のリクエストのコンテキストなしでデータのプッシュを処理します。これには redux のようなものを使用できます。基本的に、サーバーはクライアント上でreduxアクションを作成しますが、それは非常にうまく機能します!

お役に立てれば。

0
jakhicks