Использование ServletOutputStream для записи очень больших файлов в сервлете Java без проблем с памятью
Я использую IBM Websphere Application Server v6 и Java 1.4 и пытаюсь записать большие файлы CSV в ServletOutputStream
для загрузки пользователем. В настоящее время размер файлов составляет 50-750 МБ.
Меньшие файлы не вызывают слишком много проблем, но с более крупными файлами кажется, что он записывается в кучу, которая затем вызывает ошибку OutOfMemory и сбрасывает весь сервер.
Эти файлы могут обслуживаться только для аутентифицированных пользователей через HTTPS, поэтому я обслуживаю их через Servlet, а не просто вставляя их в Apache.
Используемый мной код (некоторый пух удаляется вокруг этого):
resp.setHeader("Content-length", "" + fileLength);
resp.setContentType("application/vnd.ms-excel");
resp.setHeader("Content-Disposition","attachment; filename=\"export.csv\"");
FileInputStream inputStream = null;
try
{
inputStream = new FileInputStream(path);
byte[] buffer = new byte[1024];
int bytesRead = 0;
do
{
bytesRead = inputStream.read(buffer, offset, buffer.length);
resp.getOutputStream().write(buffer, 0, bytesRead);
}
while (bytesRead == buffer.length);
resp.getOutputStream().flush();
}
finally
{
if(inputStream != null)
inputStream.close();
}
FileInputStream
, похоже, не вызывает проблемы, так как если я пишу в другой файл или просто полностью удаляю запись, использование памяти не представляется проблемой.
Я думаю, что resp.getOutputStream().write
хранится в памяти до тех пор, пока данные не будут отправлены клиенту. Таким образом, весь файл может быть прочитан и сохранен в resp.getOutputStream()
, что вызовет проблемы с памятью и сбой!
Я попытался Буферизировать эти потоки, а также попытался использовать Каналы из java.nio
, ни одна из которых, похоже, не имеет никакого отношения к проблемам с моей памятью. Я также сбросил OutputStream
один раз за итерацию цикла и после цикла, что не помогло.
Ответы
Ответ 1
Средний порядочный servletcontainer сам по-прежнему пропускает поток по умолчанию каждые ~ 2KB. На самом деле вам не нужно явно вызывать flush()
в OutputStream
HttpServletResponse
с интервалами, последовательно передавая данные из одного и того же источника. Например, Tomcat (и Websphere!) Настраивается как bufferSize
атрибут HTTP-коннектора.
Средний приличный контент-сервер также просто передает данные в фрагменты, если длина содержимого неизвестна заранее (согласно API-интерфейс сервлета!), и если клиент поддерживает HTTP 1.1.
Симптомы проблемы, по крайней мере, указывают на то, что servletcontainer буферизует весь поток в памяти перед промывкой. Это может означать, что заголовок длины содержимого не установлен и/или сервлетконтейнер не поддерживает кодирование с чередованием, и/или клиентская сторона не поддерживает кодирование с чередованием (то есть использует HTTP 1.0).
Чтобы исправить тот или иной, просто установите длину содержимого заранее:
response.setHeader("Content-Length", String.valueOf(new File(path).length()));
Ответ 2
Работает ли flush
на выходном потоке.
В самом деле, я хотел бы прокомментировать, что вы должны использовать трехмерную форму записи, поскольку буфер не обязательно полностью читается (особенно в конце файла (!)). Кроме того, попытка/наконец будет в порядке, если вы не хотите, чтобы сервер неожиданно умер.
Ответ 3
Я использовал класс, который обертывает выходной поток, чтобы сделать его многоразовым в других контекстах. Он работал хорошо для меня в получении данных в браузере быстрее, но я не смотрел на последствия памяти. (прошу простить мое устаревшее имя переменной m_)
import java.io.IOException;
import java.io.OutputStream;
public class AutoFlushOutputStream extends OutputStream {
protected long m_count = 0;
protected long m_limit = 4096;
protected OutputStream m_out;
public AutoFlushOutputStream(OutputStream out) {
m_out = out;
}
public AutoFlushOutputStream(OutputStream out, long limit) {
m_out = out;
m_limit = limit;
}
public void write(int b) throws IOException {
if (m_out != null) {
m_out.write(b);
m_count++;
if (m_limit > 0 && m_count >= m_limit) {
m_out.flush();
m_count = 0;
}
}
}
}
Ответ 4
Я также не уверен, что в этом случае работает flush()
on ServletOutputStream
, но ServletResponse.flushBuffer()
должен отправить ответ клиенту (по крайней мере, на 2.3 спецификации сервлета).
ServletResponse.setBufferSize()
звучит многообещающе.
Ответ 5
Итак, следуя вашему сценарию, разве вы не должны быть скрыты внутри этого цикла while (на каждой итерации), а не вне его? Я бы попробовал это с небольшим большим буфером.
Ответ 6
-
Класс Kevin должен закрыть поле m_out
, если он не равен null в операторе close(), мы не хотим утечки вещей, не так ли?
-
Как и оператор ServletOutputStream.flush()
, операция HttpServletResponse.flushBuffer()
может также очищать буферы. Тем не менее, это, по-видимому, специфическая для реализации информация о том, действуют ли эти операции или нет, или поддерживает ли поддержка длины содержимого HTML. Помните, что указать длину контента - это вариант для HTTP 1.0, поэтому все должно быть просто потоковым, если вы промойте вещи. Но я не вижу, чтобы
Ответ 7
Условие while не работает, вам нужно проверить -1 перед использованием. И, пожалуйста, используйте временную переменную для выходного потока, ее лучше читать и безопасно вызывать метод getOutputStream().
OutputStream outStream = resp.getOutputStream();
while(true) {
int bytesRead = inputStream.read(buffer);
if (bytesRead < 0)
break;
outStream.write(buffer, 0, bytesRead);
}
inputStream.close();
out.close();
Ответ 8
не связанный с вашими проблемами памяти, цикл while должен быть:
while(bytesRead > 0);
Ответ 9
ваш код имеет бесконечный цикл.
do
{
bytesRead = inputStream.read(buffer, offset, buffer.length);
resp.getOutputStream().write(buffer, 0, bytesRead);
}
while (bytesRead == buffer.length);
<Сильное > смещение имеет то же значение, что и в цикле, поэтому, если изначально offset = 0, оно останется таким же на каждой итерации, что вызовет бесконечный цикл и приведет к к ошибке OOM.
Ответ 10
Сервер приложений Websphere Ibm использует асинхронную передачу данных для сервлетов по умолчанию. Это означает, что он буферизует ответ. Если у вас возникли проблемы с большими данными и исключениями OutOfMemory, попробуйте изменить настройки в WAS для использования синхронного режима.
Установка WebCphere Application Server WebContainer в синхронный режим
Вы также должны позаботиться о загрузке кусков и промыть их.
Образец для загрузки из большого файла.
ServletOutputStream os = response.getOutputStream();
FileInputStream fis = new FileInputStream(file);
try {
int buffSize = 1024;
byte[] buffer = new byte[buffSize];
int len;
while ((len = fis.read(buffer)) != -1) {
os.write(buffer, 0, len);
os.flush();
response.flushBuffer();
}
} finally {
os.close();
}