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

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

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

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

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

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

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

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

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

Ответ 1

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

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

Ответ 2

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

Ответ 3

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

Ответ 4

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