Search
Write a publication
Pull to refresh

Comments 38

Перехватом исключения называется обработка ситуации

Тогда почему перехват, а не напрашивающаяся обработка? Тем более понятие перехвата у нас уже задействовано (см пункт 6 манифеста).

Просто хотел больше аналогии с throw/catch из C++. Ну а catch - это не обязательно обработка.

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

Описание классов - XSD, описание сервисов - WSDL, возможности по документированию там встроенные, протокол - HTTP, куча готовых инструментов, набор идеологий на выбор для приверженцев любых вкусов и фломастеров (вроде enterprise service bus и т.д.).

Зачем велосипед из своих кондовых описаний?

Справедливый вопрос. У нас преимущественно использовался C++, и протобаф для этого языка наиболее простой и часто используемый способ описать формат данных, а потом получить типы, которые можно использовать для сериализации/десериализации.

Кроме того, библиотека protobuf предоставляет возможность делать с этими сгенерированными данными очень многое (чтобы было нужно для красивой реализации внутренней клиентской библиотеки), а C++ с помощью шаблонов и концептов позволяет еще добиться довольно понятного API в клиентской библиотеке (пишу по памяти, могут быть ошибки):

using user = busrpc.api.chat.user.ClassDesc;
using sign_in = busrpc.api.chat.user.sign_in.MethodDesc;

user::ObjectId oid;
oid.set_nickname("test");

sign_in::Params params;
params.set_password("123");

BusrpcClass<user> cls(oid);
cls.call<sign_in>(params); // не скомпилируется, если типы user, oid,
                           // sign_in или params не те, что ожидаются 

Не знаю, как дела обстоят для C++, но скорее всего вы потеряли возможность использовать много полезных инструментов. А что приобрели? Возможность использовать привычный инструмент сериализации? И в довесок необходимость допиливать его под такие требования, которые уже реализованы для стандартных инструментов на основе XML, но не реализованы для выбранного вами.

Пилить свою реализацию приятно, но как-то бы это обосновать посерьёзнее, нежели просто "выбрал то, что знаю, остальное дописал как вижу". Например - указать на преимущества вашего ПО в сравнении с другими подходами. Хотя здесь мы опять возвращаемся к вопросу "что приобрели".

Жаль, что вы так это видите, что это все ради протабафа) Нет, конечно, привычный инструмент сериализации здесь не при чем. Приобрели мы (в большей или меньшей степени, т.к. мир не идеален) то, что описывается в манифесте, вот прям по пунктам.

Вообще, когда я начинал заниматься этой архитектурой, я думал о том, что мне хотелось бы получить на выходе. А хотелось мне API, которое формулируется в терминах обычного ООП API (чтобы любой джуниор за полчаса примерно понял, что есть в системе), при этом чтобы методы классы были реализованы на разных ЯП разными сервисами, запущенными как докер-контейнер в k8s. И естественно, чтобы была high availability (т.е несколько инстансов сервиса) и возможность простого масштабирования ( поднял еще один инстанс и готово). Я знаю, что "микро" не про размер в LOC, но мне приятно самому писать, и при необходимости разбираться в сервисах, которые содержат 300-400 строк кода и находятся в отдельном репозитории. Из-за того, что эти сервисы типовые (законнектился в очередь и собственную БД, подписался на методы, которые реализуешь, все), работа над новым начиналась с того, что разработчик просто клонировал репозиторий существующего, удалял из него файл с реализацией методов и писал новый. Можно было бы сделать темплейтный репозиторий, но в гитлабе это платная фича, решили обойтись до поры.

Заметьте, очередь сообщения в этой архитектуре необходимый компонент. Чуть ниже я отвечал, почему мне не походит grpc и аналогичные механизмы, которые основываются на p2p взаимодействии между сервисом и тем, кто его вызывает. Поэтому, я не хотел брать условный SOAP+WSDL+UDDI, или какой-нибудь OpenApi - Swagger.

Таким образом, мне надо было поверх очереди прикрутить каким-то образом стилистику ООП и RPC. Кондовость, упомянутая вами, является следствием того, что мой основной инструмент - очередь сообщений - вообще говоря не дает из коробки мне то, что я хочу. Я разумеется посмотрел не одну и не две очереди, перед тем как браться за работу, но нашел в них только базовые механизмы для организации модели request-reply, ни тебе ООП, ни RPC. Пришлось что-то придумывать.

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

Приобрели мы то, что описывается в манифесте

Ну давайте сравним:

Команда должна говорить на одном технологическом языке

