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

Замечу, что называя busrpc фреймворком, я "слегка" преувеличиваю, так как на данном этапе проект содержит всего два компонента:

  • спецификацию, определяющую терминологию, структуру и протокол busrpc бэкенда, а также некоторые другие технические документы (репозиторий busrpc-spec)

  • утилиту, предоставляющую команды для проверки реализаций на соответствие спецификации busrpc, генерации документации и другие

До уровня фреймворка этому проекту категорически не хватает таких очевидных вещей, как:

  • библиотек для разработки busrpc микросервисов под разные языки программирования

  • клиентов для отладки и мониторинга busrpc микросервисов

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

Предыстория

Долгое время я работал над бэкендом одного крупного и достаточно старого (15+ лет) проекта. Основу ему закладывали еще в далекие 2000-е, а значит, современные инструменты и принципы разработки ПО были еще либо недоступны, либо недостаточно известны, поэтому долгое время проект развивался как вариация монолитной архитектуры. Несмотря на то, что он представлял собой несколько сервисов, которые разделяли между собой бизнес-логику приложения, это нельзя было назвать микросервисной архитектурой по следующим причинам:

  • сервисы были тесно связаны друг с другом: каждый содержал в себе информацию о других, от которых он зависел, поддерживал с ними прямое соединение и общался по проприетарному протоколу

  • сервисы совместно использовали общие БД, причем далеко не всегда только для чтения

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

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

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

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

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

Разумеется, сильно страдала надежность и доступность нашего бэкенда. Падения сервисов участились, а восстановить систему после них становилось все сложнее (а в автоматическом режиме - так и вовсе практически нереально), так как вместе с ростом количества сервисов увеличивалась запутанность их взаимодействий и конфигураций. Например, правильное устранение последствий падения сервиса А могло требовать перезапуска сервиса Б.

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

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

Мой манифест

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

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

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

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

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

  5. Лучшая и самая достоверная документация - это исходный код. Зачастую, никакой иной документации не нужно.

  6. Бэкенд должен быть прозрачным в том смысле, что человек (разработчик, QA или DevOps инженер, системный администратор) должен иметь возможность посмотреть траффик между его компонентами. Это неявно подразумевает возможность фильтрации сообщений и приведения их к текстовому виду, а иначе прозрачность будет номинальная: вряд ли возможность перехватить все сообщения в бинарном виде окажется востребованной, так как мало отличается от прямого анализа логов.

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

Основы фреймворка busrpc

Как следует из названия, busrpc представляет собой один из вариантов технологии удаленного вызова процедур для организации взаимодействия между компонентами бэкенда. Кроме того, в качестве транспортного уровня используется шина (bus) сообщений.

Первоначально я хотел назвать свой проект mqrpc, так как термин "очередь сообщений" (message queue) более распространен, чем "шина сообщений" (message bus), однако такой проект уже существует на гитхабе. Разница же между этими понятиями весьма тонкая, и показалась мне несущественной. В статье я считаю эти термины синонимами.

По сравнению с типовыми реализациями RPC (например, gRPC), использование шины сообщения как промежуточного слоя между сервисами дает следующие преимущества:

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

  • сильно упрощает конфигурирование сервисов, так как им абсолютно необходимо знать лишь адрес шины сообщений

  • упрощает управление сервисами и их масштабирование

  • дает возможность мониторить работу бэкенда (см. пункт 6 манифеста)

  • позволяет использовать другие популярные паттерны взаимодействия из МСА (например, publish/subscribe)

Разумеется, есть и минусы, из которых на ум сразу приходят два:

  • дополнительная точка отказа

  • более высокая задержка (latency) вызовов в общем случае

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

Модель шины сообщений

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

Модель описывает две операции, предоставляемые шиной сообщений:

  1. PUBLISH(topic, message, [replyTo]) для отправки сообщения в топик

  2. SUBSCRIBE(topic) для получения сообщений из топика

Топик это последовательность слов, разделенных некоторым специальным символом (во всех известных мне очередях сообщений это .). Этот термин позаимствован из Kafka, в других проектах ему может соответствовать другое понятие (например, routing key в RabbitMQ, или subject в NATS).

