在Java Servlet中流式传输大文件

在Java Servlet中流式传输大文件

Streaming large files in a java servlet

我正在构建需要扩展的Java服务器。 Servlet之一将提供存储在Amazon S3中的图像。

最近在负载下,我的VM内存不足,这是在我添加了用于提供图像的代码之后,因此,我很确定流较大的servlet响应会引起麻烦。

我的问题是:从数据库或其他云存储读取数据时,如何编写Java Servlet以便将大型(> 200k)响应流回浏览器,是否有最佳实践?

我考虑过将文件写入本地临时驱动器,然后生成另一个线程来处理流,以便可以重新使用tomcat servlet线程。 这似乎很沉重。

任何想法将不胜感激。 谢谢。


如果可能,您不应将要提供的文件的全部内容存储在内存中。取而代之的是,获取数据的InputStream,并将数据分段复制到Servlet OutputStream。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ServletOutputStream out = response.getOutputStream();
InputStream in = [ code to get source input stream ];
String mimeType = [ code to get mimetype of data to be served ];
byte[] bytes = new byte[FILEBUFFERSIZE];
int bytesRead;

response.setContentType(mimeType);

while ((bytesRead = in.read(bytes)) != -1) {
    out.write(bytes, 0, bytesRead);
}

// do the following in a finally block:
in.close();
out.close();

我确实同意toby,您应该改为"将它们指向S3 url"。

至于OOM异常,您确定它与提供图像数据有关吗?假设您的JVM具有256MB的"额外"内存,可用于提供图像数据。在Google的帮助下," 256MB / 200KB" =1310。对于2GB的"额外"内存(目前这是一个非常合理的数量),可以支持10,000个并发客户端。即便如此,1300个并发客户端仍然是一个很大的数目。这是您经历过的负载类型吗?如果不是,则可能需要在其他地方查找OOM异常的原因。

编辑-关于:

In this use case the images can contain sensitive data...

几周前阅读S3文档时,我注意到您可以生成可以附加到S3 URL的过期密钥。因此,您不必公开S3上的文件。我对这项技术的理解是:

  • 初始HTML页面具有指向您的Web应用程序的下载链接
  • 用户点击下载链接
  • 您的Web应用程序会生成一个S3 URL,其中包含一个密钥,该密钥将在5分钟内过期。
  • 使用步骤3中的URL将HTTP重定向发送到客户端。
  • 用户从S3下载文件。即使下载时间超过5分钟,此方法仍然有效-下载开始后,它就可以继续完成。

  • 您为什么不只将它们指向S3网址?从S3中获取工件,然后通过您自己的服务器将其流式传输给我,这使使用S3的目的无法实现,因为S3的工作是卸载带宽并将图像处理服务交付给Amazon。


    我已经看到了很多代码,例如john-vasilef的(当前接受的)答案,在紧紧的循环中从一个流中读取块并将它们写入另一流中。

    我要提出的观点是反对不必要的代码重复,而赞成使用Apache的IOUtils。如果您已经在其他地方使用过它,或者如果您正在使用的另一个库或框架已经依赖于它,那么这是一条已知且经过良好测试的行。

    在以下代码中,我正在将对象从Amazon S3流传输到Servlet中的客户端。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import java.io.InputStream;
    import java.io.OutputStream;
    import org.apache.commons.io.IOUtils;

    InputStream in = null;
    OutputStream out = null;

    try {
        in = object.getObjectContent();
        out = response.getOutputStream();
        IOUtils.copy(in, out);
    } finally {
        IOUtils.closeQuietly(in);
        IOUtils.closeQuietly(out);
    }

    6行定义明确的模式,具有正确的流关闭效果,看起来非常可靠。


    我非常同意toby和John Vasileff的看法,如果可以忍受相关问题,S3非常适合卸载大型媒体对象。 (自己的应用程序实例可以处理10-1000MB的FLV和MP4。)例如:不过,没有部分请求(字节范围标头)。必须"手动"处理,偶尔停机等。

    如果那不是一个选择,John的代码看起来不错。我发现2k FILEBUFFERSIZE的字节缓冲区在微基准标记中是最有效的。另一个选项可能是共享的FileChannel。 (FileChannel是线程安全的。)

    也就是说,我还要补充一点,猜测造成内存不足错误的原因是经典的优化错误。通过使用严格的指标,您将提高成功的机会。

  • 以防万一-XX:+ HeapDumpOnOutOfMemoryError放入您的JVM启动参数中
  • 在负载下在运行的JVM(jmap -histo )上使用jmap
  • 分析指标(jmap -histo输出,或让jhat查看堆转储)。很有可能是您的内存不足来自意外的地方。
  • 当然还有其他工具,但是Java 5+附带的jmap和jhat都是"开箱即用"的

    I've considered writing the file to a local temp drive and then spawning another thread to handle the streaming so that the tomcat servlet thread can be re-used. This seems like it would be io heavy.

    啊,我不认为你不能那样做。即使可以,听起来也很可疑。管理连接的tomcat线程需要控制。如果遇到线程不足,请增加./conf/server.xml中的可用线程数。同样,指标是检测到此问题的方法-不仅仅是猜测。

    问题:您还在EC2上运行吗?您的tomcat的JVM启动参数是什么?


    toby是正确的,如果可以的话,您应该直接指向S3。如果您不能这样做,那么这个问题可能会有点含糊,无法给出准确的答案:
    您的Java堆有多大?内存不足时,同时打开多少个流?
    您的读/写缓冲区有多大(8K好)?
    您正在从流中读取8K,然后将8K写入输出中,对吗?您不是要从S3读取整个图像,将其缓冲在内存中,然后一次发送整个图像吗?

    如果使用8K缓冲区,则可能有1000个并发流进入?8Megs的堆空间,因此您肯定做错了...。

    顺便说一句,我并不是凭空挑出8K的,这是套接字缓冲区的默认大小,发送更多的数据(例如1Meg),您将在tcp / ip堆栈上阻塞以容纳大量内存。


    如果您可以对文件进行结构化,以使静态文件分离并位于各自的存储桶中,则可以通过使用Amazon S3 CDN CloudFront来实现当今最快的性能。


    除了John建议的内容之外,您还应该重复刷新输出流。根据您的Web容器,它可能会缓存部分甚至全部输出,并一次刷新一次(例如,计算Content-Length标头)。那会消耗很多内存。


    您必须检查两件事:

    • 您要关闭流吗?很重要
    • 也许您是"免费"提供流连接。流不是很大,但同时许多流可以窃取您的所有内存。创建一个池,以使您不能同时运行一定数量的流

    推荐阅读