【问题标题】:Slow transfers in Jetty with chunked transfer encoding at certain buffer sizeJetty 中的慢速传输,具有特定缓冲区大小的分块传输编码
【发布时间】:2012-01-27 09:34:56
【问题描述】:

我正在调查 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 编写的简单测试客户端(使用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 任务 shuffle,它使用 Jetty 传输文件的方式与缩减的示例代码非常相似,缓冲区大小为 64KB。

该问题在我们的 Linux (Debian) 服务器和我的 Windows 机器上以及 Java 1.6 和 1.7 上都重现,因此它似乎完全取决于 Jetty。

有谁知道这可能是什么原因造成的,如果有什么我可以做的吗?

【问题讨论】:

  • +1。我也观察到了这一点。但并没有真正得到好的解决方案。

标签: java hadoop jetty


【解决方案1】:

我相信我已经通过查看 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 的调用被删除时,它会让缓冲区填满,因为 24 是 4 的倍数,所以 12 字节再次溢出到下一个块中。

总结

看来,要使用 Jetty 获得良好的性能,您必须:

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

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

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

【讨论】:

  • 我怀疑您是否在码头团队中找到任何对 Jetty 6 的错误报告特别敏感的人。但如果同样的问题存在于 Jetty 7 或 8 中,那么我们将非常感谢您提供错误报告。
【解决方案2】:

是的,如果无法确定响应的大小,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)现在都很快。另请参阅问题的更新。
  • 实际上,当且仅当传递给outStream.write的缓冲区大小小于24KB(默认响应缓冲区大小)时,默认缓冲区大小似乎很快。
  • 忘记了我在没有分块的情况下评论了上面的刷新(设置内容长度)
  • 如何让 Jetty 拒绝使用 Transfer-Encoding:Chunked 默认情况下。有些客户端是古老的,它们需要“Content-Length”标头作为响应
【解决方案3】:

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

我不记得打开 GC 日志记录的 JVM 开关,但要弄清楚,看看 GC 是否在您进入 doGet 时启动。

我的 2 美分。

【讨论】:

  • 重新考虑...128m 似乎是默认值。所以我的建议不会有帮助。我会被否决而被遗忘。哦,人类。
  • 这些人说自己管理缓冲区大小可能不是一个好主意:stackoverflow.com/questions/4638974/…
  • 然而,在HttpServletResponse 上使用默认缓冲区大小实际上给了我最差的性能。
猜你喜欢
  • 2021-05-01
  • 2010-09-21
  • 2015-11-06
  • 2023-03-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多