在Java Servlet中流式传输大文件

时间:2020-03-05 18:51:12  来源:igfitidea点击:

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

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

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

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

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

解决方案

回答

我们为什么不只将它们指向S3网址?从S3中获取工件,然后通过我们自己的服务器将其流式传输给我,这使使用S3的目的无法实现,因为S3的工作是卸载带宽,并减少将图像提供给Amazon的过程。

回答

我们必须检查两件事:

  • 我们要关闭流吗?很重要
  • 也许我们是在"免费"提供流连接。流不是很大,但是同时许多流可以窃取所有内存。创建一个池,以使我们不能同时运行一定数量的流

回答

toby是正确的,如果可以的话,我们应该直接指向S3. 如果无法做到,那么这个问题有点含糊,无法给出准确的答案:
Java堆有多大?内存不足时,同时打开多少个流?
读/写缓冲区有多大(8K好)?
我们正在从流中读取8K,然后将8K写入输出中,对吗?我们不是要从S3读取整个图像,将其缓冲在内存中,然后一次发送整个图像吗?

如果使用8K缓冲区,则可能有1000个并发流进入〜8Megs的堆空间中,因此我们肯定做错了...。

顺便说一句,我不是凭空挑出8K的,这是套接字缓冲区的默认大小,发送更多的数据(例如1Meg),我们将在tcp / ip堆栈上阻塞以容纳大量内存。

回答

如果可能,我们不应将要提供的文件的全部内容存储在内存中。取而代之的是,获取数据的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异常的原因。

编辑有关:

In this use case the images can contain sensitive data...

几周前阅读S3文档时,我注意到我们可以生成可以添加到S3 URL的过期密钥。因此,我们不必公开S3上的文件。我对这项技术的理解是:

  • 初始HTML页面具有指向Web应用程序的下载链接
  • 用户点击下载链接
  • 网络应用会生成一个S3 URL,其中包含一个密钥,该密钥将在5分钟内过期。
  • 使用步骤3中的URL将HTTP重定向发送到客户端。
  • 用户从S3下载文件。即使下载时间超过5分钟,此方法仍然有效-下载开始后,它就可以继续完成。

回答

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

回答

我非常同意toby和John Vasileff的观点,如果可以忍受相关问题,S3非常适合卸载大型媒体对象。 (自己的应用程序实例可以处理10-1000MB的FLV和MP4. )例如:不过,没有部分请求(字节范围标头)。必须"手动"处理,偶尔停机等。

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

也就是说,我还要补充一点,猜测导致内存不足错误的原因是经典的优化错误。通过使用严格的指标,我们将提高成功的机会。

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

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

I've considered writing the file to a local temp drive and then spawning another thread to handle the streaming so that the tomcat servlet thread can be re-used. This seems like it would be io heavy.

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

问题:我们还在EC2上运行吗?tomcat的JVM启动参数是什么?

回答

如果我们可以对文件进行结构化,以使静态文件分离并位于各自的存储桶中,则可以通过使用Amazon S3 CDN CloudFront来实现当今最快的性能。