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

Решаем задачу по взаимодействию микросервисов на Python тремя способами

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров15K
Всего голосов 43: ↑42 и ↓1+51
Комментарии38

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

Побуду токсичным

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

Таким образом, основываясь на идее о том, что микросервисы -- это слабосвязанные объекты, можем заключить, что эти объекты не являются микросервисами, в том смысле, как это понимает, например Chris Richardson тут:

https://microservices.io/patterns/decomposition/decompose-by-business-capability.html

и

https://microservices.io/patterns/decomposition/decompose-by-subdomain.html

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

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

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

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

А также graphql сервис по вашему определению это не микросервис. И всех кого он "покрывает" нужно впихнуть в него? :)

В первом случае же никто не обращается за данными, скорее один сервис посылает их сам в Ленту, когда произошло какое-то событие. Ну и события тоже конечно хранятся)

return await main()

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

В celery тоже можно одним воркером обойтись, и тоже им в базу писать.

А вот асинхронный таск... Хм..

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

Думаю асинхронность здесь избыточна, хотя и самому нравится использовать asyncio. Ну что поделаешь, модно)

Если лента уже асинхронная, то и воркер хорошо бы сделать таким же, чтобы можно было шарить готовые функции между ними) А так наверно не критично конечно)

А чем все-таки плох синхронный подход? Простой и, в данном сценарии, наиболее надежный, с минимальной latency, простотой контроля и развития.

Синхронный подход ничем не плох. Важно понимать, чего вы хотите им добиться. Может, например, стоит объединить два компонента в один?

Автор пишет "Такое взаимодействие называется синхронным. Его лучше избегать". Хотелось бы понять, чем обусловлена такая рекомендация?

Скорее всего автор имел ввиду, что при синхронном взаимодействии происходит в среднем рост нагрузки на инфраструктуру

Но это же не так.
Сделать http вызов с serviceA на serviceB очевидно дешевле, чем с ServiceA к брокеру и с брокера на ServiceB.
Я уж не говорю, что тот же кролик "из коробки" не дает никаких гарантий, а для доставки at-least-once нужно будет громоздить кластер, персистанс и специального человека на поддержку всего этого добра. Впрочем, чистый http call тоже не дает никаких гарантий, но хотя бы не является SPOF

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

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

Ровно это я и говорю, что данные не нужно запрашивать. Их нужно отправлять. Если вы запрашиваете, значит данными не владеете. А владеет соседний компонент. Значит нагрузка растёт, потому что в большинстве случаев вы это будете делать регулярно, чего нельзя сказать об отправке

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

На мой взгляд нужно стремиться к асинхронному подходу из-за этих причин:

1) Увеличиваете длительность работы эндпоинта, а от этого замедляется система в целом, клиент долго ждет ответ от сервера и так далее снежным комом

2) Если вдруг по каким-то причинам лента приляжет на короткий промежуток времени и будет невозможно отправить событие туда, то оно просто потеряется. Чтобы этого не допустить можно добавить ретраи в код, но это еще больше увеличит длительность работы эндпоинта, замедлит систему и опять снежный ком)

  1. Это неверно. Если нужен ответ от сервиса, то все равно вызывающий ждет response-сообщения от MQ, только при этом требуется гораздо больше времени. А если ответ не нужен - то зачем его ждать?
    Клиенту нужен результат и если для его получения нужно вызвать 10 методов - они все будут вызваны, не важно, через MQ или синхронно. Но если вызывать через http/grpc, то результат будет быстрее и нагрузка на систему будет гораздо меньше.

  2. Если приляжет MQ, то нужно будет сделать точно то же, так что логика retry в коде все равно будет нужна. Впрочем, нормальный resilience для http есть во всех языках и странно его не использовать.

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

В том то и дело, что клиенту результат о том, что "событие добавлено в ленту" не нужен, это происходит автоматически и клиент об этом может и не знать)

(Прошу прощения, промахнулся по оценке, постарался компенсировать в других ваших комментариях).
В конкретных случаях - да, может быть, так как фактически речь идет о стриминге изменений между двумя bounded contexts. Правда, кролик тут неудачный выбор и нужно бы Transaction Outbox прикрутить. Впрочем, когда есть TO, то он может и синхронно отправлять события, зачем там асинхронный MQ.
Но, в любом случае, это не про "синхронные взаимодействия вообще плохи".

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

Да и "по возможности стоит избегать" - не совсем верно. Скорее уж асинхронное взаимодействие по возможности лучше избегать - как сложно контролируемое, увеличивающее latency и сложность кода.

Действительно, http мешал им… а давайте post запросы делать.

Грустно всё это.

Вот только если что-то поменялось в Ленте, например в структуре БД, Вам (но т.к. у нас "настоящий" микросервис, то не Вам, а тому, кто его написал, а это не всегда тот же, кто пишет Ленту) теперь придется переписывать Worker. И доступ к БД Ленты из Worker - тоже не лучшее решение (Worker может быть далеко). Поэтому имхо самый "правильный" вариант - последний, но оставить доступ из Worker к Ленте по HTTP/API. Если боитесь, что Worker завалит Ленту, устанавливаете throttle (не больше N, а лучше M запросов в секунду).

Всё куда проще, надо сделать worker частью Ленты. Тогда Лента сама будет вычитывать данные из очереди и класть их в свою базу нужным образом.

Это конечно проще, но "Лент" может оказаться нужно запустить много больше, чем "Worker", например....

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

Просто такими темпами Вы придете обратно в монолит ;)

Если под "монолитом" понимается то, что сервис делает более одной вещи (слушает очередь и предоставляет api) то да, деплоится сервис, который обслуживает часть домена и он единственный знает как с этой частью работать.

И то, что запущено 10 копий этого сервиса, но под чтение очереди используется 0.000001% мощности одной копии, а всё остальное выедает API на мой взгляд ничего страшного.

Можно конечно делить сервис на отдельные операции и деплоить 10 копий "Лента-GET-List", 5 копий "Лента-GET-Message" ,2 копии "Лента-POST-mesage", ... но все эти наносервисы будут зависимыми от единой доменной модели сервиса. В том числе и по деплою.

Так и до Event Bus можно дойти.

У нас нагрузки разные, лента запускается на трех подах, worker на двух, плюс еще у ленты соединение к БД на пулах, а у worker-а нет. Ваш подход не предлагает такой гибкости в настройке)

В целом, неплохое предложение по дальнейшему развитию, ну и выглядит как вариант 4) Мне показался достаточным 3-й вариант, когда воркер просто смотрит в БД, но, все мы знаем, что лучший код еще не написан)

А что если сервисов станет больше?


Вот тогда и переделаете.

Если остановиться на втором варианте, можно попробовать очередь в базе хранить. https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/index.html#sqlalchemy, не надо замарачиваться с поднятием отдельного брокера.

Моя любимая стратегия решить просто, посмотреть, что будет, переделать под новые требования.

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

Хотя если процессы налажены, то второе и третье решения будут премерно одинаково затратно по врeмени. Я работал с очередями на AWS, там это несколько строк конфигов и не надо следить за инфраструктурой.

Было бы интересно увидеть процесс публикации и обработки случая отказа брокера/сети.

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

вот кстати да.

без обработки отказов можно и на простых сокетах написать асинхронную передачу данных.

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