Это никак не относится к выбору протокола и форматов данных. Заменяем названия и получаем один язык.

Исходные коды разных разработчиков должны быть максимально изолированы друг от друга

Это никак не относится к протоколам и форматам данных.

делать новые фичи как отдельные проекты

Это никак не относится к протоколам и форматам данных.

Проверять соблюдение технических требований бэкенда должны специализированные инструменты

Это относится к протоколам и форматам данных лишь в части стандартизации, которой в вашем случае меньше, значит меньше и "стандартизированных инструментов".

Лучшая и самая достоверная документация - это исходный код

Это никак не относится к протоколам и форматам данных.

человек должен иметь возможность посмотреть траффик между его компонентами

Это относится к протоколам и форматам данных лишь в части читабельности форматов. Вы выбрали нечитабельный бинарный формат. Вы не видите несоответствия?

единый центр притяжения для всей информации

Это никак не относится к протоколам и форматам данных.

Итого: либо нет никакой связи с вашим выбором, либо связь негативная.

Но есть более реалистичный намёк:

А хотелось мне API, которое формулируется в терминах обычного ООП

Но и здесь нет никаких проблем с приближением к ООП. Ранее предложенные альтернативы отлично отображаются на объекты, данные и методы.

мне надо было поверх очереди прикрутить каким-то образом стилистику ООП

А это снова противоречие. Здесь "стилистика ООП" предполагает синхронность вызова. Вы же, нарушив стилистику, сделали вызовы асинхронными.

мне пока видится так, что у вас инструмент первичен

На самом деле я люблю велосипедить. Но всё же я стараюсь понимать, что я получаю взамен отвергнутого готового решения. В вашем случае я та к и не понял - зачем? Пока вижу лишь один мотив - захотелось творить.

А это снова противоречие. Здесь "стилистика ООП" предполагает синхронность вызова. Вы же, нарушив стилистику, сделали вызовы асинхронными.

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

Вообще, правильно ли я вас понимаю, что вы не возражаете против самой концепции RPC поверх очереди сообщений? Что идея и принципы из манифеста кажутся вам допустимыми, но вот формат данных выбран неудачно?

Было бы супер, если бы вы предоставили какой-то чуть более конкретный вариант формата. Поверьте, я без сарказма или негатива, мне вообще интересны эти дискуссии. Но мне бы хотелось чуть более предметного разговора. Взгляните, если не затруднит, на пример API из репозитория. Я в статье рассказал, как гарантирую соблюдения спецификации (есть devtool самописный), и в обсуждении с вами показал, как мы эту спецификацию используем для реализации клиентской библиотеки. Если вы для этого API предложите какое-то свое описание (пусть для какой-то небольшой части) с использованием тех форматов, что вы предлагаете, то мне будет проще взглянуть на систему с вашей стороны. Просто я профдеформированный C++ разработчик, и это безусловно отражается на моем образе мышления (типизация, шаблоны, и т.д.). Вы, как я понимаю, пишите в основном на другом ЯП, где есть свои особенности, популярные свои форматы и т.д. и вы очевидно видите возможности, которые мне неочевидны.

вы не возражаете против самой концепции RPC поверх очереди сообщений?

Я не понимаю, чем обоснован выбор технологий. До очередей мы ещё не дошли.

если бы вы предоставили какой-то чуть более конкретный вариант формата

Я же представлял - XML XSD WSDL.

Берёте любой инструмент моделирования сущностей и на выходе получаете XSD. Включаете XSD в WSDL (опять же используя массу готовых инструментов), описываете методы (вызовы) - получаете RPC, который можно интерпретировать как ООП на уровне данных. Зачем ООП на уровне вызовов - не понимаю. Собственно в Си нужно всего лишь подключить библиотеку, которая отправляет и получает соответствующий XML на/с соответствующего адреса. Всё есть уже готовое, зачем велосипед?

Не знаю, возможно в С++ эти технологии никто не использует, но весь мир работает с ними и их аналогами. Поэтому и возник вопрос - зачем свой собственный протокол? Зачем потрачены усилия на его оформление и реализацию? Эти усилия, возьми вы привычные технологии, можно было бы сэкономить. Пока не вдаёмся в остальные концепты, только выбор - запилить свой велосипед вместо взять готовый. Не верю, что в С++ нет готовых велосипедов, а это означает, что они вам не подошли и вы захотели сделать свой. Чем не подошли? Ответа нет. Чем ваш лучше? Ответа нет.

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

