在Java Servlet中stream式传输大文件

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

最近在负载下,我的虚拟机内存不足,而且在我添加代码来提供图像之后,我非常肯定,stream式传输较大的servlet响应正在导致我的麻烦。

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

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

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

如果可能,您不应该将要提供的文件的全部内容存储在内存中。 相反,获取数据的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,你应该“指向他们的S3url”。

至于OOM例外情况,你确定它与提供图像数据有关吗? 假设您的JVM有256MB的“额外”内存用于提供图像数据。 在Google的帮助下,“256MB / 200KB”= 1310.对于2GB“额外”内存(现在非常合理的数量),可以支持超过10,000个并发客户端。 即使如此,1300个同时在线的客户也是相当多的。 这是你经历的负荷types吗? 如果没有,您可能需要在别处寻找OOMexception的原因。

编辑 – 关于:

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

几个星期前,当我通读S3文档时,发现可以生成可以连接到S3 URL的超时键。 所以,你不必打开S3上的文件给公众。 我对这种技术的理解是:

  1. 最初的HTML页面有下载链接到您的Web应用程序
  2. 用户点击下载链接
  3. 您的webapp会生成一个S3 URL,其中包含一个过期的密钥,例如5分钟。
  4. 使用步骤3中的URL将HTTPredirect发送到客户端。
  5. 用户从S3下载文件。 即使下载时间超过5分钟,这也是有效的 – 一旦下载开始,它可以继续完成。

你为什么不把他们指向S3的url? 从S3中获取一个工件,然后通过自己的服务器将其stream式传输给我,这不利于使用S3的目的,即将带宽和处理服务的图像分stream到Amazon。

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

我所做的观点是反对不必要的代码重复,赞成使用Apache的IOUtils。 如果您已经在其他地方使用它,或者您正在使用的另一个库或框架已经在使用它,那么这是一条已知并经过良好testing的单一行。

在下面的代码中,我将一个对象从Amazon S3stream式传输到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); } 

一个明确的模式,适当的streamclosures6行看起来很稳固。

我非常同意toby和John Vasileff – 如果你可以容忍相关的问题,那么S3非常适合卸载大型媒体对象。 (一个自己的应用程序的实例为10-1000MB的FLV和MP4做这个。)例如:没有部分请求(字节范围头),但。 人们必须处理“手动”,偶尔停机等。

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

也就是说,我还补充说,猜测导致内存不足错误的原因是一个典型的优化错误。 你会通过严格的指标来提高成功的机会。

  1. 将-XX:+ HeapDumpOnOutOfMemoryError放入您的JVM启动参数中,以防万一
  2. 在负载下使用运行的JVM( jmap -histo <pid> )使用jmap
  3. 分析指标(jmap -histo out,或者让jhat看看你的堆转储)。 这很可能是你的记忆失忆来自意想不到的地方。

当然还有其他的工具,但是jmap和jhat随附Java 5+,

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

啊,我不认为你不能这样做。 即使你可以,这听起来可疑。 正在pipe理连接的tomcat线程需要控制。 如果您遇到线程匮乏,则增加./conf/server.xml中可用线程的数量。 同样,度量是检测这个的方法 – 不要猜测。

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

托比是正确的,你应该直接指向S3,如果可以的话。 如果你不能,那么这个问题有一点含糊不清:你的Java堆有多大? 当内存不足时,同时打开多less个数据stream?
读写/缓冲有多大(8K好)?
你正在从stream中读取8K,然后写8K到输出,对不对? 你不是试图从S3读取整个图像,将它caching在内存中,然后立即发送整个图像?

如果你使用8K缓冲区,你可能有1000个并发数据stream进入〜8M的堆空间,所以你肯定是做错了事情….

顺便说一句,我没有select8K的空气稀薄,这是套接字缓冲区的默认大小,发送更多的数据,比如1Meg,并且你将阻止在tcp / ip堆栈中保存大量的内存。

你必须检查两件事情:

  • 你正在closuresstream? 很重要
  • 也许你是“免费”的stream连接。 stream量不大,但同时有许多stream可以窃取你所有的记忆。 创build一个池,以便您不能同时运行一定数量的stream

除了Johnbuild议的之外,你应该反复刷新输出stream。 根据您的Web容器,可能会caching部分甚至全部输出,并一次性刷新它(例如,计算Content-Length标头)。 那会烧掉很多的记忆。

如果您可以构build文件以使静态文件分离并存储在自己的存储桶中,则可以通过使用Amazon S3 CDN CloudFront实现当前最快的性能。