Как стать автором
Обновить

Замечания о реактивном программировании. Технологический ландшафт

Время на прочтение 10 мин
Количество просмотров 10K
Автор оригинала: Dave Syer
Здравствуйте, дорогие читатели! Сегодня предлагаем вашему вниманию обещанную статью с обзором возможностей реактивного программирования. Приятного и плодотворного чтения.

Реактивное программирование (вновь) пользуется интересом, с ним связана большая шумиха, в которой не так легко разобраться человеку непосвященному или простому enterprise-разработчику на Java — такому, как автор этой статьи. Возможно, эта статья поможет вам понять, что к чему в этой области. Я старался придерживаться максимальной конкретики, и здесь нет ни слова о «денотационной семантике». Если вас интересует более академический текст и уйма кода на Haskell – Интернет вам в помощь, но эту статью, пожалуй, читать не стоит.

Реактивное программирование часто смешивают с конкурентным и высокопроизводительным программированием, причем настолько, что эти концепции сложно разделить, тогда как в принципе они совершенно разные. Это неизбежно приводит к путанице. Кроме того, реактивное программирование часто отождествляется или сближается с функциональным реактивным программированием (ФРП), здесь мы будем использовать эти термины в качестве синонимов. Некоторые считают, что в реактивности нет ничего нового, и что они так или иначе ею постоянно пользуются (в основном это специалисты по JavaScript). Другие считают, что реактивное программирование — дар Microsoft (компания не так давно привлекла серьезное внимание к такому программированию, выпустив ряд расширений C#). Недавно обсуждение темы активизировалось и в пространстве Enterprise Java (см. например Reactive Streams initiative), и, как всегда бывает с яркими новыми технологиями, в этой сфере легко наделать ошибок, не вполне понимая, когда и где технологию можно и следует использовать.

Что это такое?


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

Истоки реактивного программирования возникают в 1970-е или даже раньше, так что идея не нова. Однако она находит действительно серьезный отклик в некоторых аспектах современной enterprise-разработки. Резонанс (неслучайно) совпал с расцветом микросервисов и повсеместным распространением многоядерных процессоров.
Вот пара полезных кратких определений, взятых из других источников:

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

и…

Чтобы составить первое впечатление об этом, удобно сравнить вашу программу с таблицей, а все ваши переменные – с ячейками. Если значение в одной из ячеек таблицы меняется, то меняются и значения во всех остальных ячейках, которые ссылаются на первую. В ФРП все именно так. Теперь допустим, что некоторые ячейки меняются сами по себе (вернее), берутся откуда-то извне. В ситуации с GUI хороший пример – перемещение курсора мыши.

(взято из Terminology Question on Stackoverflow)

ФРП имеет много общего с высокой производительностью, конкурентностью, асинхронными операциями и неблокирующим вводом/выводом. Однако не помешает исходить из гипотезы, что ФРП не имеет ничего общего ни с чем из вышеперечисленного. Действительно, весьма естественного делегировать обработку всех этих проблем вызывающей стороне, когда работаешь по реактивной модели. Но, что касается сути или эффективности обработки этих проблем, результат полностью зависит от конкретной реализации (следовательно, реализацию нужно рассматривать максимально критично). Также возможно реализовать совершенно логичный и полезный ФРП-фреймворк синхронным однопоточным образом, но это вряд ли вам поможет, если вы хотите напрактиковаться с какими-либо новыми инструментами и библиотеками.

Реактивное программирование – примеры использования


По-видимому, новичку сложнее всего ответить на вопрос: «А зачем мне реактивное программирование». Ниже приведено несколько примеров из enterprise-среды, иллюстрирующие общие принципы использования такой парадигмы.

Вызовы внешних сервисов. В настоящее время многие сервисы базы данных реализуются в стиле REST (то есть, работают по HTTP). Поэтому их базовый протокол по сути своей блокирующий и синхронный. Возможно, не самая очевидная область для упражнений с ФРП, но на самом деле это весьма плодотворная почва, так как реализация подобных сервисов зачастую предполагает вызов других сервисов, а затем – и следующих сервисов, в зависимости от результатов первых вызовов. Если бы при таком активном вводе-выводе вы дожидались завершения одного вызова, и лишь затем отправляли бы следующий запрос, то ваш бедный клиент просто извелся бы еще до того, как вы успели бы сформировать отклик. Итак, вызовы внешних сервисов, а особенно сложная оркестровка зависимостей между вызовами — те вещи, которые действительно стоит оптимизировать. ФРП должно обеспечивать «компонуемость» логики, лежащей в основе этих операций, так, что труд разработчика вызывающего сервиса упрощается.

Потребители высококонкурентных сообщений. Обработка сообщений, в частности, высококонкурентных – типичная задача в enterprise-среде. В области реактивных фреймворков любят микробенчмаркниг, хвастаются тем, сколько сообщений в секунду можно обработать на JVM. Результаты поистине ошеломляют (без труда достигается производительность в десятки миллионов сообщений ежесекундно), но, пожалуй, они несколько искусственные – уже не столь впечатляет, когда оказывается, что контрольные точки ставились в простом цикле for. Однако, не стоит быстро списывать со счетов такую работу; легко заметить, что когда критична производительность, любой вклад в нее – как найденный. Реактивные паттерны естественным образом сочетаются с обработкой сообщений (поскольку событие красиво транслируется в сообщение), поэтому такой способ быстрее обрабатывать еще больше сообщений заслуживает внимания.

Таблицы. Пожалуй, это не вполне enterprise-пример, но как раз такой, которым в сфере enterprise вполне может воспользоваться каждый. Кроме того, этот пример хорошо схватывает философию и сложности воплощения ФРП. Если ячейка B зависит от ячейки A, а ячейка C зависит и от A, и от B, то как распространить из A и гарантировать, что С будет обновлена до отправки в B каких-либо событий изменения? Если приходится базироваться на действительно активном фреймворке, то наш ответ – «пустяк, достаточно всего лишь объявить зависимости», и в нем вся суть истинной мощи таблиц. Кроме того, этот пример подчеркивает разницу между ФРП и обычным событийно-ориентированным программированием – превращает «маршрутизацию» в «интеллектуальную маршрутизацию».

Абстрагирование (а)синхронной обработки. Этот пример более абстрактен – уже из таких, которых я собирался избегать. Кроме того, он во многом пересекается с уже упомянутыми более конкретными случаями, но, надеюсь, обсудить его все-таки будет нелишне. Основной тезис известен (и) обоснован – если разработчик готов сделать дополнительный уровень абстракции, то может просто не заморачиваться о том, синхронный код приходится вызывать или асинхронный. Поскольку асинхронное программирование – тяжелая нагрузка на ваши нежные серые клеточки, из реактивного программирования можно почерпнуть кое-какие полезные идеи. Реактивное программирование – не единственный подход к решению таких проблем, но некоторые авторы ФРП настолько крепко размышляли на эту тему, что у них получились действительно эффективные инструменты для ее решения. This Netflix

Сравнение


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

Машина событий в Ruby. Event Machine – это абстракция конкурентного программирования (обычно связанная с неблокирующим вводом/выводом). Рубисты долго мучились, пытаясь превратить язык, заточенный под однопоточный скриптинг, в инструмент для написания серверных приложений, которые а) будут работать 2) будут быстро работать и 3) будут выживать под нагрузкой. В Ruby уже довольно давно были потоки, но они не слишком активно использовались и имели дурную славу, так как не всегда нормально работали. Альтернатива, которая в настоящий момент повсеместно распространилась – настолько, что в Ruby 1.9 было предложено внести ее в ядро языка – называется Fibers (да, «волокна»). Модель программирования с использованием волокон напоминает работу с сопроцедурами (см. ниже), где отдельный нативный поток применяется для обработки большого количества конкурентных запросов (зачастую сопряженных с вводом/выводом). Сама эта модель программирования немного абстрактна, и рассуждать о ней сложно, поэтому большинство программистов пользуется оберткой, в качестве которой чаще всего берется машина событий. Машина событий не обязательно использует волокна (она абстрагирует связанные с ними проблемы), но легко найти примеры кода из веб-приложений на Ruby, где машина событий применяется с волокнами (см. эту статью Ильи Григорика или пример с волокнами из em-http-request). Это делается в основном ради обеспечения масштабируемости, которая достигается при использовании машины событий в приложениях с активным вводом/выводом, без необходимости тех некрасивых программных моделей, которые связаны со множеством вложенных обратных вызовов.