Для начала про WSDL. Мои знания о нем ограничиваются тем, что его типовое использование заключается в описании API некоторого веб-сервиса, из которого потом можно сгенерировать сущности на целевом ЯП, которые предоставляют интерфейс для установки параметров вызова, а также для отправки вызова на какой-то сервис по HTTP, SOAP или чему-то еще. При этом общение между клиентом и сервером будет организовано через прямой коннект между ними. По причинам, которые я описывал в статье и уже много раз повторял в комментариях, я видел плюсы от использования очереди сообщений в качестве backbone для своего бэка. Вы в праве не разделять эти ценности, считать их надуманными и т.д., тогда для мысленного эксперимента примите их как мою блажь - сейчас не об этом, как вы сами сказали) В связи с этим возникает вопрос, что мне даст wsdl в этом случае? Зачем мне его обертки, если мне надо отправить не на сервис по HTTP/SOAP, а в некую очередь по ее собственному протоколу? Ну наверняка как-то можно замапить одно на другое, и вы полагаете, что это не будет кондово? В любом случае получается, что инструмент, заточенный под конкретный вид использования я колхозю под непонятно что. Вероятно, вы лучше меня знакомы с WSDL и чаще его использовали, поэтому я и просил чуть большей конкретики.

Если бы существовала очередь сообщений, которая позволяет описать некоторый API на каком-то IDL и потом сгенерировать для него типы, я бы конечно вероятно ее бы и использовал, но мне такая неизвестна.

Собственно в Си нужно всего лишь подключить библиотеку, которая отправляет и получает соответствующий XML на/с соответствующего адреса. Всё есть уже готовое, зачем велосипед?

Теперь про это. Да, можно описать все те же сущности на XML. Т.е. убрать вообще WSDL, оставить голый XML и сериализовать/десериализовать его. Библиотек для работы с XML пруд-пруди на любой ЯП. Не знаю, обратили ли вы внимание, но в протабаф я использую кастомные протабаф опции для выражения своих каких-то концепций (observable параметры, например, по которым можно подписываться только на подмножество вызовов метода, имеющее определенное значение этого параметра). Потом компилятор protoc добавляет эти опции к генерируемым типам, и я могу их обработать в недрах своих библиотек. Убежден, что при использовании XML эти концепции можно выразить в XSD, про который вы тоже говорили. Согласен, что это должно сработать, и мне это в свое время в голову не пришло, так я бы пристальнее посмотрел на этот вариант. Но формально - это просто другой формат. Да, использовав его, мне бы скорее всего не пришлось писать свой инструмент для генерации документации из файлов протокола, нашелся бы какой-нибудь сторонний. Но основную часть работы по-прежнему пришлось бы проделать вручную: библиотеки, которые оборачивают все эти XML/XSD в удобные интерфейсы, клиенты, которым можно сказать "вот вызови-ка метод/процедуру, описываемую вот этой XSD и используя вот эти параметры в виде XML". И в этой схеме также возникнет потребность как-то структурировать эти XML/XSD, хранить, проверять на то, что кто-то не наломал в них дров в том или ином виде, что тоже может начать смахивать на велосипед. Кстати, раз уж на то пошло, для протобафа тоже есть свои инструменты по генерации документации из proto файлов. Я предпочел написать свой, чтобы добавить полезные для меня документирующие команды и возвращать документацию в формате, учитывающим специфику платформы.

Вообще, вы не могли бы сказать, какой ваш основной ЯП? Мне важен фидбэк именно от людей с другим технологическим стеком, чем мой.

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

И в мыслях не было ничего прятать. Говорю как на духу - я отлично знаю протабаф, очереди сообщений (особенно NATS), С++. Более того, я люблю эти инструменты, умею использовать их сильные стороны и нивелировать слабые. Было бы странно, если бы я в проекте, который определяет вообще весь путь развития бэка, решил использовать инструмент, которым я пользуюсь раз в год. Теперь же проект вышел из частного использования, поэтому нужно посмотреть, чем остальные дышат, так сказать)

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

Давайте я в не знаю, какой раз повторю, что идея - в использовании шины сообщений для RPC. В том, чтобы сервисы не общались напрямую, как принято в grpc, SOAP, json-rpc, REST, тысячи их. Какие из этого плюсы - описано в статье, да и в комментариях много про это уже сказано. Можно ли их добиться в более традиционных архитектурах - можно, но как правило сложнее (естественно, какие-то другие вещи там будут проще, да).

Спасибо за подробный ответ. Его суть сводится к использованию очереди. Очередь даёт нам асинхронность, которая даёт устойчивость инфраструктуре. Я за использование очередей, но вы эту технологию использовали не имея хорошего представления об альтернативах. Альтернатива здесь известна давно - enterprise service bus (ESB).

ESB есть не просто набор очередей. Нормальный комплект включает множество адаптеров. И веб-сервисы в качестве адаптеров - самая стандартная функциональность. WSDL описывает адресацию и протокол. XSD описывает содержимое, поступающее с/на адаптер. Стандартные библиотеки на обоих концах взаимодействия дают вам именно вашу структуру в вашем представлении (не знаю, как это выглядит в Си, но обычно это классы, экземпляры которых вы получаете/отдаёте). Вызов тоже выглядит привычно для вашего ЯП (функция). Поступление данных - та же функция, но уже с вашей реализацией и описанной ранее структурой на входе. Если в какой-то части библиотеки не реализуют, например интересующий вас вариант входных функций, вы их можете легко сделать сами, или написать генератор, если функций много.

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

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

Другое дело, если вы не знакомы с полноценной реализацией такого взаимодействия. Полный цикл действительно реализуют не так много продуктов. Какая-нибудь Kafka реализует лишь свои собственные интерфейсы, ActiveMQ - поддерживает ряд стандартов, наверняка есть адаптеры для веб-сервисов. Но по отдельности это всё неполноценные решения, поскольку заметную часть придётся дописывать самому, особенно в случаях вроде кафки. Полноценные решения, вроде продуктов от IBM (Message Broker, который ближе к Си, или ESB, который полностью на Java), позволяют автоматически получать почти всю цепочку. Хотя в случае интеграции с Си я не знаю, какие есть варианты.

Ориентация на стандарты - ключ к такой архитектуре. Если стандартов нет, то нет и генераторов и прочих вспомогательных библиотек.

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

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

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

Вы знаете, звучит как решение. Я посмотрю получше.

А что там по производительности? Знаете, звучит хоть и элегантно, но очень уж массивно. Что если нет бюджета на покупку мощных серверов и каналов? Я работал в проектах, где на ифраструктуру не особо тратились, а нагрузки были вполне себе. Но справлялись, плюсы, асинхронность везде и т.д. Есть может у вас какие-то бенчмарки?

Да, в варианте от IBM ESB использует на каждый чих по WebSphere (сервер приложений). Но зато вы получаете готовую инфраструктуру, на которую стандартным образом ставятся приложения, которые так же написаны в соответствии со стандартами. Плюс графическое представление связей между очередями, с графическими редакторами для фильтров и отображений одних типов сообщений на другие.

Но никто же не заставляет использовать именно IBM. Есть много очередей, которые утверждают, что они на самом-то деле message broker. Только в каждом случае нужно смотреть, какой функционал там реально есть, и какого нет. Чаще всего много чего нет. Например в кафке есть маршрутизация и фильтрация, но на самом деле это всего лишь обработчики на Java, которые имеют доступ к топологии, которую, в свою очередь, внутри этих же обработчиков нужно писать руками. То есть формально похоже на брокер, но по факту нужно много ручных доработок.

Общий смысл такой - надо перебирать варианты. Наверное Java вам не нужна, поэтому вы наверняка исключите очереди на ней. Так что ваша специализация на Си вам должна подсказать, какие очереди для вас правильные. А среди них смотрите возможность автоматически строить цепочки обработки. Но может ничего приличного и не найдёте, всё-таки не зря IBM за много денег продаёт свою ESB и брокера.

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

В общем - задачу анализа вариантов я с вас не сниму.

Да, в продолжение. Среди вариантов esb есть опен-сорсные и бесплатные?

Ну и жаль, что вы про esb сразу не написали) а то xml, xsd..)

Esb, тот же camel - это оверинжинеринг и используется только для интеграции легаси приложений, в которые никто не будет вносить изменения, просто потому что это невыгодно. А rpc поверх распределенной очереди, например кафки - вполне рабочий вариант, тк параллелизм достигается из коробки за счёт распределения консюмеров по партициям плюс отдельным консюмером можно подключить графовую бд для построения диаграммы последовательности вызывов, сшивка exchange идет по sourceId - пад в кубере и correlationId. Минус протобуферов - нет поддержки в популярных инструментах, offset explorer или schema registry.

Я бы только крайне не рекомендовал в общем случае использовать кафку для RPC. Самой важной метрикой для RPC является низкий и стабильный (без внезапных периодический просадок) latency между вызовом и ответом. Кафка привносит значительный оверхед в случае RPC по сравнению с некоторыми другими очередями. Т.е. в некоторых случаях может и сгодится, но в целом я бы на нее не полагался (поэтому в статье я о ней даже не упоминаю). Лично я использовал nats и с ним не было проблем.

Есть хорошая статья по производительности нескольких очередей сообщений именно в контексте request/response взаимодействия, может вас заинтересует.

Вообще, я подозреваю, что туманно предназначение типов вроде MethodDesc и аналогичных. Не буду скрывать, это все для того, чтобы сделать библиотеку для C++ в том виде, что представлен выше. Специфика языка, что поделать - нет там интроспекции из коробки. Вероятно для большинства других ЯП, такие типы будут излишни.

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

Чем вам grpc не подошёл, что возникла потребность в собственном инструменте?

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

Итак, как работает grpc? Вы создаете описание сервиса, потом protoc генерит из него код для клиента или сервера. Сервер в итоге будет слушать на некотором порту входящие соединения, а клиент коннектиться на этот порт. Т.е. клиент обязан знать, куда ему коннектиться. Если клиенту нужно вызвать несколько операций, ему нужно законнектиться к нескольким сервисам. Схожим образом устроено множество RPC-like технологий: json-rpc, SOAP, любой REST API на HTTP. Понимаете, куда я веду? Если на бумаге изобразить все взаимодействия между сервисами, получится спагетти. Как это все масштабировать, например, когда понадобится запустить дополнительно еще один инстанс grpc сервера? Переделать все клиенты, чтобы они знали про еще один сервер? Ну, так себе вариант. А! Нам нужен лоад-балансер, спрячем серверы grpc за ним, и все ок! Не забываем добавить еще стрелочек на картинку взаимодействия сервисов. Как думаете, легко будет новым сотрудникам в этом разбираться? А админам следить, где какие порты у вас открыты? А как на работающем проде посмотреть, кто там какие пакеты куда шлет, что у вас юзеры не могут залогиниться?

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

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

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

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

  2. Добавили бы какой-нибудь новый метод вроде isFirstSignIn к сервису A, чтобы какой-то другой сервис мог в некоторый момент времени с помощью него определить, нужно ли высылать приветственное сообщение.

  3. Написали бы новый grpc сервис B, в который добавили бы операцию вроде onUserSignedIn, в которой этот новый сервис проверял бы, не является ли это первым логином юзера, и если да, отправлял бы приветственное сообщение. Сервис А при этом нужно модифицировать, чтобы он вызывал этот новый метод при логине пользователя.

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

Как решали эту задачу мы, когда начали делать микросервисы? Когда наш сервис А создавался, после реализации основной операции sign_in, мы предположили, что ивент о том, что юзер залогинен в систему является достаточно полезным, и сразу решили паблишить его в нашу очередь, хоть на тот момент он особо не был нужен (ну, разве что позволял мониторить логины). Когда появилась задача на отправку приветственного сообщения, мы реализовали сервис B, который в своей БД хранит юзеров, которые еще не логинились и слушает ивенты о логине пользователей. Если залогинившийся пользователь есть в табличке, ему высылается сообщение (опять же, это осуществляется с помощью отправки пакета в очередь сообщений) и запись удаляется. Этот сервис делал человек, который вообще ни дня не работал в бэкенд-команде, писал на другом ЯП, и тем не менее он быстро и успешно с ней справился, ведь ему вообще не пришлось залазить в чужой код (в сервис А). Вы можете возразить, что если бы изначально мы не подумали о том, что ивент о логине в систему полезен и не добавили его сразу, то сервис А пришлось бы менять, и будете правы. Однако уже при следующей подобной задаче ивент будет на месте, а значит с большой степенью вероятности ничего менять не придется.

Замечу, что busrpc в этом примере вообще не при чем. Ключевым механизмом МСА является очередь сообщений (по описанным выше причинам), к которой grpc не относится. Это не меняет того, что grpc отличный инструмент, но лично я использую его для других целей, например для реализации API-шлюзов. Busrpc - это просто вариант того, как можно описывать интерфейсы, которые есть в вашей очереди сообщений. Возможно, вам дополнительно будет интересно посмотреть на пример в моем репозитории, который лучше продемонcтрирует, как может выглядеть busrpc API.

То есть фактически поменяли общение сервисов между собой на общение сервисов через pub/sub, сократив связность с остальными сервисами до связности с конкретными топиками в очереди сообщений?

Вы так говорите, как будто это что-то плохое)

Если я вас правильно понял, то вы намекаете на то что, что один вид связности я заменил другим? Я не соглашусь с вами. Связность топика и метода статична, а значит может быть где-то захардкодена и скрыта. А связи между сервисами динамичны, ведь ip и port сервисов могут меняться. Ну или потребуется какой-то discovery механизм, который будет находить сервис.

В busrpc мой код выглядит примерно так (обратите внимание, что топик там вообще нигде не фигурирует и вычисляется внутри реализации):

using user = busrpc.api.chat.user.ClassDesc;
using sign_in = busrpc.api.chat.user.sign_in.MethodDesc;

user::ObjectId oid;
oid.set_nickname("test");

sign_in::Params params;
params.set_password("123");

BusrpcClass<user> cls(oid);
sign_in::Retval result = cls.call<sign_in>(params); // синхронный вариант вызова

Вы так говорите, как будто это что-то плохое)

Ни в коем случае. Просто несколько непросто понять в чем профит и что поменялось. Поэтому предпочитаю переспросить.

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

Но суть, что связность осталась всё также на месте, просто думать про неё теперь не нужно. Думается мне, что сделать сериализацию подобных штук из/в конфиги тоже особо проблем не составит, так что статика-динамика не принципиальна в этом смысле. Связность сервисов ушла в связность кода.

Да, связаность между сервисами, которые напрямую коннектятся друг с другом, тоже скрывается, и механизмы этого хорошо изучены (service discovery).

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

Многое говорилось про прозрачность бэкенда, т.е. возможности просмотреть, какие сообщения в нем ходят. Например, проследить все методы (и ответы на них) для какого-то конкретного юзера. Это в высшей степени полезно для отладки и поиска узких мест. С очередью это почти тривиально - вы может подписаться на нужные топики (конечные точки в моей терминологии) - и вуаля, все видно. Как вы сделаете подобное в grpc? Сделать-то можно - например есть jaeger, распределенные логи, и т.д. Но это бремя упадет на ваши сервисы, т.к. им это надо будет отправлять.

Ну и вообще, в любом случае, какую-то очередь, куда ваши сервисы будут паблишить важные ивенты, вы все равно будете использовать в МСА. И grpc эту потребность не решит. В итоге у вас будет grpc + discovery + message queue. А у меня просто message queue. Видно что одна система в общем сложнее устроена, чем другая. Понятно, что эта дополнительная сложность может как-то решаться, но в целом изначально если мы берем очередь за основу, мы в принципе не сталкиваемся с целым классом проблем. Но очевидно, сталкиваемся с другими, которые в свою очередь тоже можно как-то решить.

И тут многое решает опыт использования той или иной технологии. Я вот не могу сказать, что строил полноценную МСА на grpc+discovery, но у нас была похожая реализация со своим протоколом и своим discovery (справедливости ради, значительно хуже, чем grpc). Ее я и рефакторил примерно на то, что описано в статье. Новая система мне нравится значительно больше, поэтому захотелось поделиться этим опытом.

grpc дает синхронность, ваше SOA ESB дает асинхронность, микросервисная архитектура возможна и там, и там. Сервис брокер может делать load ballance, а может только service discovery.

любой джуниор за полчаса примерно понял, что есть в системе

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

Стало проще, вместо поиска по вызову или использованию функции в одном репозитории, искать по нескольким проектам?

Как будет организовано end-to-end тестирование всего зоопарка сервисов включая совместимость версий?

grpc дает синхронность, ваше SOA ESB дает асинхронность, микросервисная архитектура возможна и там, и там.

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

Сервис брокер может делать load ballance, а может только service discovery.

Да может, конечно. Можно на L3/L4 модели OSI сделать балансировку, тогда и сервис не понадобится. С очередью сообщений это правда все сразу есть.

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

API busrpc-бэкенда, если смотреть на сгенерированную документацию, ничем не отличается от API какой-нибудь ООП либы. Сколько времени вам нужно для того, чтобы понять работу ООП либы? А сколько, чтобы начать ее использовать? Я бы хотел, чтобы для моих коллег процесс работы с API бэкенда напоминал работу с обычной библиотекой.

Стало проще, вместо поиска по вызову или использованию функции в одном репозитории, искать по нескольким проектам?

Что искать? API документировано в одном месте - в директории busrpc проекта. Для каждого метода можно увидеть, какими сервисами он вызывается или реализуется, вместе со ссылкой на их репозиторий. Ну если вам удобнее, можно выгрузить все репозитории в одну папку.

Как будет организовано end-to-end тестирование всего зоопарка сервисов включая совместимость версий?

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

Подумал над вашим вопросом о тестировании. Я бы предложил создать несколько профилей тестирования для отдельных блоков функциональности вашего проекта (например, логин пользователя, изменения профиля и т.д.). Каждый профиль тестирования состоит из набора json с параметрами методов и ожидаемых результатов. Писать такие профили сможет ваш qa отдел. Относительно не сложно создать инструмент, который будет брать json из этого профиля, с помощью protobuf либы преобразовывать его в параметры метода, коннектиться в очередь и отправлять туда вызов с этими параметрами, проверяя итоговый результат. Но повторюсь, в целом ваш вопрос больше к МСА в целом, вероятно вы найдете массу других практик и рекомендаций.

Какая конкретно архитектура для решения "задачи взаимодействия микросервисов" нужна?

Например: service discovery aka Netflix Eureka.

В вашем решении вы кардинально уменьшили связанность компонентов используя ESB. Хорошо, но это увеличит сложность всей системы. А чем выше сложность, тем больше шансов на ошибки.

Например, такие:
Что случится, если во время обработки сообщения, сервис перезагрузится, или зависнет, но в базе некоторые изменения закомитятся.

Если ответ на сообщения отправятся дважды, или ответ не отправится совсем.

Если получатель получит сообщения не в том порядке, в котором ожидает.

Что случится, если сервис в новой версии будет не совместим (protobuf номера полей поменяли) с сообщениями других сервисов. Сервис перестанет принимать, или начнет рассылать не понятное.

Нужен не только unit test, но и functional, integrational, e2e.

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

Про ваши вопросы по ошибкам, у меня к вам встречный вопрос - а что будет с вашими сервисами в этом же случае? Я думаю, что это не вопрос к механизму, по которому сервис получил сообщение, из-за которого завис. Но если отвечать на вопрос, то у нас используется паттерн circuit breaker: вызывающий сервис обнаруживает методы, которые в течение таймаута не возвращают ответ и помечает их как проблемные. При необходимости опять вызвать этот метод, сервис смотрит, есть ли сейчас уже pending вызов. Если нет, то пытается вызвать (вдруг метод починился), а если да, то сразу переходит в обработке ошибки. Это позволяет избежать глобального зависания системы, когда один метод сломался, ответов от него долго ждет один сервис, и пока он ждет, он сам блокирует другой сервис и т.д.

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

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

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

  2. Пользователь изменил что-то в профиле, но изменения не сохранились

Первую я считаю критичной, вторую нет. Система должна гарантировать насколько возможно отсутствие критичных ошибок, но может допускать ошибки второй группы. Разумеется, это не значит, что можно мириться с тем, что каждый второй запрос пользователя на изменения профиля не обрабатывается, но определенный фейл-рейт допустим. Когда он вырастает до состояния, когда пользователи или отдел QA начинают высказывать неудовольствие (писать в ваш саппорт, или лично вам во внутренней переписке), становится понятно, что с ошибкой пора что-то делать.

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

  • в рамках штатной работы системы, они не должны происходить

  • в каких-то критических ситуациях (баги, ddos и т.д.) они могут происходить

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

  • любой сервис запускается минимум в двух экземплярах

  • сервисы перезапускаются по одному

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

Почему я разделяю ошибки? Потому что гарантия надежности не дается бесплатно. Что для вас лучше, чтобы latency вашего RPC механизма было 10мс, но один из 100 000 пакетов терялся, или чтобы у вас latency было 500мс, но этот пакет бы не потерялся. Стоит ли для всего вашего API использовать механизм с максимальной надежностью? Возможно, в ваших проектах да, но мне не доводилось работать над настолько критичной инфраструктурой. В моей практике подавляющее большинство операций не нуждается в таких жестких гарантиях надежности, и нецелесообразно обеспечивать их без разбора для всех частей API. Если говорить про пример выше, то для подписок использовалась перзистентная кафка и какие-то дополнительные скрипты для мониторинга

В остальном я не то, чтобы хочу с вами спорить или доказывать, что то, как я делаю, это единственно правильный подход. Я довольно давно занимаюсь бэкенд разработкой, и вроде избавился от категоричности и безапелляционности. Вы предлагаете грамотные и хорошие решения, невозможно отрицать, что их многие успешно используют. И с помощью них можно добиться и того, что важно для меня. Я считаю, что с очередью мои задачи решаются удобнее, но их совершенно точно можно решить и вашей схеме. Например, мне нужен прозрачный бэкенд, где я могу отследить любое сообщение, и очередь мне это дает. Но вы всегда сможете мне возразить и сказать, "да я просто jaeger прикручу, распределенные логи, elasticsearch и kibana". И будете правы - будет еще и красиво. Но это работает и вашу сторону) По сути ваши замечания (спасибо за них, кстати) - это повод поискать для них решение, а не прийти к выводу, что это вообще не юзабельно.

