Java Threadpool против нового потока в сценарии с высоким запросом - PullRequest
8 голосов
/ 02 марта 2009

У меня есть старый Java-код для службы REST, который использует отдельный поток для каждого входящего запроса. То есть основной цикл зацикливается на socket.accept () и передает сокет в Runnable, который затем запускает собственный фоновый поток и запускает run сам по себе. Некоторое время это работало восхитительно, пока недавно я не заметил, что задержка принятия на обработку запроса станет неприемлемой при высокой нагрузке. Когда я говорю восхищенно хорошо, я имею в виду, что он обрабатывает 100-200 запросов в секунду без значительной загрузки ЦП. Производительность ухудшалась только тогда, когда другие демоны добавляли нагрузку, а затем только после того, как нагрузка превысила 5. Когда машина находилась под высокой нагрузкой (5-8) из-за комбинации других процессов, время от принятия до обработки было бы невероятно высоким ( 500 мс до 3000 мс), в то время как фактическая обработка осталась меньше 10 мс. Это все в двухъядерных системах Centos 5.

Будучи привыкшим к Threadpools в .NET, я предположил, что создание потоков было виновником, и я подумал, что применил бы тот же шаблон в Java. Теперь мой Runnable выполняется с ThreadPool.Executor (а пул использует и ArrayBlockingQueue). Опять же, он прекрасно работает в большинстве сценариев, если только нагрузка на машину не становится высокой, тогда время от создания runnable до запуска run () показывает примерно одинаковое смешное время. Но что еще хуже, загрузка системы почти удвоилась (10-16) с логикой пула потоков. Так что теперь у меня такие же проблемы с задержкой при удвоении нагрузки.

Я подозреваю, что конкуренция за блокировку очереди хуже, чем стоимость запуска нового нового потока без блокировок. Может кто-нибудь поделиться своим опытом новой темы против Threadpool. И если мое подозрение верно, у кого-нибудь есть альтернативный подход к работе с пулом потоков без конфликта блокировок?

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

спасибо, Арне

ОБНОВЛЕНИЕ: я переключился на Executors.newFixedThreadPool(100);, и, хотя он сохранил ту же производительность, нагрузка почти сразу удвоилась, и при его работе в течение 12 часов нагрузка оставалась стабильно равной 2x. Я предполагаю, что в моем случае новый поток на запрос дешевле.

Ответы [ 4 ]

12 голосов
/ 04 марта 2009

С конфигурацией:

new ThreadPoolExecutor(10, 100, 30, TimeUnit.SECONDS, 
        new ArrayBlockingQueue<Runnable>(100))

Затем, если 10 потоков одновременно обрабатывают запросы, дополнительные запросы добавляются в очередь, если только она не достигнет 100 запросов в очереди, и в этот момент он начнет создавать новые потоки, если только не будет 100 потоков, при обработке команда будет отклонена.

Раздел javadocs ThreadPoolExecutor (скопированный ниже) может стоить другого прочтения.

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

new ThreadPoolExecutor(100, 100, 0, TimeUnit.SECONDS, 
        new LinkedBlockingQueue<Runnable>())

Что, кстати, это то, что вы получите от Executors.newFixedThreadPool(100);


Queuing

Любая BlockingQueue может использоваться для передачи и хранения отправленных задач. Использование этой очереди взаимодействует с размером пула:

  • Если запущено меньше потоков corePoolSize, Исполнитель всегда предпочитает добавлять новый поток, а не ставить в очередь.
  • Если работает corePoolSize или несколько потоков, Исполнитель всегда предпочитает ставить запрос в очередь, а не добавлять новый поток.
  • Если запрос не может быть поставлен в очередь, создается новый поток, если только он не превысит MaximumPoolSize, и в этом случае задача будет отклонена.

Существует три основных стратегии организации очередей:

  1. Прямые передачи. Хорошим выбором по умолчанию для рабочей очереди является SynchronousQueue, который передает задачи потокам, не удерживая их в противном случае. Здесь попытка поставить задачу в очередь не удастся, если нет потоков, доступных для ее немедленного выполнения, поэтому будет создан новый поток. Эта политика избегает блокировок при обработке наборов запросов, которые могут иметь внутренние зависимости. Прямые передачи обслуживания обычно требуют неограниченного MaximumPoolSizes, чтобы избежать отклонения новых представленных задач. Это, в свою очередь, допускает возможность неограниченного роста потока, когда команды продолжают поступать в среднем быстрее, чем они могут быть обработаны.
  2. Неограниченные очереди. Использование неограниченной очереди (например, LinkedBlockingQueue без предопределенной емкости) приведет к тому, что новые задачи будут ожидать в очереди, когда все потоки corePoolSize заняты. Таким образом, никогда не будет создано больше, чем corePoolSize. (И поэтому значение MaximumPoolSize не оказывает никакого влияния.) Это может быть целесообразно, когда каждая задача полностью независима от других, поэтому задачи не могут влиять на выполнение друг друга; например, на сервере веб-страниц. Хотя этот стиль организации очередей может быть полезен для сглаживания переходных пакетов запросов, он допускает возможность неограниченного роста рабочей очереди, когда команды продолжают поступать в среднем быстрее, чем они могут быть обработаны.
  3. Ограниченные очереди. Ограниченная очередь (например, ArrayBlockingQueue) помогает предотвратить исчерпание ресурсов при использовании с конечным MaximumPoolSizes, но может быть более сложным для настройки и управления. Размеры очереди и максимальные размеры пула можно обменивать друг на друга: использование больших очередей и небольших пулов сводит к минимуму использование ЦП, ресурсы ОС и накладные расходы на переключение контекста, но может привести к искусственно низкой пропускной способности. Если задачи часто блокируются (например, если они связаны с вводом / выводом), система может запланировать время для большего количества потоков, чем вы позволите. Использование небольших очередей обычно требует больших размеров пула, что повышает нагрузку на ЦП, но может привести к недопустимым затратам на планирование, что также снижает пропускную способность.
4 голосов
/ 02 марта 2009

измерение, измерение, измерение! Где он проводит время? Что должно произойти, когда вы создаете свой Runnable? Имеется ли в Runnable что-либо, что может блокировать или задерживать создание экземпляра? Что происходит во время этой задержки?

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

Что такое среда выполнения, версия JVM и архитектура?

1 голос
/ 02 марта 2009

Реализация Sun Thread, хотя и намного быстрее, чем раньше, имеет блокировку. IIRC, ArrayBlockingQueue не должен вообще блокироваться, когда занят. Поэтому это время профилировщика (или даже несколько ctrl-\ с или jstack с).

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

0 голосов
/ 02 марта 2009

Я просто сделал это с моим собственным кодом. Я использовал профилировщик Netbeans, чтобы включить реализацию пула потоков, которую я использовал. Вы должны быть в состоянии сделать то же самое с Visual VM , но я еще не пробовал.

...