在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上的文件给公众。 我对这种技术的理解是:
- 最初的HTML页面有下载链接到您的Web应用程序
- 用户点击下载链接
- 您的webapp会生成一个S3 URL,其中包含一个过期的密钥,例如5分钟。
- 使用步骤3中的URL将HTTPredirect发送到客户端。
- 用户从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是线程安全的。)
也就是说,我还补充说,猜测导致内存不足错误的原因是一个典型的优化错误。 你会通过严格的指标来提高成功的机会。
- 将-XX:+ HeapDumpOnOutOfMemoryError放入您的JVM启动参数中,以防万一
- 在负载下使用运行的JVM( jmap -histo <pid> )使用jmap
- 分析指标(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实现当前最快的性能。