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

1. Почему реактивное программирование

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

1.1 Модель поток на запрос

Чтобы понять, что такое реактивное программирование и какие преимущества оно дает, сначала рассмотрим традиционный способ разработки веб-приложения с помощью Spring - использование Spring MVC и его развертывание в контейнере сервлетов, таком как Tomcat. Контейнер сервлетов имеет выделенный пул потоков для обработки HTTP-запросов, где каждому входящему запросу будет назначен поток, и этот поток будет обрабатывать весь жизненный цикл запроса (модель «поток на запрос»). Это означает, что приложение сможет обрабатывать количество одновременных запросов, равное размеру пула потоков. Можно настроить размер пула потоков, но поскольку каждый поток резервирует некоторую память (обычно 1 МБ), чем больший размер пула потоков мы настраиваем, тем выше потребление памяти.

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

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

1.2 Ожидание операций ввода/вывода

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

Рисунок 1 - Поток заблокирован в ожидании ответа
Рисунок 1 - Поток заблокирован в ожидании ответа

1.3 Время ответа

Другой проблемой традиционного императивного программирования является время отклика, когда службе необходимо выполнить более одного запроса ввода-вывода. Например, службе A может потребоваться вызвать службы B и C, а также выполнить поиск в базе данных, а затем вернуть в результате некоторые агрегированные данные. Это будет означать, что время ответа службы A, помимо времени ее обработки, будет суммой следующих значений:

  • время отклика услуги B (задержка сети + обработка)

  • время отклика службы C (задержка сети + обработка)

  • время ответа на запрос к базе данных (сетевая задержка + обработка)

Рисунок 2 - Последовательные вызовы
Рисунок 2 - Последовательные вызовы

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

1.4 Перегрузка клиента

Другой тип проблемы, которая может возникнуть в ландшафте микросервисов, - это когда сервис A запрашивает некоторую информацию у сервиса B, скажем, например, обо всех заказах, размещенных в течение последнего месяца. Если количество заказов окажется огромным, для службы А может возникнуть проблема получить всю эту информацию сразу. Служба A может быть перегружена большим объемом данных, что может привести, например, к ошибке нехватки памяти.

1.5 Резюме

Различные проблемы, описанные выше, - это проблемы, которые призвано решить реактивное программирование. Короче говоря, преимущества реактивного программирования заключаются в том, что мы:

  • отходим от модели поток на запрос и можем обрабатывать больше запросов с небольшим количеством потоков

  • предотвращаем блокировку потоков при ожидании завершения операций ввода-вывода

  • упрощаем параллельные вызовы

  • поддерживаем «обратное давление», давая клиенту возможность сообщить серверу, с какой нагрузкой он может справиться

2. Что такое реактивное программирование

2.1 Определение

В документации Spring дано следующее краткое определение реактивного программирования:

«Реактивное программирование - это неблокирующие приложения, которые являются асинхронными и управляемыми событиями и требуют небольшого количества потоков для масштабирования. Ключевым аспектом этого определения является концепция противодавления (backpressure), которая является механизмом, гарантирующим, что производители не перегружают потребителей».

2.2 Объяснение

Так как же всего этого достичь?

Вкратце: программированием с использованием асинхронных потоков данных. Допустим, служба A хочет получить некоторые данные из службы B. При подходе в стиле реактивного программирования служба A отправит запрос службе B, которая немедленно вернет управление (неблокирующий и асинхронный запрос). Затем запрошенные данные будут доступны службе A в виде потока данных, где служба B будет публиковать событие onNext для каждого элемента данных один за другим. Когда все данные будут опубликованы, об этом просигнализирует событие onComplete. В случае ошибки будет опубликовано событие onError, и больше никаких элементов не будет.

Рисунок 3 - Реактивный поток событий
Рисунок 3 - Реактивный поток событий

Реактивное программирование использует подход функционального стиля (аналогично Streams API), который дает возможность выполнять различные виды преобразований в потоках. Один поток можно использовать как вход для другого. Потоки можно объединять, отображать и фильтровать (операции merge, map и filter).

2.3 Реактивные системы

