15

在 REST 服务前面有一个请求队列的最佳技术解决方案(框架/方法)是什么。这样我就可以增加 REST 服务实例的数量以获得更高的可用性,并通过将请求队列放在前面来形成服务客户端的服务/事务边界。

  1. 我需要为请求队列(java)选择好的轻量级技术/框架
  2. 用它实现竞争消费者的方法。
4

3 回答 3

14

这里有几个问题,取决于你的目标。

首先,它只促进后端资源的可用性。考虑是否有 5 台服务器在后端处理队列请求。如果其中一台服务器出现故障,则排队的请求应回退到队列中,并重新传递到其余 4 台服务器之一。

但是,当这些后端服务器正在处理时,前端服务器正在处理实际的发起请求。如果这些前端服务器之一发生故障,则这些连接将完全丢失,由原始客户端重新提交请求。

前提可能是更简单的前端系统发生故障的风险较低,对于与软件相关的故障当然也是如此。但是网卡、电源、硬盘驱动器等对人类的这种虚假希望是相当不可知的,并平等地惩罚所有人。因此,在谈论整体可用性时请考虑这一点。

就设计而言,后端是一个简单的进程,等待一个 JMS 消息队列,并在每个消息到来时对其进行处理。有大量可用的示例,任何 JMS 服务器都将适合高级别的。您只需要确保消息处理是事务性的,这样如果消息处理失败,消息仍保留在队列中,并且可以重新传递到另一个消息处理程序。

您的 JMS 队列的主要要求是可集群的。JMS 服务器本身就是系统中的单点故障。丢失了 JMS 服务器,您的系统几乎陷入困境,因此您需要能够集群服务器并让消费者和生产者适当地处理故障转移。同样,这是特定于 JMS 服务器的,大多数人都这样做,但在 JMS 世界中这是非常常规的。

前端是事情变得有点棘手的地方,因为前端服务器是从 REST 请求的同步世界到后端处理器的异步世界的桥梁。REST 请求遵循典型的 RPC 模式,即使用来自套接字的请求负载、保持连接打开、处理结果并将结果传递回原始套接字。

为了体现这种移交,您应该看一下处理 Servlet 3.0 的异步 Servlet,它在 Tomcat 7、最新的 Jetty(不确定是什么版本)、Glassfish 3.x 等中可用。

在这种情况下,您要做的是在请求到达时,将名义上同步的 Servlet 调用转换为使用HttpServletRequest.startAsync(HttpServletRequest request, HttpServletResponse response).

这将返回一个 AsynchronousContext,并且一旦启动,就允许服务器释放处理线程。然后你做几件事。

  1. 从请求中提取参数。
  2. 为请求创建一个唯一 ID。
  3. 根据您的参数创建一个新的后端请求负载。
  4. 将 ID 与 AsyncContext 相关联,并保留上下文(例如将其放入应用程序范围的 Map)。
  5. 将后端请求提交到 JMS 队列。

至此,初始处理完成,您只需从 doGet(或服务,或其他)返回。由于您没有调用 AsyncContext.complete(),因此服务器不会关闭与服务器的连接。由于您通过 ID 将 AsyncContext 存储在地图中,因此暂时可以方便地安全保存。

现在,当您将请求提交到 JMS 队列时,它包含:请求的 ID(您生成的)、请求的任何参数以及发出请求的实际服务器的标识。最后一点很重要,因为处理的结果需要返回到它的原点。源由请求 ID 和服务器 ID 标识。

当您的前端服务器启动时,它还启动了一个线程,该线程的工作是侦听 JMS 响应队列。当它设置它的 JMS 连接时,它可以设置一个过滤器,例如“只给我一个服务器 ID 为 ABC123 的消息”。或者,您可以为每个前端服务器创建一个唯一的队列,后端服务器使用服务器 ID 来确定要返回回复的队列。

当后端处理器使用消息时,它们会获取请求 ID 和参数,执行工作,然后获取结果并将它们放入 JMS 响应队列。当它返回结果时,它会将原始 ServerID 和原始请求 ID 添加为消息的属性。

因此,如果您最初收到前端服务器 ABC123 的请求,后端处理器会将结果返回给该服务器。然后,该侦听器线程将在收到消息时收到通知。侦听器线程的任务是获取该消息并将其放入前端服务器的内部队列中。

