Java Threadpool против нового потока в сценарии с высоким запросом

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

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

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

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

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

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

Ответов (4)

Решение

В конфигурации:

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

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

Раздел javadocsThreadPoolExecutor (скопирован ниже), возможно, стоит прочитать еще раз.

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

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

Что, кстати, и получилось бы из Executors.newFixedThreadPool(100);


Очередь

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

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

Существуют три основные стратегии постановки в очередь:

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

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

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

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

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

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

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