Java-интенсивное приложение останавливается / зависает при увеличении no. рабочих. Где узкое место, и как вывести / контролировать его на сервере Ubuntu? - PullRequest
0 голосов
/ 23 декабря 2009

Я запускаю ночное ресурсоемкое Java-приложение на Ec2-сервере (c1.xlarge), который имеет восемь ядер, 7,5 ГБ ОЗУ (под управлением Linux / Ubuntu 9.10 (Karmic Koala) 64 бит).

Приложение спроектировано таким образом, что создается разное количество рабочих (каждый в своем собственном потоке) и извлекает сообщения из очереди для их обработки.

Пропускная способность является главной проблемой, и производительность измеряется в обработанных сообщениях в секунду. Приложение НЕ связано с ОЗУ ... И, насколько я вижу, не связано с вводом / выводом. (хотя я не звезда в Linux. Я использую dstat для проверки загрузки I / O, которая довольно низкая, и сигналов ожидания процессора (которых почти нет)).

Я вижу следующее, когда порождаю другое количество рабочих (рабочих потоков).

  1. Рабочий: пропускная способность 1,3 сообщения / сек / рабочий

  2. работник: ~ 0,8 сообщения / сек / работник

  3. работник: ~ пропускная способность 0,5 сообщения / сек / работник

  4. работник: ~ пропускная способность 0,05 сообщений / сек / работник

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

Три вопроса:

  1. Что может быть причиной сублинейной производительности одного рабочего -> двух рабочих и двух рабочих -> трех рабочих?

  2. Что может быть причиной (почти) полной остановки при переходе от трех рабочих к четырем рабочим? Это похоже на тупиковую ситуацию или что-то в этом роде (может ли это произойти из-за интенсивного переключения контекста?)

  3. Как мне начать измерять, где возникают проблемы? Моя коробка разработки имеет два процессора и работает под Windows. Я обычно присоединяю GUI-профилировщик и проверяю наличие проблем с потоками Но проблема только действительно начинает проявляться в моих более чем двух нитях.

Еще немного справочной информации:

  • Рабочие создаются с помощью Executors.newScheduledThreadPool

  • Рабочий поток выполняет вычисления на основе сообщения (загрузка процессора). Каждый рабочий поток содержит отдельную переменную persistQueue, используемую для разгрузки записи на диск (и, таким образом, использует параллелизм CPU / I / O.)

    persistQueue = новый ThreadPoolExecutor (1, 1, 100, TimeUnit.MILLISECONDS, новый ArrayBlockingQueue (maxAsyncQueueSize), новый ThreadPoolExecutor.AbortPolicy ());

Поток (на одного работника) выглядит так:

  1. Рабочий поток помещает результат сообщения в persistQueue и приступает к обработке следующего сообщения.

  2. ThreadpoolExecutor (из которых у нас есть один на рабочий поток) содержит только один поток, который обрабатывает все входящие данные (ожидающие в persistQueue) и записывает их на диск ( Berkeley DB + Apache Lucene ).

  3. Идея состоит в том, что 1. и 2. могут работать одновременно по большей части, так как 1. загружает процессор и 2. загружает ввод-вывод.

  4. Возможно, что persistQueue заполнится. Это сделано потому, что в противном случае медленная система ввода-вывода может вызвать переполнение очередей и привести к ошибкам OOM (да, это много данных). В этом случае workerThread приостанавливается до тех пор, пока не сможет записать свой контент в persistQueue. В этой настройке еще не было полной очереди (это еще одна причина, по которой я считаю, что приложение определенно не связано с вводом / выводом).

Последняя информация:

  • Рабочие изолированы от других в отношении своих данных, кроме:

    • Они используют некоторые часто используемые конечные статические карты (используемые в качестве кэшей. Карты занимают много памяти, поэтому я не могу хранить их локально для работника, даже если бы захотел). Операции, которые рабочие выполняют над этими кэшами: итерации, поиск, содержит (без записи, удаления и т. Д.)

    • Доступ к этим общим картам осуществляется без синхронизации (нет необходимости. Верно?)

    • Рабочие заполняют свои локальные данные, выбирая данные из MySQL (на основе ключей в полученном сообщении). Так что это потенциальное узкое место. Однако большая часть данных предназначена для чтения, запрашиваемые таблицы оптимизированы с использованием индексов и снова не привязаны к вводу / выводу.

    • Я должен признать, что я еще мало что сделал по оптимизации MySQL-сервера (с точки зрения config -params), но я просто не думаю, что это проблема.

  • Вывод записывается в:

    • Berkeley DB (с использованием memcached (b) -клиента). Все работники имеют общий сервер.
    • Lucene (с использованием отечественного низкоуровневого индексатора). Каждый работник имеет отдельный индексатор.
  • Проблемы возникают даже при отключении записи вывода.

Это огромный пост, я это понимаю, но я надеюсь, что вы можете дать мне несколько советов относительно того, что это может быть, или как начать мониторинг / вывод, где находится проблема.

Ответы [ 3 ]

1 голос
/ 23 декабря 2009

Если бы я был тобой, я бы не поверил никому в догадки о том, в чем проблема. Ненавижу звучать как неработающая пластинка, но есть очень простой способ выяснить это - стеки. Например, в вашем случае с 4 работниками, который работает в 20 раз медленнее, каждый раз, когда вы берете выборку из стека вызовов работника, вероятность составляет 19/20, что он будет в состоянии зависания, и вы можете понять, почему просто осматривает стек.

0 голосов
/ 23 декабря 2009

Если я правильно понял, несколько рабочих все выбирают из одной очереди, делают вычисления и передают результат своим личным авторам, как:

              / [ worker ] - [ writer, queue ]
[ msg-queue ] - [ worker ] - [ writer, queue ]
              \ [ worker ] - [ writer, queue ]

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

                                   / [ worker ] - [ writer, queue ]
[ msg-queue ] - [ fetcher, queue ] - [ worker ] - [ writer, queue ]
                                   \ [ worker ] - [ writer, queue ]

Еще одно замечание, которое я заметил в вашем описании, заключается в том, что в расчетах используется набор коллекций только для чтения, поэтому параллелизм не должен быть проблемой. Было бы неплохо выяснить, какую реализацию вы используете, даже если вы не синхронизируете использование в своей части кода, классы коллекций, такие как Vector и Hashtable, синхронизируются по умолчанию.

Использование неизменяемых версий классов коллекций помогло бы обеспечить одновременное использование карт по умолчанию.

0 голосов
/ 23 декабря 2009

Только профилирование поможет.

Но что нужно проверить:

  • Рабочие получают информацию из очереди. Какой тип очереди сохраняет поток очереди производителя?
  • Зачем использовать Executors.newScheduledThreadPool для создания ваших работников? Разве вы не хотите, чтобы они сразу же побежали?
...