Потоковая передача больших файлов в Java-сервлете

StackOverflow https://stackoverflow.com/questions/55709

  •  09-06-2019
  •  | 
  •  

Вопрос

Я создаю Java-сервер, который необходимо масштабировать.Один из сервлетов будет обслуживать изображения, хранящиеся в Amazon S3.

Недавно под нагрузкой у меня закончилась память в моей виртуальной машине, и это произошло после того, как я добавил код для обслуживания изображений, поэтому я почти уверен, что потоковая передача более крупных ответов сервлета вызывает мои проблемы.

Мой вопрос:Есть ли какая-либо передовая практика в том, как закодировать Java-сервлет для потоковой передачи большого (> 200 КБ) ответа обратно в браузер при чтении из базы данных или другого облачного хранилища?

Я рассматривал возможность записи файла на локальный временный диск, а затем создания другого потока для обработки потоковой передачи, чтобы поток сервлета Tomcat можно было повторно использовать.Кажется, это будет очень тяжело.

Любые мысли будут оценены по достоинству.Спасибо.

Это было полезно?

Решение

По возможности не следует хранить в памяти все содержимое файла, который будет обслуживаться.Вместо этого получите входной поток для данных и скопируйте данные в выходной поток сервлета по частям.Например:

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();

Я согласен с Тоби, вместо этого вам следует «указать им URL-адрес S3».

Что касается исключения OOM, вы уверены, что оно связано с обслуживанием данных изображения?Допустим, ваша JVM имеет 256 МБ «дополнительной» памяти, которую можно использовать для обслуживания данных изображения.С помощью Google «256МБ/200КБ» = 1310.При наличии 2 ГБ «дополнительной» памяти (в наши дни это очень разумное количество) можно поддерживать более 10 000 одновременных клиентов.Несмотря на это, 1300 одновременных клиентов — это довольно большое число.Это тот тип нагрузки, который вы испытали?В противном случае вам, возможно, придется поискать причину исключения OOM в другом месте.

Изменить. Относительно:

В этом случае изображения могут содержать конфиденциальные данные...

Когда я прочитал документацию S3 несколько недель назад, я заметил, что вы можете создавать ключи с истекающим сроком действия, которые можно прикреплять к URL-адресам S3.Таким образом, вам не придется открывать файлы на S3 для публики.Моё понимание техники следующее:

  1. Начальная HTML-страница содержит ссылки для загрузки вашего веб-приложения.
  2. Пользователь нажимает на ссылку для скачивания
  3. Ваше веб-приложение генерирует URL-адрес S3, который включает ключ, срок действия которого истекает, скажем, через 5 минут.
  4. Отправьте клиенту перенаправление HTTP с URL-адресом из шага 3.
  5. Пользователь загружает файл с S3.Это работает, даже если загрузка занимает более 5 минут — как только загрузка начнется, она может продолжиться до завершения.

Другие советы

Почему бы вам просто не указать им URL-адрес S3?Взятие артефакта из S3 и последующая его потоковая передача мне через собственный сервер лишают смысла использование S3, которое заключается в разгрузке полосы пропускания и обработки изображений для Amazon.

Я видел много кода, подобного ответу Джона-Василефа (в настоящее время принятому), жесткому циклу while, считывающему фрагменты из одного потока и записывающему их в другой поток.

Я бы выдвинул аргумент против ненужного дублирования кода и в пользу использования Apache IOUtils.Если вы уже используете его где-то еще или если от него уже зависит другая библиотека или фреймворк, который вы используете, это одна строка, которая известна и хорошо протестирована.

В следующем коде я передаю объект из Amazon S3 клиенту в сервлете.

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 линий четкого паттерна при правильном закрытии потока выглядят довольно солидно.

Я полностью согласен как с Тоби, так и с Джоном Василеффом: S3 отлично подходит для загрузки больших медиа-объектов, если вы можете терпеть связанные с этим проблемы.(Экземпляр собственного приложения делает это для файлов FLV и MP4 размером 10–1000 МБ.) Например:Однако никаких частичных запросов (заголовок диапазона байтов) нет.Приходится справляться с этим «вручную», периодическими простоями и т. д.

Если это не вариант, код Джона выглядит хорошо.Я обнаружил, что байтовый буфер размером 2 КБ FILEBUFFERSIZE является наиболее эффективным в микробенч-тестах.Другим вариантом может быть общий FileChannel.(FileChannels являются потокобезопасными.)

Тем не менее, я бы также добавил, что предположение о том, что вызвало ошибку нехватки памяти, является классической ошибкой оптимизации.Вы повысите свои шансы на успех, работая с жесткими показателями.

  1. На всякий случай поместите -XX:+HeapDumpOnOutOfMemoryError в параметры запуска JVM.
  2. используйте jmap на работающей JVM (jmap -histo <pid>) под нагрузкой
  3. Проанализируйте метрики (выведите jmap -histo или попросите jhat просмотреть дамп кучи).Вполне возможно, что ваша нехватка памяти возникла откуда-то неожиданно.

Конечно, существуют и другие инструменты, но jmap и jhat поставляются с Java 5+ «из коробки».

Я рассматривал возможность записи файла на локальный временный диск, а затем создания другого потока для обработки потоковой передачи, чтобы поток сервлета Tomcat можно было повторно использовать.Кажется, это будет очень тяжело.

Ах, я не думаю, что ты не сможешь этого сделать.А даже если бы и мог, это звучит сомнительно.Поток tomcat, управляющий соединением, должен контролировать ситуацию.Если вы испытываете нехватку потоков, увеличьте количество доступных потоков в ./conf/server.xml.Опять же, метрики — это способ обнаружить это, а не просто гадать.

Вопрос:Вы тоже работаете на EC2?Каковы параметры запуска JVM вашего кота?

Тоби прав, тебе следует указывать прямо на S3, если можешь.Если вы не можете, вопрос немного расплывчатый, чтобы дать точный ответ:Насколько велика ваша куча Java?Сколько потоков открыто одновременно, когда у вас заканчивается память?
Насколько велик ваш буфер чтения и записи (8 КБ — это хорошо)?
Вы читаете 8К из потока, а затем записываете 8К на выход, верно?Вы не пытаетесь прочитать все изображение из S3, буферизовать его в памяти, а затем отправить все сразу?

Если вы используете буферы размером 8 КБ, у вас может быть 1000 одновременных потоков в куче примерно 8 МБ, так что вы определенно делаете что-то не так....

Кстати, я не взял 8K из воздуха, это размер по умолчанию для буферов сокетов, отправьте больше данных, скажем, 1Meg, и вы будете блокироваться в стеке tcp/ip, содержащем большой объем памяти.

Вам нужно проверить две вещи:

  • Вы закрываете поток?Очень важно
  • Возможно, вы предоставляете потоковые подключения «бесплатно».Поток небольшой, но много-много потоков одновременно могут украсть всю вашу память.Создайте пул, чтобы одновременно не запускалось определенное количество потоков.

В дополнение к тому, что предложил Джон, вам следует неоднократно очищать выходной поток.В зависимости от вашего веб-контейнера возможно, что он кэширует часть или даже весь ваш вывод и сбрасывает его сразу (например, для расчета заголовка Content-Length).Это сожжет довольно много памяти.

Если вы можете структурировать свои файлы так, чтобы статические файлы были отдельными и в отдельной корзине, наибольшая производительность сегодня, вероятно, может быть достигнута с помощью Amazon S3 CDN. CloudFront.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top