【问题标题】:Avoid waiting on Servlet streams避免等待 Servlet 流
【发布时间】:2017-07-06 11:38:05
【问题描述】:

我的 Servler 花费了相当长的时间阅读 request.getInputStream() 并写入 response.getOutputStream()。从长远来看,这可能是一个问题,因为它阻塞线程只是为了每秒读取/写入 literally 几个字节。 (*)

我对部分请求数据从不感兴趣,在请求完全可用之前不应开始处理。响应也是如此。

我想,异步 IO 会解决它,但我想知道什么是正确的方法。也许一个 servlet FilterServletInputStream 替换为一个包装好的 ByteArrayInputStream,使用 request.startAsync 并在收集整个输入后调用链接的 servlet?

  • 已经有这样的过滤器了吗?
  • 我应该写一个还是应该使用不同的方法?

请注意,我的意思是避免在缓慢的 servlet 流上浪费线程。这与startAsync 不同,后者避免了浪费线程等待某个事件

是的,目前这是一个过早的优化。

按要求我的读取循环

我目前的输入流读取方法没有什么有趣的,但是你在这里:

private byte[] getInputBytes() throws IOException {
    ServletInputStream inputStream = request.getInputStream();
    final int len = request.getContentLength();
    if (len >= 0) {
        final byte[] result = new byte[len];
        ByteStreams.readFully(inputStream, result);
        return result;
    } else {
        return ByteStreams.toByteArray(inputStream);
    }
}

仅此而已,当数据不可用时它会阻塞; ByteStreams来自番石榴。

到目前为止我的理解总结

正如答案明确指出的那样,在不浪费线程的情况下使用 servlet 流是不可能的。 servlet 架构和通用实现都没有公开任何允许说“缓冲整个数据并仅在您收集所有内容时才给我打电话”的东西,尽管他们使用 NIO 并且可以做到。

原因可能是通常使用像 nginx 这样的反向代理,它可以做到。 nginx 默认做这个缓冲,直到two years ago 才能关闭。

居然支持案例???

鉴于很多否定的答案,我不确定,但这看起来像是我的目标

避免在缓慢的 servlet 流上浪费线程

实际上完全支持:从 3.1 开始,ServletInputStream.html#setReadListener 似乎就是为此而生的。分配用于处理Servlet#Service 的线程最初调用request.startAsync(),附加侦听器并通过简单地从service 返回返回到池中。监听器实现onDataAvailable(),当可以读取不阻塞时调用它,添加一条数据并返回。在onAllDataRead(),我可以对采集到的数据进行全程处理。

有一个example,如何使用 Jetty 来完成。它似乎也涵盖了非阻塞输出。


(*) 在日志文件中,我可以看到读取输入(100 字节标头 + 100 字节数据)的请求最多需要 8 秒。这种情况很少见,但确实会发生,尽管服务器大多处于空闲状态。所以我猜,这是一个连接非常糟糕的移动客户端(我们的一些用户从连接如此糟糕的地方连接)。

【问题讨论】:

  • Servlet 3.0 已经为 Servlet 提供了异步 API。您不必自己动手。
  • @EJP 是的,但是 servlet 异步 API 是关于在等待某事时释放线程。在这里,我的意思是异步 IO,即在读取慢速流(恰好是 servlet 流)时不浪费线程。一个实现将使用这两种方式作为异步。
  • 我真的不明白为什么你的问题附有赏金?如果你想学习如何编写一个 nio web 服务器,那就去做并测试它......已经有很多示例和代码,只需谷歌“java nio web server”...... Async io 无法解决这个事实您必须等待 tcp 驱动程序将数据包编组为字节,然后您的 java 类才能读取它们?如果我错了,我错过了什么?
  • 向我们展示您的读/写循环代码。
  • 在现实生活中只需在 servlet 引擎之前使用 nginx。见:https://stackoverflow.com/questions/28166284/does-nginx-also-buffer-http-request-from-client

标签: java servlets asynchronous


【解决方案1】:

HttpServletRequest#startAsync() 对此没有用。这仅对推送诸如 Web 套接字和良好的 'ol SSE 之类的东西有用。此外,JSR356 Web Socket API 是建立在它之上的。

您的具体问题已了解,但这绝对不能从 servlet 开始解决。由于非常简单的原因,您最终只会浪费更多线程,因为容器已经将当前线程专用于 servlet 请求,直到请求正文被完全读取到最后一位,即使它最终被新生成的读取异步线程。

为了节省线程,您实际上需要一个支持 NIO 的 servletcontainer,并在必要时打开该功能。使用 NIO,单个线程可以处理可用堆内存允许的尽可能多的 TCP 连接,而不是为每个 TCP 连接分配单个线程。然后,在您的 servlet 中,您根本不需要担心这个微妙的 I/O 任务。

几乎所有现代 servlet 容器都支持它:Undertow (WildFly)、Grizzly (GlassFish/Payara)、TomcatJetty 等。有些默认启用,有些则需要额外配置。只需使用关键字“NIO”参考他们的文档即可。

如果您实际上还想保存 servlet 请求线程本身,那么您基本上需要退后一步,删除 servlet 并在现有 NIO 连接器(Undertow、Grizzly 、码头等)。