Про интеграционные тесты я написал выше,

Мокать сервисы? извините, это ближе к functional, но не тестирование их интеграции.

паттерн circuit breaker:

Снова, немного не так. Сircuit break про отключение при перегрузки, но не только сервиса, а желательно клиента - источника проблемы.

Вы переделали архитектуру монолита на ESB? Отлично! Особенно если вам не транзакционная асинхронность подходит по бизнесу.

Но менять rpc + service discovery (один сервис-брокер), на корутины + callback (в каждом вызове), и агитировать за простоту? А нормальный не монолит, но модульник, 4layer or hexogen or plugin?

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

Hidden text

"У меня локально все работает", заменится на, "Мой сервис работает, за других, я не отвечаю".

Апи поменяли, все легло, но: "У нас документация тоже поменялась!"

Багфикс распределенной системы сложнее, чем монолита! Тем более, без тестирования всей системы. Я-бы задумался как минимум о хардкодед трейсинге: пользователя, сессии и span/action (не забываем версии сервисов).

Снова, немного не так. Сircuit break про отключение при перегрузки, но не только сервиса, а желательно клиента - источника проблемы. \

Вы что-то путаете, может с flow control (текущее ведро и т.д.), может с чем-то другим. Circuit breaker это в точности то (за исключением некоторых параметров алгоритма), что описано.

Но менять rpc + service discovery (один сервис-брокер), на корутины + callback (в каждом вызове), и агитировать за простоту?

Это вынужденное усложенение. Не всем подходят синхронная модель взаимодействие поверх пула потоков. Максимальная производительность достигается на ивент лупах, где асинхронность идет с самого низа (с сокетов). При этом там один поток (чтобы не делать лишние локи, добиться максимальной локальности кэша процессора и т.д.), и производительность (как по latency, так и по общему throughput) - значительно выше. А корутины кстати как раз избавляют от колбеков, делая код визуально приятнее.

Багфикс распределенной системы сложнее, чем монолита!

Если где-то показалось, что я считаю наоборот, то приношу извинения. С этим согласен.

А в чем проблема сделать синхронную проверку логина, и в случае успеха кинуть ивент в очередь и вернуть ответ? Зачем использовать очередь в синхронном взаимодействии?

Кстати, что будет, если общая шина станет недоступна?

Вы же и сами понимаете, что будет, если шина станет недоступна) кина не будет, все сломалось. Поэтому там обязательно кластер и проверенная технология. Я использовал nats, состоящий из 2х кластеров по 3 ноды в каждом в 2х датацентрах. За 2 года не было ни одной проблемы с ним, даже при обновлении версии натса.

По первому вопросу - ничего не мешает, более того, думаю, что так и надо делать в МСА. Просто я уже где-то выше писал, что так у вас минимум 3 технологии (grpc, очередь, service discovery), а с очередью одна. Ну и многие тривиальные с очередью вещи услодняются с grpc. Как вы посмотрите, с какой именно запрос в grpc пришел? Ивент-то в очереди вы увидете, а вот с запросом будет проблема.

Вообще, идеальная для меня технология была бы комбинация что-то вроде grpc (его idl, генерация классов) и какой-то брокер для всего этого, чтобы grpc через очередь работал, с ее возможностями (мониторинг траффика, подписка на подмножество сообщений, нативная модель pub-sub). Но я такого не знаю.

)) Да я там уже полгода не работаю, вроде справляются)

мне кажется, велосипед делаете.

Посмотрите в сторону Apache Dubbo, VertX Bus.

Т.к. dubbo запилен китайцами, там локализация хромает, вот законтрибьютить в этот проект имеет смысл имхо.

мне кажется, велосипед делаете.

Не только вам) Я вот и сам уверен, что велосипед. Создавался он под мои нужды, и свои задачи хорошо решал. Ваши технологии посмотрю, раньше с ними не сталкивался. Там правда похоже все это под Java заточено, а у нас в бэке джава не использовалась, только C++ и Go (причем с перекосом именно в сторону C++). Но вообще на первый взгляд - dubbo это типичное RPC, с service discovery, синхронной моделью взаимодействия и т.д. А меня интересуют решения, которые основаны на использовании очереди в качестве communication layer. Причины я постарался описать в статье, ну и в комментариях много было на эту тему, не буду повторяться.

Sign up to leave a comment.

Articles