Акторная модель. Акторная модель, подобно объектно-ориентированному программированию – это древний раздел информатики, истоки которого восходят еще к 1970-м. Акторы абстрагируют вычисления (а не данные и не поведения) и, следовательно, обеспечивают конкурентность. Поэтому, на практике акторы могут послужить основой для конкурентной системы. Акторы обмениваются сообщениями, поэтому в каком-то смысле они реактивны, поэтому акторная и реактивная модели программирования во многом пересекаются. Зачастую различие проводится на уровне реализации (напр. Actors в Akka можно распределять между процессами, что является отличительной чертой этого фреймворка).

Отложенные результаты (Futures). В Java 1.5 появилось целое множество новых библиотек, в том числе, "java.util.concurrent" от Дуга Ли (Doug Lea). Именно тогда появилась и концепция отложенного результата, инкапсулированная в виде Future. Это хороший пример простой абстракции асинхронного паттерна, который не вынуждает нас идти на асинхронную реализацию или использовать какую-либо конкретную модель асинхронной обработки. Как хорошо показано в статье Netflix Tech Blog: Functional Reactive in the Netflix API with RxJava, Futures великолепны, когда вам нужно конкурентно обработать набор похожих задач, но как только вы собираетесь создать зависимости между ними, либо хотите, чтобы они выполнялись с какими-то условиями, то сразу попадаете в «ад обратных вызовов». Реактивное программирование – противоядие от этой беды.