Слова, составляющие топик образуют произвольную иерархию, например, time.us, time.us.east, time.us.east.atlanta и т.д. Модель шины сообщений определяет два специальных символа, которые могут использоваться в качестве слова топика в операции SUBSCRIBE:

  1. <topic-wildcard-any1> соответствует одному любому слову

  2. <topic-wildcard-anyN> соответствует 1-или-N или 0-или-N словам (для спецификации не важно, какая именно нижняя граница используется)

Основным механизмом обмена сообщениями в абстрактной модели является publish-subscribe. Необходимая для RPC модель request-reply обеспечивается с помощью поддержки параметра replyTo в операции PUBLISH, который сообщает обработчику топик, на который он должен отправить ответ.

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

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

Протокол

В качестве бинарного протокола для платформы busrpc был выбран protobuf. Помимо того, что это просто удобный инструмент с хорошей поддержкой, знакомый большинству разработчиков, protobuf предоставляет возможности, отсутствующие у аналогов (например, Apache Thrift):

  • механизм кастомных опций, с помощью которого в язык protobuf были добавлены некоторые концепции платформы busrpc

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

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

Дизайн и терминология

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

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

Класс

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

Объектом класса, как и в ООП, называется конкретная сущность множества, моделируемого классом. Каждый объект характеризуется некоторым уникальный неизменяемым идентификатором объекта.

Метод может быть связан с конкретным объектом или с классом в целом. В последнем случае метод называется статическим. Если класс не имеет объектов, он тоже называется статическим. Статический класс может содержать только статические методы.

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

Результатом метода также является сетевое сообщение, которое содержит либо возвращаемое значение метода, либо исключение, представляемое специальным встроенным типом данных. Метод может быть объявлен как не возвращающий никакого результата (аналог void из некоторых языков программирования). Такие методы называются oneway (мне не очень нравятся варианты перевода этого термина, поэтому решил оставить на английском).

Сервис

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

  • реализация - в этом случае сервис использует операцию SUBSCRIBE для получения вызова метода и операцию PUBLISH для отправки результата метода (для oneway-метода, операция PUBLISH, очевидно, не задействуется)

  • вызов - в этом случае сервис использует операцию PUBLISH для отправки вызова метода

Отношение между сервисами и методами представляет собой N-M, то есть один сервис может использовать произвольное количество методов, а один метод может быть использован несколькими сервисами (в том числе, и реализован несколькими сервисами)

Структуры и перечисления

Busrpc структурами и перечислениями называются соответственно типы данных message и enum из protobuf.

Некоторые структуры имеют дополнительную семантику в спецификации busrpc и называются предопределенными (predefined). К ним относится особый класс структур, называемых дескрипторами, которые используются для описания высокоуровневых концепций (классов, методов, сервисов и других).

Кодируемым (encodable) protobuf типом называется не-repeated тип данных, представляющий собой одно из следующих:

  • скалярный protobuf тип, за исключением float и double

  • protobuf enum

  • protobuf message, не содержащий полей, или содержащий только поля одного из предыдущих типов, не объединенные каким-либо oneof

Примеры кодируемых и не кодируемых busrpc структур:

enum MyEnum {
  MYENUM_VALUE_0 = 0;
  MYENUM_VALUE_1 = 1;
}

// пустая структура является кодируемой
message Encodable1 { }

// кодируемая структура, т.к. она:
// 1 - состоит только из полей скалярного типа и типа перечисления
// 2 - optional не запрещен явно в определении
message Encodable2 {
  optional int32 f1 = 1;
  string f2 = 2;
  bytes f3 = 3;
  MyEnum f4 = 4;
}

// некодируемая структура
// каждое поле не позволяет рассматривать ее как кодируемую
message NotEncodable {
  float f1 = 1;
  repeated int32 f2 = 2;

  oneof MyOneof {
    int32 f3 = 3;
    string v4 = 4;
  }

  map<int32, int32> f5 = 5;
  Encodable1 f6 = 6;
}

Пространство имен

Пространство имен - это группа связанных каким-либо образом классов, структур и перечислений. Пространства имен в busrpc используется для логического разбиения API и изолирования частей.

Исключения

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

