Комментарии 15
микросервисы в 2024, ага
пора уже признать, что в большинстве случаев они не полезны а вредны
Спорное утверждение, да и заметка немного о другом :)
Что в качестве альтернативы рекомендуете? Не монолиты же?
Вы сначала мне микросервисы продайте. Покажите обоснование для перехода
Скажите в каких проектах переход с монолита на микросервисы показал себя положительно - специфика, масштаб, команды, стек
То есть плюсы перевесили минусы
Заодно про деньги расскажите или другие KPI - как вы отказались от одного сервера и нагрузили 10
Я не говорю, что везде нужны монолиты, всего лишь в 95%
А то читаю на highload.ru (не дословно) когда в Леруа Россия оборот превысил 10 млрд р в год мы задумались о том, чтобы перевести часть нагрузки (не критичной для бизнеса) в отдельные микросервисы
Разумеется, и здесь есть свои подводные камни, связанные с ростом потребления памяти для создания множества потоков и эффективностью распараллеливания (ядер может не хватить на всех).
Ядер точно не хватит). Ну и регулярные OOM, кажется, не очень коррелируют с
Для нашего сценария этот исполнитель подходит как нельзя лучше
Может WebFlux?
Спасибо, по java всегда интересно.
Там в git опечатка "Описение апи:".
Мне кажется, я бы в такой ситуации сразу начал рисовать график запросов и доказывать, что лучше не батчи по 10, а семафором каким-нибудь ограничить, чтобы в моменте было не более 10. Сергей, не задавал такой вопрос? )
Точно нет, я бы запомнил и добавил в статью. А как бы вы семафором ограничили?
Если держать в голове, что у нас его виртуальные треды, можно завести бесконечный тредпул, их плодить бесщадно на каждый запрос запрошенное N число, а в потоках блокироваться на семафоре. В зависимости от обсуждения с собеседующим можно семафор сделать или глобальный, если хочется честно лимитировать число запросов от нашего сервиса к целевому, или на внешний запрос, если "чуть более честно следовать букве ТЗ".
Если виртуальных тредов нет, можно поинтересоваться, не предполагается ли, что у нас всегда достаточно памяти под треды, и авось их тоже можно плодить.
В общем, мой коммент скорее про то, что перед реализацией можно ещё больше деталей уточнить, которые могут дать пространство для манёвра.
Очень поверхностный разбор ошибок и анализ проблемы приведен в вашей статье. Обычно после собеса есть время еще раз обдумать на холодную голову и хотя бы для анализа на Habr продемонстрировать более совершенное решение. На основании которого можно показать другим как делать правильно. Но в статье сплошные антипаттерны на тему как делать не надо, подаются под соусом "вот так правильно".
Начнем по порядку:
Кастомный ExecutorService
Если есть необходимость создать свой собтсвенный пул потоков в spring, то делать это небходимо через его контекст. Тогда не надо будет думать над тем чтобы его завершить и делать костыли вида - @PreDestroy. Т.к. этот бин находить в контексте spring, то он умеет управлять его жизненым циклом и вызовет у него при завершении метод shutdown. Вот тут сделал пример, где показано как это надо делать правильно и в консоль при завершении приложения будет выведено - customShutdown.
Graceful shutdown
Spring уже сам все умеет и не надо делать опять костыли вида - executor.awaitTermination.
достаточно добавить настройку (server.shutdown=graceful). Пример опять тут. Если запустить приложение и вызвать http://localhost:8080/test и пока сервис уснул на 5сек, остановить приложение, то в логах увидим:
2024-01-21 00:00:07.728 WARN 448719 --- [ customThread-1] c.e.demo.DemoApplication$DemoController : Execute start method - customThread-1
2024-01-21 00:00:10.687 INFO 448719 --- [ionShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
2024-01-21 00:00:12.728 WARN 448719 --- [ customThread-1] c.e.demo.DemoApplication$DemoController : Execute end method - customThread-1
2024-01-21 00:00:12.744 INFO 448719 --- [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown complete
customShutdown
откуда видно:
07.728: началось выполнение метода
10.687: поступил сигнал на заврешение приложение. запустился GracefulShutdown
12.728: дождались завершения уже запущенного метода
12.744: приложение завершилось и в консоль вывелось customShutdown (Spring сам "погасил" наш пул)
Executors.newCachedThreadPool()
Я понимаю, что за час трудно написать идеальное решение. но мне кажется этого и не требовалось. По моему мнению идея была в том, что за час вы напишете простое решение - примерно как вы и сделали, а далее уже непосредственно на устном собеседовании придете к около идеальному решению. Сам бывал на подобного рода собеседованиях, только там уже давали исходный код/проект который нужно было улучшить. Но вы и близко не приблизились к нормальному решению, даже сделав разбор задания уже после окончания собеседования. И оффер скорее всего вам не сделали. Решение вида newCachedThreadPool это извините... как стрелять из пушки по воробьям. А суть пробелмы в том что большую часть времемни приложение затрачивает на то чтобы ждать ответ по сети, а процессорные такты тратит только на маппинг входных данных и респонса, а это очень простые операции, время выполнения которых ничтожно мало и им можно пренебречь условно принять его за 0. И с этим прекрасно справится один поток - да, да.. тот самый event loop.
Правильное решение должно было быть в применении одной из технологий:
1) обычные java CompletableFuture и ассинхроный неблокирующего хттп клиент который уже есть начиная с jdk11
2) spring reactor/webClient
3) coroutines/loom
Ну и чтобы не сотрясать воздух, привожу простое решение с использованием CompletableFuture где на одном потоке свободно обрабатывается 1000 запросов в параллель за 1сек. Я сделал простые два веб метода: 1) http://localhost:8080/testHttpAsync?n=1000 - запускает в паралель n запросов и 2) http://localhost:8080/testHttpAsyncBatch?batchSize=10&n=10 - выполняет последовательно batchSize пачек, в каждой пачке по n запросов в параллель(как у вас в задании), только мне лень было заниматься маппингом и точным расчетом батчей, поэтому просто вынес их в параметры метода.
Итого на примере метода testHttpAsync видно что запрос с n=1 отрабатывает 1сек и с n=1000 тоже примерно 1сек.
И все это спокойно работает на одном потоке customThread-1 (но в реальном мире надо еще следить за памятью чтобы не было OOM)
P.S. Никого не хотел обидеть, я за конструктивную критику. Тесты тоже было делать лень, можете сами поиграться на моих примерах.
Благодарю за развёрнутый комментарий, плюсанул.
Но в статье сплошные антипаттерны на тему как делать не надо, подаются под соусом "вот так правильно"
Возможно мне стоило указать это прямо, но статья как раз о моих ошибках и о том, как делать не надо :) Executors.newCachedThreadPool()
использован исключительно для контраста с fixedThreadPool()
Если есть необходимость создать свой собтсвенный пул потоков в spring, то делать это небходимо через его контекст. Тогда не надо будет думать над тем чтобы его завершить и делать костыли вида - @PreDestroy
В том-то и дело, что если мы говорим об ExecutorService
, то graceful shutdown для него нужно прописывать руками безотносительно того, внедряется ли он через контекст или напрямую (как в моём примере). То, о чём вы говорите - это TaskExecutor
, вот он управляется контекстом и гарантирует правильное завершение всех подзадач. В моём случае ExecutorService
использовался по одной причине - я просто лучше знаю его АПИ, что было важно в условиях цейтнота.
Spring уже сам все умеет и не надо делать опять костыли вида - executor.awaitTermination.достаточно добавить настройку (server.shutdown=graceful)
В указанном вами примере речь опять же идёт о ThreadPoolTaskExecutor
.
И оффер скорее всего вам не сделали
Оффер дали, но как и написал в первом предложении намерения уходить с текущего места не было, да и по деньгам предлагали сильно меньше желаемого.
1) обычные java CompletableFuture и ассинхроный неблокирующего хттп клиент который уже есть начиная с jdk11
2) spring reactor/webClient
3) coroutines/loom
По пунктам 2 и 3 согласен, по 1 скорее нет, т.к. предложенное вами решение очень громоздкое и трудночитаемое. Если есть сильное стремление использовать реактивщину, то почему бы не использовать reactive feign?
Ожидаю плодотворной дискуссии :)
Возможно мне стоило указать это прямо, но статья как раз о моих ошибках
Название статьи намекает на некий "разбор решения", что подразумевает то о чем я уже писал. Что после окончания собеса, можно уже с холодной головой осмыслисть это еще раз и сделать вывод о том что правильнее было бы сделать вот так, хотя бы для Habr, чтобы показать людям какие есть грабли и как делать правильно. Но в итоге разбор очень поверхностный, который соответсвует начальному уровню.
Задача при всей ее кажущейся простоте имеет большой маневр для копания в глубь. О чем я также написал в своем комментарии - "А суть пробелмы в том что большую часть времемни приложение затрачивает на то чтобы ждать ответ по сети". А у вас нет ни малейшего намека на это. Я так думаю (на самом деле уверен) что работадель при оценке этой задаче смотрит именно на это - на этапе устного разбора решения - увидит ли кандитат основную проблему или нет. И предложит ли хотя бы на словах примерные пути решения. И именно от этого зависит размер оффера и позиция которую предложат соискателю.
В том-то и дело, что если мы говорим об
ExecutorService
, то graceful shutdown для него нужно прописывать руками безотносительно того, внедряется ли он через контекст или напрямую
Вы проверяли? Специально для вас сделал коммит, который показывает что это не так. Graceful shutdown также прекрасно работает.
По пунктам 2 и 3 согласен, по 1 скорее нет, т.к. предложенное вами решение очень громоздкое и трудночитаемое
Выбирать решение это уже дело вкуса. Главное чтобы оно решало основную проблему и было неблокирующим. По поводу избыточности замечу что мой метод combineHttpRequests, что ваш getJokesInBatch, которые отвечают за отправку запросов в параллель, занимают примерно одинаково место. И с тем же Spring Reactor будет также - кобинирование операторов Mono в fluent стиле, также как у меня с CompletableFuture.
Я написал код так, чтобы показать что решить задачу можно на чистом JDK(11 и выше) с минимумом сторонних фреймворков.
Если есть сильное стремление использовать реактивщину, то почему бы не использовать reactive feign
Как я уже писал - это дело вкуса. Можно использовать абсолютно любой клиент какой нравится, главное чтобы он был неблокирующим.
Задача на собеседовании, её решение и его разбор