Реактивное программирование - важный метод реализации при разработке «реактивных систем», концепция которого описана в «Reactive Manifesto - Манифесте реактивного программирования», подчеркивая требования к проектированию современных приложений, которые должны быть:

  • Responsive - Отзывчивы (дают своевременный ответ)

  • Resilient - Устойчивы (остаются отзывчивыми даже в аварийных ситуациях)

  • Elastic - Эластичны (быстрая реакция при различной рабочей нагрузке)

  • Message Driven - Управляются сообщениями (на основе асинхронной передачи сообщений)

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

3. История вопроса

3.1 REACTIVEX

В 2011 году Microsoft выпустила библиотеку Reactive Extensions (ReactiveX или Rx) для .NET, чтобы обеспечить простой способ создания асинхронных программ, управляемых событиями. Через несколько лет Reactive Extensions были перенесены на несколько языков и платформ, включая Java, JavaScript, C ++, Python и Swift. ReactiveX быстро стал межъязыковым стандартом. Разработкой реализации Java - RxJava - руководила компания Netflix, а версия 1.0 была выпущена в 2014 году.

ReactiveX использует сочетание шаблона Iterator и шаблона Observer из Gang of Four. Разница в том, что используется модель push по сравнению с обычным поведением итераторов на основе pull. Помимо наблюдения за изменениями, подписчику также сообщается о завершении и ошибках.

3.2 Спецификация реактивных потоков

Позже для Java был разработан стандарт спецификации Reactive Streams (Реактивные потоки). Реактивные потоки - это небольшая спецификация, предназначенная для реализации реактивными библиотеками, созданными для JVM. Он определяет типы, которые необходимо реализовать для достижения взаимодействия между различными реализациями. Спецификация определяет взаимодействие между асинхронными компонентами с противодавлением. Реактивные потоки были реализованы в Java 9 в виде Flow API. Назначение Flow API - действовать как спецификация взаимодействия, а не API конечного пользователя, такого как RxJava.

Спецификация Reactive Streams включает следующие интерфейсы:

Publisher:

Он представляет производителя данных/источник данных и имеет один метод, который позволяет подписчику зарегистрироваться на издателе.

public interface Publisher<T> {
    public void subscribe(Subscriber<? super T> s);
}

Subscriber:
Он представляет потребителя и имеет следующие методы:

public interface Subscriber<T> {
    public void onSubscribe(Subscription s);
    public void onNext(T t);
    public void onError(Throwable t);
    public void onComplete();
}
  • onSubscribe должны вызываться Publisher перед началом обработки и использоватся для передачи на Subscription объекта от Publisher до Subscriber

  • onNext используется для того, чтобы сигнализировать о том, что был отправлен новый элемент

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

  • onComplete используется для того, чтобы сигнализировать, что все элементы были успешно отправлены

Subscription:
Subscription содержат методы, которые позволяют клиенту управлять выдачей элементов Publisher (т.е. обеспечивать поддержку противодавления).

public interface Subscription {
    public void request(long n);
    public void cancel();
}
  • request позволяет Subscriber сообщить, Publisher сколько дополнительных элементов будет опубликовано

  • cancel позволяет подписчику отменить дальнейшую отправку элементов Publisher

Processor:
Если объект должен преобразовывать входящие элементы, а затем передавать их другому Subscriber, требуется реализация интерфейса Processor. Он действует и как Subscriber ,и как  Publisher.

public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
}

3.3 PROJECT REACTOR

Spring Framework поддерживает реактивное программирование, начиная с версии 5. Эта поддержка построена на основе Project Reactor.

Project Reactor (или просто Reactor) - это библиотека Reactive для создания неблокирующих приложений на JVM, основанная на спецификации Reactive Streams. Это основа реактивного стека в экосистеме Spring. Reactor будет темой для второй публикации в этой серии!

Ссылки

A brief history of ReactiveX and RxJava

Build Reactive RESTFUL APIs using Spring Boot/WebFlux

Java SE Flow API

Project Reactor

Reactive Manifesto

Reactive Streams Specification

ReactiveX

Spring Web Reactive Framework

Комментарии (0)