背景:
私はアプリケーションを構築しており、提案されているアーキテクチャはイベント/メッセージ駆動型のマイクロサービスアーキテクチャです。
モノリシックな方法は、私がUser/HTTP request
を持ち、直接synchronous response
を持つコマンドを実行することです。したがって、同じUser/HTTPリクエストに応答するのは「面倒」です。
問題:
ユーザーはHTTP request
をUIサービス(複数のUIサービスがあります)に送信し、キュー(Kafka/RabbitMQ /どれか)。サービスのNは、イベント/メッセージが途中で何らかの魔法を行うことをピックアップし、その後、ある時点で同じUIサービスが応答をピックアップし、HTTPを発信したユーザーにそれを返す必要がありますrequest。要求処理はASYNC
ですが、User/HTTP REQUEST->RESPONSE
はSYNC
であり、通常の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
私が期待していたように-人々は、たとえそこに収まらなくても、すべてを概念に適合させようとします。これは批判ではありません。これは私の経験からの観察であり、あなたの質問やその他の回答を読んだ後の観察です。
はい、マイクロサービスアーキテクチャは非同期メッセージングパターンに基づいていることは正しいです。しかし、UIについて話すとき、私の考えている2つのケースがあります。
UIはすぐに応答する必要があります(たとえば、読み取り操作や、ユーザーがすぐに答えを期待するコマンド)。 これらは非同期である必要はありません。すぐに画面上で応答が必要な場合、メッセージングと非同期のオーバーヘッドを追加するのはなぜですか?意味を成さない。マイクロサービスアーキテクチャは、オーバーヘッドを追加して新しい問題を作成するのではなく、問題を解決することになっています。
遅延応答を許容するためにUIを再構築できます(たとえば、結果を待つ代わりに、UIはコマンドを送信し、確認を受け取り、応答の準備中にユーザーに何かをさせることができます)。この場合、非同期性を導入できます。 gatewayサービス(UIが直接対話する)は、非同期処理(完全なイベントの待機など)を調整でき、準備ができたらUIと通信できます。そのような場合にSignalRを使用するUIを見てきましたが、ゲートウェイサービスはソケット接続を受け入れるAPIでした。ブラウザがソケットをサポートしていない場合、理想的にはポーリングにフォールバックする必要があります。とにかく、重要な点は、これは偶発性でのみ機能することです:Iは遅延応答に耐えることができます。
マイクロサービスが実際に状況に関連している場合(ケース2)、それに応じてUIフローを構成します。バックエンドのマイクロサービスに問題はありません。その場合、あなたの質問は、イベント駆動型アーキテクチャを一連のサービスに適用することです(エッジは、イベント駆動型とUIの相互作用を接続するゲートウェイマイクロサービスです)。この問題(イベント駆動型サービス)は解決可能であり、ご存知のとおりです。 UIの動作を再考できるかどうかを判断する必要があります。
より一般的な観点から-リクエストを受信すると、現在のリクエストのコンテキスト(リクエストオブジェクトがスコープ内にあることを意味します)でキューにサブスクライバーを登録できます。操作の総数の進捗を維持します)。終了状態に達すると、応答を返し、リスナーを削除します。これは、任意の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ソケットベースのプッシュメカニズムを使用するだけでは、必ずしもシステムの効率やパフォーマンスが向上するとは限りません。ただし、ソケット接続を使用してクライアントにメッセージをプッシュすることをお勧めします。これにより、アーキテクチャがより一般的になり(クライアントがサービスのように動作する場合でも)、一貫性があり、懸念事項をより適切に分離できます。また、ビジネスロジックを気にすることなく、プッシュサービスを個別にスケーリングできます。サガパターンを拡張して、部分的な障害やタイムアウトの場合のロールバックを有効にし、システムをより管理しやすくすることができます。
以下は、I Serviceを実装して、通常のHTTP要求/応答フローで動作する方法の非常に基本的な例です。 node.jsを使用しますevents.EventEmitter
クラスは、適切なHTTPハンドラーに応答を「ルーティング」します。
実装の概要:
プロデューサー/コンシューマークライアントをKafkaに接続する
EventEmitter
クラスからグローバルイベントディスパッチャーを作成します
エラーやタイムアウト処理などを省き、コードをできるだけ小さくしようとしたことに注意してください!
また、kafkaProduceTopic
とkafkaConsumTopic
はテストを簡素化するための同じトピックであり、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);
});
Promises の使用はどうですか? Socket.ioは、リアルタイムが必要な場合のソリューションにもなります。
[〜#〜] cqrs [〜#〜] もご覧ください。このアーキテクチャパターンは、イベント駆動型モデルとマイクロサービスアーキテクチャに適合します。
さらに良い。 this を読んでください。
残念ながら、このようなことを達成するには、長いポーリングまたは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アクションを作成しますが、それは非常にうまく機能します!
お役に立てれば。