【讨论】:

  • IIUIC 我的 Jetty 已经使用 NIO 来为每个请求分配线程,而不是为每个连接分配线程。所以我可以想象它可以被告知在分配线程之前等待整个响应,而不是只等待第一个字节。
  • 您必须自己为此编写 asyncreqprocessor,但这比听起来容易吗?至少在我看来,具体的实现不确定是否会有任何收获,有可能会有,这取决于很多其他因素......无论如何有趣的实验练习,大多数def
  • “为了每个请求分配线程而不是每个连接线程” 这不是 NIO 的重点。如果没有 NIO,您至少会浪费 2 个线程来处理这个缓慢的请求。一个用于 TCP,一个用于 servlet。使用 NIO,您将只“浪费”一个线程来处理这个缓慢的请求(用于 servlet 的线程;用于 TCP 的线程可用于其他连接/请求)。但无论如何,您最终至少需要一个可用线程。您还如何以线程安全的方式处理请求正文?
  • 没错,你需要一个线程来处理那些 SLOW 请求,该线程可以在阻塞容器之外以异步方式单独管理,性能将归结为有多少内核以及你的操作系统将如何处理调度......非常实验性,因为无法判断在重负载下会如何表现,甚至可能会减慢速度,所以仍然需要超时等......就像我上面所说的很多因素需要考虑
  • @Kin:如已回答,根据 Servlet API,这是不可能的。由于非常简单的原因,您最终只会浪费更多线程,因为容器已经将当前线程专用于 servlet 请求,直到请求正文被完全读取到最后一位,即使它最终被新生成的读取异步线程。您基本上需要退后一步,删除 servlet 并在现有 NIO 连接器(Undertow、Grizzly、Jetty 等)之上实现自定义服务。
【解决方案2】:
  1. 你不能。 Servlet 容器将线程分配给请求,这就是它的结束,它被分配了。这就是模型。如果您不喜欢这样,您将不得不停止使用 Servlet。
  2. 即使您可以解决 (1),您也无法在输入流上启动异步 I/O。
  3. 处理缓慢请求的方法是通过为您正在使用的任何容器设置适当的设置来将它们超时... 如果您确实遇到了问题,而且远不清楚你真的这样做了,服务器大部分是空闲的,这种情况很少发生。
  4. 您的读取循环可以毫无区别地进行区分。只需将请求输入流读到底。 servlet 容器已经确保流的结束发生在 content-length(如果提供)处。

【讨论】:

  • 2 号可能是用词不当?不能肯定地说,因为这是理论上的东西,但我的直觉是你应该能够抽象并委托给异步进程。
  • @Kin Async IO !== 异步进程。 OP 只需要一个支持 NIO 的 web/servlet 容器。这不能从 servlet 开始控制。
  • 为什么不呢?据我了解,op希望委托实际读取字节的方法在另一个线程中运行异步,其主要特征是它没有被容器使用......由接收原始请求的容器分配的线程将“完成”并且 serversocketchannel 将接收原始请求并调用 doGet 或 doPost 或 api 中的任何其他方法...请向我解释为什么无法控制此行为?它甚至不必是 hacky,它可以作为一个完全有效的模块或容器的一个特性存在
  • @Kin 1. 这里没有“用词不当”,这不是正确的词。您不能从 Java 输入流启动异步 I/O。它没有 API。 2. OP 无权访问正在读取的方法。它隐藏在HttpServletRequestInputStream 的外观后面,并且第三次,您不能从Java 输入流启动异步I/O。 3.ServerSocketChannels除了入站连接请求什么都不会收到,如果你的意思是SocketChannels,它们不是异步的,如果是从Sockets获取的,它们甚至不是非阻塞的。
  • 你不能从输入流启动异步io,但是你可以启动异步io并传递一个输入流进行处理,当输入流完成时,你可以让异步io完成请求.. . 真的那么难弄清楚吗?用词不当也绝对是正确的词,但我不会就此与您争论...请参阅下面 BalusC 对他的回答的评论,谈论完全相同的事情
【解决方案3】:

有一个名为org.apache.catalina.connector.CoyoteAdapter 的类,它是接收来自TCP 工作线程的封送请求的类。它有一种称为“服务”的方法,可以完成大部分繁重的工作。这个方法被另一个类调用:org.apache.coyote.http11.Http11Processor,它也有一个同名的方法。

我觉得很有趣,我在代码中看到了这么多处理异步 io 的钩子,这让我想知道这不是容器的内置功能吗?无论如何,以我有限的知识,我能想到的实现您所说的功能的最佳方法是创建一个类:

public class MyAsyncReqHandlingAdapter extends CoyoteAdapter@Override service() 方法并自己动手...我现在没有时间专门做这件事,但我将来可能会重新访问。

在这种方法中,您需要一种方法来识别 请求并处理它们,方法是将它们交给单线程 nio 处理器并在该级别“完成”请求,鉴于源代码:

https://github.com/apache/tomcat/blob/075920d486ca37e0286586a9f017b4159ac63d65/java/org/apache/coyote/http11/Http11Processor.java

https://github.com/apache/tomcat/blob/3361b1321201431e65d59d168254cff4f8f8dc55/java/org/apache/catalina/connector/CoyoteAdapter.java

您应该能够弄清楚该怎么做。有趣的问题,是的,它可以做到。我在规范中看到的没有任何内容表明它不能......

【讨论】:

  • 这显然是针对 tomcat 的,但您应该能够从任何开源容器中获取相同的信息...
猜你喜欢
  • 1970-01-01
  • 2015-03-07
  • 1970-01-01
  • 2022-01-21
  • 2014-12-04
  • 2021-06-21
  • 2021-12-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多