Параллелизм планировщика Spring Reactor Webflux

Для полностью неблокирующих сквозных реактивных вызовов рекомендуется явно вызывать publishOn или subscribeOn для переключения планировщиков? Для задач, потребляющих или не потребляющих ЦП, выгодно ли всегда использовать параллельный поток для оптимизации производительности?

2 ответа

Решение

Для полностью неблокирующих сквозных реактивных вызовов рекомендуется явно вызывать publishOn или subscribeOn для переключения планировщиков?

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

Для задач, потребляющих или не потребляющих ЦП, выгодно ли всегда использовать параллельный поток для оптимизации производительности?

Абсолютно нет, рассмотрим этот пример:

Flux.range(1, 10)
        .parallel(4)
        .runOn(Schedulers.parallel())
        .sequential()
        .elapsed()
        .subscribe(i -> System.out.printf(" %s ", i));

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

Flux.range(1, 10)
        .elapsed()
        .subscribe(i -> System.out.printf(" %s ", i));

Теперь рассмотрим это:

public static <T> T someMethodThatBlocks(T i, int ms) {
    try { Thread.sleep( ms ); }
    catch (InterruptedException e) {}
    return i;
}

// some method here
Flux.range(1, 10)
        .parallel(4)
        .runOn(Schedulers.parallel())
        .map(i -> someMethodThatBlocks(i, 200))
        .sequential()
        .elapsed()
        .subscribe(i -> System.out.printf(" %s ", i));

Результат похож на:

 [210,3]  [5,1]  [0,2]  [0,4]  [196,6]  [0,8]  [0,5]  [4,7]  [196,10]  [0,9] 

Как видите, первый ответ пришел после 210 мс, за которыми следуют 3 ответа с близкими 0прошедшее время между ними. Цикл повторяется снова и снова. Здесь вы должны использовать параллельный поток. Обратите внимание, что создание большего количества потоков не гарантирует производительности, потому что при большем количестве потоков переключение контекста добавляет много накладных расходов, и, следовательно, код следует протестировать задолго до развертывания. Если есть много блокирующих вызовов, наличие более одного потока на ЦП может дать вам повышение производительности, но если выполняемые вызовы интенсивны по ЦП, то наличие более одного потока на ЦП снизит производительность из-за переключения контекста.

В общем, это всегда зависит от того, чего вы хотите достичь.

Стоит отметить, что я предполагаю, что контекст здесь - это Webflux, а не реактор в целом (поскольку вопрос помечен как таковой). Конечно, рекомендации могут сильно различаться, если мы говорим об обобщенном варианте использования реактора без учета Webflux.

Для полностью неблокирующих сквозных реактивных вызовов рекомендуется явно вызывать publishOn или subscribeOn для переключения планировщиков?

Общая рекомендация - не вызывать эти методы явно, если у вас нет на то причины. (Нет ничего плохого в том, чтобы использовать их в правильном контексте, но делать это "просто потому", скорее всего, не принесет пользы.)

Для задач, потребляющих или не потребляющих ЦП, выгодно ли всегда использовать параллельный поток для оптимизации производительности?

Это зависит от того, чего вы стремитесь достичь, и что вы подразумеваете под задачами, потребляющими центральный процессор (или процессором). Обратите внимание, что здесь я говорю о задачах, действительно интенсивно использующих ЦП, а не о блокировке кода - и в этом случае я бы в идеале передал часть, интенсивно использующую ЦП, на другой микросервис, что позволит вам масштабировать это по мере необходимости отдельно от службы Webflux.

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

Вместо этого я бы рекомендовал два подхода:

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