2008-09-11 24 views
36

我正在构建一个需要扩展的java服务器。其中一个servlet将提供存储在Amazon S3中的图像。在java servlet中流式传输大文件

最近在加载的时候,我的虚拟机内存耗尽,当时我添加了代码来提供图像,所以我非常肯定,流式传输更大的servlet响应会导致我的麻烦。

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

我曾考虑将文件写入本地临时驱动器,然后产生另一个线程来处理流,以便tomcat servlet线程可以重新使用。这看起来好像会很重。

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

回答

47

如果可能,您不应该将要提供的文件的全部内容存储在内存中。相反,获取数据的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上向公众开放这些文件。我对技术的理解是:

  1. 初始HTML页面有下载链接到你的web应用下载链接
  2. 你的web应用程序生成包括到期的一个关键的S3 URL上
  3. 用户点击,可以说, 5分钟。
  4. 使用步骤3中的URL发送HTTP重定向到客户端。
  5. 用户从S3下载文件。即使下载时间超过5分钟,这也可以工作 - 一旦下载开始,它可以继续完成。
+0

嗯,由于没有设置内容长度,所以servlet容器必须进行缓冲,因为在流式传输任何数据之前需要设置内容长度头。所以不知道你节省了多少内存? – 2012-02-01 15:54:36

+1

彼得,如果你不能直接指向用户的云服务URL,并且你想设置内容长度头,并且你不知道大小,并且你不能查询云服务的大小,那么我猜你的最好的办法是首先流到服务器上的临时文件。当然,在将第一个字节发送到客户端之前,在服务器上保存副本可能会导致用户认为请求失败,具体取决于云 - >服务器传输需要多长时间。 – 2012-02-13 03:55:37

17

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

0

你必须检查两件事情:

  • 你关闭流?非常重要的
  • 也许你正在给流连接“免费”。这个流不是很大,但同时有许多流可以窃取你所有的记忆。创建一个池,以便您不能同时运行一定数量的流
1

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

如果使用8K的缓冲区,可以有1000个并发流中的堆空间〜8Megs去,所以你肯定做错了什么....

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

0

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

2

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

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

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

  1. 将-XX:+ HeapDumpOnOutOfMemoryError到您JVM启动参数,以防万一
  2. 荷载作用下运行的JVM( JMAP -histo <PID>
  3. 采用开放JMAP
  4. Analyize指标( jmap -histo out,或者让jhat看看你的堆转储)。很可能你的内存不足来自意想不到的地方。

当然还有其他的工具在那里,但JMAP &来与jHat与Java 5+“开箱即用”

我已经考虑文件写入到本地临时驱动器,然后产生另一个线程来处理流,以便tomcat servlet线程可以被重用。这看起来好像会很重。

啊,我认为你不能这样做。即使你可以,这听起来很可疑。正在管理连接的tomcat线程需要控制。如果遇到线程匮乏,则增加./conf/server.xml中可用线程的数量。同样,指标是检测这种情况的方式 - 不要猜测。

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

0

如果是这样的静态文件是独立的,在自己的桶,你可以组织你的文件,今天最快的性能可能可以使用Amazon S3的CDN,CloudFront实现。

10

我见过很多像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行定义良好的模式似乎非常稳固。