Comments 30
— Вот туда я лопатку уронил!
Я что-то не понял трюк со счетчиком.
- В onRequest делаем increment()
- В onNext делаем get()
Что нам дает get()? Он же может выдать любое значение, в зависимости от того, какие звезды сойдутся.
Реактор — прекрасная концепция. На нём суперски писать новые сервисы с нуля. А потом увольняться, чтобы не дай бог не успели придти с предложением где-нибудь посередине чуть-чуть поменять бизнес-логику.
Осмелюсь возразить, в настоящий момент успешно разрабатываем и поддерживаем 40 микросервисов в проекте :) Spring Boot + Kotlin + Reactor, новые разработчики проходят стадию отрицания при знакомстве с реактором, но потом всем начинает нравиться, когда понимают как его правильно готовить. Проводим периодические семинары и обучение внутри команды.
Типичный сервис по перекладываюнию джейсонов:
Получаем запрос от пользователя.
Пишем-читаем несколько БД или внешних сервисов.
Отдаем результат пользователю.
Подавляющую часть времени этот сервис проводит в вызовах внешних сервисов, а сам ничего не делает. Даем пул побольше и проблема паралельности входящих запросов решена. (не надо прямо так в лоб делать сервисы для миллиона рпс. а вот для тысячи вполне можно)
Дальше надо ускорить ответ пользователю. Паралелим вызовы внешних БД/сервисов там где это можно.
Все. Если код написан более-менее нормально то у нас производительность максимальна для данной архитектуры, нагрузку держим максимальную для данных ядер.
И этот код можно читать и поддерживать без боли. Все понятно и просто. И зачем тут городить все что вы предлагаете? Оно сложно, неочевидно и вызывает проблемы при попытке хотя бы прочитать ваш код.
Вечер добрый, конкретно мы, используем реактор, потому что перед нами стоит задача по разработке системы, которая обрабатывает десятки миллионов транзакций в день, десятки тысяч отчетов, платежных документов и всего сопутствующего. Я не агитирую всех и каждого начать использовать реактор по любому поводу и без. В этой статье я описал ряд проблем и решений, с которыми мы сталкивались и боролись в нашей команде. Надеюсь это сэкономит время и силы другим разработчикам. Реактор позволяет бережно использовать потоки, из коробки дает некоторые интересные возможности, 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 мб/поток?
В 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 применяется в Сбере
Вот не самая лучшая отсылка, ей богу :)
Реактор хорош, спору нет, но только когда не вылезает за границы ниши, в которой он хорош. На "границе сред", где есть ожидание ввода/вывода — да, шикарен. Пробросить с минимальной обработкой из одной трубы в другую — тоже да. Но строить полноценную логику — увольте. Если прям категорически важна легковесная асинхронность посредине — лучше уж в корутины развернуть, тем паче, что у коллег котлин.
Вот как раз подобные, э-э-э, типичные джейсоноперекладывалки замечательно укладываются в асинхронщину. И не пользоваться этим глупо.
И да что в этом коде не так с чтением кода? Замечательно читается и замечательно редактируется. Особенно если вместо Flux взять простые сопрограммы, но это не обязательно.
Про prefetch можете ещё раз объяснить, пожалуйста?
Спасибо за статью. На работе используем стек Spring WebFlux, Reactor и Kotlin. Напили два микросервиса на них. В принципе норм.
А поясните момент, почему не стоит брать concatMap?
Разгоняем REACTOR