44

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

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

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

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

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

4

8 回答 8

58

如果可能,您不应将要提供的文件的全部内容存储在内存中。相反,获取数据的 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 页面包含指向您的 webapp 的下载链接
  2. 用户点击下载链接
  3. 您的 web 应用程序会生成一个 S3 URL,其中包含一个在 5 分钟后到期的密钥。
  4. 使用步骤 3 中的 URL 向客户端发送 HTTP 重定向。
  5. 用户从 S3 下载文件。即使下载需要超过 5 分钟,这也有效 - 一旦下载开始,它可以继续完成。
于 2008-09-11T03:53:45.523 回答
17

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

于 2008-09-11T02:45:02.617 回答
11

我见过很多代码,例如 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 行定义明确的模式以及适当的流关闭看起来非常扎实。

于 2014-04-23T18:31:36.330 回答
2

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

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

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

于 2008-09-11T03:38:08.917 回答
2

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

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

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

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

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

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

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

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

于 2008-09-11T06:24:24.727 回答
0

你必须检查两件事:

  • 你要关闭流吗?很重要
  • 也许您正在“免费”提供流连接。流并不大,但同时很多很多流可以窃取你所有的内存。创建一个池,这样您就不能同时运行一定数量的流
于 2008-09-11T03:23:16.673 回答
0

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

于 2008-09-11T06:16:03.170 回答
0

如果您可以构建文件以使静态文件独立且位于各自的存储桶中,则可以通过使用 Amazon S3 CDN CloudFront来实现当今最快的性能。

于 2009-10-01T15:23:14.750 回答