Java и очереди: проблемы насыщения многопоточным вводом-выводом

Этот вопрос относится к последней версии Java.

30 потоков-производителей помещают строки в абстрактную очередь. Один поток записи появляется из той же очереди и записывает строку в файл, который находится на жестком диске RAID со скоростью вращения 5400 об/мин. Данные передаются со скоростью примерно 111 МБ/с и извлекаются/записываются со скоростью примерно 80 МБ/с. Программа живет 5600 секунд, этого достаточно, чтобы в очереди накопилось около 176 ГБ данных. С другой стороны, я ограничен в общей сложности 64 ГБ основной памяти.

Мой вопрос: какой тип очереди я должен использовать?

Вот что я пробовал до сих пор.

1) ArrayBlockingQueue. Проблема с этой ограниченной очередью заключается в том, что независимо от начального размера массива у меня всегда возникают проблемы с живучестью, как только он заполняется. Фактически через несколько секунд после запуска программы top сообщает только об одном активном потоке. Профилирование показывает, что в среднем потоки-производители тратят большую часть своего времени на ожидание освобождения очереди. Это не зависит от того, использую ли я политику справедливого доступа (со вторым аргументом в конструкторе, установленным в true).

2) ConcurrentLinkedQueue. Что касается живучести, эта неограниченная очередь работает лучше. Пока у меня не закончится память, примерно через семьсот секунд все тридцать потоков-производителей будут активны. Однако после того, как я превыслю лимит в 64 ГБ, все станет невероятно медленным. Я предполагаю, что это связано с проблемами пейджинга, хотя я не проводил никаких экспериментов, чтобы доказать это.

Я вижу два выхода из своего положения.

1) Купи SSD. Надеюсь, увеличение скорости ввода-вывода поможет.

2) Сжать выходной поток перед записью в файл.

Есть ли альтернатива? Я что-то упустил в том, как построена/используется любая из вышеперечисленных очередей? Есть ли более умный способ их использования? В книге Java Concurrency in Practice предлагается ряд политик насыщения (раздел 8.3.3) на случай, если ограниченные очереди заполняются быстрее, чем они могут быть исчерпаны, но, к сожалению, ни одна из них — прерывание, выполнение вызывающего объекта и два отбрасывания политики --- применяются в моем сценарии.


person user2650947    schedule 05.10.2013    source источник
comment
Когда вы осознаете, что диск является вашим узким местом, и независимо от того, как вы играете с программным обеспечением, производительность вашего диска — это то, с чем вам придется работать. Это означает сжатие данных и/или покупку более быстрого диска. Обратите внимание, что вы можете получить SSD со скоростью 400 МБ/с примерно за 100 долларов.   -  person Peter Lawrey    schedule 05.10.2013
comment
Если ваши производители производят быстрее, чем ваш потребитель, вы должны сделать своего потребителя быстрее или своих производителей медленнее.   -  person Raedwald    schedule 05.10.2013
comment
Кажется, вы удивлены тем, что попытка хранить 176 ГБ объектов на компьютере с 64 ГБ замедляет работу вашей программы.   -  person Raedwald    schedule 05.10.2013


Ответы (5)


Ищите узкое место. Вы производите больше, чем потребляете, ограниченная очередь имеет смысл, так как вы не хотите исчерпать память.

Постарайтесь сделать своего потребителя быстрее. Профилируйте и смотрите, где тратится больше всего времени. Так как вы пишете на диск вот некоторые мысли:

  • Не могли бы вы использовать NIO для решения вашей проблемы? (возможно FileChannel#transferTo())
  • Смывать только при необходимости.
  • Если у вас достаточно резервов ЦП, сжать поток? (как вы уже упоминали)
  • оптимизируйте свои диски по скорости (рейд-кэш и т. д.)
  • более быстрые диски

Как уже сказал @Flavio, для шаблона производитель-потребитель я не вижу здесь проблем, и все должно быть так, как сейчас. В конце концов, самая медленная сторона контролирует скорость.

person Ortwin Angermeier    schedule 05.10.2013
comment
Фактически использование ограниченной очереди замедляет производителей до тех пор, пока их общая скорость не сравняется со скоростью потребителя. - person Raedwald; 05.10.2013
comment
Я принимаю этот ответ из-за упоминания NIO, о котором я не знал. Спасибо. - person user2650947; 05.10.2013

Я не вижу здесь проблемы. В ситуации производитель-потребитель система всегда будет двигаться со скоростью более медленной стороны. Если производитель быстрее, чем потребитель, он будет замедлен до скорости потребителя, когда очередь заполнится.

Если ваше ограничение состоит в том, что вы не можете замедлить производителя, вам придется найти способ ускорить потребителя. Профилируйте потребителя (не начинайте слишком замысловато, несколько System.nanoTime() вызовов часто дают достаточно информации), проверьте, где он проводит большую часть своего времени, и оттуда начните оптимизацию. Если у вас есть узкое место в ЦП, вы можете улучшить свой алгоритм, добавить больше потоков и т. д. Если у вас есть узкое место на диске, попробуйте писать меньше (сжатие — хорошая идея), возьмите более быстрый диск, пишите на два диска вместо одного...

person Flavio    schedule 05.10.2013

В соответствии с java "реализация очереди" существуют другие классы, которые должны быть правильным для вас:

  • LinkedBlockingQueue
  • ПриоритетБлокингОчередь
  • DelayQueue
  • Синхронная очередь
  • LinkedTransferQueue
  • TransferQueue

Я не знаю производительности этих классов или использования памяти, но вы можете попробовать сами.

Я надеюсь, что это поможет вам.

person Valerio Emanuele    schedule 05.10.2013

Почему у вас 30 продюсеров. Этот номер зафиксирован проблемным доменом или это просто номер, который вы выбрали? Если последнее, вы должны уменьшить количество производителей до тех пор, пока они не будут производить с общей скоростью, превышающей потребление лишь на небольшую величину, и использовать блокирующую очередь (как предлагали другие). Тогда вы будете держать своего потребителя занятым, что ограничивает производительность, при этом сводя к минимуму использование других ресурсов (память, потоки).

person Raedwald    schedule 05.10.2013

у вас есть только 2 выхода: сделать поставщиков медленнее или быстрее потребителей. Более медленных производителей можно добиться разными способами, в частности, с помощью ограниченных очередей. Чтобы потребитель работал быстрее, попробуйте https://www.google.ru/search?q=java+memory-mapped+file . Посмотрите на https://github.com/peter-lawrey/Java-Chronicle.

Другой способ — освободить поток записи от работы по подготовке буферов записи из строк. Пусть потоки-производители выдают готовые буферы, а не строки. Используйте ограниченное количество буферов, скажем, 2*threadnumber=60. Выделите все буферы в начале, а затем используйте их повторно. Используйте очередь для пустых буферов. Производящий поток берет буфер из этой очереди, заполняет его и помещает в очередь записи. Поток записи берет буферы из потока записи, записывает на диск и помещает в очередь пустых буферов.

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

person Alexei Kaigorodov    schedule 05.10.2013