Путь самурая: от Servlet к Reactive Programming



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

    Меня зовут Александр, я техлид в команде кабинета участника сделки в ДомКлике. В этой статье я не буду пересказывать документацию по Spring Webflux, она есть и очень подробная. А расскажу о том, как мы полностью перешли на реактивное программирование в нашем проекте, что нас сподвигло на это, и что в итоге получилось.

    Вступление


    Шёл 2018 год…

    У нас примерно 700 (микро)сервисов, применяющих различные технологии и написанных на разных языках программирования. Часть сервисов скрыта от внешнего взора, тогда как другая часть является фронт-офисом для наших клиентов. Одним из таких (микро)сервисов мы и являемся.

    Технологический стек не представляет чего-то необычного для 2018 года: это контейнер сервлетов (Tomcat), Spring Framework (webmvc, data, hibernate, resttemplate и дальше по списку), PostgreSQL в качестве хранилища и RabbitMQ для асинхронного взаимодействия (+ кластер ELK для журналирования). В качестве CI/CD у нас Jenkins, где это всё собирается, пакуется в Docker и доставляется в кластер Kubernetes.


    Мы не являемся высоконагруженным сервисом и обслуживаем в довольно спокойном темпе около 30-40 запросов в секунду на трёх активных подах (по 1 процессору и 2 Гб ОЗУ). Так мы беспроблемно существовали, развивали продукт и добавляли новую функциональность, пока не столкнулись с проблемами, о которых никто и не задумывался…

    Первые звоночки


    В один прекрасный день мы начали ловить в K8s перезагрузки по liveness probe.

    Liveness probe сервиса был настроен таким образом, что учитывал доступность базы данных (SELECT 1) и использовал общий с приложением datasource и пулл коннектов. Расследование инцидента показало, что при очередной проверке K8s мы не могли получить подключение из пулла и возвращали ошибки, что приводило к перезагрузке поды.

    Сначала мы вынесли liveness probe на выделенный datasource, и перезагрузки прекратились. Новый удар прилетел с другой стороны: теперь нам не хватало свободных подключений в пулле для обработки рядовых запросов. Мы начали анализировать код приложения, чтобы понять первоисточник проблемы.

    Оказалось, у нас много мест в коде, где мы в рамках транзакционных методов вызывали блокирующие операции, поэтому подключения к базе удерживались дольше. Там, где это было возможно, мы вынесли внешние обращения за пределы транзакции, но кардинально ситуация не улучшилась. Любое отклонение длительности ответа внешнего ресурса от «нормы» вело к тому, что для новых запросов просто не хватало свободных подключений к базе. Начинали копиться запросы на уровне Tomcat, довольно быстро мы пересекали верхнюю границу в 200 запросов и вставали в очередь, начинались отказы в обслуживании (таймауты).

    Другие (микро)сервисы тоже не стояли на месте — росли, развивались, менялись профили их нагрузки, поэтому длительность ответов начала расти. Постоянно масштабировать размер пулла подключений к базе тоже не вариант: открывать и держать подключение к PostgreSQL довольно дорого, к тому же начинаются проблемы с синхронизацией локальных копий данных с реальными. И мы решили выбросить всё в мусор и начать с чистого листа.

    Время рефакторинга


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

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

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


    Количество потоков в приложении напрямую зависит от количества запросов к системе, но не только. Так как I/O-операции блокируют текущий поток, то любой затык внешнего ресурса экспоненциально увеличивает количество потоков в приложении (так как внешние вызовы продолжают поступать), что рано или поздно приведет к откидыванию запросов по таймауту и снижению доступности системы (Servlet < 3.1 — рано или поздно кончатся потоки у контейнера приложений; Servlet 3.1 поддерживает nio, но приложение начнёт пухнуть по памяти и процессору, и погрязнет в переключениях контекста). Ситуация стала критической когда rediness/liveness probe уже не мог пробиться через очередь подключений.

    Мы начали смотреть в сторону альтернативных подходов.

    Полной противоположностью императивному программированию на servlet является реактивное программирование — это парадигма асинхронного программирования, связанная с потоками данных и распространением изменений. Одной из её реализаций на JVM является Project Reactor, на основе которого построен Spring Webflux.


    При таком подходе не было необходимости плодить потоки в приложении, мы работали с фиксированным количество потоков (Event Loop), обрабатывающих события в порядке очереди. В качестве аналога можно привести классическое асинхронное программирование, но лишенное callback hell. Мы перестали блокироваться, вся система перестала зависеть от скорости внешних ресурсов и стала отзывчивей.

    Вторая проблема — слишком объемные ответы. При использовании servlet'ов мы старались снизить количество обращений к бэкенду за данными, по максимуму насыщая ответы информацией (это не было проблемой, потому что у нас были копии в базе). Переходя на сбор данных по внешним сервисам нам необходимо было на один запрос клиента обратиться к 5-6 внешним системам. Причем большинство запросов были последовательными, что ожидаемо начало драматически сказываться на скорости ответа клиенту. Выход из ситуации — раздробить один большой запрос на кучку мелких, перекладывая ответственность за сбор данных на потребителя. Мы «искусственно» увеличили частоту запросов к бэкенду, но это уже не было проблемой, потому что мы перестали блокироваться и плодить потоки, а фронтенд мог эффективно распараллеливать запросы и запрашивать только ту информацию, которая его интересует в данный момент.

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

    Жизнь после рефакторинга


    К лету 2019 года все запросы с фронтенда нашего кабинета уже шли к новому бэкенду, нагрузка выросла примерно в 2-3 раза по сравнению со старой версией (мы увеличили количество запросов). На текущий момент мы в production уже практически год, и вот некоторые результаты:

    1. У нас не было ни одного отказа из-за приложения (были инфраструктурные проблемы, не более).
    2. Из всех сервисов ДомКлик наш кабинет сейчас показывает самое низкое время до отображения (конечно, не без помощи коллег из фронтенда).
    3. Практически отсутствуют отказы в обслуживании запросов.
    4. Мы стали меньше зависеть от внешних сервисов. Даже если какой-то из них отказывает, мы продолжаем показывать работоспособную часть кабинета.
    5. Синтетические тесты показали, что мы можем держать до 1000 запросов в секунду без драматического увеличения времени ответа при текущей конфигурации (3 поды по 1 процессору и 1,5 Гб ОЗУ). Но в реальности мы, к сожалению, упираемся в быстродействие смежных систем.


    В каждой бочке есть...


    В завершение я бы хотел немного рассказать о проблемах и сложностях, с которым мы столкнулись.

    Блокирующие вызовы


    Если у тебя фиксированный event loop, который отвечает за обработку всего и вся, то любая случайная блокировка этого потока моментально приводит к фатальным последствиям. Необходимо убедиться, что используемая функциональность не блокирующая. А если всё-таки блокирующая, то не забывать уводить выполнение в соответствующий scheduler (обычно Elastic). Для поиска блокирующих вызовов есть полезный инструмент Blockhound.

    Отсутствие поддержки реляционных баз


    К моменту написания этой статьи проблема уже неактуальна, потому что вышла стабильная версия драйвера R2DBC. Но когда его не было, проект пришлось перевести на MongoDB, либо мучиться с отдельным пуллом потоков для взаимодействия с БД.

    Отладка


    Логи важны, особенно когда произошло что-то неожиданное. Это необходимое средство отладки. Если вы начинаете писать проект на Spring Webflux, то будьте готовы к тому, что придется подключать к приложению дополнительный агент — Reactor Debug Agent (и в проде тоже), иначе логи будут выглядеть так:

    Запрещено для детей, беременных женщин, пожилых и людей с заболеваниями сердца
    java.lang.IndexOutOfBoundsException: Source emitted more than one item
    	at reactor.core.publisher.MonoSingle$SingleSubscriber.onNext(MonoSingle.java:129)
    	at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:445)
    	at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:379)
    	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:121)
    	at reactor.core.publisher.FluxRange$RangeSubscription.slowPath(FluxRange.java:154)
    	at reactor.core.publisher.FluxRange$RangeSubscription.request(FluxRange.java:109)
    	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:162)
    	at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:332)
    	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:90)
    	at reactor.core.publisher.FluxRange.subscribe(FluxRange.java:68)
    	at reactor.core.publisher.FluxMapFuseable.subscribe(FluxMapFuseable.java:63)
    	at reactor.core.publisher.FluxFlatMap.subscribe(FluxFlatMap.java:97)
    	at reactor.core.publisher.MonoSingle.subscribe(MonoSingle.java:58)
    	at reactor.core.publisher.Mono.subscribe(Mono.java:3096)
    	at reactor.core.publisher.Mono.subscribeWith(Mono.java:3204)
    	at reactor.core.publisher.Mono.subscribe(Mono.java:3090)
    	at reactor.core.publisher.Mono.subscribe(Mono.java:3057)
    	at reactor.core.publisher.Mono.subscribe(Mono.java:3029)
            ...
    


    Польза от такого лога сомнительна, своего кода в трассировке стека вы не увидите.

    Если вы привыкли использовать АРМ и это не New Relic/Dynatrace или другие коммерческие мастодонты, то можете выкинуть его сразу — адекватной инструментации Netty и Spring Webflux практически нигде нету.

    Качество кода


    Реактивный подход заставляет писать код по-другому. Если вы привыкли писать в императивном стиле и, не дай бог, в enterprise-стиле, то перестроится довольно сложно. При оформлении кода в виде стримов неудобно реализовывать сложную логику, логику с ветвлением или пробрасывать какое-либо значение из начала в конец стрима. Приходится пристальнее следить за качеством кода, а программистам — перестраиваться. У нас поначалу выходило так себе…

    Еще не самый худший пример
    Flux.fromIterable(participants)
        .flatMap { participant -> getPerson(participant).map { PersonInfo(participant, it) } }
        .collectList()
        .flatMapMany { recipients ->
            val borrower = recipients
                .filter { it.participant.role == ParticipantRole.BORROWER }
                .map { it.person }
                .firstOrNull() ?: throw BusinessException(SystemError.BORROWER_NOT_FOUND)
    
            recipients
                .filter { if (currentParticipant == null) true else currentParticipant.casId == it.participant.casId }
                .distinctBy { it.person.confirmedPhone }
                .toFlux()
                .flatMap { recipient ->
                    val template = templateMapping.getValue(recipient.participant.role)
                    createNotification(dealId, message, borrower, template, recipient.person)
                }
        }
        .flatMap { notification ->
            logger.info { "Sending sms $notification to ${notification.recipient.casId}" }
            notificationClient.send(notification).map {
                notification
            }
        }
        .onErrorContinue { exception, obj ->
            handleException(obj, message, exception)
        }
        .onErrorResume {
            handleException(null, message, it)
            Flux.empty()
        }
    


    С выходом Spring Boot 2.2.0 разработчики добавили поддержку Kotlin Coroutines взамен Flux/Mono, и стало намного удобнее писать, читать и поддерживать код. Сейчас мы активно переходим на корутины и, вполне вероятно, опишем свой опыт в следующей статье.
    ДомКлик
    Место силы

    Комментарии 24

      0
      Спасибо за статью! Разве у томката нельзя настроить размер тред пула и если он весь исчерпан складывать сообщения в очередь на обработку?
        +1
        Конечно можно, но если внешние ресурсы деградируют, то это как снежный ком (старые запросы в ожидании, новые порождают еще большую нагрузку на внешние системы). Очередь будет разрастаться и коннекты начнут отпадать по таймауту на уровне клиента/nginx так и не попав в обработку.

        Проблема еще кроется в том, что очередь общая и есть запросы, которые не требуют обращения к внешним ресурсам и быстро обрабатываются. Они также начнут падать. Особенно остро это касается health-чеков k8s, которые отваливались находясь в очереди и прибивали поду.
          0
          Так это у вас с перфомансом проблемы были, а не с потоками.
          Почитайте что-нибудь по теории массового обслуживания.
          Очередь может увеличиваться, только если ресурсов меньше, чем требуется.
          Надо было открыть профайлер и разобраться почему 40 запросов в секунду, которые не считают параметры чёрных дыр, так нагружают сервера

          И правильно, что health-check прибивается — какой же это health, хотя «быстрые» запросы можно на уровне прокси пропускать.

          Я уверен, что пока вы свою архитектуру переделывали, кто-то в вашей команде тупо пофиксил перформанс. А вы этого даже и не заметили.

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

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

            Не понял, что же правильно в том, что рабочее приложение ложится по health check? Мы же тем самым только усугубляем ситуацию.

            Что касается множества запросов, то этим мы:
            1. Ускоряем загрузку фронта. Когда мы получаем всю информацию для страницы в одном запросе, то время ответа будет равно времени самого медленного ресурса, к которому мы сходили за данными, в нашем же случае фронт отрисовывается по мере получения порций данных.
            2. Отказоустойчивость. Если один из ресурсов так и не ответил — не беда, мы отрисуем страницу с доступной информацией и покажем клиенту, что вот эти данные сейчас недоступны попробуйте позже.
            3. Проще управлять и развивать.
              0
              Ну вы точно также могли бы и на бэкенде подождать ответа и, если ответ не пришёл, то записать null в соответствующее поле DTO. Ваше решение выглядит как перекладывание проблем с бакэнда на UI.

              У UI разработчиков и так (как правило) самая сложная работа, так теперь им ещё
              надо ломать голову о том, как правильно ошибки обрабатывать от ваших 700 микросервисов.
                +1
                В общем случае null <> ошибка. Конечно, можно это обойти, но выглядеть это в модели данных будет не очень.

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

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

                Вообще это конечно две крайности (1 эндпоит который тащит все vs 100500 эндпоинтов на каждый чих), а правда я считаю находится где-то посередине и это разумное разделение, при котором фронт может запросить ровно те данные, которые ему нужны в данный момент.
                  0
                  Если сервис затупил, то ничего ждать не надо. Ваш сервис фасад просто пометит его как недоступный и не будет обращаться некоторое время.
                  Будет вместо актуальных данных какой-нибудь null вставлять, либо старые данные

                    +1
                    Отдача кэшированных данных для нас огромная проблема ибо каналов информирования клиента об изменении ключевых данных очень много и клиент, что логично, пытается зайти и увидеть эти данные. Если мы начнем после уведомления показывать старые данные — это проблема.

                    По circuit breaker.
                    Это все отлично работает при параллельных запросах, а не последовательных, логика формирования ответа, который зависит от нескольких источников очень сильно усложняется в ситуациях, когда вам нужно поддерживать более 1 версии API с разной логикой и форматом.
        0
        Почему на сервлетах >3.1 программа начинает пухнуть, захлебываясь в переключениях контекста, а rx этой проблемы лишен? Кажется, при большом количестве подключений сервис также должен начать деградировать, причем на первый взгляд кажется, что порядок запросов должен быть сравним
          +1
          С приходом спецификации Servlet 3.1 мы получили NIO при обработки запросов, но проблема в том, что в WebMVC API до сих пор много блокирующего кода, стандартные модули Spring'а и драйвера в них также блокирующие. По своей сути перехода на контейнеры Servlet 3.1 вообще ничего не меняют, у нас также куча блокировок, но зато теперь мы можем возвращать CompletableFuture в контроллерах…
          0

          не пользуйте R2DBC в продакшене, он медленный и еще не стабильный.
          для постгреса наилучший вариант https://vertx.io/docs/vertx-pg-client/java/


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


          Hooks.onEachOperator(...);

          Оно нормально работает, но меня напрягает что логи пишутся в некоторых случаях, а оборачиваем в свою логику по определению и выставлению MDC мы для кадого Mono/Flux

            +2
            Насколько мне известно, это пока единственный вариант поддержки MDC, его мы и используем.
            Для варианта с корутинами есть kotlinx-coroutines-slf4j. Пока мы его не пробовали, но что-то мне подсказывает, что там не все так гладко с интеграцией между CoroutineContext и ReactorContext.

            Спасибо за отзыв по R2DBC, у нас есть мотивация его протестировать, вероятно будет материал для статьи по этому поводу.
              0
              MDC можно держать в контексте запроса и вытаскивать по необходимости. Пример из документации.
                0

                Это не особо удобно, сравните


                public static <T> Consumer<Signal<T>> logOnNext(Consumer<T> logStatement) {
                    return signal -> {
                        if (!signal.isOnNext()) return; 
                        Optional<String> toPutInMdc = signal.getContext().getOrEmpty("CONTEXT_KEY"); 
                
                        toPutInMdc.ifPresentOrElse(tpim -> {
                            try (MDC.MDCCloseable cMdc = MDC.putCloseable("MDC_KEY", tpim)) { 
                                logStatement.accept(signal.get()); 
                            }
                        },
                        () -> logStatement.accept(signal.get())); 
                    };
                }

                с просто log.info('some'):
                так же зачастую надо логировать не только на onNext но и внутри flatMapи тогда приходиться закручивать все в


                Mono.subscriberContext()
                                .flatMap(context -> {
                                    val mdcValues = signal.getContext().getOrEmpty("MDC_KEY");
                                    MDC.setContextMap(mdcValues);
                                    ...
                                });

                или map тогда надо брать и переделывать ее на flatMap
                вообщем вместо простой вещи получается лапша за которой прячется бизнес логика.

                  0
                  Это не особо удобно, сравните

                  Это один раз засовывается в методы в какой-нибудь util-либе и везде потом используется. У нас это выглядит примерно так:
                  .doOnEach(mdcNext(() -> log.info("in")))
                  //какие-то действия
                  .doOnEach(mdcComplete(() -> log.info("out")))
                  .doOnEach(mdcError(e -> log.error("thrown {}", e.getMessage())))
                  

                  так же зачастую надо логировать не только на onNext но и внутри flatMap

                  Тут согласен, в таких ситуациях приходится оборачивать в Mono.subscriberContext(), но такое встречается нечасто и несильно напрягает.
                  Основной плюс такого подхода — минимум оверхеда.
                    0

                    У нас в коде наоборот, onNext не особо надо логировать, а вот в местах бизнес логики такого много.


                    Тут согласен, в таких ситуациях приходится оборачивать в Mono.subscriberContext(), но такое встречается нечасто и несильно напрягает.
              +1
              А можно чуть подробнее про R2DBC? Вопрос для нас действительно актуальный. Насколько проседает производительность, есть ли бенчмарки по R2DBC?
              И что с ним в плане стабильности? Недавно вышел spring boot 2.3 в него как раз вошел r2dbc, по всей видимости ребята из pivotal посчитали его готовым к использованию в продакшене.

              Не впервые слышу про медлительность R2DBC, но пока не видел тому подтверждений, при все этом его использование много приятнее альтернатив учитывает интеграцию со spring data.
                0

                Вот в подобной теме ссылки дали
                https://habr.com/ru/post/500446/#comment_21678412


                или вот еще пример https://github.com/r2dbc/r2dbc-postgresql/issues/138


                Я пытался к проду прикрутить где то пару месяцев назад и вылазили странные ошибки. Например возникала ошибка что данных для Моно есть несколько а не одно как должно быть Ошибки поправили, но мне не хотелось выкатывать такое нестабильное поведение на прод.
                Поэтому просто сделал прослойку которая заворачивает vert.x постгрес клиент в Mono/Flux и живу горя не зная.

              0
              Мы не являемся высоконагруженным сервисом и обслуживаем в довольно спокойном темпе около 30-40 запросов в секунду на трёх активных подах (по 1 процессору и 2 Гб ОЗУ).

              Тоесть вы запускали многопоточное приложение на 1 процессоре и, удивительно, оно работало не очень. Конечно, подход с event loop работает лучше в таких условиях. Только вот это уже не Kotlin, а не пойми что
                  .onErrorContinue { exception, obj ->
                      handleException(obj, message, exception)
                  }
                  .onErrorResume {
                      handleException(null, message, it)
                      Flux.empty()
                  }

              Например, нет exception-ов. Чем отличается onErrorContinue от onErrorResume неясно. Ну тоесть вы добровольно отказались от возможностей языка и пишете программы на языке фреймворка.
                +1
                Только вот это уже не Kotlin, а не пойми что
                Например, нет exception-ов.

                Тут я вас не понял, это все тот же Kotlin. Exception-ы есть, но также и дополнительный механизм работы с ним. Если вы, к примеру, используете Java Streams — это тоже не Java?

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

                Тут вопрос не в том сколько процессоров, а в том что его утилизация была в районе плинтуса. Мы можем налить туда процессоров, памяти, поднять стоимость эксплуатации, но зачем если можно гораздо эффективнее утилизировать ресурсы.
                  0
                  Если вы, к примеру, используете Java Streams — это тоже не Java?

                  Ну Java Streams прямо скажем такая себе, очень спорная Java
                  Exception-ы есть, но также и дополнительный механизм работы с ним.

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

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

                    Расскажу за свой пример никак не связанный с авторским.
                    Есть сервис который должен отсылать сотни пуш нотификаций на мобильные устройства в секунду. и есть внешний провайдер через который это надо делать, с ним подписан контракт и на его мобильный сдк завязано приложение и с него не спрыгнуть. И для этого провайдера вполне нормальная ситуация когда он вместо ~100ms на запрос делает по 6-20 сек. И пофиг сколько я процессоров накидаю в микросервис, все просто будет отжирать память и ждать ответа. Так же забиваются хттп потоки для веб сервера и он просто не сможет обрабатывать новые входящие сообщения. И простое добавление реактивщины избавляет от этих проблем — тк надо всего пару потоков на асинхронный прием запроса, обработки и потом асинхронный запрос к провайдеру.


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

                0
                Нам очень полезно логировать входящие/исходящие запросы, так как это существенно облегчает коммуникацию со внешними системами при выяснении, из-за чего возникла та или иная ошибка. Насколько я понимаю, в реактивном подходе это означает блокировку.
                Логируете ли вы запросы? Я имею ввиду тело запроса.
                  +1

                  Да, действительно, есть проблема с логированием тела запроса к другому ресурсу, но в нашем случаем мы вполне обходимся логированием входного DTO на уровне клиента (передаваемый как аргумент). Заголовки можно достать через фильтр.


                  Для входящих запросов мы обходимся логами на UI. Приходится идти на компромиссы.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое