Производительность загрузки многопоточных файлов Java - PullRequest
18 голосов
/ 05 августа 2010

Недавно работая над проектом, который требовал большего количества операций ввода-вывода, чем я привык, я почувствовал, что хочу заглянуть в обычные библиотеки (в частности, Commons IO) и заняться более глубокими проблемами ввода-вывода.

В качестве академического теста я решил реализовать базовый многопоточный загрузчик HTTP.Идея проста: укажите URL для загрузки, и код загрузит файл.Чтобы увеличить скорость загрузки, файл разделяется на части, и каждый блок загружается одновременно (используя заголовок HTTP Range: bytes=x-x), чтобы использовать максимально возможную пропускную способность.

У меня есть рабочий прототип, но, как вы уже догадались,это не совсем идеально.На данный момент я вручную запускаю 3 "загрузчика" темы, каждая из которых загружает 1/3 файла.Эти потоки используют общий синхронизированный экземпляр «File Writer» для фактической записи файлов на диск.Когда все потоки завершены, «средство записи файлов» завершается, и все открытые потоки закрываются.Некоторые фрагменты кода, чтобы дать вам представление:

Запуск потока:

ExecutorService downloadExecutor = Executors.newFixedThreadPool(3);
...
downloadExecutor.execute(new Downloader(fileWriter, download, start1, end1));
downloadExecutor.execute(new Downloader(fileWriter, download, start2, end2));
downloadExecutor.execute(new Downloader(fileWriter, download, start3, end3));

Каждый поток «загрузчик» загружает кусок (буферизованный) и использует «средство записи файлов» длязапись на диск:

int bytesRead = 0;
byte[] buffer = new byte[1024*1024];
InputStream inStream = entity.getContent();
long seekOffset = chunkStart;
while ((bytesRead = inStream.read(buffer)) != -1)
{
    fileWriter.write(buffer, bytesRead, seekOffset);
    seekOffset += bytesRead;
}

«Модуль записи файлов» записывает на диск, используя RandomAccessFile to seek() и write() фрагменты на диск:

public synchronized void write(byte[] bytes, int len, long start) throws IOException
{
      output.seek(start);
      output.write(bytes, 0, len);
}

Все вещисчитается, что этот подход, кажется, работает.Тем не менее, это не очень хорошо работает.Я был бы признателен за некоторые советы / помощь / мнения по следующим пунктам.Очень признателен.

  1. Использование ЦП этого кода не за горами.Для этого он использует половину моего ЦП (50% каждого из двух ядер), что в геометрической прогрессии больше, чем сопоставимые инструменты загрузки, которые практически не нагружают ЦП.Я немного озадачен тем, откуда происходит это использование процессора, поскольку я не ожидал этого.
  2. Обычно, кажется, 1 из 3 потоков, который отстает от значительно.Другие 2 потока завершатся, после чего третьему потоку (который, как представляется, в основном первый поток с первым чанком) требуется 30 или более секунд для завершения.Из диспетчера задач я вижу, что процесс javaw все еще выполняет небольшие записи ввода-вывода, но я не знаю, почему это происходит (я предполагаю условия гонки?).
  3. Несмотря на то, что я 'выбрав довольно большой буфер (1 МБ), у меня возникает ощущение, что InputStream практически никогда не заполняет буфер, что вызывает больше операций ввода-вывода, чем хотелось бы.У меня сложилось впечатление, что в этом сценарии было бы лучше сохранить доступ к IO как минимум, но я не знаю точно, является ли это лучшим подходом.
  4. Я понимаю, что Java может небыть идеальным языком, чтобы делать что-то подобное, но я убежден, что производительность будет намного выше, чем в текущей реализации.Стоит ли исследовать NIO в этом случае?

Примечание: Я использую Apache HTTPClient для выполнения HTTP-взаимодействия, откуда берется entity.getContent() (на случай, если кому-то интересно).

Ответы [ 4 ]

6 голосов
/ 05 декабря 2010

Чтобы ответить на мои собственные вопросы:

  1. Увеличение использования ЦП произошло из-за цикла while() {}, ожидающего завершения потоков.Как выясняется, awaitTermination - гораздо лучшая альтернатива, чтобы дождаться завершения Executor:)
  2. (И 3 и 4). Это похоже на природу зверя;в итоге я достиг того, что хотел сделать, используя тщательную синхронизацию различных потоков, каждый из которых загружает порцию данных (ну, в частности, записи этих порций обратно на диск).
3 голосов
/ 05 августа 2010

Предположительно, HTTP-клиент Apache будет выполнять некоторую буферизацию с меньшим буфером. Ему понадобится буфер для разумного чтения заголовка HTTP и, вероятно, для обработки фрагментированного кодирования.

2 голосов
/ 05 августа 2010

Моей непосредственной мыслью о лучшей производительности в Windows было бы использование портов завершения ввода-вывода .Что я не знаю, так это (а) есть ли подобные концепции в других ОС, и (б) есть ли подходящие Java-оболочки?Однако, если для вас не важна переносимость, возможно, вам удастся свернуть свою оболочку с помощью JNI.

0 голосов
/ 05 августа 2010

Установить очень большой сокет приемного буфера.Но на самом деле ваша производительность будет ограничена пропускной способностью сети, а не пропускной способностью процессора.На самом деле все, что вы делаете, - это выделяете 1/3 пропускной способности сети для каждого загрузчика.Я был бы удивлен, если бы вы получили много пользы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...