Под выбрасыванием исключение понимается возвращение в качестве результата метода экземпляра Exception. Перехватом исключения называется обработка ситуации, когда результатом метода является исключение.

Спецификация busrpc требует, чтобы исключения, которые вызывающий сервис не может обработать, отправлялись дальше вверх по цепочке вызовов. Этот механизм в обычных языках программирования называется exception propagation.

Например, предположим, что сервис S0 вызывает метод M1, который реализуется сервисом S1. В рамках обработки этого вызова, сервис S1 вызывает метод M2, реализуемый сервисом S2. Если M2 столкнется с каким-то проблемами, он может выбросить исключение E, которое S2 возвращает на S1 в качестве результата. В свою очередь, M1 может перехватить E и каким-то образом его обработать. Если этого не происходит, то сервис S1 обязан вернуть сервису S0 то же самое исключение E в качестве результата метода M1.

Исключения busrpc могут преобразовываться клиентскими библиотеками в исключения целевого языка программирования. Это нужно учитывать, потому что все механизмы исключений в ЯП имеют свою цену. Например, могут значительно снижать производительность, когда количество выбрасываемых исключений становится слишком велико. Чтобы избежать возможных проблем, нужно помнить, что в busrpc исключения нужны для того, чтобы сообщать об исключительных ситуациях, которые происходят достаточно редко (иначе какие же они исключительные), обычно свидетельствуют о серьезном техническом сбое и как правило имеют тривиальную обработку (вывод в лог, установка алерта и т.д.).

Конечная точка

Конечная точка вызова (call endpoint) - это топик, в который отправляются вызовы метода (указывается в параметре topic операции PUBLISH). Конечная точка представляет из себя последовательность <namespace>.<class>.<method>.<object-id>[.<observable-params>].<eof>, где:

  • <namespace>, <class> и <method> являются именем пространства имен, класса и метода соответственно

  • <object-id> содержит идентификатор объекта, для которого вызывается метод, или специальное слово <null>, если вызываемый метод является статическим

  • <observable-params> представляет собой последовательность слов, каждое из которых содержит значение наблюдаемого параметра метода; наблюдаемые параметры метода идентичны обычным, за исключением того, что они должны иметь кодируемый тип и дополнительно являются частью конечной точки метода (подробнее о них мы поговорим позднее)

  • <eof> - это специальное слово, служащее индикатором конца последовательности наблюдаемых параметров

Конечная точка результата (result endpoint) - это топик, на котором вызывающий ожидает результат метода (указывается в параметре replyTo операции PUBLISH). Эта конечная точка имеет формат <result-endpoint-prefix>.<call-endpoint>, где:

  • <result-endpoint-prefix> содержит некоторую последовательность слов, точный формат которой определяется в зависимости от используемой шины сообщений; в общем случае префикс содержит информацию, необходимую для демультиплексирования результатов метода и определения, к какому вызову они относятся

  • <call-endpoint> содержит конечную точку, использованную для вызова метода

Некоторые популярные виды конечных точек получили отдельные названия:

  • конечная точка пространства имен (namespace endpoint) <namespace>.<topic-wildcard-anyN> - вызовы всех методов всех классов из пространства имен

  • конечная точка класса (class endpoint) <namespace>.<class>.<topic-wildcard-anyN> - вызовы всех методов класса

  • конечная точка метода (method endpoint) <namespace>.<class>.<method>.<topic-wildcard-anyN> - все вызовы метода

  • конечная точка объекта (object endpoint) <namespace>.<class>.<topic-wildcard-any1>.<object-id>.<topic-wildcard-anyN> - вызовы всех методов над конкретным объектом

  • конечная точка значения (value endpoint) <namespace>.<class>.<method>.<topic-wildcard-any1>.<observable-params>.<topic-wildcard-anyN> - все вызовы метода с определенными значениями некоторых наблюдаемых параметров

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

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

Областью видимости структуры/перечисления является та часть API, в которой эта структура/перечисление может использоваться.

