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

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

К картинке с реактором так и напрашивается подпись (извините не сдержался):
— Вот туда я лопатку уронил!

Я что-то не понял трюк со счетчиком.


  1. В onRequest делаем increment()
  2. В onNext делаем get()

Что нам дает get()? Он же может выдать любое значение, в зависимости от того, какие звезды сойдутся.

Дает текущее значение счетчика, которое будет равно количеству одновременно существующих (параллельных) задач в реакторе на конкретный момент когда был сделан get(), удобно мониторить текущее состояние

Реактор — прекрасная концепция. На нём суперски писать новые сервисы с нуля. А потом увольняться, чтобы не дай бог не успели придти с предложением где-нибудь посередине чуть-чуть поменять бизнес-логику.

Осмелюсь возразить, в настоящий момент успешно разрабатываем и поддерживаем 40 микросервисов в проекте :) Spring Boot + Kotlin + Reactor, новые разработчики проходят стадию отрицания при знакомстве с реактором, но потом всем начинает нравиться, когда понимают как его правильно готовить. Проводим периодические семинары и обучение внутри команды.

А зачем все это? Чтобы потешить самолюбие и написать write only код это понятно. А кроме этого зачем?

Типичный сервис по перекладываюнию джейсонов:

Получаем запрос от пользователя.
Пишем-читаем несколько БД или внешних сервисов.
Отдаем результат пользователю.

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

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

Вечер добрый, конкретно мы, используем реактор, потому что перед нами стоит задача по разработке системы, которая обрабатывает десятки миллионов транзакций в день, десятки тысяч отчетов, платежных документов и всего сопутствующего. Я не агитирую всех и каждого начать использовать реактор по любому поводу и без. В этой статье я описал ряд проблем и решений, с которыми мы сталкивались и боролись в нашей команде. Надеюсь это сэкономит время и силы другим разработчикам. Реактор позволяет бережно использовать потоки, из коробки дает некоторые интересные возможности, retry / backpressure / cancel обработчики и т.п. Удачно подходит для микросервисной архитектуры. Код кажется write-only, нечитаемым, одноразовым, потому что здесь происходит смена парадигмы программирования. Императивное -> Функциональное -> Реактивное. Для тех кто работает с реактором достаточное количество времени, все выглядит вполне "maintainable".

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

Compared to traditional imperative and functional programming, reactive programming requires a mindset-shift in order to apply the concepts and techniques effectively. The benefits we gain support us in some key challenges that every engineer is facing with essentially every (micro-) service in today’s backend architectures: handling of blocking IO, backpressure, managing highly varying loads as well as message and error propagation.

А зачем вы экономите потоки? Есть фонд какой-то который их собирает или что?
Типичные 500-1000, да даже 5000 потоков Джава переваривает достаточно спокойно. Куда вам больше?

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

Императивное -> Функциональное -> Реактивное

Это так не работает. Даже более менее чистое функциональное программирование массово не нужно оказалось. Элементы и куски — да, очень удобно. Но не более того.

А Реактор с 5 летней историей вообще никак не взлетает. Срок вполне достаточный.

Если стоит вопрос, экономить ресурсы или нет, я обычно выбираю экономить. В своей практике периодически сталкиваюсь с OOM, и в долгосрочной перспективе выбираю оптимизацию и рефакторинг вместо "завалить железом". Не считаю удачной идеей в 5000 потоков опрашивать микросервисы, когда могу сделать это с помощью 1. Не считаю удачной идеей открывать 5000 коннектов к базе. Считаю что разработчик должен полностью контролировать ресурсы, которые использует его приложение. Создание потока, операция затратная, также существует понятие context switch. Сталкивался с ситуациями когда при большой нагрузке веб-сервер начинает реджектить запросы, упирается в лимиты сессий. Сталкивался с ситуациями когда система теряет стабильность из-за того что один микросервис выходит из под контроля, превышая разумные рамки по созданию файловых дескрипторов. Считаю что если приложение работает используя 5000 потоков, или даже 1000 потоков, то с ним что-то не в порядке, пока в своем опыте не встречал необходимости так тратить ресурсы.

Реактор развивается и обновляется, не вижу с этим каких-либо проблем. Возможно, он не взлетает в Ваших проектах, у нас взлетел.

Если стоит вопрос, экономить ресурсы или нет, я обычно выбираю экономить. В своей практике периодически сталкиваюсь с OOM, и в долгосрочной перспективе выбираю оптимизацию и рефакторинг вместо «завалить железом».

Конечно, ресурсы экономить надо. ЦПУ, РАМ. Они денег стоят.
А потоки здесь при чем? Поток для себя забирает примерно 16кб памяти. 5000 потоков заберут примерно 80 мегабайт. Столько потоков бывает в хм большом и нагруженном микросервисе. Там 80 мегабайт на фоне общего потребления потеряются.

Не считаю удачной идеей открывать 5000 коннектов к базе.

Конечно, поэтому придумали пулы.

Создание потока, операция затратная, также существует понятие context switch.

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

Считаю что если приложение работает используя 5000 потоков, или даже 1000 потоков, то с ним что-то не в порядке, пока в своем опыте не встречал необходимости так тратить ресурсы.

Возьмем популярный веб сервер jetty. У него поток на каждого клиента. Сотни выбираются сразу. До тысячи добраться легко.
Вы считаете что с разработчиками jetty что-то не в порядке, они не умеют считать ресурсы и написали код неоптимально?

Реактор развивается и обновляется, не вижу с этим каких-либо проблем. Возможно, он не взлетает в Ваших проектах, у нас взлетел.

Он в мире не взлетает. Процент использования в больших проектах что-то около нуля.

Какие-то странные у Вас потоки, по 16 кб. А стек по умолчанию на 1 мб/поток?

Вы все еще сидите на jdk8? Сочуствую, но пора обновляться.
В jdk11 уже нет никакого мегабайта.

Истина где-то посередине


$ java -Xss16k -version

The Java thread stack size specified is too small. Specify at least 136k
Error: Could not create the Java Virtual Machine.

java у меня 11-я

Это вы максимум ограничили. При старте потока столько не выделяется.
Реально выделяется что-то ближе к моим цифрам.

Примерно так посмотреть можно java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -version

