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

Busrpc — фреймворк для разработки микросервисов

Время на прочтение23 мин
Количество просмотров12K

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

Ссылки

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

Теги:
Хабы:
Всего голосов 14: ↑13 и ↓1+12
Комментарии38

Публикации