Jetty中的传输速度缓慢,在某些缓冲区大小时使用分块传输编码

我正在调查Jetty 6.1.26的性能问题。 Jetty似乎使用Transfer-Encoding: chunked ,并且根据使用的缓冲区大小,在本地传输时这可能非常慢。

我用一个servlet创建了一个小型Jetty测试应用程序来演示这个问题。

 import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.mortbay.jetty.Server; import org.mortbay.jetty.nio.SelectChannelConnector; import org.mortbay.jetty.servlet.Context; public class TestServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { final int bufferSize = 65536; resp.setBufferSize(bufferSize); OutputStream outStream = resp.getOutputStream(); FileInputStream stream = null; try { stream = new FileInputStream(new File("test.data")); int bytesRead; byte[] buffer = new byte[bufferSize]; while( (bytesRead = stream.read(buffer, 0, bufferSize)) > 0 ) { outStream.write(buffer, 0, bytesRead); outStream.flush(); } } finally { if( stream != null ) stream.close(); outStream.close(); } } public static void main(String[] args) throws Exception { Server server = new Server(); SelectChannelConnector ret = new SelectChannelConnector(); ret.setLowResourceMaxIdleTime(10000); ret.setAcceptQueueSize(128); ret.setResolveNames(false); ret.setUseDirectBuffers(false); ret.setHost("0.0.0.0"); ret.setPort(8080); server.addConnector(ret); Context context = new Context(); context.setDisplayName("WebAppsContext"); context.setContextPath("/"); server.addHandler(context); context.addServlet(TestServlet.class, "/test"); server.start(); } } 

在我的实验中,我使用128MB测试文件,servlet返回到客户端,使用localhost连接。 使用Java编写的简单测试客户端(使用URLConnection )下载这些数据需要3.8秒,这非常慢(是的,它是33MB / s,听起来不是很慢,除了这是纯粹本地的并且输入文件是缓存的;它应该快得多)。

现在这里变得奇怪了。 如果我使用wget下载数据,这是一个HTTP / 1.0客户端,因此不支持分块传输编码,它只需要0.1秒。 这是一个更好的数字。

现在,当我将bufferSize更改为4096时,Java客户端需要0.3秒。

如果我完全删除对resp.setBufferSize的调用(似乎使用24KB块大小),Java客户端现在需要7.1秒,而wget突然同样慢!

请注意我不是Jetty的专家。 我在Hadoop 0.20.203.0中使用reduce task shuffling诊断性能问题时遇到了这个问题,它使用Jetty以类似于简化示例代码的方式传输文件,缓冲区大小为64KB。

问题在我们的Linux(Debian)服务器和我的Windows机器上以及Java 1.6和1.7上都会重现,所以它似乎完全依赖于Jetty。

有没有人知道可能导致这种情况的原因,如果我能做些什么呢?

我相信通过查看Jetty源代码,我自己找到了答案。 它实际上是响应缓冲区大小,传递给outStream.write的缓冲区大小以及是否调用outStream.flush (在某些情况下)的复杂相互作用。 问题在于Jetty使用其内部响应缓冲区的方式,以及如何将写入输出的数据复制到该缓冲区,以及何时以及如何刷新缓冲区。

如果与outStream.write使用的缓冲区的大小等于响应缓冲区(我认为多个也可以),或者使用更少的outStream.flush ,那么性能就可以了。 然后将每个write调用直接刷新到输出,这很好。 但是,当写入缓冲区较大而不是响应缓冲区的倍数时,这似乎会导致处理刷新的方式有些奇怪,导致额外的刷新,从而导致性能不佳。

在分块传输编码的情况下,电缆中存在额外的扭结。 对于除第一个块之外的所有块,Jetty保留12个字节的响应缓冲区以包含块大小。 这意味着在我的原始示例中使用64KB写入和响应缓冲区,适合响应缓冲区的实际数据量仅为65524字节,因此写入缓冲区的部分也会溢出到多个刷新中。 查看此场景的捕获网络跟踪,我看到第一个块是64KB,但所有后续块都是65524个字节。 在这种情况下, outStream.flush没有任何区别。

当使用4KB缓冲区时,我只有在outStream.flushoutStream.flush看到快速速度。 事实certificateresp.setBufferSize只会增加缓冲区大小,因为默认大小是24KB, resp.setBufferSize(4096)是一个无操作。 但是,我现在正在编写4KB的数据,即使保留了12个字节也适合24KB缓冲区,然后通过outStream.flush调用将其刷新为4KB块。 但是,当删除对flush的调用时,它将让缓冲区填满,再次将12个字节溢出到下一个块中,因为24是4的倍数。

结论

看来为了获得Jetty的良好性能,您必须:

  • 调用setContentLength (没有chunked传输编码)并使用与响应缓冲区大小相同的write缓冲区时。
  • 使用分块传输编码时,使用比响应缓冲区大小至少小12字节的写缓冲区,并在每次写入后调用flush

请注意,“慢”方案的性能仍然是这样,您可能只会看到本地主机或非常快(1Gbps或更高)网络连接的差异。

我想我应该为此针对Hadoop和/或Jetty提交问题报告。

是的,如果无法确定响应的大小,Jetty将默认为Transfer-Encoding: Chunked

如果你知道响应的大小那将是什么。 你需要调用resp.setContentLength(135*1000*1000*1000); 在这种情况下,而不是

resp.setBufferSize();

实际上设置resp.setBufferSize是无关紧要的。

在打开OutputStream之前,就在此行之前: OutputStream outStream = resp.getOutputStream(); 你需要调用resp.setContentLength(135*1000*1000*1000);

(上面一行)

给它一个旋转。 看看是否有效。 这些是我对理论的猜测。

这是纯粹的推测,但我猜这是某种垃圾收集器问题。 当您运行具有更多堆的JVM时,Java客户端的性能是否会提高… java -Xmx 128m

我不记得JVM开关打开GC日志记录了,但是想一想,看看GC是否正在进入你的doGet。

我的2美分。