这个内部队列由一个线程池支持,该线程池的工作是将请求有效负载发送回原始连接。它通过从消息中提取原始请求 ID、从前面讨论的内部映射中查找 AsyncContext,然后将结果发送到与 AsyncContext 关联的 HttpServletResponse 来实现这一点。最后,它调用 AsyncContext.complete() (或类似方法)来告诉服务器您已完成并允许它释放连接。

对于内务管理,您应该在前端服务器上有另一个线程,它的工作是检测请求何时在地图中等待太久。原始消息的一部分应该是请求开始的时间。该线程可以每秒唤醒一次,扫描地图中的请求,对于任何存在时间过长(例如 30 秒)的请求,它可以将请求放入另一个内部队列,由一组处理程序消耗,这些处理程序旨在通知请求超时的客户端。

您需要这些内部队列,以便主要处理逻辑不会卡在等待客户端使用数据。这可能是连接速度慢或其他原因,因此您不想阻止所有其他待处理的请求来一一处理它们。

最后,您需要考虑到您很可能会从响应队列中收到一条消息,该消息针对您的内部映射中不再存在的请求。一方面,请求可能已经超时,所以它不应该再存在了。另一方面,该前端服务器可能已停止并重新启动,因此挂起请求的内部映射将简单地为空。此时,如果您检测到您有一个不再存在的请求的回复,您应该简单地丢弃它(好吧,记录它,然后丢弃它)。

您不能重用这些请求,实际上并没有负载均衡器返回客户端。如果客户端允许您通过发布的端点进行回调,那么请确保您可以让另一个 JMS 消息处理程序发出这些请求。但这不是 REST 之类的东西,在这个级别的讨论中,REST 更多的是客户端/服务器/RPC。

至于哪个框架支持比原始 Servlet 更高级别的异步 Servlet(例如 JAX-RS 的 Jersey 或类似的东西),我不能说。我不知道什么框架在那个级别支持它。似乎这是 Jersey 2.0 的一个功能,尚未发布。可能还有其他人,你必须四处看看。另外,不要专注于 Servlet 3.0。Servlet 3.0 只是一段时间以来在单个容器中使用的技术的标准化(尤其是 Jetty),因此您可能希望查看 Servlet 3.0 之外的容器特定选项。

但是概念是一样的。最大的收获是带有过滤的 JMS 连接的响应队列侦听器、到 AsyncContext 的内部请求映射,以及在应用程序中执行实际工作的内部队列和线程池。

于 2013-01-07T06:19:52.353 回答
4

如果您放宽了必须使用 Java 的要求,您可以考虑使用 HAProxy。它非常轻量级,非常标准,并且做了很多好事(请求池/keepalives/排队)。

不过,在实施请求队列之前请三思。除非您的流量非常突发,否则它只会损害系统在负载下的性能。

假设您的系统每秒可以处理 100 个请求。您的 HTTP 服务器有一个有限的工作线程池。请求池可以提供帮助的唯一方法是每秒接收超过 100 个请求。在您的工作线程池已满后,请求开始堆积在您的负载均衡器池中。由于它们到达的速度比您处理它们的速度快,因此队列变得越来越大……越来越大……越来越大。最终要么这个池也被填满,要么你的 RAM 用完了,负载均衡器(以及整个系统)严重崩溃。

如果您的 Web 服务器太忙,请开始拒绝请求并在线获取一些额外容量。

如果您能够及时获得额外的容量来处理请求,请求池肯定会有所帮助。它也会对你造成非常严重的伤害。在您的 HTTP 服务器的工作线程池之前打开辅助请求池之前,请考虑后果。

于 2013-01-07T06:25:47.053 回答
0

我们使用的设计是一个 REST 接口接收所有请求并将它们分派到消息队列(即 Rabbitmq)

然后工作人员听取消息并按照一定的规则执行它们。如果一切都失败了,您仍然会在 MQ 中收到请求,如果您有大量请求,您可以添加工作人员......

查看这个主题演讲,它展示了这个概念的力量!

http://www.springsource.org/SpringOne2GX2012

于 2013-01-09T14:36:27.827 回答