Jetty中的缓慢传输与特定缓冲区大小的分块传输编码

Sve*_*ven 17 java hadoop 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();
    }

}
Run Code Online (Sandbox Code Playgroud)

在我的实验中,我使用了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.

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

Sve*_*ven 13

我相信通过查看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提交问题报告.

  • 我怀疑你是否发现码头团队中的任何人对Jetty 6的错误报告特别敏感.但如果Jetty 7或8中存在同样的问题,那么错误报告将受到高度赞赏. (2认同)