在 java servlet 中流式传输大文件

Posted

技术标签:

【中文标题】在 java servlet 中流式传输大文件【英文标题】:Streaming large files in a java servlet 【发布时间】:2010-09-08 12:22:04 【问题描述】:

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

最近在负载下,我的虚拟机内存不足,这是在我添加代码以提供图像之后,所以我很确定流较大的 servlet 响应会导致我的麻烦。

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

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

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

【问题讨论】:

【参考方案1】:

如果可能,您不应将要提供的文件的全部内容存储在内存中。相反,获取数据的 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();

我同意 toby 的观点,您应该改为“将它们指向 S3 url。”

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

编辑 - 关于:

在此用例中,图像可能包含敏感数据...

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

    初始 html 页面包含指向您的 web 应用的下载链接 用户点击下载链接 您的 Web 应用会生成一个 S3 URL,其中包含一个在 5 分钟后到期的密钥。 使用步骤 3 中的 URL 向客户端发送 HTTP 重定向。 用户从 S3 下载文件。即使下载时间超过 5 分钟,这也能正常工作 - 一旦下载开始,它可以继续完成。

【讨论】:

嗯,由于没有设置内容长度,servlet 容器必须缓冲,因为它需要设置内容长度标头才能流式传输任何数据。所以不确定你节省了多少内存? Peter,如果您无法直接将用户指向云服务 URL,并且您想要设置内容长度标头,并且您还不知道大小,并且您无法查询云服务大小,那么我想你最好的选择是首先流式传输到服务器上的临时文件。当然,在向客户端发送第一个字节之前在服务器上保存一个副本可能会导致用户认为请求失败,具体取决于云 -> 服务器传输需要多长时间。 @PeterKriens content-length 标头不是强制性的。此外,您可以在只需要指定块长度的情况下使用分块传输。 当 servlet 设置 Content-Length 标头时,可以立即发送输出而无需任何中间缓冲区。否则服务器必须缓冲或实现分块传输。一般来说,您应该能够通过 HEAD 从 blob 存储中获取所需的信息。【参考方案2】:

为什么不直接将它们指向 S3 url?从 S3 获取工件,然后通过您自己的服务器将其流式传输给我,这违背了使用 S3 的目的,即将带宽和处理图像提供给 Amazon。

【讨论】:

指向 s3 url - 我假设你说的只是给浏览器一个 s3 url 开始。如果您的图像或视频是医学文物并且是敏感的怎么办? S3 确实支持过期网址。但是您不能通过电子邮件发送即将到期的 URL?一个尚未过期的 url 仍然可以被历史上的其他人使用,所有这些都是不安全的,例如医疗保健产品。【参考方案3】:

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

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

在以下代码中,我将一个对象从 Amazon S3 流式传输到 servlet 中的客户端。

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 行定义明确的模式以及适当的流关闭看起来非常可靠。

【讨论】:

我同意使用可用的,但是您的代码有问题:如果response.getOutputStream() 生成异常,您的InputStream in 对象将不会关闭。 Java 7 的 try-with-resources 特性现在应该是模式,未来的 CommonsIO 将有 this。漂亮的帽子:) @Evandro 很好——这个怎么样? (另外,谢谢:) closeQuitely 现在已被弃用。 commons.apache.org/proper/commons-io/apidocs/org/apache/commons/…【参考方案4】:

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

如果您使用 8K 缓冲区,则可能有 1000 个并发流在约 8Megs 的堆空间中进入,所以您肯定做错了什么......

顺便说一句,我不是凭空选择 8K,它是套接字缓冲区的默认大小,发送更多数据,比如 1Meg,你将阻塞持有大量内存的 tcp/ip 堆栈。

【讨论】:

你说的是什么意思 - 直接指向 S3。您的意思是将 S3 url 传递给浏览器以便他们可以流式传输它吗?【参考方案5】:

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

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

也就是说,我还要补充一点,猜测导致内存不足错误的原因是典型的优化错误。使用硬指标可以提高成功的机会。

    将 -XX:+HeapDumpOnOutOfMemoryError 放入 JVM 启动参数中,以防万一 在负载下运行的 JVM (jmap -histo ) 上使用 jmap 分析指标(jmap -histo 输出,或让 jhat 查看您的堆转储)。很可能您的内存不足来自意外的地方。

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

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

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

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

【讨论】:

【参考方案6】:

你必须检查两件事:

您要关闭流吗?非常重要 也许您正在“免费”提供流连接。流并不大,但同时很多很多流可以窃取你所有的内存。创建一个池,这样您就不能同时运行一定数量的流

【讨论】:

【参考方案7】:

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

【讨论】:

【参考方案8】:

如果您可以对文件进行结构化,以便将静态文件分开并放在各自的存储桶中,那么使用 Amazon S3 CDN CloudFront 可能会实现当今最快的性能。

【讨论】:

以上是关于在 java servlet 中流式传输大文件的主要内容,如果未能解决你的问题,请参考以下文章

在 ASP.NET 中流式传输大文件上传

java处理大文本方案

如何在 PowerShell 中使用 XmlReader 流式传输大/巨大的 XML 文件?

用于 chrome 中 html5 视频的 Servlet 流式传输 mp4

Groovy Grails,如何在控制器的响应中流式传输或缓冲大文件?

Spring WebClient:如何将大字节 [] 流式传输到文件?