Map-reduce и fork-join Абстракции параллельной обработки полезны, и за примерами далеко ходить не надо. Map-reduce и fork-join недавно развились в мире Java под влиянием инструментов массивной распределенной параллельной обработки (MapReduce и Hadoop), а также в рамках самого JDK в версии 1.7 (Fork-Join). Эти абстракции полезны, но (как и отложенные результаты) они мелковаты. ФРП может использоваться и как абстракция простой параллельной обработки, но ее возможности далеко не ограничиваются компонуемостью и декларативной коммуникацией.

Сопроцедуры. "Сопроцедура" – это генерализация «подпроцедуры». В ней есть точка входа и точки выхода, как в подпроцедуре, но после выхода сопроцедура передает управление другой сопроцедуре (не обязательно той, которая ее вызвала), причем приобретенное состояние сохраняется и запоминается до следующего вызова. Сопроцедуры могут использоваться как «кирпичики» для более высокоуровневых сущностей, например, акторов или потоков. Одна из целей реактивного программирования – обеспечить точно такую абстракцию для коммуникации параллельно работающих агентов, поэтому сопроцедуры (если они доступны) очень полезны. Существуют разнообразные варианты сопроцедур, некоторые из них в целом строже других, но более гибкие, чем упрощенные сопроцедуры. Волокна (выше мы обсуждали их в связи с машиной событий) – одна из таких разновидностей, другая разновидность называется «генераторы» (встречаются в Scala и Python).

Реактивное программирование на Java


Java нельзя назвать «реактивным языком», так как нативно он не поддерживает сопроцедуры. Есть другие языки для JVM (Scala и Clojure), которые более естественно поддерживают реактивные модели, но в самом Java такая возможность отсутствовала вплоть до версии 9. Однако, Java – настоящий локомотив enterprise-разработки, и в последнее время ведется огромная работа по созданию реактивных уровней над JDK. Здесь мы лишь очень кратко рассмотрим некоторые из них.