У меня вот так получилось
- Thread (reserved=16454KB, committed=590KB)
(thread #16)
(stack: reserved=16384KB, committed=520KB)
(malloc=53KB #98)
(arena=17KB #30)

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

Зарезервированная память != использованная память.
jdk научилась очень оптимально в этом месте память тратить.

Вот тут почитать можно dzone.com/articles/how-much-memory-does-a-java-thread-take

Ну хотя бы committed уже использованная память? А там 520 Кб, как раз среднее между вашей оценкой и оценкой вашего собеседника.

Не совсем. Это реально использованая память в среднем на поток. Потоки что-то делают и потребляют память. Даже в таком пустом примере.

Для сборки честного примера сколько требует один ничего не делающий поток надо сделать что-то вроде пула тысяч на 10 потоков которые не делают ничего. И вывести аналогичную статистику.
Я подозреваю что она даже от ОС зависеть будет.

Скоро соберу такой пример для иллюстрации… Действительно неочевидное место.
Как и обещал более честный пример:
java -version
openjdk version "11.0.10" 2021-01-19
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.10+9)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.10+9, mixed mode)


Код примера:
    static ThreadPoolExecutor tpe = (ThreadPoolExecutor) Executors.newFixedThreadPool(10000);
    static Object lock = new Object();


    public static void main(String[] args) throws IOException {

        synchronized (lock) {
            for(int i=0; i<10000; ++i) {
                tpe.submit(() -> {
                    synchronized (lock) {
                        System.out.println("newer happend");
                    }
                });
            }

            System.exit(0);
        }
    }


Параметры VM
-Xms1G
-Xmx1G
-XX:+UnlockDiagnosticVMOptions
-XX:NativeMemoryTracking=summary
-XX:+PrintNMTStatistics


Результат
- Thread (reserved=10304449KB, committed=663961KB)
(thread #10018)
(stack: reserved=10258432KB, committed=617944KB)
(malloc=34278KB #60110)
(arena=11739KB #20035)


663961KB на 10_000 потоков или 66 килобайт на поток. На самом деле еще немного меньше, там на самом деле больше потоков. Но это уже не принципиально. Порядок примерно такой.

Расходы с которыми можно смириться.

Алексей «Наше Все» Шипилёв такие тесты не одобряет, но порядок оверхера на поток понять хватит.

PS: Ради интереса на 15 и на 17 jdk прогнал тоже самое. Результат примерно такой же.

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

Я лично в своих проектах (джава 15, к слову) уменьшаю Xss до 512 кб, потому что уменьшать дальше страшновато. То, что commited != max это понятно и замечательно.

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

Неправильный тезис был такой: Поток потребляет мегабайт или около того. Просто так. Сам по себе. И значит их стоит экономить.
Возьмем популярный веб сервер jetty. У него поток на каждого клиента. Сотни выбираются сразу. До тысячи добраться легко.
Вы считаете что с разработчиками jetty что-то не в порядке, они не умеют считать ресурсы и написали код неоптимально?

Даже в старых версиях Jetty на каждого клиента поток не выделялся, он брался из пула с верхним лимитом по умолчанию равным 200. Пул можно раздуть, конечно, но это всё равно не позволяло справиться с проблемой c10k. Начиная с версии 9.3 под капотом у них мультиплексирование неблокирующихся сокетов, а пулы потоков используются только для поддержки спецификации сервлетов. Причём они писали, что активно экспериментируют с реактивным подходом для разработки более удобного API и упрощения кода. Проще говоря, сами разработчики Jetty знают, как писать сопровождаемый и производительный код, но своим пользователям предоставляют возможность писать иначе.

Он в мире не взлетает. Процент использования в больших проектах что-то около нуля.

Возможно, такое впечатление у вас сложилось потому, что Spring Reactor приходится конкурировать с более зрелым Akka Streams в достаточно узкой нише высоконагруженных проектов. Или потому, что вы просто не знаете о всех случаях его успешного применения. Например Spring Reactor применяется в Сбере, у которого проекты несомненно большие.
Даже в старых версиях Jetty на каждого клиента поток не выделялся, он брался из пула с верхним лимитом по умолчанию равным 200.

А где я говорил слово выделяется? Естественно там пул. Настраиваемый.

Поток используется. Он именно используется для работы, не для поддержки чего-то там. В 9.х все тоже самое.
support.sonatype.com/hc/en-us/articles/360000744687-Understanding-Eclipse-Jetty-9-4-8-Thread-Allocation
webtide.com/thread-starvation-with-eat-what-you-kill-2

в достаточно узкой нише высоконагруженных проектов

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

Например Spring Reactor применяется в Сбере, у которого проекты несомненно большие.

Так себе пример. У них нет ни одного удачного проекта, кроме собственно банка.
Как раз то место где можно писать write only код, а потом следующие перепишут. Или проект просто умрет.
Например Spring Reactor применяется в Сбере

Вот не самая лучшая отсылка, ей богу :)


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

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

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

И да что в этом коде не так с чтением кода? Замечательно читается и замечательно редактируется. Особенно если вместо Flux взять простые сопрограммы, но это не обязательно.

Про prefetch можете ещё раз объяснить, пожалуйста?

Прелесть статьи в том, что ее можно перечитать :) задайте конкретные вопросы, что не понятно, попробуем разобраться

Спасибо за статью. На работе используем стек Spring WebFlux, Reactor и Kotlin. Напили два микросервиса на них. В принципе норм.

А поясните момент, почему не стоит брать concatMap?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий