代码之家  ›  专栏  ›  技术社区  ›  Steve Buikhuizen

在Javaservlet中流式处理大型文件

  •  38
  • Steve Buikhuizen  · 技术社区  · 18 年前

    最近在负载不足的情况下,我的虚拟机内存用完了,这是在我添加了用于提供图像的代码之后,所以我很确定流式处理较大的servlet响应会给我带来麻烦。

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

    我考虑将文件写入本地临时驱动器,然后生成另一个线程来处理流,以便可以重用TomcatServlet线程。这看起来会很重。

    如有任何想法,将不胜感激。谢谢

    8 回复  |  直到 18 年前
        1
  •  57
  •   John Vasileff    18 年前

    如果可能,您不应该将要提供服务的文件的全部内容存储在内存中。相反,获取数据的InputStream,并将数据分块复制到Servlet OutputStream。例如:

    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();
    

    我同意托比的观点,你应该“把他们指向S3URL”

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

    编辑-关于:

    在这个用例中,图像可以包含敏感数据。。。

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

    1. 用户点击下载链接
    2. 您的webapp生成一个S3URL,其中包含一个密钥,该密钥将在5分钟内过期。
    3. 使用步骤3中的URL向客户端发送HTTP重定向。
    4. 用户从S3下载文件。即使下载时间超过5分钟,这种方法仍然有效——一旦下载开始,它可以一直持续到完成。
        2
  •  17
  •   airportyh    18 年前

    你为什么不直接把它们指向S3URL呢?从S3获取一个工件,然后通过您自己的服务器将其流式传输给我,这与使用S3的目的背道而驰,S3的目的是为了将为Amazon提供图像服务的带宽和处理过程卸载。

        3
  •  11
  •   blast_hardcheese    12 年前

    我见过很多代码,比如john vasilef(目前已被接受)的答案,一个紧密的while循环,从一个流中读取块并将它们写入另一个流。

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

    在下面的代码中,我将在servlet中将一个对象从AmazonS3流式传输到客户机。

    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行定义明确的模式和适当的流关闭似乎相当坚实。

        4
  •  2
  •   Tony BenBrahim    18 年前

    托比是对的,如果可以的话,你应该直接指向S3。如果你不能回答,那么这个问题就有点模糊,无法给出准确的回答: 您的java堆有多大?内存不足时,有多少流同时打开?

    您正在从流中读取8K,然后将8K写入输出,对吗?您不是要从S3读取整个图像,将其缓冲在内存中,然后立即发送整个图像吗?

    如果您使用8K缓冲区,那么在~8meg的堆空间中可能会有1000个并发流,因此您肯定做错了。。。。

        5
  •  2
  •   Stu Thompson Helter Scelter    18 年前

    我非常同意toby和John Vasileff的观点——如果您能够容忍相关问题,S3非常适合卸载大型媒体对象。(我们自己的应用程序的一个实例对10-1000MB FLV和MP4执行此操作。)例如:没有部分请求(字节范围标头)。必须“手动”处理,偶尔停机等。。

    如果这不是一个选项,John的代码看起来不错。我发现2k FILEBUFFERSIZE的字节缓冲区在microbench标记中是最有效的。另一个选项可能是共享文件通道。(文件通道是线程安全的。)

    也就是说,我还要补充一点,猜测导致内存不足错误的原因是一个典型的优化错误。你可以通过努力工作来提高成功的机会。

    1. 将-XX:+HeapDumpOnOutOfMemoryError放入JVM启动参数中,以防万一
    2. 在运行的JVM上使用jmap(
    3. 分析度量(jmap-histooutput,或者让jhat查看堆转储)。很可能是你的记忆不足来自于某个意想不到的地方。

    当然还有其他工具,但是jmap&jhat附带Java 5+“开箱即用”

    我考虑将文件写入本地临时驱动器,然后生成另一个线程来处理流,以便可以重用TomcatServlet线程。这看起来会很重。

    啊,我不认为你做不到。即使你可以,这听起来也很可疑。管理连接的tomcat线程需要控制。如果遇到线程不足,请增加./conf/server.xml中的可用线程数。同样,度量是检测这一点的方法——不要只是猜测。

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

        6
  •  0
  •   Marcio Aguiar    18 年前

    您必须检查两件事:

    • 你要关闭小溪吗?非常重要
    • 也许你是在“免费”提供流连接。数据流不是很大,但是同时有很多数据流可以偷走你所有的内存。创建一个池,使您不能同时运行一定数量的流
        7
  •  0
  •   Johannes Passing    18 年前

    除了John建议的之外,还应该反复刷新输出流。根据web容器的不同,它可能缓存部分甚至所有输出,并立即刷新(例如,计算内容长度标题)。那会烧掉很多记忆。

        8
  •  0
  •   Emil Sit    17 年前

    如果您可以对文件进行结构化,使静态文件彼此独立并位于各自的存储桶中,那么使用Amazon S3 CDN可能会获得当今最快的性能, CloudFront .

    推荐文章