Реактивные потоки – это очень низкоуровневый контракт, выраженный в виде нескольких интерфейсов Java (плюс TCK), также применимый в других языках. Интерфейсы предоставляют базовые простейшие элементы Publisher и Subscriber с ярко выраженным обратным давлением, тем самым формируя общий язык для интероперабельных библиотек. Реактивные потоки были внедрены в JDK какjava.util.concurrent.Flow в версии 9.

RxJava: Netflix некоторое время использовали реактивные паттерны на внутрикорпоративном уровне, и выпустили инструменты, использовавшиеся по лицензии свободного ПО под названием Netflix/RxJava (потом сделали ребрендинг: «ReactiveX/RxJava»). Существует и переход к Reactive Streams. RxJava – это библиотека «второго поколения» по классификации Дэвида Карнока (David Karnok) Generations of Reactive.

Reactor – это фреймворк для Java от команды разработчиков Pivotal (той самой, что создала Spring). Он строится непосредственно на Reactive Streams, поэтому никакой переход не требуется. Проект Reactor IO предоставляет обертки для низкоуровневых сетевых сред исполнения вроде Netty and Aeron. Reactor – это библиотека «четвертого поколения» по классификации Дэвида Карнока (David Karnok) Generations of Reactive.

Spring Framework 5.0 (первые итоги подведены в июне 2016) оснащен реактивными возможностями – так, в нем есть инструменты для создания HTTP-серверов и клиентов. Нынешние пользователи Spring на веб-уровне найдут много знакомого в модели программирования, где аннотации используются для декорирования методов контроллера для обработки HTTP-запросов. При этом диспетчеризация реактивных запросов и обратного давления в основном передается на уровень фреймворка. Spring строится на основе Reactor, но также предоставляет API, позволяющие выражать его возможности при помощи разнообразных библиотек (напр. Reactor или RxJava). Можно работать с Tomcat, Jetty, Netty (через Reactor IO) и Undertow для серверного сетевого стека.

Ratpack – это подборка библиотек для создания высокопроизводительных сервисов по HTTP. Она строится на Netty и реализует Reactive Streams для интероперабельности (так, выше по стеку вы можете использовать другие реализации Reactive Streams). Spring поддерживается как нативный компонент и может использоваться для внедрения зависимостей при помощи каких-нибудь простых вспомогательных классов. Также есть некоторая автоконфигурация, позволяющая пользователям Spring Boot встраивать Ratpack в приложение Spring, загружать конечную точку HTTP и слушать ее, а не пользоваться одним из встроенных серверов, поставляемых непосредственно с Spring Boot.

Akka – это инструментарий для построения приложений при помощи паттерна Actor на Scala или Java, причем межпроцессная коммуникация выполняется посредством Akka Streams, а контракты Reactive Streams просто встроены. Akka — это библиотека «третьего поколения» по классификации Дэвида Карнока (David Karnok) Generations of Reactive.

Почему именно сейчас?


Чем обусловлен повышенный интерес к реактивности в Enterprise Java? Это далеко не просто технологическая причуда – казалось бы, люди просто накинулись на мешок с новыми блестящими игрушками. Стимул заключается в эффективном использовании ресурсов, точнее – в экономии на серверах и датацентрах. Реактивное программирование должно помочь достичь большего меньшими силами, а именно – обрабатывать более высокие нагрузки при помощи меньшего количества потоков. Именно здесь пересекаются реактивное программирование и неблокирующий асинхронный ввод/вывод. Если реактивное программирование действительно хорошо подходит для решения конкретной проблемы, то эффект драматический. Впрочем, в противном случае ситуация может только усугубиться.

Заключение


В этой статье мы в самом общем плане рассмотрели реактивное программирование в контексте современной enterprise-разработки. Существует ряд реактивных библиотек и фреймворков для JVM, все они активно разрабатываются. Во многом они схожи, но, благодаря Reactive Streams, хорошо взаимодействуют друг с другом.
Теги:
Хабы:
+1
Комментарии 0
Комментарии Комментировать

Публикации

Информация

Сайт
piter.com
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия