背景:
我正在构建一个应用程序,并且所建议的体系结构是基于微服务体系结构的事件/消息驱动。
处理问题的整体方法是我拥有一个User/HTTP request
,并且该命令会操作某些具有直接synchronous response
的命令。因此,响应同一用户/ HTTP请求是“无麻烦的”。
问题:
用户将HTTP request
发送到 UI服务(有多个UI服务),从而将一些事件触发到队列(Kafka / RabbitMQ / any)。 N个服务选择事件/消息在此过程中发挥了神奇作用,然后然后在某个时刻,同一UI Service应该选择一个响应并将其返回给发起HTTP请求的用户。根据典型的HTTP交互,请求处理为ASYNC
,但User/HTTP REQUEST->RESPONSE
为SYNC
。
问题: 在这个不可知论/事件驱动的世界中,我如何向发起该操作的同一UI服务(该服务通过HTTP与用户进行交互)发送响应?
到目前为止我的研究 我一直在环顾四周,似乎有些人正在使用WebSockets解决该问题。
但是,复杂性在于,需要有一些映射(RequestId->Websocket(Client-Server))
的表,该表用于“发现”网关中哪个节点具有用于特定响应的websocket连接。但是,即使我理解了问题和复杂性,我仍然被困在找不到在实施层可以提供有关如何解决此问题的信息的文章。 AND 这仍然不是一个可行的选择,因为第三方集成,例如期望REQUEST->RESPONSE
的付款提供商(WorldPay)-特别是在3DS验证上。
所以我有点不愿意认为WebSockets是一个选择。但是,即使WebSocket适用于Web界面应用程序,但用于连接到外部系统的API也不是很好的体系结构。
** ** ** 更新: ** **
即使对于带有202 Accepted
,Location header
和retry-after header
的WebService API来说,长轮询也是可能的解决方案,但对于高并发性和高能力的网站而言,它并不是高性能。
想象一下,有很多人试图在他们发出的每个请求上获取交易状态更新,而您必须使CDN缓存无效(现在就去解决这个问题!哈)。
但最重要且与我的情况相关的是我有第三方API(例如付款系统),其中3DS系统具有由付款提供商系统处理的自动重定向,并且他们期望使用典型的REQUEST/RESPONSE flow
,因此该模型不适用于我,套接字模型也无法工作。
由于这种用例,HTTP REQUEST/RESPONSE
应该以一种典型的方式进行处理,在这种情况下,我有一个笨拙的客户端,期望处理的复杂性在后端处理。
所以我正在寻找一种解决方案,在外部我有一个典型的Request->Response
(SYNC)并且状态的复杂性(系统的ASYNCrony)在内部处理
长时间轮询的示例,但是该模型不适用于不在我控制范围内的第三方API,例如3DS Redirects
上的付款提供商。
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
答案 0 :(得分:2)
下面是一个非常简单的示例,说明如何实现 UI服务,以便它与常规HTTP请求/响应流一起使用。它使用node.js events.EventEmitter
类将响应“路由”到正确的HTTP处理程序。
实施概述:
将生产者/消费者客户端连接到Kafka
从EventEmitter
类创建全局事件调度程序
请注意,我试图将代码保持尽可能小,省去了错误和超时处理等问题!
还要注意,kafkaProduceTopic
和kafkaConsumTopic
是简化测试的相同主题,无需其他服务/功能即可生成 UI服务消费主题。>
该代码假定已安装kafka-node
的{{1}}和uuid
软件包,并且可以在npm
上访问Kafka。
localhost:9092
答案 1 :(得分:2)
从更一般的角度来看-收到请求后,您可以在当前请求的上下文中(即请求对象在范围内)在队列中注册订阅者,这将在负责的服务完成其工作时(例如,维护操作总数进度的状态机)。当达到终止状态时,它将返回响应并删除侦听器。我认为这可以在任何发布/订阅样式消息队列中使用。这是我所建议内容的过度简化的演示。
// 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套接字的推送机制并不一定会提高系统的效率或性能。但是,建议您使用套接字连接将消息推送到客户端,因为它可使您的体系结构更通用(甚至客户端的行为类似于您的服务),并且保持一致,并可以更好地分离问题。它还将使您能够独立扩展推服务,而不必担心业务逻辑。可以扩展使用saga模式,以在发生部分故障或超时的情况下启用回滚,并使您的系统更易于管理。
答案 2 :(得分:2)
正如我所期望的那样-人们尝试将所有内容都放入一个概念中,即使它不适合该概念。这不是批评,这是根据我的经验以及阅读您的问题和其他答案后的观察。
是的,微服务架构基于异步消息传递模式是正确的。但是,当我们谈论UI时,我想到了两种可能的情况:
UI立即需要响应(例如,读取操作或用户期望其立即响应的命令)。 这些不必是异步的。如果立即在屏幕上需要响应,为什么还要增加消息传递和异步的开销?没有道理。微服务体系结构应该解决问题,而不是通过增加开销来创建新问题。
UI可以进行重组以容忍延迟的响应(例如,UI不必等待结果,而可以仅提交命令,接收确认,并在准备响应时让用户执行其他操作)。在这种情况下,您可以引入异步。 gateway 服务(与UI直接交互的服务)可以协调异步处理(等待完整的事件等),并且准备就绪后,可以与UI进行通信。在这种情况下,我已经看到UI使用SignalR,并且网关服务是接受套接字连接的API。如果浏览器不支持套接字,则理想情况下应回退到轮询。无论如何,重要的一点是,这只能在偶然情况下起作用: UI可以容忍延迟的答案。
如果微服务确实与您的情况相关(案例2),则相应地构建UI流程,并且后端微服务应该没有挑战。在这种情况下,您的问题归结为将事件驱动的体系结构应用于服务集(边缘是连接事件驱动和UI交互的网关微服务)。这个问题(事件驱动的服务)可以解决,您知道这一点。您只需要确定是否可以重新考虑UI的工作原理即可。
答案 3 :(得分:2)
问题:在这个不可知/事件驱动的世界中,我如何向发起操作的同一 UI 服务(通过 HTTP 与用户交互的服务)发送响应?
<块引用>所以我正在寻找一种解决方案,在该解决方案中,我在外部有一个典型的请求->响应(SYNC),并且状态的复杂性(系统的 ASYNCrony)在内部进行处理
我认为需要注意的重要事项如下。
简而言之:
系统的边缘可以是同步的,同时它以基于事件的异步方式在内部处理内容。参见:illustration 和 {{ 3}}。
UI 服务(Web 服务器或 API 网关)可以触发 async/await 函数(用 Node.js 编写),然后继续处理其他请求。然后,当 UI 服务从后端的另一个微服务接收到 'OrderConfirmed' 事件(例如通过侦听 Kafka 日志)时,它将再次接收用户请求(回调)并将指定的响应发送给客户端。
试试backing,它会为你处理。
长:
客户端可以同步服务,而后端异步处理其内部事件。客户不知道,或需要知道。但客户必须等待。所以要注意处理时间长。因为客户端的 HTTP 请求可能会超时(对于 HTTP 请求通常为 Reactive Interaction Gateway (RIG)。或者如果 UI 服务在 30 sec to 2 min 中运行,它也可能超时;默认为 1 分钟后,但可以延长到 9 分钟)。但如果请求是端到端同步的,超时实际上也是一个问题。异步事件驱动架构并没有改变这一点,它只是提升了人们的关注度。
由于客户端发送同步请求,因此它被阻塞(用户必须在 UI 中等待)。然而,这并不意味着 UI 服务(Web 服务器或 API 网关)必须被阻塞。它可以触发 async/await 函数(用 Node.js 编写),然后继续处理其他请求。然后,当 UI Service 收到 'OrderConfirmed' 事件时,它会再次拿起该函数(回调)并将指定的响应发送给客户端。
后端边缘与第 3 方系统交互的任何(微)服务都可以同步进行,使用典型的 HTTP 请求/响应流(尽管通常您希望异步/await 也在这里,以在 3rd 方处理时释放您自己的微服务资源)。当它同步接收来自第 3 方的响应时,它可以向后端的其余部分发送一个异步事件(例如“StockResupplied”事件)。如果 UI 服务设计为在给出响应之前等待此类事件,则可以传播回客户端。
灵感来源:Cloud Function。
上述替代方案是:
以用户不必阻塞/等待的方式设计客户端的 UI(可能已经给出了乐观的成功响应,符合“乐观 UI”原则),但随后异步接收 {{ 3}}。
“服务器发送事件 (SSE) 是一种技术,它使浏览器(客户端)能够通过 HTTP 连接从服务器接收自动更新,例如基于文本的事件数据。”< /p>
SSE 在后台“仅使用一个长期存在的 HTTP 连接”。
您可能还想知道:
“很高兴知道 SSE 受到最大打开连接数的限制,这在打开各种选项卡时尤其痛苦,因为每个浏览器的限制是 6。”
“与 SSE 相比,WebSockets 的设置要复杂得多,而且任务量也大。”
来源:Brad Irby's answer to a related question
此外:
使用 Server-Sent Event (SSE),但它更复杂,会给用户带来烦人的弹出窗口,并且更适合在浏览器外部和/或网络应用程序关闭时发送通知。
您可能还想看看:
https://www.telerik.com/blogs/websockets-vs-server-sent-events,对于一些不错的架构图,以及一个Web Push API with a Service Worker is another option to SSE,Reactive Interaction Gateway (RIG) 在这个答案中。 RIG 是一种用于微服务的可扩展的免费开源 API 网关,可处理 SSE、WebSockets、长轮询等。readme “订阅 Kafka 主题,同时保持与所有活动前端的连接,将事件转发到以可扩展的方式发送给他们的用户。除此之外,它还处理授权,因此您的服务也不必关心这些。”
答案 4 :(得分:0)
不幸的是,我相信您可能必须使用长时间轮询或网络套接字来完成类似的事情。您需要向用户“推送”某些内容,或者保持http请求处于打开状态,直到有内容返回。
要处理将数据返回给实际用户的问题,可以使用类似socket.io的方法。当用户连接时,socket.io将创建一个ID。每当用户连接时,您都将用户ID映射到ID socket.io。 一旦每个请求都附加了一个用户ID,您就可以将结果发送回正确的客户端。流将是这样的:
网络请求顺序(带有数据和用户ID的POST)
ui服务将订单置于队列中(此订单应具有userId)
x个服务按订单运行(每次都传递userId)
ui服务从主题消费。有时,数据显示在主题上。它消耗的数据具有userId,ui服务会查找映射以找出要发送到哪个套接字。
无论您的UI上运行的是什么代码,都必须是事件驱动的,因此它将处理数据推送而无需原始请求的上下文。您可以为此使用redux之类的东西。本质上,您将让服务器在客户端上创建redux动作,它运行良好!
希望这会有所帮助。
答案 5 :(得分:0)
答案 6 :(得分:-3)
好问题。我的答案是,在系统中引入同步流。
我正在使用RabbitMq,所以我不了解kafka,但您应该搜索kafka的同步流。
WebSockets确实显得有些不足。
希望有帮助。