REST 服务前面的请求队列

2022-09-03 14:06:43

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

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

答案 1

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

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

但是,当这些后端服务器正在处理时,前端服务器将保留实际的启动请求。如果其中一个前端服务器出现故障,则这些连接将完全丢失,并且将由原始客户端重新提交请求。

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

至于设计,后端是一个简单的过程,等待JMS消息队列,并在每个消息到达时对其进行处理。有很多这样的例子,任何JMS服务器都可以在较高级别上使用。您所需要做的就是确保消息处理是事务性的,以便在消息处理失败时,消息将保留在队列中,并且可以重新传递给另一个消息处理程序。

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

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

为了体现这一点,你应该看看异步Servlet处理Servlet 3.0引入,并在Tomcat 7,最新的Jetty(不确定是什么版本),Glassfish 3.x等中可用。

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

这将返回一个异步上下文,一旦启动,将允许服务器释放处理线程。然后你做几件事。

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

此时,初始处理已完成,您只需从 doGet(或服务或其他任何内容)返回即可。由于您尚未调用 AsyncContext.complete(),因此服务器不会关闭与服务器的连接。由于您在地图中按 ID 设置了 AsyncContext 存储,因此暂时可以方便地进行安全保存。

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

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

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

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

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

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

您需要这些内部队列,以便主处理逻辑不会卡住等待客户端使用数据。它可能是连接速度慢或其他东西,因此您不希望阻止所有其他挂起的请求以逐个处理它们。

最后,您需要说明,您很可能会从响应队列中收到一条消息,该消息已不复存在于内部映射中。首先,请求可能已超时,因此它不应再存在。另一方面,该前端服务器可能已停止并重新启动,因此挂起请求的内部映射将只是空的。此时,如果您检测到对不再存在的请求有回复,则应直接丢弃它(好吧,记录它,然后丢弃它)。

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

至于哪个框架支持异步Servlet的级别高于原始Servlet(例如JaX-RS的Jersey或类似的东西),我不能说。我不知道在这个级别上有什么框架支持它。似乎这是泽西岛2.0的一个功能,它还没有出来。很可能还有其他人,你必须环顾四周。另外,不要执着于Servlet 3.0。Servlet 3.0 只是一段时间以来在单个容器中使用的技术的标准化(特别是 Jetty),因此您可能希望查看 Servlet 3.0 之外特定于容器的选项。

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


答案 2

如果你放宽了它必须在Java中的要求,你可以考虑HAProxy。它非常轻量级,非常标准,并且做了很多好事(请求池/保持活动/排队)。

但是,在实现请求队列之前,请三思而后行。除非您的流量非常突发,否则除了损害系统在负载下的性能外,什么都不会。

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

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

如果您可以及时获得额外的容量来处理请求,则请求池当然会有所帮助。它也会严重伤害你。在 HTTP 服务器的工作线程池前面打开辅助请求池之前,请仔细考虑后果。