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

18

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

我创建了一个小的Jetty测试应用程序,其中包含一个演示此问题的单个servlet。

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返回。使用Java编写的简单测试客户端(使用URLConnection)下载该数据需要3.8秒,这非常慢(是的,它的速度是33MB / s,听起来不慢,但这是纯粹的本地和输入文件已缓存; 它应该更快)。
现在问题出现了。如果我使用HTTP / 1.0客户端wget下载数据,因此不支持分块传输编码,则只需0.1秒即可完成。 这是一个更好的数字。
现在,当我将bufferSize 更改为4096时,Java客户端需要0.3秒。
如果我完全删除对resp.setBufferSize 的调用(似乎使用24KB块大小),那么Java客户端现在需要7.1秒,并且wget突然也变得同样慢!
请注意,我绝不是Jetty的专家。在诊断Hadoop 0.20.203.0中reduce任务洗牌的性能问题时,我偶然发现了这个问题。Hadoop在Jetty中以与简化版代码类似的方式传输文件,使用64KB缓冲区大小。
该问题在我们的Linux(Debian)服务器和我的Windows机器上都会出现,并且在Java 1.6和1.7上均表现出来,因此似乎仅依赖于Jetty。
有人知道是什么原因引起的吗?是否有什么我可以做的?

1
+1. 我也注意到了这个问题。但是并没有找到一个好的解决方案。 - Thomas Jungblut
3个回答

15

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

如果与outStream.write一起使用的缓冲区大小等于响应缓冲区的大小(我认为多个也可以),或者小于响应缓冲区并且使用outStream.flush,那么性能就很好。每个write调用都会直接刷新到输出中,这很好。但是,当写入缓冲区较大且不是响应缓冲区的倍数时,似乎会导致处理刷新的某些怪异行为,导致额外的刷新,从而导致性能下降。

在块传输编码的情况下,有一个额外的变化。对于除第一个块以外的所有块,Jetty保留12个字节的响应缓冲区来包含块大小。这意味着在我最初的例子中,使用64KB写入和响应缓冲区的实际数据量只有65524字节,因此,写入缓冲区的某些部分会溢出到多个刷新中。查看捕获的网络跟踪记录,我发现第一个块是64KB,但所有后续块都是65524字节。在这种情况下,outStream.flush没有任何影响。

当使用4KB缓冲区时,只有在调用outStream.flush时才能看到快速速度。事实证明,resp.setBufferSize只会增加缓冲区大小,而默认大小为24KB,resp.setBufferSize(4096)是无效操作。然而,现在我正在写入4KB数据块,这些数据块即使包括保留的12字节也适合24KB缓冲区,然后由outStream.flush调用作为4KB块刷新。但是,当删除对flush的调用时,它将允许缓冲区填满,再次溢出12字节到下一个块中,因为24是4的倍数。
总之,似乎要在Jetty中获得良好的性能,您必须:
- 在调用setContentLength(未分块传输编码)时,并使用与响应缓冲区大小相同的缓冲区进行write。 - 当使用分块传输编码时,使用比响应缓冲区大小小至少12字节的写缓冲区,并在每次写入后调用flush
请注意,“慢”场景的性能仍然可以让您看到差异,但可能只在本地主机或非常快速(1Gbps或更高)的网络连接上。我想我应该对Hadoop和/或Jetty提出问题报告。

2
我怀疑在Jetty 6的团队中找不到任何人对错误报告特别敏感。但是,如果相同的问题存在于Jetty 7或8中,则高度赞赏错误报告。 - Tim

1

是的,如果无法确定响应大小,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);

(上面那行代码)

试一下,看看是否有效。 这些都是我从理论上猜测的。


感谢您的回复。resp.setContentLength 没有 resp.setBufferSize 仍然很慢。但是,使用 resp.setContentLength 后,我尝试的两个缓冲区大小(64KB 和 4KB)现在都很快。还请查看问题的更新。 - Sven
实际上,如果传递给outStream.write的缓冲区大小小于24KB(默认响应缓冲区大小),则默认缓冲区大小才快。 - Sven
忘记了在没有分块的情况下(设置内容长度),我已经注释掉了上面的刷新。 - manocha_ak
如何让Jetty默认拒绝使用Transfer-Encoding:Chunked。一些客户端比较老旧,需要在响应中包含“Content-Length”头信息。 - 4ntoine

0

这只是一种猜测,但我猜测这可能是某种垃圾回收器问题。当您使用更多堆运行JVM时,Java客户端的性能是否会提高,例如... java -Xmx 128m

我不记得打开GC日志记录的JVM开关,但找出来看看是否在进入doGet时启动了GC。

以上仅供参考。


转念一想......128m 似乎是默认值。所以我的建议不会有帮助的。我将被投票淹没。哦,人性啊。 - Bob Kuhar
这些人认为自己管理缓冲区大小可能不是一个好主意:https://dev59.com/72445IYBdhLWcg3w6eRo - Bob Kuhar
然而,实际上使用HttpServletResponse的默认缓冲区大小给我带来了最差的性能。 - Sven

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接