Рубрики
Без рубрики

Не Пейте Слишком Много Реактивной Прохладной Помощи

Почему реактивное программирование – это не серебряная пуля. Помеченный как реактивный, java, программирование.

Перекрестный постинг статьи в моем блоге .

В последние годы активно продвигалась идея “реактивных серверных приложений”, особенно с помощью новых фреймворков. В мире Java это было RxJava , vert.x и недавно Весенний поток веб-страниц .

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

Вокруг этого существует пара мифов, наиболее распространенных…

Это правда?

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

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

Модель реактивного программирования предполагает централизованную обработку всего, что входит в программу и выходит из нее (ввод-вывод). Один поток, как правило, выполняет несколько задач, перебирая возможные события, которые могут произойти. Есть ли новое TCP-соединение? Записан ли мой байт в поток? Загружен ли файл с диска? Как только это будет сделано, он сообщит какому-нибудь зарегистрированному фрагменту кода, что это сделано. Тогда это будет функция обратного вызова.

Итак, является ли этот синхронный код медленным по сравнению с реактивным кодом? На самом деле синхронный код работает быстрее, потому что поток не используется для выполнения разных промежуточных задач. Например, когда вы в данный момент записываете в поток TCP, ничто не прерывает отправку байтов, потому что какая-то другая часть программы хочет прочитать из файла. Таким образом, при условии идеального канала (например, бесконечной пропускной способности ввода-вывода) синхронный код будет быстрее. Но в большинстве случаев ввод-вывод выполняется значительно медленнее, чем вычисления, что означает, что у вас достаточно времени для выполнения других задач в ожидании завершения ввода-вывода.

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

Смотрите здесь , например. В какой-то момент это съедает всю вашу память и процессор.

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

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

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

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

Это проиллюстрировано в паре постов, которые вы можете найти здесь и здесь .

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

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

    someFunction() {
        value = getValue()
        print(value)
    }

вам нужно сделать что-то вроде этого:

    someFunction() {
        getAsyncValue(callback)
    }

    callback(myValue) {
        print(myValue);
    }

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

    someFunction() {
        getAsyncValue(valueCallback)
    }

    valueCallback(myValue) {
        getUserFromDbAsync(dbCallBack)
    }

    dbCallback(user) {
        print(user + myValue);
    }

Это комбинация двух вызовов, и ой, как мне пройти мое значение для функции dbCallback ?

Теперь я могу использовать лямбды и замыкания для замораживания плавающих значений внутри кода:

    someFunction() {
        getAsyncValue(myValue -> {
            getUserFromDbAsync(user -> {
                print(user + myValue)
            })
        })
    }

Так лучше? А как насчет обработки ошибок? Легко:

    someFunction() {
        getAsyncValue(myValue -> {
            getUserFromDbAsync(user -> {
                print(user + myValue)
            }).onError(() -> {
                print("can't get user")
            }
        }).onError(() -> {
            print("can't get value")
        })
    }

Что делать, если мне нужно обработать ошибку пользователя БД в моем Значении GetAsync обратном вызове? Взгляните на мою обзорную статью vert.x 2 . Она старая, но в ней изложены некоторые идеи этой статьи.

С реактивными стеками, такими как WebFlux, вы можете использовать связанные псевдофункциональные вызовы, такие как:

    someFunction() {
        getAsyncValue()
            .map(myValue -> print);
    }

Однако затем становится сложнее, когда вы хотите объединить вышеприведенные вызовы и добавить дескриптор ошибок. У вас есть длинная цепочка .карта , Плоская карта и почтовый индекс это не сильно улучшит ситуацию по сравнению с асинхронной версией и плохо по сравнению с синхронной версией. Взгляните на примеры здесь . Фактически, цепочка вызовов одного метода распределена по 12 строкам, включая псевдо-вызовы if-else.

Во всех вышеперечисленных случаях, используя ссылки на функции, лямбды или реактивные расширения, код страдает в:

  • Удобочитаемость
    • Очевидно, что код длиннее
    • Это более сложно
    • У него больше нет последовательного потока. Или, проще говоря, код выполняется в другом порядке, как это записано в источниках.
  • Ремонтопригодность
    • Трудно добавить функциональность
    • Отладка этого кода намного сложнее. Вы не можете просто пройти через это
  • Проверяемость
    • Вы не можете просто издеваться над 1 или 2 классами и вызывать метод. Вам нужно создать объекты, которые ведут себя синхронно, но на самом деле являются асинхронными.
  • Качество кода
    • Поскольку код трудно переварить путем чтения, в нем будет больше ошибок, чем в синхронном коде.
  • Скорость разработки
    • Очень простые задачи, такие как, например, два запроса REST с разными типами возврата, которые должны основываться друг на друге (например, получение основных данных, обогащение некоторыми связанными данными и возврат слияния в пользовательский интерфейс)
    • Проще говоря: вы не можете просто писать так, как вы думаете

Я написал профессиональные приложения с помощью vert.x, RxJava и совсем недавно Spring Web Flux, и вышеперечисленные проблемы можно было найти во всех них в разных вариантах.

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

    someFunction() {
        try {
            myValue = getAsyncValue()
            user = getUserFromDbAsync()
            print(user + myValue)
        } catch (ComputationException e) {
            print("can't get value")
        } catch (DbException e) {
            print("can't get user")            
        }

Теперь у вас будет хороший путь в трех строках и отдельная обработка ошибок. Код очень прост для понимания. Что происходит, так это то, что среда выполнения остановит выполнение getUserFromDbAsync до тех пор, пока БД не вернет данные. В то же время поток будет использоваться для других задач.

Erlang реализует это в своей модели актера и виртуальной машине BEAM. Когда вы читаете из файла, вы просто вызываете io:get_line и получаете результат. Виртуальная машина приостановит работу текущего участника и его процесса Erlang (который представляет собой легкий поток, занимающий всего несколько байт памяти).

Для JVM в настоящее время существует Вид проекта который пытается реализовать продолжения в JVM и абстракцию, называемую Fiber, как часть JDK. На мой взгляд, это выглядит многообещающе, но это займет некоторое время, и я не уверен на 100%, что это когда-нибудь станет частью Java.

В Python есть asyncio , который предоставляет некоторые дополнительные языковые функции, более продвинутые, чем Java, но, тем не менее, он слишком открыт, ИМХО.

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

Взято из моего разговора с Джошем:

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

Проблема в том, что после того, как вы некоторое время писали приложения с помощью реактивного молотка (у которого крутая кривая обучения), вы рассматриваете каждую задачу как гвоздь. Это было бы нормально, если бы качество кода не пострадало, но объективно это так.

И неопытным разработчикам это может показаться крутым, потому что это так трудно понять. Как будто только лучшие разработчики поймут это. Но поверьте мне, это не так. Лучший язык и дизайн API – это те, которые легко понять, написать и прочитать .

Оригинал: “https://dev.to/stealthmusic/dont-drink-too-much-reactive-cool-aid-20lk”