Jetty 中的慢速传输,在特定缓冲区大小下使用分块传输编码

2022-09-02 22:07:16

我正在调查 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 将该文件返回到客户端,客户端使用 localhost 进行连接。使用用Java编写的简单测试客户端下载此数据需要3.8秒,这是非常慢的(是的,它是33MB / s,这听起来并不慢,除了这是纯粹的本地并且输入文件被缓存;它应该快得多)。URLConnection

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

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

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

请注意,我绝不是Jetty的专家。我在诊断Hadoop 0.20.203.0中的性能问题时偶然发现了这个问题,该问题具有减少任务洗牌功能,该问题使用Jetty以类似于简化的示例代码的方式传输文件,缓冲区大小为64KB。

这个问题在我们的Linux(Debian)服务器和我的Windows机器上都重现,并且在Java 1.6和1.7上都重现,因此它似乎完全依赖于Jetty。

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


答案 1

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

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

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

使用4KB缓冲区时,我只在调用时看到快速速度。事实证明,这只会增加缓冲区大小,并且由于默认大小为24KB,因此是no-op。但是,我现在写入的是 4KB 的数据片段,即使保留了 12 个字节,这些数据也适合 24KB 缓冲区,然后通过调用将其刷新为 4KB 块。但是,当删除对 的调用时,它会让缓冲区填满,再次有 12 个字节溢出到下一个块中,因为 24 是 4 的倍数。outStream.flushresp.setBufferSizeresp.setBufferSize(4096)outStream.flushflush

结语

似乎要获得Jetty的良好性能,您必须:

  • 调用时(无分块传输编码),并使用与响应缓冲区大小相同的缓冲区。setContentLengthwrite
  • 使用分块传输编码时,请使用至少比响应缓冲区大小小 12 个字节的写入缓冲区,并在每次写入后进行调用。flush

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

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


答案 2

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

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

resp.setBufferSize();

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

在打开输出流之前,即在此行之前:您需要调用OutputStream outStream = resp.getOutputStream();resp.setContentLength(135*1000*1000*1000);

(上面的一行)

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


推荐