Бэкенд построенный на платформе busrpc может содержать следующие области видимости:

  • единственную глобальную область видимости, содержащую типы, которые доступны везде

  • единственную область видимости API, содержащую типы, доступные в любой части API бэкенда

  • область видимости пространства имен, содержащую типы, доступные только внутри пространства имен

  • область видимости класса, содержащую типы, доступные только методам класса

  • область видимости метода, содержащую типы, доступные только в рамках конкретного метода

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

  • область видимости сервиса, содержащую внутренние типы конкретного сервиса

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

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

Busrpc проект

Фреймворк busrpc определяет следующую структуру директорий busrpc проекта:

<project-dir>/
├── busrpc.proto
├── api/
│   ├── <namespace-dir>/
|       ├── namespace.proto 
│       ├── <class-dir>/
│           ├── class.proto
│           ├── <method-dir>/
│               ├── method.proto
├── implementation/
│   ├── <service-dir>/
|       ├── service.proto

Компонентами этой структуры являются:

  • корневая директория busrpc проекта <project-dir>, содержащая файл busrpc.proto, в котором определены некоторые встроенные типы и кастомные protobuf опции платформы

  • директория api/, содержащая API бэкенда

  • отдельная директория <namespace-dir>/ для каждого пространства имен, содержащая поддиректории классов, входящих в пространство имен и файл дескриптора пространства имен namespace.proto

  • отдельная директория <class-dir>/ для каждого класса, содержащая поддиректории методов класса и файл дескриптора класса class.proto

  • отдельная директория <method-dir>/ для каждого метода, содержащая файл дескриптора метода method.proto

  • директория <implementation>/, являющаяся корнем для непубличных типов и поддиректорий сервисов

  • директория <service-dir>/, содержащая файл дескриптора сервиса service.proto

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

Имена protobuf пакетов

Структура директорий busrpc проекта определяет имена protobuf пакетов (указываются в proto файле с помощью package) следующим образом:

  • имя пакета, используемое в файлах из корневой директории проекта должно быть busrpc

  • в остальных файлах имя пакета должно состоять из имен директорий, составляющих относительный путь к файлу (например, содержимое файла api/my_namespace/file.proto должно находиться в пакете busrpc.api.my_namespace

Файл дескриптора пространства имен

Этот файл должен содержать дескриптор пространства имен, который представляет собой предопределенную структуру с именем NamespaceDesc. На данный момент, спецификация не определяет для этой структуры никакого особого формата, однако она обязательно должна присутствовать:

// file api/chat/namespace.proto

message NamespaceDesc { }

Здесь и далее я буду ссылаться на пример busrpc проекта из репозитория. Пример представляет собой бэкенд простого IM приложения.

Файл дескриптора класса

Этот файл должен содержать дескриптор класса, который представляет собой предопределенную структуру с именем ClassDesc. Дескриптор класса может содержать вложенные структуры, рассматриваемые в следующих подразделах.

ObjectId

Структура ObjectId определяет идентификатор объекта класса. Поскольку идентификатор объекта используется в конечных точках, структура ObjectId должна быть кодируемой.

// class 'user'
// file api/chat/user/class.proto

message ClassDesc {
  message ObjectId {
    string username = 1;
  }
}

Если дескриптор класса не содержит ObjectId, то класс считается статическим.

Файл дескриптора метода

Этот файл должен содержать дескриптор метода, который представляет собой предопределенную структуру с именем MethodDesc. Дескриптор метода может содержать вложенные структуры, рассматриваемые в следующих подразделах.

Params и Retval

Предопределенные структуры Params и Retval определяют параметры метода и его возвращаемое значение. Если в дескрипторе отсутствует структура Retval, то метод является oneway. Также спецификация busrpc разрешает не определять структуру Params - в этом случае метод рассматривается как не имеющий параметров.

// method user::sign_in
// file api/chat/user/sign_in/method.proto

enum Result {
  RESULT_SUCCESS = 0;
  RESULT_INVALID_PASSWORD = 1;
}

message MethodDesc {
  message Params {
    string password = 1;
  }

  message Retval {
    Result result = 1;
  }
}

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

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

// method user::send_message
// file api/chat/user/send_message/method.proto

message MethodDesc {
  message Params {
    string receiver = 1 [(observable) = true];
    string text = 2;
  }

  message Retval { }
}

Static

Предопределенная структура Static делает метод статическим.

// method user::sign_up
// file api/chat/user/sign_up/method.proto
// user does not exist until he is signed up, so we define this method as static

enum Result {
  RESULT_SUCCESS = 0;
  RESULT_USERNAME_ALREADY_TAKEN = 1;
  RESULT_PASSWORD_TOO_WEAK = 2;
}

message MethodDesc {
  message Params {
    string username = 1;
    string password = 2;
  }

  message Retval {
    Result result = 1;
  }

  message Static { }
}

Статический класс может содержать только статические методы.

Файл дескриптора сервиса

Этот файл должен содержать дескриптор сервиса, который представляет собой предопределенную структуру с именем ServiceDesc. Дескриптор сервиса может содержать вложенные структуры, рассматриваемые в следующих подразделах.

Config

Предопределенная структура Config описывает конфигурационные параметры сервиса.

Фреймворк busrpc предоставляет кастомную строковую опцию default_value, с помощью которой можно задавать произвольное значение по умолчанию для полей любой структуры. Конечно, сама библиотека protobuf ничего не знает про семантику этой опции, поэтому ее поддержка должна осуществляться в клиентских библиотеках busrpc.

Опция default_value часто используется для полей таких предопределенных структур, как Params и Config.

message ServiceDesc {
  message Config {
    string bus_ip = 1 [(default_value) = "127.0.0.1"];
    uint32 port = 2 [(default_value) = "4222"];
  }
}

Implements и Invokes

Предопределенные структуры Implements и Invokes содержат информацию о методах, реализуемых и вызываемых сервисом. Эта информация выражается через типы полей структур, в качестве которых используются дескрипторы методов MethodDesc.

Рассматриваемым структурам по определению приходится ссылаться на типы, которые по общему правилу невидимы для них, т.к. находятся в иной области видимости. Платформа busrpc делает исключение для этих структур и не трактует это как ошибку.

// service 'account'
// file implementation/account/service.proto

message ServiceDesc {
  // ...

  message Implements {
    busrpc.api.chat.user.sign_in.MethodDesc method1 = 1;
    busrpc.api.chat.user.sign_up.MethodDesc method2 = 2;
  }

  message Invokes {
    busrpc.api.chat.user.on_signed_in.MethodDesc method1 = 1;
    busrpc.api.chat.user.on_signed_up.MethodDesc method2 = 2;
  }
}

Встроенные типы

Несколько типов данных играют особую роль в платформе busrpc и предоставляются ее разработчиком в файле busrpc.proto, который можно скачать из репозитория. Эти типы рассматриваются в следующих подразделах

Errc

Тип Errc представляет собой перечисление, которое содержит коды ошибок, используемых для исключений платформы busrpc. По умолчанию Errcопределяется следующим образом:

// file busrpc.proto

enum Errc {
  ERRC_UNEXPECTED = 0;
}

Конкретный busrpc проект может расширять этот тип своими константами.

Exception

Структура Exception представляет исключение в платформе busrpc и определяется следующим образом:

// file busrpc.proto

message Exception {
  Errc code = 1;
}

Конкретный busrpc проект может расширять этот тип, добавляя в него поля с дополнительной информацией об исключении. Например, итоговый тип может иметь такой вид:

// file busrpc.proto

message Exception {
  Errc code = 1;
  optional string description = 2;
  optional string service_name = 3;
  optional string namespace_name = 4;
  optional string class_name = 5;
  optional string method_name = 6;
}

CallMessage

Структура CallMessage определяет формат сетевого сообщения, с помощью которого передается вызов метода:

// file busrpc.proto

message CallMessage {
  optional bytes object_id = 1;
  optional bytes params = 2;
}

Поле object_id содержит сериализованный идентификатор объекта (структура ClassDesc.ObjectId), для которого вызывается метод. В случае, если вызывается статический метод, это поле не должно устанавливаться.

Поле params содержит сериализованные параметры метода (структура MethodDesc.Params). Если метод не имеет параметров, то поле не устанавливается при вызове метода.

Спецификация busrpc запрещает вносить какие-либо изменения в этот тип, так как утилиты для работы с платформой рассчитывают на определенный формат сообщений.

ResultMessage

Структура ResultMessageопределяет формат сетевого сообщения, которое передается в качестве результата метода:

// file busrpc.proto

message ResultMessage {
  oneof Result {
    bytes retval = 1;
    busrpc.Exception exception = 2;
  }
}

Поле retval содержит сериализованное возвращаемое значение метода (структура MethodDesc.Retval), в случае, если метод завершается без исключения. Иначе поле exception используется для передачи выброшенного исключения.

Спецификация busrpc запрещает вносить какие-либо изменения в этот тип, так как утилиты для работы с платформой рассчитывают на определенный формат сообщений.

Документирование

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

В платформе busrpc я использую принцип "код как документация", стараясь как можно больше информации предоставлять через код proto файлов и структуру проекта:

  • сущности API отображаются на поддиректории проекта

  • конечные точки методов можно определить прямо из дескриптора метода

  • структура бэкенда (сервисы, их конфигурация и ответственность) также содержится в директории проекта (поддиректория implementation/)

Остальная документация указывается в виде комментариев в proto файлах и дополнительных документирующих командах. Тем, кому доводилось использовать такой инструмент, как Doxygen, этот подход покажется очень знакомым.

Базовые правила

Блочным комментарием называется последовательность из одной или более строк комментариев без пропусков между ними. Какой именно формат комментария используется не играет роли. В следующим примере содержится два блочных комментария:

// block 1, line 1
// block 1, line 2
// block 1, line 3

/* block 2, line 1
   block 2, line 2 */

Блочный комментарий считается привязанным к определенной сущности в proto файле, если он размещен непосредственно перед ней. При этом первая строка блочного комментария считается кратким описанием, а блок целиком - полным описанием.

// Brief description of MyEnum.
// Additional information about MyEnum.
enum MyEnum {
  // Brief description of MYENUM_VALUE_0.
  // Additional information about MYENUM_VALUE_0.
  MYENUM_VALUE_0 = 0;

  // This comment is not bound!
  // MYENUM_VALUE_1 is not documented.

  MYENUM_VALUE_1 = 1;
}

Блочный комментарий, привязанный к какому-либо дескриптору (ClassDesc, MethodDesc и т.д.) считается относящимся к соответствующей сущности busrpc (классу, методу и т.д.).

Документирующие команды

Документирующие команды позволяют указывать информацию, которая дополнительно обладает некоторой семантикой. Документирующая команда имеет формат \name value и должна целиком размещаться в одной строке комментария. Команды могут идти вперемешку со строками, представляющими части полного описания, однако рекомендуется их размещать единой группой в каком-то фиксированном месте (например, в конце блочного комментария).

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

Примерами документирующих команд является команды \pre и \post, которые используются для описание пред- и постусловий метода, \author и \email, которые определяют автора и его адрес электронной почты, а также некоторые другие (полный список есть в спецификации busrpc).

// method user::sign_in
// file api/chat/user/sign_in/method.proto

// Sign-in user for Chat application.
// \post Method busrpc.api.chat.user.on_signed_in is called.
message MethodDesc {
  message Params {
    // Password.
    string password = 1;
  }

  message Retval {
    // Result code.
    Result result = 1;
  }
}

Инструменты

Платформа busrpc предоставляет инструмент разработчика busrpc проектов, который можно найти в этом репозитории. Инструмент реализует следующие команды:

  • imports - для получения списка файлов, напрямую или опосредованно импортируемых заданным файлом или файлами. Эта команд предназначена для упрощения компиляции proto файлов, например, с ее помощью легко получить все необходимые proto файлы, которые нужно передать компилятору protoc для генерации протокола какого-либо сервиса: busrpc imports -r PROJECT_DIR implementation/my_service/service.proto.

  • check - для проверки busrpc проекта на соответствие спецификации.

  • gendoc - для генерации документации busrpc проекта. На данном этапе в качестве формата поддерживается только JSON. В моем предыдущем проекте была команда фронтенда, которая без проблем сделала для меня инструмент, строящий из JSON документа, создаваемого командой gendoc, красивую HTML документацию.

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

  • call - для вызова произвольного метода.

  • impl для реализация произвольного метода и (опционально) отправки некоторого фиксированного ответа. Эта операция полезна тем, что при разработке сервиса, можно использовать ее в качестве мока для еще нереализованных методов, от которых зависит сервис.

  • observe - для мониторинга вызовов и результатов одного или нескольких методов.

Клиентские библиотеки

К сожалению, пока я не могу пока предоставить каких-либо клиентских библиотек, упрощающих разработку busrpc-сервисов. В своем предыдущем проекте я создавал такую на C++ и могу поделиться общими идеями, как сделать ее удобной. Если вкратце, то с современными фичами C++20 и возможностями интроспекции из библиотеки protobuf можно привести вызов метода busrpc к виду, сильно напоминающему вызов обычного метода класса. Все предложения по клиентским библиотекам можно вносить репозитории busrpc.

Ссылки

В заключение оставлю ссылки на проект:

Комментарии (34)


  1. ProMix
    05.01.2023 13:27

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

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


    1. pananton Автор
      05.01.2023 13:30

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


  1. murkin-kot
    05.01.2023 14:14
    +2

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

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

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


    1. pananton Автор
      05.01.2023 21:03

      Справедливый вопрос. У нас преимущественно использовался 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 не те, что ожидаются 


      1. murkin-kot
        05.01.2023 23:55

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

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


        1. pananton Автор
          06.01.2023 04:53

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

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

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

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

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


          1. murkin-kot
            06.01.2023 14:30

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


            1. pananton Автор
              06.01.2023 16:01

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

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

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

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


              1. murkin-kot
                06.01.2023 20:45

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

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

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

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

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

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

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


                1. pananton Автор
                  07.01.2023 12:34

                  Для начала про 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, тысячи их. Какие из этого плюсы - описано в статье, да и в комментариях много про это уже сказано. Можно ли их добиться в более традиционных архитектурах - можно, но как правило сложнее (естественно, какие-то другие вещи там будут проще, да).


                  1. murkin-kot
                    07.01.2023 15:34

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

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

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

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

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

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

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

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

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


                    1. pananton Автор
                      07.01.2023 19:09

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

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


                      1. murkin-kot
                        08.01.2023 00:32

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

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

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

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

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


                    1. pananton Автор
                      07.01.2023 19:26

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


                    1. pananton Автор
                      07.01.2023 19:48

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


                1. pananton Автор
                  07.01.2023 12:39

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


        1. pananton Автор
          06.01.2023 05:44

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


  1. AlexSteelax
    05.01.2023 15:24
    +2

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


    1. pananton Автор
      05.01.2023 20:15
      +1

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

      Итак, как работает 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.


      1. domix32
        05.01.2023 23:46

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


        1. pananton Автор
          06.01.2023 05:30

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

          Если я вас правильно понял, то вы намекаете на то что, что один вид связности я заменил другим? Я не соглашусь с вами. Связность топика и метода статична, а значит может быть где-то захардкодена и скрыта. А связи между сервисами динамичны, ведь 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); // синхронный вариант вызова


          1. domix32
            06.01.2023 15:02
            +1

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

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

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

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


            1. pananton Автор
              06.01.2023 16:45

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

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

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

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

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


      1. vlad4kr7
        06.01.2023 05:44

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

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

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

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

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


        1. pananton Автор
          06.01.2023 06:14

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

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

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

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

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

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

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

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

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

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


        1. pananton Автор
          06.01.2023 06:45

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


          1. vlad4kr7
            06.01.2023 07:06
            +2

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

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

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

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

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

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

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

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


            1. pananton Автор
              06.01.2023 15:34

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


              1. vlad4kr7
                06.01.2023 18:50
                +1

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

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

                паттерн circuit breaker:

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

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

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

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

                Hidden text

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

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

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


                1. pananton Автор
                  07.01.2023 12:56

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

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

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

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

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

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


      1. brutfooorcer
        07.01.2023 13:53

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

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


        1. pananton Автор
          07.01.2023 14:26

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

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

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


  1. ivymike
    07.01.2023 11:55

    Отличная штука, делающая bus factor = 1)))


    1. pananton Автор
      07.01.2023 12:43

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